Compare commits

..

3 Commits

Author SHA1 Message Date
Dmitry Popov
b1f1098df6 chore: release version v1.13.7 2024-10-31 22:56:59 +01:00
Dmitry Popov
ed5d824c0a refactor(Map): Map event handling refactoring 2024-10-31 19:19:23 +01:00
Dmitry Popov
d8e4631981 refactor(Map): Map event handling refactoring 2024-10-31 19:19:07 +01:00
128 changed files with 3232 additions and 5901 deletions

View File

@@ -1,7 +1,12 @@
FROM elixir:1.17-otp-27
FROM elixir:1.16-otp-25
RUN apt install -yq curl gnupg
RUN apt update -yq
RUN apt install -yq curl gnupg mc inotify-tools
RUN apt --fix-broken install
RUN apt remove -y nodejs nodejs-doc
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
RUN apt install -y nodejs
RUN npm install --global yarn
RUN mix local.hex --force

View File

@@ -2,7 +2,7 @@ version: "0.1"
services:
db:
image: postgres:13-alpine
image: postgres:14.3
restart: always
environment:
POSTGRES_USER: postgres
@@ -10,13 +10,13 @@ services:
ports:
- "5432:5432"
volumes:
- db-new:/var/lib/postgresql/data
- db:/var/lib/postgresql/data
wanderer:
environment:
PORT: 8000
DB_HOST: db
WEB_APP_URL: "http://localhost:8000"
WEB_APP_URL: "http://localhost:4444"
ERL_AFLAGS: "-kernel shell_history enabled"
build:
context: .
@@ -33,4 +33,4 @@ services:
volumes:
elixir-artifacts: {}
db-new: {}
db: {}

View File

@@ -18,4 +18,4 @@ jobs:
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.PORT }}
script: |
/home/wanderer/app/deploy.sh ${{ github.event.release.tag_name }}
/app/release/linux/deploy.sh ${{ github.event.release.tag_name }}

View File

@@ -1,3 +1,3 @@
erlang 26.2.5.5
elixir 1.17.3-otp-26
erlang 25.3
elixir 1.16-otp-25
nodejs 18.0.0

View File

