mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-28 20:13:24 +00:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b16ec0490f | ||
|
|
eceaf1d73b | ||
|
|
34cf668a33 | ||
|
|
c22d410c9f | ||
|
|
fc6af867f2 | ||
|
|
2d96114984 | ||
|
|
fd7e19e490 | ||
|
|
f7d996f5b2 | ||
|
|
f8ab1383ab | ||
|
|
e1559aac94 | ||
|
|
2e17cce5cd | ||
|
|
8fb831f171 | ||
|
|
cb84f34515 | ||
|
|
272cce1a77 | ||
|
|
e0e3ed1580 | ||
|
|
c4c848cf37 | ||
|
|
32d25d86eb | ||
|
|
863adccac1 | ||
|
|
2d527e1d16 | ||
|
|
9a64ad6fa7 | ||
|
|
5ce472ebff | ||
|
|
76588af12f | ||
|
|
134f169eb9 | ||
|
|
7c2d731c4c | ||
|
|
c7e2a290cf | ||
|
|
5ea966892a | ||
|
|
b879db76b7 | ||
|
|
d13a628029 | ||
|
|
7c1e2595e3 | ||
|
|
a99e8a915e | ||
|
|
36f424da0b | ||
|
|
c0a65d5a23 | ||
|
|
02e31333d2 | ||
|
|
d69616119d | ||
|
|
dbc770d40b | ||
|
|
e69a8fece5 | ||
|
|
cf20be8a77 | ||
|
|
450bcb649c | ||
|
|
a00395351e | ||
|
|
3b24c760ff | ||
|
|
3801f0be18 | ||
|
|
f3104db2e4 | ||
|
|
602b1028c3 | ||
|
|
4f98e979a2 | ||
|
|
e0f46c4af7 | ||
|
|
bc8a9a2b85 | ||
|
|
c2b03f925d | ||
|
|
efa2e52054 | ||
|
|
cedf5761f8 | ||
|
|
f601bb8751 | ||
|
|
c39d2a56d2 | ||
|
|
805722bbe8 | ||
|
|
fe3e38343b | ||
|
|
616e82c497 | ||
|
|
ab7e47b91f | ||
|
|
cf1c103a46 | ||
|
|
71202a4a29 | ||
|
|
a7e0ceac4c | ||
|
|
6bce701aab | ||
|
|
f8b9e206a5 | ||
|
|
4c1ec2004b | ||
|
|
ebed74d239 | ||
|
|
c789b69b54 | ||
|
|
24c32511d8 | ||
|
|
302fb0642d | ||
|
|
06e7b6e3eb | ||
|
|
33acd55eaa | ||
|
|
dec82e89c2 | ||
|
|
f5ac5bc4ec | ||
|
|
b6c680e802 | ||
|
|
5fa57c13b4 | ||
|
|
acc81fda44 | ||
|
|
7ab5acf45f | ||
|
|
0d4ffbcc22 | ||
|
|
a9253ac2df | ||
|
|
d00b4843a7 | ||
|
|
6068de2c71 | ||
|
|
73da427c6b | ||
|
|
9b7ec0ddfe | ||
|
|
c2f5f14c44 | ||
|
|
0b7c3588d5 | ||
|
|
a51fac5736 | ||
|
|
726c3d0704 | ||
|
|
8dd564dbd0 | ||
|
|
e33c65cddc | ||
|
|
f2fbd2ead0 | ||
|
|
123a2e45eb | ||
|
|
f8d2d9c680 | ||
|
|
9dcbef9a79 | ||
|
|
0b14857a12 | ||
|
|
bd3d516f60 | ||
|
|
40d0bd8cea | ||
|
|
873946a1a6 | ||
|
|
968deeb254 | ||
|
|
959041be52 | ||
|
|
3319520179 | ||
|
|
580fcf3657 | ||
|
|
53dae7c520 | ||
|
|
6d59d709f1 | ||
|
|
4343e9070c | ||
|
|
b62373fb5f | ||
|
|
3da98f8e56 | ||
|
|
494d24952e | ||
|
|
8a6b17bd7b | ||
|
|
d2e859a74e | ||
|
|
4a78d55d22 | ||
|
|
dc252b8c4b | ||
|
|
c433205e89 | ||
|
|
d6bc5b57b1 | ||
|
|
280a286266 | ||
|
|
d5c18b5de3 | ||
|
|
7452e5d011 | ||
|
|
71674b0d52 | ||
|
|
5b4824bd5d | ||
|
|
deda16a7da | ||
|
|
0b7c067de7 | ||
|
|
0d0db8c129 | ||
|
|
9f1b7994a3 | ||
|
|
378df0ac70 | ||
|
|
0e4a132f69 | ||
|
|
631746375d | ||
|
|
7dc01dad54 | ||
|
|
8a9807d3e5 | ||
|
|
39df3c97ce | ||
|
|
46c1ccdfcc | ||
|
|
8817536038 | ||
|
|
c3bb23a6ee | ||
|
|
7e9c4c575e | ||
|
|
5a70eee91e | ||
|
|
228f6990a1 | ||
|
|
d80ed0e70e | ||
|
|
4576c75737 | ||
|
|
99dcf49fbc | ||
|
|
6fb3edbfd6 | ||
|
|
854524a03c |
271
CHANGELOG.md
271
CHANGELOG.md
@@ -2,6 +2,277 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.81.0](https://github.com/wanderer-industries/wanderer/compare/v1.80.0...v1.81.0) (2025-10-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: fix pwa icons + add screen in manifest
|
||||
|
||||
## [v1.80.0](https://github.com/wanderer-industries/wanderer/compare/v1.79.6...v1.80.0) (2025-10-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added PWA web manifest
|
||||
|
||||
## [v1.79.6](https://github.com/wanderer-industries/wanderer/compare/v1.79.5...v1.79.6) (2025-10-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed modals auto-save on Enter.
|
||||
|
||||
## [v1.79.5](https://github.com/wanderer-industries/wanderer/compare/v1.79.4...v1.79.5) (2025-10-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed system details modal auto-save on Enter.
|
||||
|
||||
## [v1.79.4](https://github.com/wanderer-industries/wanderer/compare/v1.79.3...v1.79.4) (2025-09-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed updating connection time status based on linked signature data. Fixed FR gas sites parsing.
|
||||
|
||||
## [v1.79.3](https://github.com/wanderer-industries/wanderer/compare/v1.79.2...v1.79.3) (2025-09-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed connection passages count
|
||||
|
||||
## [v1.79.2](https://github.com/wanderer-industries/wanderer/compare/v1.79.1...v1.79.2) (2025-09-26)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.79.1](https://github.com/wanderer-industries/wanderer/compare/v1.79.0...v1.79.1) (2025-09-26)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.79.0](https://github.com/wanderer-industries/wanderer/compare/v1.78.1...v1.79.0) (2025-09-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Updated connections EOL logic
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed eslint problems
|
||||
|
||||
* Map: Update lifetime design and buttons
|
||||
|
||||
* Map: Update wormhole lifetime UI and removed unnecessary code
|
||||
|
||||
## [v1.78.1](https://github.com/wanderer-industries/wanderer/compare/v1.78.0...v1.78.1) (2025-09-24)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* pr feedback
|
||||
|
||||
* removed wormhole only logic error
|
||||
|
||||
## [v1.78.0](https://github.com/wanderer-industries/wanderer/compare/v1.77.19...v1.78.0) (2025-09-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: added support for jumpgates connection type
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add support for Bridge. Made all tooltips left and right paddings.
|
||||
|
||||
## [v1.77.19](https://github.com/wanderer-industries/wanderer/compare/v1.77.18...v1.77.19) (2025-09-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed for all Large wormholes jump mass from 300 to 375. Fixed jump mass and total mass for N290, K329. Fixed static for J005663 was H296 now Y790. Added J492 wormhole. Change lifetime for E587 from 16 to 48
|
||||
|
||||
## [v1.77.18](https://github.com/wanderer-industries/wanderer/compare/v1.77.17...v1.77.18) (2025-09-13)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.77.17](https://github.com/wanderer-industries/wanderer/compare/v1.77.16...v1.77.17) (2025-09-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated ACL create/update APIs
|
||||
|
||||
## [v1.77.16](https://github.com/wanderer-industries/wanderer/compare/v1.77.15...v1.77.16) (2025-09-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed issue with ACL add members button for managers. Added WANDERER_RESTRICT_ACLS_CREATION env support.
|
||||
|
||||
## [v1.77.15](https://github.com/wanderer-industries/wanderer/compare/v1.77.14...v1.77.15) (2025-09-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix problem with unnecessary rerenders and loads routes if move/positioning widgets.
|
||||
|
||||
## [v1.77.14](https://github.com/wanderer-industries/wanderer/compare/v1.77.13...v1.77.14) (2025-09-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed issue with loading connection info
|
||||
|
||||
## [v1.77.13](https://github.com/wanderer-industries/wanderer/compare/v1.77.12...v1.77.13) (2025-09-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added an extra check for offline characters to reduce errors
|
||||
|
||||
## [v1.77.12](https://github.com/wanderer-industries/wanderer/compare/v1.77.11...v1.77.12) (2025-09-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Decreased character tracking grace period
|
||||
|
||||
## [v1.77.11](https://github.com/wanderer-industries/wanderer/compare/v1.77.10...v1.77.11) (2025-09-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed CSP errors
|
||||
|
||||
## [v1.77.10](https://github.com/wanderer-industries/wanderer/compare/v1.77.9...v1.77.10) (2025-09-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Removed invalid invite options
|
||||
|
||||
## [v1.77.9](https://github.com/wanderer-industries/wanderer/compare/v1.77.8...v1.77.9) (2025-09-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Auto select following char system on start
|
||||
|
||||
## [v1.77.8](https://github.com/wanderer-industries/wanderer/compare/v1.77.7...v1.77.8) (2025-09-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking
|
||||
|
||||
## [v1.77.7](https://github.com/wanderer-industries/wanderer/compare/v1.77.6...v1.77.7) (2025-09-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking
|
||||
|
||||
## [v1.77.6](https://github.com/wanderer-industries/wanderer/compare/v1.77.5...v1.77.6) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added grace period to reduce false-positive cases
|
||||
|
||||
## [v1.77.5](https://github.com/wanderer-industries/wanderer/compare/v1.77.4...v1.77.5) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* resolve tracking issues
|
||||
|
||||
## [v1.77.4](https://github.com/wanderer-industries/wanderer/compare/v1.77.3...v1.77.4) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* pr feedback
|
||||
|
||||
* ensure pub/sub occurs after acl api change
|
||||
|
||||
## [v1.77.3](https://github.com/wanderer-industries/wanderer/compare/v1.77.2...v1.77.3) (2025-08-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
* Fixed character tracking settings
|
||||
|
||||
## [v1.77.2](https://github.com/wanderer-industries/wanderer/compare/v1.77.1...v1.77.2) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* update system signature api to return correct system id
|
||||
|
||||
## [v1.77.1](https://github.com/wanderer-industries/wanderer/compare/v1.77.0...v1.77.1) (2025-08-28)
|
||||
|
||||
|
||||
|
||||
@@ -18,5 +18,28 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
"linebreak-style": "off",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "primereact/button",
|
||||
"importNames": ["Button"],
|
||||
"message": "Use WdButton instead Button"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/forbid-elements": [
|
||||
"error",
|
||||
{
|
||||
"forbid": [
|
||||
{
|
||||
"element": "Button",
|
||||
"message": "Use WdButton instead Button"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,8 +5,7 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import clsx from 'clsx';
|
||||
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Button } from 'primereact/button';
|
||||
import { LayoutEventBlocker, WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
const AVAILABLE_TAGS = [
|
||||
'A',
|
||||
@@ -61,7 +60,7 @@ export const useTagMenu = (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
|
||||
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
|
||||
{AVAILABLE_TAGS.map(x => (
|
||||
<Button
|
||||
<WdButton
|
||||
outlined={system?.tag !== x}
|
||||
severity="warning"
|
||||
key={x}
|
||||
@@ -71,9 +70,9 @@ export const useTagMenu = (
|
||||
onClick={() => system?.tag !== x && onSystemTag(x)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
</WdButton>
|
||||
))}
|
||||
<Button
|
||||
<WdButton
|
||||
disabled={!isSelectedTag}
|
||||
icon="pi pi-ban"
|
||||
size="small"
|
||||
@@ -81,7 +80,7 @@ export const useTagMenu = (
|
||||
outlined
|
||||
severity="help"
|
||||
onClick={() => onSystemTag()}
|
||||
></Button>
|
||||
></WdButton>
|
||||
</div>
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import clsx from 'clsx';
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
@@ -16,8 +23,6 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
import classes from './Map.module.scss';
|
||||
import { MapProvider, useMapState } from './MapProvider';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
@@ -26,14 +31,9 @@ import {
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import clsx from 'clsx';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
|
||||
}
|
||||
|
||||
.ConnectionBridge {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
|
||||
}
|
||||
|
||||
.ConnectionSave {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-save));
|
||||
}
|
||||
@@ -15,3 +19,14 @@
|
||||
.SelectedItem {
|
||||
background-color: var(--selected-item-bg);
|
||||
}
|
||||
|
||||
.FastActions {
|
||||
:global {
|
||||
.p-menuitem-content {
|
||||
background-color: initial !important;
|
||||
}
|
||||
.p-menuitem-content:hover {
|
||||
background-color: initial !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
@@ -13,14 +6,25 @@ import {
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { Edge } from 'reactflow';
|
||||
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
|
||||
export interface ContextMenuConnectionProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
onDeleteConnection(): void;
|
||||
onChangeTimeState(): void;
|
||||
onChangeTimeState(lifetime: TimeStatus): void;
|
||||
onChangeMassState(state: MassState): void;
|
||||
onChangeShipSizeStatus(state: ShipSizeStatus): void;
|
||||
onChangeType(type: ConnectionType): void;
|
||||
onToggleMassSave(isLocked: boolean): void;
|
||||
onHide(): void;
|
||||
edge?: Edge<SolarSystemConnection>;
|
||||
@@ -32,6 +36,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
onChangeTimeState,
|
||||
onChangeMassState,
|
||||
onChangeShipSizeStatus,
|
||||
onChangeType,
|
||||
onToggleMassSave,
|
||||
onHide,
|
||||
edge,
|
||||
@@ -41,88 +46,128 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const sourceInfo = getSystemStaticInfo(edge.data?.source);
|
||||
const targetInfo = getSystemStaticInfo(edge.data?.target);
|
||||
|
||||
const bothNullsec =
|
||||
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
|
||||
|
||||
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
|
||||
const isWormhole = edge.data?.type !== ConnectionType.gate;
|
||||
|
||||
if (edge.data?.type === ConnectionType.bridge) {
|
||||
return [
|
||||
{
|
||||
label: `Set as Wormhole`,
|
||||
icon: 'pi hero-arrow-uturn-left',
|
||||
command: () => onChangeType(ConnectionType.wormhole),
|
||||
},
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (edge.data?.type === ConnectionType.gate) {
|
||||
return [
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...(isWormhole
|
||||
{
|
||||
className: clsx(classes.FastActions, '!h-[54px]'),
|
||||
template: () => {
|
||||
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `EOL`,
|
||||
className: clsx({
|
||||
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
|
||||
}),
|
||||
icon: PrimeIcons.CLOCK,
|
||||
command: onChangeTimeState,
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
})),
|
||||
},
|
||||
...(bothNullsec
|
||||
? [
|
||||
{
|
||||
label: `Set as Bridge`,
|
||||
icon: 'pi hero-forward',
|
||||
command: () => onChangeType(ConnectionType.bridge),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
|
||||
}, [
|
||||
edge,
|
||||
onChangeTimeState,
|
||||
onDeleteConnection,
|
||||
onChangeType,
|
||||
onChangeShipSizeStatus,
|
||||
onToggleMassSave,
|
||||
onChangeMassState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" />
|
||||
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" className="!w-[250px]" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdLifetimeSelector, WdLifetimeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
|
||||
|
||||
export const LifetimeActionsWrapper = (props: WdLifetimeSelectorProps) => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
|
||||
<div className="text-[12px] text-stone-500 font-semibold">Life time:</div>
|
||||
|
||||
<WdLifetimeSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
setEdge(undefined);
|
||||
};
|
||||
|
||||
const onChangeTimeState = () => {
|
||||
const onChangeTimeState = (lifetime: TimeStatus) => {
|
||||
if (!edge || !edge.data) {
|
||||
return;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: edge.data.time_status === TimeStatus.default ? TimeStatus.eol : TimeStatus.default,
|
||||
value: lifetime,
|
||||
},
|
||||
});
|
||||
setEdge(undefined);
|
||||
|
||||
@@ -56,7 +56,8 @@ export const KillsCounter = ({
|
||||
className={className}
|
||||
tooltipClassName="!px-0"
|
||||
size={size}
|
||||
interactive={true}
|
||||
interactive
|
||||
smallPaddings
|
||||
>
|
||||
{children}
|
||||
</WdTooltipWrapper>
|
||||
|
||||
@@ -46,7 +46,13 @@ export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIc
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
>
|
||||
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
|
||||
<WdTooltipWrapper
|
||||
content={pilotTooltipContent}
|
||||
position={TooltipPosition.right}
|
||||
offset={0}
|
||||
interactive={true}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.hoverTarget)}>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
stroke: #80a5c5;
|
||||
stroke-width: 3px;
|
||||
|
||||
&.time1 {
|
||||
stroke: #f11ab2;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
&.time4 {
|
||||
stroke: #a654e3;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
&.TimeCrit {
|
||||
stroke: #f11ab2;
|
||||
stroke-width: 4px;
|
||||
@@ -29,6 +39,13 @@
|
||||
&.Gate {
|
||||
stroke: #9aff40;
|
||||
}
|
||||
|
||||
&.Bridge {
|
||||
stroke: #9aff40;
|
||||
|
||||
stroke-dasharray: 10 5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
}
|
||||
|
||||
.EdgePathFront {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PrimeIcons } from 'primereact/api';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
const MAP_TRANSLATES: Record<string, string> = {
|
||||
[Position.Top]: 'translate(-48%, 0%)',
|
||||
@@ -42,7 +43,9 @@ export const SHIP_SIZES_COLORS = {
|
||||
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
|
||||
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
|
||||
const isWormhole = data?.type !== ConnectionType.gate;
|
||||
const isWormhole = data?.type === ConnectionType.wormhole;
|
||||
const isGate = data?.type === ConnectionType.gate;
|
||||
const isBridge = data?.type === ConnectionType.bridge;
|
||||
|
||||
const {
|
||||
data: { isThickConnections },
|
||||
@@ -55,9 +58,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
|
||||
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
|
||||
|
||||
const method = isWormhole ? getBezierPath : getBezierPath;
|
||||
|
||||
const [edgePath, labelX, labelY] = method({
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: sx - offset.x,
|
||||
sourceY: sy - offset.y,
|
||||
sourcePosition: sourcePos,
|
||||
@@ -67,7 +68,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
});
|
||||
|
||||
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
|
||||
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
|
||||
}, [isThickConnections, sourceNode, targetNode]);
|
||||
|
||||
if (!sourceNode || !targetNode || !data) {
|
||||
return null;
|
||||
@@ -79,9 +80,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
id={`back_${id}`}
|
||||
className={clsx(classes.EdgePathBack, {
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
|
||||
[classes.time1]: isWormhole && data.time_status === TimeStatus._1h,
|
||||
[classes.time4]: isWormhole && data.time_status === TimeStatus._4h,
|
||||
[classes.Hovered]: hovered,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -95,7 +98,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
|
||||
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
|
||||
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -147,6 +151,19 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isBridge && (
|
||||
<WdTooltipWrapper
|
||||
content="Ansiblex Jump Bridge"
|
||||
position={TooltipPosition.top}
|
||||
className={clsx(
|
||||
classes.LinkLabel,
|
||||
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
|
||||
)}
|
||||
>
|
||||
B
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
|
||||
<WdTooltipWrapper
|
||||
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}
|
||||
|
||||
@@ -58,6 +58,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -716,11 +716,12 @@ export const STATUS_CLASSES: Record<number, string> = {
|
||||
[STATUSES.dangerous]: 'eve-system-status-dangerous',
|
||||
};
|
||||
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
|
||||
|
||||
export const TYPE_NAMES = {
|
||||
[ConnectionType.wormhole]: 'Wormhole',
|
||||
[ConnectionType.gate]: 'Gate',
|
||||
[ConnectionType.bridge]: 'Jumpgate',
|
||||
};
|
||||
|
||||
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];
|
||||
|
||||
@@ -15,3 +15,12 @@ export const isKnownSpace = (wormholeClassID: number) => {
|
||||
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
|
||||
return spaces.includes(wormholeClassID);
|
||||
};
|
||||
|
||||
export const isNullsecSpace = (wormholeClassID: number) => {
|
||||
switch (wormholeClassID) {
|
||||
case SOLAR_SYSTEM_CLASS_IDS.ns:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
@@ -11,6 +13,20 @@ export const useMapInit = () => {
|
||||
const ref = useRef({ rf, data, update });
|
||||
ref.current = { update, data, rf };
|
||||
|
||||
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
|
||||
|
||||
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
|
||||
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
@@ -24,7 +40,6 @@ export const useMapInit = () => {
|
||||
hubs,
|
||||
}: CommandInit) => {
|
||||
const { update } = ref.current;
|
||||
const { rf } = ref.current;
|
||||
|
||||
const updateData: Partial<MapData> = {};
|
||||
|
||||
@@ -63,11 +78,13 @@ export const useMapInit = () => {
|
||||
update(updateData);
|
||||
|
||||
if (systems) {
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
handleUpdateSystems(systems);
|
||||
// rf.setNodes(systems.map(convertSystem2Node));
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
handleUpdateConnections(connections);
|
||||
// rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -19,8 +18,11 @@ import {
|
||||
CommandUpdateSystems,
|
||||
MapHandlers,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import {
|
||||
useCenterSystem,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
useMapAddSystems,
|
||||
@@ -28,10 +30,8 @@ import {
|
||||
useMapInit,
|
||||
useMapRemoveSystems,
|
||||
useMapUpdateSystems,
|
||||
useCenterSystem,
|
||||
useSelectSystems,
|
||||
} from './api';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
|
||||
const mapInit = useMapInit();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
|
||||
const useThrottle = () => {
|
||||
const throttleSeed = useRef<number | null>(null);
|
||||
|
||||
@@ -118,6 +118,7 @@ $homeDark30: color.adjust($homeBase, $lightness: -30%);
|
||||
|
||||
--conn-time-eol: #7452c3e3;
|
||||
--conn-frigate: #325d88;
|
||||
--conn-bridge: rgba(135, 185, 93, 0.85);
|
||||
--conn-save: rgba(155, 102, 45, 0.85);
|
||||
--selected-item-bg: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { SystemViewStandalone, WdButton, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
|
||||
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import classes from './AddSystemDialog.module.scss';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
|
||||
|
||||
@@ -34,6 +33,7 @@ export const AddSystemDialog = ({
|
||||
data: { wormholesData },
|
||||
} = useMapRootState();
|
||||
|
||||
// TODO fix it
|
||||
const inputRef = useRef<any>();
|
||||
const onShow = useCallback(() => {
|
||||
inputRef.current?.focus();
|
||||
@@ -62,6 +62,7 @@ export const AddSystemDialog = ({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO fix it
|
||||
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
|
||||
const amatch = a.label.indexOf(query);
|
||||
const bmatch = b.label.indexOf(query);
|
||||
@@ -114,90 +115,93 @@ export const AddSystemDialog = ({
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-3 px-1.5">
|
||||
<div className="flex flex-col gap-2 py-3.5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<IconField>
|
||||
<AutoComplete
|
||||
ref={inputRef}
|
||||
multiple
|
||||
showEmptyMessage
|
||||
scrollHeight="300px"
|
||||
value={selectedItem}
|
||||
suggestions={filteredItems}
|
||||
completeMethod={searchItems}
|
||||
onChange={e => {
|
||||
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
|
||||
}}
|
||||
emptyMessage="Not found any system..."
|
||||
placeholder="Type here..."
|
||||
field="label"
|
||||
id="value"
|
||||
className="w-full"
|
||||
itemTemplate={(item: SearchSystemItem) => {
|
||||
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
|
||||
const sortedStatics = sortWHClasses(wormholesData, statics);
|
||||
const isWH = isWormholeSpace(system_class);
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-3 px-1.5">
|
||||
<div className="flex flex-col gap-2 py-3.5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<IconField>
|
||||
<AutoComplete
|
||||
ref={inputRef}
|
||||
multiple
|
||||
showEmptyMessage
|
||||
scrollHeight="300px"
|
||||
value={selectedItem}
|
||||
suggestions={filteredItems}
|
||||
completeMethod={searchItems}
|
||||
onChange={e => {
|
||||
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
|
||||
}}
|
||||
emptyMessage="Not found any system..."
|
||||
placeholder="Type here..."
|
||||
field="label"
|
||||
id="value"
|
||||
className="w-full"
|
||||
itemTemplate={(item: SearchSystemItem) => {
|
||||
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
|
||||
const sortedStatics = sortWHClasses(wormholesData, statics);
|
||||
const isWH = isWormholeSpace(system_class);
|
||||
|
||||
return (
|
||||
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
|
||||
<SystemViewStandalone
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
|
||||
{effect_name && isWH && (
|
||||
<WHEffectView
|
||||
effectName={effect_name}
|
||||
effectPower={effect_power}
|
||||
className={classes.SearchItemEffect}
|
||||
return (
|
||||
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
|
||||
<SystemViewStandalone
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isWH && (
|
||||
<div className="flex gap-1 grow justify-between">
|
||||
<div></div>
|
||||
<div className="flex gap-1">
|
||||
{sortedStatics.map(x => (
|
||||
<WHClassView key={x} whClassName={x} />
|
||||
))}
|
||||
{effect_name && isWH && (
|
||||
<WHEffectView
|
||||
effectName={effect_name}
|
||||
effectPower={effect_power}
|
||||
className={classes.SearchItemEffect}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isWH && (
|
||||
<div className="flex gap-1 grow justify-between">
|
||||
<div></div>
|
||||
<div className="flex gap-1">
|
||||
{sortedStatics.map(x => (
|
||||
<WHClassView key={x} whClassName={x} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
selectedItemTemplate={(item: SearchSystemItem) => (
|
||||
<SystemViewStandalone
|
||||
security={item.system_static_info.security}
|
||||
system_class={item.system_static_info.system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</IconField>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
selectedItemTemplate={(item: SearchSystemItem) => (
|
||||
<SystemViewStandalone
|
||||
security={item.system_static_info.security}
|
||||
system_class={item.system_static_info.system_class}
|
||||
solar_system_id={item.value}
|
||||
class_title={item.class_title}
|
||||
solar_system_name={item.label}
|
||||
region_name={item.region_name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</IconField>
|
||||
|
||||
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
|
||||
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<WdButton
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
outlined
|
||||
disabled={!selectedItem || selectedItem.length !== 1}
|
||||
size="small"
|
||||
label="Submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
outlined
|
||||
disabled={!selectedItem || selectedItem.length !== 1}
|
||||
size="small"
|
||||
label="Submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
SystemView,
|
||||
TimeAgo,
|
||||
TooltipPosition,
|
||||
WdButton,
|
||||
WdImgButton,
|
||||
WdImgButtonTooltip,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
@@ -13,7 +14,6 @@ import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { Button } from 'primereact/button';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
@@ -256,7 +256,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
)}
|
||||
></Toast>
|
||||
|
||||
<Button
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { WdImageSize, WdImgButton, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SystemCustomLabelDialog {
|
||||
systemId: string;
|
||||
@@ -126,7 +125,7 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<WdButton type="submit" onClick={handleSave} outlined size="small" label="Save"></WdButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
|
||||
@@ -116,14 +115,14 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (signature: SystemSignature) => {
|
||||
(signature: SystemSignature) => {
|
||||
if (!signature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { outCommand } = ref.current;
|
||||
|
||||
await outCommand({
|
||||
outCommand({
|
||||
type: OutCommand.linkSignatureToSystem,
|
||||
data: {
|
||||
...data,
|
||||
@@ -131,32 +130,9 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
},
|
||||
});
|
||||
|
||||
if (parseSignatureCustomInfo(signature.custom_info).isEOL === true) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionTimeStatus,
|
||||
data: {
|
||||
source: data.solar_system_source,
|
||||
target: data.solar_system_target,
|
||||
value: TimeStatus.eol,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const whShipSize = getWhSize(wormholes, signature.type);
|
||||
if (whShipSize !== undefined && whShipSize !== null) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionShipSizeType,
|
||||
data: {
|
||||
source: data.solar_system_source,
|
||||
target: data.solar_system_target,
|
||||
value: whShipSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
},
|
||||
[data, setVisible, wormholes],
|
||||
[data, setVisible],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { SystemView, WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { PingType } from '@/hooks/Mapper/types/ping.ts';
|
||||
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
const PING_TITLES = {
|
||||
[PingType.Rally]: 'RALLY',
|
||||
@@ -63,7 +62,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '450px' }}
|
||||
onShow={onShow}
|
||||
onHide={() => {
|
||||
@@ -92,7 +91,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
|
||||
<WdButton type="submit" onClick={handleSave} size="small" severity="danger" label="Ping!" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SystemSettingsDialog {
|
||||
systemId: string;
|
||||
@@ -114,7 +113,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
<Dialog
|
||||
header="System settings"
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '450px' }}
|
||||
onShow={onShow}
|
||||
onHide={() => {
|
||||
@@ -226,7 +225,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<WdButton onClick={handleSave} outlined size="small" label="Save" type="submit" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import {
|
||||
RoutesType,
|
||||
useRouteProvider,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
|
||||
interface RoutesSettingsDialog {
|
||||
visible: boolean;
|
||||
@@ -83,7 +81,7 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Apply"></Button>
|
||||
<WdButton onClick={handleSave} outlined size="small" label="Apply"></WdButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
@@ -64,12 +65,8 @@ export const useLoadRoutes = ({
|
||||
systems?.length,
|
||||
connections,
|
||||
hubs,
|
||||
routesSettings,
|
||||
...Object.keys(routesSettings)
|
||||
.sort()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
.map(x => routesSettings[x]),
|
||||
// we need make it flat recursively
|
||||
...flattenValues(routesSettings),
|
||||
...deps,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
SIGNATURE_SETTINGS,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface SystemSignatureSettingsDialogProps {
|
||||
settings: SignatureSettingsType;
|
||||
@@ -92,7 +92,7 @@ export const SystemSignatureSettingsDialog = ({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<WdButton onClick={handleSave} outlined size="small" label="Save" />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useState } 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 { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface StructuresEditDialogProps {
|
||||
visible: boolean;
|
||||
@@ -54,14 +54,13 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
// 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()),
|
||||
);
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO fix it
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
@@ -96,9 +95,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// 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,
|
||||
);
|
||||
setEditData(prev => (prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null));
|
||||
};
|
||||
|
||||
const handleStatusChange = (val: string) => {
|
||||
@@ -125,6 +122,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// fetch corporation ticker if we have an ownerId
|
||||
if (editData.ownerId) {
|
||||
try {
|
||||
// TODO fix it
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: editData.ownerId },
|
||||
@@ -157,11 +155,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
<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 ?? ''}
|
||||
/>
|
||||
<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>
|
||||
@@ -204,10 +198,12 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
{statusesRequiringTimer.includes(editData.status) && (
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Timer <br /> (Eve Time):</span>
|
||||
<span>
|
||||
Timer <br /> (Eve Time):
|
||||
</span>
|
||||
<Calendar
|
||||
value={editData.endTime ? new Date(editData.endTime) : undefined}
|
||||
onChange={(e) => handleChange('endTime', e.value ?? '')}
|
||||
onChange={e => handleChange('endTime', e.value ?? '')}
|
||||
showTime
|
||||
hourFormat="24"
|
||||
dateFormat="yy-mm-dd"
|
||||
@@ -227,8 +223,8 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
</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} />
|
||||
<WdButton label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
|
||||
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,21 @@ const SystemKillsContent = () => {
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
const whCacheRef = useMemo(() => new Map<number, boolean>(), []);
|
||||
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
const isWormholeSystem = useCallback(
|
||||
(systemId: number): boolean => {
|
||||
const cached = whCacheRef.get(systemId);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const info = getSystemStaticInfo(systemId);
|
||||
const isWH = info?.system_class != null ? isWormholeSpace(Number(info.system_class)) : false;
|
||||
|
||||
whCacheRef.set(systemId, isWH);
|
||||
return isWH;
|
||||
},
|
||||
[whCacheRef],
|
||||
);
|
||||
|
||||
const { kills, isLoading, error } = useSystemKills({
|
||||
systemId,
|
||||
@@ -30,15 +43,9 @@ const SystemKillsContent = () => {
|
||||
const showLoading = isLoading && kills.length === 0;
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
|
||||
return kills.filter(kill => {
|
||||
if (!systemStaticInfo) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
return false;
|
||||
}
|
||||
return isWormholeSpace(systemStaticInfo.system_class);
|
||||
});
|
||||
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
|
||||
if (!settingsKills.whOnly) return kills;
|
||||
return kills.filter(kill => isWormholeSystem(Number(kill.solar_system_id)));
|
||||
}, [kills, settingsKills.whOnly, isWormholeSystem]);
|
||||
|
||||
if (!isSubscriptionActive) {
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemView, TooltipPosition, WdButton, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import {
|
||||
AddSystemDialog,
|
||||
SearchOnSubmitCallback,
|
||||
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
|
||||
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
interface KillsSettingsDialogProps {
|
||||
@@ -158,7 +156,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button onClick={handleApply} label="Apply" outlined size="small" />
|
||||
<WdButton onClick={handleApply} label="Apply" outlined size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import classes from './Connections.module.scss';
|
||||
import { Sidebar } from 'primereact/sidebar';
|
||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
ConnectionType,
|
||||
ConnectionOutput,
|
||||
ConnectionInfoOutput,
|
||||
ConnectionOutput,
|
||||
ConnectionType,
|
||||
OutCommand,
|
||||
Passage,
|
||||
SolarSystemConnection,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { Sidebar } from 'primereact/sidebar';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import classes from './Connections.module.scss';
|
||||
|
||||
import { PassageCard } from './PassageCard';
|
||||
import { InfoDrawer, SystemView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { InfoDrawer, SystemView, TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PassageCard } from './PassageCard';
|
||||
|
||||
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
|
||||
|
||||
@@ -78,7 +77,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
}, [connections, selectedConnection]);
|
||||
|
||||
const isWormhole = useMemo(() => {
|
||||
return cnInfo?.type !== ConnectionType.gate;
|
||||
return cnInfo?.type === ConnectionType.wormhole;
|
||||
}, [cnInfo]);
|
||||
|
||||
const [passages, setPassages] = useState<Passage[]>([]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Button } from 'primereact/button';
|
||||
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
@@ -10,6 +9,7 @@ import { MapUserSettings, RemoteAdminSettingsResponse } from '@/hooks/Mapper/map
|
||||
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
|
||||
import fastDeepEqual from 'fast-deep-equal';
|
||||
import { useDetectSettingsChanged } from '@/hooks/Mapper/components/hooks';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const AdminSettings = () => {
|
||||
const {
|
||||
@@ -92,7 +92,7 @@ export const AdminSettings = () => {
|
||||
<div className="w-full h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<Button
|
||||
<WdButton
|
||||
// @ts-ignore
|
||||
ref={cfRef}
|
||||
onClick={cfShow}
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
|
||||
import { SettingsListItem } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipPosition, WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
|
||||
@@ -42,7 +41,7 @@ export const CommonSettings = () => {
|
||||
<div className="grid grid-cols-[1fr_auto]">
|
||||
<div />
|
||||
<WdTooltipWrapper content="This dangerous action. And can not be undone" position={TooltipPosition.top}>
|
||||
<Button
|
||||
<WdButton
|
||||
// @ts-ignore
|
||||
ref={cfRef}
|
||||
className="py-[4px]"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
type SaveDefaultSettingsReturn = { success: boolean; error: string };
|
||||
|
||||
@@ -65,7 +65,7 @@ export const DefaultSettings = () => {
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<Button
|
||||
<WdButton
|
||||
onClick={handleSaveAsDefault}
|
||||
icon="pi pi-save"
|
||||
size="small"
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
|
||||
import { Button } from 'primereact/button';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
|
||||
import { callToastSuccess } from '@/hooks/Mapper/helpers';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
import { RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const ServerSettings = () => {
|
||||
const {
|
||||
@@ -64,7 +64,7 @@ export const ServerSettings = () => {
|
||||
<div className="w-full h-full flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div>
|
||||
<Button
|
||||
<WdButton
|
||||
// @ts-ignore
|
||||
ref={cfRef}
|
||||
onClick={cfShow}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/compon
|
||||
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Button } from 'primereact/button';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export interface WidgetsSettingsProps {}
|
||||
|
||||
@@ -33,7 +32,7 @@ export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
|
||||
|
||||
<div className="grid grid-cols-[1fr_auto]">
|
||||
<div />
|
||||
<Button className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets"></Button>
|
||||
<WdButton className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { saveTextFile } from '@/hooks/Mapper/utils';
|
||||
import { Button } from 'primereact/button';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
|
||||
return {
|
||||
@@ -139,7 +139,7 @@ export const OldSettingsDialog = () => {
|
||||
className="w-[640px] h-[400px] text-text-color min-h-0"
|
||||
footer={
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
<WdButton
|
||||
// @ts-ignore
|
||||
ref={cfRef}
|
||||
onClick={cfShow}
|
||||
@@ -168,7 +168,7 @@ export const OldSettingsDialog = () => {
|
||||
<div className="h-[30px]"></div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
<WdButton
|
||||
onClick={handleExportClipboard}
|
||||
icon="pi pi-copy"
|
||||
size="small"
|
||||
@@ -176,7 +176,7 @@ export const OldSettingsDialog = () => {
|
||||
label="Export to Clipboard"
|
||||
/>
|
||||
|
||||
<Button
|
||||
<WdButton
|
||||
onClick={handleExportAsFile}
|
||||
icon="pi pi-download"
|
||||
size="small"
|
||||
|
||||
@@ -8,11 +8,14 @@ import {
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
|
||||
import { Button } from 'primereact/button';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & { linked_system: string };
|
||||
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
|
||||
linked_system: string;
|
||||
k162Type: string;
|
||||
time_status: TimeStatus;
|
||||
};
|
||||
|
||||
export interface MapSettingsProps {
|
||||
systemId: string;
|
||||
@@ -22,10 +25,7 @@ export interface MapSettingsProps {
|
||||
}
|
||||
|
||||
export const SignatureSettings = ({ systemId, show, onHide, signatureData }: MapSettingsProps) => {
|
||||
const {
|
||||
outCommand,
|
||||
data: { wormholes },
|
||||
} = useMapRootState();
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const handleShow = async () => {};
|
||||
const signatureForm = useForm<Partial<SystemSignaturePrepared>>({});
|
||||
@@ -52,41 +52,13 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
solar_system_target: values.linked_system,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: need fix
|
||||
if (values.isEOL) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionTimeStatus,
|
||||
data: {
|
||||
source: systemId,
|
||||
target: values.linked_system,
|
||||
value: TimeStatus.eol,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (values.type) {
|
||||
const whShipSize = getWhSize(wormholes, values.type);
|
||||
if (whShipSize !== undefined && whShipSize !== null) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateConnectionShipSizeType,
|
||||
data: {
|
||||
source: systemId,
|
||||
target: values.linked_system,
|
||||
value: whShipSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out = {
|
||||
...out,
|
||||
custom_info: JSON.stringify({
|
||||
// TODO: need fix
|
||||
k162Type: values.k162Type,
|
||||
// TODO: need fix
|
||||
isEOL: values.isEOL,
|
||||
time_status: values.time_status,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -153,7 +125,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
signatureForm.reset();
|
||||
onHide();
|
||||
},
|
||||
[signatureData, signatureForm, outCommand, systemId, onHide, wormholes],
|
||||
[signatureData, signatureForm, outCommand, systemId, onHide],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -165,18 +137,17 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
const { linked_system, custom_info, ...rest } = signatureData;
|
||||
|
||||
let k162Type = null;
|
||||
let isEOL = false;
|
||||
let time_status = TimeStatus._24h;
|
||||
if (custom_info) {
|
||||
const customInfo = JSON.parse(custom_info);
|
||||
k162Type = customInfo.k162Type;
|
||||
isEOL = customInfo.isEOL;
|
||||
time_status = customInfo.time_status;
|
||||
}
|
||||
|
||||
signatureForm.reset({
|
||||
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
|
||||
// TODO: need fix
|
||||
k162Type: k162Type,
|
||||
isEOL: isEOL,
|
||||
time_status: time_status,
|
||||
...rest,
|
||||
});
|
||||
}, [signatureForm, signatureData]);
|
||||
@@ -185,7 +156,8 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
<Dialog
|
||||
header={`Signature Edit [${signatureData?.eve_id}]`}
|
||||
visible={show}
|
||||
draggable={false}
|
||||
draggable
|
||||
resizable={false}
|
||||
style={{ width: '390px' }}
|
||||
onShow={handleShow}
|
||||
onHide={() => {
|
||||
@@ -220,8 +192,8 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
|
||||
<div className="flex gap-2 justify-end px-[0.75rem] pb-[0.5rem]">
|
||||
<WdButton type="submit" outlined size="small" label="Save" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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)} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SignatureEOLCheckbox.tsx';
|
||||
@@ -3,7 +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';
|
||||
import { SignatureLifetimeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLifetimeSelect.tsx';
|
||||
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
@@ -29,10 +29,10 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<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>
|
||||
<div className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Lifetime:</span>
|
||||
<SignatureLifetimeSelect name="time_status" />
|
||||
</div>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Temp. Name:</span>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { WdLifetimeSelector } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
|
||||
|
||||
export interface SignatureEOLCheckboxProps {
|
||||
name: string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
export const SignatureLifetimeSelect = ({ name, defaultValue = false }: SignatureEOLCheckboxProps) => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<div className="my-1">
|
||||
<Controller
|
||||
// @ts-ignore
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => {
|
||||
// @ts-ignore
|
||||
return <WdLifetimeSelector lifetime={field.value} onChangeLifetime={e => field.onChange(e)} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './SignatureGroupSelect';
|
||||
export * from './SignatureGroupContent';
|
||||
export * from './SignatureK162TypeSelect';
|
||||
export * from './SignatureLifetimeSelect';
|
||||
|
||||
7
assets/js/hooks/Mapper/components/ui-kit/WdButton.tsx
Normal file
7
assets/js/hooks/Mapper/components/ui-kit/WdButton.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Button, ButtonProps } from 'primereact/button';
|
||||
|
||||
export const WdButton = ({ type = 'button', ...props }: ButtonProps) => {
|
||||
// eslint-disable-next-line react/forbid-elements
|
||||
return <Button {...props} type={type} />;
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
|
||||
const LIFE_TIME = [
|
||||
{
|
||||
id: TimeStatus._1h,
|
||||
label: '1H',
|
||||
className: 'bg-purple-400 hover:!bg-purple-400',
|
||||
inactiveClassName: 'bg-purple-400/30',
|
||||
description: 'Less than one 1 hours remaining',
|
||||
},
|
||||
{
|
||||
id: TimeStatus._4h,
|
||||
label: '4H',
|
||||
className: 'bg-purple-300 hover:!bg-purple-300',
|
||||
inactiveClassName: 'bg-purple-300/30',
|
||||
description: 'Less than one 4 hours remaining',
|
||||
},
|
||||
{
|
||||
id: TimeStatus._4h30m,
|
||||
label: '4.5H',
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
description: 'Less than one 4.5 hours remaining. All small holes have such lifetime.',
|
||||
},
|
||||
{
|
||||
id: TimeStatus._16h,
|
||||
label: '16H',
|
||||
className: 'bg-orange-300 hover:!bg-orange-300',
|
||||
inactiveClassName: 'bg-orange-400/30',
|
||||
description: 'Less than one 16 hours remaining',
|
||||
},
|
||||
{
|
||||
id: TimeStatus._24h,
|
||||
label: '24H',
|
||||
className: 'bg-orange-300 hover:!bg-orange-300',
|
||||
inactiveClassName: 'bg-orange-400/30',
|
||||
description: 'Less than one 24 hours remaining',
|
||||
},
|
||||
{
|
||||
id: TimeStatus._48h,
|
||||
label: '48H',
|
||||
className: 'bg-orange-300 hover:!bg-orange-300',
|
||||
inactiveClassName: 'bg-orange-400/30',
|
||||
description: 'Less than one 24 hours remaining. Related only with C6. B041, B520, U319, C391.',
|
||||
},
|
||||
];
|
||||
|
||||
export interface WdLifetimeSelectorProps {
|
||||
lifetime?: TimeStatus;
|
||||
onChangeLifetime(lifetime: TimeStatus): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdLifetimeSelector = ({
|
||||
lifetime = TimeStatus._24h,
|
||||
onChangeLifetime,
|
||||
className,
|
||||
}: WdLifetimeSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[1fr_1fr_1fr_1fr_1fr_1fr] gap-1', className)}>
|
||||
{LIFE_TIME.map(x => (
|
||||
<WdButton
|
||||
key={x.id}
|
||||
outlined={false}
|
||||
value={x.label}
|
||||
tooltip={x.description}
|
||||
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
|
||||
size="small"
|
||||
className={clsx(
|
||||
`py-[1px] justify-center min-w-auto w-auto border-0 text-[12px] font-bold leading-[20px]`,
|
||||
{ [x.inactiveClassName]: lifetime !== x.id },
|
||||
x.className,
|
||||
)}
|
||||
onClick={() => onChangeLifetime(x.id)}
|
||||
>
|
||||
{x.label}
|
||||
</WdButton>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
|
||||
content: (() => React.ReactNode) | React.ReactNode;
|
||||
targetSelector?: string;
|
||||
interactive?: boolean;
|
||||
smallPaddings?: boolean;
|
||||
}
|
||||
|
||||
export interface OffsetPosition {
|
||||
@@ -47,6 +48,7 @@ export const WdTooltip = forwardRef(
|
||||
position: tPosition = TooltipPosition.default,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
smallPaddings = false,
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
@@ -264,10 +266,14 @@ export const WdTooltip = forwardRef(
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute px-1 py-1',
|
||||
'absolute px-2 py-1',
|
||||
'border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos == null && 'invisible',
|
||||
{
|
||||
'pointer-events-auto': interactive,
|
||||
'pointer-events-none': !interactive,
|
||||
invisible: pos == null,
|
||||
'!px-1': smallPaddings,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -8,13 +8,26 @@ export type WdTooltipWrapperProps = {
|
||||
content?: (() => ReactNode) | ReactNode;
|
||||
size?: TooltipSize;
|
||||
interactive?: boolean;
|
||||
smallPaddings?: boolean;
|
||||
tooltipClassName?: string;
|
||||
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
(
|
||||
{ className, children, content, offset, position, targetSelector, interactive, size, tooltipClassName, ...props },
|
||||
{
|
||||
className,
|
||||
children,
|
||||
content,
|
||||
offset,
|
||||
position,
|
||||
targetSelector,
|
||||
interactive,
|
||||
smallPaddings,
|
||||
size,
|
||||
tooltipClassName,
|
||||
...props
|
||||
},
|
||||
forwardedRef,
|
||||
) => {
|
||||
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
|
||||
@@ -31,6 +44,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
|
||||
position={position}
|
||||
content={content}
|
||||
interactive={interactive}
|
||||
smallPaddings={smallPaddings}
|
||||
targetSelector={finalTargetSelector}
|
||||
className={clsx(size && sizeClass(size), tooltipClassName)}
|
||||
/>
|
||||
|
||||
6
assets/js/hooks/Mapper/components/ui-kit/constants.ts
Normal file
6
assets/js/hooks/Mapper/components/ui-kit/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const BUILT_IN_TOOLTIP_OPTIONS = {
|
||||
mouseTrack: true,
|
||||
mouseTrackLeft: 10,
|
||||
className:
|
||||
'rounded-[3px] bg-stone-900/90 px-1 py-1 [&_.p-tooltip-text]:!text-stone-300 text-[13px] [&_.p-tooltip-text]:!p-1',
|
||||
};
|
||||
@@ -21,3 +21,5 @@ export * from './LoadingWrapper';
|
||||
export * from './WdMenuItem';
|
||||
export * from './MenuItemWithInfo';
|
||||
export * from './MarkdownTextViewer.tsx';
|
||||
export * from './WdButton.tsx';
|
||||
export * from './constants.ts';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './useClipboard';
|
||||
export * from './useConfirmPopup';
|
||||
export * from './useEventBuffer';
|
||||
export * from './useHotkey';
|
||||
export * from './usePageVisibility';
|
||||
export * from './useSkipContextMenu';
|
||||
export * from './useThrottle';
|
||||
export * from './useConfirmPopup';
|
||||
|
||||
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useCallback, useRef } from 'react';
|
||||
export type UseEventBufferHandler<T> = (event: T) => void;
|
||||
|
||||
export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<T[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = eventsBufferRef.current.shift()!;
|
||||
handler(event);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleEvent = useCallback(event => {
|
||||
if (!eventTickRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push(event);
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
return { handleEvent };
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CommandInit } from '@/hooks/Mapper/types';
|
||||
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { CommandInit } from '@/hooks/Mapper/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useMapInit = () => {
|
||||
const { update } = useMapRootState();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -8,24 +7,25 @@ import {
|
||||
CommandCharactersUpdated,
|
||||
CommandCharacterUpdated,
|
||||
CommandCommentAdd,
|
||||
CommandCommentRemoved,
|
||||
CommandInit,
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPingAdded,
|
||||
CommandPingCancelled,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
CommandRemoveSystems,
|
||||
CommandRoutes,
|
||||
Commands,
|
||||
CommandSignaturesUpdated,
|
||||
CommandTrackingCharactersData,
|
||||
CommandUpdateConnection,
|
||||
CommandUpdateSystems,
|
||||
CommandUserSettingsUpdated,
|
||||
Commands,
|
||||
MapHandlers,
|
||||
CommandCommentRemoved,
|
||||
CommandPingAdded,
|
||||
CommandPingCancelled,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
useCommandComments,
|
||||
@@ -39,9 +39,9 @@ import {
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { DetailedKill } from '../../types/kills';
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
|
||||
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapInit = useMapInit();
|
||||
@@ -63,127 +63,123 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum ConnectionType {
|
||||
wormhole,
|
||||
gate,
|
||||
bridge,
|
||||
}
|
||||
|
||||
export enum MassState {
|
||||
@@ -10,8 +11,13 @@ export enum MassState {
|
||||
}
|
||||
|
||||
export enum TimeStatus {
|
||||
default,
|
||||
eol,
|
||||
reserved, // TODO: this reserved for not broke prev solution
|
||||
_1h,
|
||||
_4h,
|
||||
_4h30m,
|
||||
_16h,
|
||||
_24h,
|
||||
_48h,
|
||||
}
|
||||
|
||||
export enum ShipSizeStatus {
|
||||
|
||||
@@ -122,7 +122,7 @@ export enum SignatureGroupRU {
|
||||
export enum SignatureGroupFR {
|
||||
CosmicSignature = 'Signature cosmique (groupe)',
|
||||
Wormhole = 'Trou de ver',
|
||||
GasSite = 'Site de gaz',
|
||||
GasSite = 'Site de collecte de gaz',
|
||||
RelicSite = 'Site de reliques',
|
||||
DataSite = 'Site de données',
|
||||
OreSite = 'Site de minerai',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
// const inIndex = 0;
|
||||
// const prevEventTime = +new Date();
|
||||
@@ -10,10 +11,28 @@ const LAST_VERSION_KEY = 'wandererLastVersion';
|
||||
// @ts-ignore
|
||||
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
|
||||
const visible = usePageVisibility();
|
||||
|
||||
const wasHiddenOnce = useRef(false);
|
||||
const visibleRef = useRef(visible);
|
||||
visibleRef.current = visible;
|
||||
|
||||
// @ts-ignore
|
||||
const handleBufferedEvent = useCallback(({ type, body }) => {
|
||||
if (!visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleMapEvent } = useEventBuffer<any>(handleBufferedEvent);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
|
||||
// defaultValue: false,
|
||||
@@ -54,52 +73,6 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
|
||||
[hooksRef.current],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<{ type; body }[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = eventsBufferRef.current.shift()!;
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleMapEvent = useCallback(({ type, body }) => {
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const currentTime = +new Date();
|
||||
// const timeDiff = currentTime - prevEventTime;
|
||||
// prevEventTime = currentTime;
|
||||
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
|
||||
|
||||
if (!eventTickRef.current || !visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push({ type, body });
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && !wasHiddenOnce.current) {
|
||||
wasHiddenOnce.current = true;
|
||||
|
||||
132
assets/js/hooks/Mapper/utils/flattenValues.ts
Normal file
132
assets/js/hooks/Mapper/utils/flattenValues.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
const TYPE_ORDER = [
|
||||
'undefined',
|
||||
'null',
|
||||
'boolean',
|
||||
'number',
|
||||
'bigint',
|
||||
'string',
|
||||
'symbol',
|
||||
'function',
|
||||
'date',
|
||||
'regexp',
|
||||
'other',
|
||||
] as const;
|
||||
type TypeTag = (typeof TYPE_ORDER)[number];
|
||||
|
||||
const getTypeTag = (v: unknown): TypeTag => {
|
||||
if (v === undefined) return 'undefined';
|
||||
if (v === null) return 'null';
|
||||
const t = typeof v;
|
||||
if (t === 'boolean' || t === 'number' || t === 'bigint' || t === 'string' || t === 'symbol' || t === 'function')
|
||||
return t as TypeTag;
|
||||
const tag = Object.prototype.toString.call(v);
|
||||
if (tag === '[object Date]') return 'date';
|
||||
if (tag === '[object RegExp]') return 'regexp';
|
||||
return 'other';
|
||||
};
|
||||
|
||||
const cmp = (a: unknown, b: unknown): number => {
|
||||
const ta = getTypeTag(a);
|
||||
const tb = getTypeTag(b);
|
||||
if (ta !== tb) return TYPE_ORDER.indexOf(ta) - TYPE_ORDER.indexOf(tb);
|
||||
|
||||
switch (ta) {
|
||||
case 'undefined':
|
||||
case 'null':
|
||||
return 0;
|
||||
case 'boolean':
|
||||
return (a as boolean) === (b as boolean) ? 0 : a ? 1 : -1;
|
||||
case 'number': {
|
||||
const na = a as number,
|
||||
nb = b as number;
|
||||
const aIsNaN = Number.isNaN(na),
|
||||
bIsNaN = Number.isNaN(nb);
|
||||
if (aIsNaN || bIsNaN) return aIsNaN && bIsNaN ? 0 : aIsNaN ? 1 : -1; // NaN в конец чисел
|
||||
return na === nb ? 0 : na < nb ? -1 : 1;
|
||||
}
|
||||
case 'bigint': {
|
||||
const ba = a as bigint,
|
||||
bb = b as bigint;
|
||||
return ba === bb ? 0 : ba < bb ? -1 : 1;
|
||||
}
|
||||
case 'string':
|
||||
return (a as string).localeCompare(b as string);
|
||||
case 'symbol': {
|
||||
const da = (a as symbol).description ?? '';
|
||||
const db = (b as symbol).description ?? '';
|
||||
return da.localeCompare(db);
|
||||
}
|
||||
case 'function':
|
||||
// @ts-ignore
|
||||
return ((a as Function).name || '').localeCompare((b as Function).name || '');
|
||||
case 'date':
|
||||
return (a as Date).getTime() - (b as Date).getTime();
|
||||
case 'regexp':
|
||||
return a!.toString().localeCompare(b!.toString());
|
||||
default:
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
};
|
||||
|
||||
const isIterable = (v: unknown): v is Iterable<unknown> =>
|
||||
v != null && typeof (v as any)[Symbol.iterator] === 'function';
|
||||
|
||||
const pushTypedArrayValues = (v: unknown, out: unknown[]) => {
|
||||
if (ArrayBuffer.isView(v) && !(v instanceof DataView)) {
|
||||
// @ts-ignore
|
||||
out.push(...(v as ArrayLike<number> as any));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate this func with ChatGPT 5. Cause it pure func and looks like what i need
|
||||
* May be in net we can find smtng like that
|
||||
* @param input
|
||||
*/
|
||||
export const flattenValues = (input: unknown): unknown[] => {
|
||||
const out: unknown[] = [];
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
const visit = (v: unknown): void => {
|
||||
const tag = getTypeTag(v);
|
||||
if (tag !== 'other') {
|
||||
out.push(v);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v && typeof v === 'object') {
|
||||
if (seen.has(v)) return;
|
||||
seen.add(v);
|
||||
|
||||
if (pushTypedArrayValues(v, out)) return;
|
||||
|
||||
if (v instanceof Map) {
|
||||
for (const val of v.values()) visit(val);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v instanceof Set) {
|
||||
for (const val of v.values()) visit(val);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(v) || isIterable(v)) {
|
||||
for (const item of v as Iterable<unknown>) visit(item);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(v)) {
|
||||
// @ts-ignore
|
||||
visit((v as never)[key]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
out.push(v);
|
||||
};
|
||||
|
||||
visit(input);
|
||||
return out.sort(cmp);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import 'phoenix_html';
|
||||
import './live_reload.css';
|
||||
|
||||
const animateBg = function (bgCanvas) {
|
||||
const { TweenMax, _ } = window;
|
||||
const { TweenMax } = window;
|
||||
/**
|
||||
* Utility function for returning a random integer in a given range
|
||||
* @param {Int} max
|
||||
@@ -212,12 +212,6 @@ const animateBg = function (bgCanvas) {
|
||||
};
|
||||
}
|
||||
window.myJump = new JumpToHyperspace(bgCanvas);
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
_.debounce(() => {
|
||||
window.myJump.reset();
|
||||
}, 250),
|
||||
);
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
BIN
assets/static/apple-touch-icon.png
Executable file
BIN
assets/static/apple-touch-icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/static/favicon-96x96.png
Executable file
BIN
assets/static/favicon-96x96.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 977 B |
33
assets/static/site.webmanifest
Executable file
33
assets/static/site.webmanifest
Executable file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Wanderer",
|
||||
"short_name": "Wanderer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#171717",
|
||||
"background_color": "#171717",
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "web-app-manifest.webp",
|
||||
"sizes": "720x1280",
|
||||
"type": "image/webp"
|
||||
},
|
||||
{
|
||||
"src": "web-app-manifest-wide.webp",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/webp",
|
||||
"form_factor": "wide"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets/static/web-app-manifest-192x192.png
Executable file
BIN
assets/static/web-app-manifest-192x192.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/static/web-app-manifest-512x512.png
Executable file
BIN
assets/static/web-app-manifest-512x512.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
assets/static/web-app-manifest-wide.webp
Normal file
BIN
assets/static/web-app-manifest-wide.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/static/web-app-manifest.webp
Normal file
BIN
assets/static/web-app-manifest.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -121,6 +121,11 @@ restrict_maps_creation =
|
||||
|> get_var_from_path_or_env("WANDERER_RESTRICT_MAPS_CREATION", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
restrict_acls_creation =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_RESTRICT_ACLS_CREATION", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
config :wanderer_app,
|
||||
web_app_url: web_app_url,
|
||||
git_sha: System.get_env("GIT_SHA", "111"),
|
||||
@@ -150,6 +155,7 @@ config :wanderer_app,
|
||||
map_connection_eol_expire_timeout_mins: map_connection_eol_expire_timeout_mins,
|
||||
wallet_tracking_enabled: wallet_tracking_enabled,
|
||||
restrict_maps_creation: restrict_maps_creation,
|
||||
restrict_acls_creation: restrict_acls_creation,
|
||||
subscription_settings: %{
|
||||
plans: [
|
||||
%{
|
||||
|
||||
@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
accept [
|
||||
:map_id,
|
||||
:character_id,
|
||||
:tracked,
|
||||
:followed
|
||||
:tracked
|
||||
]
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
|
||||
@@ -147,8 +147,13 @@ defmodule WandererApp.Api.MapConnection do
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
# where 0 - normal
|
||||
# where 1 - end of life
|
||||
# 0 - normal (env settings)
|
||||
# 1 - EOL 1h
|
||||
# 2 - EOL 4h
|
||||
# 3 - EOL 4.5h
|
||||
# 4 - EOL 16h
|
||||
# 5 - EOL 24h
|
||||
# 6 - EOL 48h
|
||||
attribute :time_status, :integer do
|
||||
default(0)
|
||||
|
||||
@@ -168,6 +173,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
# where 0 - Wormhole
|
||||
# where 1 - Gate
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ defmodule WandererApp.Application do
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererAppWeb.PresenceGracePeriodManager,
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
|
||||
@@ -129,21 +129,23 @@ defmodule WandererApp.CachedInfo do
|
||||
|
||||
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
# Create normalized cache key (smaller ID first for bidirectional lookup)
|
||||
{id1, id2} = if from_solar_system_id < to_solar_system_id do
|
||||
{from_solar_system_id, to_solar_system_id}
|
||||
else
|
||||
{to_solar_system_id, from_solar_system_id}
|
||||
end
|
||||
|
||||
{id1, id2} =
|
||||
if from_solar_system_id < to_solar_system_id do
|
||||
{from_solar_system_id, to_solar_system_id}
|
||||
else
|
||||
{to_solar_system_id, from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
|
||||
|
||||
case WandererApp.Cache.lookup(cache_key) do
|
||||
{:ok, nil} ->
|
||||
# Build jump index if not exists
|
||||
build_jump_index()
|
||||
WandererApp.Cache.lookup(cache_key)
|
||||
|
||||
result -> result
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,17 +154,19 @@ defmodule WandererApp.CachedInfo do
|
||||
{:ok, jumps} ->
|
||||
jumps
|
||||
|> Enum.each(fn jump ->
|
||||
{id1, id2} = if jump.from_solar_system_id < jump.to_solar_system_id do
|
||||
{jump.from_solar_system_id, jump.to_solar_system_id}
|
||||
else
|
||||
{jump.to_solar_system_id, jump.from_solar_system_id}
|
||||
end
|
||||
|
||||
{id1, id2} =
|
||||
if jump.from_solar_system_id < jump.to_solar_system_id do
|
||||
{jump.from_solar_system_id, jump.to_solar_system_id}
|
||||
else
|
||||
{jump.to_solar_system_id, jump.from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
WandererApp.Cache.put(cache_key, jump)
|
||||
end)
|
||||
|
||||
_ -> :error
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -37,13 +37,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
}
|
||||
|
||||
@pause_tracking_timeout :timer.minutes(60 * 10)
|
||||
@offline_timeout :timer.minutes(10)
|
||||
@offline_timeout :timer.minutes(5)
|
||||
@online_error_timeout :timer.minutes(10)
|
||||
@ship_error_timeout :timer.minutes(10)
|
||||
@location_error_timeout :timer.minutes(10)
|
||||
@online_forbidden_ttl :timer.seconds(7)
|
||||
@offline_check_delay_ttl :timer.seconds(15)
|
||||
@online_limit_ttl :timer.seconds(7)
|
||||
@forbidden_ttl :timer.seconds(5)
|
||||
@forbidden_ttl :timer.seconds(10)
|
||||
@limit_ttl :timer.seconds(5)
|
||||
@location_limit_ttl :timer.seconds(1)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@@ -71,18 +72,19 @@ defmodule WandererApp.Character.Tracker do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
|
||||
|> case do
|
||||
nil ->
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:last_online_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
last_online_time ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), last_online_time, :millisecond)
|
||||
|
||||
if duration >= @offline_timeout do
|
||||
pause_tracking(character_id)
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_online_time")
|
||||
|
||||
:ok
|
||||
else
|
||||
@@ -186,7 +188,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_online()
|
||||
|
||||
def update_online(%{track_online: true, character_id: character_id} = character_state) do
|
||||
def update_online(
|
||||
%{track_online: true, character_id: character_id, is_online: is_online} = character_state
|
||||
) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
|
||||
when not is_nil(access_token) ->
|
||||
@@ -197,8 +201,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
# Monitor cache for potential evictions before ESI call
|
||||
|
||||
case WandererApp.Esi.get_character_online(eve_id,
|
||||
access_token: access_token,
|
||||
character_id: character_id
|
||||
@@ -211,70 +213,67 @@ defmodule WandererApp.Character.Tracker do
|
||||
"character:#{character_id}:last_online_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
else
|
||||
# Delay next online updates for offline characters
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
ttl: @offline_check_delay_ttl
|
||||
)
|
||||
end
|
||||
|
||||
if online.online == true && online.online != is_online do
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:corporation_info_forbidden")
|
||||
end
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
|
||||
|
||||
try do
|
||||
WandererApp.Character.update_character(character_id, online)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_online"
|
||||
)
|
||||
if online.online != is_online do
|
||||
try do
|
||||
WandererApp.Character.update_character(character_id, online)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_online"
|
||||
)
|
||||
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
|
||||
update = %{
|
||||
character_state
|
||||
| is_online: online.online,
|
||||
track_ship: online.online,
|
||||
track_location: online.online
|
||||
}
|
||||
try do
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
character_state
|
||||
| is_online: online.online,
|
||||
track_ship: online.online,
|
||||
track_location: online.online
|
||||
})
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character state in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_state"
|
||||
)
|
||||
|
||||
try do
|
||||
WandererApp.Character.update_character_state(character_id, update)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character state in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_state"
|
||||
)
|
||||
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_online",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character online tracking failed #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_online"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
@@ -301,28 +300,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_online",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character online tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_online",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
@@ -332,15 +309,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_online",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character online tracking failed",
|
||||
Logger.error("ESI_ERROR: Character online tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -417,21 +386,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_info",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character info tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_info"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
@@ -443,33 +397,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_info",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character info tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_info",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
@@ -479,21 +406,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_info",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
Logger.error("ESI_ERROR: Character info tracking failed",
|
||||
Logger.error("ESI_ERROR: Character info tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -540,21 +459,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_ship",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character ship tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_ship"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
@@ -573,34 +477,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_ship",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character ship tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_ship",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
@@ -610,15 +486,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_ship",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character ship tracking failed",
|
||||
Logger.error("ESI_ERROR: Character ship tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -641,14 +509,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_ship",
|
||||
error_type: "wrong_response",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character ship tracking failed - wrong response",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -711,14 +571,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_location",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character location tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -740,34 +592,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_location",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character location tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_location",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
@@ -777,15 +601,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_location",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character location tracking failed",
|
||||
Logger.error("ESI_ERROR: Character location tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -804,14 +620,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_location",
|
||||
error_type: "wrong_response",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character location tracking failed - wrong response",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -873,14 +681,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_wallet",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character wallet tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -899,34 +699,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_wallet",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character wallet tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_wallet",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:wallet_forbidden",
|
||||
true,
|
||||
@@ -936,15 +708,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_wallet",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed",
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -960,15 +724,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
error ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_wallet",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed",
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -1073,6 +829,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
when old_corporation_id != corporation_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
@@ -1112,6 +869,17 @@ defmodule WandererApp.Character.Tracker do
|
||||
state
|
||||
|> Map.merge(%{corporation_id: corporation_id})
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:corporation_info_forbidden",
|
||||
true,
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
state
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
|
||||
|
||||
@@ -13,10 +13,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
}
|
||||
|
||||
@check_start_queue_interval :timer.seconds(1)
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
@garbage_collection_interval :timer.minutes(5)
|
||||
@untrack_characters_interval :timer.minutes(5)
|
||||
@inactive_character_timeout :timer.minutes(5)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
@@ -54,6 +53,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
true
|
||||
)
|
||||
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[character_id],
|
||||
@@ -69,29 +70,25 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def stop_tracking(state, character_id) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
{:ok, %{start_time: start_time}} <-
|
||||
WandererApp.Character.get_character_state(character_id, false) do
|
||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
[],
|
||||
fn tracked_characters ->
|
||||
tracked_characters
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
@@ -118,25 +115,17 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def add_to_untrack_queue(map_id, character_id) do
|
||||
if not WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
|
||||
WandererApp.Cache.insert(
|
||||
"#{map_id}:#{character_id}:untrack_requested",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
[{map_id, character_id} | untrack_queue]
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def remove_from_untrack_queue(map_id, character_id) do
|
||||
WandererApp.Cache.delete("#{map_id}:#{character_id}:untrack_requested")
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[],
|
||||
@@ -240,50 +229,32 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
WandererApp.Cache.lookup!("character_untrack_queue", [])
|
||||
|> Task.async_stream(
|
||||
fn {map_id, character_id} ->
|
||||
untrack_timeout_reached =
|
||||
if WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
|
||||
untrack_requested =
|
||||
WandererApp.Cache.lookup!(
|
||||
"#{map_id}:#{character_id}:untrack_requested",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), untrack_requested, :millisecond)
|
||||
duration >= @untrack_character_timeout
|
||||
else
|
||||
false
|
||||
end
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
|
||||
Logger.debug(fn -> "Untrack timeout reached: #{inspect(untrack_timeout_reached)}" end)
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
|
||||
if untrack_timeout_reached do
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
{:ok, _updated} =
|
||||
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
|
||||
ship: character.ship,
|
||||
ship_name: character.ship_name,
|
||||
ship_item_id: character.ship_item_id,
|
||||
solar_system_id: character.solar_system_id,
|
||||
structure_id: character.structure_id,
|
||||
station_id: character.station_id
|
||||
})
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
{:ok, _updated} =
|
||||
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
|
||||
ship: character.ship,
|
||||
ship_name: character.ship_name,
|
||||
ship_item_id: character.ship_item_id,
|
||||
solar_system_id: character.solar_system_id,
|
||||
structure_id: character.structure_id,
|
||||
station_id: character.station_id
|
||||
})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
end
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
@update_location_interval :timer.seconds(1)
|
||||
@update_online_interval :timer.seconds(5)
|
||||
@check_offline_characters_interval :timer.minutes(2)
|
||||
@check_offline_characters_interval :timer.minutes(5)
|
||||
@check_online_errors_interval :timer.minutes(1)
|
||||
@check_ship_errors_interval :timer.minutes(1)
|
||||
@check_location_errors_interval :timer.minutes(1)
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
|
||||
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
|
||||
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
|
||||
# Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
|
||||
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
|
||||
Process.send_after(self(), :update_location, 300)
|
||||
Process.send_after(self(), :update_ship, 500)
|
||||
Process.send_after(self(), :update_info, 1500)
|
||||
@@ -176,11 +176,15 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
|
||||
character_id
|
||||
])
|
||||
end)
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_online(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
@@ -234,17 +238,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
if WandererApp.Character.can_pause_tracking?(character_id) do
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_offline,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
WandererApp.Character.Tracker.check_offline(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
@@ -397,11 +391,15 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
|
||||
character_id
|
||||
])
|
||||
end)
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_location(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
@@ -434,11 +432,15 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
|
||||
character_id
|
||||
])
|
||||
end)
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_ship(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
@@ -473,9 +475,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
|
||||
character_id
|
||||
])
|
||||
WandererApp.Character.Tracker.update_info(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
@@ -519,9 +519,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
|
||||
character_id
|
||||
])
|
||||
WandererApp.Character.Tracker.update_wallet(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
with {:ok, character} <-
|
||||
WandererApp.Character.get_by_eve_id(character_eve_id),
|
||||
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
|
||||
{:ok, %{tracked: is_tracked}} <-
|
||||
do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
# Determine which event to send based on tracking mode and previous state
|
||||
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
Builds tracking data for all characters with access to a map.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
|
||||
{:ok, character_settings} <-
|
||||
WandererApp.Character.Activity.get_map_character_settings(map_id),
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
# Map characters to tracking data
|
||||
{:ok, characters_data} =
|
||||
build_character_tracking_data(characters_with_access, character_settings)
|
||||
build_character_tracking_data(characters_with_access)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_access, characters_with_access)
|
||||
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
# Helper to build tracking data for each character
|
||||
defp build_character_tracking_data(characters, character_settings) do
|
||||
defp build_character_tracking_data(characters) do
|
||||
{:ok,
|
||||
Enum.map(characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
|
||||
%{
|
||||
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
|
||||
tracked: (setting && setting.tracked) || false
|
||||
tracked: char.tracked
|
||||
}
|
||||
end)}
|
||||
end
|
||||
|
||||
# Private implementation of update character tracking
|
||||
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||
|> case do
|
||||
# Untracking flow
|
||||
{:ok, %{tracked: true} = existing_settings} ->
|
||||
|
||||
@@ -49,6 +49,12 @@ defmodule WandererApp.Env do
|
||||
)
|
||||
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_acls_creation"
|
||||
)
|
||||
def restrict_acls_creation?(), do: get_key(:restrict_acls_creation, false)
|
||||
|
||||
def sse_enabled?() do
|
||||
Application.get_env(@app, :sse, [])
|
||||
|> Keyword.get(:enabled, false)
|
||||
|
||||
@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
|
||||
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id),
|
||||
do:
|
||||
map_id
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
|
||||
map_id
|
||||
|> map_pid()
|
||||
|> case do
|
||||
pid when is_pid(pid) ->
|
||||
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
|
||||
|
||||
_ ->
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", false)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
|
||||
do:
|
||||
|
||||
@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
|
||||
sigs
|
||||
# Add solar_system_id to each signature and remove system_id
|
||||
Enum.map(sigs, fn sig ->
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, sys.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
end)
|
||||
else
|
||||
err ->
|
||||
Logger.error("[list_signatures] error: #{inspect(err)}")
|
||||
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _solar_system_id} = params
|
||||
) do
|
||||
attrs = Map.put(params, "character_eve_id", char_id)
|
||||
%{"solar_system_id" => solar_system_id} = params
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
# Convert solar_system_id to system_id for internal use
|
||||
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("character_eve_id", char_id)
|
||||
|> Map.put("system_id", system.id)
|
||||
|> Map.delete("solar_system_id")
|
||||
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: params["solar_system_id"],
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
{:ok, attrs}
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
# Try to fetch the created signature to return with proper fields
|
||||
with {:ok, sigs} <-
|
||||
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
|
||||
sig when not is_nil(sig) <- List.first(sigs) do
|
||||
result =
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
{:ok, result}
|
||||
else
|
||||
_ ->
|
||||
# Fallback: return attrs with solar_system_id added
|
||||
attrs_result =
|
||||
attrs
|
||||
|> Map.put(:solar_system_id, solar_system_id)
|
||||
|> Map.drop(["system_id"])
|
||||
|
||||
{:ok, attrs_result}
|
||||
end
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
)
|
||||
|
||||
{:error, :system_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _invalid} = _params
|
||||
),
|
||||
do: {:error, :missing_params}
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
|
||||
{:ok, attrs}
|
||||
# Fetch the updated signature to return with proper fields
|
||||
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
|
||||
result =
|
||||
updated_sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
_ -> {:ok, attrs}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
|
||||
@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
map_update = %{acls: map.acls, scope: map.scope}
|
||||
|
||||
WandererApp.Map.update_map(map_id, map_update)
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
broadcast_acl_updates({:ok, result}, map_id)
|
||||
|
||||
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_updated(map_id, acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:ok =
|
||||
acl_id
|
||||
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_deleted(map_id, _acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
character_ids =
|
||||
map_id
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def update_tracked_characters(map_id) do
|
||||
Task.start_link(fn ->
|
||||
{:ok, map_tracked_character_ids} =
|
||||
{:ok, all_map_tracked_character_ids} =
|
||||
map_id
|
||||
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|
||||
|> case do
|
||||
@@ -67,30 +67,19 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
map_active_tracked_characters =
|
||||
map_tracked_character_ids
|
||||
|> Enum.filter(fn character -> character in tracked_characters end)
|
||||
|
||||
{:ok, old_map_tracked_characters} =
|
||||
{:ok, actual_map_tracked_characters} =
|
||||
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
|
||||
|
||||
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
|
||||
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
|
||||
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
|
||||
characters_to_remove,
|
||||
fn ids ->
|
||||
(ids ++ characters_to_remove) |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
|
||||
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
@@ -98,7 +87,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
is_character_map_active?(map_id, character_id)
|
||||
character_map_active = is_character_map_active?(map_id, character_id)
|
||||
|
||||
character_map_active
|
||||
|> untrack_character(map_id, character_id)
|
||||
end)
|
||||
end
|
||||
@@ -126,15 +117,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def cleanup_characters(map_id, owner_id) do
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
WandererApp.Cache.get_and_remove(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
acls =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:acls, [])
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
invalidate_character_ids
|
||||
|> Task.async_stream(
|
||||
@@ -186,11 +180,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:error, reason} ->
|
||||
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
end
|
||||
|
||||
defp remove_and_untrack_characters(map_id, character_ids) do
|
||||
@@ -224,86 +213,100 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
|
||||
def update_characters(%{map_id: map_id} = state) do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
try do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|
||||
|> Enum.filter(fn character_id -> character_id in presence_character_ids end)
|
||||
|> Enum.map(fn character_id ->
|
||||
Task.start_link(fn ->
|
||||
character_updates =
|
||||
maybe_update_online(map_id, character_id) ++
|
||||
maybe_update_tracking_status(map_id, character_id) ++
|
||||
maybe_update_location(map_id, character_id) ++
|
||||
maybe_update_ship(map_id, character_id) ++
|
||||
maybe_update_alliance(map_id, character_id) ++
|
||||
maybe_update_corporation(map_id, character_id)
|
||||
presence_character_ids
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
character_updates =
|
||||
maybe_update_online(map_id, character_id) ++
|
||||
maybe_update_tracking_status(map_id, character_id) ++
|
||||
maybe_update_location(map_id, character_id) ++
|
||||
maybe_update_ship(map_id, character_id) ++
|
||||
maybe_update_alliance(map_id, character_id) ++
|
||||
maybe_update_corporation(map_id, character_id)
|
||||
|
||||
character_updates
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.map(fn update ->
|
||||
update
|
||||
|> case do
|
||||
{:character_location, location_info, old_location_info} ->
|
||||
update_location(
|
||||
character_id,
|
||||
location_info,
|
||||
old_location_info,
|
||||
state
|
||||
)
|
||||
character_updates
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.map(fn update ->
|
||||
update
|
||||
|> case do
|
||||
{:character_location, location_info, old_location_info} ->
|
||||
update_location(
|
||||
character_id,
|
||||
location_info,
|
||||
old_location_info,
|
||||
state
|
||||
)
|
||||
|
||||
:broadcast
|
||||
:broadcast
|
||||
|
||||
{:character_ship, _info} ->
|
||||
:broadcast
|
||||
{:character_ship, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_online, _info} ->
|
||||
:broadcast
|
||||
{:character_online, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_tracking, _info} ->
|
||||
:broadcast
|
||||
{:character_tracking, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_alliance, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids]
|
||||
end
|
||||
)
|
||||
{:character_alliance, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
:broadcast
|
||||
:broadcast
|
||||
|
||||
{:character_corporation, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids]
|
||||
end
|
||||
)
|
||||
{:character_corporation, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
:broadcast
|
||||
:broadcast
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(fn update ->
|
||||
case update do
|
||||
:broadcast ->
|
||||
update_character(map_id, character_id)
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(fn update ->
|
||||
case update do
|
||||
:broadcast ->
|
||||
update_character(map_id, character_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
:ok
|
||||
:ok
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
{:error, reason} -> Logger.error("Error in update_characters: #{inspect(reason)}")
|
||||
end)
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Map Server] update_characters => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
defp update_character(map_id, character_id) do
|
||||
|
||||
@@ -68,10 +68,38 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
# this class of systems will guaranty that no one real class will take that place
|
||||
# @unknown 100_100
|
||||
#
|
||||
|
||||
# default (env) setting, not EOL
|
||||
@connection_time_status_default 0
|
||||
# EOL 1h
|
||||
@connection_time_status_eol 1
|
||||
# EOL 4h
|
||||
@connection_time_status_eol_4 2
|
||||
# EOL 4.5h
|
||||
@connection_time_status_eol_4_5 3
|
||||
# EOL 16h
|
||||
@connection_time_status_eol_16 4
|
||||
# EOL 24h
|
||||
@connection_time_status_eol_24 5
|
||||
# EOL 48h
|
||||
@connection_time_status_eol_48 6
|
||||
|
||||
# EOL 1h
|
||||
@connection_eol_minutes 60
|
||||
# EOL 4h
|
||||
@connection_eol_4_minutes 4 * 60
|
||||
# EOL 4.5h
|
||||
@connection_eol_4_5_minutes 4.5 * 60
|
||||
# EOL 16h
|
||||
@connection_eol_16_minutes 16 * 60
|
||||
# EOL 24h
|
||||
@connection_eol_24_minutes 24 * 60
|
||||
# EOL 48h
|
||||
@connection_eol_48_minutes 48 * 60
|
||||
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@connection_type_bridge 2
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
@@ -146,6 +174,18 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
state
|
||||
end
|
||||
|
||||
def update_connection_type(
|
||||
%{map_id: map_id} = state,
|
||||
%{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
character_id: character_id
|
||||
} = _connection_info,
|
||||
type
|
||||
) do
|
||||
state
|
||||
end
|
||||
|
||||
def get_connection_info(
|
||||
%{map_id: map_id} = _state,
|
||||
%{
|
||||
@@ -182,6 +222,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
set_start_time(map_id, connection_id, DateTime.utc_now())
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -230,35 +272,38 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
state =
|
||||
map_id
|
||||
|> WandererApp.Map.list_connections!()
|
||||
|> Enum.filter(fn %{
|
||||
id: connection_id,
|
||||
inserted_at: inserted_at,
|
||||
solar_system_source: solar_system_source_id,
|
||||
solar_system_target: solar_system_target_id,
|
||||
type: type
|
||||
} ->
|
||||
connection_start_time = get_start_time(map_id, connection_id)
|
||||
|
||||
type != @connection_type_stargate &&
|
||||
DateTime.diff(DateTime.utc_now(), connection_start_time, :hour) >=
|
||||
connection_auto_eol_hours &&
|
||||
is_connection_valid(
|
||||
:wormholes,
|
||||
solar_system_source_id,
|
||||
solar_system_target_id
|
||||
)
|
||||
end)
|
||||
|> Enum.reduce(state, fn %{
|
||||
id: connection_id,
|
||||
solar_system_source: solar_system_source_id,
|
||||
solar_system_target: solar_system_target_id
|
||||
solar_system_target: solar_system_target_id,
|
||||
time_status: time_status,
|
||||
type: type
|
||||
},
|
||||
state ->
|
||||
state
|
||||
|> update_connection_time_status(%{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
time_status: @connection_time_status_eol
|
||||
})
|
||||
if type == @connection_type_wormhole do
|
||||
connection_start_time = get_start_time(map_id, connection_id)
|
||||
new_time_status = get_new_time_status(connection_start_time, time_status)
|
||||
|
||||
if new_time_status != time_status &&
|
||||
is_connection_valid(
|
||||
:wormholes,
|
||||
solar_system_source_id,
|
||||
solar_system_target_id
|
||||
) do
|
||||
set_start_time(map_id, connection_id, DateTime.utc_now())
|
||||
|
||||
state
|
||||
|> update_connection_time_status(%{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
time_status: new_time_status
|
||||
})
|
||||
else
|
||||
state
|
||||
end
|
||||
else
|
||||
state
|
||||
end
|
||||
end)
|
||||
|
||||
state =
|
||||
@@ -268,37 +313,38 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
id: connection_id,
|
||||
solar_system_source: solar_system_source_id,
|
||||
solar_system_target: solar_system_target_id,
|
||||
time_status: time_status,
|
||||
type: type
|
||||
} ->
|
||||
connection_mark_eol_time =
|
||||
get_connection_mark_eol_time(map_id, connection_id)
|
||||
|
||||
reverse_connection =
|
||||
WandererApp.Map.get_connection(
|
||||
map_id,
|
||||
solar_system_target_id,
|
||||
solar_system_source_id
|
||||
)
|
||||
|
||||
is_connection_exist =
|
||||
is_connection_exist(
|
||||
map_id,
|
||||
solar_system_source_id,
|
||||
solar_system_target_id
|
||||
) || not is_nil(reverse_connection)
|
||||
|
||||
is_connection_valid =
|
||||
is_connection_valid(
|
||||
:wormholes,
|
||||
solar_system_source_id,
|
||||
solar_system_target_id
|
||||
)
|
||||
) ||
|
||||
not is_nil(
|
||||
WandererApp.Map.get_connection(
|
||||
map_id,
|
||||
solar_system_target_id,
|
||||
solar_system_source_id
|
||||
)
|
||||
)
|
||||
|
||||
not is_connection_exist ||
|
||||
(type != @connection_type_stargate && is_connection_valid &&
|
||||
DateTime.diff(DateTime.utc_now(), connection_mark_eol_time, :hour) >=
|
||||
(type == @connection_type_wormhole &&
|
||||
time_status == @connection_time_status_eol &&
|
||||
is_connection_valid(
|
||||
:wormholes,
|
||||
solar_system_source_id,
|
||||
solar_system_target_id
|
||||
) &&
|
||||
DateTime.diff(
|
||||
DateTime.utc_now(),
|
||||
get_connection_mark_eol_time(map_id, connection_id),
|
||||
:hour
|
||||
) >=
|
||||
connection_auto_expire_hours - connection_auto_eol_hours +
|
||||
+connection_eol_expire_timeout_hours)
|
||||
connection_eol_expire_timeout_hours)
|
||||
end)
|
||||
|> Enum.reduce(state, fn %{
|
||||
solar_system_source: solar_system_source_id,
|
||||
@@ -318,26 +364,20 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
when not is_nil(location) and not is_nil(old_location) and
|
||||
not is_nil(old_location.solar_system_id) and
|
||||
location.solar_system_id != old_location.solar_system_id do
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
if not is_manual do
|
||||
character_id
|
||||
|> WandererApp.Character.get_character!()
|
||||
|> case do
|
||||
nil ->
|
||||
:ok
|
||||
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
|
||||
|
||||
character ->
|
||||
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.Api.MapChainPassages.new(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id,
|
||||
ship_type_id: character.ship,
|
||||
ship_name: character.ship_name,
|
||||
solar_system_source_id: old_location.solar_system_id,
|
||||
solar_system_target_id: location.solar_system_id
|
||||
})
|
||||
end
|
||||
{:ok, _} =
|
||||
WandererApp.Api.MapChainPassages.new(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id,
|
||||
ship_type_id: character.ship,
|
||||
ship_name: character.ship_name,
|
||||
solar_system_source_id: old_location.solar_system_id,
|
||||
solar_system_target_id: location.solar_system_id
|
||||
})
|
||||
end
|
||||
|
||||
case WandererApp.Map.check_connection(map_id, location, old_location) do
|
||||
@@ -356,37 +396,19 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
@connection_type_wormhole
|
||||
end
|
||||
|
||||
# Check if either system is C1 before creating the connection
|
||||
{:ok, source_system_info} = get_system_static_info(old_location.solar_system_id)
|
||||
{:ok, target_system_info} = get_system_static_info(location.solar_system_id)
|
||||
|
||||
# Set ship size type based on system classes and special rules
|
||||
ship_size_type =
|
||||
get_ship_size_type(
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id,
|
||||
connection_type
|
||||
)
|
||||
|
||||
time_status =
|
||||
if connection_type == @connection_type_wormhole do
|
||||
cond do
|
||||
# C1 systems always get medium
|
||||
source_system_info.system_class == @c1 or target_system_info.system_class == @c1 ->
|
||||
@medium_ship_size
|
||||
|
||||
# C13 systems always get frigate
|
||||
source_system_info.system_class == @c13 or target_system_info.system_class == @c13 ->
|
||||
@frigate_ship_size
|
||||
|
||||
# C4 to null gets frigate (unless C4 is shattered)
|
||||
(source_system_info.system_class == @c4 and target_system_info.system_class == @ns and
|
||||
not source_system_info.is_shattered) or
|
||||
(target_system_info.system_class == @c4 and
|
||||
source_system_info.system_class == @ns and
|
||||
not target_system_info.is_shattered) ->
|
||||
@frigate_ship_size
|
||||
|
||||
true ->
|
||||
# Default to large for other wormhole connections
|
||||
@large_ship_size
|
||||
end
|
||||
@connection_time_status_eol_24
|
||||
else
|
||||
# Default to large for non-wormhole connections
|
||||
@large_ship_size
|
||||
@connection_time_status_default
|
||||
end
|
||||
|
||||
{:ok, connection} =
|
||||
@@ -395,7 +417,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_source: old_location.solar_system_id,
|
||||
solar_system_target: location.solar_system_id,
|
||||
type: connection_type,
|
||||
ship_size_type: ship_size_type
|
||||
ship_size_type: ship_size_type,
|
||||
time_status: time_status
|
||||
})
|
||||
|
||||
if connection_type == @connection_type_wormhole do
|
||||
@@ -422,8 +445,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status: connection.time_status
|
||||
})
|
||||
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
{:ok, _} =
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
|
||||
character_id: character_id,
|
||||
@@ -470,12 +491,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
def set_start_time(map_id, connection_id, start_time) do
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:start_time",
|
||||
start_time
|
||||
)
|
||||
end
|
||||
def set_start_time(map_id, connection_id, start_time),
|
||||
do:
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:conn_#{connection_id}:start_time",
|
||||
start_time
|
||||
)
|
||||
|
||||
def can_add_location(_scope, nil), do: false
|
||||
|
||||
@@ -668,4 +689,79 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp get_ship_size_type(
|
||||
source_solar_system_id,
|
||||
target_solar_system_id,
|
||||
@connection_type_wormhole
|
||||
) do
|
||||
# Check if either system is C1 before creating the connection
|
||||
{:ok, source_system_info} = get_system_static_info(source_solar_system_id)
|
||||
{:ok, target_system_info} = get_system_static_info(target_solar_system_id)
|
||||
|
||||
cond do
|
||||
# C1 systems always get medium
|
||||
source_system_info.system_class == @c1 or target_system_info.system_class == @c1 ->
|
||||
@medium_ship_size
|
||||
|
||||
# C13 systems always get frigate
|
||||
source_system_info.system_class == @c13 or target_system_info.system_class == @c13 ->
|
||||
@frigate_ship_size
|
||||
|
||||
# C4 to null gets frigate (unless C4 is shattered)
|
||||
(source_system_info.system_class == @c4 and target_system_info.system_class == @ns and
|
||||
not source_system_info.is_shattered) or
|
||||
(target_system_info.system_class == @c4 and
|
||||
source_system_info.system_class == @ns and
|
||||
not target_system_info.is_shattered) ->
|
||||
@frigate_ship_size
|
||||
|
||||
true ->
|
||||
# Default to large for other wormhole connections
|
||||
@large_ship_size
|
||||
end
|
||||
end
|
||||
|
||||
# Default to large for non-wormhole connections
|
||||
defp get_ship_size_type(_source_solar_system_id, _target_solar_system_id, _connection_type),
|
||||
do: @large_ship_size
|
||||
|
||||
defp get_new_time_status(_start_time, @connection_time_status_default),
|
||||
do: @connection_time_status_eol_24
|
||||
|
||||
defp get_new_time_status(start_time, old_time_status) do
|
||||
left_minutes =
|
||||
get_time_status_minutes(old_time_status) -
|
||||
DateTime.diff(DateTime.utc_now(), start_time, :minute)
|
||||
|
||||
cond do
|
||||
left_minutes <= @connection_eol_minutes ->
|
||||
@connection_time_status_eol
|
||||
|
||||
left_minutes <= @connection_eol_4_minutes ->
|
||||
@connection_time_status_eol_4
|
||||
|
||||
left_minutes <= @connection_eol_4_5_minutes ->
|
||||
@connection_time_status_eol_4_5
|
||||
|
||||
left_minutes <= @connection_eol_16_minutes ->
|
||||
@connection_time_status_eol_16
|
||||
|
||||
left_minutes <= @connection_eol_24_minutes ->
|
||||
@connection_time_status_eol_24
|
||||
|
||||
left_minutes <= @connection_eol_48_minutes ->
|
||||
@connection_time_status_eol_48
|
||||
|
||||
true ->
|
||||
@connection_time_status_default
|
||||
end
|
||||
end
|
||||
|
||||
defp get_time_status_minutes(@connection_time_status_eol), do: @connection_eol_minutes
|
||||
defp get_time_status_minutes(@connection_time_status_eol_4), do: @connection_eol_4_minutes
|
||||
defp get_time_status_minutes(@connection_time_status_eol_4_5), do: @connection_eol_4_5_minutes
|
||||
defp get_time_status_minutes(@connection_time_status_eol_16), do: @connection_eol_16_minutes
|
||||
defp get_time_status_minutes(@connection_time_status_eol_24), do: @connection_eol_24_minutes
|
||||
defp get_time_status_minutes(@connection_time_status_eol_48), do: @connection_eol_48_minutes
|
||||
end
|
||||
|
||||
@@ -25,14 +25,14 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
]
|
||||
|
||||
@systems_cleanup_timeout :timer.minutes(30)
|
||||
@characters_cleanup_timeout :timer.minutes(1)
|
||||
@connections_cleanup_timeout :timer.minutes(2)
|
||||
@characters_cleanup_timeout :timer.minutes(5)
|
||||
@connections_cleanup_timeout :timer.minutes(1)
|
||||
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@backup_state_timeout :timer.minutes(1)
|
||||
@update_presence_timeout :timer.seconds(5)
|
||||
@update_characters_timeout :timer.seconds(1)
|
||||
@update_tracked_characters_timeout :timer.seconds(1)
|
||||
@update_tracked_characters_timeout :timer.minutes(1)
|
||||
|
||||
def new(), do: __struct__()
|
||||
def new(args), do: __struct__(args)
|
||||
@@ -96,11 +96,17 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
)
|
||||
|
||||
Process.send_after(self(), :update_characters, @update_characters_timeout)
|
||||
Process.send_after(self(), :update_tracked_characters, 100)
|
||||
|
||||
Process.send_after(
|
||||
self(),
|
||||
:update_tracked_characters,
|
||||
@update_tracked_characters_timeout
|
||||
)
|
||||
|
||||
Process.send_after(self(), :update_presence, @update_presence_timeout)
|
||||
Process.send_after(self(), :cleanup_connections, 5_000)
|
||||
Process.send_after(self(), :cleanup_connections, @connections_cleanup_timeout)
|
||||
Process.send_after(self(), :cleanup_systems, 10_000)
|
||||
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
|
||||
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", true)
|
||||
@@ -127,6 +133,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
|
||||
|
||||
WandererApp.Cache.delete("map_#{map_id}:started")
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
|
||||
|
||||
@@ -278,7 +285,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
end
|
||||
|
||||
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
|
||||
AclsImpl.handle_acl_updated(map_id, acl_id)
|
||||
AclsImpl.handle_acl_deleted(map_id, acl_id)
|
||||
|
||||
state
|
||||
end
|
||||
@@ -580,18 +587,27 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
characters_ids =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
{:ok, old_presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:old_presence_character_ids", [])
|
||||
|
||||
new_present_character_ids =
|
||||
presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(old_presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
not_present_character_ids =
|
||||
characters_ids
|
||||
old_presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
CharactersImpl.track_characters(map_id, presence_character_ids)
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:old_presence_character_ids",
|
||||
presence_character_ids
|
||||
)
|
||||
|
||||
CharactersImpl.track_characters(map_id, new_present_character_ids)
|
||||
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
|
||||
|
||||
broadcast!(
|
||||
|
||||
@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
|
||||
end
|
||||
end
|
||||
|
||||
def load_characters(map, character_settings, user_id) do
|
||||
def load_characters(map, user_id) when not is_nil(map) do
|
||||
{:ok, user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
characters =
|
||||
map_available_characters =
|
||||
map
|
||||
|> get_map_available_characters(user_characters)
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
|
||||
map.id,
|
||||
map_available_characters |> Enum.map(& &1.id)
|
||||
)
|
||||
|
||||
characters =
|
||||
map_available_characters
|
||||
|> Enum.map(fn c ->
|
||||
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
|
||||
end)
|
||||
@@ -108,6 +117,8 @@ defmodule WandererApp.Maps do
|
||||
{:ok, %{characters: characters}}
|
||||
end
|
||||
|
||||
def load_characters(_map, _user_id), do: {:ok, %{characters: []}}
|
||||
|
||||
def map_character(
|
||||
%{
|
||||
name: name,
|
||||
@@ -176,48 +187,57 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map_characters-#{map_id}",
|
||||
opts: [ttl: :timer.seconds(2)]
|
||||
)
|
||||
defp _get_map_characters(%{id: map_id} = map) do
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}}
|
||||
map_characters =
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
map_characters ->
|
||||
{:ok, map_characters}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_available_characters(map, user_characters) do
|
||||
@@ -227,7 +247,7 @@ defmodule WandererApp.Maps do
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}} = _get_map_characters(map)
|
||||
}} = get_map_characters(map)
|
||||
|
||||
user_characters
|
||||
|> Enum.filter(fn c ->
|
||||
|
||||
@@ -8,13 +8,13 @@ defmodule WandererApp.MapChainPassagesRepo do
|
||||
to
|
||||
)
|
||||
|> case do
|
||||
{:ok, connection} ->
|
||||
{:ok, %{inserted_at: inserted_at} = _connection} when not is_nil(inserted_at) ->
|
||||
{:ok, from_passages} =
|
||||
WandererApp.Api.MapChainPassages.by_connection(%{
|
||||
map_id: map_id,
|
||||
from: from,
|
||||
to: to,
|
||||
after: connection.inserted_at
|
||||
after: inserted_at
|
||||
})
|
||||
|
||||
{:ok, to_passages} =
|
||||
@@ -22,7 +22,7 @@ defmodule WandererApp.MapChainPassagesRepo do
|
||||
map_id: map_id,
|
||||
from: to,
|
||||
to: from,
|
||||
after: connection.inserted_at
|
||||
after: inserted_at
|
||||
})
|
||||
|
||||
from_passages =
|
||||
@@ -39,7 +39,7 @@ defmodule WandererApp.MapChainPassagesRepo do
|
||||
|
||||
{:ok, passages}
|
||||
|
||||
{:error, _error} ->
|
||||
_error ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,20 +53,8 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
def get_tracked_by_map_all(map_id),
|
||||
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
|
||||
|
||||
def get_by_map(map_id, character_id) do
|
||||
case get_by_map_filtered(map_id, [character_id]) do
|
||||
{:ok, [setting | _]} ->
|
||||
{:ok, setting}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def track(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -75,6 +63,7 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def untrack(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -83,22 +72,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def track!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case track(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to track: #{inspect(error)}"
|
||||
error -> raise "Failed to track: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def untrack!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case untrack(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to untrack: #{inspect(error)}"
|
||||
error -> raise "Failed to untrack: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,22 +100,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def follow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.follow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case follow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to follow: #{inspect(error)}"
|
||||
error -> raise "Failed to follow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.unfollow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case unfollow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
error -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ defmodule WandererAppWeb do
|
||||
those modules here.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(assets fonts images icons favicon.ico robots.txt woff woff2 lottie)
|
||||
def static_paths,
|
||||
do:
|
||||
~w(assets fonts images icons favicon.ico site.webmanifest apple-touch-icon.png web-app-manifest-192x192.png web-app-manifest-512x512.png web-app-manifest.webp web-app-manifest-wide.webp robots.txt woff woff2 lottie)
|
||||
|
||||
def router do
|
||||
quote do
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
{assigns[:page_title] || "Welcome"}
|
||||
</.live_title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
|
||||
<link
|
||||
|
||||
@@ -596,10 +596,24 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
acl -> acl.id
|
||||
end)
|
||||
|
||||
updated_acls = current_acl_ids ++ [new_acl_id]
|
||||
updated_acls =
|
||||
if new_acl_id in current_acl_ids do
|
||||
current_acl_ids
|
||||
else
|
||||
current_acl_ids ++ [new_acl_id]
|
||||
end
|
||||
|
||||
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
|
||||
{:ok, updated_map} ->
|
||||
# Only broadcast if we actually added a new ACL
|
||||
unless new_acl_id in current_acl_ids do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"maps:#{loaded_map.id}",
|
||||
{:map_acl_updated, [new_acl_id], []}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, updated_map}
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -192,6 +192,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_added
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -199,6 +201,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member added event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
end
|
||||
|
||||
@@ -300,6 +305,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_updated
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -307,6 +314,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member updated event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
end
|
||||
|
||||
@@ -385,6 +395,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_removed
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -392,6 +404,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member removed event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
end
|
||||
|
||||
@@ -417,6 +432,14 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# Private Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -26,23 +26,74 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel carousel-center bg-neutral rounded-box max-w-[80%] space-x-4 p-4">
|
||||
<%= for post <- @posts do %>
|
||||
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
|
||||
<div class="artboard-horizontal phone-1 relative hover:text-white mt-10">
|
||||
<img
|
||||
class="rounded-lg shadow-lg block !w-[400px] !h-[200px] opacity-75"
|
||||
src={post.cover_image_uri}
|
||||
/>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
<div
|
||||
id="posts-container"
|
||||
class="bg-neutral rounded-box max-w-[90%] p-4 max-h-[60vh] overflow-y-auto"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<%= for post <- @posts do %>
|
||||
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
|
||||
<div class="artboard-horizontal phone-1 relative hover:text-white">
|
||||
<img
|
||||
class="rounded-lg shadow-lg block !w-[300px] !h-[180px] opacity-75"
|
||||
src={post.cover_image_uri}
|
||||
/>
|
||||
<div class="absolute rounded-lg top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
</div>
|
||||
<h3 class="absolute bottom-4 left-14 !text-md font-bold break-normal pt-6 pb-2 ccp-font text-white">
|
||||
{post.title}
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="absolute bottom-4 left-14 font-bold break-normal pt-6 pb-2 ccp-font text-white">
|
||||
{post.title}
|
||||
</h3>
|
||||
</div>
|
||||
</.link>
|
||||
<% end %>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const postsContainer = document.getElementById('posts-container');
|
||||
if (!postsContainer) return;
|
||||
|
||||
let scrollSpeed = 0.5; // pixels per frame
|
||||
let isScrolling = true;
|
||||
let scrollDirection = 1; // 1 for down, -1 for up
|
||||
|
||||
function autoScroll() {
|
||||
if (!isScrolling) return;
|
||||
|
||||
const maxScroll = postsContainer.scrollHeight - postsContainer.clientHeight;
|
||||
|
||||
if (maxScroll <= 0) return; // No need to scroll if content fits
|
||||
|
||||
postsContainer.scrollTop += scrollSpeed * scrollDirection;
|
||||
|
||||
// Reverse direction when reaching top or bottom
|
||||
if (postsContainer.scrollTop >= maxScroll) {
|
||||
scrollDirection = -1;
|
||||
} else if (postsContainer.scrollTop <= 0) {
|
||||
scrollDirection = 1;
|
||||
}
|
||||
|
||||
requestAnimationFrame(autoScroll);
|
||||
}
|
||||
|
||||
// Pause scrolling on hover
|
||||
postsContainer.addEventListener('mouseenter', () => {
|
||||
isScrolling = false;
|
||||
});
|
||||
|
||||
// Resume scrolling when mouse leaves
|
||||
postsContainer.addEventListener('mouseleave', () => {
|
||||
isScrolling = true;
|
||||
requestAnimationFrame(autoScroll);
|
||||
});
|
||||
|
||||
// Start autoscroll after a delay
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(autoScroll);
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
<%!-- <div class="carousel carousel-center !bg-neutral rounded-box max-w-4xl space-x-6 p-4">
|
||||
|
||||
</div> --%>
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
solar_system_id: %OpenApiSpex.Schema{type: :integer},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
character_eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
@@ -31,13 +31,13 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
},
|
||||
required: [
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:eve_id,
|
||||
:character_eve_id
|
||||
],
|
||||
example: %{
|
||||
id: "sig-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
eve_id: "ABC-123",
|
||||
character_eve_id: "123456789",
|
||||
name: "Wormhole K162",
|
||||
@@ -122,7 +122,15 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
{:ok, signature} ->
|
||||
case WandererApp.Api.MapSystem.by_id(signature.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: signature})
|
||||
# Add solar_system_id and remove system_id
|
||||
# Convert to a plain map to avoid encoder issues
|
||||
signature_data =
|
||||
signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
json(conn, %{data: signature_data})
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
|
||||
@@ -13,6 +13,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.User
|
||||
alias WandererApp.SecurityAudit
|
||||
alias WandererApp.Audit.RequestContext
|
||||
@@ -140,43 +141,60 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
defp authenticate_bearer_token(conn) do
|
||||
case get_req_header(conn, "authorization") do
|
||||
["Bearer " <> token] ->
|
||||
validate_api_token(token)
|
||||
validate_api_token(conn, token)
|
||||
|
||||
_ ->
|
||||
{:error, "Missing or invalid authorization header"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_api_token(token) do
|
||||
# Look up the map by its public API key
|
||||
case find_map_by_api_key(token) do
|
||||
{:ok, map} when not is_nil(map) ->
|
||||
# Get the actual owner of the map
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
# Return the map owner as the authenticated user
|
||||
{:ok, user, map}
|
||||
defp validate_api_token(conn, token) do
|
||||
# Check for map identifier in path params
|
||||
# According to PR feedback, routes supply params["map_identifier"]
|
||||
case conn.params["map_identifier"] do
|
||||
nil ->
|
||||
# No map identifier in path - this might be a general API endpoint
|
||||
# For now, we'll return an error since we need to validate against a specific map
|
||||
{:error, "Authentication failed", :no_map_context}
|
||||
|
||||
identifier ->
|
||||
# Resolve the identifier (could be UUID or slug)
|
||||
case resolve_map_identifier(identifier) do
|
||||
{:ok, map} ->
|
||||
# Validate the token matches this specific map's API key
|
||||
if is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token) do
|
||||
# Get the map owner
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
{:ok, user, map}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
end
|
||||
else
|
||||
{:error, "Authentication failed", :invalid_token_for_map}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
{:error, "Authentication failed", :map_not_found}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_map_by_api_key(api_key) do
|
||||
# Import necessary modules
|
||||
import Ash.Query
|
||||
# Helper to resolve map by ID or slug
|
||||
defp resolve_map_identifier(identifier) do
|
||||
alias WandererApp.Api.Map
|
||||
|
||||
# Query for map with matching public API key
|
||||
Map
|
||||
|> filter(public_api_key == ^api_key)
|
||||
|> Ash.read_one()
|
||||
# Try as UUID first
|
||||
case Map.by_id(identifier) do
|
||||
{:ok, map} ->
|
||||
{:ok, map}
|
||||
|
||||
_ ->
|
||||
# Try as slug
|
||||
Map.get_map_by_slug(identifier)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_user_role(user) do
|
||||
|
||||
@@ -38,8 +38,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
:ok = track_characters(characters)
|
||||
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(
|
||||
selected_acl: nil,
|
||||
selected_acl_id: "",
|
||||
allow_acl_creation: not WandererApp.Env.restrict_acls_creation?(),
|
||||
user_id: user_id,
|
||||
access_lists: access_lists |> Enum.map(fn acl -> map_ui_acl(acl, nil) end),
|
||||
characters: characters,
|
||||
@@ -34,6 +35,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(
|
||||
selected_acl: nil,
|
||||
selected_acl_id: "",
|
||||
allow_acl_creation: false,
|
||||
access_lists: [],
|
||||
characters: [],
|
||||
members: []
|
||||
@@ -188,7 +190,11 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("create", %{"form" => form}, socket) do
|
||||
def handle_event(
|
||||
"create",
|
||||
%{"form" => form},
|
||||
%{assigns: %{allow_acl_creation: true}} = socket
|
||||
) do
|
||||
case WandererApp.Api.AccessList.new(form) do
|
||||
{:ok, _acl} ->
|
||||
{:ok, access_lists} = WandererApp.Acls.get_available_acls(socket.assigns.current_user)
|
||||
@@ -408,9 +414,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
current_user_has_role?(socket.assigns.current_user, access_list, :admin) ->
|
||||
true
|
||||
|
||||
not is_nil(eve_character_id) and
|
||||
(characters_has_role?([eve_character_id], access_list, :admin) or
|
||||
characters_has_role?([eve_character_id], access_list, :manager)) and
|
||||
not is_nil(eve_character_id) &&
|
||||
characters_has_roles?([eve_character_id], access_list, [:admin, :manager]) &&
|
||||
not current_user_has_role?(socket.assigns.current_user, access_list, :admin) ->
|
||||
false
|
||||
|
||||
@@ -470,12 +475,12 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> put_flash(:info, "Only Characters can have Admin or Manager roles")
|
||||
|> push_navigate(to: ~p"/access-lists/#{socket.assigns.selected_acl_id}")
|
||||
|
||||
defp characters_has_role?(character_eve_ids, access_list, role_atom) do
|
||||
access_list.members
|
||||
|> Enum.any?(fn member ->
|
||||
member.eve_character_id in character_eve_ids and member.role == role_atom
|
||||
end)
|
||||
end
|
||||
defp characters_has_roles?(character_eve_ids, %{members: members} = _access_list, role_atoms),
|
||||
do:
|
||||
members
|
||||
|> Enum.any?(fn %{eve_character_id: eve_character_id, role: role} = _member ->
|
||||
eve_character_id in character_eve_ids and role in role_atoms
|
||||
end)
|
||||
|
||||
defp current_user_is_owner?(current_user, access_list) do
|
||||
character_ids = current_user.characters |> Enum.map(& &1.id)
|
||||
@@ -486,18 +491,16 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
defp current_user_has_role?(current_user, access_list, role_atom) do
|
||||
character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
|
||||
characters_has_role?(character_eve_ids, access_list, role_atom)
|
||||
characters_has_roles?(character_eve_ids, access_list, [role_atom])
|
||||
end
|
||||
|
||||
defp can_add_members?(nil, _current_user), do: false
|
||||
|
||||
defp can_add_members?(access_list, current_user) do
|
||||
character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
user_character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
|
||||
member = access_list.members |> Enum.find(&(&1.eve_character_id in character_eve_ids))
|
||||
|
||||
current_user_is_owner?(current_user, access_list) or
|
||||
(not is_nil(member) and member.role in [:admin, :manager])
|
||||
current_user_is_owner?(current_user, access_list) ||
|
||||
characters_has_roles?(user_character_eve_ids, access_list, [:admin, :manager])
|
||||
end
|
||||
|
||||
defp can_delete_member?(
|
||||
@@ -512,9 +515,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
current_user_has_role?(current_user, access_list, :admin) ->
|
||||
true
|
||||
|
||||
not is_nil(eve_character_id) and
|
||||
(characters_has_role?([eve_character_id], access_list, :admin) or
|
||||
characters_has_role?([eve_character_id], access_list, :manager)) and
|
||||
not is_nil(eve_character_id) &&
|
||||
characters_has_roles?([eve_character_id], access_list, [:admin, :manager]) &&
|
||||
not current_user_has_role?(current_user, access_list, :admin) ->
|
||||
false
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
</button>
|
||||
</:action>
|
||||
</.table>
|
||||
<.link class="btn mt-2 w-full btn-neutral rounded-none" patch={~p"/access-lists/new"}>
|
||||
<.link
|
||||
:if={@allow_acl_creation}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/new"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">New Access List</h3>
|
||||
</.link>
|
||||
@@ -195,78 +199,13 @@
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:add_members]}
|
||||
:if={@live_action in [:add_members] && can_add_members?(@access_list, @current_user)}
|
||||
title="Add Member"
|
||||
class="!w-[500px]"
|
||||
id="add_member"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/access-lists/#{@selected_acl_id}")}
|
||||
>
|
||||
<%!-- <div class="mt-4 mb-2 p-tabmenu p-component " data-pc-section="tabmenu">
|
||||
<ul
|
||||
class="p-tabmenu-nav border-none h-[25px] w-full flex"
|
||||
role="menubar"
|
||||
data-pc-section="menu"
|
||||
>
|
||||
<li
|
||||
id="pr_id_17_0"
|
||||
class="p-tabmenuitem p-highlight"
|
||||
role="presentation"
|
||||
data-p-highlight="true"
|
||||
data-p-disabled="false"
|
||||
data-pc-section="menuitem"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
role="menuitem"
|
||||
aria-label="Router Link"
|
||||
tabindex="0"
|
||||
class="p-menuitem-link"
|
||||
data-pc-section="action"
|
||||
>
|
||||
<span class="p-menuitem-text" data-pc-section="label">Character</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
id="pr_id_17_1"
|
||||
class="p-tabmenuitem"
|
||||
role="presentation"
|
||||
data-p-highlight="false"
|
||||
data-p-disabled="false"
|
||||
data-pc-section="menuitem"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
role="menuitem"
|
||||
aria-label="Programmatic"
|
||||
tabindex="-1"
|
||||
class="p-menuitem-link"
|
||||
data-pc-section="action"
|
||||
>
|
||||
<span class="p-menuitem-text" data-pc-section="label">Corporation</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
id="pr_id_17_2"
|
||||
class="p-tabmenuitem"
|
||||
role="presentation"
|
||||
data-p-highlight="false"
|
||||
data-p-disabled="false"
|
||||
data-pc-section="menuitem"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
role="menuitem"
|
||||
aria-label="External"
|
||||
tabindex="-1"
|
||||
class="p-menuitem-link"
|
||||
data-pc-section="action"
|
||||
>
|
||||
<span class="p-menuitem-text" data-pc-section="label">Alliance</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div> --%>
|
||||
<.form :let={f} for={@member_form} phx-submit={@live_action}>
|
||||
<.live_select
|
||||
field={f[:member_id]}
|
||||
|
||||
@@ -318,11 +318,18 @@ defmodule WandererAppWeb.AdminLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :add_invite_link, _params, uri) do
|
||||
invite_types =
|
||||
if socket.assigns.map_subscriptions_enabled? do
|
||||
[%{label: "User", id: :user}, %{label: "Admin", id: :admin}]
|
||||
else
|
||||
[%{label: "User", id: :user}]
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :admin)
|
||||
|> assign(:uri, URI.parse(uri))
|
||||
|> assign(:page_title, "Add Invite Link")
|
||||
|> assign(:invite_types, [%{label: "User", id: :user}, %{label: "Admin", id: :admin}])
|
||||
|> assign(:invite_types, invite_types)
|
||||
|> assign(:valid_types, [
|
||||
%{label: "1D", id: 1},
|
||||
%{label: "1W", id: 7},
|
||||
|
||||
@@ -75,13 +75,12 @@ defmodule WandererAppWeb.CharactersLive do
|
||||
def handle_event("delete", %{"character_id" => character_id}, socket) do
|
||||
WandererApp.Character.TrackerManager.stop_tracking(character_id)
|
||||
|
||||
{:ok, map_user_settings} =
|
||||
{:ok, map_character_settings} =
|
||||
WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
|
||||
|
||||
map_user_settings
|
||||
map_character_settings
|
||||
|> Enum.each(fn settings ->
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.untrack()
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
|
||||
end)
|
||||
|
||||
{:ok, updated_character} =
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
|
||||
|
||||
{:ok,
|
||||
@@ -14,7 +14,6 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
characters: [],
|
||||
selected_map: nil,
|
||||
selected_map_slug: nil,
|
||||
user_id: user_id,
|
||||
maps: maps |> Enum.sort_by(& &1.name, :asc)
|
||||
)}
|
||||
end
|
||||
@@ -37,24 +36,22 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
end
|
||||
|
||||
defp apply_action(socket, :characters, %{"slug" => map_slug} = _params) do
|
||||
selected_map = socket.assigns.maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
|
||||
|
||||
user_id = socket.assigns.user_id
|
||||
defp apply_action(
|
||||
%{assigns: %{current_user: current_user, maps: maps}} = socket,
|
||||
:characters,
|
||||
%{"slug" => map_slug} = _params
|
||||
) do
|
||||
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
|> assign(
|
||||
selected_map: selected_map,
|
||||
selected_map_slug: map_slug,
|
||||
character_settings: character_settings
|
||||
selected_map_slug: map_slug
|
||||
)
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, character_settings, user_id)
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -71,55 +68,36 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
|
||||
def handle_event(
|
||||
"toggle_track",
|
||||
%{"character_id" => character_id},
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
) do
|
||||
selected_map = socket.assigns.selected_map
|
||||
character_settings = socket.assigns.character_settings
|
||||
|
||||
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
|
||||
nil ->
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id,
|
||||
tracked: true
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
character_setting ->
|
||||
case character_setting.tracked do
|
||||
true ->
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.untrack!()
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_setting.character_id
|
||||
])
|
||||
|
||||
_ ->
|
||||
character_setting
|
||||
|> WandererApp.MapCharacterSettingsRepo.track!()
|
||||
end
|
||||
end
|
||||
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: false} ->
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
|
||||
characters =
|
||||
characters
|
||||
|> Enum.map(fn c ->
|
||||
WandererApp.Maps.map_character(
|
||||
c,
|
||||
character_settings |> Enum.find(&(&1.character_id == c.id))
|
||||
)
|
||||
end)
|
||||
%{tracked: true} ->
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_id
|
||||
])
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(character_settings: character_settings)
|
||||
|> assign_async(:characters, fn ->
|
||||
{:ok, %{characters: characters}}
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user