mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-03-15 16:16:02 +00:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bf6893524 | ||
|
|
e45e6a39eb | ||
|
|
7f1691b2db | ||
|
|
d1006b329a | ||
|
|
e49471fb94 | ||
|
|
ad35d9e172 | ||
|
|
18d50329bc | ||
|
|
d8fb980a3b | ||
|
|
b8b3bc60ad | ||
|
|
80d5dd1eb1 | ||
|
|
1ab0e96cbb | ||
|
|
e3a13b9554 | ||
|
|
1f3387e4ff | ||
|
|
95d2fa232a | ||
|
|
eed1d8bc27 | ||
|
|
c451735559 | ||
|
|
aa586b7994 | ||
|
|
39317831f9 | ||
|
|
b71bc94d4f | ||
|
|
0e920a58e6 | ||
|
|
9385751332 | ||
|
|
ffaa48ff9e | ||
|
|
94665f4e68 | ||
|
|
e9fd0665c8 | ||
|
|
9a0271f711 | ||
|
|
0c68535656 | ||
|
|
9ed350befa | ||
|
|
c410f5f37d | ||
|
|
8559be00f0 | ||
|
|
1a24ee4c74 | ||
|
|
35ea4e5f1e | ||
|
|
de86703737 | ||
|
|
c5af43dca1 | ||
|
|
549fa1d2cf | ||
|
|
34a4d5dc9f | ||
|
|
15142f188b | ||
|
|
daf4a81568 | ||
|
|
8c5340e911 | ||
|
|
6b0f636964 | ||
|
|
09ebd29eb4 | ||
|
|
35bd5645bf | ||
|
|
a6948ee1da | ||
|
|
98b3f5855c | ||
|
|
11ad48b40a | ||
|
|
ecd018abfe | ||
|
|
f430f74e98 | ||
|
|
9e146d1117 | ||
|
|
0a707fb423 | ||
|
|
8cda76cc43 | ||
|
|
2005e6f3dd | ||
|
|
ab066a342f | ||
|
|
82b4a5f35a | ||
|
|
ca3a25b836 | ||
|
|
8e46c01a8a | ||
|
|
9d9fa3c6b5 | ||
|
|
0e24501225 | ||
|
|
25a3d8951e | ||
|
|
f4ddc8dc8b | ||
|
|
ac9b46e24d | ||
|
|
40d0a0777a | ||
|
|
608792d99a | ||
|
|
dc9e0c821e | ||
|
|
79d4fd0e43 | ||
|
|
5d03c1ecc7 | ||
|
|
2eef05495e | ||
|
|
f724455a1e | ||
|
|
33bbb3425c | ||
|
|
a919bd9038 | ||
|
|
8ae34cd94a | ||
|
|
2f38da52e8 | ||
|
|
89d7df0ba2 | ||
|
|
ba0c10d2e4 | ||
|
|
996c88d839 | ||
|
|
80e998cf79 | ||
|
|
d2bcb89fa1 | ||
|
|
922f296f17 | ||
|
|
71dc20c933 | ||
|
|
80f7d34d3d | ||
|
|
113fe1c695 | ||
|
|
5550844912 | ||
|
|
0228e68a1d | ||
|
|
3424667af1 | ||
|
|
6c7b28a6c1 | ||
|
|
3988079cd3 | ||
|
|
f5d407fee0 | ||
|
|
a857422c46 | ||
|
|
ec6717d0ef | ||
|
|
56dacdcbbd | ||
|
|
c8e17b1691 | ||
|
|
19c7fe59ee | ||
|
|
682100c231 | ||
|
|
f9ac79cdcc | ||
|
|
f09f220645 | ||
|
|
e585cdfd20 | ||
|
|
3a3180f7b3 | ||
|
|
53abc580e5 | ||
|
|
8710d172a0 | ||
|
|
301a380a4b | ||
|
|
8c911f89e0 | ||
|
|
d7e09fc94e | ||
|
|
3b7e191898 | ||
|
|
f351fbaf20 | ||
|
|
016e793ba7 | ||
|
|
db483fd253 | ||
|
|
911ba231cd | ||
|
|
b3053f325d | ||
|
|
4ab47334fc | ||
|
|
e163f02526 | ||
|
|
9e22dba8f1 | ||
|
|
9631406def | ||
|
|
f6ae448c3b | ||
|
|
46345ef596 | ||
|
|
1625f16c8f | ||
|
|
b4ef9ae983 | ||
|
|
3b9c2dd996 | ||
|
|
8a0f9a58d0 | ||
|
|
5fe8caac0d | ||
|
|
f18f567727 | ||
|
|
91acc49980 | ||
|
|
ae3873a225 | ||
|
|
b351c6cc26 | ||
|
|
698244d945 | ||
|
|
2c7dd9dc5b | ||
|
|
36934cce0b | ||
|
|
b7da7e4ecb | ||
|
|
6471ea5590 | ||
|
|
b46bcac642 | ||
|
|
52d90361e9 | ||
|
|
1c902d3319 | ||
|
|
8f671a359b | ||
|
|
840c416684 | ||
|
|
56e29ad30a | ||
|
|
cd8f8b5801 | ||
|
|
70e013fa3d | ||
|
|
d6bfaf8008 | ||
|
|
95944199a0 | ||
|
|
3bd5db8cf3 | ||
|
|
a245330ada | ||
|
|
1226b6abf3 | ||
|
|
7a1f5c0966 | ||
|
|
e5afa1d5bc | ||
|
|
1473fe8646 | ||
|
|
7039ced11e | ||
|
|
42b5bb337f | ||
|
|
1dbb24f6ec | ||
|
|
c242f510e0 | ||
|
|
c59d51636e | ||
|
|
c5a8aa1b4d | ||
|
|
cba050a9e7 | ||
|
|
59fcbef3b1 | ||
|
|
2f1eb6eeaa | ||
|
|
71ae326cf7 | ||
|
|
07829caf0f | ||
|
|
a5850b5a8d | ||
|
|
9f6849209b | ||
|
|
7bd295cbad | ||
|
|
078e5fc19e | ||
|
|
3877e121c3 | ||
|
|
dcb2a0cdb2 | ||
|
|
a7d6b06332 | ||
|
|
8f6da817db | ||
|
|
378f22a1ef | ||
|
|
14730097b2 | ||
|
|
e8bff3098a |
@@ -16,3 +16,8 @@ export WANDERER_SSE_ENABLED="true"
|
||||
export WANDERER_WEBHOOKS_ENABLED="true"
|
||||
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
||||
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
||||
|
||||
# Promo codes for map subscriptions (optional)
|
||||
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
|
||||
# Codes are case-insensitive, discounts stack with period discounts
|
||||
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ repomix*
|
||||
/priv/static/images/
|
||||
/priv/static/*.js
|
||||
/priv/static/*.css
|
||||
/priv/static/*-*.png
|
||||
/priv/static/*-*.webp
|
||||
/priv/static/*-*.webmanifest
|
||||
|
||||
# Dialyzer PLT files
|
||||
/priv/plts/
|
||||
|
||||
324
CHANGELOG.md
324
CHANGELOG.md
@@ -2,6 +2,330 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.97.0](https://github.com/wanderer-industries/wanderer/compare/v1.96.6...v1.97.0) (2026-03-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* signatures: Sync mass status with connection, show it on unsplashed sigs
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Connection context menu and wormhole. Change UI for select wormhole mass state. Change UI for select ship-size for wormhole. Add ability to set mass for signatures
|
||||
|
||||
## [v1.96.6](https://github.com/wanderer-industries/wanderer/compare/v1.96.5...v1.96.6) (2026-03-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed tracking issues
|
||||
|
||||
## [v1.96.5](https://github.com/wanderer-industries/wanderer/compare/v1.96.4...v1.96.5) (2026-02-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed access token refresh issues
|
||||
|
||||
## [v1.96.4](https://github.com/wanderer-industries/wanderer/compare/v1.96.3...v1.96.4) (2026-02-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Change character token validity status. Now we will see red frame and icon for tracked characters which token was expired.
|
||||
|
||||
## [v1.96.3](https://github.com/wanderer-industries/wanderer/compare/v1.96.2...v1.96.3) (2026-02-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* tracking: Fixed character tracking issues
|
||||
|
||||
## [v1.96.2](https://github.com/wanderer-industries/wanderer/compare/v1.96.1...v1.96.2) (2026-02-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Added icons for RoutesBy
|
||||
|
||||
## [v1.96.1](https://github.com/wanderer-industries/wanderer/compare/v1.96.0...v1.96.1) (2026-02-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.96.0](https://github.com/wanderer-industries/wanderer/compare/v1.95.0...v1.96.0) (2026-02-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* signatures: Fixed creator visibility issues. Added 4.5 hour color for unsplashed
|
||||
|
||||
## [v1.95.0](https://github.com/wanderer-industries/wanderer/compare/v1.94.0...v1.95.0) (2026-02-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* subscriptions: Added top map donators support
|
||||
|
||||
* Added lost files
|
||||
|
||||
* Added paywall for RoutesBy widget
|
||||
|
||||
* removed unnecessary env variable for routes
|
||||
|
||||
* Add systems with Security Status cleaning. Add trade hubs. Add ability to store data for this widget
|
||||
|
||||
* Add Routes By widget. Allow to find nearest blue loot and red loot stations. Added ability to set waypoint to station.
|
||||
|
||||
* auto add system on sig addition
|
||||
|
||||
* map: Reviewed changes
|
||||
|
||||
* map: Logic for multiple owner updates
|
||||
|
||||
* map: wip New Dialog for Structure Owners
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* signatures: Fixed back linked sigs data sync and leading to system override issues
|
||||
|
||||
* signatures: Moved C1/C2/C3 and C4/C5 to the bottom of the available list
|
||||
|
||||
* use cache for sse
|
||||
|
||||
* adding system when linked signature is provided
|
||||
|
||||
* saving updates to unknown sigs
|
||||
|
||||
* wh position and sig type change
|
||||
|
||||
* api updates and linked sig addition
|
||||
|
||||
* api fixes and format
|
||||
|
||||
* Wrong file added to commits
|
||||
|
||||
## [v1.94.0](https://github.com/wanderer-industries/wanderer/compare/v1.93.0...v1.94.0) (2026-02-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* administration: Added registered characters admin view with cort/ally info, sort and filter options
|
||||
|
||||
## [v1.93.0](https://github.com/wanderer-industries/wanderer/compare/v1.92.0...v1.93.0) (2026-02-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* subscriptions: Added an ability to withdraw from map to user balance
|
||||
|
||||
## [v1.92.0](https://github.com/wanderer-industries/wanderer/compare/v1.91.11...v1.92.0) (2026-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Added ability to select a range of wh classes for k162.
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Show c1/c2/c3 or c4/c5 or link signature modal
|
||||
|
||||
## [v1.91.11](https://github.com/wanderer-industries/wanderer/compare/v1.91.10...v1.91.11) (2026-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* allow sig api when map relay is off
|
||||
|
||||
## [v1.91.10](https://github.com/wanderer-industries/wanderer/compare/v1.91.9...v1.91.10) (2026-01-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove actor context requirement from sig api
|
||||
|
||||
## [v1.91.9](https://github.com/wanderer-industries/wanderer/compare/v1.91.8...v1.91.9) (2026-01-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed rally point cancel logic
|
||||
|
||||
## [v1.91.8](https://github.com/wanderer-industries/wanderer/compare/v1.91.7...v1.91.8) (2026-01-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed rally point cancel logic
|
||||
|
||||
## [v1.91.7](https://github.com/wanderer-industries/wanderer/compare/v1.91.6...v1.91.7) (2026-01-05)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.6](https://github.com/wanderer-industries/wanderer/compare/v1.91.5...v1.91.6) (2026-01-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed new connections got deleted after linked signature cleanup
|
||||
|
||||
## [v1.91.5](https://github.com/wanderer-industries/wanderer/compare/v1.91.4...v1.91.5) (2025-12-30)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.4](https://github.com/wanderer-industries/wanderer/compare/v1.91.3...v1.91.4) (2025-12-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connections create between k-space systems (considered as wh connection)
|
||||
|
||||
## [v1.91.3](https://github.com/wanderer-industries/wanderer/compare/v1.91.2...v1.91.3) (2025-12-28)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.2](https://github.com/wanderer-industries/wanderer/compare/v1.91.1...v1.91.2) (2025-12-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes updates & logic
|
||||
|
||||
## [v1.91.1](https://github.com/wanderer-industries/wanderer/compare/v1.91.0...v1.91.1) (2025-12-25)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.91.0](https://github.com/wanderer-industries/wanderer/compare/v1.90.13...v1.91.0) (2025-12-24)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* admin: added maps administration view with basic info, search, restore/delete, acls view and edit options
|
||||
|
||||
## [v1.90.13](https://github.com/wanderer-industries/wanderer/compare/v1.90.12...v1.90.13) (2025-12-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed welcome page
|
||||
|
||||
## [v1.90.12](https://github.com/wanderer-industries/wanderer/compare/v1.90.11...v1.90.12) (2025-12-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed permissions update after character corp updates
|
||||
|
||||
## [v1.90.11](https://github.com/wanderer-industries/wanderer/compare/v1.90.10...v1.90.11) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
|
||||
|
||||
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: skip systems or connections cleanup for not started maps
|
||||
|
||||
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed scopes
|
||||
|
||||
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes & signatures clean up behaviour
|
||||
|
||||
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
|
||||
|
||||
|
||||
|
||||
@@ -1001,3 +1001,27 @@ body > div:first-of-type {
|
||||
.verticalTabsContainer .p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Blog post CTA links - only in main post content */
|
||||
.post-content a {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
|
||||
color: white !important;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none !important;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
.post-content a:hover {
|
||||
background: linear-gradient(135deg, #db2777 0%, #7c3aed 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4);
|
||||
}
|
||||
|
||||
.post-content a:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback } from 'react';
|
||||
import classes from './Characters.module.scss';
|
||||
import {
|
||||
TooltipPosition,
|
||||
WdEveEntityPortrait,
|
||||
WdEveEntityPortraitSize,
|
||||
WdTooltipWrapper,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdCharStateWrapper } from '@/hooks/Mapper/components/characters/components/WdCharStateWrapper.tsx';
|
||||
|
||||
interface CharactersProps {
|
||||
data: CharacterTypeRaw[];
|
||||
}
|
||||
@@ -17,7 +22,7 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
|
||||
const {
|
||||
outCommand,
|
||||
data: { mainCharacterEveId, followingCharacterEveId },
|
||||
data: { mainCharacterEveId, followingCharacterEveId, expiredCharacters },
|
||||
} = useMapRootState();
|
||||
|
||||
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
|
||||
@@ -35,61 +40,48 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const items = data.map(character => (
|
||||
<li
|
||||
key={character.eve_id}
|
||||
className="flex flex-col items-center justify-center"
|
||||
onClick={() => handleSelect(character)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !character.online,
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.name}
|
||||
>
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
|
||||
'text-yellow-500 text-[9px] rounded-[1px] z-10',
|
||||
'pi',
|
||||
PrimeIcons.STAR_FILL,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
const items = data.map(character => {
|
||||
const isExpired = expiredCharacters.includes(character.eve_id);
|
||||
|
||||
{followingCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
|
||||
'text-sky-300 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-angle-double-right',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDocked(character.location) && <div className={classes.Docked} />}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full h-full bg-transparent cursor-pointer',
|
||||
'bg-center bg-no-repeat bg-[length:100%]',
|
||||
'transition-opacity',
|
||||
'shadow-[inset_0_1px_6px_1px_#000000]',
|
||||
{
|
||||
['opacity-60']: !character.online,
|
||||
['opacity-100']: character.online,
|
||||
},
|
||||
)}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
|
||||
></div>
|
||||
</div>
|
||||
</li>
|
||||
));
|
||||
return (
|
||||
<li
|
||||
key={character.eve_id}
|
||||
className="flex flex-col items-center justify-center"
|
||||
onClick={() => handleSelect(character)}
|
||||
>
|
||||
<WdTooltipWrapper
|
||||
position={TooltipPosition.bottom}
|
||||
content={isExpired ? `Token is expired for ${character.name}` : character.name}
|
||||
>
|
||||
<WdCharStateWrapper
|
||||
eve_id={character.eve_id}
|
||||
location={character.location}
|
||||
isExpired={isExpired}
|
||||
isMain={mainCharacterEveId === character.eve_id}
|
||||
isFollowing={followingCharacterEveId === character.eve_id}
|
||||
isOnline={character.online}
|
||||
>
|
||||
<WdEveEntityPortrait
|
||||
eveId={character.eve_id}
|
||||
size={WdEveEntityPortraitSize.w33}
|
||||
className={clsx(
|
||||
'flex w-full h-full bg-transparent cursor-pointer',
|
||||
'bg-center bg-no-repeat bg-[length:100%]',
|
||||
'transition-opacity',
|
||||
'shadow-[inset_0_1px_6px_1px_#000000]',
|
||||
{
|
||||
['opacity-60']: !isExpired && !character.online,
|
||||
['opacity-100']: !isExpired && character.online,
|
||||
['opacity-50']: isExpired,
|
||||
},
|
||||
'!border-0',
|
||||
)}
|
||||
/>
|
||||
</WdCharStateWrapper>
|
||||
</WdTooltipWrapper>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="flex gap-1 characters" id="characters" ref={parent}>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import classes from './WdCharStateWrapper.module.scss';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import { LocationRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
type WdCharStateWrapperProps = {
|
||||
eve_id: string;
|
||||
isExpired?: boolean;
|
||||
isMain?: boolean;
|
||||
isFollowing?: boolean;
|
||||
location: LocationRaw | null;
|
||||
isOnline: boolean;
|
||||
} & WithChildren;
|
||||
|
||||
export const WdCharStateWrapper = ({
|
||||
location,
|
||||
isOnline,
|
||||
isMain,
|
||||
isFollowing,
|
||||
isExpired,
|
||||
children,
|
||||
}: WdCharStateWrapperProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !isExpired && !isOnline,
|
||||
['border-lime-600/70']: !isExpired && isOnline,
|
||||
['border-red-600/70']: isExpired,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{isMain && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
|
||||
'text-yellow-500 text-[9px] rounded-[1px] z-10',
|
||||
'pi',
|
||||
PrimeIcons.STAR_FILL,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isFollowing && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
|
||||
'text-sky-300 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-angle-double-right',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDocked(location) && <div className={classes.Docked} />}
|
||||
{isExpired && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[4px] left-[4px] w-[10px] h-[10px]',
|
||||
'text-red-400 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-exclamation-triangle',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WdCharStateWrapper.tsx';
|
||||
@@ -8,3 +8,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ContextMenu {
|
||||
width: max-content;
|
||||
min-width: unset;
|
||||
|
||||
:global {
|
||||
.p-submenu-list {
|
||||
width: max-content;
|
||||
min-width: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import React, { RefObject, useCallback, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import classes from './ContextMenuSystemInfo.module.scss';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
|
||||
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { Route, RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { useGetOwnOnlineCharacters } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
|
||||
import { sortStationsByDistance } from './sortStationsByDistance.ts';
|
||||
|
||||
export interface ContextMenuSystemInfoProps {
|
||||
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
|
||||
hubs: string[];
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
systemId: string | undefined;
|
||||
systemIdFrom?: string | undefined;
|
||||
@@ -37,11 +39,106 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
onWaypointSet,
|
||||
systemId,
|
||||
systemIdFrom,
|
||||
hubs,
|
||||
routes,
|
||||
}) => {
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
|
||||
const { toggleHubCommand, hubs } = useRouteProvider();
|
||||
const getOwnOnlineCharacters = useGetOwnOnlineCharacters();
|
||||
|
||||
const getStationWaypointItems = useCallback(
|
||||
(destinationId: string, chars: CharacterTypeRaw[]): MenuItem[] => [
|
||||
{
|
||||
label: 'Set Destination',
|
||||
icon: PrimeIcons.SEND,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: true,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint',
|
||||
icon: PrimeIcons.DIRECTIONS_ALT,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: false,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint Front',
|
||||
icon: PrimeIcons.DIRECTIONS,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[onWaypointSet],
|
||||
);
|
||||
|
||||
const getStationsMenu = useCallback(
|
||||
(stations: RouteStationSummary[]) => {
|
||||
const chars = getOwnOnlineCharacters().filter(x => x.online);
|
||||
const sortedStations = sortStationsByDistance(stations);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Stations',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
items: sortedStations.map(station => {
|
||||
const destinationId = station.station_id.toString();
|
||||
const specialClass = station.special ? '[&_.p-menuitem-text]:text-orange-400' : '';
|
||||
|
||||
if (chars.length === 0) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: specialClass || undefined,
|
||||
items: [{ label: 'No online characters', disabled: true }],
|
||||
};
|
||||
}
|
||||
|
||||
if (chars.length === 1) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: specialClass || undefined,
|
||||
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: `${specialClass} w-[500px]`.trim(),
|
||||
items: [
|
||||
{
|
||||
label: 'All',
|
||||
icon: PrimeIcons.USERS,
|
||||
items: getStationWaypointItems(destinationId, chars),
|
||||
},
|
||||
...chars.map(char => ({
|
||||
label: char.name,
|
||||
icon: PrimeIcons.USER,
|
||||
items: getStationWaypointItems(destinationId, [char]),
|
||||
})),
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
[getOwnOnlineCharacters, getStationWaypointItems],
|
||||
);
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
|
||||
@@ -50,6 +147,10 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
if (!systemId || !system) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const route = routes.find(x => x.destination?.toString() === systemId);
|
||||
const stationItems = route?.stations?.length ? getStationsMenu(route.stations) : [];
|
||||
|
||||
return [
|
||||
{
|
||||
className: classes.FastActions,
|
||||
@@ -69,15 +170,20 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
{ separator: true },
|
||||
...getJumpPlannerMenu(system, routes),
|
||||
...getWaypointMenu(systemId, system.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
...stationItems,
|
||||
...(toggleHubCommand
|
||||
? [
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!systemOnMap
|
||||
? [
|
||||
{
|
||||
@@ -94,15 +200,18 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
systems,
|
||||
getJumpPlannerMenu,
|
||||
getWaypointMenu,
|
||||
getStationsMenu,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
onAddSystem,
|
||||
onOpenSettings,
|
||||
toggleHubCommand,
|
||||
routes,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
<ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
|
||||
|
||||
const ROMAN_VALUES: Record<string, number> = {
|
||||
I: 1,
|
||||
V: 5,
|
||||
X: 10,
|
||||
L: 50,
|
||||
C: 100,
|
||||
D: 500,
|
||||
M: 1000,
|
||||
};
|
||||
|
||||
const MAX_DISTANCE = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const romanToInt = (value: string): number | null => {
|
||||
const chars = value.toUpperCase().split('');
|
||||
|
||||
if (chars.length === 0 || chars.some(char => ROMAN_VALUES[char] === undefined)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
let prev = 0;
|
||||
|
||||
for (let i = chars.length - 1; i >= 0; i--) {
|
||||
const current = ROMAN_VALUES[chars[i]];
|
||||
if (current < prev) {
|
||||
total -= current;
|
||||
} else {
|
||||
total += current;
|
||||
prev = current;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
const parseOrbitIndex = (value: string | undefined): number | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
const asInt = Number.parseInt(trimmed, 10);
|
||||
|
||||
if (!Number.isNaN(asInt) && `${asInt}` === trimmed) {
|
||||
return asInt;
|
||||
}
|
||||
|
||||
return romanToInt(trimmed);
|
||||
};
|
||||
|
||||
const extractPlanetOrbit = (name: string): number | null => {
|
||||
const firstPart = name.split(' - ')[0] ?? '';
|
||||
const match = firstPart.match(/([IVXLCDM]+|\d+)(?:\s*\([^)]*\))?$/i);
|
||||
return parseOrbitIndex(match?.[1]);
|
||||
};
|
||||
|
||||
const extractMoonOrbit = (name: string): number | null => {
|
||||
const match = name.match(/\bMoon\s+([IVXLCDM]+|\d+)\b/i);
|
||||
return parseOrbitIndex(match?.[1]);
|
||||
};
|
||||
|
||||
const stationSortKey = (station: RouteStationSummary): [number, number, string, number] => {
|
||||
return [
|
||||
extractPlanetOrbit(station.station_name) ?? MAX_DISTANCE,
|
||||
// If there is no moon in the station name, treat it as closer than moon orbits.
|
||||
extractMoonOrbit(station.station_name) ?? 0,
|
||||
station.station_name.toLowerCase(),
|
||||
station.station_id,
|
||||
];
|
||||
};
|
||||
|
||||
export const sortStationsByDistance = (stations: RouteStationSummary[]): RouteStationSummary[] => {
|
||||
return [...stations].sort((a, b) => {
|
||||
const aKey = stationSortKey(a);
|
||||
const bKey = stationSortKey(b);
|
||||
|
||||
for (let i = 0; i < aKey.length; i++) {
|
||||
if (aKey[i] < bKey[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (aKey[i] > bKey[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export const useContextMenuSystemInfoHandlers = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current.toggleHubCommand(system);
|
||||
ref.current.toggleHubCommand?.(system);
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export const useDetectSettingsChanged = () => {
|
||||
storedSettings: {
|
||||
interfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsRoutesBy,
|
||||
settingsLocal,
|
||||
settingsSignatures,
|
||||
settingsOnTheMap,
|
||||
@@ -16,7 +17,15 @@ export const useDetectSettingsChanged = () => {
|
||||
|
||||
useEffect(
|
||||
() => setCounter(x => x + 1),
|
||||
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
|
||||
[
|
||||
interfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsRoutesBy,
|
||||
settingsLocal,
|
||||
settingsSignatures,
|
||||
settingsOnTheMap,
|
||||
settingsKills,
|
||||
],
|
||||
);
|
||||
|
||||
return counter;
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
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';
|
||||
@@ -14,6 +6,8 @@ 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 { MassStatusActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/MassStatusActionsWrapper.tsx';
|
||||
import { ShipSizeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/ShipSizeActionsWrapper.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';
|
||||
@@ -86,16 +80,28 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
|
||||
},
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
className: clsx(classes.FastActions, '!h-[54px]'),
|
||||
template: () => {
|
||||
return (
|
||||
<MassStatusActionsWrapper
|
||||
massStatus={edge.data?.mass_status}
|
||||
onChangeMassStatus={onChangeMassState}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
className: clsx(classes.FastActions, '!h-[64px]'),
|
||||
template: () => {
|
||||
return (
|
||||
<ShipSizeActionsWrapper shipSize={edge.data?.ship_size_type} onChangeShipSize={onChangeShipSizeStatus} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
@@ -105,41 +111,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
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
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
})),
|
||||
},
|
||||
...(bothNullsec
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import {
|
||||
WdMassStatusSelector,
|
||||
WdMassStatusSelectorProps,
|
||||
} from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
|
||||
|
||||
export const MassStatusActionsWrapper = (props: WdMassStatusSelectorProps) => {
|
||||
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">Mass status:</div>
|
||||
|
||||
<WdMassStatusSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdShipSizeSelector, WdShipSizeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdShipSizeSelector.tsx';
|
||||
|
||||
export const ShipSizeActionsWrapper = (props: WdShipSizeSelectorProps) => {
|
||||
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">Ship size:</div>
|
||||
|
||||
<WdShipSizeSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { UnsplashedSignatureColumn } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Tag } from 'primereact/tag';
|
||||
@@ -177,17 +177,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedLeft}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedRight}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { UnsplashedSignatureColumn } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
|
||||
@@ -157,17 +157,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedLeft}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedRight}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.Signature {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: block;
|
||||
|
||||
& > .Box {
|
||||
width: 13px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > .Eol {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
}
|
||||
export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) => {
|
||||
const {
|
||||
data: { wormholesData },
|
||||
} = useMapRootState();
|
||||
|
||||
const whData = useMemo(() => wormholesData[signature.type], [signature.type, wormholesData]);
|
||||
const whClass = useMemo(() => (whData ? WORMHOLES_ADDITIONAL_INFO[whData.dest] : null), [whData]);
|
||||
|
||||
const customInfo = useMemo(() => {
|
||||
return parseSignatureCustomInfo(signature.custom_info);
|
||||
}, [signature]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!customInfo?.k162Type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return K162_TYPES_MAP[customInfo.k162Type];
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.time_status === TimeStatus._1h;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
if (signature.type === 'K162' && k162TypeOption) {
|
||||
const k162Data = wormholesData[k162TypeOption.whClassName];
|
||||
const k162Class = k162Data ? WORMHOLES_ADDITIONAL_INFO[k162Data.dest] : null;
|
||||
return k162Class ? WORMHOLE_CLASS_STYLES[k162Class.wormholeClassID] : '';
|
||||
}
|
||||
return whClass ? WORMHOLE_CLASS_STYLES[whClass.wormholeClassID] : '';
|
||||
}, [signature, whClass, k162TypeOption, wormholesData]);
|
||||
|
||||
return (
|
||||
<WdTooltipWrapper
|
||||
className={clsx(classes.Signature)}
|
||||
// @ts-ignore
|
||||
content={
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
|
||||
</svg>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { resolveSignatureFillVar } from '@/hooks/Mapper/components/map/helpers/wormholeClassFillVars';
|
||||
import { MassState, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes';
|
||||
|
||||
interface PillData {
|
||||
signature: SystemSignature;
|
||||
fill: string;
|
||||
isEOL: boolean;
|
||||
is4H: boolean;
|
||||
isVerge: boolean;
|
||||
isHalf: boolean;
|
||||
}
|
||||
|
||||
const PILL_W = 13;
|
||||
const PILL_H = 8;
|
||||
const GAP = 2;
|
||||
const COLS = 4;
|
||||
|
||||
interface UnsplashedSignatureColumnProps {
|
||||
signatures: SystemSignature[];
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
}
|
||||
|
||||
export const UnsplashedSignatureColumn = ({ signatures, wormholesData }: UnsplashedSignatureColumnProps) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [hoveredIndex, setHoveredIndex] = useState(-1);
|
||||
|
||||
const pills: PillData[] = useMemo(() => {
|
||||
return signatures.map(sig => {
|
||||
const customInfo = parseSignatureCustomInfo(sig.custom_info);
|
||||
return {
|
||||
signature: sig,
|
||||
fill: resolveSignatureFillVar(sig, wormholesData),
|
||||
isEOL: customInfo.time_status === TimeStatus._1h,
|
||||
is4H: customInfo.time_status === TimeStatus._4h,
|
||||
isVerge: customInfo.mass_status === MassState.verge,
|
||||
isHalf: customInfo.mass_status === MassState.half,
|
||||
};
|
||||
});
|
||||
}, [signatures, wormholesData]);
|
||||
|
||||
const count = pills.length;
|
||||
if (count === 0) return null;
|
||||
|
||||
const cols = Math.min(count, COLS);
|
||||
const rows = Math.ceil(count / COLS);
|
||||
const svgWidth = cols * (PILL_W + GAP) - GAP;
|
||||
const svgHeight = rows * (PILL_H + GAP) - GAP;
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(e: React.MouseEvent<SVGSVGElement>) => {
|
||||
const svg = svgRef.current;
|
||||
if (!svg) return;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const localX = e.clientX - rect.left;
|
||||
const localY = e.clientY - rect.top;
|
||||
|
||||
const col = Math.floor(localX / (PILL_W + GAP));
|
||||
const row = Math.floor(localY / (PILL_H + GAP));
|
||||
|
||||
const withinPill =
|
||||
localX - col * (PILL_W + GAP) <= PILL_W && localY - row * (PILL_H + GAP) <= PILL_H;
|
||||
|
||||
const idx = withinPill ? row * COLS + col : -1;
|
||||
setHoveredIndex(idx < count ? idx : -1);
|
||||
},
|
||||
[count],
|
||||
);
|
||||
|
||||
const onMouseLeave = useCallback(() => setHoveredIndex(-1), []);
|
||||
|
||||
// Compute tooltip position relative to viewport
|
||||
let tooltipX = 0;
|
||||
let tooltipY = 0;
|
||||
if (hoveredIndex >= 0 && svgRef.current) {
|
||||
const svgRect = svgRef.current.getBoundingClientRect();
|
||||
const col = hoveredIndex % COLS;
|
||||
const row = Math.floor(hoveredIndex / COLS);
|
||||
tooltipX = svgRect.left + col * (PILL_W + GAP) + PILL_W / 2;
|
||||
tooltipY = svgRect.top + row * (PILL_H + GAP) - 4;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ display: 'block', position: 'relative', top: 3 }}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{pills.map((pill, i) => {
|
||||
const col = i % COLS;
|
||||
const row = Math.floor(i / COLS);
|
||||
const x = col * (PILL_W + GAP);
|
||||
const y = row * (PILL_H + GAP);
|
||||
return (
|
||||
<g key={pill.signature.eve_id} transform={`translate(${x},${y})`}>
|
||||
<rect y="1" width="13" height="4" rx="2" fill={pill.fill} />
|
||||
{pill.isEOL && <rect x="4" width="5" height="6" rx="1" fill="#a153ac" />}
|
||||
{pill.is4H && <rect x="4" width="5" height="6" rx="1" fill="#d8b4fe" />}
|
||||
{pill.isVerge && <rect x="0" width="5" height="6" rx="1" fill="#af0000" />}
|
||||
{pill.isHalf && <rect x="0" width="5" height="6" rx="1" fill="#ffd700" />}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{hoveredIndex >= 0 &&
|
||||
pills[hoveredIndex] &&
|
||||
createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: tooltipX,
|
||||
top: tooltipY,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--surface-card, #1e1e2e)',
|
||||
border: '1px solid var(--surface-border, #333)',
|
||||
borderRadius: 6,
|
||||
padding: '4px 6px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{pills[hoveredIndex].signature.eve_id}</b>}>
|
||||
{renderInfoColumn(pills[hoveredIndex].signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
export * from './UnsplashedSignature.tsx';
|
||||
export * from './UnsplashedSignatureColumn.tsx';
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './getShapeClass';
|
||||
export * from './getBackgroundClass';
|
||||
export * from './prepareUnsplashedChunks';
|
||||
export * from './checkPermissions';
|
||||
export * from './wormholeClassFillVars';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
|
||||
// Maps WORMHOLE_CLASS_STYLES CSS class names → CSS variable references for inline SVG fill.
|
||||
// The CSS classes set `color: var(--eve-wh-type-color-*)`, but SVG `fill` needs an explicit value.
|
||||
const CLASS_NAME_TO_CSS_VAR: Record<string, string> = {
|
||||
'eve-wh-type-color-c1': 'var(--eve-wh-type-color-c1)',
|
||||
'eve-wh-type-color-c2': 'var(--eve-wh-type-color-c2)',
|
||||
'eve-wh-type-color-c3': 'var(--eve-wh-type-color-c3)',
|
||||
'eve-wh-type-color-c4': 'var(--eve-wh-type-color-c4)',
|
||||
'eve-wh-type-color-c5': 'var(--eve-wh-type-color-c5)',
|
||||
'eve-wh-type-color-c6': 'var(--eve-wh-type-color-c6)',
|
||||
'eve-wh-type-color-high': 'var(--eve-wh-type-color-high)',
|
||||
'eve-wh-type-color-low': 'var(--eve-wh-type-color-low)',
|
||||
'eve-wh-type-color-null': 'var(--eve-wh-type-color-null)',
|
||||
'eve-wh-type-color-thera': 'var(--eve-wh-type-color-thera)',
|
||||
'eve-wh-type-color-c13': 'var(--eve-wh-type-color-c13)',
|
||||
'eve-wh-type-color-drifter': 'var(--eve-wh-type-color-drifter)',
|
||||
'eve-wh-type-color-zarzakh': 'var(--eve-wh-type-color-zarzakh)',
|
||||
// eve-kind-color-abyss and eve-kind-color-penalty both resolve to --eve-wh-type-color-c6
|
||||
'eve-kind-color-abyss': 'var(--eve-wh-type-color-c6)',
|
||||
'eve-kind-color-penalty': 'var(--eve-wh-type-color-c6)',
|
||||
};
|
||||
|
||||
const DEFAULT_FILL = '#833ca4';
|
||||
|
||||
export function resolveSignatureFillVar(
|
||||
signature: SystemSignature,
|
||||
wormholesData: Record<string, WormholeDataRaw>,
|
||||
): string {
|
||||
const customInfo = parseSignatureCustomInfo(signature.custom_info);
|
||||
|
||||
// K162 override: use the k162Type to look up the real destination class
|
||||
if (signature.type === 'K162' && customInfo.k162Type) {
|
||||
const k162Option = K162_TYPES_MAP[customInfo.k162Type];
|
||||
if (k162Option) {
|
||||
const k162Data = wormholesData[k162Option.whClassName];
|
||||
const k162Class = k162Data ? WORMHOLES_ADDITIONAL_INFO[k162Data.dest] : null;
|
||||
if (k162Class) {
|
||||
const className = WORMHOLE_CLASS_STYLES[k162Class.wormholeClassID];
|
||||
if (className && CLASS_NAME_TO_CSS_VAR[className]) {
|
||||
return CLASS_NAME_TO_CSS_VAR[className];
|
||||
}
|
||||
}
|
||||
}
|
||||
return DEFAULT_FILL;
|
||||
}
|
||||
|
||||
// Normal type lookup
|
||||
const whData = wormholesData[signature.type];
|
||||
if (!whData) return DEFAULT_FILL;
|
||||
|
||||
const whClass = WORMHOLES_ADDITIONAL_INFO[whData.dest];
|
||||
if (!whClass) return DEFAULT_FILL;
|
||||
|
||||
const className = WORMHOLE_CLASS_STYLES[whClass.wormholeClassID];
|
||||
if (!className) return DEFAULT_FILL;
|
||||
|
||||
return CLASS_NAME_TO_CSS_VAR[className] || DEFAULT_FILL;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { Regions, REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature, WormholeDataRaw } from '@/hooks/Mapper/types';
|
||||
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
|
||||
import { useSystemName } from './useSystemName';
|
||||
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
|
||||
@@ -46,6 +46,7 @@ export interface SolarSystemNodeVars {
|
||||
userCharacters: string[];
|
||||
unsplashedLeft: Array<SystemSignature>;
|
||||
unsplashedRight: Array<SystemSignature>;
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
isThickConnections: boolean;
|
||||
isRally: boolean;
|
||||
classTitle: string | null;
|
||||
@@ -72,7 +73,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
|
||||
const {
|
||||
storedSettings: { interfaceSettings },
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
data: { systemSignatures: mapSystemSignatures, pings },
|
||||
} = useMapRootState();
|
||||
|
||||
const systemStaticInfo = useMemo(() => {
|
||||
@@ -108,7 +109,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
pings,
|
||||
systemHighlighted,
|
||||
},
|
||||
outCommand,
|
||||
@@ -211,6 +211,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
charactersInSystem,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
wormholesData,
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: computedTemporaryName,
|
||||
|
||||
@@ -121,6 +121,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!ping) {
|
||||
setIsShow(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,27 +162,26 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
};
|
||||
}, [interfaceSettings]);
|
||||
|
||||
if (!ping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
|
||||
// Only render Toast when there's a ping
|
||||
return (
|
||||
<>
|
||||
<Toast
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
{ping && (
|
||||
<Toast
|
||||
key={ping.id}
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
@@ -253,28 +253,33 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
{/*/>*/}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
></Toast>
|
||||
)}
|
||||
></Toast>
|
||||
)}
|
||||
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
{ping && (
|
||||
<>
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
/>
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
|
||||
import {
|
||||
SOLAR_SYSTEM_CLASS_IDS,
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
SOLAR_SYSTEM_CLASS_IDS,
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
} 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';
|
||||
@@ -91,7 +91,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
|
||||
if (k162TypeInfo) {
|
||||
// Check if the k162Type matches our target system class
|
||||
return customInfo.k162Type === targetSystemClassGroup;
|
||||
return k162TypeInfo.value.includes(targetSystemClassGroup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SystemStructures,
|
||||
WRoutesPublic,
|
||||
WRoutesUser,
|
||||
WRoutesBy,
|
||||
WSystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
@@ -18,6 +19,7 @@ export enum WidgetsIds {
|
||||
signatures = 'signatures',
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
routesBy = 'routesBy',
|
||||
structures = 'structures',
|
||||
kills = 'kills',
|
||||
comments = 'comments',
|
||||
@@ -60,6 +62,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesPublic />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routesBy,
|
||||
position: { x: 10, y: 740 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesBy />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
position: { x: 10, y: 10 },
|
||||
@@ -112,6 +121,10 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
id: WidgetsIds.routes,
|
||||
label: 'Routes',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routesBy,
|
||||
label: 'Routes By',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
label: 'User Routes',
|
||||
|
||||
@@ -41,7 +41,7 @@ export const RoutesWidgetContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, systems, isSubscriptionActive },
|
||||
} = useMapRootState();
|
||||
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
|
||||
const { hubs = [], routesList, isRestricted, loading, nohubsPlaceholder } = useRouteProvider();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
@@ -105,7 +105,11 @@ export const RoutesWidgetContent = () => {
|
||||
}
|
||||
|
||||
if (hubs.length === 0) {
|
||||
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center select-none">
|
||||
{nohubsPlaceholder ?? 'Routes not set'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -129,7 +133,6 @@ export const RoutesWidgetContent = () => {
|
||||
offset: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<SystemView
|
||||
systemId={route.destination.toString()}
|
||||
className={clsx('select-none text-center cursor-context-menu')}
|
||||
@@ -138,7 +141,7 @@ export const RoutesWidgetContent = () => {
|
||||
showCustomName
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? (route.systems?.length ?? 2) : ''}</div>
|
||||
<div className="pl-2 pb-0.5">
|
||||
<RoutesList data={route} onContextMenu={handleContextMenu} />
|
||||
</div>
|
||||
@@ -147,9 +150,7 @@ export const RoutesWidgetContent = () => {
|
||||
})}
|
||||
</div>
|
||||
</LoadingWrapper>
|
||||
|
||||
<ContextMenuSystemInfo
|
||||
hubs={hubs}
|
||||
routes={preparedRoutes}
|
||||
systems={systems}
|
||||
systemStatics={systemStatics}
|
||||
@@ -162,9 +163,10 @@ export const RoutesWidgetContent = () => {
|
||||
|
||||
type RoutesWidgetCompProps = {
|
||||
title: ReactNode | string;
|
||||
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
|
||||
};
|
||||
|
||||
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
|
||||
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
|
||||
const { data, update, addHubCommand } = useRouteProvider();
|
||||
|
||||
@@ -183,7 +185,7 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => addHubCommand(item.value.toString()),
|
||||
async item => addHubCommand?.(item.value.toString()),
|
||||
[addHubCommand],
|
||||
);
|
||||
|
||||
@@ -191,15 +193,17 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<span className="select-none">{title}</span>
|
||||
<div className="select-none flex items-center gap-2">{title}</div>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
{addHubCommand && (
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
@@ -223,24 +227,38 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RoutesWidgetContent />
|
||||
{renderContent ? (
|
||||
renderContent(
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>,
|
||||
compact,
|
||||
)
|
||||
) : (
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
|
||||
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
{addHubCommand && (
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
|
||||
({ title, ...props }, ref) => {
|
||||
({ title, renderContent, ...props }, ref) => {
|
||||
return (
|
||||
<RoutesProvider {...props} ref={ref}>
|
||||
<RoutesWidgetComp title={title} />
|
||||
<RoutesWidgetComp title={title} renderContent={renderContent} />
|
||||
</RoutesProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './useLoadRoutes';
|
||||
export * from './useLoadRoutesBy';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
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';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
type UseLoadRoutesByProps = {
|
||||
loadRoutesCommand: LoadRoutesCommand;
|
||||
routesList: RoutesList | undefined;
|
||||
data: RoutesType;
|
||||
deps?: unknown[];
|
||||
};
|
||||
|
||||
export const useLoadRoutesBy = ({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
routesList,
|
||||
deps = [],
|
||||
}: UseLoadRoutesByProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const prevSys = usePrevious(selectedSystems);
|
||||
const ref = useRef({ prevSys, selectedSystems });
|
||||
ref.current = { prevSys, selectedSystems };
|
||||
|
||||
const loadRoutes = useCallback(
|
||||
(systemId: string, settings: RoutesType) => {
|
||||
loadRoutesCommand(systemId, settings);
|
||||
setLoading(true);
|
||||
},
|
||||
[loadRoutesCommand],
|
||||
);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.routesListBy) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [routesList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSystems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
loadRoutes(systemId, routesSettings);
|
||||
}, [loadRoutes, selectedSystems, ...flattenValues(routesSettings), ...deps]);
|
||||
|
||||
return { loading, loadRoutes, setLoading };
|
||||
};
|
||||
@@ -12,9 +12,10 @@ export type RoutesWidgetProps = {
|
||||
routesList: RoutesList | undefined;
|
||||
loading: boolean;
|
||||
|
||||
addHubCommand: AddHubCommand;
|
||||
toggleHubCommand: ToggleHubCommand;
|
||||
addHubCommand?: AddHubCommand;
|
||||
toggleHubCommand?: ToggleHubCommand;
|
||||
isRestricted?: boolean;
|
||||
nohubsPlaceholder?: string;
|
||||
};
|
||||
|
||||
export type RoutesProviderInnerProps = RoutesWidgetProps;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { FINAL_DURATION_MS } from '../constants';
|
||||
|
||||
// Strip frontend-only fields that should never be sent to the backend.
|
||||
// "linked_system" is an object the frontend uses; the backend expects "linked_system_id" (integer)
|
||||
// which is set via a separate linkSignatureToSystem call.
|
||||
function stripFrontendFields(s: ExtendedSystemSignature) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { linked_system, pendingDeletion, pendingAddition, pendingUntil, finalTimeoutId, character_name, ...rest } =
|
||||
s as any;
|
||||
return rest;
|
||||
}
|
||||
|
||||
export function prepareUpdatePayload(
|
||||
systemId: string,
|
||||
added: ExtendedSystemSignature[],
|
||||
@@ -9,9 +19,9 @@ export function prepareUpdatePayload(
|
||||
) {
|
||||
return {
|
||||
system_id: systemId,
|
||||
added: added.map(s => ({ ...s })),
|
||||
updated: updated.map(s => ({ ...s })),
|
||||
removed: removed.map(s => ({ ...s })),
|
||||
added: added.map(stripFrontendFields),
|
||||
updated: updated.map(stripFrontendFields),
|
||||
removed: removed.map(stripFrontendFields),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSig
|
||||
|
||||
const extended = serverSigs.map(s => ({
|
||||
...s,
|
||||
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
|
||||
character_name: s.character_name ?? characters.find(c => c.eve_id === s.character_eve_id)?.name,
|
||||
})) as ExtendedSystemSignature[];
|
||||
|
||||
setSignatures(() => extended);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
@@ -21,17 +21,27 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
{row.temporary_name && <span className={clsx('text-[12px]')}>{row.temporary_name}</span>}
|
||||
|
||||
{customInfo.time_status === TimeStatus._1h && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.bottom} content="Signature marked as EOL">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{customInfo.isCrit && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as Crit">
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.bottom} content="Signature marked as Crit">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{customInfo.mass_status === MassState.verge && (
|
||||
<WdTooltipWrapper
|
||||
offset={5}
|
||||
position={TooltipPosition.bottom}
|
||||
content="Signature marked as Verge of collapse"
|
||||
>
|
||||
<div className="pi pi-exclamation-triangle text-red-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{row.type && (
|
||||
<WHClassView
|
||||
className="text-[11px]"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, ClipboardEvent, useRef } from 'react';
|
||||
import React, { useCallback, ClipboardEvent, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import {
|
||||
@@ -13,7 +13,9 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
|
||||
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
|
||||
import { useSystemStructures } from './hooks/useSystemStructures';
|
||||
import { processSnippetText } from './helpers';
|
||||
import { processSnippetText, StructureItem } from './helpers';
|
||||
import { SystemStructuresOwnersDialog } from './SystemStructuresOwnersDialog/SystemStructuresOwnersDialog';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const SystemStructures: React.FC = () => {
|
||||
const {
|
||||
@@ -24,6 +26,7 @@ export const SystemStructures: React.FC = () => {
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(labelRef, 260);
|
||||
@@ -48,6 +51,18 @@ export const SystemStructures: React.FC = () => {
|
||||
[processClipboard],
|
||||
);
|
||||
|
||||
const handleSave = (updatedStructures: StructureItem[]) => {
|
||||
handleUpdateStructures(updatedStructures)
|
||||
}
|
||||
|
||||
const handleOpenDialog = useCallback(() => {
|
||||
setShowEditDialog(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setShowEditDialog(false)
|
||||
}, [])
|
||||
|
||||
const handlePasteTimer = useCallback(async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
@@ -71,8 +86,19 @@ export const SystemStructures: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2.5">
|
||||
{structures.length > 1 && (
|
||||
<WdImgButton
|
||||
className={clsx(PrimeIcons.USER_EDIT, 'text-sky-400 hover:text-sky-200 transition duration-300')}
|
||||
onClick={handleOpenDialog}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: 'Update all structure owners',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<WdImgButton
|
||||
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
|
||||
className={clsx(PrimeIcons.CLOCK, 'text-sky-400 hover:text-sky-200 transition duration-300')}
|
||||
onClick={handlePasteTimer}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
@@ -117,6 +143,15 @@ export const SystemStructures: React.FC = () => {
|
||||
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
{showEditDialog && (
|
||||
<SystemStructuresOwnersDialog
|
||||
visible={showEditDialog}
|
||||
structures={structures}
|
||||
onClose={handleCloseDialog}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,14 @@ import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { formatToISO, statusesRequiringTimer, StructureItem, StructureStatus } from '../helpers';
|
||||
import {
|
||||
calendarDateToUtcIso,
|
||||
formatToISO,
|
||||
statusesRequiringTimer,
|
||||
StructureItem,
|
||||
StructureStatus,
|
||||
utcToCalendarDate,
|
||||
} from '../helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
@@ -72,7 +79,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
// If this is the endTime (Date from Calendar), we store as ISO or string:
|
||||
if (field === 'endTime' && val instanceof Date) {
|
||||
return { ...prev, endTime: val.toISOString() };
|
||||
return { ...prev, endTime: calendarDateToUtcIso(val) };
|
||||
}
|
||||
|
||||
return { ...prev, [field]: val };
|
||||
@@ -188,7 +195,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
Timer <br /> (Eve Time):
|
||||
</span>
|
||||
<Calendar
|
||||
value={editData.endTime ? new Date(editData.endTime) : undefined}
|
||||
value={editData.endTime ? utcToCalendarDate(editData.endTime) : undefined}
|
||||
onChange={e => handleChange('endTime', e.value ?? '')}
|
||||
showTime
|
||||
hourFormat="24"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
.systemStructuresOwnersDialog {
|
||||
|
||||
.p-dialog-content {
|
||||
background-color: var(--surface-800) !important;
|
||||
}
|
||||
|
||||
.p-dialog-header {
|
||||
background-color: var(--surface-700);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icon,
|
||||
.p-dialog-header-title {
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.p-inputtext {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ddd !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
.p-button {
|
||||
font-size: 12px !important;
|
||||
padding: 0.3rem 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import clsx from 'clsx';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { StructureItem } from '../helpers';
|
||||
|
||||
interface StructuresOwnersEditDialogProps {
|
||||
visible: boolean;
|
||||
structures: StructureItem[];
|
||||
onClose: () => void;
|
||||
onSave: (updatedStuctures: StructureItem[]) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresOwnersDialog: React.FC<StructuresOwnersEditDialogProps> = ({
|
||||
visible,
|
||||
structures,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [ownerInput, setOwnerInput] = useState('');
|
||||
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
const { show } = useToast();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
const [editData, setEditData] = useState<StructureItem[]>(structures);
|
||||
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO fix it
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
show({
|
||||
severity: 'error',
|
||||
summary: 'Failed to fetch owners',
|
||||
detail: `${err}`,
|
||||
life: 10000,
|
||||
});
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
);
|
||||
|
||||
// when user picks a corp from auto-complete
|
||||
const handleSelectOwner = (selected: { label: string; value: string }) => {
|
||||
setOwnerInput(selected.label);
|
||||
|
||||
setEditData(
|
||||
structures.map(item => {
|
||||
return { ...item, ownerName: selected.label, ownerId: selected.value };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
if (!editData) return;
|
||||
|
||||
// Get all unique owner IDs that need ticker lookup
|
||||
const allOwnerIds = editData.filter(x => x.ownerId != null).map(x => x.ownerId as string);
|
||||
|
||||
const uniqueOwnerIds = [...new Set(allOwnerIds)];
|
||||
|
||||
// Fetch all tickers in parallel
|
||||
const tickerResults = await Promise.all(
|
||||
uniqueOwnerIds.map(async ownerId => {
|
||||
try {
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: ownerId },
|
||||
});
|
||||
return { ownerId, ticker: ticker ?? '' };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ticker for ownerId:', ownerId, err);
|
||||
return { ownerId, ticker: '' };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Create a map of ownerId -> ticker for quick lookup
|
||||
const tickerMap = new Map(tickerResults.map(r => [r.ownerId, r.ticker]));
|
||||
|
||||
// Create new array with updated values (no mutation)
|
||||
const updatedStructures = editData.map(structure => {
|
||||
if (!structure.ownerId) {
|
||||
return structure;
|
||||
}
|
||||
|
||||
return {
|
||||
...structure,
|
||||
ownerTicker: tickerMap.get(structure.ownerId) ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
onSave(updatedStructures);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onHide={onClose}
|
||||
header={'Update All Structure Owners'}
|
||||
className={clsx('myStructuresOwnersDialog', 'text-stone-200 w-full max-w-md')}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-[14px]">
|
||||
<div className="flex gap-2">
|
||||
Updating the corporation name below will update all structures currently saved within the system.
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
|
||||
<span className="mt-1">Structures to update:</span>
|
||||
<ul>
|
||||
{structures &&
|
||||
structures.map((item, i) => (
|
||||
<li key={i}>
|
||||
{item.structureType || 'Unknown Type'} - {item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Owner:</span>
|
||||
<AutoComplete
|
||||
id="owner"
|
||||
value={ownerInput}
|
||||
suggestions={ownerSuggestions}
|
||||
completeMethod={searchOwners}
|
||||
minLength={3}
|
||||
delay={400}
|
||||
field="label"
|
||||
placeholder="Corporation name..."
|
||||
onChange={e => setOwnerInput(e.value)}
|
||||
onSelect={e => handleSelectOwner(e.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 mt-4">
|
||||
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -43,6 +43,29 @@ export function mapServerStructure(serverData: any): StructureItem {
|
||||
};
|
||||
}
|
||||
|
||||
export function utcToCalendarDate(utcIso: string): Date {
|
||||
// Parse ISO components manually to avoid browser quirks with
|
||||
// 6-digit microsecond precision from Elixir's :utc_datetime_usec.
|
||||
const m = utcIso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
|
||||
if (m) {
|
||||
const [, yr, mo, dy, hr, mi, sc] = m;
|
||||
return new Date(+yr, +mo - 1, +dy, +hr, +mi, +sc);
|
||||
}
|
||||
// Fallback for non-ISO strings
|
||||
const d = new Date(utcIso);
|
||||
return new Date(d.getTime() + d.getTimezoneOffset() * 60_000);
|
||||
}
|
||||
|
||||
export function calendarDateToUtcIso(localDate: Date): string {
|
||||
// Read local-time components (which represent EVE/UTC time) and
|
||||
// build the ISO string directly — no timezone arithmetic needed.
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return (
|
||||
`${localDate.getFullYear()}-${pad(localDate.getMonth() + 1)}-${pad(localDate.getDate())}` +
|
||||
`T${pad(localDate.getHours())}:${pad(localDate.getMinutes())}:${pad(localDate.getSeconds())}.000Z`
|
||||
);
|
||||
}
|
||||
|
||||
export function formatToISO(datetimeLocal: string): string {
|
||||
if (!datetimeLocal) return '';
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { useLoadRoutesBy } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { SelectItemOptionsType } from 'primereact/selectitem';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import clsx from 'clsx';
|
||||
import { RoutesByCategoryType, RoutesByScopeType, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
import { TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
export type RoutesByType = RoutesByCategoryType;
|
||||
|
||||
type WRoutesByProps = {
|
||||
type?: RoutesByType;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const ROUTES_BY_OPTIONS: SelectItemOptionsType = [
|
||||
{
|
||||
label: 'Blue Loot',
|
||||
value: 'blueLoot',
|
||||
icon: 'images/30747_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Red Loot',
|
||||
value: 'redLoot',
|
||||
icon: 'images/89219_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
icon: 'images/map.png',
|
||||
},
|
||||
{
|
||||
label: 'Turnur',
|
||||
value: 'turnur',
|
||||
icon: 'images/map.png',
|
||||
},
|
||||
{
|
||||
label: 'Security Office',
|
||||
value: 'so_cleaning',
|
||||
icon: 'images/concord-so.png',
|
||||
},
|
||||
{
|
||||
label: 'Trade Hubs',
|
||||
value: 'trade_hubs',
|
||||
icon: 'images/market.png',
|
||||
},
|
||||
];
|
||||
const ROUTES_BY_SECURITY_OPTIONS = [
|
||||
{ label: 'All', value: 'ALL' },
|
||||
{ label: 'High', value: 'HIGH' },
|
||||
];
|
||||
|
||||
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { settingsRoutesBy, settingsRoutesByUpdate },
|
||||
data,
|
||||
} = useMapRootState();
|
||||
|
||||
const criteriaType = settingsRoutesBy.type ?? type;
|
||||
const securityType = settingsRoutesBy.scope ?? 'ALL';
|
||||
const routesSettings = settingsRoutesBy.routes ?? DEFAULT_ROUTES_SETTINGS;
|
||||
const routesListBy = data.routesListBy;
|
||||
const availableRoutesBy = data.availableRoutesBy;
|
||||
|
||||
const routesByOptions = useMemo(() => {
|
||||
if (!availableRoutesBy || availableRoutesBy.length === 0) {
|
||||
return ROUTES_BY_OPTIONS;
|
||||
}
|
||||
|
||||
return ROUTES_BY_OPTIONS.filter(option => availableRoutesBy.includes(option.value as RoutesByType));
|
||||
}, [availableRoutesBy]);
|
||||
|
||||
const resolvedCriteriaType = useMemo(() => {
|
||||
const optionValues = routesByOptions.map(option => option.value as RoutesByType);
|
||||
|
||||
if (optionValues.length === 0) {
|
||||
return criteriaType;
|
||||
}
|
||||
|
||||
return optionValues.includes(criteriaType) ? criteriaType : optionValues[0];
|
||||
}, [routesByOptions, criteriaType]);
|
||||
|
||||
const loadRoutesCommand: LoadRoutesCommand = useCallback(
|
||||
async (systemId, currentRoutesSettings) => {
|
||||
await outCommand({
|
||||
type: OutCommand.getRoutesBy,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
type: resolvedCriteriaType,
|
||||
securityType: securityType === 'HIGH' ? 'high' : 'both',
|
||||
routes_settings: currentRoutesSettings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[outCommand, resolvedCriteriaType, securityType],
|
||||
);
|
||||
|
||||
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
|
||||
|
||||
const { loading: internalLoading } = useLoadRoutesBy({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
routesList: routesListBy,
|
||||
deps: [resolvedCriteriaType, securityType],
|
||||
});
|
||||
|
||||
const updateRoutesSettings = useCallback(
|
||||
(next: RoutesType) => settingsRoutesByUpdate(prev => ({ ...prev, routes: next })),
|
||||
[settingsRoutesByUpdate],
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const compactSmall = useMaxWidth(ref, 180);
|
||||
const compactMiddle = useMaxWidth(ref, 245);
|
||||
|
||||
const titleNode = useMemo(
|
||||
() => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="select-none">{title}</span>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.QUESTION_CIRCLE}
|
||||
tooltip={{
|
||||
position: TooltipPosition.top,
|
||||
content: 'Alpha map users can access only 1 route',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[title],
|
||||
);
|
||||
|
||||
return (
|
||||
<RoutesWidget
|
||||
title={titleNode}
|
||||
nohubsPlaceholder="Not found any destinations"
|
||||
renderContent={(content /*, compact*/) => (
|
||||
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
|
||||
{content}
|
||||
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
|
||||
{!compactSmall && (
|
||||
<Dropdown
|
||||
value={securityType}
|
||||
options={ROUTES_BY_SECURITY_OPTIONS}
|
||||
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, scope: e.value as RoutesByScopeType }))}
|
||||
className="w-[90px] [&_span]:!text-[12px]"
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
value={resolvedCriteriaType}
|
||||
itemTemplate={e => (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.icon && <img src={e.icon} height="18" width="18" />}
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
)}
|
||||
valueTemplate={e => {
|
||||
if (!e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (compactMiddle) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[50px]">
|
||||
{e.icon ? <img src={e.icon} height="18" width="18" /> : <span>{e.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.icon && <img src={e.icon} height="18" width="18" />}
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
options={routesByOptions}
|
||||
onChange={e => settingsRoutesByUpdate(prev => ({ ...prev, type: e.value as RoutesByCategoryType }))}
|
||||
className={clsx({
|
||||
['w-[130px]']: !compactMiddle,
|
||||
['w-[65px]']: compactMiddle,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
data={routesSettings}
|
||||
update={updateRoutesSettings}
|
||||
hubs={hubs}
|
||||
routesList={routesListBy}
|
||||
loading={internalLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { WRoutesBy } from './WRoutesBy';
|
||||
export type { RoutesByType } from './WRoutesBy';
|
||||
@@ -6,4 +6,5 @@ export * from './SystemStructures';
|
||||
export * from './WSystemKills';
|
||||
export * from './WRoutesUser';
|
||||
export * from './WRoutesPublic';
|
||||
export * from './WRoutesBy';
|
||||
export * from './CommentsWidget';
|
||||
|
||||
@@ -38,9 +38,11 @@ export const OldSettingsDialog = () => {
|
||||
localWidget: createSettings(widgetLocal, {}),
|
||||
widgets: createSettings(widgetsOld, {}),
|
||||
routes: createSettings(widgetRoutes, {}),
|
||||
routesBy: createSettings(widgetRoutes, {}),
|
||||
onTheMap: createSettings(onTheMapOld, {}),
|
||||
signaturesWidget: createSettings(signatures, {}),
|
||||
interface: createSettings(interfaceSettings, {}),
|
||||
map: createSettings(null, { viewport: { zoom: 1, x: 0, y: 0 } }),
|
||||
};
|
||||
|
||||
if (asFile) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState, OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -15,6 +15,7 @@ type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
|
||||
linked_system: string;
|
||||
k162Type: string;
|
||||
time_status: TimeStatus;
|
||||
mass_status: MassState;
|
||||
};
|
||||
|
||||
export interface MapSettingsProps {
|
||||
@@ -59,6 +60,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
custom_info: JSON.stringify({
|
||||
k162Type: values.k162Type,
|
||||
time_status: values.time_status,
|
||||
mass_status: values.mass_status,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -139,16 +141,19 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
|
||||
let k162Type = null;
|
||||
let time_status = TimeStatus._24h;
|
||||
let mass_status = MassState.normal;
|
||||
if (custom_info) {
|
||||
const customInfo = JSON.parse(custom_info);
|
||||
k162Type = customInfo.k162Type;
|
||||
time_status = customInfo.time_status;
|
||||
mass_status = customInfo.mass_status ?? MassState.normal;
|
||||
}
|
||||
|
||||
signatureForm.reset({
|
||||
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
|
||||
k162Type: k162Type,
|
||||
time_status: time_status,
|
||||
mass_status: mass_status,
|
||||
...rest,
|
||||
});
|
||||
}, [signatureForm, signatureData]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootConten
|
||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
|
||||
import { SignatureLifetimeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLifetimeSelect.tsx';
|
||||
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
|
||||
import { SignatureMassStatusSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureMassStatusSelect.tsx';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
const { watch } = useFormContext<SystemSignature>();
|
||||
@@ -34,6 +35,11 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<SignatureLifetimeSelect name="time_status" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Mass status:</span>
|
||||
<SignatureMassStatusSelect name="mass_status" />
|
||||
</div>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Temp. Name:</span>
|
||||
<SignatureTempName />
|
||||
|
||||
@@ -13,6 +13,26 @@ export const renderK162Type = (option: K162Type) => {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
|
||||
const arr = whClassName.split('_');
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
{arr.map(x => (
|
||||
<WHClassView
|
||||
key={x}
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={x}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { MassState, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { WdMassStatusSelector } from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
|
||||
|
||||
export interface SignatureMassStatusSelectProps {
|
||||
name: string;
|
||||
defaultValue?: MassState;
|
||||
}
|
||||
|
||||
export const SignatureMassStatusSelect = ({
|
||||
name,
|
||||
defaultValue = MassState.normal,
|
||||
}: SignatureMassStatusSelectProps) => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<div className="my-1">
|
||||
<Controller
|
||||
// @ts-ignore
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => {
|
||||
// @ts-ignore
|
||||
return <WdMassStatusSelector massStatus={field.value} onChangeMassStatus={e => field.onChange(e)} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,3 +2,4 @@ export * from './SignatureGroupSelect';
|
||||
export * from './SignatureGroupContent';
|
||||
export * from './SignatureK162TypeSelect';
|
||||
export * from './SignatureLifetimeSelect';
|
||||
export * from './SignatureMassStatusSelect';
|
||||
|
||||
@@ -4,12 +4,16 @@ import { DataTable } from 'primereact/datatable';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const TrackingCharactersList = () => {
|
||||
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
|
||||
const { trackingCharacters, main, following, updateTracking } = useTracking();
|
||||
const refVars = useRef({ trackingCharacters });
|
||||
refVars.current = { trackingCharacters };
|
||||
const {
|
||||
data: { expiredCharacters },
|
||||
} = useMapRootState();
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(trackingCharacters.filter(x => x.tracked));
|
||||
@@ -66,7 +70,9 @@ export const TrackingCharactersList = () => {
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
headerClassName="[&_div]:ml-2"
|
||||
body={row => {
|
||||
return <CharacterCard showCorporationLogo showTicker isOwn {...row.character} />;
|
||||
const isExpired = expiredCharacters.includes(row.character.eve_id);
|
||||
|
||||
return <CharacterCard showCorporationLogo showTicker isOwn isExpired={isExpired} {...row.character} />;
|
||||
}}
|
||||
/>
|
||||
</DataTable>
|
||||
|
||||
@@ -38,7 +38,7 @@ const TrackingDialogComp = ({ visible, onHide }: TrackingDialogProps) => {
|
||||
resizable={false}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
className="w-[640px] h-[400px] text-text-color min-h-0"
|
||||
className="w-[640px] h-[600px] text-text-color min-h-0"
|
||||
>
|
||||
<TabView
|
||||
className="vertical-tabs-container h-full [&_.p-tabview-panels]:!pr-0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
|
||||
import { createContext, useCallback, useContext, useRef, useState } from 'react';
|
||||
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useCallback } from 'react';
|
||||
import classes from './CharacterCard.module.scss';
|
||||
import { ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
|
||||
import { WdCharStateWrapper } from '../../characters/components';
|
||||
|
||||
export type CharacterCardProps = {
|
||||
compact?: boolean;
|
||||
@@ -29,6 +30,9 @@ export type CharacterCardProps = {
|
||||
showAllyLogo?: boolean;
|
||||
showAllyLogoPlaceholder?: boolean;
|
||||
simpleMode?: boolean;
|
||||
isExpired?: boolean;
|
||||
isMain?: boolean;
|
||||
isFollowing?: boolean;
|
||||
} & WithIsOwnCharacter &
|
||||
WithClassName;
|
||||
|
||||
@@ -55,6 +59,10 @@ export const CharacterCard = ({
|
||||
showTicker,
|
||||
useSystemsCache,
|
||||
className,
|
||||
isExpired,
|
||||
isMain,
|
||||
isFollowing,
|
||||
|
||||
...char
|
||||
}: CharacterCardInnerProps) => {
|
||||
const handleSelect = useCallback(() => {
|
||||
@@ -204,7 +212,22 @@ export const CharacterCard = ({
|
||||
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
|
||||
<div className="w-full flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<WdEveEntityPortrait eveId={char.eve_id} size={WdEveEntityPortraitSize.w33} />
|
||||
<WdCharStateWrapper
|
||||
eve_id={char.eve_id}
|
||||
location={char.location}
|
||||
isExpired={isExpired}
|
||||
isOnline={char.online}
|
||||
isMain={isMain}
|
||||
isFollowing={isFollowing}
|
||||
>
|
||||
<WdEveEntityPortrait
|
||||
eveId={char.eve_id}
|
||||
size={WdEveEntityPortraitSize.w33}
|
||||
className={clsx({
|
||||
['border-red-600/50']: isExpired,
|
||||
})}
|
||||
/>
|
||||
</WdCharStateWrapper>
|
||||
|
||||
{showCorporationLogo && (
|
||||
<WdTooltipWrapper position={TooltipPosition.top} content={char.corporation_name}>
|
||||
|
||||
@@ -13,7 +13,7 @@ export type SystemViewProps = {
|
||||
|
||||
export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomName, ...rest }: SystemViewProps) => {
|
||||
const memSystems = useMemo(() => [systemId], [systemId]);
|
||||
const { systems, loading } = useLoadSystemStatic({ systems: memSystems });
|
||||
const { systems, lastUpdateKey, loading } = useLoadSystemStatic({ systems: memSystems });
|
||||
|
||||
const {
|
||||
data: { systems: mapSystems },
|
||||
@@ -23,9 +23,10 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
|
||||
if (!systemId) {
|
||||
return customSystemInfo;
|
||||
}
|
||||
|
||||
return systems.get(parseInt(systemId));
|
||||
// eslint-disable-next-line
|
||||
}, [customSystemInfo, systemId, systems, loading]);
|
||||
}, [customSystemInfo, systemId, systems, lastUpdateKey, loading]);
|
||||
|
||||
const mapSystemInfo = useMemo(() => {
|
||||
if (!showCustomName) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { MassState } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
|
||||
const MASS_STATUS = [
|
||||
{
|
||||
id: MassState.verge,
|
||||
label: 'Verge',
|
||||
className: 'bg-red-400 hover:!bg-red-400',
|
||||
inactiveClassName: 'bg-red-400/30',
|
||||
description: 'Mass status: Verge of collapse',
|
||||
},
|
||||
{
|
||||
id: MassState.half,
|
||||
label: 'Half',
|
||||
className: 'bg-orange-300 hover:!bg-orange-300',
|
||||
inactiveClassName: 'bg-orange-300/30',
|
||||
description: 'Mass status: Half',
|
||||
},
|
||||
{
|
||||
id: MassState.normal,
|
||||
label: 'Normal',
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
description: 'Mass status: Normal',
|
||||
},
|
||||
];
|
||||
|
||||
export interface WdMassStatusSelectorProps {
|
||||
massStatus?: MassState;
|
||||
onChangeMassStatus(massStatus: MassState): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdMassStatusSelector = ({
|
||||
massStatus = MassState.normal,
|
||||
onChangeMassStatus,
|
||||
className,
|
||||
}: WdMassStatusSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[auto_auto_auto] gap-1', className)}>
|
||||
{MASS_STATUS.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]: massStatus !== x.id },
|
||||
x.className,
|
||||
)}
|
||||
onClick={() => onChangeMassStatus(x.id)}
|
||||
>
|
||||
{x.label}
|
||||
</WdButton>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { ShipSizeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
import {
|
||||
SHIP_SIZES_DESCRIPTION,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
|
||||
const SHIP_SIZE_STYLES: Record<ShipSizeStatus, { className: string; inactiveClassName: string }> = {
|
||||
[ShipSizeStatus.small]: {
|
||||
className: 'bg-indigo-400 hover:!bg-indigo-400',
|
||||
inactiveClassName: 'bg-indigo-400/30',
|
||||
},
|
||||
[ShipSizeStatus.medium]: {
|
||||
className: 'bg-cyan-500 hover:!bg-cyan-500',
|
||||
inactiveClassName: 'bg-cyan-500/30',
|
||||
},
|
||||
[ShipSizeStatus.large]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
[ShipSizeStatus.freight]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
[ShipSizeStatus.capital]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
};
|
||||
|
||||
export interface WdShipSizeSelectorProps {
|
||||
shipSize?: ShipSizeStatus;
|
||||
onChangeShipSize(shipSize: ShipSizeStatus): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdShipSizeSelector = ({
|
||||
shipSize = ShipSizeStatus.large,
|
||||
onChangeShipSize,
|
||||
className,
|
||||
}: WdShipSizeSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[1fr_1fr_1fr_1fr_1fr] gap-1', className)}>
|
||||
{SHIP_SIZES_NAMES_ORDER.map(size => {
|
||||
const style = SHIP_SIZE_STYLES[size];
|
||||
const tooltip = `${SHIP_SIZES_NAMES[size]} • ${SHIP_SIZES_SIZE[size]} t. ${SHIP_SIZES_DESCRIPTION[size]}`;
|
||||
|
||||
return (
|
||||
<WdButton
|
||||
key={size}
|
||||
outlined={false}
|
||||
value={SHIP_SIZES_NAMES_SHORT[size]}
|
||||
tooltip={tooltip}
|
||||
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
|
||||
size="small"
|
||||
className={clsx(
|
||||
`py-[1px] justify-center min-w-auto w-auto border-0 text-[11px] font-bold leading-[20px]`,
|
||||
{ [style.inactiveClassName]: shipSize !== size },
|
||||
style.className,
|
||||
)}
|
||||
onClick={() => onChangeShipSize(size)}
|
||||
>
|
||||
<span className="text-[11px] font-bold">{SHIP_SIZES_NAMES_SHORT[size]}</span>
|
||||
</WdButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -133,6 +133,16 @@ export const K162_TYPES: K162Type[] = [
|
||||
value: 'pochven',
|
||||
whClassName: 'F216',
|
||||
},
|
||||
{
|
||||
label: 'C1/C2/C3',
|
||||
value: 'c1_c2_c3',
|
||||
whClassName: 'E004_D382_L477',
|
||||
},
|
||||
{
|
||||
label: 'C4/C5',
|
||||
value: 'c4_c5',
|
||||
whClassName: 'M001_L614',
|
||||
},
|
||||
];
|
||||
|
||||
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
StringBoolean,
|
||||
TrackingCharacter,
|
||||
UseCharactersCacheData,
|
||||
UseCommentsData,
|
||||
@@ -28,12 +27,14 @@ import {
|
||||
MapSettings,
|
||||
MapUserSettings,
|
||||
OnTheMapSettingsType,
|
||||
RoutesByType,
|
||||
RoutesType,
|
||||
} from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import {
|
||||
DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
DEFAULT_MAP_SETTINGS,
|
||||
DEFAULT_ON_THE_MAP_SETTINGS,
|
||||
DEFAULT_ROUTES_BY_SETTINGS,
|
||||
DEFAULT_ROUTES_SETTINGS,
|
||||
DEFAULT_WIDGET_LOCAL_SETTINGS,
|
||||
STORED_INTERFACE_DEFAULT_VALUES,
|
||||
@@ -55,6 +56,7 @@ export type MapRootData = MapUnionTypes & {
|
||||
trackingCharactersData: TrackingCharacter[];
|
||||
loadingPublicRoutes: boolean;
|
||||
map_slug: string | null;
|
||||
expiredCharacters: string[];
|
||||
};
|
||||
|
||||
const INITIAL_DATA: MapRootData = {
|
||||
@@ -76,6 +78,8 @@ const INITIAL_DATA: MapRootData = {
|
||||
userHubs: [],
|
||||
routes: undefined,
|
||||
userRoutes: undefined,
|
||||
routesListBy: undefined,
|
||||
availableRoutesBy: [],
|
||||
kills: [],
|
||||
connections: [],
|
||||
detailedKills: {},
|
||||
@@ -99,6 +103,7 @@ const INITIAL_DATA: MapRootData = {
|
||||
pings: [],
|
||||
loadingPublicRoutes: false,
|
||||
map_slug: null,
|
||||
expiredCharacters: [],
|
||||
};
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
@@ -132,6 +137,8 @@ export interface MapRootContextProps {
|
||||
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
|
||||
settingsRoutes: RoutesType;
|
||||
settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>;
|
||||
settingsRoutesBy: RoutesByType;
|
||||
settingsRoutesByUpdate: Dispatch<SetStateAction<RoutesByType>>;
|
||||
settingsLocal: LocalWidgetSettings;
|
||||
settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>;
|
||||
settingsSignatures: SignatureSettingsType;
|
||||
@@ -179,6 +186,8 @@ const MapRootContext = createContext<MapRootContextProps>({
|
||||
setInterfaceSettings: () => null,
|
||||
settingsRoutes: DEFAULT_ROUTES_SETTINGS,
|
||||
settingsRoutesUpdate: () => null,
|
||||
settingsRoutesBy: { ...DEFAULT_ROUTES_BY_SETTINGS, routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes } },
|
||||
settingsRoutesByUpdate: () => null,
|
||||
settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS,
|
||||
settingsLocalUpdate: () => null,
|
||||
settingsSignatures: DEFAULT_SIGNATURE_SETTINGS,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MiniMapPlacement,
|
||||
OnTheMapSettingsType,
|
||||
PingsPlacement,
|
||||
RoutesByType,
|
||||
RoutesType,
|
||||
} from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_WIDGETS, STORED_VISIBLE_WIDGETS_DEFAULT } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
@@ -43,6 +44,12 @@ export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
|
||||
showShipName: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_ROUTES_BY_SETTINGS: RoutesByType = {
|
||||
routes: DEFAULT_ROUTES_SETTINGS,
|
||||
scope: 'ALL',
|
||||
type: 'blueLoot',
|
||||
};
|
||||
|
||||
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
|
||||
hideOffline: false,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
DEFAULT_MAP_SETTINGS,
|
||||
DEFAULT_ON_THE_MAP_SETTINGS,
|
||||
DEFAULT_ROUTES_BY_SETTINGS,
|
||||
DEFAULT_ROUTES_SETTINGS,
|
||||
DEFAULT_WIDGET_LOCAL_SETTINGS,
|
||||
getDefaultWidgetProps,
|
||||
@@ -17,6 +18,11 @@ export const createWidgetSettings = <T>(settings: T) => {
|
||||
};
|
||||
|
||||
export const createDefaultStoredSettings = (): MapUserSettings => {
|
||||
const defaultRoutesBy = {
|
||||
...DEFAULT_ROUTES_BY_SETTINGS,
|
||||
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
|
||||
};
|
||||
|
||||
return {
|
||||
version: STORED_SETTINGS_VERSION,
|
||||
migratedFromOld: false,
|
||||
@@ -24,6 +30,7 @@ export const createDefaultStoredSettings = (): MapUserSettings => {
|
||||
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
|
||||
widgets: createWidgetSettings(getDefaultWidgetProps()),
|
||||
routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS),
|
||||
routesBy: createWidgetSettings(defaultRoutesBy),
|
||||
onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS),
|
||||
signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS),
|
||||
interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES),
|
||||
@@ -43,6 +50,11 @@ export const getDefaultSettingsByType = (type: SettingsTypes): SettingsWrapper<a
|
||||
return createWidgetSettings(getDefaultWidgetProps());
|
||||
case SettingsTypes.routes:
|
||||
return createWidgetSettings(DEFAULT_ROUTES_SETTINGS);
|
||||
case SettingsTypes.routesBy:
|
||||
return createWidgetSettings({
|
||||
...DEFAULT_ROUTES_BY_SETTINGS,
|
||||
routes: { ...DEFAULT_ROUTES_BY_SETTINGS.routes },
|
||||
});
|
||||
case SettingsTypes.onTheMap:
|
||||
return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS);
|
||||
case SettingsTypes.signaturesWidget:
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './useCommandComments';
|
||||
export * from './useGetCacheCharacter';
|
||||
export * from './useCommandsActivity';
|
||||
export * from './useCommandPings';
|
||||
export * from './useCommandPingBlocked';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
||||
import { CommandPingBlocked } from '@/hooks/Mapper/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useCommandPingBlocked = () => {
|
||||
const { show } = useToast();
|
||||
|
||||
const pingBlocked = useCallback(
|
||||
({ message }: CommandPingBlocked) => {
|
||||
show({
|
||||
severity: 'warn',
|
||||
summary: 'Cannot create ping',
|
||||
detail: message,
|
||||
life: 5000,
|
||||
});
|
||||
},
|
||||
[show],
|
||||
);
|
||||
|
||||
return { pingBlocked };
|
||||
};
|
||||
@@ -14,8 +14,8 @@ export const useCommandPings = () => {
|
||||
ref.current.update({ pings });
|
||||
}, []);
|
||||
|
||||
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => {
|
||||
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type);
|
||||
const pingCancelled = useCallback(({ id }: CommandPingCancelled) => {
|
||||
const newPings = ref.current.pings.filter(x => x.id !== id);
|
||||
ref.current.update({ pings: newPings });
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ export const useMapInit = () => {
|
||||
user_permissions,
|
||||
options,
|
||||
is_subscription_active,
|
||||
available_routes_by,
|
||||
main_character_eve_id,
|
||||
following_character_eve_id,
|
||||
user_hubs,
|
||||
map_slug,
|
||||
expired_characters,
|
||||
} = props;
|
||||
|
||||
const updateData: Partial<MapRootData> = {};
|
||||
@@ -85,6 +87,10 @@ export const useMapInit = () => {
|
||||
updateData.isSubscriptionActive = is_subscription_active;
|
||||
}
|
||||
|
||||
if (available_routes_by) {
|
||||
updateData.availableRoutesBy = available_routes_by;
|
||||
}
|
||||
|
||||
if (system_static_infos) {
|
||||
system_static_infos.forEach(static_info => {
|
||||
addSystemStatic(static_info);
|
||||
@@ -103,6 +109,10 @@ export const useMapInit = () => {
|
||||
updateData.map_slug = map_slug;
|
||||
}
|
||||
|
||||
if ('expired_characters' in props) {
|
||||
updateData.expiredCharacters = expired_characters;
|
||||
}
|
||||
|
||||
update(updateData);
|
||||
},
|
||||
[update, addSystemStatic],
|
||||
|
||||
@@ -112,3 +112,23 @@ export const useUserRoutes = () => {
|
||||
update({ userRoutes: value });
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useRoutesListBy = () => {
|
||||
const {
|
||||
update,
|
||||
data: { routesListBy },
|
||||
} = useMapRootState();
|
||||
|
||||
const ref = useRef({ update, routesListBy });
|
||||
ref.current = { update, routesListBy };
|
||||
|
||||
return useCallback((value: CommandRoutes) => {
|
||||
const { update, routesListBy } = ref.current;
|
||||
|
||||
if (areRoutesListsEqual(routesListBy, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update({ routesListBy: value });
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,6 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
||||
|
||||
const removeComment = useCallback((systemId: number, commentId: string) => {
|
||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||
console.log('cSystem', cSystem);
|
||||
if (!cSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPingAdded,
|
||||
CommandPingBlocked,
|
||||
CommandPingCancelled,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
@@ -29,6 +30,7 @@ import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
useCommandComments,
|
||||
useCommandPingBlocked,
|
||||
useCommandPings,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
@@ -36,6 +38,7 @@ import {
|
||||
useMapInit,
|
||||
useMapUpdated,
|
||||
useRoutes,
|
||||
useRoutesListBy,
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
@@ -59,8 +62,10 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapUpdated = useMapUpdated();
|
||||
const mapRoutes = useRoutes();
|
||||
const mapUserRoutes = useUserRoutes();
|
||||
const mapRoutesListBy = useRoutesListBy();
|
||||
const { addComment, removeComment } = useCommandComments();
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { pingBlocked } = useCommandPingBlocked();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
@@ -112,6 +117,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.routesListBy:
|
||||
mapRoutesListBy(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
@@ -172,6 +180,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingBlocked:
|
||||
pingBlocked(data as CommandPingBlocked);
|
||||
break;
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -56,6 +56,12 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
map_slug,
|
||||
'routes',
|
||||
);
|
||||
const [settingsRoutesBy, settingsRoutesByUpdate] = useSettingsValueAndSetter(
|
||||
mapUserSettings,
|
||||
setMapUserSettings,
|
||||
map_slug,
|
||||
'routesBy',
|
||||
);
|
||||
|
||||
const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter(
|
||||
mapUserSettings,
|
||||
@@ -188,6 +194,8 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
setInterfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsRoutesUpdate,
|
||||
settingsRoutesBy,
|
||||
settingsRoutesByUpdate,
|
||||
settingsLocal,
|
||||
settingsLocalUpdate,
|
||||
settingsSignatures,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { to_1 } from './to_1.ts';
|
||||
import { to_2 } from './to_2.ts';
|
||||
import { to_3 } from './to_3.ts';
|
||||
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export default [to_1, to_2] as MigrationStructure[];
|
||||
export default [to_1, to_2, to_3] as MigrationStructure[];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_ROUTES_BY_SETTINGS, DEFAULT_ROUTES_SETTINGS } from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
|
||||
export const to_3: MigrationStructure = {
|
||||
to: 3,
|
||||
up: (prev: any) => {
|
||||
const rawRoutesBy = prev?.routesBy;
|
||||
const hasStructuredRoutesBy =
|
||||
rawRoutesBy && typeof rawRoutesBy === 'object' && 'routes' in rawRoutesBy;
|
||||
|
||||
const routes = hasStructuredRoutesBy
|
||||
? { ...DEFAULT_ROUTES_SETTINGS, ...rawRoutesBy.routes }
|
||||
: { ...DEFAULT_ROUTES_SETTINGS, ...(rawRoutesBy ?? prev?.routes ?? {}) };
|
||||
|
||||
const scopeRaw = hasStructuredRoutesBy ? rawRoutesBy?.scope : undefined;
|
||||
const scope = scopeRaw === 'HIGH' ? 'HIGH' : 'ALL';
|
||||
|
||||
const type = hasStructuredRoutesBy && rawRoutesBy?.type ? rawRoutesBy.type : DEFAULT_ROUTES_BY_SETTINGS.type;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
routesBy: {
|
||||
...DEFAULT_ROUTES_BY_SETTINGS,
|
||||
...(hasStructuredRoutesBy ? rawRoutesBy : {}),
|
||||
scope,
|
||||
type,
|
||||
routes,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -47,6 +47,22 @@ export type RoutesType = {
|
||||
avoid: number[];
|
||||
};
|
||||
|
||||
export type RoutesByCategoryType =
|
||||
| 'blueLoot'
|
||||
| 'redLoot'
|
||||
| 'thera'
|
||||
| 'turnur'
|
||||
| 'so_cleaning'
|
||||
| 'trade_hubs';
|
||||
|
||||
export type RoutesByScopeType = 'ALL' | 'HIGH';
|
||||
|
||||
export type RoutesByType = {
|
||||
routes: RoutesType;
|
||||
scope: RoutesByScopeType;
|
||||
type: RoutesByCategoryType;
|
||||
};
|
||||
|
||||
export type LocalWidgetSettings = {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
@@ -79,6 +95,7 @@ export type MapUserSettings = {
|
||||
interface: SettingsWrapper<InterfaceStoredSettings>;
|
||||
onTheMap: SettingsWrapper<OnTheMapSettingsType>;
|
||||
routes: SettingsWrapper<RoutesType>;
|
||||
routesBy: SettingsWrapper<RoutesByType>;
|
||||
localWidget: SettingsWrapper<LocalWidgetSettings>;
|
||||
signaturesWidget: SettingsWrapper<SignatureSettingsType>;
|
||||
killsWidget: SettingsWrapper<KillsWidgetSettings>;
|
||||
@@ -98,6 +115,7 @@ export enum SettingsTypes {
|
||||
localWidget = 'localWidget',
|
||||
widgets = 'widgets',
|
||||
routes = 'routes',
|
||||
routesBy = 'routesBy',
|
||||
onTheMap = 'onTheMap',
|
||||
signaturesWidget = 'signaturesWidget',
|
||||
interface = 'interface',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const STORED_SETTINGS_VERSION = 2;
|
||||
export const STORED_SETTINGS_VERSION = 3;
|
||||
|
||||
export const LS_KEY_LEGASY = 'map-user-settings';
|
||||
export const LS_KEY = 'map-user-settings-v3';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Ma
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
|
||||
@@ -25,6 +26,7 @@ export enum Commands {
|
||||
detailedKillsUpdated = 'detailed_kills_updated',
|
||||
routes = 'routes',
|
||||
userRoutes = 'user_routes',
|
||||
routesListBy = 'routes_list_by',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
selectSystems = 'select_systems',
|
||||
@@ -41,6 +43,7 @@ export enum Commands {
|
||||
refreshTrackingData = 'refresh_tracking_data',
|
||||
pingAdded = 'ping_added',
|
||||
pingCancelled = 'ping_cancelled',
|
||||
pingBlocked = 'ping_blocked',
|
||||
}
|
||||
|
||||
export type Command =
|
||||
@@ -61,6 +64,7 @@ export type Command =
|
||||
| Commands.detailedKillsUpdated
|
||||
| Commands.routes
|
||||
| Commands.userRoutes
|
||||
| Commands.routesListBy
|
||||
| Commands.selectSystem
|
||||
| Commands.selectSystems
|
||||
| Commands.centerSystem
|
||||
@@ -77,7 +81,8 @@ export type Command =
|
||||
| Commands.showTracking
|
||||
| Commands.refreshTrackingData
|
||||
| Commands.pingAdded
|
||||
| Commands.pingCancelled;
|
||||
| Commands.pingCancelled
|
||||
| Commands.pingBlocked;
|
||||
|
||||
export type CommandInit = {
|
||||
systems: SolarSystemRawType[];
|
||||
@@ -99,9 +104,11 @@ export type CommandInit = {
|
||||
options: MapOptions;
|
||||
reset?: boolean;
|
||||
is_subscription_active?: boolean;
|
||||
available_routes_by?: RoutesByCategoryType[];
|
||||
main_character_eve_id?: string | null;
|
||||
following_character_eve_id?: string | null;
|
||||
map_slug?: string;
|
||||
expired_characters: string[];
|
||||
};
|
||||
|
||||
export type CommandAddSystems = SolarSystemRawType[];
|
||||
@@ -119,6 +126,7 @@ export type CommandSignaturesUpdated = string;
|
||||
export type CommandMapUpdated = Partial<CommandInit>;
|
||||
export type CommandRoutes = RoutesList;
|
||||
export type CommandUserRoutes = RoutesList;
|
||||
export type CommandRoutesListBy = RoutesList;
|
||||
export type CommandKillsUpdated = Kill[];
|
||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
|
||||
export type CommandSelectSystem = string | undefined;
|
||||
@@ -161,6 +169,10 @@ export type CommandUpdateTracking = {
|
||||
};
|
||||
export type CommandPingAdded = PingData[];
|
||||
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
|
||||
export type CommandPingBlocked = {
|
||||
reason: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface UserSettings {
|
||||
primaryCharacterId?: string;
|
||||
@@ -193,6 +205,7 @@ export interface CommandData {
|
||||
[Commands.mapUpdated]: CommandMapUpdated;
|
||||
[Commands.routes]: CommandRoutes;
|
||||
[Commands.userRoutes]: CommandUserRoutes;
|
||||
[Commands.routesListBy]: CommandRoutesListBy;
|
||||
[Commands.killsUpdated]: CommandKillsUpdated;
|
||||
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
|
||||
[Commands.selectSystem]: CommandSelectSystem;
|
||||
@@ -212,6 +225,7 @@ export interface CommandData {
|
||||
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
|
||||
[Commands.pingAdded]: CommandPingAdded;
|
||||
[Commands.pingCancelled]: CommandPingCancelled;
|
||||
[Commands.pingBlocked]: CommandPingBlocked;
|
||||
}
|
||||
|
||||
export interface MapHandlers {
|
||||
@@ -225,6 +239,7 @@ export enum OutCommand {
|
||||
deleteUserHub = 'delete_user_hub',
|
||||
getRoutes = 'get_routes',
|
||||
getUserRoutes = 'get_user_routes',
|
||||
getRoutesBy = 'get_routes_by',
|
||||
getCharacterJumps = 'get_character_jumps',
|
||||
getStructures = 'get_structures',
|
||||
getSignatures = 'get_signatures',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { RoutesByCategoryType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export type MapUnionTypes = {
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
@@ -20,6 +21,8 @@ export type MapUnionTypes = {
|
||||
systemSignatures: Record<string, SystemSignature[]>;
|
||||
routes?: RoutesList;
|
||||
userRoutes?: RoutesList;
|
||||
routesListBy?: RoutesList;
|
||||
availableRoutesBy?: RoutesByCategoryType[];
|
||||
kills: Record<number, number>;
|
||||
connections: SolarSystemConnection[];
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
|
||||
@@ -13,12 +13,19 @@ export type SystemStaticInfoShort = Pick<
|
||||
|
||||
type MappedSystem = SolarSystemStaticInfoRaw | undefined;
|
||||
|
||||
export type RouteStationSummary = {
|
||||
station_id: number;
|
||||
station_name: string;
|
||||
special?: boolean;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
destination: number;
|
||||
has_connection: boolean;
|
||||
origin: number;
|
||||
systems?: number[];
|
||||
mapped_systems?: MappedSystem[];
|
||||
stations?: RouteStationSummary[];
|
||||
success?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
time_status?: number;
|
||||
isCrit?: boolean;
|
||||
mass_status?: number;
|
||||
};
|
||||
|
||||
export type SystemSignature = {
|
||||
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
};
|
||||
|
||||
refreshZone.addEventListener('click', handleUpdate);
|
||||
refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
// refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
|
||||
this.updated();
|
||||
},
|
||||
|
||||
BIN
assets/static/images/news/2026/01-01-roadmap/cover.webp
Normal file
BIN
assets/static/images/news/2026/01-01-roadmap/cover.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/static/images/news/2026/01-05-weekly-giveaway/cover.webp
Normal file
BIN
assets/static/images/news/2026/01-05-weekly-giveaway/cover.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
@@ -92,6 +92,31 @@ map_subscription_extra_hubs_10_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
||||
|
||||
# Parse promo codes from environment variable
|
||||
# Format: "CODE1:10,CODE2:20" where numbers are discount percentages
|
||||
promo_codes =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_PROMO_CODES", "")
|
||||
|> case do
|
||||
"" ->
|
||||
%{}
|
||||
|
||||
codes_string ->
|
||||
codes_string
|
||||
|> String.split(",")
|
||||
|> Enum.map(fn entry ->
|
||||
case String.split(String.trim(entry), ":") do
|
||||
[code, discount] ->
|
||||
{String.upcase(String.trim(code)), String.to_integer(String.trim(discount))}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
map_connection_auto_expire_hours =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
|
||||
@@ -176,7 +201,8 @@ config :wanderer_app,
|
||||
}
|
||||
],
|
||||
extra_characters_50: map_subscription_extra_characters_50_price,
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price,
|
||||
promo_codes: promo_codes
|
||||
},
|
||||
# Finch pool configuration - separate pools for different services
|
||||
# ESI Character Tracking pool - high capacity for bulk character operations
|
||||
|
||||
@@ -39,6 +39,8 @@ defmodule WandererApp.Api.Character do
|
||||
define(:active_by_user,
|
||||
action: :active_by_user
|
||||
)
|
||||
|
||||
define(:admin_all, action: :admin_all)
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -69,6 +71,10 @@ defmodule WandererApp.Api.Character do
|
||||
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
||||
end
|
||||
|
||||
read :admin_all do
|
||||
prepare build(load: [:user])
|
||||
end
|
||||
|
||||
read :last_active do
|
||||
argument(:from, :utc_datetime, allow_nil?: false)
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ defmodule WandererApp.Api.Map do
|
||||
)
|
||||
|
||||
define(:duplicate, action: :duplicate)
|
||||
define(:admin_all, action: :admin_all)
|
||||
define(:restore, action: :restore)
|
||||
end
|
||||
|
||||
calculations do
|
||||
@@ -107,6 +109,12 @@ defmodule WandererApp.Api.Map do
|
||||
prepare WandererApp.Api.Preparations.FilterMapsByRoles
|
||||
end
|
||||
|
||||
read :admin_all do
|
||||
# Admin-only action that bypasses FilterMapsByRoles
|
||||
# Returns ALL maps including soft-deleted ones with owner and ACLs loaded
|
||||
prepare build(load: [:owner, :acls])
|
||||
end
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
:name,
|
||||
@@ -194,6 +202,14 @@ defmodule WandererApp.Api.Map do
|
||||
change(set_attribute(:deleted, true))
|
||||
end
|
||||
|
||||
update :restore do
|
||||
# Admin-only action to restore a soft-deleted map
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:deleted, false))
|
||||
end
|
||||
|
||||
update :update_api_key do
|
||||
accept [:public_api_key]
|
||||
require_atomic? false
|
||||
@@ -202,6 +218,11 @@ defmodule WandererApp.Api.Map do
|
||||
update :toggle_webhooks do
|
||||
accept [:webhooks_enabled]
|
||||
require_atomic? false
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.Map.update_webhooks_enabled(record.id, record.webhooks_enabled)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
update :toggle_sse do
|
||||
@@ -210,6 +231,11 @@ defmodule WandererApp.Api.Map do
|
||||
|
||||
# Validate subscription when enabling SSE
|
||||
validate &validate_sse_subscription/2
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.Map.update_sse_enabled(record.id, record.sse_enabled)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
|
||||
@@ -80,6 +80,10 @@ defmodule WandererApp.Api.MapPing do
|
||||
|
||||
filter(expr(inserted_at <= ^arg(:inserted_before)))
|
||||
end
|
||||
|
||||
# Admin action for cleanup - no actor filtering
|
||||
read :all_pings do
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -123,7 +123,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:group,
|
||||
:type,
|
||||
:custom_info,
|
||||
:deleted
|
||||
:deleted,
|
||||
:linked_system_id
|
||||
]
|
||||
end
|
||||
|
||||
@@ -140,7 +141,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:type,
|
||||
:custom_info,
|
||||
:deleted,
|
||||
:update_forced_at
|
||||
:update_forced_at,
|
||||
:linked_system_id
|
||||
]
|
||||
|
||||
primary? true
|
||||
|
||||
@@ -5,6 +5,8 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_transactions_v1")
|
||||
@@ -19,6 +21,7 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
define(:by_map, action: :by_map)
|
||||
define(:by_user, action: :by_user)
|
||||
define(:create, action: :create)
|
||||
define(:top_donators, action: :top_donators)
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -45,6 +48,35 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
argument(:user_id, :uuid, allow_nil?: false)
|
||||
filter(expr(user_id == ^arg(:user_id)))
|
||||
end
|
||||
|
||||
action :top_donators, {:array, :struct} do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
argument(:after, :utc_datetime, allow_nil?: true)
|
||||
|
||||
run fn input, _context ->
|
||||
base =
|
||||
from(t in __MODULE__,
|
||||
where:
|
||||
t.map_id == ^input.arguments.map_id and
|
||||
t.type == :in and
|
||||
not is_nil(t.user_id),
|
||||
group_by: [t.user_id],
|
||||
select: %{user_id: t.user_id, total_amount: sum(t.amount)},
|
||||
order_by: [desc: sum(t.amount)],
|
||||
limit: 10
|
||||
)
|
||||
|
||||
query =
|
||||
case input.arguments[:after] do
|
||||
nil -> base
|
||||
after_date -> base |> where([t], t.inserted_at >= ^after_date)
|
||||
end
|
||||
|
||||
query
|
||||
|> WandererApp.Repo.all()
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -45,7 +45,17 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
:active?
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
defaults [:read]
|
||||
|
||||
# Custom destroy to invalidate cache
|
||||
destroy :destroy do
|
||||
require_atomic? false
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
update :update do
|
||||
accept [
|
||||
@@ -60,6 +70,12 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
]
|
||||
|
||||
require_atomic? false
|
||||
|
||||
# Invalidate cache when subscription is updated
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
read :by_map do
|
||||
@@ -124,6 +140,12 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
secret = generate_webhook_secret()
|
||||
Ash.Changeset.force_change_attribute(changeset, :secret, secret)
|
||||
end
|
||||
|
||||
# Invalidate cache when subscription is created
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
|
||||
update :rotate_secret do
|
||||
@@ -134,6 +156,11 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
new_secret = generate_webhook_secret()
|
||||
Ash.Changeset.change_attribute(changeset, :secret, new_secret)
|
||||
end
|
||||
|
||||
change after_action(fn _changeset, record, _context ->
|
||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
||||
{:ok, record}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@ defmodule WandererApp.Application do
|
||||
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
|
||||
id: :wanderer_app_cache_worker
|
||||
),
|
||||
# Cache for webhook subscriptions - 5 minute TTL to reduce DB load
|
||||
Supervisor.child_spec(
|
||||
{Cachex, name: :webhook_subscriptions_cache, default_ttl: :timer.minutes(5)},
|
||||
id: :webhook_subscriptions_cache_worker
|
||||
),
|
||||
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
@@ -112,6 +117,7 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Scheduler,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
WandererApp.Server.TurnurDataFetcher,
|
||||
{WandererApp.Character.TrackerPoolSupervisor, []},
|
||||
{WandererApp.Map.MapPoolSupervisor, []},
|
||||
WandererApp.Character.TrackerManager,
|
||||
|
||||
@@ -265,6 +265,10 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"[Tracker] update_online skipped for character #{character_id} - no valid access token"
|
||||
end)
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
@@ -601,6 +605,10 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"[Tracker] update_location skipped for character #{character_id} - no valid access token"
|
||||
end)
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
@@ -731,6 +739,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: nil})
|
||||
end
|
||||
@@ -769,6 +785,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id})
|
||||
|
||||
@@ -823,6 +847,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
}}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{corporation_id: corporation_id})
|
||||
|
||||
|
||||
@@ -215,8 +215,11 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
end
|
||||
|
||||
# Check if a character has permission to be tracked on a map
|
||||
defp check_character_tracking_permission(character, map_id) do
|
||||
@doc """
|
||||
Checks if a character has permission to be tracked on a map.
|
||||
Returns {:ok, :allowed} or {:error, reason}.
|
||||
"""
|
||||
def check_character_tracking_permission(character, map_id) do
|
||||
with {:ok, %{acls: acls, owner_id: owner_id}} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
|
||||
@@ -42,6 +42,35 @@ defmodule WandererApp.Env do
|
||||
def corp_eve_id(), do: get_key(:corp_id, -1)
|
||||
def subscription_settings(), do: get_key(:subscription_settings)
|
||||
|
||||
@doc """
|
||||
Returns the promo code configuration map.
|
||||
Keys are uppercase code strings, values are discount percentages.
|
||||
"""
|
||||
def promo_codes() do
|
||||
case subscription_settings() do
|
||||
%{promo_codes: codes} when is_map(codes) -> codes
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a promo code and returns the discount percentage.
|
||||
Returns {:ok, discount_percent} if valid, {:error, :invalid_code} otherwise.
|
||||
Codes are case-insensitive.
|
||||
"""
|
||||
def validate_promo_code(nil), do: {:error, :invalid_code}
|
||||
def validate_promo_code(""), do: {:error, :invalid_code}
|
||||
|
||||
def validate_promo_code(code) when is_binary(code) do
|
||||
normalized = String.upcase(String.trim(code))
|
||||
|
||||
case Map.get(promo_codes(), normalized) do
|
||||
nil -> {:error, :invalid_code}
|
||||
discount when is_integer(discount) and discount > 0 and discount <= 100 -> {:ok, discount}
|
||||
_ -> {:error, :invalid_code}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
|
||||
@@ -754,6 +754,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
new_expires_at: token.expires_at
|
||||
)
|
||||
|
||||
# Clear any previous invalid_grant failure counter on successful refresh
|
||||
WandererApp.Cache.delete("character:#{character_id}:invalid_grant_count")
|
||||
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
@@ -786,12 +789,12 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error during token refresh",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
# Track consecutive invalid_grant failures before permanently invalidating tokens.
|
||||
# EVE SSO can return invalid_grant for transient server issues, so we require
|
||||
# 3 consecutive failures before wiping tokens.
|
||||
fail_key = "character:#{character_id}:invalid_grant_count"
|
||||
count = WandererApp.Cache.lookup!(fail_key, 0) + 1
|
||||
WandererApp.Cache.put(fail_key, count, ttl: :timer.hours(2))
|
||||
|
||||
# Emit telemetry for token refresh failures
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
@@ -800,8 +803,28 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :invalid_grant}
|
||||
if count >= 3 do
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, invalidating tokens)",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete(fail_key)
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :invalid_grant}
|
||||
else
|
||||
Logger.warning(
|
||||
"TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, deferring invalidation)",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
@@ -833,20 +856,44 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{} = error},
|
||||
character,
|
||||
_character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
_scopes
|
||||
) do
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
{:error, :invalid_grant}
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Transient OAuth2 error during token refresh",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
time_since_expiry_seconds: time_since_expiry
|
||||
)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
character_id: character_id,
|
||||
error_type: "oauth2_error",
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(error, character, character_id, expires_at, scopes) do
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :failed}
|
||||
defp handle_refresh_token_result(error, _character, character_id, expires_at, _scopes) do
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Unexpected error during token refresh",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
time_since_expiry_seconds: time_since_expiry
|
||||
)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
character_id: character_id,
|
||||
error_type: "unexpected_error",
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
|
||||
defp invalidate_character_tokens(character, character_id, expires_at, scopes) do
|
||||
@@ -854,7 +901,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
with {:ok, _} <- WandererApp.Api.Character.update(character, attrs) do
|
||||
WandererApp.Character.update_character(character_id, attrs)
|
||||
:ok
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
|
||||
@@ -865,5 +911,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
alias WandererApp.Utils.JSONUtil
|
||||
|
||||
@eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||
# @eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||
@eve_db_dump_url "https://wanderer-industries.github.io/wanderer-assets/sde-files"
|
||||
|
||||
@dump_file_names [
|
||||
"invGroups.csv",
|
||||
|
||||
@@ -2,9 +2,11 @@ defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
@moduledoc """
|
||||
Handles SSE access control checks including subscription validation.
|
||||
|
||||
Note: Community Edition mode is automatically handled by the
|
||||
WandererApp.Map.is_subscription_active?/1 function, which returns
|
||||
{:ok, true} when subscriptions are disabled globally.
|
||||
IMPORTANT: This module is optimized for high-frequency calls during event delivery.
|
||||
All checks use cached data to avoid database queries on every event.
|
||||
|
||||
Note: Community Edition mode is automatically handled - when subscriptions are
|
||||
disabled globally, we skip the subscription check entirely.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
@@ -15,16 +17,14 @@ defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
- {:error, reason} if SSE is not allowed
|
||||
|
||||
Checks in order:
|
||||
1. Global SSE enabled (config)
|
||||
2. Map exists
|
||||
3. Map SSE enabled (per-map setting)
|
||||
4. Subscription active (CE mode handled internally)
|
||||
1. Global SSE enabled (config check - no DB)
|
||||
2. Map SSE enabled (cache check - no DB)
|
||||
3. Subscription active (cache check or skipped in CE mode - no DB)
|
||||
"""
|
||||
def sse_allowed?(map_id) do
|
||||
with :ok <- check_sse_globally_enabled(),
|
||||
{:ok, map} <- fetch_map(map_id),
|
||||
:ok <- check_map_sse_enabled(map),
|
||||
:ok <- check_subscription_or_ce(map_id) do
|
||||
:ok <- check_map_sse_enabled_cached(map_id),
|
||||
:ok <- check_subscription_or_ce_cached(map_id) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -37,31 +37,47 @@ defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the map by ID.
|
||||
# Returns {:ok, map} or {:error, :map_not_found}
|
||||
defp fetch_map(map_id) do
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, _map} = result -> result
|
||||
_ -> {:error, :map_not_found}
|
||||
# Uses the map cache with fallback to DB query
|
||||
defp check_map_sse_enabled_cached(map_id) do
|
||||
case WandererApp.Map.sse_enabled_with_status(map_id) do
|
||||
{:ok, true} -> :ok
|
||||
{:ok, false} -> {:error, :sse_disabled_for_map}
|
||||
{:error, :not_found} -> {:error, :map_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_map_sse_enabled(map) do
|
||||
if map.sse_enabled do
|
||||
# Checks subscription status using cached data.
|
||||
# In CE mode (subscriptions disabled globally), this is a fast config check.
|
||||
# In Enterprise mode, uses cached map state's subscription settings.
|
||||
defp check_subscription_or_ce_cached(map_id) do
|
||||
# Fast path: CE mode - subscriptions disabled globally
|
||||
if not WandererApp.Env.map_subscriptions_enabled?() do
|
||||
:ok
|
||||
else
|
||||
{:error, :sse_disabled_for_map}
|
||||
# Enterprise mode: check cached subscription status from map state
|
||||
check_subscription_from_cache(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if map has active subscription or if running Community Edition.
|
||||
#
|
||||
# Returns :ok if:
|
||||
# - Community Edition (handled internally by is_subscription_active?/1), OR
|
||||
# - Map has active subscription
|
||||
#
|
||||
# Returns {:error, :subscription_required} if subscription check fails.
|
||||
defp check_subscription_or_ce(map_id) do
|
||||
# Checks subscription status from the map cache.
|
||||
# Falls back to DB query only if cache miss.
|
||||
defp check_subscription_from_cache(map_id) do
|
||||
case WandererApp.Map.subscription_active_cached?(map_id) do
|
||||
{:ok, true} ->
|
||||
:ok
|
||||
|
||||
{:ok, false} ->
|
||||
{:error, :subscription_required}
|
||||
|
||||
{:error, :not_cached} ->
|
||||
# Cache miss - fall back to DB check
|
||||
# This should be rare as maps are initialized when accessed
|
||||
fallback_subscription_check(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Fallback to DB query - only used when cache miss
|
||||
defp fallback_subscription_check(map_id) do
|
||||
case WandererApp.Map.is_subscription_active?(map_id) do
|
||||
{:ok, true} -> :ok
|
||||
{:ok, false} -> {:error, :subscription_required}
|
||||
|
||||
@@ -166,6 +166,37 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
||||
end
|
||||
|
||||
defp get_active_subscriptions(map_id) do
|
||||
# Use cache to avoid DB query on every event
|
||||
cache_key = "map:#{map_id}"
|
||||
|
||||
case Cachex.get(:webhook_subscriptions_cache, cache_key) do
|
||||
{:ok, nil} ->
|
||||
# Cache miss - fetch from DB and cache
|
||||
fetch_and_cache_subscriptions(map_id, cache_key)
|
||||
|
||||
{:ok, subscriptions} ->
|
||||
# Cache hit
|
||||
{:ok, subscriptions}
|
||||
|
||||
{:error, _reason} ->
|
||||
# Cache error - fall back to DB
|
||||
fetch_subscriptions_from_db(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_and_cache_subscriptions(map_id, cache_key) do
|
||||
case fetch_subscriptions_from_db(map_id) do
|
||||
{:ok, subscriptions} = result ->
|
||||
# Cache for 5 minutes (TTL set on cache, but explicit here for clarity)
|
||||
Cachex.put(:webhook_subscriptions_cache, cache_key, subscriptions)
|
||||
result
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_subscriptions_from_db(map_id) do
|
||||
try do
|
||||
subscriptions = MapWebhookSubscription.active_by_map!(map_id)
|
||||
{:ok, subscriptions}
|
||||
@@ -409,17 +440,25 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
||||
end
|
||||
|
||||
defp webhooks_allowed?(map_id, webhooks_globally_enabled) do
|
||||
with true <- webhooks_globally_enabled,
|
||||
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||
true <- map.webhooks_enabled do
|
||||
:ok
|
||||
else
|
||||
false -> {:error, :webhooks_globally_disabled}
|
||||
nil -> {:error, :webhooks_globally_disabled}
|
||||
{:error, :not_found} -> {:error, :map_not_found}
|
||||
%{webhooks_enabled: false} -> {:error, :webhooks_disabled_for_map}
|
||||
{:error, reason} -> {:error, reason}
|
||||
error -> {:error, {:unexpected_error, error}}
|
||||
cond do
|
||||
not webhooks_globally_enabled ->
|
||||
{:error, :webhooks_globally_disabled}
|
||||
|
||||
not WandererApp.Map.webhooks_enabled?(map_id) ->
|
||||
{:error, :webhooks_disabled_for_map}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invalidates the webhook subscriptions cache for a map.
|
||||
Called when subscriptions are created, updated, or deleted.
|
||||
"""
|
||||
def invalidate_cache(map_id) do
|
||||
cache_key = "map:#{map_id}"
|
||||
Cachex.del(:webhook_subscriptions_cache, cache_key)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,10 +8,13 @@ defmodule WandererApp.Map do
|
||||
require Logger
|
||||
|
||||
@map_state_cache :map_state_cache
|
||||
# Default plan indicates no active subscription (free tier)
|
||||
@default_subscription_plan :alpha
|
||||
|
||||
defstruct map_id: nil,
|
||||
name: nil,
|
||||
scope: :none,
|
||||
scopes: nil,
|
||||
owner_id: nil,
|
||||
characters: [],
|
||||
systems: Map.new(),
|
||||
@@ -20,17 +23,32 @@ defmodule WandererApp.Map do
|
||||
acls: [],
|
||||
options: Map.new(),
|
||||
characters_limit: nil,
|
||||
hubs_limit: nil
|
||||
hubs_limit: nil,
|
||||
sse_enabled: false,
|
||||
webhooks_enabled: false,
|
||||
subscription_plan: @default_subscription_plan
|
||||
|
||||
def new(
|
||||
%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} =
|
||||
input
|
||||
) do
|
||||
# Extract the new scopes array field if present (nil if not set)
|
||||
scopes = Map.get(input, :scopes)
|
||||
# Extract SSE/webhooks settings (default to false if not present)
|
||||
sse_enabled = Map.get(input, :sse_enabled, false)
|
||||
webhooks_enabled = Map.get(input, :webhooks_enabled, false)
|
||||
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
|
||||
map =
|
||||
struct!(__MODULE__,
|
||||
map_id: map_id,
|
||||
scope: scope,
|
||||
scopes: scopes,
|
||||
owner_id: owner_id,
|
||||
name: name,
|
||||
acls: acls,
|
||||
hubs: hubs
|
||||
hubs: hubs,
|
||||
sse_enabled: sse_enabled,
|
||||
webhooks_enabled: webhooks_enabled
|
||||
)
|
||||
|
||||
update_map(map_id, map)
|
||||
@@ -128,7 +146,7 @@ defmodule WandererApp.Map do
|
||||
|
||||
def is_subscription_active?(map_id, _map_subscriptions_enabled) do
|
||||
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
||||
{:ok, plan != :alpha}
|
||||
{:ok, plan != @default_subscription_plan}
|
||||
end
|
||||
|
||||
def get_options(map_id),
|
||||
@@ -315,12 +333,17 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, %{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit
|
||||
}) do
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, subscription_settings) do
|
||||
characters_limit = Map.get(subscription_settings, :characters_limit)
|
||||
hubs_limit = Map.get(subscription_settings, :hubs_limit)
|
||||
plan = Map.get(subscription_settings, :plan, @default_subscription_plan)
|
||||
|
||||
map_id
|
||||
|> update_map(%{characters_limit: characters_limit, hubs_limit: hubs_limit})
|
||||
|> update_map(%{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit,
|
||||
subscription_plan: plan
|
||||
})
|
||||
|
||||
map_id
|
||||
|> get_map!()
|
||||
@@ -334,6 +357,99 @@ defmodule WandererApp.Map do
|
||||
|> get_map!()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates SSE enabled setting in the map cache.
|
||||
Called when the map's sse_enabled setting changes.
|
||||
"""
|
||||
def update_sse_enabled(map_id, sse_enabled)
|
||||
when is_binary(map_id) and is_boolean(sse_enabled) do
|
||||
update_map(map_id, %{sse_enabled: sse_enabled})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates webhooks enabled setting in the map cache.
|
||||
Called when the map's webhooks_enabled setting changes.
|
||||
"""
|
||||
def update_webhooks_enabled(map_id, webhooks_enabled)
|
||||
when is_binary(map_id) and is_boolean(webhooks_enabled) do
|
||||
update_map(map_id, %{webhooks_enabled: webhooks_enabled})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if SSE is enabled for a map using the cache.
|
||||
Falls back to DB query if map is not in cache.
|
||||
Returns a boolean (defaults to false if map not found).
|
||||
"""
|
||||
def sse_enabled?(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
Map.get(map, :sse_enabled, false)
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Cache miss - fall back to DB
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, db_map} -> db_map.sse_enabled
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if SSE is enabled for a map with explicit not_found handling.
|
||||
Returns {:ok, boolean} or {:error, :not_found}.
|
||||
"""
|
||||
def sse_enabled_with_status(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
{:ok, Map.get(map, :sse_enabled, false)}
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Cache miss - fall back to DB
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, db_map} -> {:ok, db_map.sse_enabled}
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if webhooks are enabled for a map using the cache.
|
||||
Falls back to DB query if map is not in cache.
|
||||
"""
|
||||
def webhooks_enabled?(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
Map.get(map, :webhooks_enabled, false)
|
||||
|
||||
{:error, :not_found} ->
|
||||
# Cache miss - fall back to DB
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, db_map} -> db_map.webhooks_enabled
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if subscription is active for a map using the cache.
|
||||
Returns {:ok, true} if active, {:ok, false} if not, or {:error, :not_cached} if not in cache.
|
||||
|
||||
Note: In CE mode (subscriptions disabled), use is_subscription_active?/1 which
|
||||
handles this case without cache lookup.
|
||||
"""
|
||||
def subscription_active_cached?(map_id) do
|
||||
case get_map(map_id) do
|
||||
{:ok, map} ->
|
||||
plan = Map.get(map, :subscription_plan, @default_subscription_plan)
|
||||
{:ok, plan != @default_subscription_plan}
|
||||
|
||||
_ ->
|
||||
{:error, :not_cached}
|
||||
end
|
||||
end
|
||||
|
||||
def add_systems!(map, []), do: map
|
||||
|
||||
def add_systems!(%{map_id: map_id} = map, [system | rest]) do
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule WandererApp.Map.Manager do
|
||||
@maps_queue :maps_queue
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_cleanup_interval :timer.minutes(5)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
# Test-aware async task runner
|
||||
@@ -99,6 +99,7 @@ defmodule WandererApp.Map.Manager do
|
||||
def handle_info(:cleanup_pings, state) do
|
||||
try do
|
||||
cleanup_expired_pings()
|
||||
cleanup_orphaned_pings()
|
||||
{:noreply, state}
|
||||
rescue
|
||||
e ->
|
||||
@@ -141,6 +142,55 @@ defmodule WandererApp.Map.Manager do
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_orphaned_pings() do
|
||||
case WandererApp.MapPingsRepo.get_orphaned_pings() do
|
||||
{:ok, []} ->
|
||||
:ok
|
||||
|
||||
{:ok, orphaned_pings} ->
|
||||
Logger.info(
|
||||
"[cleanup_orphaned_pings] Found #{length(orphaned_pings)} orphaned pings, cleaning up..."
|
||||
)
|
||||
|
||||
Enum.each(orphaned_pings, fn %{id: ping_id, map_id: map_id, type: type, system: system} =
|
||||
ping ->
|
||||
reason =
|
||||
cond do
|
||||
is_nil(ping.system) -> "system deleted"
|
||||
is_nil(ping.character) -> "character deleted"
|
||||
is_nil(ping.map) -> "map deleted"
|
||||
not is_nil(system) and system.visible == false -> "system hidden (visible=false)"
|
||||
true -> "unknown"
|
||||
end
|
||||
|
||||
Logger.warning(
|
||||
"[cleanup_orphaned_pings] Destroying orphaned ping #{ping_id} (map_id: #{map_id}, reason: #{reason})"
|
||||
)
|
||||
|
||||
# Broadcast cancellation if map_id is still valid
|
||||
if map_id do
|
||||
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
end
|
||||
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
Logger.info(
|
||||
"[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Failed to fetch orphaned pings: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_maps() do
|
||||
chunks =
|
||||
@maps_queue
|
||||
|
||||
@@ -126,4 +126,12 @@ defmodule WandererApp.Map.Operations do
|
||||
@doc "Delete a signature in a map"
|
||||
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
||||
|
||||
@doc "Link a signature to a target system"
|
||||
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
defdelegate link_signature(conn, sig_id, params), to: Signatures
|
||||
|
||||
@doc "Unlink a signature from its target system"
|
||||
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
defdelegate unlink_signature(conn, sig_id), to: Signatures
|
||||
end
|
||||
|
||||
@@ -72,28 +72,36 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
:ok
|
||||
end
|
||||
|
||||
def estimate_price(params, renew?, promo_code \\ nil)
|
||||
|
||||
def estimate_price(
|
||||
%{
|
||||
"period" => period,
|
||||
"characters_limit" => characters_limit,
|
||||
"hubs_limit" => hubs_limit
|
||||
},
|
||||
renew?
|
||||
} = params,
|
||||
renew?,
|
||||
promo_code
|
||||
)
|
||||
when is_binary(characters_limit),
|
||||
do:
|
||||
estimate_price(
|
||||
%{
|
||||
period: period |> String.to_integer(),
|
||||
characters_limit: characters_limit |> String.to_integer(),
|
||||
hubs_limit: hubs_limit |> String.to_integer()
|
||||
},
|
||||
renew?
|
||||
)
|
||||
when is_binary(characters_limit) do
|
||||
# Extract promo_code from params if passed there (from form)
|
||||
promo_code = promo_code || Map.get(params, "promo_code")
|
||||
|
||||
estimate_price(
|
||||
%{
|
||||
period: period |> String.to_integer(),
|
||||
characters_limit: characters_limit |> String.to_integer(),
|
||||
hubs_limit: hubs_limit |> String.to_integer()
|
||||
},
|
||||
renew?,
|
||||
promo_code
|
||||
)
|
||||
end
|
||||
|
||||
def estimate_price(
|
||||
%{characters_limit: characters_limit, hubs_limit: hubs_limit} = params,
|
||||
renew?
|
||||
renew?,
|
||||
promo_code
|
||||
) do
|
||||
%{
|
||||
plans: plans,
|
||||
@@ -136,7 +144,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
total_price = estimated_price * period
|
||||
|
||||
{:ok, discount} =
|
||||
{:ok, period_discount} =
|
||||
calc_discount(
|
||||
period,
|
||||
total_price,
|
||||
@@ -144,13 +152,27 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
renew?
|
||||
)
|
||||
|
||||
{:ok, total_price, discount}
|
||||
# Calculate promo discount on price after period discount
|
||||
price_after_period_discount = total_price - period_discount
|
||||
|
||||
{:ok, promo_discount, promo_valid?} =
|
||||
calc_promo_discount(promo_code, price_after_period_discount)
|
||||
|
||||
total_discount = period_discount + promo_discount
|
||||
|
||||
{:ok, total_price, total_discount, promo_valid?}
|
||||
end
|
||||
|
||||
def calc_additional_price(params, selected_subscription, promo_code \\ nil)
|
||||
|
||||
def calc_additional_price(
|
||||
%{"characters_limit" => characters_limit, "hubs_limit" => hubs_limit},
|
||||
selected_subscription
|
||||
%{"characters_limit" => characters_limit, "hubs_limit" => hubs_limit} = params,
|
||||
selected_subscription,
|
||||
promo_code
|
||||
) do
|
||||
# Extract promo_code from params if passed there (from form)
|
||||
promo_code = promo_code || Map.get(params, "promo_code")
|
||||
|
||||
%{
|
||||
plans: plans,
|
||||
extra_characters_50: extra_characters_50,
|
||||
@@ -189,7 +211,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
total_price = additional_price * period
|
||||
|
||||
{:ok, discount} =
|
||||
{:ok, period_discount} =
|
||||
calc_discount(
|
||||
period,
|
||||
total_price,
|
||||
@@ -197,7 +219,15 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
false
|
||||
)
|
||||
|
||||
{:ok, total_price, discount}
|
||||
# Calculate promo discount on price after period discount
|
||||
price_after_period_discount = total_price - period_discount
|
||||
|
||||
{:ok, promo_discount, promo_valid?} =
|
||||
calc_promo_discount(promo_code, price_after_period_discount)
|
||||
|
||||
total_discount = period_discount + promo_discount
|
||||
|
||||
{:ok, total_price, total_discount, promo_valid?}
|
||||
end
|
||||
|
||||
defp get_active_months(subscription) do
|
||||
@@ -255,6 +285,22 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
when period >= 3,
|
||||
do: {:ok, round(total_price * month_3_discount)}
|
||||
|
||||
# Calculates the promo code discount amount.
|
||||
# Returns {:ok, discount_amount, is_valid?}
|
||||
defp calc_promo_discount(nil, _price), do: {:ok, 0, false}
|
||||
defp calc_promo_discount("", _price), do: {:ok, 0, false}
|
||||
|
||||
defp calc_promo_discount(promo_code, price) when is_binary(promo_code) do
|
||||
case WandererApp.Env.validate_promo_code(promo_code) do
|
||||
{:ok, discount_percent} ->
|
||||
discount_amount = round(price * discount_percent / 100)
|
||||
{:ok, discount_amount, true}
|
||||
|
||||
{:error, :invalid_code} ->
|
||||
{:ok, 0, false}
|
||||
end
|
||||
end
|
||||
|
||||
def get_balance(map) do
|
||||
map
|
||||
|> WandererApp.MapRepo.load_relationships([
|
||||
@@ -302,7 +348,8 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
defp renew_subscription(%{auto_renew?: true, map: map} = subscription)
|
||||
when is_map(subscription) do
|
||||
with {:ok, estimated_price, discount} <- estimate_price(subscription, true),
|
||||
# No promo code for auto-renewals, ignore the promo_valid? return value
|
||||
with {:ok, estimated_price, discount, _promo_valid?} <- estimate_price(subscription, true),
|
||||
{:ok, map_balance} <- get_balance(map) do
|
||||
case map_balance >= estimated_price do
|
||||
true ->
|
||||
|
||||
@@ -63,13 +63,31 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
if is_nil(src_info) or is_nil(tgt_info) do
|
||||
{:error, :invalid_system_info}
|
||||
else
|
||||
# Get wormhole_type for ship size inference
|
||||
wormhole_type = attrs["wormhole_type"]
|
||||
|
||||
# Build extra_info map with optional connection attributes
|
||||
extra_info =
|
||||
%{}
|
||||
|> maybe_add_extra("time_status", attrs["time_status"])
|
||||
|> maybe_add_extra("mass_status", attrs["mass_status"])
|
||||
|> maybe_add_extra("locked", attrs["locked"])
|
||||
|> maybe_add_extra("wormhole_type", wormhole_type)
|
||||
|
||||
info = %{
|
||||
solar_system_source_id: src_info.solar_system_id,
|
||||
solar_system_target_id: tgt_info.solar_system_id,
|
||||
character_id: char_id,
|
||||
type: parse_type(attrs["type"]),
|
||||
ship_size_type:
|
||||
resolve_ship_size(attrs["type"], attrs["ship_size_type"], src_info, tgt_info)
|
||||
resolve_ship_size(
|
||||
attrs["type"],
|
||||
attrs["ship_size_type"],
|
||||
wormhole_type,
|
||||
src_info,
|
||||
tgt_info
|
||||
),
|
||||
extra_info: if(extra_info == %{}, do: nil, else: extra_info)
|
||||
}
|
||||
|
||||
case Server.add_connection(map_id, info) do
|
||||
@@ -95,10 +113,11 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# Determines the ship size for a connection, applying wormhole-specific rules
|
||||
# for C1, C13, and C4⇄NS links, falling back to the caller's provided size or Large.
|
||||
defp resolve_ship_size(type_val, ship_size_val, src_info, tgt_info) do
|
||||
# If wormhole_type is provided (e.g., "H296"), infer ship size from it.
|
||||
defp resolve_ship_size(type_val, ship_size_val, wormhole_type, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
wormhole_ship_size(ship_size_val, src_info, tgt_info)
|
||||
wormhole_ship_size(ship_size_val, wormhole_type, src_info, tgt_info)
|
||||
|
||||
_other ->
|
||||
# Stargates and others just use the parsed or default size
|
||||
@@ -108,15 +127,45 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# -- Wormhole‑specific sizing rules ----------------------------------------
|
||||
|
||||
defp wormhole_ship_size(ship_size_val, src, tgt) do
|
||||
defp wormhole_ship_size(ship_size_val, wormhole_type, src, tgt) do
|
||||
# First, try to infer from wormhole_type (e.g., "H296", "C5", etc.)
|
||||
inferred_size = infer_ship_size_from_wormhole_type(wormhole_type)
|
||||
# Parse ship_size_val early to handle string values correctly
|
||||
parsed_ship_size = parse_ship_size(ship_size_val, nil)
|
||||
|
||||
cond do
|
||||
c1_system?(src, tgt) -> @medium_ship_size
|
||||
c13_system?(src, tgt) -> @small_ship_size
|
||||
c4_to_ns?(src, tgt) -> @small_ship_size
|
||||
true -> parse_ship_size(ship_size_val, @large_ship_size)
|
||||
# If user explicitly provided a ship_size_val, use it
|
||||
not is_nil(parsed_ship_size) ->
|
||||
parsed_ship_size
|
||||
|
||||
# If we could infer from wormhole_type, use that
|
||||
not is_nil(inferred_size) ->
|
||||
inferred_size
|
||||
|
||||
# Otherwise fall back to system class rules
|
||||
c1_system?(src, tgt) ->
|
||||
@medium_ship_size
|
||||
|
||||
c13_system?(src, tgt) ->
|
||||
@small_ship_size
|
||||
|
||||
c4_to_ns?(src, tgt) ->
|
||||
@small_ship_size
|
||||
|
||||
true ->
|
||||
@large_ship_size
|
||||
end
|
||||
end
|
||||
|
||||
# Infer ship size from wormhole type name using EVE static data
|
||||
defp infer_ship_size_from_wormhole_type(nil), do: nil
|
||||
defp infer_ship_size_from_wormhole_type(""), do: nil
|
||||
defp infer_ship_size_from_wormhole_type("K162"), do: nil
|
||||
|
||||
defp infer_ship_size_from_wormhole_type(wormhole_type) do
|
||||
WandererApp.Utils.EVEUtil.get_wh_size(wormhole_type)
|
||||
end
|
||||
|
||||
defp c1_system?(%{system_class: @c1_system_class}, _), do: true
|
||||
defp c1_system?(_, %{system_class: @c1_system_class}), do: true
|
||||
defp c1_system?(_, _), do: false
|
||||
@@ -162,6 +211,9 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
defp parse_type(_), do: @connection_type_wormhole
|
||||
|
||||
defp maybe_add_extra(map, _key, nil), do: map
|
||||
defp maybe_add_extra(map, key, value), do: Map.put(map, key, value)
|
||||
|
||||
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
|
||||
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user