@@ -1,305 +1,6 @@
# Change Log
<!-- changelog -->
## [v1.27.1](https://github.com/wanderer-industries/wanderer/compare/v1.27.0...v1.27.1) (2024-12-04)
### Bug Fixes:
* Map: Fix 'On the map' visibility
## [v1.27.0](https://github.com/wanderer-industries/wanderer/compare/v1.26.1...v1.27.0) (2024-12-03)
### Features:
* Map: Hide 'On the map' list for 'Viewer' role
## [v1.26.1](https://github.com/wanderer-industries/wanderer/compare/v1.26.0...v1.26.1) (2024-12-03)
### Bug Fixes:
* Signatures: Fix error on splash wh
## [v1.26.0](https://github.com/wanderer-industries/wanderer/compare/v1.25.2...v1.26.0) (2024-12-03)
### Features:
* Signatures: Keep 'Lazy delete' enabled setting
## [v1.25.2](https://github.com/wanderer-industries/wanderer/compare/v1.25.1...v1.25.2) (2024-12-01)
### Bug Fixes:
* Signatures: Fix lazy delete on system switch
## [v1.25.1](https://github.com/wanderer-industries/wanderer/compare/v1.25.0...v1.25.1) (2024-11-28)
### Bug Fixes:
* Signatures: Fix colors & add 'Backspace' hotkey to delete signatures
## [v1.25.0](https://github.com/wanderer-industries/wanderer/compare/v1.24.2...v1.25.0) (2024-11-28)
### Features:
* Signatures: Automatically remove signature if linked system removed
## [v1.24.2](https://github.com/wanderer-industries/wanderer/compare/v1.24.1...v1.24.2) (2024-11-27)
### Bug Fixes:
* Signatures: Fix paste signatures
## [v1.24.1](https://github.com/wanderer-industries/wanderer/compare/v1.24.0...v1.24.1) (2024-11-27)
## [v1.24.0](https://github.com/wanderer-industries/wanderer/compare/v1.23.0...v1.24.0) (2024-11-27)
### Features:
* Signatures: Added "Lazy delete" option & got rid of update popup
## [v1.23.0](https://github.com/wanderer-industries/wanderer/compare/v1.22.0...v1.23.0) (2024-11-26)
### Features:
* Map: Lock systems available to manager/admin roles only (#75)
* Map: Lock systems available to manager/admin roles only
* Map: Fix add system & add acl member select behaviour
## [v1.22.0](https://github.com/wanderer-industries/wanderer/compare/v1.21.0...v1.22.0) (2024-11-26)
### Features:
* Map: Rework design of checkboxes in Signatures settings dialog. Rework design of checkboxes in Routes settings dialog. Now signature will deleteing by Delete hotkey was Backspace. Fixed size of group column in signatures list. Instead Updated column will be Added, updated may be turn on in settings. (#76)
## [v1.21.0](https://github.com/wanderer-industries/wanderer/compare/v1.20.1...v1.21.0) (2024-11-24)
### Features:
* Map: add new gate design, change EOL placement
## [v1.20.1](https://github.com/wanderer-industries/wanderer/compare/v1.20.0...v1.20.1) (2024-11-22)
## [v1.20.0](https://github.com/wanderer-industries/wanderer/compare/v1.19.3...v1.20.0) (2024-11-22)
### Features:
* Core: Add connection type for Gates, add new Update logic
## [v1.19.3](https://github.com/wanderer-industries/wanderer/compare/v1.19.2...v1.19.3) (2024-11-20)
### Bug Fixes:
* Core: Fix adding systems on splash (#71)
* Core: Fix adding systems on splash
## [v1.19.2](https://github.com/wanderer-industries/wanderer/compare/v1.19.1...v1.19.2) (2024-11-19)
## [v1.19.1](https://github.com/wanderer-industries/wanderer/compare/v1.19.0...v1.19.1) (2024-11-19)
## [v1.19.0](https://github.com/wanderer-industries/wanderer/compare/v1.18.1...v1.19.0) (2024-11-19)
### Features:
* Signatures: Add user setting to show Inserted time in a separate column
## [v1.18.1](https://github.com/wanderer-industries/wanderer/compare/v1.18.0...v1.18.1) (2024-11-17)
## [v1.18.0](https://github.com/wanderer-industries/wanderer/compare/v1.17.0...v1.18.0) (2024-11-16)
### Features:
* Map: a lot of design issues
## [v1.17.0](https://github.com/wanderer-industries/wanderer/compare/v1.16.1...v1.17.0) (2024-11-15)
### Features:
* Signatures: Add user setting to show Description in a separate column
## [v1.16.1](https://github.com/wanderer-industries/wanderer/compare/v1.16.0...v1.16.1) (2024-11-15)
### Bug Fixes:
* Signatures: Fix signature stored filters
## [v1.16.0](https://github.com/wanderer-industries/wanderer/compare/v1.15.5...v1.16.0) (2024-11-15)
### Features:
* Signatures: Add additional filters support to signature list, show description icon
## [v1.15.5](https://github.com/wanderer-industries/wanderer/compare/v1.15.4...v1.15.5) (2024-11-14)
## [v1.15.4](https://github.com/wanderer-industries/wanderer/compare/v1.15.3...v1.15.4) (2024-11-14)
### Bug Fixes:
* Core: Untracked characters still tracked on map (#63)
## [v1.15.3](https://github.com/wanderer-industries/wanderer/compare/v1.15.2...v1.15.3) (2024-11-13)
## [v1.15.2](https://github.com/wanderer-industries/wanderer/compare/v1.15.1...v1.15.2) (2024-11-07)
## [v1.15.1](https://github.com/wanderer-industries/wanderer/compare/v1.15.0...v1.15.1) (2024-11-07)
### Bug Fixes:
* Dev: Update .devcontainer instructions
## [v1.15.0](https://github.com/wanderer-industries/wanderer/compare/v1.14.1...v1.15.0) (2024-11-07)
### Features:
* Connections: Add connection mark EOL time (#56)
## [v1.14.1](https://github.com/wanderer-industries/wanderer/compare/v1.14.0...v1.14.1) (2024-11-06)
### Bug Fixes:
* Core: Fix character tracking permissions
## [v1.14.0](https://github.com/wanderer-industries/wanderer/compare/v1.13.12...v1.14.0) (2024-11-05)
### Features:
* ACL: Add an ability to assign member role without DnD
## [v1.13.12](https://github.com/wanderer-industries/wanderer/compare/v1.13.11...v1.13.12) (2024-11-04)
### Bug Fixes:
* Map: Fix system revert issues
## [v1.13.11](https://github.com/wanderer-industries/wanderer/compare/v1.13.10...v1.13.11) (2024-11-02)
### Bug Fixes:
* Map: Fix system revert issues
## [v1.13.10](https://github.com/wanderer-industries/wanderer/compare/v1.13.9...v1.13.10) (2024-11-01)
### Bug Fixes:
* Map: Fix system revert issues
## [v1.13.9](https://github.com/wanderer-industries/wanderer/compare/v1.13.8...v1.13.9) (2024-11-01)
## [v1.13.8](https://github.com/wanderer-industries/wanderer/compare/v1.13.7...v1.13.8) (2024-10-28)
## [v1.13.0](https://github.com/wanderer-industries/wanderer/compare/v1.12.11...v1.13.0) (2024-10-28)

View File

@@ -20,11 +20,11 @@ Interested to learn more? [Check more on our website](https://wanderer.ltd/news)
Wanderer is open source project and we have a free as in beer and self-hosted solution called [Wanderer Community Edition (CE)](https://wanderer.ltd/news/community-edition). Here are the differences between Wanderer and Wanderer CE:
| | Wanderer Cloud | Wanderer Community Edition |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Infrastructure management** | Easy and convenient. It takes 2 minutes to register your character and create a map. We manage everything so you dont have to worry about anything and can focus on gameplay. | You do it all yourself. You need to get a server and you need to manage your infrastructure. You are responsible for installation, maintenance, upgrades, server capacity, uptime, backup, security, stability, consistency, loading time and so on. |
| **Release schedule** | Continuously developed and improved with new features and updates multiple times per week. | Latest features and improvements won't be immediately available. |
| **Server location** | All visitor data is exclusively processed on EU-owned cloud infrastructure. We keep your site data on a secure, encrypted and green energy powered server in Germany. This ensures that your site data is protected by the strict European Union data privacy laws and ensures compliance with GDPR. Your website data never leaves the EU. | You have full control and can host your instance on any server in any country that you wish. Host it on a server in your basement or host it with any cloud provider wherever you want, even those that are not GDPR compliant. |
| | Wanderer Cloud | Wanderer Community Edition |
| ------------- | ------------- | ------------- |
| **Infrastructure management** | Easy and convenient. It takes 2 minutes to register your character and create a map. We manage everything so you dont have to worry about anything and can focus on gameplay. | You do it all yourself. You need to get a server and you need to manage your infrastructure. You are responsible for installation, maintenance, upgrades, server capacity, uptime, backup, security, stability, consistency, loading time and so on.|
| **Release schedule** | Continuously developed and improved with new features and updates multiple times per week. | Latest features and improvements won't be immediately available.|
| **Server location** | All visitor data is exclusively processed on EU-owned cloud infrastructure. We keep your site data on a secure, encrypted and green energy powered server in Germany. This ensures that your site data is protected by the strict European Union data privacy laws and ensures compliance with GDPR. Your website data never leaves the EU. | You have full control and can host your instance on any server in any country that you wish. Host it on a server in your basement or host it with any cloud provider wherever you want, even those that are not GDPR compliant.|
Interested in self-hosting Wanderer CE on your server? Take a look at our [Wanderer CE installation instructions](https://github.com/wanderer-industries/community-edition/).
@@ -54,13 +54,7 @@ Now you can visit [`localhost:8000`](http://localhost:8000) from your browser.
#### Using .devcontainer
- Run devcontainer
- Install additional dependencies inside Dev container
- `root@0d0a785313b6:/app# apt update`
- `root@0d0a785313b6:/app# curl -sL https://deb.nodesource.com/setup_18.x | bash -`
- `root@0d0a785313b6:/app# apt-get install nodejs inotify-tools -y`
- `root@0d0a785313b6:/app# mix setup`
- See how to run server in #Run section
- See how to start server in #setup section
#### Using nix flakes

View File

@@ -466,407 +466,3 @@ body {
transform: rotate(-360deg);
}
}
/* Map refresh */
.socket {
scale: 0.5;
width: 150px;
height: 150px;
left: 50%;
/* margin-left: -75px; */
top: 50%;
/* margin-top: -50px; */
}
.hex-brick {
background: #000;
width: 30px;
height: 17px;
position: absolute;
top: 5px;
animation-name: fade;
animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-name: fade;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
}
.hex-brick--active {
animation-name: fade-active;
-webkit-animation-name: fade-active;
}
.h2 {
transform: rotate(60deg);
-webkit-transform: rotate(60deg);
}
.h3 {
transform: rotate(-60deg);
-webkit-transform: rotate(-60deg);
}
.gel {
height: 30px;
width: 30px;
transition: all 0.3s;
-webkit-transition: all 0.3s;
position: absolute;
top: 50%;
left: 50%;
}
.center-gel {
margin-left: -15px;
margin-top: -15px;
animation-name: pulse-version;
animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-name: pulse-version;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
}
.c1 {
margin-left: -47px;
margin-top: -15px;
}
.c2 {
margin-left: -31px;
margin-top: -43px;
}
.c3 {
margin-left: 1px;
margin-top: -43px;
}
.c4 {
margin-left: 17px;
margin-top: -15px;
}
.c5 {
margin-left: -31px;
margin-top: 13px;
}
.c6 {
margin-left: 1px;
margin-top: 13px;
}
.c7 {
margin-left: -63px;
margin-top: -43px;
}
.c8 {
margin-left: 33px;
margin-top: -43px;
}
.c9 {
margin-left: -15px;
margin-top: 41px;
}
.c10 {
margin-left: -63px;
margin-top: 13px;
}
.c11 {
margin-left: 33px;
margin-top: 13px;
}
.c12 {
margin-left: -15px;
margin-top: -71px;
}
.c13 {
margin-left: -47px;
margin-top: -71px;
}
.c14 {
margin-left: 17px;
margin-top: -71px;
}
.c15 {
margin-left: -47px;
margin-top: 41px;
}
.c16 {
margin-left: 17px;
margin-top: 41px;
}
.c17 {
margin-left: -79px;
margin-top: -15px;
}
.c18 {
margin-left: 49px;
margin-top: -15px;
}
.c19 {
margin-left: -63px;
margin-top: -99px;
}
.c20 {
margin-left: 33px;
margin-top: -99px;
}
.c21 {
margin-left: 1px;
margin-top: -99px;
}
.c22 {
margin-left: -31px;
margin-top: -99px;
}
.c23 {
margin-left: -63px;
margin-top: 69px;
}
.c24 {
margin-left: 33px;
margin-top: 69px;
}
.c25 {
margin-left: 1px;
margin-top: 69px;
}
.c26 {
margin-left: -31px;
margin-top: 69px;
}
.c27 {
margin-left: -79px;
margin-top: -15px;
}
.c28 {
margin-left: -95px;
margin-top: -43px;
}
.c29 {
margin-left: -95px;
margin-top: 13px;
}
.c30 {
margin-left: 49px;
margin-top: 41px;
}
.c31 {
margin-left: -79px;
margin-top: -71px;
}
.c32 {
margin-left: -111px;
margin-top: -15px;
}
.c33 {
margin-left: 65px;
margin-top: -43px;
}
.c34 {
margin-left: 65px;
margin-top: 13px;
}
.c35 {
margin-left: -79px;
margin-top: 41px;
}
.c36 {
margin-left: 49px;
margin-top: -71px;
}
.c37 {
margin-left: 81px;
margin-top: -15px;
}
.r1 {
animation-name: pulse-version;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.2s;
-webkit-animation-name: pulse-version;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.2s;
}
.r2 {
animation-name: pulse-version;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.4s;
-webkit-animation-name: pulse-version;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.4s;
}
.r3 {
animation-name: pulse-version;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.6s;
-webkit-animation-name: pulse-version;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.6s;
}
.r1 > .hex-brick {
animation-name: fade;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.2s;
-webkit-animation-name: fade;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.2s;
}
.r1 > .hex-brick--active {
animation-name: fade-active;
-webkit-animation-name: fade-active;
}
.r2 > .hex-brick {
animation-name: fade;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.4s;
-webkit-animation-name: fade;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.4s;
}
.r2 > .hex-brick--active {
animation-name: fade-active;
-webkit-animation-name: fade-active;
}
.r3 > .hex-brick {
animation-name: fade;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-delay: 0.6s;
-webkit-animation-name: fade;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-delay: 0.6s;
}
.r3 > .hex-brick--active {
animation-name: fade-active;
-webkit-animation-name: fade-active;
}
@keyframes pulse-version {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(0.01);
transform: scale(0.01);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes fade {
0% {
background: #09d0e2;
}
50% {
background: #8ae6ee;
}
100% {
background: #09d0e2;
}
}
@keyframes fade-active {
0% {
background: #ff52d9;
}
50% {
background: #ff52d9;
}
100% {
background: #ff52d9;
}
}
@-webkit-keyframes pulse {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
50% {
-webkit-transform: scale(0.01);
transform: scale(0.01);
}
100% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@-webkit-keyframes fade {
0% {
background: #abf8ff;
}
50% {
background: #389ca6;
}
100% {
background: #abf8ff;
}
}
/* Map refresh END */

View File

@@ -15,10 +15,11 @@ const ErrorFallback = () => {
};
export default function MapRoot({ hooks }) {
const mapRef = useRef<MapHandlers>(null);
const providerRef = useRef<MapHandlers>(null);
const hooksRef = useRef<any>(hooks);
const mapperHandlerRefs = useRef([providerRef]);
const mapperHandlerRefs = useRef([mapRef, providerRef]);
const { handleCommand, handleMapEvent, handleMapEvents } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
@@ -40,7 +41,7 @@ export default function MapRoot({ hooks }) {
return (
<PrimeReactProvider>
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand} mapRef={mapRef}>
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
<ReactFlowProvider>
<MapRootContent />

View File

@@ -1,19 +1,20 @@
import { useCallback } from 'react';
import clsx from 'clsx';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
const [parent] = useAutoAnimate();
const { mapRef } = useMapRootState();
const handleSelect = useCallback((character: CharacterTypeRaw) => {
emitMapEvent({
name: Commands.centerSystem,
data: character?.location?.solar_system_id?.toString(),
});
}, []);
const handleSelect = useCallback(
(character: CharacterTypeRaw) => {
mapRef.current?.command(Commands.centerSystem, character?.location?.solar_system_id?.toString());
},
[mapRef],
);
const items = data.map(character => (
<li

View File

@@ -6,8 +6,6 @@ import { PrimeIcons } from 'primereact/api';
import { ContextMenuSystemProps } from '@/hooks/Mapper/components/contexts';
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export const useContextMenuSystemItems = ({
onDeleteSystem,
@@ -27,7 +25,6 @@ export const useContextMenuSystemItems = ({
const getStatus = useStatusMenu(systems, systemId, onSystemStatus);
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
const getWaypointMenu = useWaypointMenu(onWaypointSet);
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]);
return useMemo(() => {
const system = systemId ? getSystemById(systems, systemId) : undefined;
@@ -61,25 +58,19 @@ export const useContextMenuSystemItems = ({
command: onHubToggle,
},
...(system.locked
? canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: [
...(canLockSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
{ separator: true },
{
label: 'Delete',
@@ -89,7 +80,6 @@ export const useContextMenuSystemItems = ({
]),
];
}, [
canLockSystem,
systems,
systemId,
getTags,

View File

@@ -1,25 +1,25 @@
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import { RefObject, useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { Commands, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Commands, MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import * as React from 'react';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
interface UseContextMenuSystemHandlersProps {
hubs: string[];
outCommand: OutCommandHandler;
mapRef: RefObject<MapHandlers>;
}
export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>();
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
const ref = useRef({ hubs, system, outCommand });
ref.current = { hubs, system, outCommand };
const ref = useRef({ hubs, system, outCommand, mapRef });
ref.current = { hubs, system, outCommand, mapRef };
const open = useCallback(
(ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
@@ -48,7 +48,7 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand }: UseContex
}, []);
const onAddSystem = useCallback(() => {
const { system: solarSystemId, outCommand } = ref.current;
const { system: solarSystemId, outCommand, mapRef } = ref.current;
if (!solarSystemId) {
return;
}
@@ -60,11 +60,7 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand }: UseContex
},
});
setTimeout(() => {
emitMapEvent({
name: Commands.centerSystem,
data: solarSystemId,
});
mapRef.current?.command(Commands.centerSystem, solarSystemId);
setSystem(undefined);
}, 200);
}, []);

View File

@@ -1,17 +1,19 @@
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect } from 'react';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useRef } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
Edge,
MiniMap,
Node,
NodeChange,
NodeDragHandler,
OnConnect,
OnMoveEnd,
OnSelectionChangeFunc,
SelectionDragHandler,
SelectionMode,
useEdgesState,
useNodesState,
NodeChange,
useReactFlow,
} from 'reactflow';
import 'reactflow/dist/style.css';
@@ -19,7 +21,8 @@ import classes from './Map.module.scss';
import './styles/neon-theme.scss';
import './styles/eve-common.scss';
import { MapProvider, useMapState } from './MapProvider';
import { useNodesState, useEdgesState, useMapHandlers, useUpdateNodes } from './hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapHandlers, useUpdateNodes } from './hooks';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import {
ContextMenuConnection,
@@ -34,6 +37,7 @@ import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
@@ -89,14 +93,12 @@ interface MapCompProps {
refn: ForwardedRef<MapHandlers>;
onCommand: OutCommandHandler;
onSelectionChange: OnMapSelectionChange;
onManualDelete(systems: string[]): void;
onConnectionInfoClick?(e: SolarSystemConnection): void;
onSelectionContextMenu?: NodeSelectionMouseHandler;
minimapClasses?: string;
isShowMinimap?: boolean;
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
showKSpaceBG?: boolean;
isThickConnections?: boolean;
}
const MapComp = ({
@@ -107,20 +109,26 @@ const MapComp = ({
onSystemContextMenu,
onConnectionInfoClick,
onSelectionContextMenu,
onManualDelete,
isShowMinimap,
showKSpaceBG,
isThickConnections,
}: MapCompProps) => {
const { getNode } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
const [nodes, , onNodesChange] = useNodesState<SolarSystemRawType>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>[]>(initialEdges);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers();
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const {
data: { systems },
} = useMapRootState();
const { deleteSystems } = useDeleteSystems();
const systemsRef = useRef({ systems });
systemsRef.current = { systems };
const onConnect: OnConnect = useCallback(
params => {
@@ -178,41 +186,35 @@ const MapComp = ({
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
const systemsIdsToRemove: string[] = [];
const nextChanges = changes.reduce((acc, change) => {
if (change.type !== 'remove') {
return [...acc, change];
}
const node = getNode(change.id);
if (!node) {
return [...acc, change];
}
if (node.data.locked) {
if (change.type === 'remove') {
const node = getNode(change.id);
const { systems = [] } = systemsRef.current;
if (node?.data?.id && !systems.map(s => s.id).includes(node?.data?.id)) {
return [...acc, change];
} else if (!node?.data?.locked) {
systemsIdsToRemove.push(node?.data?.id);
}
return acc;
}
systemsIdsToRemove.push(node.data.id);
return [...acc, change];
}, [] as NodeChange[]);
if (systemsIdsToRemove.length > 0) {
onManualDelete(systemsIdsToRemove);
if (systemsIdsToRemove.length) {
deleteSystems(systemsIdsToRemove);
}
onNodesChange(nextChanges);
},
[getNode, onManualDelete, onNodesChange],
[deleteSystems, getNode, onNodesChange],
);
useEffect(() => {
update(x => ({
...x,
showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections,
}));
}, [showKSpaceBG, isThickConnections, update]);
}, [showKSpaceBG, update]);
return (
<>
@@ -236,7 +238,6 @@ const MapComp = ({
onConnectStart={() => update({ isConnecting: true })}
onConnectEnd={() => update({ isConnecting: false })}
onNodeMouseEnter={(_, node) => update({ hoverNodeId: node.id })}
// onKeyUp=
onNodeMouseLeave={() => update({ hoverNodeId: null })}
onEdgeClick={(_, t) => {
onConnectionInfoClick?.(t.data);

View File

@@ -8,7 +8,6 @@ export type MapData = MapUnionTypes & {
hoverNodeId: string | null;
visibleNodes: Set<string>;
showKSpaceBG: boolean;
isThickConnections: boolean;
};
interface MapProviderProps {
@@ -18,7 +17,6 @@ interface MapProviderProps {
const INITIAL_DATA: MapData = {
wormholesData: {},
wormholes: [],
effects: {},
characters: [],
userCharacters: [],
@@ -31,8 +29,6 @@ const INITIAL_DATA: MapData = {
hoverNodeId: null,
visibleNodes: new Set(),
showKSpaceBG: false,
isThickConnections: false,
userPermissions: {},
};
export interface MapContextProps {

View File

@@ -3,7 +3,7 @@ import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { Edge } from '@reactflow/core/dist/esm/types/edges';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import { MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import classes from './ContextMenuConnection.module.scss';
import { MASS_STATE_NAMES, MASS_STATE_NAMES_ORDER } from '@/hooks/Mapper/components/map/constants.ts';
@@ -35,49 +35,36 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
}
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
const isWormhole = edge.data?.type !== ConnectionType.gate;
return [
...(isWormhole
? [
{
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
]
: []),
...(isWormhole
? [
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.normal : ShipSizeStatus.small,
),
},
]
: []),
...(isWormhole
? [
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
]
: []),
...(isWormhole && !isFrigateSize
{
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.normal : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `Mass status`,

View File

@@ -4,7 +4,7 @@ import { ContextMenu } from 'primereact/contextmenu';
import { useMapState } from '../../MapProvider.tsx';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Edge } from '@reactflow/core/dist/esm/types/edges';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import { MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
export const useContextMenuConnectionHandlers = () => {
@@ -47,23 +47,6 @@ export const useContextMenuConnectionHandlers = () => {
setEdge(undefined);
};
const onChangeType = useCallback((type: ConnectionType) => {
const { edge, outCommand } = ref.current;
if (!edge) {
return;
}
outCommand({
type: OutCommand.updateConnectionType,
data: {
source: edge.source,
target: edge.target,
value: type,
},
});
}, []);
const onChangeMassState = useCallback((status: MassState) => {
const { edge, outCommand } = ref.current;
@@ -135,7 +118,6 @@ export const useContextMenuConnectionHandlers = () => {
contextMenuRef,
onDeleteConnection,
onChangeTimeState,
onChangeType,
onChangeMassState,
onChangeShipSizeStatus,
onToggleMassSave,

View File

@@ -21,11 +21,7 @@
}
&.Frigate {
stroke: #d4f0ff;
}
&.Gate {
stroke: #1c1e15;
stroke: #4e62c9;
}
&.Hovered {
@@ -41,16 +37,9 @@
}
&.Frigate {
stroke: #d4f0ff;
stroke: #41acd7;
}
}
&.Tick {
stroke-width: 3px;
&.Hovered {
stroke-width: 3px;
}
}
}
@@ -72,19 +61,6 @@
stroke: #ef7dce;
}
}
&.Tick {
stroke-width: 5px;
&.TimeCrit {
stroke-width: 6px;
}
}
&.Gate {
stroke: #9aff40;
}
}
.ClickPath {
@@ -117,14 +93,5 @@
width: 5px;
height: 5px;
z-index: 1001;
&.Tick {
width: 7px;
height: 7px;
&.Right {
margin-left: 0px;
}
}
}

View File

@@ -1,64 +1,39 @@
import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
import clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import { MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import { PrimeIcons } from 'primereact/api';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
const MAP_TRANSLATES: Record<string, string> = {
[Position.Top]: 'translate(-48%, 0%)',
[Position.Top]: 'translate(-50%, 0%)',
[Position.Bottom]: 'translate(-50%, -100%)',
[Position.Left]: 'translate(0%, -50%)',
[Position.Right]: 'translate(-100%, -50%)',
};
const MAP_OFFSETS_TICK: Record<string, { x: number; y: number }> = {
[Position.Top]: { x: 0, y: 3 },
[Position.Bottom]: { x: 0, y: -3 },
[Position.Left]: { x: 3, y: 0 },
[Position.Right]: { x: -3, y: 0 },
};
const MAP_OFFSETS: Record<string, { x: number; y: number }> = {
[Position.Top]: { x: 0, y: 0 },
[Position.Bottom]: { x: 0, y: 0 },
[Position.Left]: { x: 0, y: 0 },
[Position.Right]: { x: 0, y: 0 },
};
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
const isWormhole = data?.type !== ConnectionType.gate;
const {
data: { isThickConnections },
} = useMapState();
const [hovered, setHovered] = useState(false);
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
const [edgePath, labelX, labelY] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetPosition: targetPos,
targetX: tx + offset.x,
targetY: ty + offset.y,
targetX: tx,
targetY: ty,
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
}, [sourceNode, targetNode]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -69,10 +44,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
<path
id={`back_${id}`}
className={clsx(classes.EdgePathBack, {
[classes.Tick]: isThickConnections,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.TimeCrit]: data.time_status === TimeStatus.eol,
[classes.Hovered]: hovered,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -81,12 +54,10 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
<path
id={`front_${id}`}
className={clsx(classes.EdgePathFront, {
[classes.Tick]: isThickConnections,
[classes.Hovered]: hovered,
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: !isWormhole,
[classes.MassVerge]: data.mass_status === MassState.verge,
[classes.MassHalf]: data.mass_status === MassState.half,
[classes.Frigate]: data.ship_size_type === ShipSizeStatus.small,
})}
d={path}
markerEnd={markerEnd}
@@ -104,19 +75,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
<EdgeLabelRenderer>
<div
className={clsx(
classes.Handle,
{ [classes.Tick]: isThickConnections, [classes.Right]: Position.Right === sourcePos },
'react-flow__handle absolute nodrag pointer-events-none',
)}
className={clsx(classes.Handle, 'react-flow__handle absolute nodrag pointer-events-none')}
style={{ transform: `${MAP_TRANSLATES[sourcePos]} translate(${sx}px,${sy}px)` }}
/>
<div
className={clsx(
classes.Handle,
{ [classes.Tick]: isThickConnections },
'react-flow__handle absolute nodrag pointer-events-none',
)}
className={clsx(classes.Handle, 'react-flow__handle absolute nodrag pointer-events-none')}
style={{ transform: `${MAP_TRANSLATES[targetPos]} translate(${tx}px,${ty}px)` }}
/>
@@ -126,7 +89,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}
>
{isWormhole && data.locked && (
{data.locked && (
<WdTooltipWrapper
content="Save mass"
className={clsx(

View File

@@ -212,14 +212,6 @@ $tooltip-bg: #202020; // Темный фон для подсказок
color: #ffb01d;
}
/* Firefox kostyl */
@-moz-document url-prefix() {
.classSystemName {
font-family: inherit !important;
font-weight: bold;
}
}
.classSystemName {
//font-weight: bold;
}
@@ -270,13 +262,6 @@ $tooltip-bg: #202020; // Темный фон для подсказок
& > * {
line-height: 10px;
}
/* Firefox kostyl */
@-moz-document url-prefix() {
position: relative;
top: -1px;
}
}
.Handlers {
@@ -314,25 +299,4 @@ $tooltip-bg: #202020; // Темный фон для подсказок
&.HandleLeft {
left: -2px;
}
&.Tick {
width: 7px;
height: 7px;
&.HandleTop {
top: -3px;
}
&.HandleRight {
right: -3px;
}
&.HandleBottom {
bottom: -3px;
}
&.HandleLeft {
left: -3px;
}
}
}

View File

@@ -79,7 +79,6 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
hoverNodeId,
visibleNodes,
showKSpaceBG,
isThickConnections,
},
outCommand,
} = useMapState();
@@ -240,40 +239,28 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
<div onMouseDownCapture={dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
className={clsx(classes.Handle, classes.HandleTop, { [classes.selected]: selected })}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
className={clsx(classes.Handle, classes.HandleRight, { [classes.selected]: selected })}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
className={clsx(classes.Handle, classes.HandleBottom, { [classes.selected]: selected })}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
className={clsx(classes.Handle, classes.HandleLeft, { [classes.selected]: selected })}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"

View File

@@ -1,4 +1,4 @@
import { ConnectionType, MassState } from '@/hooks/Mapper/types';
import { MassState } from '@/hooks/Mapper/types';
export enum SOLAR_SYSTEM_CLASS_IDS {
ccp1 = -1,
@@ -712,13 +712,6 @@ export const STATUS_CLASSES: Record<number, string> = {
[STATUSES.dangerous]: 'eve-system-status-dangerous',
};
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
export const TYPE_NAMES = {
[ConnectionType.wormhole]: 'Wormhole',
[ConnectionType.gate]: 'Gate',
};
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];
export const MASS_STATE_NAMES = {

View File

@@ -12,7 +12,6 @@ export const useMapAddSystems = () => {
return useCallback((systems: CommandAddSystems) => {
const { rf } = ref.current;
const nodes = rf.getNodes();
const prepared: Node[] = systems.filter(x => !nodes.some(y => x.id === y.id)).map(convertSystem2Node);
rf.addNodes(prepared);
}, []);

View File

@@ -5,21 +5,24 @@ import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts
export const useMapRemoveSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();
const ref = useRef({ onSelectionChange, rf });
ref.current = { onSelectionChange, rf };
const ref = useRef({ onSelectionChange });
ref.current = { onSelectionChange };
return useCallback((systems: CommandRemoveSystems) => {
ref.current.rf.deleteElements({ nodes: systems.map(x => ({ id: `${x}` })) });
return useCallback(
(systems: CommandRemoveSystems) => {
rf.deleteElements({ nodes: systems.map(x => ({ id: `${x}` })) });
const newSelection = ref.current.rf
.getNodes()
.filter(x => !systems.includes(parseInt(x.id)))
.filter(x => x.selected)
.map(x => x.id);
const newSelection = rf
.getNodes()
.filter(x => !systems.includes(parseInt(x.id)))
.filter(x => x.selected)
.map(x => x.id);
ref.current.onSelectionChange({
systems: newSelection,
connections: [],
});
}, []);
ref.current.onSelectionChange({
systems: newSelection,
connections: [],
});
},
[rf],
);
};

View File

@@ -1,3 +1,2 @@
export * from './useMapHandlers';
export * from './useUpdateNodes';
export * from './useNodesEdgesState';

View File

@@ -19,6 +19,8 @@ import {
MapHandlers,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
import {
useCommandsCharacters,
useCommandsConnections,
@@ -58,16 +60,13 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
removeConnections(data as CommandRemoveConnections);
@@ -112,17 +111,13 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
connections: [],
});
selectSystem(systemId as CommandSelectSystem);
}, 500);
}, 100);
break;
case Commands.routes:
// do nothing here
break;
case Commands.signaturesUpdated:
// do nothing here
break;
case Commands.linkSignatureToSystem:
// do nothing here
break;
@@ -136,4 +131,20 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
},
[],
);
useMapEventListener(event => {
switch (event.name) {
case Commands.addConnections:
addConnections(event.data as CommandAddConnections);
break;
case Commands.addSystems:
mapAddSystems(event.data as CommandAddSystems);
break;
case Commands.removeSystems:
removeSystems(event.data as CommandRemoveSystems);
break;
default:
break;
}
});
};

View File

@@ -1,36 +0,0 @@
import { useState, useCallback, type Dispatch, type SetStateAction } from 'react';
import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import { OnNodesChange, Edge, OnEdgesChange, Node } from 'reactflow';
/**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases.
*
* @public
* @param initialNodes
* @returns an array [nodes, setNodes, onNodesChange]
*/
export function useNodesState<NodeType extends Node>(
initialNodes: NodeType[],
): [NodeType[], Dispatch<SetStateAction<NodeType[]>>, OnNodesChange] {
const [nodes, setNodes] = useState(initialNodes);
const onNodesChange: OnNodesChange = useCallback(changes => setNodes(nds => applyNodeChanges(changes, nds)), []);
return [nodes, setNodes, onNodesChange];
}
/**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases.
*
* @public
* @param initialEdges
* @returns an array [edges, setEdges, onEdgesChange]
*/
export function useEdgesState<EdgeType extends Edge = Edge>(
initialEdges: EdgeType[],
): [EdgeType[], Dispatch<SetStateAction<EdgeType[]>>, OnEdgesChange] {
const [edges, setEdges] = useState(initialEdges);
const onEdgesChange: OnEdgesChange = useCallback(changes => setEdges(eds => applyEdgeChanges(changes, eds)), []);
return [edges, setEdges, onEdgesChange];
}

View File

@@ -1,174 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { EdgeChange, NodeChange, Node, Edge } from 'reactflow';
// This function applies changes to nodes or edges that are triggered by React Flow internally.
// When you drag a node for example, React Flow will send a position change update.
// This function then applies the changes and returns the updated elements.
function applyChanges(changes: any[], elements: any[]): any[] {
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
if (changes.some(c => c.type === 'reset')) {
return changes.filter(c => c.type === 'reset').map(c => c.item);
}
const updatedElements: any[] = [];
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
const changesMap = new Map<any, any[]>();
const addItemChanges: any[] = [];
for (const change of changes) {
if (change.type === 'add') {
addItemChanges.push(change);
continue;
} else if (change.type === 'remove' || change.type === 'replace') {
// For a 'remove' change we can safely ignore any other changes queued for
// the same element, it's going to be removed anyway!
changesMap.set(change.id, [change]);
} else {
const elementChanges = changesMap.get(change.id);
if (elementChanges) {
// If we have some changes queued already, we can do a mutable update of
// that array and save ourselves some copying.
elementChanges.push(change);
} else {
changesMap.set(change.id, [change]);
}
}
}
for (const element of elements) {
const changes = changesMap.get(element.id);
// When there are no changes for an element we can just push it unmodified,
// no need to copy it.
if (!changes) {
updatedElements.push(element);
continue;
}
// If we have a 'remove' change queued, it'll be the only change in the array
if (changes[0].type === 'remove') {
continue;
}
if (changes[0].type === 'replace') {
updatedElements.push({ ...changes[0].item });
continue;
}
// For other types of changes, we want to start with a shallow copy of the
// object so React knows this element has changed. Sequential changes will
/// each _mutate_ this object, so there's only ever one copy.
const updatedElement = { ...element };
for (const change of changes) {
applyChange(change, updatedElement);
}
updatedElements.push(updatedElement);
}
// we need to wait for all changes to be applied before adding new items
// to be able to add them at the correct index
if (addItemChanges.length) {
addItemChanges.forEach(change => {
if (change.index !== undefined) {
updatedElements.splice(change.index, 0, { ...change.item });
} else {
updatedElements.push({ ...change.item });
}
});
}
return updatedElements;
}
// Applies a single change to an element. This is a *mutable* update.
function applyChange(change: any, element: any): any {
switch (change.type) {
case 'select': {
element.selected = change.selected;
break;
}
case 'position': {
if (typeof change.position !== 'undefined') {
element.position = change.position;
}
if (typeof change.dragging !== 'undefined') {
element.dragging = change.dragging;
}
break;
}
case 'dimensions': {
if (typeof change.dimensions !== 'undefined') {
element.measured ??= {};
element.measured.width = change.dimensions.width;
element.measured.height = change.dimensions.height;
if (change.setAttributes) {
element.width = change.dimensions.width;
element.height = change.dimensions.height;
}
}
if (typeof change.resizing === 'boolean') {
element.resizing = change.resizing;
}
break;
}
}
}
/**
* Drop in function that applies node changes to an array of nodes.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param nodes - Array of nodes to apply the changes to
* @returns Array of updated nodes
* @example
* const onNodesChange = useCallback(
(changes) => {
setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
},
[setNodes],
);
return (
<ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
);
*/
export function applyNodeChanges<NodeType extends Node = Node>(changes: NodeChange[], nodes: NodeType[]): NodeType[] {
return applyChanges(changes, nodes) as NodeType[];
}
/**
* Drop in function that applies edge changes to an array of edges.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param edges - Array of edge to apply the changes to
* @returns Array of updated edges
* @example
* const onEdgesChange = useCallback(
(changes) => {
setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
},
[setEdges],
);
return (
<ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
);
*/
export function applyEdgeChanges<EdgeType extends Edge = Edge>(changes: EdgeChange[], edges: EdgeType[]): EdgeType[] {
return applyChanges(changes, edges) as EdgeType[];
}

View File

@@ -48,7 +48,6 @@ const restoreWindowsFromLS = (): WidgetGridItem[] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const raw = localStorage.getItem(SESSION_KEY.windows);
if (!raw) {
console.warn('No windows found in local storage!!');
return DEFAULT_WINDOWS;
}
@@ -64,7 +63,7 @@ const restoreWindowsFromLS = (): WidgetGridItem[] => {
};
export const MapInterface = () => {
const [items, setItems] = useState<WidgetGridItem[]>(restoreWindowsFromLS);
const [items, setItems] = useState<WidgetGridItem[]>(restoreWindowsFromLS());
return (
<WidgetsGrid

View File

@@ -6,23 +6,17 @@ import { SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { SHOW_DESCRIPTION_COLUMN_SETTING } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures';
import {
Setting,
COSMIC_SIGNATURE,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import { SignatureGroup } from '@/hooks/Mapper/types';
interface SystemLinkSignatureDialogProps {
data: CommandLinkSignatureToSystem;
setVisible: (visible: boolean) => void;
}
const signatureSettings: Setting[] = [
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true },
{ key: SignatureGroup.Wormhole, name: 'Wormhole', value: true },
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: true, isFilter: false },
];
const signatureSettings: Setting[] = [{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true }];
export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignatureDialogProps) => {
const { outCommand } = useMapRootState();
@@ -65,7 +59,6 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
>
<SystemSignaturesContent
systemId={`${data.solar_system_source}`}
hideLinkedSignatures
settings={signatureSettings}
onSelect={handleSelect}
selectable={true}

View File

@@ -5,7 +5,7 @@ import { SystemViewStandalone, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapp
import { getBackgroundClass, getShapeClass } from '@/hooks/Mapper/components/map/helpers';
import { MouseEvent, useCallback, useRef, useState } from 'react';
import { Commands } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export type RouteSystemProps = {
destination: number;
@@ -88,10 +88,11 @@ export interface RoutesListProps {
export const RoutesList = ({ data, onContextMenu }: RoutesListProps) => {
const [selected, setSelected] = useState<number | null>(null);
const { mapRef } = useMapRootState();
const handleClick = useCallback(
(systemId: number) => emitMapEvent({ name: Commands.centerSystem, data: systemId?.toString() }),
[],
(systemId: number) => mapRef.current?.command(Commands.centerSystem, systemId.toString()),
[mapRef],
);
if (!data.has_connection) {

View File

@@ -1,11 +1,12 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import {
RoutesType,
useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { CheckboxChangeEvent } from 'primereact/checkbox';
interface RoutesSettingsDialog {
visible: boolean;
@@ -37,8 +38,8 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
currentData.current = data;
const handleChangeEvent = useCallback(
(propName: keyof RoutesType) => (event: boolean) => {
optionsRef.current = { ...optionsRef.current, [propName]: event };
(propName: keyof RoutesType) => (event: CheckboxChangeEvent) => {
optionsRef.current = { ...optionsRef.current, [propName]: event.checked };
updateKey(x => x + 1);
},
[],
@@ -70,14 +71,14 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
setVisible(false);
}}
>
<div className="flex flex-col gap-3 p-2.5">
<div className="flex flex-col gap-2 mb-2">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
{checkboxes.map(({ label, propName }) => (
<PrettySwitchbox
<WdCheckbox
key={propName}
label={label}
checked={optionsRef.current[propName]}
setChecked={handleChangeEvent(propName)}
value={optionsRef.current[propName]}
onChange={handleChangeEvent(propName)}
/>
))}
</div>

View File

@@ -30,6 +30,7 @@ const sortByDist = (a: Route, b: Route) => {
export const RoutesWidgetContent = () => {
const {
data: { selectedSystems, hubs = [], systems, routes },
mapRef,
outCommand,
} = useMapRootState();
@@ -41,6 +42,7 @@ export const RoutesWidgetContent = () => {
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
outCommand,
hubs,
mapRef,
});
const preparedHubs = useMemo(() => {

View File

@@ -1,79 +0,0 @@
.verticalTabsContainer {
display: flex;
width: 100%;
min-height: 300px;
:global {
.p-tabview {
width: 100%;
display: flex;
align-items: flex-start;
}
.p-tabview-panels {
padding: 6px 1rem !important;
flex-grow: 1;
}
.p-tabview-nav-container {
border-right: none;
height: 100%;
}
.p-tabview-nav {
flex-direction: column;
width: 150px;
min-height: 100%;
border: none;
li {
width: 100%;
border-right: 4px solid var(--surface-hover);
background-color: var(--surface-card);
transition:
background-color 200ms,
border-right-color 200ms;
&:hover {
background-color: var(--surface-hover);
border-right: 4px solid var(--surface-100);
}
.p-tabview-nav-link {
transition: color 200ms;
justify-content: flex-end;
padding: 10px;
//background-color: var(--surface-card);
background-color: initial;
border: none;
color: var(--gray-400);
border-radius: initial;
font-weight: 400;
margin: 0;
}
&.p-tabview-selected {
background-color: var(--surface-50);
border-right: 4px solid var(--primary-color);
.p-tabview-nav-link {
font-weight: 600;
color: var(--primary-color);
}
&:hover {
//background-color: var(--surface-hover);
border-right: 4px solid var(--primary-color);
}
}
}
}
.p-tabview-panel {
flex-grow: 1;
}
}
}

View File

@@ -1,11 +1,9 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useState } from 'react';
import { Button } from 'primereact/button';
import { TabPanel, TabView } from 'primereact/tabview';
import styles from './SystemSignatureSettingsDialog.module.scss';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Checkbox } from 'primereact/checkbox';
export type Setting = { key: string; name: string; value: boolean; isFilter?: boolean };
export type Setting = { key: string; name: string; value: boolean };
export const COSMIC_SIGNATURE = 'Cosmic Signature';
export const COSMIC_ANOMALY = 'Cosmic Anomaly';
@@ -26,12 +24,8 @@ export const SystemSignatureSettingsDialog = ({
onSave,
onCancel,
}: SystemSignatureSettingsDialogProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const [settings, setSettings] = useState<Setting[]>(defaultSettings);
const filterSettings = settings.filter(setting => setting.isFilter);
const userSettings = settings.filter(setting => !setting.isFilter);
const handleSettingsChange = (key: string) => {
setSettings(prevState => prevState.map(item => (item.key === key ? { ...item, value: !item.value } : item)));
};
@@ -41,45 +35,23 @@ export const SystemSignatureSettingsDialog = ({
}, [onSave, settings]);
return (
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg h-[500px]">
<div className="flex flex-col gap-3 justify-between h-full">
<Dialog header="Filter signatures" visible draggable={false} style={{ width: '300px' }} onHide={onCancel}>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
className={styles.verticalTabView}
>
<TabPanel header="Filters" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{filterSettings.map(setting => {
return (
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{userSettings.map(setting => {
return (
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>
</TabPanel>
</TabView>
</div>
{settings.map(setting => {
return (
<div key={setting.key} className="flex items-center">
<Checkbox
inputId={setting.key}
checked={setting.value}
onChange={() => handleSettingsChange(setting.key)}
/>
<label htmlFor={setting.key} className="ml-2">
{setting.name}
</label>
</div>
);
})}
</div>
<div className="flex gap-2 justify-end">

View File

@@ -1,11 +1,5 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import {
InfoDrawer,
LayoutEventBlocker,
TooltipPosition,
WdImgButton,
WdCheckbox,
} from '@/hooks/Mapper/components/ui-kit';
import { InfoDrawer, LayoutEventBlocker, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import {
Setting,
@@ -18,41 +12,25 @@ import {
SHIP,
DRONE,
} from './SystemSignatureSettingsDialog';
import { SignatureGroup } from '@/hooks/Mapper/types';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { PrimeIcons } from 'primereact/api';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CheckboxChangeEvent } from 'primereact/checkbox';
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_2';
export const SHOW_DESCRIPTION_COLUMN_SETTING = 'show_description_column_setting';
export const SHOW_UPDATED_COLUMN_SETTING = 'SHOW_UPDATED_COLUMN_SETTING';
export const LAZY_DELETE_SIGNATURES_SETTING = 'LAZY_DELETE_SIGNATURES_SETTING';
export const KEEP_LAZY_DELETE_SETTING = 'KEEP_LAZY_DELETE_ENABLED_SETTING';
const settings: Setting[] = [
{ key: SHOW_UPDATED_COLUMN_SETTING, name: 'Show Updated Column', value: false, isFilter: false },
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: false, isFilter: false },
{ key: LAZY_DELETE_SIGNATURES_SETTING, name: 'Lazy Delete Signatures', value: false, isFilter: false },
{ key: KEEP_LAZY_DELETE_SETTING, name: 'Keep "Lazy Delete" Enabled', value: false, isFilter: false },
{ key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true, isFilter: true },
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true, isFilter: true },
{ key: DEPLOYABLE, name: 'Show Deployables', value: true, isFilter: true },
{ key: STRUCTURE, name: 'Show Structures', value: true, isFilter: true },
{ key: STARBASE, name: 'Show Starbase', value: true, isFilter: true },
{ key: SHIP, name: 'Show Ships', value: true, isFilter: true },
{ key: DRONE, name: 'Show Drones And Charges', value: true, isFilter: true },
{ key: SignatureGroup.Wormhole, name: 'Show Wormholes', value: true, isFilter: true },
{ key: SignatureGroup.RelicSite, name: 'Show Relic Sites', value: true, isFilter: true },
{ key: SignatureGroup.DataSite, name: 'Show Data Sites', value: true, isFilter: true },
{ key: SignatureGroup.OreSite, name: 'Show Ore Sites', value: true, isFilter: true },
{ key: SignatureGroup.GasSite, name: 'Show Gas Sites', value: true, isFilter: true },
{ key: SignatureGroup.CombatSite, name: 'Show Combat Sites', value: true, isFilter: true },
{ key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true },
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true },
{ key: DEPLOYABLE, name: 'Show Deployables', value: true },
{ key: STRUCTURE, name: 'Show Structures', value: true },
{ key: STARBASE, name: 'Show Starbase', value: true },
{ key: SHIP, name: 'Show Ships', value: true },
{ key: DRONE, name: 'Show Drones And Charges', value: true },
];
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings';
const defaultSettings = () => {
return [...settings];
};
@@ -69,25 +47,12 @@ export const SystemSignatures = () => {
const isNotSelectedSystem = selectedSystems.length !== 1;
const lazyDeleteValue = useMemo(() => {
return settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)!.value;
}, [settings]);
const handleSettingsChange = useCallback((settings: Setting[]) => {
setSettings(settings);
localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(settings));
setVisible(false);
}, []);
const handleLazyDeleteChange = useCallback((value: boolean) => {
setSettings(settings => {
const lazyDelete = settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)!;
lazyDelete.value = value;
localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(settings));
return [...settings];
});
}, []);
useEffect(() => {
const restoredSettings = localStorage.getItem(SIGNATURE_SETTINGS_KEY);
@@ -103,15 +68,6 @@ export const SystemSignatures = () => {
<div className="flex gap-1">System Signatures</div>
<LayoutEventBlocker className="flex gap-2.5">
<WdCheckbox
size="xs"
labelSide="left"
label={'Lazy delete'}
value={lazyDeleteValue}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={(event: CheckboxChangeEvent) => handleLazyDeleteChange(!!event.checked)}
/>
<WdImgButton
className={PrimeIcons.QUESTION_CIRCLE}
tooltip={{
@@ -135,7 +91,8 @@ export const SystemSignatures = () => {
</InfoDrawer>
<InfoDrawer title={<b className="text-slate-50">How to delete?</b>}>
For delete any signature first of all you need select before
<br /> and then use <b className="text-sky-500">Del</b>
<br /> and then use <b className="text-sky-500">Del</b> or{' '}
<b className="text-sky-500">Backspace</b>
</InfoDrawer>
</div>
) as React.ReactNode,
@@ -151,7 +108,7 @@ export const SystemSignatures = () => {
System is not selected
</div>
) : (
<SystemSignaturesContent systemId={systemId} settings={settings} onLazyDeleteChange={handleLazyDeleteChange} />
<SystemSignaturesContent systemId={systemId} settings={settings} />
)}
{visible && (
<SystemSignatureSettingsDialog

View File

@@ -1,8 +1,8 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useClipboard } from '@/hooks/Mapper/hooks/useClipboard';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { DataTable, DataTableRowClickEvent, DataTableRowMouseEvent, SortOrder } from 'primereact/datatable';
import { Column } from 'primereact/column';
@@ -11,7 +11,6 @@ import useRefState from 'react-usestateref';
import { Setting } from '../SystemSignatureSettingsDialog';
import { useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { useClipboard } from '@/hooks/Mapper/hooks/useClipboard';
import classes from './SystemSignaturesContent.module.scss';
import clsx from 'clsx';
@@ -22,55 +21,40 @@ import {
getRowColorByTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers';
import {
renderAddedTimeLeft,
renderDescription,
renderIcon,
renderInfoColumn,
renderUpdatedTimeLeft,
renderTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import useLocalStorageState from 'use-local-storage-state';
import { PrimeIcons } from 'primereact/api';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import {
SHOW_DESCRIPTION_COLUMN_SETTING,
SHOW_UPDATED_COLUMN_SETTING,
LAZY_DELETE_SIGNATURES_SETTING,
KEEP_LAZY_DELETE_SETTING,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures';
type SystemSignaturesSortSettings = {
sortField: string;
sortOrder: SortOrder;
};
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
sortField: 'inserted_at',
sortField: 'updated_at',
sortOrder: -1,
};
interface SystemSignaturesContentProps {
systemId: string;
settings: Setting[];
hideLinkedSignatures?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
}
export const SystemSignaturesContent = ({
systemId,
settings,
hideLinkedSignatures,
selectable,
onSelect,
onLazyDeleteChange,
}: SystemSignaturesContentProps) => {
export const SystemSignaturesContent = ({ systemId, settings, selectable, onSelect }: SystemSignaturesContentProps) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]);
const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [parsedSignatures, setParsedSignatures] = useState<SystemSignature[]>([]);
const [askUser, setAskUser] = useState(false);
const [selectedSignature, setSelectedSignature] = useState<SystemSignature | null>(null);
const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null);
@@ -82,20 +66,10 @@ export const SystemSignaturesContent = ({
const tableRef = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(tableRef, 260);
const medium = useMaxWidth(tableRef, 380);
const refData = useRef({ selectable });
refData.current = { selectable };
const tooltipRef = useRef<WdTooltipHandlers>(null);
const { clipboardContent, setClipboardContent } = useClipboard();
const lazyDeleteValue = useMemo(() => {
return settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false;
}, [settings]);
const keepLazyDeleteValue = useMemo(() => {
return settings.find(setting => setting.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false;
}, [settings]);
const { clipboardContent } = useClipboard();
const handleResize = useCallback(() => {
if (tableRef.current) {
@@ -106,38 +80,13 @@ export const SystemSignaturesContent = ({
}
}, []);
const groupSettings = useMemo(() => settings.filter(s => (GROUPS_LIST as string[]).includes(s.key)), [settings]);
const showDescriptionColumn = useMemo(
() => settings.find(s => s.key === SHOW_DESCRIPTION_COLUMN_SETTING)?.value,
[settings],
);
const showUpdatedColumn = useMemo(() => settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value, [settings]);
const filteredSignatures = useMemo(() => {
return signatures
.filter(x => {
if (hideLinkedSignatures && !!x.linked_system) {
return false;
}
const isCosmicSignature = x.kind === COSMIC_SIGNATURE;
if (isCosmicSignature) {
const showCosmicSignatures = settings.find(y => y.key === COSMIC_SIGNATURE)?.value;
if (showCosmicSignatures) {
return !x.group || groupSettings.find(y => y.key === x.group)?.value;
} else {
return !!x.group && groupSettings.find(y => y.key === x.group)?.value;
}
}
return settings.find(y => y.key === x.kind)?.value;
})
.filter(x => settings.find(y => y.key === x.kind)?.value)
.sort((a, b) => {
return new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime();
});
}, [signatures, settings, groupSettings, hideLinkedSignatures]);
}, [signatures, settings]);
const handleGetSignatures = useCallback(async () => {
const { signatures } = await outCommand({
@@ -145,17 +94,33 @@ export const SystemSignaturesContent = ({
data: { system_id: systemId },
});
setAskUser(false);
setSignatures(signatures);
}, [outCommand, systemId]);
// const updateSignatures = useCallback(
// async (newSignatures: SystemSignature[], updateOnly: boolean) => {
// const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
// const { signatures: updatedSignatures } = await outCommand({
// type: OutCommand.updateSignatures,
// data: {
// system_id: systemId,
// added,
// updated,
// removed,
// },
// });
// setSignatures(() => updatedSignatures);
// setSelectedSignatures([]);
// },
// [outCommand, systemId],
// );
const handleUpdateSignatures = useCallback(
async (newSignatures: SystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newSignatures,
updateOnly,
skipUpdateUntouched,
);
async (newSignatures: SystemSignature[], updateOnly: boolean) => {
const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
const { signatures: updatedSignatures } = await outCommand({
type: OutCommand.updateSignatures,
@@ -173,32 +138,34 @@ export const SystemSignaturesContent = ({
[outCommand, systemId],
);
const handleDeleteSelected = useCallback(
async (e: KeyboardEvent) => {
if (selectable) {
return;
}
if (selectedSignatures.length === 0) {
return;
}
e.preventDefault();
e.stopPropagation();
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(
signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
true,
);
},
[handleUpdateSignatures, selectable, signatures, selectedSignatures],
);
const handleDeleteSelected = useCallback(async () => {
if (selectable) {
return;
}
if (selectedSignatures.length === 0) {
return;
}
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(
signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
);
}, [handleUpdateSignatures, selectable, signatures, selectedSignatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
}, [signatures]);
const handleReplaceAll = useCallback(() => {
handleUpdateSignatures(parsedSignatures, false);
setAskUser(false);
}, [parsedSignatures, handleUpdateSignatures]);
const handleUpdateOnly = useCallback(() => {
handleUpdateSignatures(parsedSignatures, true);
setAskUser(false);
}, [parsedSignatures, handleUpdateSignatures]);
const handleSelectSignatures = useCallback(
// TODO still will be good to define types if we use typescript
// @ts-ignore
@@ -212,51 +179,38 @@ export const SystemSignaturesContent = ({
[onSelect, selectable],
);
const handlePaste = async (clipboardContent: string) => {
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected);
useEffect(() => {
if (selectable) {
return;
}
if (!clipboardContent) {
return;
}
const newSignatures = parseSignatures(
clipboardContent,
settings.map(x => x.key),
);
handleUpdateSignatures(newSignatures, !lazyDeleteValue);
const { removed } = getActualSigs(signaturesRef.current, newSignatures, false);
if (lazyDeleteValue && !keepLazyDeleteValue) {
onLazyDeleteChange?.(false);
if (!signaturesRef.current || !signaturesRef.current.length || !removed.length) {
handleUpdateSignatures(newSignatures, false);
} else {
setParsedSignatures(newSignatures);
setAskUser(true);
}
};
const handleEnterRow = useCallback(
(e: DataTableRowMouseEvent) => {
setHoveredSig(filteredSignatures[e.index]);
tooltipRef.current?.show(e.originalEvent);
},
[filteredSignatures],
);
const handleLeaveRow = useCallback((e: DataTableRowMouseEvent) => {
tooltipRef.current?.hide(e.originalEvent);
setHoveredSig(null);
}, []);
useEffect(() => {
if (refData.current.selectable) {
return;
}
if (!clipboardContent?.text) {
return;
}
handlePaste(clipboardContent.text);
setClipboardContent(null);
}, [clipboardContent, selectable, lazyDeleteValue, keepLazyDeleteValue]);
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected);
}, [clipboardContent, selectable]);
useEffect(() => {
if (!systemId) {
setSignatures([]);
setAskUser(false);
return;
}
@@ -290,6 +244,19 @@ export const SystemSignaturesContent = ({
};
}, []);
const handleEnterRow = useCallback(
(e: DataTableRowMouseEvent) => {
setHoveredSig(filteredSignatures[e.index]);
tooltipRef.current?.show(e.originalEvent);
},
[filteredSignatures],
);
const handleLeaveRow = useCallback((e: DataTableRowMouseEvent) => {
tooltipRef.current?.hide(e.originalEvent);
setHoveredSig(null);
}, []);
const renderToolbar = (/*row: SystemSignature*/) => {
return (
<div className="flex justify-end items-center gap-2 mr-[4px]">
@@ -341,7 +308,7 @@ export const SystemSignaturesContent = ({
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200');
}
const dateClass = getRowColorByTimeLeft(row.inserted_at ? new Date(row.inserted_at) : undefined);
const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined);
if (!dateClass) {
return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200');
}
@@ -368,7 +335,6 @@ export const SystemSignaturesContent = ({
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={compact}
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
sortable
></Column>
<Column
@@ -379,46 +345,22 @@ export const SystemSignaturesContent = ({
style={{ maxWidth: nameColumnWidth }}
hidden={compact || medium}
></Column>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
body={renderDescription}
hidden={compact}
sortable
></Column>
)}
<Column
field="inserted_at"
header="Added"
field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderAddedTimeLeft}
body={renderTimeLeft}
sortable
></Column>
{showUpdatedColumn && (
<Column
field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderUpdatedTimeLeft}
sortable
></Column>
)}
{!selectable && (
<Column
bodyClassName="p-0 pl-1 pr-2"
field="group"
body={renderToolbar}
// headerClassName={headerClasses}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
></Column>
)}
<Column
bodyClassName="p-0 pl-1 pr-2"
field="group"
body={renderToolbar}
// headerClassName={headerClasses}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
></Column>
</DataTable>
</>
)}
@@ -428,13 +370,32 @@ export const SystemSignaturesContent = ({
content={hoveredSig ? <SignatureView {...hoveredSig} /> : null}
/>
{showSignatureSettings && (
<SignatureSettings
systemId={systemId}
show
onHide={() => setShowSignatureSettings(false)}
signatureData={selectedSignature}
/>
<SignatureSettings
systemId={systemId}
show={showSignatureSettings}
onHide={() => setShowSignatureSettings(false)}
signatureData={selectedSignature}
/>
{askUser && (
<div className="absolute left-[1px] top-[29px] h-[calc(100%-30px)] w-[calc(100%-3px)] bg-stone-900/10 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
<div className="text-stone-400/80 text-sm">
<div className="flex flex-col text-center gap-2">
<button className="p-button p-component p-button-outlined p-button-sm btn-wide">
<span className="p-button-label p-c" onClick={handleUpdateOnly}>
Update
</span>
</button>
<button className="p-button p-component p-button-outlined p-button-sm btn-wide">
<span className="p-button-label p-c" onClick={handleReplaceAll}>
Update & Delete missing
</span>
</button>
</div>
</div>
</div>
</div>
)}
</div>
</>

View File

@@ -6,7 +6,6 @@ export const getActualSigs = (
oldSignatures: SystemSignature[],
newSignatures: SystemSignature[],
updateOnly: boolean,
skipUpdateUntouched?: boolean,
): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => {
const updated: SystemSignature[] = [];
const removed: SystemSignature[] = [];
@@ -20,8 +19,6 @@ export const getActualSigs = (
const isNeedUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
if (isNeedUpgrade) {
updated.push({ ...oldSig, group: newSig.group, name: newSig.name });
} else if (!skipUpdateUntouched) {
updated.push({ ...oldSig });
}
} else {
if (!updateOnly) {

View File

@@ -1,7 +1,5 @@
export * from './renderIcon';
export * from './renderDescription';
export * from './renderName';
export * from './renderAddedTimeLeft';
export * from './renderUpdatedTimeLeft';
export * from './renderTimeLeft';
export * from './renderLinkedSystem';
export * from './renderInfoColumn';

View File

@@ -1,10 +0,0 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { TimeLeft } from '@/hooks/Mapper/components/ui-kit';
export const renderAddedTimeLeft = (row: SystemSignature) => {
return (
<div className="flex w-full items-center">
<TimeLeft cDate={row.inserted_at ? new Date(row.inserted_at) : undefined} />
</div>
);
};

View File

@@ -1,5 +0,0 @@
import { SystemSignature } from '@/hooks/Mapper/types';
export const renderDescription = (row: SystemSignature) => {
return <span title={row?.description}>{row?.description}</span>;
};

View File

@@ -0,0 +1,3 @@
.whFontSize {
font-size: 11px !important;
}

View File

@@ -1,32 +1,18 @@
import { PrimeIcons } from 'primereact/api';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import {
k162Types,
renderK162Type,
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import clsx from 'clsx';
import { renderName } from './renderName.tsx';
import classes from './renderInfoColumn.module.scss';
export const renderInfoColumn = (row: SystemSignature) => {
if (!row.group || row.group === SignatureGroup.Wormhole) {
let k162TypeOption = null;
if (row.custom_info) {
const customInfo = JSON.parse(row.custom_info);
if (customInfo.k162Type) {
k162TypeOption = k162Types.find(x => x.value === customInfo.k162Type);
}
}
return (
<div className="flex justify-start items-center gap-[4px]">
<div className="flex justify-start items-center gap-[6px]">
{row.type && (
<WHClassView
className="text-[11px]"
classNameWh="!text-[11px] !font-bold"
classNameWh={classes.whFontSize}
highlightName
hideWhClass={!!row.linked_system}
whClassName={row.type}
noOffset
@@ -34,10 +20,9 @@ export const renderInfoColumn = (row: SystemSignature) => {
/>
)}
{!row.linked_system && row.type === 'K162' && !!k162TypeOption && <>{renderK162Type(k162TypeOption)}</>}
{row.linked_system && (
<>
{/*<span className="w-4 h-4 hero-arrow-long-right"></span>*/}
<span title={row.linked_system?.solar_system_name}>
<SystemViewStandalone
className={clsx('select-none text-center cursor-context-menu')}
@@ -47,23 +32,13 @@ export const renderInfoColumn = (row: SystemSignature) => {
</span>
</>
)}
{row.description && (
<WdTooltipWrapper content={row.description}>
<span className={clsx(PrimeIcons.EXCLAMATION_CIRCLE, 'text-[12px]')}></span>
</WdTooltipWrapper>
)}
</div>
);
}
return (
<div className="flex gap-1 items-center">
{renderName(row)}{' '}
{row.description && (
<WdTooltipWrapper content={row.description}>
<span className={clsx(PrimeIcons.EXCLAMATION_CIRCLE, 'text-[12px]')}></span>
</WdTooltipWrapper>
)}
</div>
);
if (row.description != null && row.description.length > 0) {
return <span title={row.description}>{row.description}</span>;
}
return renderName(row);
};

View File

@@ -1,7 +1,7 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { TimeLeft } from '@/hooks/Mapper/components/ui-kit';
export const renderUpdatedTimeLeft = (row: SystemSignature) => {
export const renderTimeLeft = (row: SystemSignature) => {
return (
<div className="flex w-full items-center">
<TimeLeft cDate={row.updated_at ? new Date(row.updated_at) : undefined} />

View File

@@ -7,13 +7,13 @@ import { useCallback, useState } from 'react';
import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/components';
import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx';
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
import { MapSettings } from "@/hooks/Mapper/components/mapRootContent/components/MapSettings";
export interface MapRootContentProps {}
// eslint-disable-next-line no-empty-pattern
export const MapRootContent = ({}: MapRootContentProps) => {
const { interfaceSettings } = useMapRootState();
const { mapRef, interfaceSettings } = useMapRootState();
const { isShowMenu } = interfaceSettings;
const [showOnTheMap, setShowOnTheMap] = useState(false);
@@ -26,7 +26,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
useSkipContextMenu();
return (
<Layout map={<MapWrapper />}>
<Layout map={<MapWrapper refn={mapRef} />}>
{!isShowMenu ? (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<div className="absolute top-0 left-0 w-[calc(100%-3.5rem)] h-full pointer-events-none">

View File

@@ -1,22 +1,13 @@
import classes from './Connections.module.scss';
import { Sidebar } from 'primereact/sidebar';
import { useEffect, useMemo, useState, useCallback } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import {
ConnectionType,
ConnectionOutput,
ConnectionInfoOutput,
OutCommand,
Passage,
SolarSystemConnection,
} from '@/hooks/Mapper/types';
import { ConnectionOutput, OutCommand, Passage, SolarSystemConnection } from '@/hooks/Mapper/types';
import { PassageCard } from './PassageCard';
import { InfoDrawer, SystemView } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
@@ -77,49 +68,26 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
return connections.find(x => x.source === selectedConnection.source && x.target === selectedConnection.target);
}, [connections, selectedConnection]);
const isWormhole = useMemo(() => {
return cnInfo?.type !== ConnectionType.gate;
}, [cnInfo]);
const [passages, setPassages] = useState<Passage[]>([]);
const [info, setInfo] = useState<ConnectionInfoOutput | null>(null);
const loadInfo = useCallback(
async (connection: SolarSystemConnection) => {
const result = await outCommand<ConnectionInfoOutput>({
type: OutCommand.getConnectionInfo,
data: {
from: connection.source,
to: connection.target,
},
});
setInfo(result);
},
[outCommand],
);
const loadPassages = useCallback(
async (connection: SolarSystemConnection) => {
const result = await outCommand<ConnectionOutput>({
type: OutCommand.getPassages,
data: {
from: connection.source,
to: connection.target,
},
});
setPassages(result.passages.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at)));
},
[outCommand],
);
useEffect(() => {
if (!selectedConnection) {
return;
}
loadInfo(selectedConnection);
loadPassages(selectedConnection);
const loadInfo = async () => {
const result = await outCommand<ConnectionOutput>({
type: OutCommand.getPassages,
data: {
from: selectedConnection.source,
to: selectedConnection.target,
},
});
setPassages(result.passages.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at)));
};
loadInfo();
}, [selectedConnection]);
const approximateMass = useMemo(() => {
@@ -141,7 +109,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
>
<div className={clsx(classes.SidebarContent, '')}>
{/* Connection Info */}
<div className="px-2 flex flex-col gap-2">
<div className="px-2 pb-3 flex flex-col gap-2">
{/* Connection Info Row */}
<InfoDrawer title="Connection" rightSide>
<div className="flex justify-end gap-2 items-center">
@@ -159,25 +127,10 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
</div>
</InfoDrawer>
<div className="flex justify-between gap-2">
{/*Left column*/}
<div>
{isWormhole && info?.marl_eol_time && (
<InfoDrawer title="Mark EOL Time">
<TimeAgo timestamp={info.marl_eol_time} />
</InfoDrawer>
)}
</div>
{/*Right column*/}
<div>
{isWormhole && (
<InfoDrawer title="Approximate mass of passages" rightSide>
{kgToTons(approximateMass)}
</InfoDrawer>
)}
</div>
</div>
{/* Connection Info Row */}
<InfoDrawer title="Approximate mass of passages" rightSide>
{kgToTons(approximateMass)}
</InfoDrawer>
<div className="flex gap-2"></div>
</div>

View File

@@ -5,8 +5,6 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { OutCommand } from '@/hooks/Mapper/types';
import { MenuItem } from 'primereact/menuitem';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export interface MapContextMenuProps {
onShowOnTheMap?: () => void;
@@ -16,8 +14,6 @@ export interface MapContextMenuProps {
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContextMenuProps) => {
const { outCommand, setInterfaceSettings } = useMapRootState();
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
const menuRight = useRef<Menu>(null);
const handleAddCharacter = useCallback(() => {
@@ -28,40 +24,34 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
}, [outCommand]);
const items = useMemo(() => {
return (
[
{
label: 'Tracking',
icon: 'pi pi-user-plus',
command: handleAddCharacter,
visible: true,
},
{
label: 'On the map',
icon: 'pi pi-hashtag',
command: onShowOnTheMap,
visible: canTrackCharacters,
},
{ separator: true, visible: true },
{
label: 'Settings',
icon: `pi pi-cog`,
command: onShowMapSettings,
visible: true,
},
{
label: 'Dock menu',
icon: 'pi pi-window-maximize',
command: () =>
setInterfaceSettings(x => ({
...x,
isShowMenu: !x.isShowMenu,
})),
visible: true,
},
] as MenuItem[]
).filter(item => item.visible);
}, [canTrackCharacters, handleAddCharacter, onShowMapSettings, onShowOnTheMap, setInterfaceSettings]);
return [
{
label: 'Tracking',
icon: 'pi pi-user-plus',
command: handleAddCharacter,
},
{
label: 'On the map',
icon: 'pi pi-hashtag',
command: onShowOnTheMap,
},
{ separator: true },
{
label: 'Settings',
icon: `pi pi-cog`,
command: onShowMapSettings,
},
{
label: 'Dock menu',
icon: 'pi pi-window-maximize',
command: () =>
setInterfaceSettings(x => ({
...x,
isShowMenu: !x.isShowMenu,
})),
},
] as MenuItem[];
}, [handleAddCharacter, onShowMapSettings, onShowOnTheMap, setInterfaceSettings]);
return (
<div className="ml-1">

View File

@@ -61,7 +61,6 @@ const CONNECTIONS_CHECKBOXES_PROPS: CheckboxesList = [
const UI_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowMenu, label: 'Enable compact map menu bar' },
{ prop: InterfaceStoredSettingsProps.isThickConnections, label: 'Thicker connections' },
];
export const MapSettings = ({ show, onHide }: MapSettingsProps) => {

View File

@@ -6,9 +6,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
interface RightBarProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
@@ -17,8 +14,6 @@ interface RightBarProps {
export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) => {
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
const handleAddCharacter = useCallback(() => {
@@ -69,17 +64,15 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
</button>
</WdTooltipWrapper>
{canTrackCharacters && (
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowOnTheMap}
>
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
)}
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowOnTheMap}
>
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
</div>
<div className="flex flex-col items-center mb-2 gap-1">

View File

@@ -1,5 +1,6 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect } from 'react';
// import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import {
@@ -24,120 +25,102 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
const { outCommand } = useMapRootState();
const handleShow = async () => {};
const signatureForm = useForm<Partial<SystemSignaturePrepared>>({});
const form = useForm<Partial<SystemSignaturePrepared>>({});
const handleSave = useCallback(
async (e: any) => {
e?.preventDefault();
if (!signatureData) {
return;
}
const { group, ...values } = signatureForm.getValues();
let out = { ...signatureData };
switch (group) {
case SignatureGroup.Wormhole:
if (values.linked_system) {
await outCommand({
type: OutCommand.linkSignatureToSystem,
data: {
signature_eve_id: signatureData.eve_id,
solar_system_source: systemId,
solar_system_target: values.linked_system,
},
});
}
out = {
...out,
custom_info: JSON.stringify({
k162Type: values.k162Type,
}),
};
if (values.type != null) {
out = { ...out, type: values.type };
}
if (signatureData.group !== SignatureGroup.Wormhole) {
out = { ...out, name: '' };
}
break;
case SignatureGroup.CosmicSignature:
out = { ...out, type: '', name: '' };
break;
default:
if (values.name != null) {
out = { ...out, name: values.name ?? '' };
}
}
if (values.description != null) {
out = { ...out, description: values.description };
}
// Note: when type of signature changed from WH to other type - we should drop name
if (
group !== SignatureGroup.Wormhole && // new
signatureData.group === SignatureGroup.Wormhole && // prev
signatureData.linked_system
) {
await outCommand({
type: OutCommand.unlinkSignature,
data: { signature_eve_id: signatureData.eve_id, solar_system_source: systemId },
});
out = { ...out, type: '' };
}
if (group === SignatureGroup.Wormhole && signatureData.linked_system != null && values.linked_system === null) {
await outCommand({
type: OutCommand.unlinkSignature,
data: { signature_eve_id: signatureData.eve_id, solar_system_source: systemId },
});
}
// Note: despite groups have optional type - this will always set
out = { ...out, group: group! };
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added: [],
updated: [out],
removed: [],
},
});
signatureForm.reset();
onHide();
},
[signatureForm, onHide, outCommand, signatureData, systemId],
);
useEffect(() => {
const handleSave = useCallback(async () => {
if (!signatureData) {
signatureForm.reset();
return;
}
const { linked_system, custom_info, ...rest } = signatureData;
const { group, ...values } = form.getValues();
let out = { ...signatureData };
let k162Type = null;
if (custom_info) {
const customInfo = JSON.parse(custom_info);
k162Type = customInfo.k162Type;
switch (group) {
case SignatureGroup.Wormhole:
if (values.linked_system) {
await outCommand({
type: OutCommand.linkSignatureToSystem,
data: {
signature_eve_id: signatureData.eve_id,
solar_system_source: systemId,
solar_system_target: values.linked_system,
},
});
}
if (values.type != null) {
out = { ...out, type: values.type };
}
if (signatureData.group !== SignatureGroup.Wormhole) {
out = { ...out, name: '' };
}
break;
case SignatureGroup.CosmicSignature:
out = { ...out, type: '', name: '' };
break;
default:
if (values.name != null) {
out = { ...out, name: values.name ?? '' };
}
}
signatureForm.reset({
if (values.description != null) {
out = { ...out, description: values.description };
}
// Note: when type of signature changed from WH to other type - we should drop name
if (
group !== SignatureGroup.Wormhole && // new
signatureData.group === SignatureGroup.Wormhole && // prev
signatureData.linked_system
) {
await outCommand({
type: OutCommand.unlinkSignature,
data: { signature_eve_id: signatureData.eve_id, solar_system_source: systemId },
});
out = { ...out, type: '' };
}
if (group === SignatureGroup.Wormhole && signatureData.linked_system != null && values.linked_system === null) {
await outCommand({
type: OutCommand.unlinkSignature,
data: { signature_eve_id: signatureData.eve_id, solar_system_source: systemId },
});
}
// Note: despite groups have optional type - this will always set
out = { ...out, group: group! };
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added: [],
updated: [out],
removed: [],
},
});
form.reset();
onHide();
}, [form, onHide, outCommand, signatureData, systemId]);
useEffect(() => {
if (!signatureData) {
form.reset();
return;
}
const { linked_system, ...rest } = signatureData;
form.reset({
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
k162Type: k162Type,
...rest,
});
}, [signatureForm, signatureData]);
}, [form, signatureData]);
return (
<Dialog
@@ -155,34 +138,32 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
}}
>
<SystemsSettingsProvider initialValue={{ systemId }}>
<FormProvider {...signatureForm}>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-2 justify-between">
<div className="w-full flex flex-col gap-1 p-1 min-h-[150px]">
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Group:</span>
<SignatureGroupSelect name="group" />
</label>
<FormProvider {...form}>
<div className="flex flex-col gap-2 justify-between">
<div className="w-full flex flex-col gap-1 p-1 min-h-[150px]">
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Group:</span>
<SignatureGroupSelect name="group" />
</label>
<SignatureGroupContent />
<SignatureGroupContent />
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Description:</span>
<Controller
name="description"
control={signatureForm.control}
render={({ field }) => (
<InputText placeholder="Type description" value={field.value} onChange={field.onChange} />
)}
/>
</label>
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
</div>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Description:</span>
<Controller
name="description"
control={form.control}
render={({ field }) => (
<InputText placeholder="Type description" value={field.value} onChange={field.onChange} />
)}
/>
</label>
</div>
</form>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} outlined size="small" label="Save"></Button>
</div>
</div>
</FormProvider>
</SystemsSettingsProvider>
</Dialog>

View File

@@ -1,13 +1,7 @@
import { useFormContext } from 'react-hook-form';
import { SystemSignature } from '@/hooks/Mapper/types';
import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureWormholeTypeSelect';
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
export const SignatureGroupContentWormholes = () => {
const { watch } = useFormContext<SystemSignature>();
const type = watch('type');
return (
<>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
@@ -15,13 +9,6 @@ export const SignatureGroupContentWormholes = () => {
<SignatureWormholeTypeSelect name="type" />
</label>
{type === 'K162' && (
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>K162 Type:</span>
<SignatureK162TypeSelect name="k162Type" />
</label>
)}
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Leads To:</span>
<SignatureLeadsToSelect name="linked_system" />

View File

@@ -1,136 +0,0 @@
import { Dropdown } from 'primereact/dropdown';
import clsx from 'clsx';
import { Controller, useFormContext } from 'react-hook-form';
import { useMemo } from 'react';
import { SystemSignature } from '@/hooks/Mapper/types';
import { WHClassView } from '@/hooks/Mapper/components/ui-kit';
export const k162Types = [
{
label: 'Hi-Sec',
value: 'hs',
whClassName: 'A641',
},
{
label: 'Low-Sec',
value: 'ls',
whClassName: 'J377',
},
{
label: 'Null-Sec',
value: 'ns',
whClassName: 'C248',
},
{
label: 'C1',
value: 'c1',
whClassName: 'E004',
},
{
label: 'C2',
value: 'c2',
whClassName: 'D382',
},
{
label: 'C3',
value: 'c3',
whClassName: 'L477',
},
{
label: 'C4',
value: 'c4',
whClassName: 'M001',
},
{
label: 'C5',
value: 'c5',
whClassName: 'L614',
},
{
label: 'C6',
value: 'c6',
whClassName: 'G008',
},
{
label: 'C13',
value: 'c13',
whClassName: 'A009',
},
{
label: 'Thera',
value: 'thera',
whClassName: 'F353',
},
{
label: 'Pochven',
value: 'pochven',
whClassName: 'F216',
},
];
const renderNoValue = () => <div className="flex gap-2 items-center">-Unknown-</div>;
// @ts-ignore
export const renderK162Type = (option: {
label?: string;
value: string;
security?: string;
system_class?: number;
whClassName?: string;
}) => {
if (!option) {
return renderNoValue();
}
const { value, whClassName = '' } = option;
if (value == null) {
return renderNoValue();
}
return (
<WHClassView
classNameWh="!text-[11px] !font-bold"
hideWhClassName
hideTooltip
whClassName={whClassName}
noOffset
useShortTitle
/>
);
};
export interface SignatureK162TypeSelectProps {
name: string;
defaultValue?: string;
}
export const SignatureK162TypeSelect = ({ name, defaultValue = '' }: SignatureK162TypeSelectProps) => {
const { control } = useFormContext<SystemSignature>();
const options = useMemo(() => {
return [{ value: null }, ...k162Types];
}, []);
return (
<Controller
// @ts-ignore
name={name}
control={control}
defaultValue={defaultValue}
render={({ field }) => {
return (
<Dropdown
value={field.value}
onChange={field.onChange}
options={options}
optionValue="value"
placeholder="Select K162 type"
className={clsx('w-full')}
scrollHeight="240px"
itemTemplate={renderK162Type}
valueTemplate={renderK162Type}
/>
);
}}
/>
);
};

View File

@@ -13,14 +13,13 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
// @ts-ignore
const renderLinkedSystemItem = (option: { value: string }) => {
const { value } = option;
if (value == null) {
return <div className="flex gap-2 items-center">- Unknown -</div>;
if (option.value == null) {
return <div className="flex gap-2 items-center">No linked system</div>;
}
return (
<div className="flex gap-2 items-center">
<SystemView systemId={value} className={classes.SystemView} />
<SystemView systemId={option.value} className={classes.SystemView} />
</div>
);
};
@@ -66,7 +65,6 @@ export const SignatureLeadsToSelect = ({ name, defaultValue = '' }: SignatureLea
const leadsToOptions = useMemo(() => {
return [
{ value: null },
...leadsTo
.filter(systemId => {
const systemStatic = systemStatics.get(parseInt(systemId));

View File

@@ -1,3 +1,2 @@
export * from './SignatureGroupSelect';
export * from './SignatureGroupContent';
export * from './SignatureK162TypeSelect';

View File

@@ -1,14 +1,14 @@
import { Map } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useRef, useState } from 'react';
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { ForwardedRef, useCallback, useRef, useState } from 'react';
import { MapHandlers, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal';
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import {
SystemCustomLabelDialog,
SystemLinkSignatureDialog,
SystemSettingsDialog,
SystemLinkSignatureDialog,
} from '@/hooks/Mapper/components/mapInterface/components';
import classes from './MapWrapper.module.scss';
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
@@ -20,45 +20,25 @@ import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
interface MapWrapperProps {
refn: ForwardedRef<MapHandlers>;
}
// TODO: INFO - this component needs for abstract work with Map instance
export const MapWrapper = () => {
export const MapWrapper = ({ refn }: MapWrapperProps) => {
const {
update,
outCommand,
data: { selectedConnections, selectedSystems, hubs, systems },
interfaceSettings: {
isShowMenu,
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
isShowKSpace,
isThickConnections,
},
interfaceSettings: { isShowMenu, isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap, isShowKSpace },
} = useMapRootState();
const { deleteSystems } = useDeleteSystems();
const { mapRef, runCommand } = useCommonMapEventProcessor();
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
const { handleSystemMultipleContext, ...systemMultipleCtxProps } = useContextMenuSystemMultipleHandlers();
const [openSettings, setOpenSettings] = useState<string | null>(null);
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
useMapEventListener(event => {
switch (event.name) {
case Commands.linkSignatureToSystem:
setOpenLinkSignatures(event.data);
return true;
}
runCommand(event);
});
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems };
const onSelectionChange: OnMapSelectionChange = useCallback(
({ systems, connections }) => {
@@ -79,6 +59,9 @@ export const MapWrapper = () => {
[update],
);
const [openSettings, setOpenSettings] = useState<string | null>(null);
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
const handleCommand: OutCommandHandler = useCallback(
event => {
switch (event.type) {
@@ -112,19 +95,22 @@ export const MapWrapper = () => {
[open],
);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const handleConnectionDbClick = useCallback((e: SolarSystemConnection) => setSelectedConnection(e), []);
const handleManualDelete = useCallback((toDelete: string[]) => {
const restDel = toDelete.filter(x => ref.current.systems.some(y => y.id === x));
if (restDel.length > 0) {
ref.current.deleteSystems(restDel);
useMapEventListener(event => {
switch (event.name) {
case Commands.linkSignatureToSystem:
setOpenLinkSignatures(event.data);
return true;
}
}, []);
});
return (
<>
<Map
ref={mapRef}
ref={refn}
onCommand={handleCommand}
onSelectionChange={onSelectionChange}
onConnectionInfoClick={handleConnectionDbClick}
@@ -133,8 +119,6 @@ export const MapWrapper = () => {
minimapClasses={!isShowMenu ? classes.MiniMap : undefined}
isShowMinimap={isShowMinimap}
showKSpaceBG={isShowKSpace}
onManualDelete={handleManualDelete}
isThickConnections={isThickConnections}
/>
{openSettings != null && (

View File

@@ -1,38 +0,0 @@
import { MutableRefObject, useCallback, useEffect, useRef } from 'react';
import { Command, Commands, MapHandlers } from '@/hooks/Mapper/types';
import { MapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useCommonMapEventProcessor = () => {
const mapRef = useRef<MapHandlers>() as MutableRefObject<MapHandlers>;
const {
data: { systems },
} = useMapRootState();
const refQueue = useRef<MapEvent<Command>[]>([]);
// const ref = useRef({})
const runCommand = useCallback(({ name, data }: MapEvent<Command>) => {
switch (name) {
case Commands.addSystems:
case Commands.removeSystems:
// case Commands.addConnections:
refQueue.current.push({ name, data });
return;
}
// @ts-ignore hz why here type error
mapRef.current?.command(name, data);
}, []);
useEffect(() => {
refQueue.current.forEach(x => mapRef.current?.command(x.name, x.data));
refQueue.current = [];
}, [systems]);
return {
mapRef,
runCommand,
};
};

View File

@@ -4,7 +4,7 @@ import classes from './CharacterCard.module.scss';
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
type CharacterCardProps = {
compact?: boolean;
@@ -34,12 +34,11 @@ export const CharacterCard = ({
useSystemsCache,
...char
}: CharacterCardProps) => {
const { mapRef } = useMapRootState();
const handleSelect = useCallback(() => {
emitMapEvent({
name: Commands.centerSystem,
data: char?.location?.solar_system_id?.toString(),
});
}, [char]);
mapRef.current?.command(Commands.centerSystem, char?.location?.solar_system_id?.toString());
}, [mapRef, char]);
return (
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')} onClick={handleSelect}>

View File

@@ -3,23 +3,15 @@ import { SystemViewStandalone } from '@/hooks/Mapper/components/ui-kit';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { useMemo } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
export type SystemViewProps = {
systemId: string;
systemInfo?: SolarSystemStaticInfoRaw;
hideRegion?: boolean;
useSystemsCache?: boolean;
showCustomName?: boolean;
} & WithClassName;
export const SystemView = ({
systemId,
systemInfo: customSystemInfo,
hideRegion,
className,
showCustomName,
}: SystemViewProps) => {
export const SystemView = ({ systemId, hideRegion, className, showCustomName }: SystemViewProps) => {
const memSystems = useMemo(() => [systemId], [systemId]);
const { systems, loading } = useLoadSystemStatic({ systems: memSystems });
@@ -28,12 +20,9 @@ export const SystemView = ({
} = useMapRootState();
const systemInfo = useMemo(() => {
if (!systemId) {
return customSystemInfo;
}
return systems.get(parseInt(systemId));
// eslint-disable-next-line
}, [customSystemInfo, systemId, systems, loading]);
}, [systemId, systems, loading]);
const mapSystemInfo = useMemo(() => {
if (!showCustomName) {

View File

@@ -25,6 +25,7 @@ export const SystemViewStandalone = ({
className,
hideRegion,
customName,
class_title,
system_class,
solar_system_name,
@@ -37,7 +38,6 @@ export const SystemViewStandalone = ({
...props
}: SystemViewStandaloneProps) => {
const classTitleColor = getSystemClassStyles({ systemClass: system_class, security });
const isWH = isWormholeSpace(system_class);
const handleClick = useCallback(

View File

@@ -1,4 +1,5 @@
.WHClassViewRoot {
}
.WHClassViewContent {

View File

@@ -18,9 +18,7 @@ export interface WHClassViewProps {
whClassName: string;
noOffset?: boolean;
useShortTitle?: boolean;
hideTooltip?: boolean;
hideWhClass?: boolean;
hideWhClassName?: boolean;
highlightName?: boolean;
className?: string;
classNameWh?: string;
@@ -30,9 +28,7 @@ export const WHClassView = ({
whClassName,
noOffset,
useShortTitle,
hideTooltip,
hideWhClass,
hideWhClassName,
highlightName,
className,
classNameWh,
@@ -41,8 +37,6 @@ export const WHClassView = ({
data: { wormholesData },
} = useMapRootState();
console.log(whClassName);
const whData = useMemo(() => wormholesData[whClassName], [whClassName, wormholesData]);
const whClass = useMemo(() => WORMHOLES_ADDITIONAL_INFO[whData.dest], [whData.dest]);
const whClassStyle = WORMHOLE_CLASS_STYLES[whClass?.wormholeClassID] ?? '';
@@ -51,27 +45,25 @@ export const WHClassView = ({
return (
<div className={clsx(classes.WHClassViewRoot, className)}>
{!hideTooltip && (
<Tooltip
target={`.wh-name${whClassName}${uid}`}
position="right"
mouseTrack
mouseTrackLeft={20}
mouseTrackTop={30}
className="border border-green-300 rounded border-opacity-10 bg-stone-900 bg-opacity-90 "
>
<div className="flex gap-3">
<div className="flex flex-col gap-1">
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
</div>
<div className="flex flex-col gap-1">
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
</div>
<Tooltip
target={`.wh-name${whClassName}${uid}`}
position="right"
mouseTrack
mouseTrackLeft={20}
mouseTrackTop={30}
className="border border-green-300 rounded border-opacity-10 bg-stone-900 bg-opacity-90 "
>
<div className="flex gap-3">
<div className="flex flex-col gap-1">
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
</div>
</Tooltip>
)}
<div className="flex flex-col gap-1">
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
</div>
</div>
</Tooltip>
<div
className={clsx(
@@ -81,7 +73,7 @@ export const WHClassView = ({
`wh-name${whClassName}${uid}`,
)}
>
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
<span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>
{!hideWhClass && whClass && (
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
{useShortTitle ? whClass.shortTitle : whClass.shortName}

View File

@@ -1,13 +1,13 @@
import { useState, useEffect, useCallback } from 'react';
export const useClipboard = () => {
const [clipboardContent, setClipboardContent] = useState<{ text: string } | null>(null);
const [clipboardContent, setClipboardContent] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const getClipboardContent = useCallback(async () => {
try {
const text = await navigator.clipboard.readText();
setClipboardContent({ text });
setClipboardContent(text);
setError(null);
} catch (err) {
setError('Failed to read clipboard content.');
@@ -18,7 +18,7 @@ export const useClipboard = () => {
const handlePaste = (event: ClipboardEvent) => {
const text = event.clipboardData?.getData('text');
if (text) {
setClipboardContent({ text });
setClipboardContent(text);
setError(null);
}
};
@@ -30,5 +30,5 @@ export const useClipboard = () => {
};
}, []);
return { clipboardContent, error, getClipboardContent, setClipboardContent };
return { clipboardContent, error, getClipboardContent };
};

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: (e: KeyboardEvent) => void) => {
export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: () => void) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((!isMetaKey || event.ctrlKey || event.metaKey) && hotkeys.includes(event.key)) {
@@ -8,14 +8,14 @@ export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: (e: K
return;
}
event.preventDefault();
callback(event);
callback();
}
};
window.addEventListener('keydown', handleKeyDown, { capture: true });
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
window.removeEventListener('keydown', handleKeyDown);
};
}, [isMetaKey, hotkeys, callback]);
};

View File

@@ -1,6 +1,6 @@
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext } from 'react';
import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { createContext, Dispatch, ForwardedRef, forwardRef, RefObject, SetStateAction, useContext } from 'react';
import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import useLocalStorageState from 'use-local-storage-state';
@@ -25,33 +25,30 @@ const INITIAL_DATA: MapRootData = {
selectedSystems: [],
selectedConnections: [],
userPermissions: {},
};
export enum InterfaceStoredSettingsProps {
isShowMenu = 'isShowMenu',
isShowMinimap = 'isShowMinimap',
isShowKSpace = 'isShowKSpace',
isThickConnections = 'isThickConnections',
}
export type InterfaceStoredSettings = {
isShowMenu: boolean;
isShowMinimap: boolean;
isShowKSpace: boolean;
isThickConnections: boolean;
};
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMenu: false,
isShowMinimap: true,
isShowKSpace: false,
isThickConnections: false,
};
export interface MapRootContextProps {
update: ContextStoreDataUpdate<MapRootData>;
data: MapRootData;
mapRef: RefObject<MapHandlers>;
outCommand: OutCommandHandler;
interfaceSettings: InterfaceStoredSettings;
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
@@ -60,6 +57,7 @@ export interface MapRootContextProps {
const MapRootContext = createContext<MapRootContextProps>({
update: () => {},
data: { ...INITIAL_DATA },
mapRef: { current: null },
// @ts-ignore
outCommand: async () => void 0,
interfaceSettings: STORED_INTERFACE_DEFAULT_VALUES,
@@ -69,6 +67,7 @@ const MapRootContext = createContext<MapRootContextProps>({
type MapRootProviderProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fwdRef: ForwardedRef<any>;
mapRef: RefObject<MapHandlers>;
outCommand: OutCommandHandler;
} & WithChildren;
@@ -79,7 +78,7 @@ const MapRootHandlers = forwardRef(({ children }: WithChildren, fwdRef: Forwarde
});
// eslint-disable-next-line react/display-name
export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProviderProps) => {
export const MapRootProvider = ({ children, fwdRef, mapRef, outCommand }: MapRootProviderProps) => {
const { update, ref } = useContextStore<MapRootData>({ ...INITIAL_DATA });
const [interfaceSettings, setInterfaceSettings] = useLocalStorageState<InterfaceStoredSettings>(
@@ -95,6 +94,7 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
update,
data: ref,
outCommand: outCommand,
mapRef: mapRef,
setInterfaceSettings,
interfaceSettings,
}}

View File

@@ -1,6 +1,5 @@
export * from './useMapInit';
export * from './useMapUpdated';
export * from './useMapCheckPermissions';
export * from './useRoutes';
export * from './useCommandsConnections';
export * from './useCommandsSystems';

View File

@@ -1,7 +1,6 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef } from 'react';
import { CommandAddSystems, CommandRemoveSystems, CommandUpdateSystems } from '@/hooks/Mapper/types';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
export const useCommandsSystems = () => {
const {
@@ -9,52 +8,40 @@ export const useCommandsSystems = () => {
data: { systems },
} = useMapRootState();
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
const ref = useRef({ systems, update });
ref.current = { systems, update };
const ref = useRef({ systems, update, addSystemStatic });
ref.current = { systems, update, addSystemStatic };
const addSystems = useCallback((systemsToAdd: CommandAddSystems) => {
const { update, addSystemStatic, systems } = ref.current;
systemsToAdd.forEach(sys => {
if (sys.system_static_info) {
addSystemStatic(sys.system_static_info);
}
});
update(
{
systems: [...systems.filter(sys => !systemsToAdd.some(x => sys.id === x.id)), ...systemsToAdd],
},
true,
);
}, []);
const addSystems = useCallback(
(addSystems: CommandAddSystems) => {
update({
systems: [...ref.current.systems.filter(sys => addSystems.some(x => sys.id !== x.id)), ...addSystems],
});
},
[update],
);
const removeSystems = useCallback((toRemove: CommandRemoveSystems) => {
const { update, systems } = ref.current;
update(
{
systems: systems.filter(x => !toRemove.includes(parseInt(x.id))),
},
true,
);
}, []);
const updateSystems = useCallback((updatedSystems: CommandUpdateSystems) => {
const { update, systems } = ref.current;
const out = systems.map(current => {
const newSystem = updatedSystems.find(x => current.id === x.id);
if (!newSystem) {
return current;
}
return newSystem;
update({
systems: systems.filter(x => !toRemove.includes(parseInt(x.id))),
});
update({ systems: out }, true);
}, []);
const updateSystems = useCallback(
(systems: CommandUpdateSystems) => {
const out = ref.current.systems.map(current => {
const newSystem = systems.find(x => current.id === x.id);
if (!newSystem) {
return current;
}
return newSystem;
});
update({ systems: out });
},
[update],
);
return { addSystems, removeSystems, updateSystems };
};

View File

@@ -1,11 +0,0 @@
import { useMemo } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export const useMapCheckPermissions = (permissions: UserPermission[]) => {
const {
data: { userPermissions },
} = useMapRootState();
return useMemo(() => permissions.every(x => userPermissions[x]), [permissions, userPermissions]);
};

View File

@@ -19,7 +19,6 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
user_permissions,
}: CommandInit) => {
const updateData: Partial<MapRootData> = {};
@@ -52,10 +51,6 @@ export const useMapInit = () => {
updateData.connections = connections;
}
if (user_permissions) {
updateData.userPermissions = user_permissions;
}
if (hubs) {
updateData.hubs = hubs;
}

View File

@@ -24,7 +24,7 @@ interface UseLoadSystemStaticProps {
systems: (number | string)[];
}
export const useLoadSystemStatic = ({ systems = [] }: UseLoadSystemStaticProps) => {
export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
const { outCommand } = useMapRootState();
const [loading, setLoading] = useState(false);
const [lastUpdateKey, setLastUpdateKey] = useState(0);
@@ -51,9 +51,6 @@ export const useLoadSystemStatic = ({ systems = [] }: UseLoadSystemStaticProps)
}, []);
useEffect(() => {
if (!systems.length) {
return;
}
loadSystems(systems);
// eslint-disable-next-line
}, [systems]);

View File

@@ -44,75 +44,87 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
case Commands.addSystems:
addSystems(data as CommandAddSystems);
setTimeout(() => {
emitMapEvent({ name: Commands.addSystems, data });
}, 100);
break;
case Commands.updateSystems: // USED
case Commands.updateSystems:
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
case Commands.removeSystems:
removeSystems(data as CommandRemoveSystems);
setTimeout(() => {
emitMapEvent({ name: Commands.removeSystems, data });
}, 100);
break;
case Commands.addConnections: // USED
case Commands.addConnections:
addConnections(data as CommandAddConnections);
setTimeout(() => {
emitMapEvent({ name: Commands.addConnections, data });
}, 100);
break;
case Commands.removeConnections: // USED
case Commands.removeConnections:
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
case Commands.centerSystem:
// do nothing here
break;
case Commands.linkSignatureToSystem: // USED
case Commands.selectSystem:
// do nothing here
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
case Commands.linkSignatureToSystem:
// TODO command data type lost
// @ts-ignore
emitMapEvent({ name: Commands.linkSignatureToSystem, data });
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.signaturesUpdated:
// TODO command data type lost
// @ts-ignore
emitMapEvent({ name: Commands.signaturesUpdated, data });
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
emitMapEvent({ name: type, data });
},
};
},

View File

@@ -1,8 +1,3 @@
export enum ConnectionType {
wormhole,
gate,
}
export enum MassState {
normal,
half,
@@ -37,6 +32,4 @@ export type SolarSystemConnection = {
source: string;
target: string;
type?: ConnectionType;
};

View File

@@ -11,10 +11,6 @@ export type Passage = {
character: PassageLimitedCharacterType;
};
export type ConnectionInfoOutput = {
marl_eol_time: string;
};
export type ConnectionOutput = {
passages: Passage[];
};

View File

@@ -6,4 +6,3 @@ export * from './system';
export * from './mapUnionTypes';
export * from './signatures';
export * from './connectionPassages';
export * from './permissions';

View File

@@ -4,7 +4,6 @@ import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { Kill } from '@/hooks/Mapper/types/kills.ts';
import { UserPermissions } from '@/hooks/Mapper/types';
export enum Commands {
init = 'init',
@@ -59,7 +58,7 @@ export type CommandInit = {
characters: CharacterTypeRaw[];
present_characters: string[];
user_characters: string[];
user_permissions: UserPermissions;
user_permissions: any;
hubs: string[];
routes: RoutesList;
reset?: boolean;
@@ -119,9 +118,7 @@ export enum OutCommand {
getCharacterJumps = 'get_character_jumps',
getSignatures = 'get_signatures',
getSystemStaticInfos = 'get_system_static_infos',
getConnectionInfo = 'get_connection_info',
updateConnectionTimeStatus = 'update_connection_time_status',
updateConnectionType = 'update_connection_type',
updateConnectionMassStatus = 'update_connection_mass_status',
updateConnectionShipSizeType = 'update_connection_ship_size_type',
updateConnectionLocked = 'update_connection_locked',

View File

@@ -4,7 +4,6 @@ import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { UserPermissions } from '@/hooks/Mapper/types';
export type MapUnionTypes = {
wormholesData: Record<string, WormholeDataRaw>;
@@ -18,5 +17,4 @@ export type MapUnionTypes = {
routes?: RoutesList;
kills: Record<number, number>;
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;
};

View File

@@ -1,19 +0,0 @@
export enum UserPermission {
ADMIN_MAP = 'admin_map',
MANAGE_MAP = 'manage_map',
VIEW_SYSTEM = 'view_system',
VIEW_CHARACTER = 'view_character',
VIEW_CONNECTION = 'view_connection',
ADD_SYSTEM = 'add_system',
ADD_CONNECTION = 'add_connection',
UPDATE_SYSTEM = 'update_system',
TRACK_CHARACTER = 'track_character',
DELETE_CONNECTION = 'delete_connection',
DELETE_SYSTEM = 'delete_system',
LOCK_SYSTEM = 'lock_system',
ADD_ACL = 'add_acl',
DELETE_ACL = 'delete_acl',
DELETE_MAP = 'delete_map',
}
export type UserPermissions = Record<UserPermission, boolean>;

View File

@@ -21,12 +21,9 @@ export type SystemSignature = {
eve_id: string;
kind: string;
name: string;
custom_info?: string;
description?: string;
group: SignatureGroup;
type: string;
k162Type?: string;
linked_system?: SolarSystemStaticInfoRaw;
inserted_at?: string;
updated_at?: string;
};

View File

@@ -1,6 +1,7 @@
import { RefObject, useCallback } from 'react';
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { getQueryVariable } from './utils';
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
const handleCommand = useCallback(
@@ -15,6 +16,10 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
);
const handleMapEvent = useCallback(({ type, body }) => {
if (getQueryVariable('debug') === 'true') {
console.log(type, body);
}
handlerRefs.forEach(ref => {
if (!ref.current) {
return;

View File

@@ -8,7 +8,7 @@ export default {
const selector = '#' + this.el.id;
const droppable = new Droppable(containers, {
delay: 100,
delay: 150,
draggable: '.draggable',
dropzone: '.dropzone',
mirror: {

View File

@@ -4,26 +4,13 @@ export default {
mounted() {
const hook = this;
const refreshZone = hook.el.querySelector('#refresh-area');
const button = hook.el.querySelector('.update-button');
const handleUpdate = function (e: Event) {
const hexBricks = hook.el.querySelectorAll('.hex-brick');
// Add a new class to each element
hexBricks.forEach(el => {
el.classList.add('hex-brick--active');
});
setTimeout(() => {
const lastVersion = hook.el.dataset.version;
localStorage.setItem(LAST_VERSION_KEY, lastVersion);
window.location.reload();
}, 2000);
};
refreshZone.addEventListener('click', handleUpdate);
refreshZone.addEventListener('mouseover', handleUpdate);
button.addEventListener('click', function () {
const lastVersion = hook.el.dataset.version;
localStorage.setItem(LAST_VERSION_KEY, lastVersion);
window.location.reload();
});
this.updated();
},

View File

@@ -13,6 +13,8 @@
},
"dependencies": {
"@formkit/auto-animate": "0.7.0",
"@react-rxjs/core": "^0.10.7",
"@react-rxjs/utils": "^0.9.7",
"@shopify/draggable": "^1.1.3",
"clsx": "^2.1.1",
"daisyui": "^4.11.1",
@@ -31,7 +33,8 @@
"react-grid-layout": "^1.3.4",
"react-hook-form": "^7.53.1",
"react-usestateref": "^1.0.9",
"reactflow": "^11.11.4",
"reactflow": "^11.10.4",
"rxjs": "^7.8.1",
"tailwindcss": "^3.3.6",
"topbar": "^3.0.0",
"use-local-storage-state": "^19.3.1"

View File

@@ -469,6 +469,19 @@
resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@react-rxjs/core@^0.10.7":
version "0.10.7"
resolved "https://registry.npmjs.org/@react-rxjs/core/-/core-0.10.7.tgz"
integrity sha512-dornp8pUs9OcdqFKKRh9+I2FVe21gWufNun6RYU1ddts7kUy9i4Thvl0iqcPFbGY61cJQMAJF7dxixWMSD/A/A==
dependencies:
"@rx-state/core" "0.1.4"
use-sync-external-store "^1.0.0"
"@react-rxjs/utils@^0.9.7":
version "0.9.7"
resolved "https://registry.npmjs.org/@react-rxjs/utils/-/utils-0.9.7.tgz"
integrity sha512-m9CUTdRsglObvUAlYfB24QvN+QH4XqCGEKnCdSILIeOx7mMqSi9TTFp2zrj5XqtMiLnj4ReAdDxrXegLPB73bQ==
"@reactflow/background@11.3.14":
version "11.3.14"
resolved "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz"
@@ -637,6 +650,11 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503"
integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==
"@rx-state/core@0.1.4":
version "0.1.4"
resolved "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz"
integrity sha512-Z+3hjU2xh1HisLxt+W5hlYX/eGSDaXXP+ns82gq/PLZpkXLu0uwcNUh9RLY3Clq4zT+hSsA3vcpIGt6+UAb8rQ==
"@shopify/draggable@^1.1.3":
version "1.1.3"
resolved "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.1.3.tgz"
@@ -3262,9 +3280,9 @@ react@18.2.0:
dependencies:
loose-envify "^1.1.0"
reactflow@^11.11.4:
reactflow@^11.10.4:
version "11.11.4"
resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653"
resolved "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz"
integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==
dependencies:
"@reactflow/background" "11.3.14"
@@ -3403,6 +3421,13 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
safe-array-concat@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz"
@@ -3707,7 +3732,7 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tslib@^2.6.2:
tslib@^2.1.0, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@@ -3813,7 +3838,7 @@ use-local-storage-state@^19.3.1:
resolved "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.3.1.tgz"
integrity sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q==
use-sync-external-store@1.2.0:
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==

View File

@@ -138,7 +138,6 @@ config :ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth,
System.get_env("EVE_CLIENT_WITH_CORP_WALLET_SECRET", "<EVE_CLIENT_WITH_CORP_WALLET_SECRET>")
config :logger,
truncate: :infinity,
level:
String.to_existing_atom(
System.get_env(
@@ -172,65 +171,43 @@ config :wanderer_app, WandererApp.Scheduler,
timeout: :infinity
if config_env() == :prod do
database_unix_socket =
System.get_env("DATABASE_UNIX_SOCKET")
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
database =
database_unix_socket
maybe_ipv6 =
config_dir
|> get_var_from_path_or_env("ECTO_IPV6", "false")
|> String.to_existing_atom()
|> case do
nil ->
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
true -> [:inet6]
_ -> []
end
_ ->
System.get_env("DATABASE_NAME") ||
raise """
environment variable DATABASE_NAME is missing.
For example: "wanderer"
"""
db_ssl_enabled =
config_dir
|> get_var_from_path_or_env("DATABASE_SSL_ENABLED", "false")
|> String.to_existing_atom()
db_ssl_verify_none =
config_dir
|> get_var_from_path_or_env("DATABASE_SSL_VERIFY_NONE", "false")
|> String.to_existing_atom()
client_opts =
if db_ssl_verify_none do
[verify: :verify_none]
end
config :wanderer_app, WandererApp.Repo,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
if not is_nil(database_unix_socket) do
config :wanderer_app, WandererApp.Repo,
socket_dir: database_unix_socket,
database: database
else
db_ssl_enabled =
config_dir
|> get_var_from_path_or_env("DATABASE_SSL_ENABLED", "false")
|> String.to_existing_atom()
db_ssl_verify_none =
config_dir
|> get_var_from_path_or_env("DATABASE_SSL_VERIFY_NONE", "false")
|> String.to_existing_atom()
client_opts =
if db_ssl_verify_none do
[verify: :verify_none]
end
maybe_ipv6 =
config_dir
|> get_var_from_path_or_env("ECTO_IPV6", "false")
|> String.to_existing_atom()
|> case do
true -> [:inet6]
_ -> []
end
config :wanderer_app, WandererApp.Repo,
url: database,
ssl: db_ssl_enabled,
ssl_opts: client_opts,
socket_options: maybe_ipv6
end
url: database_url,
ssl: db_ssl_enabled,
ssl_opts: client_opts,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
# The secret key base is used to sign/encrypt cookies and other secrets.
# A default value is used in config/dev.exs and config/test.exs but you

View File

@@ -3,10 +3,9 @@
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'wanderer-test'
app = 'wanderer'
primary_region = 'ams'
kill_signal = 'SIGTERM'
swap_size_mb = 512
[build]
@@ -14,14 +13,18 @@ swap_size_mb = 512
release_command = '/app/bin/migrate.sh'
[env]
PHX_HOST = 'wanderer-test.fly.dev'
PHX_HOST = 'wanderer.fly.dev'
PHX_SERVER = 'true'
PORT = '8080'
[metrics]
port = 4021
path = "/metrics"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = 'off'
auto_stop_machines = false
auto_start_machines = false
min_machines_running = 0
processes = ['app']
@@ -32,9 +35,6 @@ swap_size_mb = 512
soft_limit = 1000
[[vm]]
size = 'shared-cpu-1x'
[[metrics]]
port = 4021
path = '/metrics'
https = false
memory = '1gb'
cpu_kind = 'shared'
cpus = 1

View File

@@ -4,6 +4,8 @@ defmodule WandererApp.Api.Calculations.CalcMapPermissions do
use Ash.Resource.Calculation
require Ash.Query
import Bitwise
@impl true
def load(_query, _opts, _context) do
[
@@ -15,8 +17,116 @@ defmodule WandererApp.Api.Calculations.CalcMapPermissions do
end
@impl true
def calculate([record], _opts, %{actor: actor}),
do: WandererApp.Permissions.check_characters_access(actor.characters, record.acls)
def calculate([record], _opts, %{actor: actor}) do
characters = actor.characters
character_ids = characters |> Enum.map(& &1.id)
character_eve_ids = characters |> Enum.map(& &1.eve_id)
character_corporation_ids =
characters |> Enum.map(& &1.corporation_id) |> Enum.map(&to_string/1)
character_alliance_ids = characters |> Enum.map(& &1.alliance_id) |> Enum.map(&to_string/1)
result =
record.acls
|> Enum.reduce([0, 0], fn acl, acc ->
is_owner? = acl.owner_id in character_ids
is_character_member? =
acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end)
is_corporation_member? =
acl.members
|> Enum.any?(fn member -> member.eve_corporation_id in character_corporation_ids end)
is_alliance_member? =
acl.members
|> Enum.any?(fn member -> member.eve_alliance_id in character_alliance_ids end)
if is_owner? || is_character_member? || is_corporation_member? || is_alliance_member? do
case acc do
[_, -1] ->
[-1, -1]
[-1, char_acc] ->
char_acl_mask =
acl.members
|> Enum.filter(fn member ->
member.eve_character_id in character_eve_ids
end)
|> Enum.reduce(0, fn member, acc ->
case acc do
-1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc)
end
end)
char_acc =
case char_acl_mask do
-1 -> -1
_ -> char_acc ||| char_acl_mask
end
[-1, char_acc]
[any_acc, char_acc] ->
any_acl_mask =
acl.members
|> Enum.filter(fn member ->
member.eve_character_id in character_eve_ids ||
member.eve_corporation_id in character_corporation_ids ||
member.eve_alliance_id in character_alliance_ids
end)
|> Enum.reduce(0, fn member, acc ->
case acc do
-1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc)
end
end)
char_acl_mask =
acl.members
|> Enum.filter(fn member ->
member.eve_character_id in character_eve_ids
end)
|> Enum.reduce(0, fn member, acc ->
case acc do
-1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc)
end
end)
any_acc =
case any_acl_mask do
-1 -> -1
_ -> any_acc ||| any_acl_mask
end
char_acc =
case char_acl_mask do
-1 -> -1
_ -> char_acc ||| char_acl_mask
end
[any_acc, char_acc]
end
else
acc
end
end)
case result do
[_, -1] ->
[-1]
[-1, char_acc] ->
[char_acc]
[any_acc, _char_acc] ->
[any_acc]
end
end
@impl true
def calculate(_records, _opts, _context) do

View File

@@ -17,12 +17,12 @@ defmodule WandererApp.Api.MapCharacterSettings do
action: :read_by_map
)
define(:tracked_by_map_filtered,
action: :tracked_by_map_filtered
define(:tracked_by_map,
action: :tracked_by_map
)
define(:tracked_by_map_all,
action: :tracked_by_map_all
action: :read_tracked_by_map
)
define(:track, action: :track)
@@ -38,7 +38,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
defaults [:create, :read, :update, :destroy]
read :tracked_by_map_filtered do
read :tracked_by_map do
argument(:map_id, :string, allow_nil?: false)
argument(:character_ids, {:array, :uuid}, allow_nil?: false)
@@ -52,7 +52,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
filter(expr(map_id == ^arg(:map_id)))
end
read :tracked_by_map_all do
read :read_tracked_by_map do
argument(:map_id, :string, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and tracked == true))
end

View File

@@ -29,16 +29,13 @@ defmodule WandererApp.Api.MapConnection do
define(:update_ship_size_type, action: :update_ship_size_type)
define(:update_locked, action: :update_locked)
define(:update_custom_info, action: :update_custom_info)
define(:update_type, action: :update_type)
define(:update_wormhole_type, action: :update_wormhole_type)
end
actions do
default_accept [
:map_id,
:solar_system_source,
:solar_system_target,
:type
:solar_system_target
]
defaults [:create, :read, :update, :destroy]
@@ -95,14 +92,6 @@ defmodule WandererApp.Api.MapConnection do
update :update_custom_info do
accept [:custom_info]
end
update :update_type do
accept [:type]
end
update :update_wormhole_type do
accept [:wormhole_type]
end
end
attributes do
@@ -137,14 +126,6 @@ defmodule WandererApp.Api.MapConnection do
allow_nil?(true)
end
# where 0 - Wormhole
# where 1 - Gate
attribute :type, :integer do
default(0)
allow_nil?(true)
end
attribute :wormhole_type, :string
attribute :count_of_passage, :integer do

View File

@@ -12,7 +12,6 @@ defmodule WandererApp.Api.MapSystem do
code_interface do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:by_id,
get_by: [:id],

View File

@@ -16,7 +16,6 @@ defmodule WandererApp.Api.MapSystemSignature do
define(:update, action: :update)
define(:update_linked_system, action: :update_linked_system)
define(:update_type, action: :update_type)
define(:update_group, action: :update_group)
define(:by_id,
get_by: [:id],
@@ -24,7 +23,6 @@ defmodule WandererApp.Api.MapSystemSignature do
)
define(:by_system_id, action: :by_system_id, args: [:system_id])
define(:by_linked_system_id, action: :by_linked_system_id, args: [:linked_system_id])
end
actions do
@@ -56,8 +54,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:description,
:kind,
:group,
:type,
:custom_info
:type
]
argument :system_id, :uuid, allow_nil?: false
@@ -74,9 +71,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:description,
:kind,
:group,
:type,
:custom_info,
:updated
:type
]
primary? true
@@ -91,21 +86,11 @@ defmodule WandererApp.Api.MapSystemSignature do
accept [:type]
end
update :update_group do
accept [:group]
end
read :by_system_id do
argument(:system_id, :string, allow_nil?: false)
filter(expr(system_id == ^arg(:system_id)))
end
read :by_linked_system_id do
argument(:linked_system_id, :integer, allow_nil?: false)
filter(expr(linked_system_id == ^arg(:linked_system_id)))
end
end
attributes do
@@ -138,12 +123,6 @@ defmodule WandererApp.Api.MapSystemSignature do
attribute :kind, :string
attribute :group, :string
attribute :custom_info, :string do
allow_nil? true
end
attribute :updated, :integer
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -3,8 +3,6 @@ defmodule WandererApp.Application do
use Application
require Logger
@impl true
def start(_type, _args) do
children =
@@ -47,16 +45,7 @@ defmodule WandererApp.Application do
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: WandererApp.Supervisor]
Supervisor.start_link(children, opts)
|> case do
{:ok, _pid} = ok ->
ok
{:error, info} = e ->
Logger.error("Failed to start application: #{inspect(info)}")
e
end
end
# Tell Phoenix to update the endpoint configuration

View File

@@ -61,18 +61,12 @@ defmodule WandererApp.Character do
end)
end
def get_character_state(character_id, init_if_empty? \\ true) do
def get_character_state(character_id) do
case Cachex.get(:character_state_cache, character_id) do
{:ok, nil} ->
case init_if_empty? do
true ->
character_state = WandererApp.Character.Tracker.init(character_id: character_id)
Cachex.put(:character_state_cache, character_id, character_state)
{:ok, character_state}
_ ->
{:ok, nil}
end
character_state = WandererApp.Character.Tracker.init(character_id: character_id)
Cachex.put(:character_state_cache, character_id, character_state)
{:ok, character_state}
{:ok, character_state} ->
{:ok, character_state}

View File

@@ -83,28 +83,22 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def stop_tracking(%__MODULE__{} = state, character_id) do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id, false)
{:ok, %{start_time: start_time}} = WandererApp.Character.get_character_state(character_id)
case character_state do
nil ->
state
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{duration: duration})
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
%{start_time: start_time} ->
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{duration: duration})
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.delete_character_state(character_id)
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.delete_character_state(character_id)
tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
WandererApp.Cache.insert("tracked_characters", tracked_characters)
WandererApp.Cache.insert("tracked_characters", tracked_characters)
%{state | characters: tracked_characters}
end
%{state | characters: tracked_characters}
end
def update_track_settings(
@@ -435,19 +429,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def handle_info({:stop_track, character_id}, state) do
WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking")
|> case do
false ->
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
state = state |> stop_tracking(character_id)
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
state
_ ->
state
end
@logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
stop_tracking(state, character_id)
end
def handle_info(_event, state),

View File

@@ -37,32 +37,32 @@ defmodule WandererApp.Esi.ApiClient do
@logger Application.compile_env(:wanderer_app, :logger)
def get_server_status, do: get("/status")
def get_server_status, do: _get("/status")
def set_autopilot_waypoint(add_to_beginning, clear_other_waypoints, destination_id, opts \\ []),
do:
post_esi(
"/ui/autopilot/waypoint",
opts
|> Keyword.merge(
params: %{
add_to_beginning: add_to_beginning,
clear_other_waypoints: clear_other_waypoints,
destination_id: destination_id
}
)
def set_autopilot_waypoint(add_to_beginning, clear_other_waypoints, destination_id, opts \\ []) do
_post_esi(
"/ui/autopilot/waypoint",
opts
|> Keyword.merge(
params: %{
add_to_beginning: add_to_beginning,
clear_other_waypoints: clear_other_waypoints,
destination_id: destination_id
}
)
)
end
def post_characters_affiliation(character_eve_ids, _opts)
when is_list(character_eve_ids),
do:
post(
"#{@base_url}/characters/affiliation/",
json: character_eve_ids,
params: %{
datasource: "tranquility"
}
)
when is_list(character_eve_ids) do
_post(
"#{@base_url}/characters/affiliation/",
json: character_eve_ids,
params: %{
datasource: "tranquility"
}
)
end
def find_routes(map_id, origin, hubs, routes_settings) do
origin = origin |> String.to_integer()
@@ -184,7 +184,7 @@ defmodule WandererApp.Esi.ApiClient do
routes =
all_routes
|> Enum.map(fn route_info ->
map_route_info(route_info)
_map_route_info(route_info)
end)
|> Enum.filter(fn route_info -> not is_nil(route_info) end)
@@ -200,7 +200,7 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, result}
_ ->
case get_all_routes_custom(hubs, origin, params) do
case _get_all_routes_custom(hubs, origin, params) do
{:ok, result} ->
WandererApp.Cache.insert(
cache_key,
@@ -210,21 +210,22 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, result}
{:error, _error} ->
{:error, error} ->
@logger.error("Error getting custom routes for #{inspect(origin)}: #{inspect(error)}")
@logger.error("Error getting custom routes for #{inspect(origin)}: #{inspect(hubs)}")
@logger.error(
"Error getting custom routes for #{inspect(origin)}: #{inspect(params)}"
)
get_all_routes_eve(hubs, origin, params, opts)
_get_all_routes_eve(hubs, origin, params, opts)
end
end
end
defp get_all_routes_custom(hubs, origin, params),
defp _get_all_routes_custom(hubs, origin, params),
do:
post(
_post(
"#{get_custom_route_base_url()}/route/multiple",
[
json: %{
@@ -238,7 +239,7 @@ defmodule WandererApp.Esi.ApiClient do
|> Keyword.merge(@timeout_opts)
)
def get_all_routes_eve(hubs, origin, params, opts),
def _get_all_routes_eve(hubs, origin, params, opts),
do:
{:ok,
hubs
@@ -307,7 +308,7 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_character_info(eve_id, opts \\ []) do
case get(
case _get(
"/characters/#{eve_id}/",
opts |> _with_cache_opts()
) do
@@ -384,7 +385,7 @@ defmodule WandererApp.Esi.ApiClient do
defp _get_routes_eve(origin, destination, params, opts),
do:
get(
_get(
"/route/#{origin}/#{destination}/?#{params |> Plug.Conn.Query.encode()}",
opts |> _with_cache_opts()
)
@@ -393,14 +394,14 @@ defmodule WandererApp.Esi.ApiClient do
defp _get_alliance_info(alliance_eve_id, info_path, opts),
do:
get(
_get(
"/alliances/#{alliance_eve_id}/#{info_path}",
opts |> _with_cache_opts()
)
defp _get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
_get(
"/corporations/#{corporation_eve_id}/#{info_path}",
opts |> _with_cache_opts()
)
@@ -415,7 +416,7 @@ defmodule WandererApp.Esi.ApiClient do
character_id = opts |> Keyword.get(:character_id, nil)
if not _is_access_token_expired?(character_id) do
get(
_get(
path,
auth_opts,
opts
@@ -436,7 +437,7 @@ defmodule WandererApp.Esi.ApiClient do
defp _get_corporation_auth_data(corporation_eve_id, info_path, opts),
do:
get(
_get(
"/corporations/#{corporation_eve_id}/#{info_path}",
[params: opts[:params] || []] ++
(opts |> _get_auth_opts() |> _with_cache_opts()),
@@ -447,14 +448,14 @@ defmodule WandererApp.Esi.ApiClient do
opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
end
defp post_esi(path, opts),
defp _post_esi(path, opts),
do:
post(
_post(
"#{@base_url}#{path}",
[params: opts[:params] || []] ++ (opts |> _get_auth_opts())
)
defp get(path, api_opts \\ [], opts \\ []) do
defp _get(path, api_opts \\ [], opts \\ []) do
try do
case Req.get("#{@base_url}#{path}", api_opts |> Keyword.merge(@retry_opts)) do
{:ok, %{status: 200, body: body}} ->
@@ -486,7 +487,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp post(url, opts) do
defp _post(url, opts) do
try do
case Req.post("#{url}", opts) do
{:ok, %{status: status, body: body}} when status in [200, 201] ->
@@ -524,7 +525,7 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, token} ->
auth_opts = [access_token: token.access_token] |> _get_auth_opts()
get(
_get(
path,
api_opts |> Keyword.merge(auth_opts),
opts |> Keyword.merge(retry_count: retry_count + 1)
@@ -599,7 +600,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp map_route_info(
defp _map_route_info(
%{
"origin" => origin,
"destination" => destination,
@@ -608,14 +609,14 @@ defmodule WandererApp.Esi.ApiClient do
} = _route_info
),
do:
map_route_info(%{
_map_route_info(%{
origin: origin,
destination: destination,
systems: result_systems,
success: success
})
defp map_route_info(
defp _map_route_info(
%{origin: origin, destination: destination, systems: result_systems, success: success} =
_route_info
) do
@@ -637,5 +638,5 @@ defmodule WandererApp.Esi.ApiClient do
}
end
defp map_route_info(_), do: nil
defp _map_route_info(_), do: nil
end

View File

@@ -8,7 +8,6 @@ defmodule WandererApp.Map do
defstruct map_id: nil,
name: nil,
scope: :none,
owner_id: nil,
characters: [],
systems: Map.new(),
hubs: [],
@@ -17,12 +16,11 @@ defmodule WandererApp.Map do
characters_limit: nil,
hubs_limit: nil
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
def new(%{id: map_id, name: name, scope: scope, acls: acls, hubs: hubs}) do
map =
struct!(__MODULE__,
map_id: map_id,
scope: scope,
owner_id: owner_id,
name: name,
acls: acls,
hubs: hubs
@@ -216,7 +214,7 @@ defmodule WandererApp.Map do
%{visible: true} = system ->
system
_system ->
_ ->
nil
end
end
@@ -264,7 +262,7 @@ defmodule WandererApp.Map do
case not Map.has_key?(systems, solar_system_id) do
true ->
map_id
|> update_map(%{systems: Map.put(systems, solar_system_id, system)})
|> update_map(%{systems: Map.put_new(systems, solar_system_id, system)})
:ok

View File

@@ -183,24 +183,12 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.delete_connection/2, [connection_info]})
def get_connection_info(map_id, connection_info) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.call({&Impl.get_connection_info/2, [connection_info]}, :timer.minutes(1))
def update_connection_time_status(map_id, connection_info) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.update_connection_time_status/2, [connection_info]})
def update_connection_type(map_id, connection_info) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.update_connection_type/2, [connection_info]})
def update_connection_mass_status(map_id, connection_info) when is_binary(map_id),
do:
map_id

File diff suppressed because it is too large Load Diff

View File

@@ -237,7 +237,7 @@ defmodule WandererApp.Map.SubscriptionManager do
defp _renew_subscription(%{auto_renew?: true} = subscription) when is_map(subscription) do
with {:ok, %{map: map}} <-
subscription |> WandererApp.MapSubscriptionRepo.load_relationships([:map]),
{:ok, estimated_price, discount} <- estimate_price(subscription, true),
{:ok, estimated_price} <- estimate_price(subscription, true),
{:ok, map_balance} <- get_balance(map) do
case map_balance >= estimated_price do
true ->
@@ -245,7 +245,7 @@ defmodule WandererApp.Map.SubscriptionManager do
WandererApp.MapTransactionRepo.create(%{
map_id: map.id,
user_id: nil,
amount: estimated_price - discount,
amount: estimated_price,
type: :out
})
@@ -267,7 +267,7 @@ defmodule WandererApp.Map.SubscriptionManager do
:telemetry.execute([:wanderer_app, :map, :subscription, :renew], %{count: 1}, %{
map_id: map.id,
amount: estimated_price - discount
amount: estimated_price
})
:ok

View File

@@ -1,180 +0,0 @@
defmodule WandererApp.Map.Server.AclsImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.Impl
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
def handle_map_acl_updated(%{map_id: map_id, map: old_map} = state, added_acls, removed_acls) do
{:ok, map} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
track_acls(added_acls)
result =
(added_acls ++ removed_acls)
|> Task.async_stream(
fn acl_id ->
update_acl(acl_id)
end,
max_concurrency: 10,
timeout: :timer.seconds(15)
)
|> Enum.reduce(
%{
eve_alliance_ids: [],
eve_character_ids: [],
eve_corporation_ids: []
},
fn result, acc ->
case result do
{:ok, val} ->
{:ok,
%{
eve_alliance_ids: eve_alliance_ids,
eve_character_ids: eve_character_ids,
eve_corporation_ids: eve_corporation_ids
}} = val
%{
acc
| eve_alliance_ids: eve_alliance_ids ++ acc.eve_alliance_ids,
eve_character_ids: eve_character_ids ++ acc.eve_character_ids,
eve_corporation_ids: eve_corporation_ids ++ acc.eve_corporation_ids
}
error ->
Logger.error("Failed to update map #{map_id} acl: #{inspect(error, pretty: true)}")
acc
end
end
)
map_update = %{acls: map.acls, scope: map.scope}
WandererApp.Map.update_map(map_id, map_update)
broadcast_acl_updates({:ok, result}, map_id)
%{state | map: Map.merge(old_map, map_update)}
end
def handle_acl_updated(map_id, acl_id) do
{:ok, map} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: map.acls})
:ok =
acl_id
|> update_acl()
|> broadcast_acl_updates(map_id)
end
end
def track_acls([]), do: :ok
def track_acls([acl_id | rest]) do
track_acl(acl_id)
track_acls(rest)
end
defp track_acl(acl_id),
do: @pubsub_client.subscribe(WandererApp.PubSub, "acls:#{acl_id}")
defp broadcast_acl_updates(
{:ok,
%{
eve_character_ids: eve_character_ids,
eve_corporation_ids: eve_corporation_ids,
eve_alliance_ids: eve_alliance_ids
}},
map_id
) do
eve_character_ids
|> Enum.uniq()
|> Enum.each(fn eve_character_id ->
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{eve_character_id}",
:update_permissions
)
end)
eve_corporation_ids
|> Enum.uniq()
|> Enum.each(fn eve_corporation_id ->
@pubsub_client.broadcast(
WandererApp.PubSub,
"corporation:#{eve_corporation_id}",
:update_permissions
)
end)
eve_alliance_ids
|> Enum.uniq()
|> Enum.each(fn eve_alliance_id ->
@pubsub_client.broadcast(
WandererApp.PubSub,
"alliance:#{eve_alliance_id}",
:update_permissions
)
end)
character_ids =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:characters, [])
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
:ok
end
defp broadcast_acl_updates(_, _map_id), do: :ok
defp update_acl(acl_id) do
{:ok, %{owner: owner, members: members}} =
WandererApp.AccessListRepo.get(acl_id, [:owner, :members])
result =
members
|> Enum.reduce(
%{eve_character_ids: [owner.eve_id], eve_corporation_ids: [], eve_alliance_ids: []},
fn member, acc ->
case member do
%{eve_character_id: eve_character_id} when not is_nil(eve_character_id) ->
acc
|> Map.put(:eve_character_ids, [eve_character_id | acc.eve_character_ids])
%{eve_corporation_id: eve_corporation_id} when not is_nil(eve_corporation_id) ->
acc
|> Map.put(:eve_corporation_ids, [eve_corporation_id | acc.eve_corporation_ids])
%{eve_alliance_id: eve_alliance_id} when not is_nil(eve_alliance_id) ->
acc
|> Map.put(:eve_alliance_ids, [eve_alliance_id | acc.eve_alliance_ids])
_ ->
acc
end
end
)
{:ok, result}
end
end

View File

@@ -1,459 +0,0 @@
defmodule WandererApp.Map.Server.CharactersImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
def get_characters(%{map_id: map_id} = _state),
do: {:ok, map_id |> WandererApp.Map.list_characters()}
def add_character(%{map_id: map_id} = state, %{id: character_id} = character, track_character) do
Task.start_link(fn ->
with :ok <- map_id |> WandererApp.Map.add_character(character),
{:ok, _} <-
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: track_character
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
{:ok, character} = WandererApp.Character.get_character(character_id)
Impl.broadcast!(map_id, :character_added, character)
:ok
end
end)
state
end
def remove_character(map_id, character_id) do
Task.start_link(fn ->
with :ok <- WandererApp.Map.remove_character(map_id, character_id),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_removed, character)
:telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1})
:ok
else
{:error, _error} ->
:ok
end
end)
end
def update_tracked_characters(map_id) do
Task.start_link(fn ->
{:ok, map_tracked_character_ids} =
map_id
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|> case do
{:ok, settings} -> {:ok, settings |> Enum.map(&Map.get(&1, :character_id))}
_ -> {:ok, []}
end
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
map_active_tracked_characters =
map_tracked_character_ids
|> Enum.filter(fn character -> character in tracked_characters end)
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
:ok
end)
end
def untrack_characters(map_id, character_ids),
do:
character_ids
|> Enum.each(fn character_id ->
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: false
})
end)
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
"map_#{map_id}:invalidate_character_ids",
[]
)
invalidate_character_ids
|> Task.async_stream(
fn character_id ->
character_id
|> WandererApp.Character.get_character()
|> case do
{:ok, character} ->
acls =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:acls, [])
[character_permissions] =
WandererApp.Permissions.check_characters_access([character], acls)
map_permissions =
WandererApp.Permissions.get_map_permissions(
character_permissions,
owner_id,
[character_id]
)
case map_permissions do
%{view_system: false} ->
{:remove_character, character_id}
%{track_character: false} ->
{:remove_character, character_id}
_ ->
:ok
end
_ ->
:ok
end
end,
timeout: :timer.seconds(60),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, {:remove_character, character_id}} ->
remove_and_untrack_characters(map_id, [character_id])
:ok
{:ok, _result} ->
:ok
{:error, reason} ->
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
end)
WandererApp.Cache.insert(
"map_#{map_id}:invalidate_character_ids",
[]
)
end
defp remove_and_untrack_characters(map_id, character_ids) do
Logger.debug(fn ->
"Map #{map_id} - remove and untrack characters #{inspect(character_ids)}"
end)
map_id
|> untrack_characters(character_ids)
map_id
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(character_ids)
|> case do
{:ok, settings} ->
settings
|> Enum.each(fn s ->
WandererApp.MapCharacterSettingsRepo.untrack(s)
remove_character(map_id, s.character_id)
end)
_ ->
:ok
end
end
def track_characters(_map_id, []), do: :ok
def track_characters(map_id, [character_id | rest]) do
track_character(map_id, character_id)
track_characters(map_id, rest)
end
def update_characters(%{map_id: map_id} = state) do
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|> Enum.map(fn character_id ->
Task.start_link(fn ->
character_updates =
maybe_update_online(map_id, character_id) ++
maybe_update_location(map_id, character_id) ++
maybe_update_ship(map_id, character_id) ++
maybe_update_alliance(map_id, character_id) ++
maybe_update_corporation(map_id, character_id)
character_updates
|> Enum.filter(fn update -> update != :skip end)
|> Enum.map(fn update ->
update
|> case do
{:character_location, location_info, old_location_info} ->
update_location(
character_id,
location_info,
old_location_info,
state
)
:broadcast
{:character_ship, _info} ->
:broadcast
{:character_online, _info} ->
:broadcast
{:character_alliance, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
:broadcast
{:character_corporation, _info} ->
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
[character_id],
fn ids ->
[character_id | ids]
end
)
:broadcast
_ ->
:skip
end
end)
|> Enum.filter(fn update -> update != :skip end)
|> Enum.uniq()
|> Enum.each(fn update ->
case update do
:broadcast ->
update_character(map_id, character_id)
_ ->
:ok
end
end)
:ok
end)
end)
end
defp update_character(map_id, character_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
Impl.broadcast!(map_id, :character_updated, character)
end
defp update_location(
character_id,
location,
old_location,
%{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state
) do
case is_nil(old_location.solar_system_id) and
ConnectionsImpl.can_add_location(map.scope, location.solar_system_id) do
true ->
:ok = SystemsImpl.maybe_add_system(map_id, location, nil, rtree_name, map_opts)
_ ->
ConnectionsImpl.is_connection_valid(
map.scope,
old_location.solar_system_id,
location.solar_system_id
)
|> case do
true ->
:ok =
SystemsImpl.maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
:ok =
SystemsImpl.maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
:ok =
ConnectionsImpl.maybe_add_connection(map_id, location, old_location, character_id)
_ ->
:ok
end
end
end
defp track_character(map_id, character_id),
do:
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: true,
track_online: true,
track_location: true,
track_ship: true
})
defp maybe_update_online(map_id, character_id) do
with {:ok, old_online} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:online"),
{:ok, %{online: online}} <-
WandererApp.Character.get_character(character_id) do
case old_online != online do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:online",
online
)
[{:character_online, %{online: online}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update online: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_ship(map_id, character_id) do
with {:ok, old_ship_type_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_type_id"),
{:ok, old_ship_name} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:ship_name"),
{:ok, %{ship: ship_type_id, ship_name: ship_name}} <-
WandererApp.Character.get_character(character_id) do
case old_ship_type_id != ship_type_id or
old_ship_name != ship_name do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:ship_type_id",
ship_type_id
)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:ship_name",
ship_name
)
[{:character_ship, %{ship: ship_type_id, ship_name: ship_name}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update ship: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_location(map_id, character_id) do
WandererApp.Cache.lookup!(
"character:#{character_id}:location_started",
false
)
|> case do
true ->
{:ok, old_solar_system_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id")
{:ok, %{solar_system_id: solar_system_id}} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:solar_system_id",
solar_system_id
)
case solar_system_id != old_solar_system_id do
true ->
[
{:character_location, %{solar_system_id: solar_system_id},
%{solar_system_id: old_solar_system_id}}
]
_ ->
[:skip]
end
false ->
{:ok, old_solar_system_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id")
{:ok, %{solar_system_id: solar_system_id} = _character} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:solar_system_id",
solar_system_id
)
if is_nil(old_solar_system_id) or solar_system_id != old_solar_system_id do
[
{:character_location, %{solar_system_id: solar_system_id}, %{solar_system_id: nil}}
]
else
[:skip]
end
end
end
defp maybe_update_alliance(map_id, character_id) do
with {:ok, old_alliance_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:alliance_id"),
{:ok, %{alliance_id: alliance_id}} <-
WandererApp.Character.get_character(character_id) do
case old_alliance_id != alliance_id do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:alliance_id",
alliance_id
)
[{:character_alliance, %{alliance_id: alliance_id}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update alliance: #{inspect(error, pretty: true)}")
[:skip]
end
end
defp maybe_update_corporation(map_id, character_id) do
with {:ok, old_corporation_id} <-
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:corporation_id"),
{:ok, %{corporation_id: corporation_id}} <-
WandererApp.Character.get_character(character_id) do
case old_corporation_id != corporation_id do
true ->
WandererApp.Cache.insert(
"map:#{map_id}:character:#{character_id}:corporation_id",
corporation_id
)
[{:character_corporation, %{corporation_id: corporation_id}}]
_ ->
[:skip]
end
else
error ->
Logger.error("Failed to update corporation: #{inspect(error, pretty: true)}")
[:skip]
end
end
end

View File

@@ -1,511 +0,0 @@
defmodule WandererApp.Map.Server.ConnectionsImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.Impl
# @ccp1 -1
@c1 1
@c2 2
@c3 3
@c4 4
@c5 5
@c6 6
@hs 7
@ls 8
@ns 9
# @ccp2 10
# @ccp3 11
@thera 12
@c13 13
@sentinel 14
@baribican 15
@vidette 16
@conflux 17
@redoubt 18
@a1 19
@a2 20
@a3 21
@a4 22
@a5 23
@ccp4 24
# @pochven 25
# @zarzakh 10100
@jita 30_000_142
@wh_space [
@c1,
@c2,
@c3,
@c4,
@c5,
@c6,
@c13,
@thera,
@sentinel,
@baribican,
@vidette,
@conflux,
@redoubt
]
@known_space [@hs, @ls, @ns]
@prohibited_systems [@jita]
@prohibited_system_classes [
@a1,
@a2,
@a3,
@a4,
@a5,
@ccp4
]
# this class of systems will guaranty that no one real class will take that place
# @unknown 100_100
#
@connection_time_status_eol 1
@connection_auto_eol_hours 21
@connection_auto_expire_hours 24
@connection_eol_expire_timeout :timer.hours(3) + :timer.minutes(30)
@connection_type_wormhole 0
@connection_type_stargate 1
def init_eol_cache(map_id, connections_eol_time) do
connections_eol_time
|> Enum.each(fn {connection_id, connection_eol_time} ->
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
connection_eol_time,
ttl: @connection_eol_expire_timeout
)
end)
end
def add_connection(
%{map_id: map_id} = state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
character_id: character_id
} = _connection_info
) do
:ok =
maybe_add_connection(
map_id,
%{solar_system_id: solar_system_target_id},
%{
solar_system_id: solar_system_source_id
},
character_id
)
state
end
def delete_connection(
%{map_id: map_id} = state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = _connection_info
) do
:ok =
maybe_remove_connection(map_id, %{solar_system_id: solar_system_target_id}, %{
solar_system_id: solar_system_source_id
})
state
end
def get_connection_info(
%{map_id: map_id} = _state,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = _connection_info
) do
WandererApp.Map.find_connection(
map_id,
solar_system_source_id,
solar_system_target_id
)
|> case do
{:ok, %{id: connection_id}} ->
connection_mark_eol_time = get_connection_mark_eol_time(map_id, connection_id, nil)
{:ok, %{marl_eol_time: connection_mark_eol_time}}
_ ->
{:error, :not_found}
end
end
def update_connection_time_status(
%{map_id: map_id} = state,
connection_update
),
do:
update_connection(state, :update_time_status, [:time_status], connection_update, fn
%{id: connection_id, time_status: time_status} ->
case time_status == @connection_time_status_eol do
true ->
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
DateTime.utc_now(),
ttl: @connection_eol_expire_timeout
)
_ ->
WandererApp.Cache.delete("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
end
end)
def update_connection_type(
state,
connection_update
),
do: update_connection(state, :update_type, [:type], connection_update)
def update_connection_mass_status(
state,
connection_update
),
do: update_connection(state, :update_mass_status, [:mass_status], connection_update)
def update_connection_ship_size_type(
state,
connection_update
),
do: update_connection(state, :update_ship_size_type, [:ship_size_type], connection_update)
def update_connection_locked(
state,
connection_update
),
do: update_connection(state, :update_locked, [:locked], connection_update)
def update_connection_custom_info(
state,
connection_update
),
do: update_connection(state, :update_custom_info, [:custom_info], connection_update)
def cleanup_connections(%{map_id: map_id} = state) do
state =
map_id
|> WandererApp.Map.list_connections!()
|> Enum.filter(fn %{
inserted_at: inserted_at,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id,
type: type
} ->
type != @connection_type_stargate &&
DateTime.diff(DateTime.utc_now(), inserted_at, :hour) >=
@connection_auto_eol_hours &&
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
)
end)
|> Enum.reduce(state, fn %{
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id
},
state ->
state
|> update_connection_time_status(%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id,
time_status: @connection_time_status_eol
})
end)
state =
map_id
|> WandererApp.Map.list_connections!()
|> Enum.filter(fn %{
id: connection_id,
inserted_at: inserted_at,
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id,
type: type
} ->
connection_mark_eol_time =
get_connection_mark_eol_time(map_id, connection_id)
reverse_connection =
WandererApp.Map.get_connection(
map_id,
solar_system_target_id,
solar_system_source_id
)
is_connection_exist =
is_connection_exist(
map_id,
solar_system_source_id,
solar_system_target_id
) || not is_nil(reverse_connection)
is_connection_valid =
is_connection_valid(
:wormholes,
solar_system_source_id,
solar_system_target_id
)
not is_connection_exist ||
(type != @connection_type_stargate && is_connection_valid &&
(DateTime.diff(DateTime.utc_now(), inserted_at, :hour) >=
@connection_auto_expire_hours ||
DateTime.diff(DateTime.utc_now(), connection_mark_eol_time, :hour) >=
@connection_auto_expire_hours - @connection_auto_eol_hours))
end)
|> Enum.reduce(state, fn %{
solar_system_source: solar_system_source_id,
solar_system_target: solar_system_target_id
},
state ->
state
|> delete_connection(%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
})
end)
state
end
def maybe_add_connection(map_id, location, old_location, character_id)
when not is_nil(location) and not is_nil(old_location) and
not is_nil(old_location.solar_system_id) and
location.solar_system_id != old_location.solar_system_id do
character_id
|> WandererApp.Character.get_character!()
|> case do
nil ->
:ok
character ->
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
end
case WandererApp.Map.check_connection(map_id, location, old_location) do
:ok ->
connection_type =
is_connection_valid(
:stargates,
old_location.solar_system_id,
location.solar_system_id
)
|> case do
true ->
@connection_type_stargate
_ ->
@connection_type_wormhole
end
{:ok, connection} =
WandererApp.MapConnectionRepo.create(%{
map_id: map_id,
solar_system_source: old_location.solar_system_id,
solar_system_target: location.solar_system_id,
type: connection_type
})
WandererApp.Map.add_connection(map_id, connection)
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
})
Impl.broadcast!(map_id, :add_connection, connection)
Impl.broadcast!(map_id, :maybe_link_signature, %{
character_id: character_id,
solar_system_source: old_location.solar_system_id,
solar_system_target: location.solar_system_id
})
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
end
end
def maybe_add_connection(_map_id, _location, _old_location, _character_id), do: :ok
def can_add_location(_scope, nil), do: false
def can_add_location(:all, _solar_system_id), do: true
def can_add_location(:none, _solar_system_id), do: false
def can_add_location(scope, solar_system_id) do
system_static_info =
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
system_static_info
_ ->
%{system_class: nil}
end
case scope do
:wormholes ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
@wh_space |> Enum.member?(system_static_info.system_class)
:stargates ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
@known_space |> Enum.member?(system_static_info.system_class)
_ ->
false
end
end
def is_connection_exist(map_id, from_solar_system_id, to_solar_system_id),
do:
not is_nil(
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: from_solar_system_id}
)
) &&
not is_nil(
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: to_solar_system_id}
)
)
def is_connection_valid(_scope, nil, _to_solar_system_id), do: false
def is_connection_valid(:all, _from_solar_system_id, _to_solar_system_id), do: true
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id) do
{:ok, known_jumps} =
WandererApp.Api.MapSolarSystemJumps.find(%{
before_system_id: from_solar_system_id,
current_system_id: to_solar_system_id
})
system_static_info =
case WandererApp.CachedInfo.get_system_static_info(to_solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
system_static_info
_ ->
%{system_class: nil}
end
case scope do
:wormholes ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?() and to_solar_system_id != @jita and
from_solar_system_id != @jita
:stargates ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not (known_jumps |> Enum.empty?())
end
end
def get_connection_mark_eol_time(map_id, connection_id, default \\ DateTime.utc_now()) do
WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
|> case do
nil ->
default
value ->
value
end
end
defp maybe_remove_connection(map_id, location, old_location)
when not is_nil(location) and not is_nil(old_location) and
location.solar_system_id != old_location.solar_system_id do
case WandererApp.Map.find_connection(
map_id,
location.solar_system_id,
old_location.solar_system_id
) do
{:ok, connection} when not is_nil(connection) ->
:ok = WandererApp.MapConnectionRepo.destroy(map_id, connection)
Impl.broadcast!(map_id, :remove_connections, [connection])
map_id |> WandererApp.Map.remove_connection(connection)
_error ->
:ok
end
end
defp maybe_remove_connection(_map_id, _location, _old_location), do: :ok
defp update_connection(
%{map_id: map_id} = state,
update_method,
attributes,
%{
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = update,
callback_fn \\ nil
) do
with {:ok, connection} <-
WandererApp.Map.find_connection(
map_id,
solar_system_source_id,
solar_system_target_id
),
{:ok, update_map} <- Impl.get_update_map(update, attributes),
{:ok, updated_connection} <-
apply(WandererApp.MapConnectionRepo, update_method, [
connection,
update_map
]),
:ok <-
WandererApp.Map.update_connection(
map_id,
connection |> Map.merge(update_map)
) do
if not is_nil(callback_fn) do
callback_fn.(updated_connection)
end
Impl.broadcast!(map_id, :update_connection, updated_connection)
state
else
{:error, error} ->
Logger.error("Failed to update connection: #{inspect(error, pretty: true)}")
state
end
end
end

View File

@@ -1,506 +0,0 @@
defmodule WandererApp.Map.Server.SystemsImpl do
@moduledoc false
require Logger
alias WandererApp.Map.Server.{Impl}
@ddrt Application.compile_env(:wanderer_app, :ddrt)
@system_auto_expire_minutes 15
@system_inactive_timeout :timer.minutes(15)
def init_last_activity_cache(map_id, systems_last_activity) do
systems_last_activity
|> Enum.each(fn {system_id, last_activity} ->
WandererApp.Cache.put(
"map_#{map_id}:system_#{system_id}:last_activity",
last_activity,
ttl: @system_inactive_timeout
)
end)
end
def init_map_systems(state, [] = _systems), do: state
def init_map_systems(%{map_id: map_id, rtree_name: rtree_name} = state, systems) do
systems
|> Enum.each(fn %{id: system_id, solar_system_id: solar_system_id} = system ->
@ddrt.insert(
{solar_system_id, WandererApp.Map.PositionCalculator.get_system_bounding_rect(system)},
rtree_name
)
WandererApp.Cache.put(
"map_#{map_id}:system_#{system_id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
end)
state
end
def add_system(
%{map_id: map_id} = state,
%{
solar_system_id: solar_system_id
} = system_info,
user_id,
character_id
) do
case map_id |> WandererApp.Map.check_location(%{solar_system_id: solar_system_id}) do
{:ok, _location} ->
state |> _add_system(system_info, user_id, character_id)
{:error, :already_exists} ->
state
end
end
def cleanup_systems(%{map_id: map_id} = state) do
expired_systems =
map_id
|> WandererApp.Map.list_systems!()
|> Enum.filter(fn %{
id: system_id,
visible: system_visible,
locked: system_locked,
solar_system_id: solar_system_id
} = _system ->
last_updated_time =
WandererApp.Cache.get("map_#{map_id}:system_#{system_id}:last_activity")
if system_visible and not system_locked and
(is_nil(last_updated_time) or
DateTime.diff(DateTime.utc_now(), last_updated_time, :minute) >=
@system_auto_expire_minutes) do
no_active_connections? =
map_id
|> WandererApp.Map.find_connections(solar_system_id)
|> Enum.empty?()
no_active_characters? =
map_id |> WandererApp.Map.get_system_characters(solar_system_id) |> Enum.empty?()
no_active_connections? and no_active_characters?
else
false
end
end)
|> Enum.map(& &1.solar_system_id)
case expired_systems |> Enum.empty?() do
false ->
state |> delete_systems(expired_systems, nil, nil)
_ ->
state
end
end
def update_system_name(
state,
update
),
do: state |> update_system(:update_name, [:name], update)
def update_system_description(
state,
update
),
do: state |> update_system(:update_description, [:description], update)
def update_system_status(
state,
update
),
do: state |> update_system(:update_status, [:status], update)
def update_system_tag(
state,
update
),
do: state |> update_system(:update_tag, [:tag], update)
def update_system_locked(
state,
update
),
do: state |> update_system(:update_locked, [:locked], update)
def update_system_labels(
state,
update
),
do: state |> update_system(:update_labels, [:labels], update)
def update_system_position(
%{rtree_name: rtree_name} = state,
update
),
do:
state
|> update_system(
:update_position,
[:position_x, :position_y],
update,
fn updated_system ->
@ddrt.update(
updated_system.solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(updated_system),
rtree_name
)
end
)
def add_hub(
%{map_id: map_id} = state,
hub_info
) do
with :ok <- WandererApp.Map.add_hub(map_id, hub_info),
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs(),
{:ok, _} <-
WandererApp.MapRepo.update_hubs(map_id, hubs) do
Impl.broadcast!(map_id, :update_map, %{hubs: hubs})
state
else
error ->
Logger.error("Failed to add hub: #{inspect(error, pretty: true)}")
state
end
end
def remove_hub(
%{map_id: map_id} = state,
hub_info
) do
with :ok <- WandererApp.Map.remove_hub(map_id, hub_info),
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs(),
{:ok, _} <-
WandererApp.MapRepo.update_hubs(map_id, hubs) do
Impl.broadcast!(map_id, :update_map, %{hubs: hubs})
state
else
error ->
Logger.error("Failed to remove hub: #{inspect(error, pretty: true)}")
state
end
end
def delete_systems(
%{map_id: map_id, rtree_name: rtree_name} = state,
removed_ids,
user_id,
character_id
) do
connections_to_remove =
removed_ids
|> Enum.map(fn solar_system_id ->
WandererApp.Map.find_connections(map_id, solar_system_id)
end)
|> List.flatten()
|> Enum.uniq_by(& &1.id)
:ok = WandererApp.Map.remove_connections(map_id, connections_to_remove)
:ok = WandererApp.Map.remove_systems(map_id, removed_ids)
removed_ids
|> Enum.each(fn solar_system_id ->
map_id
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|> case do
{:ok, _} ->
:ok
{:error, error} ->
Logger.error("Failed to remove system from map: #{inspect(error, pretty: true)}")
:ok
end
end)
connections_to_remove
|> Enum.each(fn connection ->
Logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end)
WandererApp.MapConnectionRepo.destroy(map_id, connection)
end)
removed_ids
|> Enum.map(fn solar_system_id ->
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
end)
|> List.flatten()
|> Enum.uniq_by(& &1.system_id)
|> Enum.each(fn s ->
{:ok, %{system: system}} = s |> Ash.load([:system])
Ash.destroy!(s)
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
end)
@ddrt.delete(removed_ids, rtree_name)
Impl.broadcast!(map_id, :remove_connections, connections_to_remove)
Impl.broadcast!(map_id, :systems_removed, removed_ids)
case not is_nil(user_id) do
true ->
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_ids: removed_ids
})
:telemetry.execute(
[:wanderer_app, :map, :systems, :remove],
%{count: removed_ids |> Enum.count()}
)
:ok
_ ->
:ok
end
state
end
def maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
when not is_nil(location) do
case WandererApp.Map.check_location(map_id, location) do
{:ok, location} ->
{:ok, position} = calc_new_system_position(map_id, old_location, rtree_name, map_opts)
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(
map_id,
location.solar_system_id
) do
{:ok, existing_system} when not is_nil(existing_system) ->
{:ok, updated_system} =
existing_system
|> WandererApp.MapSystemRepo.update_position!(%{
position_x: position.x,
position_y: position.y
})
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.update_visible!(%{visible: true})
|> WandererApp.MapSystemRepo.cleanup_tags()
@ddrt.insert(
{existing_system.solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: position.x,
position_y: position.y
})},
rtree_name
)
WandererApp.Cache.put(
"map_#{map_id}:system_#{updated_system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
WandererApp.Map.add_system(map_id, updated_system)
Impl.broadcast!(map_id, :add_system, updated_system)
:ok
_ ->
{:ok, solar_system_info} =
WandererApp.CachedInfo.get_system_static_info(location.solar_system_id)
WandererApp.MapSystemRepo.create(%{
map_id: map_id,
solar_system_id: location.solar_system_id,
name: solar_system_info.solar_system_name,
position_x: position.x,
position_y: position.y
})
|> case do
{:ok, new_system} ->
@ddrt.insert(
{new_system.solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(new_system)},
rtree_name
)
WandererApp.Cache.put(
"map_#{map_id}:system_#{new_system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
WandererApp.Map.add_system(map_id, new_system)
Impl.broadcast!(map_id, :add_system, new_system)
:ok
error ->
Logger.warning("Failed to create system: #{inspect(error, pretty: true)}")
:ok
end
end
error ->
Logger.debug("Skip adding system: #{inspect(error, pretty: true)}")
:ok
end
end
def maybe_add_system(_map_id, _location, _old_location, _rtree_name, _map_opts), do: :ok
defp _add_system(
%{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state,
%{
solar_system_id: solar_system_id,
coordinates: coordinates
} = system_info,
user_id,
character_id
) do
%{"x" => x, "y" => y} =
coordinates
|> case do
%{"x" => x, "y" => y} ->
%{"x" => x, "y" => y}
_ ->
%{x: x, y: y} =
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
%{"x" => x, "y" => y}
end
{:ok, system} =
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
{:ok, existing_system} when not is_nil(existing_system) ->
use_old_coordinates = Map.get(system_info, :use_old_coordinates, false)
if use_old_coordinates do
@ddrt.insert(
{solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: existing_system.position_x,
position_y: existing_system.position_y
})},
rtree_name
)
existing_system
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
else
@ddrt.insert(
{solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: x,
position_y: y
})},
rtree_name
)
existing_system
|> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y})
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.cleanup_tags!()
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
end
_ ->
{:ok, solar_system_info} =
WandererApp.CachedInfo.get_system_static_info(solar_system_id)
@ddrt.insert(
{solar_system_id,
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
position_x: x,
position_y: y
})},
rtree_name
)
WandererApp.MapSystemRepo.create(%{
map_id: map_id,
solar_system_id: solar_system_id,
name: solar_system_info.solar_system_name,
position_x: x,
position_y: y
})
end
:ok = map_id |> WandererApp.Map.add_system(system)
WandererApp.Cache.put(
"map_#{map_id}:system_#{system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
Impl.broadcast!(map_id, :add_system, system)
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: solar_system_id
})
state
end
defp calc_new_system_position(map_id, old_location, rtree_name, opts),
do:
{:ok,
map_id
|> WandererApp.Map.find_system_by_location(old_location)
|> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)}
defp update_system(
%{map_id: map_id} = state,
update_method,
attributes,
update,
callback_fn \\ nil
) do
with :ok <- WandererApp.Map.update_system_by_solar_system_id(map_id, update),
{:ok, system} <-
WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(
map_id,
update.solar_system_id
),
{:ok, update_map} <- Impl.get_update_map(update, attributes) do
{:ok, updated_system} =
apply(WandererApp.MapSystemRepo, update_method, [
system,
update_map
])
if not is_nil(callback_fn) do
callback_fn.(updated_system)
end
update_map_system_last_activity(map_id, updated_system)
state
else
error ->
Logger.error("Fail ed to update system: #{inspect(error, pretty: true)}")
state
end
end
defp update_map_system_last_activity(
map_id,
updated_system
) do
WandererApp.Cache.put(
"map_#{map_id}:system_#{updated_system.id}:last_activity",
DateTime.utc_now(),
ttl: @system_inactive_timeout
)
Impl.broadcast!(map_id, :update_system, updated_system)
end
end

Some files were not shown because too many files have changed in this diff Show More