Compare commits

...

57 Commits

Author SHA1 Message Date
CI
9aec57166d chore: release version v1.29.5
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-14 22:32:16 +00:00
Dmitry Popov
a3739f2950 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-14 23:31:47 +01:00
Dmitry Popov
3d3b152758 fix(Core): Fix character trackers cleanup 2024-12-14 23:31:42 +01:00
CI
0e03730543 chore: release version v1.29.4
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-10 11:01:09 +00:00
Dmitry Popov
97e07a6511 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-10 12:00:38 +01:00
Dmitry Popov
a77a51ba15 fix(Core): Small fixes 2024-12-10 12:00:34 +01:00
CI
42e706e1c2 chore: release version v1.29.3
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-07 18:02:05 +00:00
Dmitry Popov
025dd06053 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-07 19:01:36 +01:00
Dmitry Popov
bcb421d879 fix(Core): Increased eve DB data download timeout 2024-12-07 19:01:32 +01:00
CI
66056ab54b chore: release version v1.29.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-07 08:32:58 +00:00
Dmitry Popov
bb92f76ceb Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-07 09:32:28 +01:00
Dmitry Popov
84076b340b fix(Core): Fix unpkg CDN issues, fix Abyssals sites adding as systems on map 2024-12-07 09:32:24 +01:00
CI
48caae5c0e chore: release version v1.29.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-05 10:54:41 +00:00
Dmitry Popov
77dd23795a Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-05 11:54:05 +01:00
Dmitry Popov
2771d6304e chore: release version v1.28.1 2024-12-05 11:54:01 +01:00
CI
9946edffa4 chore: release version v1.29.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-05 08:52:42 +00:00
Dmitry Popov
50bf2fd9d3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-05 09:52:13 +01:00
Dmitry Popov
bdcde168aa feat(Signatures): Show 'Unsplashed' signatures on the map (optionally) 2024-12-05 09:52:07 +01:00
CI
5807142e20 chore: release version v1.28.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-04 20:14:44 +00:00
Dmitry Popov
ec2d9565b9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-04 21:14:18 +01:00
Dmitry Popov
a18a71c73d chore: release version v1.27.1 2024-12-04 21:14:14 +01:00
CI
93a6bd1156 chore: release version v1.28.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-04 17:02:43 +00:00
Dmitry Popov
581a410aef Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-04 18:02:08 +01:00
Dmitry Popov
ab02fe988c feat(Map): Added an option to show 'Offline characters' to map admins & managers only
- updated UI layout for map settings modal
2024-12-04 18:01:50 +01:00
CI
b8d20fb21b chore: release version v1.27.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-04 08:14:30 +00:00
Dmitry Popov
12fa1a0be8 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-04 09:13:57 +01:00
Dmitry Popov
85a84f7507 fix(Map): Fix 'On the map' visibility 2024-12-04 09:13:54 +01:00
CI
2385313013 chore: release version v1.27.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-03 21:04:59 +00:00
Dmitry Popov
c7ce727571 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-03 22:04:26 +01:00
Dmitry Popov
8b165ff478 feat(Map): Hide 'On the map' list for 'Viewer' role 2024-12-03 22:04:20 +01:00
CI
6d7d0cc72d chore: release version v1.26.1 2024-12-03 20:05:05 +00:00
Dmitry Popov
f7eba5d4fd Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-03 21:04:32 +01:00
Dmitry Popov
73ef6dae73 fix(Signatures): Fix error on splash wh 2024-12-03 21:04:26 +01:00
CI
7fa6df1e5e chore: release version v1.26.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-12-03 16:54:46 +00:00
Dmitry Popov
e1a2ffb151 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-03 17:54:18 +01:00
Dmitry Popov
6d7727a32d feat(Signatures): Keep 'Lazy delete' enabled setting 2024-12-03 17:54:13 +01:00
CI
6d7a94bd5a chore: release version v1.25.2
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-01 07:26:29 +00:00
Dmitry Popov
ecc3fb17e1 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-12-01 08:25:59 +01:00
Dmitry Popov
209e2bf0a5 fix(Signatures): Fix lazy delete on system switch 2024-12-01 08:25:10 +01:00
CI
b1947e57a4 chore: release version v1.25.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-11-28 23:20:10 +00:00
Dmitry Popov
74507501a5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-11-29 00:19:43 +01:00
Dmitry Popov
c73481fd58 fix(Signatures): Fix colors & add 'Backspace' hotkey to delete signatures 2024-11-29 00:19:26 +01:00
CI
7795ad0b0c chore: release version v1.25.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-11-28 10:09:57 +00:00
Dmitry Popov
aff768f413 feat(Signatures): Automatically remove signature if linked system removed 2024-11-28 11:09:24 +01:00
CI
310b60f5b6 chore: release version v1.24.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-11-27 23:42:04 +00:00
Dmitry Popov
100f0be86a fix(Signatures): Fix paste signatures 2024-11-28 00:41:01 +01:00
CI
87e115e40d chore: release version v1.24.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-11-27 15:37:38 +00:00
Dmitry Popov
ef5f36e4c4 Signatures k162 types (#77)
* feat(Signatures): Added ability to specify K162 type (actually next types supported Hi/Low/Null/C1-C6/Thera/Pochven)
2024-11-27 19:37:10 +04:00
CI
099650420d chore: release version v1.24.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-11-27 13:28:03 +00:00
Dmitry Popov
8ccf7fffa5 feat(Signatures): Added "Lazy delete" option & got rid of update popup 2024-11-27 14:27:27 +01:00
CI
b97a055bf7 chore: release version v1.23.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2024-11-26 21:05:58 +00:00
Dmitry Popov
663fee6699 feat(Map): Lock systems available to manager/admin roles only (#75)
* feat(Map): Lock systems available to manager/admin roles only

* feat(Map): Fix add system & add acl member select behaviour
2024-11-27 01:05:26 +04:00
CI
33d5f3938b chore: release version v1.22.0 2024-11-26 19:03:01 +00:00
Aleksei Chichenkov
ef6b45d7a1 feat(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)
Co-authored-by: achichenkov <aleksei.chichenkov@telleqt.ai>
2024-11-26 23:02:30 +04:00
CI
c1ecd3690e chore: release version v1.21.0
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-11-24 15:55:38 +00:00
Aleksei Chichenkov
3250fe1ec6 Merge pull request #74 from wanderer-industries/dessign-issues-2
feat(Map): add new gate design, change EOL placement
2024-11-24 18:55:12 +03:00
achichenkov
48e8cd93b9 feat(Map): add new gate design, change EOL placement 2024-11-24 18:30:40 +03:00
91 changed files with 6460 additions and 3611 deletions

View File

@@ -2,6 +2,187 @@
<!-- changelog -->
## [v1.29.5](https://github.com/wanderer-industries/wanderer/compare/v1.29.4...v1.29.5) (2024-12-14)
### Bug Fixes:
* Core: Fix character trackers cleanup
## [v1.29.4](https://github.com/wanderer-industries/wanderer/compare/v1.29.3...v1.29.4) (2024-12-10)
### Bug Fixes:
* Core: Small fixes
## [v1.29.3](https://github.com/wanderer-industries/wanderer/compare/v1.29.2...v1.29.3) (2024-12-07)
### Bug Fixes:
* Core: Increased eve DB data download timeout
## [v1.29.2](https://github.com/wanderer-industries/wanderer/compare/v1.29.1...v1.29.2) (2024-12-07)
### Bug Fixes:
* Core: Fix unpkg CDN issues, fix Abyssals sites adding as systems on map
## [v1.29.1](https://github.com/wanderer-industries/wanderer/compare/v1.29.0...v1.29.1) (2024-12-05)
## [v1.29.0](https://github.com/wanderer-industries/wanderer/compare/v1.28.1...v1.29.0) (2024-12-05)
### Features:
* Signatures: Show 'Unsplashed' signatures on the map (optionally)
## [v1.28.1](https://github.com/wanderer-industries/wanderer/compare/v1.28.0...v1.28.1) (2024-12-04)
## [v1.28.0](https://github.com/wanderer-industries/wanderer/compare/v1.27.1...v1.28.0) (2024-12-04)
### Features:
* Map: Added an option to show 'Offline characters' to map admins & managers only
## [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)

View File

@@ -870,3 +870,63 @@ body {
}
}
/* Map refresh END */
.inputContainer {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
}
.inputContainer > span:nth-child(1),
.inputContainer > label:nth-child(1) {
color: var(--gray-200);
font-size: 13px;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.inputContainer > :nth-child(2) {
border-bottom: 2px dotted #3f3f3f;
height: 1px;
margin: 0 12px;
}
.smallInputSwitch {
height: 100%;
display: flex;
align-items: center;
}
.smallInputSwitch .p-inputswitch {
height: 1rem;
width: 2rem;
}
.smallInputSwitch .p-inputswitch.p-inputswitch-checked .p-inputswitch-slider::before {
transform: translateX(1rem);
}
.smallInputSwitch .p-inputswitch.p-highlight .p-inputswitch-slider:before {
transform: translateX(1rem);
}
.smallInputSwitch .p-inputswitch .p-inputswitch-slider::before {
width: 0.8rem;
height: 0.8rem;
margin-top: -0.4rem;
margin-left: -3px;
}
.checkboxRoot.sizeXS {
width: 14px;
height: 14px;
}
.checkboxRoot.sizeXS .p-checkbox-box,
.checkboxRoot.sizeXS .p-checkbox-input {
width: 14px;
height: 14px;
}
.checkboxRoot.sizeM {
width: 16px;
height: 16px;
}
.checkboxRoot.sizeM .p-checkbox-box,
.checkboxRoot.sizeM .p-checkbox-input {
width: 16px;
height: 16px;
}

View File

@@ -6,6 +6,8 @@ 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,
@@ -25,6 +27,7 @@ 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;
@@ -58,19 +61,25 @@ export const useContextMenuSystemItems = ({
command: onHubToggle,
},
...(system.locked
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
? canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []
: [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
...(canLockSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
{ separator: true },
{
label: 'Delete',
@@ -80,6 +89,7 @@ export const useContextMenuSystemItems = ({
]),
];
}, [
canLockSystem,
systems,
systemId,
getTags,

View File

@@ -32,6 +32,7 @@ const INITIAL_DATA: MapData = {
visibleNodes: new Set(),
showKSpaceBG: false,
isThickConnections: false,
userPermissions: {},
};
export interface MapContextProps {

View File

@@ -24,6 +24,10 @@
stroke: #d4f0ff;
}
&.Gate {
stroke: #1c1e15;
}
&.Hovered {
stroke: #4e5d6c;
stroke-width: 2px;
@@ -76,6 +80,11 @@
stroke-width: 6px;
}
}
&.Gate {
stroke: #9aff40;
}
}
.ClickPath {

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, 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';
@@ -46,7 +46,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const [edgePath, labelX, labelY] = getBezierPath({
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
sourcePosition: sourcePos,
@@ -54,8 +56,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
targetX: tx + offset.x,
targetY: ty + offset.y,
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode]);
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -69,6 +72,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.Tick]: isThickConnections,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.Hovered]: hovered,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -82,6 +86,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}

View File

@@ -6,7 +6,7 @@ $pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020; // Темный фон для подсказок
$tooltip-bg: #202020; // Dark background for tooltips
.RootCustomNode {
display: flex;
@@ -136,7 +136,7 @@ $tooltip-bg: #202020; // Темный фон для подсказок
.Bookmarks {
position: absolute;
width: 100%;
z-index: 0;
z-index: 1;
display: flex;
left: 4px;
@@ -182,6 +182,42 @@ $tooltip-bg: #202020; // Темный фон для подсказок
}
}
.Unsplashed {
position: absolute;
width: calc(50% - 4px);
z-index: -1;
display: flex;
flex-wrap: wrap;
gap: 2px;
left: 2px;
&--right {
left: calc(50% + 6px);
}
& > .Signature {
width: 13px;
height: 4px;
position: relative;
top: 3px;
border-radius: 5px;
color: #ffffff;
font-size: 8px;
text-align: center;
padding-top: 2px;
font-weight: bolder;
padding-left: 3px;
padding-right: 3px;
display: block;
background-color: #833ca4;
&:not(:first-child) {
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
}
}
}
.icon {
width: 8px;
height: 8px;
@@ -276,7 +312,6 @@ $tooltip-bg: #202020; // Темный фон для подсказок
position: relative;
top: -1px;
}
}
.Handlers {

View File

@@ -3,6 +3,8 @@ import { Handle, Position, WrapNodeProps } from 'reactflow';
import { MapSolarSystemType } from '../../map.types';
import classes from './SolarSystemNode.module.scss';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
EFFECT_BACKGROUND_STYLES,
LABELS_INFO,
@@ -12,8 +14,9 @@ import {
} from '@/hooks/Mapper/components/map/constants.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { PrimeIcons } from 'primereact/api';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
@@ -50,6 +53,9 @@ export const getActivityType = (count: number) => {
// eslint-disable-next-line react/display-name
export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarSystemType>) => {
const { interfaceSettings } = useMapRootState();
const { isShowUnsplashedSignatures } = interfaceSettings;
const {
system_class,
security,
@@ -63,6 +69,8 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
solar_system_name,
} = data.system_static_info;
const signatures = data.system_signatures;
const { locked, name, tag, status, labels, id } = data || {};
const customName = solar_system_name !== name ? name : undefined;
@@ -128,6 +136,22 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
}
return prepareUnsplashedChunks(
signatures
.filter(s => s.group === 'Wormhole' && !s.linked_system)
.map(s => ({
eve_id: s.eve_id,
type: s.type,
custom_info: s.custom_info,
})),
);
}, [isShowUnsplashedSignatures, signatures]);
return (
<>
{visible && (
@@ -237,6 +261,22 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
)}
</div>
{visible && isShowUnsplashedSignatures && (
<div className={classes.Unsplashed}>
{unsplashedLeft.map(x => (
<UnsplashedSignature key={x.sig_id} signature={x} />
))}
</div>
)}
{visible && isShowUnsplashedSignatures && (
<div className={clsx([classes.Unsplashed, classes['Unsplashed--right']])}>
{unsplashedRight.map(x => (
<UnsplashedSignature key={x.sig_id} signature={x} />
))}
</div>
)}
<div onMouseDownCapture={dbClick} className={classes.Handlers}>
<Handle
type="source"

View File

@@ -0,0 +1,18 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.Signature {
position: relative;
top: 3px;
display: block;
& > .Box {
width: 13px;
height: 4px;
border-radius: 4px;
color: #ffffff;
font-size: 8px;
text-align: center;
font-weight: bolder;
display: block;
}
}

View File

@@ -0,0 +1,65 @@
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
import classes from './UnsplashedSignature.module.scss';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import { useMemo } from 'react';
import clsx from 'clsx';
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { k162Types } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
interface UnsplashedSignatureProps {
signature: SystemSignature;
}
export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) => {
const {
data: { wormholesData },
} = useMapRootState();
const whData = useMemo(() => wormholesData[signature.type], [signature.type, wormholesData]);
const whClass = useMemo(() => (whData ? WORMHOLES_ADDITIONAL_INFO[whData.dest] : null), [whData]);
const k162TypeOption = useMemo(() => {
if (!signature.custom_info) {
return null;
}
const customInfo = JSON.parse(signature.custom_info);
if (!customInfo.k162Type) {
return null;
}
return k162Types.find(x => x.value === customInfo.k162Type);
}, [signature]);
const whClassStyle = useMemo(() => {
if (signature.type === 'K162' && k162TypeOption) {
const k162Data = wormholesData[k162TypeOption.whClassName];
const k162Class = k162Data ? WORMHOLES_ADDITIONAL_INFO[k162Data.dest] : null;
return k162Class ? WORMHOLE_CLASS_STYLES[k162Class.wormholeClassID] : '';
}
return whClass ? WORMHOLE_CLASS_STYLES[whClass.wormholeClassID] : '';
}, [signature, whClass, k162TypeOption, wormholesData]);
return (
<WdTooltipWrapper
className={clsx(classes.Signature)}
content={
(
<div className="flex flex-col gap-1">
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
{renderInfoColumn(signature)}
</InfoDrawer>
</div>
) as React.ReactNode
}
>
<div className={clsx(classes.Box, whClassStyle)}>
<svg width="13" height="4" viewBox="0 0 13 4" xmlns="http://www.w3.org/2000/svg">
<rect width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
</svg>
</div>
</WdTooltipWrapper>
);
};

View File

@@ -0,0 +1 @@
export * from './UnsplashedSignature.tsx';

View File

@@ -3,3 +3,4 @@ export * from './convertSystem2Node';
export * from './getSystemClassStyles';
export * from './getShapeClass';
export * from './getBackgroundClass';
export * from './prepareUnsplashedChunks';

View File

@@ -0,0 +1,27 @@
// Helper function to split an array into chunks of size
const chunkArray = (array: any[], size: number) => {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
};
export const prepareUnsplashedChunks = (items: any[]) => {
// Split the items into chunks of 4
const chunks = chunkArray(items, 4);
// Get the column elements
const leftColumn: any[] = [];
const rightColumn: any[] = [];
chunks.forEach((chunk, index) => {
const column = index % 2 === 0 ? leftColumn : rightColumn;
chunk.forEach(item => {
column.push(item);
});
});
return [leftColumn, rightColumn];
};

View File

@@ -6,6 +6,7 @@ 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,
@@ -20,6 +21,7 @@ interface SystemLinkSignatureDialogProps {
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 },
];
export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignatureDialogProps) => {

View File

@@ -8,6 +8,8 @@ import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { CharacterCard, LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
import useLocalStorageState from 'use-local-storage-state';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
type CharItemProps = {
compact: boolean;
@@ -62,6 +64,14 @@ export const LocalCharacters = () => {
const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
const showOffline = useMemo(
() => !restrictOfflineShowing || isAdminOrManager,
[isAdminOrManager, restrictOfflineShowing],
);
const itemTemplate = useItemTemplate();
const sorted = useMemo(() => {
@@ -70,13 +80,13 @@ export const LocalCharacters = () => {
.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id), compact: settings.compact }))
.sort(sortCharacters);
if (!settings.showOffline) {
if (!showOffline || !settings.showOffline) {
return sorted.filter(c => c.online);
}
return sorted;
// eslint-disable-next-line
}, [characters, settings.showOffline, settings.compact, systemId, userCharacters, presentCharacters]);
}, [showOffline, characters, settings.showOffline, settings.compact, systemId, userCharacters, presentCharacters]);
const isNobodyHere = sorted.length === 0;
const isNotSelectedSystem = selectedSystems.length !== 1;
@@ -88,14 +98,16 @@ export const LocalCharacters = () => {
<div className="flex justify-between items-center text-xs w-full">
<span className="select-none">Local{showList ? ` [${sorted.length}]` : ''}</span>
<LayoutEventBlocker className="flex items-center gap-2">
<WdCheckbox
size="xs"
labelSide="left"
label={'Show offline'}
value={settings.showOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))}
/>
{showOffline && (
<WdCheckbox
size="xs"
labelSide="left"
label={'Show offline'}
value={settings.showOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))}
/>
)}
<span
className={clsx('w-4 h-4 cursor-pointer', {
@@ -115,7 +127,9 @@ export const LocalCharacters = () => {
)}
{isNobodyHere && !isNotSelectedSystem && (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">Nobody here</div>
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
Nobody here
</div>
)}
{showList && (

View File

@@ -1,12 +1,11 @@
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 { CheckboxChangeEvent } from 'primereact/checkbox';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
interface RoutesSettingsDialog {
visible: boolean;
@@ -38,8 +37,8 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
currentData.current = data;
const handleChangeEvent = useCallback(
(propName: keyof RoutesType) => (event: CheckboxChangeEvent) => {
optionsRef.current = { ...optionsRef.current, [propName]: event.checked };
(propName: keyof RoutesType) => (event: boolean) => {
optionsRef.current = { ...optionsRef.current, [propName]: event };
updateKey(x => x + 1);
},
[],
@@ -71,14 +70,14 @@ export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDial
setVisible(false);
}}
>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3 p-2.5">
<div className="flex flex-col gap-2 mb-2">
{checkboxes.map(({ label, propName }) => (
<WdCheckbox
<PrettySwitchbox
key={propName}
label={label}
value={optionsRef.current[propName]}
onChange={handleChangeEvent(propName)}
checked={optionsRef.current[propName]}
setChecked={handleChangeEvent(propName)}
/>
))}
</div>

View File

@@ -1,9 +1,9 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useState } from 'react';
import { Button } from 'primereact/button';
import { Checkbox } from 'primereact/checkbox';
import { TabPanel, TabView } from 'primereact/tabview';
import styles from './SystemSignatureSettingsDialog.module.scss';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
export type Setting = { key: string; name: string; value: boolean; isFilter?: boolean };
@@ -41,8 +41,8 @@ export const SystemSignatureSettingsDialog = ({
}, [onSave, settings]);
return (
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg">
<div className="flex flex-col gap-3">
<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">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView
@@ -54,16 +54,12 @@ export const SystemSignatureSettingsDialog = ({
<div className="w-full h-full flex flex-col gap-1">
{filterSettings.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>
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>
@@ -72,16 +68,12 @@ export const SystemSignatureSettingsDialog = ({
<div className="w-full h-full flex flex-col gap-1">
{userSettings.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>
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>

View File

@@ -1,5 +1,11 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { InfoDrawer, LayoutEventBlocker, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import {
InfoDrawer,
LayoutEventBlocker,
TooltipPosition,
WdImgButton,
WdCheckbox,
} from '@/hooks/Mapper/components/ui-kit';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import {
Setting,
@@ -14,19 +20,24 @@ import {
} from './SystemSignatureSettingsDialog';
import { SignatureGroup } from '@/hooks/Mapper/types';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState, useMemo } 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_v4_1';
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_2';
export const SHOW_DESCRIPTION_COLUMN_SETTING = 'show_description_column_setting';
export const SHOW_INSERTED_COLUMN_SETTING = 'show_inserted_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_INSERTED_COLUMN_SETTING, name: 'Show Inserted Column', value: false, isFilter: false },
{ 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 },
@@ -58,12 +69,25 @@ 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);
@@ -79,6 +103,15 @@ 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={{
@@ -102,7 +135,7 @@ 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">Backspace</b>
<br /> and then use <b className="text-sky-500">Del</b>
</InfoDrawer>
</div>
) as React.ReactNode,
@@ -118,7 +151,7 @@ export const SystemSignatures = () => {
System is not selected
</div>
) : (
<SystemSignaturesContent systemId={systemId} settings={settings} />
<SystemSignaturesContent systemId={systemId} settings={settings} onLazyDeleteChange={handleLazyDeleteChange} />
)}
{visible && (
<SystemSignatureSettingsDialog

View File

@@ -1,5 +1,4 @@
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';
@@ -12,6 +11,7 @@ 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,10 +22,10 @@ import {
getRowColorByTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers';
import {
renderAddedTimeLeft,
renderDescription,
renderIcon,
renderInfoColumn,
renderInsertedTimeLeft,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import useLocalStorageState from 'use-local-storage-state';
@@ -36,7 +36,9 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import {
SHOW_DESCRIPTION_COLUMN_SETTING,
SHOW_INSERTED_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;
@@ -44,7 +46,7 @@ type SystemSignaturesSortSettings = {
};
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
sortField: 'updated_at',
sortField: 'inserted_at',
sortOrder: -1,
};
@@ -54,6 +56,7 @@ interface SystemSignaturesContentProps {
hideLinkedSignatures?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
}
export const SystemSignaturesContent = ({
systemId,
@@ -61,14 +64,13 @@ export const SystemSignaturesContent = ({
hideLinkedSignatures,
selectable,
onSelect,
onLazyDeleteChange,
}: 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);
@@ -80,10 +82,20 @@ 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 } = useClipboard();
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 handleResize = useCallback(() => {
if (tableRef.current) {
@@ -100,10 +112,7 @@ export const SystemSignaturesContent = ({
[settings],
);
const showInsertedColumn = useMemo(
() => settings.find(s => s.key === SHOW_INSERTED_COLUMN_SETTING)?.value,
[settings],
);
const showUpdatedColumn = useMemo(() => settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value, [settings]);
const filteredSignatures = useMemo(() => {
return signatures
@@ -136,13 +145,17 @@ export const SystemSignaturesContent = ({
data: { system_id: systemId },
});
setAskUser(false);
setSignatures(signatures);
}, [outCommand, systemId]);
const handleUpdateSignatures = useCallback(
async (newSignatures: SystemSignature[], updateOnly: boolean) => {
const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
async (newSignatures: SystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newSignatures,
updateOnly,
skipUpdateUntouched,
);
const { signatures: updatedSignatures } = await outCommand({
type: OutCommand.updateSignatures,
@@ -160,34 +173,32 @@ export const SystemSignaturesContent = ({
[outCommand, systemId],
);
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 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 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
@@ -201,38 +212,51 @@ export const SystemSignaturesContent = ({
[onSelect, selectable],
);
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace'], handleDeleteSelected);
useEffect(() => {
if (selectable) {
return;
}
if (!clipboardContent) {
return;
}
const handlePaste = async (clipboardContent: string) => {
const newSignatures = parseSignatures(
clipboardContent,
settings.map(x => x.key),
);
const { removed } = getActualSigs(signaturesRef.current, newSignatures, false);
handleUpdateSignatures(newSignatures, !lazyDeleteValue);
if (!signaturesRef.current || !signaturesRef.current.length || !removed.length) {
handleUpdateSignatures(newSignatures, false);
} else {
setParsedSignatures(newSignatures);
setAskUser(true);
if (lazyDeleteValue && !keepLazyDeleteValue) {
onLazyDeleteChange?.(false);
}
}, [clipboardContent, selectable]);
};
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);
useEffect(() => {
if (!systemId) {
setSignatures([]);
setAskUser(false);
return;
}
@@ -266,19 +290,6 @@ 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]">
@@ -330,7 +341,7 @@ export const SystemSignaturesContent = ({
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200');
}
const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined);
const dateClass = getRowColorByTimeLeft(row.inserted_at ? new Date(row.inserted_at) : undefined);
if (!dateClass) {
return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200');
}
@@ -357,11 +368,11 @@ export const SystemSignaturesContent = ({
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={compact}
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
sortable
></Column>
<Column
field="info"
// header="Info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
body={renderInfoColumn}
style={{ maxWidth: nameColumnWidth }}
@@ -378,26 +389,26 @@ export const SystemSignaturesContent = ({
></Column>
)}
{showInsertedColumn && (
<Column
field="inserted_at"
header="Added"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderAddedTimeLeft}
sortable
></Column>
{showUpdatedColumn && (
<Column
field="inserted_at"
header="Inserted"
field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderInsertedTimeLeft}
body={renderUpdatedTimeLeft}
sortable
></Column>
)}
<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"
@@ -424,27 +435,6 @@ export const SystemSignaturesContent = ({
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,6 +6,7 @@ export const getActualSigs = (
oldSignatures: SystemSignature[],
newSignatures: SystemSignature[],
updateOnly: boolean,
skipUpdateUntouched?: boolean,
): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => {
const updated: SystemSignature[] = [];
const removed: SystemSignature[] = [];
@@ -19,7 +20,7 @@ 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 {
} else if (!skipUpdateUntouched) {
updated.push({ ...oldSig });
}
} else {

View File

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

View File

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

View File

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

View File

@@ -2,21 +2,31 @@ 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-[6px]">
<div className="flex justify-start items-center gap-[4px]">
{row.type && (
<WHClassView
className="text-[11px]"
classNameWh={classes.whFontSize}
highlightName
classNameWh="!text-[11px] !font-bold"
hideWhClass={!!row.linked_system}
whClassName={row.type}
noOffset
@@ -24,9 +34,10 @@ 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')}

View File

@@ -82,7 +82,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
}, [cnInfo]);
const [passages, setPassages] = useState<Passage[]>([]);
const [info, setInfo] = useState<ConnectionInfoOutput>(null);
const [info, setInfo] = useState<ConnectionInfoOutput | null>(null);
const loadInfo = useCallback(
async (connection: SolarSystemConnection) => {
@@ -141,7 +141,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
>
<div className={clsx(classes.SidebarContent, '')}>
{/* Connection Info */}
<div className="px-2 pb-3 flex flex-col gap-2">
<div className="px-2 flex flex-col gap-2">
{/* Connection Info Row */}
<InfoDrawer title="Connection" rightSide>
<div className="flex justify-end gap-2 items-center">
@@ -159,18 +159,25 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
</div>
</InfoDrawer>
{/* Connection Info Row */}
{isWormhole && (
<>
<InfoDrawer title="Approximate mass of passages" rightSide>
{kgToTons(approximateMass)}
</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>
<InfoDrawer title="Mark EOL Time" rightSide>
{info?.marl_eol_time ? <TimeAgo timestamp={info.marl_eol_time} /> : ' unknown '}
</InfoDrawer>
</>
)}
{/*Right column*/}
<div>
{isWormhole && (
<InfoDrawer title="Approximate mass of passages" rightSide>
{kgToTons(approximateMass)}
</InfoDrawer>
)}
</div>
</div>
<div className="flex gap-2"></div>
</div>

View File

@@ -5,6 +5,8 @@ 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;
@@ -14,6 +16,8 @@ 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(() => {
@@ -24,34 +28,40 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
}, [outCommand]);
const items = useMemo(() => {
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 (
[
{
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 (
<div className="ml-1">

View File

@@ -53,6 +53,7 @@ const SYSTEMS_CHECKBOXES_PROPS: CheckboxesList = [
const SIGNATURES_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: UserSettingsRemoteProps.link_signature_on_splash, label: 'Link signature on splash' },
{ prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures, label: 'Show unsplashed signatures' },
];
const CONNECTIONS_CHECKBOXES_PROPS: CheckboxesList = [
@@ -128,7 +129,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
return (
<Dialog
header="Map settings"
header="Map user settings"
visible={show}
draggable={false}
style={{ width: '550px' }}

View File

@@ -8,6 +8,8 @@ import clsx from 'clsx';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { CharacterCard, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import useLocalStorageState from 'use-local-storage-state';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
type WindowLocalSettingsType = {
compact: boolean;
@@ -50,14 +52,22 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
defaultValue: STORED_DEFAULT_VALUES,
});
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
const showOffline = useMemo(
() => !restrictOfflineShowing || isAdminOrManager,
[isAdminOrManager, restrictOfflineShowing],
);
const sorted = useMemo(() => {
const out = characters.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id) })).sort(sortCharacters);
if (!settings.hideOffline) {
if (showOffline && !settings.hideOffline) {
return out;
}
return out.filter(x => x.online);
}, [characters, settings.hideOffline, userCharacters]);
}, [showOffline, characters, settings.hideOffline, userCharacters]);
return (
<Sidebar
@@ -70,14 +80,16 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
>
<div className={clsx(classes.SidebarContent, '')}>
<div className={'flex justify-end items-center gap-2 px-3'}>
<WdCheckbox
size="m"
labelSide="left"
label={'Hide offline'}
value={settings.hideOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, hideOffline: !settings.hideOffline }))}
/>
{showOffline && (
<WdCheckbox
size="m"
labelSide="left"
label={'Hide offline'}
value={settings.hideOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, hideOffline: !settings.hideOffline }))}
/>
)}
</div>
<VirtualScroller

View File

@@ -6,6 +6,9 @@ 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;
@@ -14,6 +17,8 @@ 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(() => {
@@ -64,19 +69,21 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
</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>
{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>
)}
</div>
<div className="flex flex-col items-center mb-2 gap-1">
<WdTooltipWrapper content="User settings" position={TooltipPosition.left}>
<WdTooltipWrapper content="Map user settings" 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"

View File

@@ -49,6 +49,13 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
});
}
out = {
...out,
custom_info: JSON.stringify({
k162Type: values.k162Type,
}),
};
if (values.type != null) {
out = { ...out, type: values.type };
}
@@ -117,10 +124,17 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
return;
}
const { linked_system, ...rest } = signatureData;
const { linked_system, custom_info, ...rest } = signatureData;
let k162Type = null;
if (custom_info) {
const customInfo = JSON.parse(custom_info);
k162Type = customInfo.k162Type;
}
signatureForm.reset({
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
k162Type: k162Type,
...rest,
});
}, [signatureForm, signatureData]);

View File

@@ -1,7 +1,13 @@
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]">
@@ -9,6 +15,13 @@ 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

@@ -0,0 +1,136 @@
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

@@ -0,0 +1 @@
export * from './SignatureK162TypeSelect.tsx';

View File

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

View File

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

View File

@@ -3,15 +3,23 @@ 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, hideRegion, className, showCustomName }: SystemViewProps) => {
export const SystemView = ({
systemId,
systemInfo: customSystemInfo,
hideRegion,
className,
showCustomName,
}: SystemViewProps) => {
const memSystems = useMemo(() => [systemId], [systemId]);
const { systems, loading } = useLoadSystemStatic({ systems: memSystems });
@@ -20,9 +28,12 @@ export const SystemView = ({ systemId, hideRegion, className, showCustomName }:
} = useMapRootState();
const systemInfo = useMemo(() => {
if (!systemId) {
return customSystemInfo;
}
return systems.get(parseInt(systemId));
// eslint-disable-next-line
}, [systemId, systems, loading]);
}, [customSystemInfo, systemId, systems, loading]);
const mapSystemInfo = useMemo(() => {
if (!showCustomName) {

View File

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

View File

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

View File

@@ -18,7 +18,9 @@ export interface WHClassViewProps {
whClassName: string;
noOffset?: boolean;
useShortTitle?: boolean;
hideTooltip?: boolean;
hideWhClass?: boolean;
hideWhClassName?: boolean;
highlightName?: boolean;
className?: string;
classNameWh?: string;
@@ -28,7 +30,9 @@ export const WHClassView = ({
whClassName,
noOffset,
useShortTitle,
hideTooltip,
hideWhClass,
hideWhClassName,
highlightName,
className,
classNameWh,
@@ -45,25 +49,27 @@ export const WHClassView = ({
return (
<div className={clsx(classes.WHClassViewRoot, className)}>
<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>
{!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>
</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>
</div>
</Tooltip>
</Tooltip>
)}
<div
className={clsx(
@@ -73,7 +79,7 @@ export const WHClassView = ({
`wh-name${whClassName}${uid}`,
)}
>
<span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>
{!hideWhClassName && <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<string | null>(null);
const [clipboardContent, setClipboardContent] = useState<{ text: 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 };
return { clipboardContent, error, getClipboardContent, setClipboardContent };
};

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: () => void) => {
export const useHotkey = (isMetaKey: boolean, hotkeys: string[], callback: (e: KeyboardEvent) => 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: () =>
return;
}
event.preventDefault();
callback();
callback(event);
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, [isMetaKey, hotkeys, callback]);
};

View File

@@ -1,6 +1,6 @@
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
import { createContext, Dispatch, ForwardedRef, forwardRef, RefObject, SetStateAction, useContext } from 'react';
import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext } from 'react';
import { 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,6 +25,8 @@ const INITIAL_DATA: MapRootData = {
selectedSystems: [],
selectedConnections: [],
userPermissions: {},
options: {},
};
export enum InterfaceStoredSettingsProps {
@@ -32,6 +34,7 @@ export enum InterfaceStoredSettingsProps {
isShowMinimap = 'isShowMinimap',
isShowKSpace = 'isShowKSpace',
isThickConnections = 'isThickConnections',
isShowUnsplashedSignatures = 'isShowUnsplashedSignatures',
}
export type InterfaceStoredSettings = {
@@ -39,6 +42,7 @@ export type InterfaceStoredSettings = {
isShowMinimap: boolean;
isShowKSpace: boolean;
isThickConnections: boolean;
isShowUnsplashedSignatures: boolean;
};
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
@@ -46,6 +50,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMinimap: true,
isShowKSpace: false,
isThickConnections: false,
isShowUnsplashedSignatures: false,
};
export interface MapRootContextProps {

View File

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

View File

@@ -2,11 +2,14 @@ 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';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
export const useCommandsSystems = () => {
const {
update,
data: { systems },
outCommand,
} = useMapRootState();
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
@@ -56,5 +59,29 @@ export const useCommandsSystems = () => {
update({ systems: out }, true);
}, []);
return { addSystems, removeSystems, updateSystems };
const updateSystemSignatures = useCallback(
async (systemId: string) => {
const { update, systems } = ref.current;
const { signatures } = await outCommand({
type: OutCommand.getSignatures,
data: { system_id: `${systemId}` },
});
const out = systems.map(current => {
if (current.id === `${systemId}`) {
return { ...current, system_signatures: signatures };
}
return current;
});
update({ systems: out }, true);
emitMapEvent({ name: Commands.updateSystems, data: out });
},
[outCommand],
);
return { addSystems, removeSystems, updateSystems, updateSystemSignatures };
};

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,10 @@
import { useMemo } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useMapGetOption = (option: string) => {
const {
data: { options },
} = useMapRootState();
return useMemo(() => options[option], [option, options]);
};

View File

@@ -19,6 +19,8 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
user_permissions,
options,
}: CommandInit) => {
const updateData: Partial<MapRootData> = {};
@@ -51,10 +53,18 @@ export const useMapInit = () => {
updateData.connections = connections;
}
if (user_permissions) {
updateData.userPermissions = user_permissions;
}
if (hubs) {
updateData.hubs = hubs;
}
if (options) {
updateData.options = options;
}
if (system_static_infos) {
system_static_infos.forEach(static_info => {
addSystemStatic(static_info);

View File

@@ -13,6 +13,7 @@ import {
CommandRemoveSystems,
CommandRoutes,
Commands,
CommandSignaturesUpdated,
CommandUpdateConnection,
CommandUpdateSystems,
MapHandlers,
@@ -31,7 +32,7 @@ import { emitMapEvent } from '@/hooks/Mapper/events';
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapInit = useMapInit();
const { addSystems, removeSystems, updateSystems } = useCommandsSystems();
const { addSystems, removeSystems, updateSystems, updateSystemSignatures } = useCommandsSystems();
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
const { charactersUpdated, characterAdded, characterRemoved, characterUpdated, presentCharacters } =
useCommandsCharacters();
@@ -88,7 +89,7 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
break;
case Commands.signaturesUpdated: // USED
// do nothing here
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.linkSignatureToSystem: // USED

View File

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

View File

@@ -4,6 +4,7 @@ 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',
@@ -58,9 +59,10 @@ export type CommandInit = {
characters: CharacterTypeRaw[];
present_characters: string[];
user_characters: string[];
user_permissions: any;
user_permissions: UserPermissions;
hubs: string[];
routes: RoutesList;
options: Record<string, string | boolean>;
reset?: boolean;
};
export type CommandAddSystems = SolarSystemRawType[];

View File

@@ -4,6 +4,7 @@ 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>;
@@ -17,4 +18,6 @@ export type MapUnionTypes = {
routes?: RoutesList;
kills: Record<number, number>;
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;
options: Record<string, string | boolean>;
};

View File

@@ -0,0 +1,19 @@
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,9 +21,11 @@ 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,5 +1,7 @@
import { XYPosition } from 'reactflow';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
export enum SolarSystemStaticInfoRawNames {
regionId = 'region_id',
constellationId = 'constellation_id',
@@ -116,4 +118,5 @@ export type SolarSystemRawType = {
name: string | null;
system_static_info: SolarSystemStaticInfoRaw;
system_signatures: SystemSignature[];
};

View File

@@ -12,11 +12,16 @@ defmodule WandererApp.Api.MapCharacterSettings do
code_interface do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:read_by_map,
action: :read_by_map
)
define(:by_map_filtered,
action: :by_map_filtered
)
define(:tracked_by_map_filtered,
action: :tracked_by_map_filtered
)
@@ -38,6 +43,13 @@ defmodule WandererApp.Api.MapCharacterSettings do
defaults [:create, :read, :update, :destroy]
read :by_map_filtered do
argument(:map_id, :string, allow_nil?: false)
argument(:character_ids, {:array, :uuid}, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and character_id in ^arg(:character_ids)))
end
read :tracked_by_map_filtered do
argument(:map_id, :string, allow_nil?: false)
argument(:character_ids, {:array, :uuid}, allow_nil?: false)

View File

@@ -24,6 +24,7 @@ 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
@@ -55,7 +56,8 @@ defmodule WandererApp.Api.MapSystemSignature do
:description,
:kind,
:group,
:type
:type,
:custom_info
]
argument :system_id, :uuid, allow_nil?: false
@@ -73,6 +75,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:kind,
:group,
:type,
:custom_info,
:updated
]
@@ -97,6 +100,12 @@ defmodule WandererApp.Api.MapSystemSignature do
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
@@ -129,6 +138,10 @@ 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)

View File

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

View File

@@ -446,8 +446,13 @@ defmodule WandererApp.Character.Tracker do
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> maybe_update_alliance()
_error ->
Logger.warning("Failed to get corporation info for #{corporation_id}")
error ->
Logger.warning(
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
character_id: character_id,
corporation_id: corporation_id
)
state
end
end

View File

@@ -83,22 +83,28 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def stop_tracking(%__MODULE__{} = state, character_id) do
{:ok, %{start_time: start_time}} = WandererApp.Character.get_character_state(character_id)
{:ok, character_state} = WandererApp.Character.get_character_state(character_id, false)
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)
case character_state do
nil ->
state
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)
%{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)
tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == 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.insert("tracked_characters", tracked_characters)
tracked_characters = state.characters |> Enum.reject(fn c_id -> c_id == character_id end)
%{state | characters: tracked_characters}
WandererApp.Cache.insert("tracked_characters", tracked_characters)
%{state | characters: tracked_characters}
end
end
def update_track_settings(
@@ -429,8 +435,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def handle_info({:stop_track, character_id}, state) do
@logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
stop_tracking(state, character_id)
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
end
def handle_info(_event, state),

View File

@@ -49,7 +49,7 @@ defmodule WandererApp.EveDataService do
end)
end)
Task.await_many(tasks, :timer.minutes(1))
Task.await_many(tasks, :timer.minutes(30))
end
def download_file(file_name) do

View File

@@ -14,6 +14,7 @@ defmodule WandererApp.Map do
hubs: [],
connections: Map.new(),
acls: [],
options: Map.new(),
characters_limit: nil,
hubs_limit: nil
@@ -69,6 +70,9 @@ defmodule WandererApp.Map do
def get_characters_limit(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 100)}
def get_options(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:options, Map.new())}
@doc """
Returns a full list of characters in the map
"""
@@ -187,7 +191,7 @@ defmodule WandererApp.Map do
case characters |> Enum.member?(character_id) do
true ->
map_id
|> update_map(%{characters: Enum.reject(characters, fn id -> id == character_id end)})
|> update_map(%{characters: characters |> Enum.reject(fn id -> id == character_id end)})
:ok
@@ -251,6 +255,13 @@ defmodule WandererApp.Map do
map
end
def update_options!(%{map_id: map_id} = map, options) do
map_id
|> update_map(%{options: options})
map
end
def add_systems!(map, []), do: map
def add_systems!(%{map_id: map_id} = map, [system | rest]) do

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
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

@@ -0,0 +1,459 @@
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",
[]
)
acls =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:acls, [])
invalidate_character_ids
|> Task.async_stream(
fn character_id ->
character_id
|> WandererApp.Character.get_character()
|> case do
{:ok, character} ->
[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_by_map_filtered(character_ids)
|> case do
{:ok, settings} ->
settings
|> Enum.each(fn s ->
WandererApp.MapCharacterSettingsRepo.destroy!(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,7 +1,6 @@
defmodule WandererApp.Map.Server.ConnectionsImpl do
@moduledoc """
Holds state for a map and exposes an interface to managing the map instance
"""
@moduledoc false
require Logger
alias WandererApp.Map.Server.Impl
@@ -357,8 +356,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
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
@@ -381,6 +378,9 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
@known_space |> Enum.member?(system_static_info.system_class)
:all ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class))
_ ->
false
end

View File

@@ -0,0 +1,506 @@
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("Failed 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

View File

@@ -15,6 +15,8 @@ defmodule WandererApp.Permissions do
@add_acl 1024
@delete_acl 2048
@delete_map 4096
@manage_map 8192
@admin_map 16384
@viewer_role [@view_system, @view_character, @view_connection]
@member_role @viewer_role ++
@@ -24,11 +26,10 @@ defmodule WandererApp.Permissions do
@update_system,
@track_character,
@delete_connection,
@delete_system,
@lock_system
@delete_system
]
@manager_role @member_role
@admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map]
@manager_role @member_role ++ [@lock_system, @manage_map]
@admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map, @admin_map]
@viewer_role_mask @viewer_role |> Enum.reduce(0, fn x, acc -> x ||| acc end)
@member_role_mask @member_role |> Enum.reduce(0, fn x, acc -> x ||| acc end)
@@ -72,6 +73,8 @@ defmodule WandererApp.Permissions do
def get_permissions(user_permissions) do
%{
admin_map: check_permission(user_permissions, @admin_map),
manage_map: check_permission(user_permissions, @manage_map),
view_system: check_permission(user_permissions, @view_system),
view_character: check_permission(user_permissions, @view_character),
view_connection: check_permission(user_permissions, @view_connection),

View File

@@ -11,6 +11,13 @@ defmodule WandererApp.MapCharacterSettingsRepo do
character_ids: character_ids
})
def get_by_map_filtered(map_id, character_ids),
do:
WandererApp.Api.MapCharacterSettings.by_map_filtered(%{
map_id: map_id,
character_ids: character_ids
})
def get_all_by_map(map_id),
do: WandererApp.Api.MapCharacterSettings.read_by_map(%{map_id: map_id})
@@ -22,4 +29,6 @@ defmodule WandererApp.MapCharacterSettingsRepo do
def track!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track!()
def untrack!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack!()
def destroy!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.destroy!()
end

View File

@@ -1,7 +1,11 @@
defmodule WandererApp.MapRepo do
use WandererApp, :repository
@default_map_options %{"layout" => "left_to_right", "store_custom_labels" => "false"}
@default_map_options %{
"layout" => "left_to_right",
"store_custom_labels" => "false",
"restrict_offline_showing" => "false"
}
def get(map_id, relationships \\ []) do
map_id

View File

@@ -143,15 +143,10 @@ defmodule WandererAppWeb.CoreComponents do
def server_status(assigns) do
~H"""
<div
class="flex flex-col p-4 items-center absolute bottom-16 left-1 gap-2 tooltip tooltip-right"
class="flex flex-col p-4 items-center absolute bottom-16 left-2 gap-2 tooltip tooltip-right"
data-tip="server: Tranquility"
>
<div
:if={@online}
class="absolute block w-4 h-4 rounded-full shadow-inner bg-green-500 animate-ping"
>
</div>
<div class={"block w-4 h-4 rounded-full shadow-inner #{if @online, do: " bg-green-500", else: "bg-red-500"}"}>
<div class={"block w-2 h-2 rounded-full shadow-inner #{if @online, do: " bg-green-500", else: "bg-red-500"}"}>
</div>
</div>
"""
@@ -336,6 +331,7 @@ defmodule WandererAppWeb.CoreComponents do
"""
attr(:id, :any, default: nil)
attr(:class, :string, default: nil)
attr(:wrapper_class, :string, default: nil)
attr(:name, :any)
attr(:label, :string, default: nil)
attr(:prefix, :string, default: nil)
@@ -381,21 +377,62 @@ defmodule WandererAppWeb.CoreComponents do
end)
~H"""
<div phx-feedback-for={@name} class="form-control mt-8">
<label class="label cursor-pointer gap-2">
<span class="label-text"><%= @label %></span>
<input type="hidden" name={@name} value="false" />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="checkbox"
{@rest}
/>
<div phx-feedback-for={@name} class="form-control mt-2">
<label class="inputContainer" for={@name}>
<span><%= @label %></span>
<div></div>
<div class="smallInputSwitch">
<div class="flex items-center">
<div
class={[
"checkboxRoot sizeM p-checkbox p-component",
classes("p-highlight": @checked)
]}
data-p-highlight={@checked}
data-p-disabled="false"
data-pc-name="checkbox"
data-pc-section="root"
>
<input
id={@id}
name={@name}
type="checkbox"
class="p-checkbox-input"
aria-invalid="false"
data-pc-section="input"
value="true"
checked={@checked}
{@rest}
/>
<div
class="p-checkbox-box"
data-p-highlight={@checked}
data-p-disabled="false"
data-pc-section="box"
>
<svg
:if={@checked}
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="p-icon p-checkbox-icon"
aria-hidden="true"
data-pc-section="icon"
>
<path
d="M4.86199 11.5948C4.78717 11.5923 4.71366 11.5745 4.64596 11.5426C4.57826 11.5107 4.51779 11.4652 4.46827 11.4091L0.753985 7.69483C0.683167 7.64891 0.623706 7.58751 0.580092 7.51525C0.536478 7.44299 0.509851 7.36177 0.502221 7.27771C0.49459 7.19366 0.506156 7.10897 0.536046 7.03004C0.565935 6.95111 0.613367 6.88 0.674759 6.82208C0.736151 6.76416 0.8099 6.72095 0.890436 6.69571C0.970973 6.67046 1.05619 6.66385 1.13966 6.67635C1.22313 6.68886 1.30266 6.72017 1.37226 6.76792C1.44186 6.81567 1.4997 6.8786 1.54141 6.95197L4.86199 10.2503L12.6397 2.49483C12.7444 2.42694 12.8689 2.39617 12.9932 2.40745C13.1174 2.41873 13.2343 2.47141 13.3251 2.55705C13.4159 2.64268 13.4753 2.75632 13.4938 2.87973C13.5123 3.00315 13.4888 3.1292 13.4271 3.23768L5.2557 11.4091C5.20618 11.4652 5.14571 11.5107 5.07801 11.5426C5.01031 11.5745 4.9368 11.5923 4.86199 11.5948Z"
fill="currentColor"
>
</path>
</svg>
</div>
</div>
<label for={@name} class="select-none ml-1.5"></label>
</div>
</div>
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@@ -403,24 +440,28 @@ defmodule WandererAppWeb.CoreComponents do
def input(%{type: "range"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<label class="form-control w-full">
<div class="form-control w-full">
<.label for={@id}>
<span class="label-text"><%= @label %></span>
<span class="label-value"><%= @value %></span>
<span><%= @label %></span>
<div></div>
<%= @value %>
</.label>
<input
type="range"
id={@id}
name={@name}
value={@value}
class={[
"p-component w-full",
@class,
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
/>
</label>
<div>
<input
type="range"
id={@id}
name={@name}
value={@value}
class={[
"p-component w-full",
@class,
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
/>
</div>
</div>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
@@ -428,13 +469,20 @@ defmodule WandererAppWeb.CoreComponents do
def input(%{type: "select"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<div
phx-feedback-for={@name}
class={[
"inputContainer",
@wrapper_class
]}
>
<.label :if={@label} for={@id}><%= @label %></.label>
<div :if={@label}></div>
<select
id={@id}
name={@name}
class={[
"w-full",
"p-component",
@class
]}
multiple={@multiple}
@@ -503,9 +551,9 @@ defmodule WandererAppWeb.CoreComponents do
def label(assigns) do
~H"""
<div for={@for} class="label">
<label for={@for} class="inputContainer">
<%= render_slot(@inner_block) %>
</div>
</label>
"""
end

View File

@@ -32,14 +32,22 @@
/>
<script
crossorigin="anonymous"
src="https://unpkg.com/react@18/umd/react.production.min.js"
integrity={integrity_hash("https://unpkg.com/react@18/umd/react.production.min.js")}
src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"
integrity={
integrity_hash(
"https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js https://unpkg.com/react@18/umd/react.production.min.js"
)
}
>
</script>
<script
crossorigin="anonymous"
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
integrity={integrity_hash("https://unpkg.com/react-dom@18/umd/react-dom.production.min.js")}
src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"
integrity={
integrity_hash(
"https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"
)
}
>
</script>

View File

@@ -48,7 +48,7 @@ defmodule WandererAppWeb.MapPicker do
:if={maps}
type="select"
field={f[:map_slug]}
class="select h-8 min-h-[0px] !pt-1 !pb-1 text-sm bg-neutral-900"
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
placeholder="Select a map..."
options={Enum.map(@maps.result, fn map -> {map.label, map.value} end)}
/>

View File

@@ -8,7 +8,8 @@ defmodule WandererAppWeb.UserActivity do
end
@impl true
def update(assigns,
def update(
assigns,
socket
) do
{:ok,
@@ -116,7 +117,6 @@ defmodule WandererAppWeb.UserActivity do
<h6 class="text-base leading-[150%] font-semibold dark:text-white">
<%= _get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"])) %>
</h6>
</div>
</div>
"""
@@ -125,7 +125,6 @@ defmodule WandererAppWeb.UserActivity do
@impl true
def handle_event("undo", %{"event-data" => event_data} = _params, socket) do
# notify_to(socket.assigns.notify_to, socket.assigns.event_name, map_slug)
IO.inspect(event_data)
{:noreply, socket}
end

View File

@@ -222,7 +222,7 @@ defmodule WandererAppWeb.AccessListsLive do
def handle_event(
"add_members",
%{"member_id" => member_id} = _params,
%{"member_id" => [member_id]} = _params,
%{assigns: assigns} = socket
)
when is_binary(member_id) and member_id != "" do

View File

@@ -136,8 +136,10 @@
<.input
type="select"
field={f[:owner_id]}
class="p-dropdown p-component p-inputwrapper mt-8"
placeholder="Select a map owner"
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
wrapper_class="mt-2"
label="Owner"
placeholder="Select an owner"
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
/>
<div class="modal-action">
@@ -228,6 +230,7 @@
available_option_class="w-full"
debounce={250}
update_min_len={3}
mode={:tags}
options={@member_search_options}
placeholder="Search a character/corporation/alliance"
>

View File

@@ -39,7 +39,8 @@ defmodule WandererAppWeb.AclMember do
<.input
type="select"
field={f[:role]}
class="select h-8 min-h-[0px] !pt-1 !pb-1 text-sm bg-neutral-900 w-[70px]"
class="select h-8 min-h-[0px] !pt-1 !pb-1 text-sm bg-neutral-900"
wrapper_class="w-[60px] mr-16"
placeholder="Select a role..."
options={Enum.map(@roles, fn role -> {role.label, role.value} end)}
/>

View File

@@ -74,23 +74,20 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
}
} = socket
) do
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
_ -> {:ok, []}
end
{:noreply,
socket
|> assign(
show_tracking?: true,
character_settings: character_settings
)
|> assign(show_tracking?: true)
|> assign_async(:characters, fn ->
{:ok, map} =
map_id
|> WandererApp.MapRepo.get([:acls])
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
_ -> {:ok, []}
end
map
|> WandererApp.Maps.load_characters(
character_settings,
@@ -122,12 +119,17 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
%{
assigns: %{
map_id: map_id,
character_settings: character_settings,
current_user: current_user,
only_tracked_characters: only_tracked_characters
}
} = socket
) do
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
_ -> {:ok, []}
end
socket =
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
nil ->
@@ -202,7 +204,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
socket
|> assign(user_characters: user_character_eve_ids)
|> assign(has_tracked_characters?: has_tracked_characters?(user_character_eve_ids))
|> assign(character_settings: character_settings)
|> assign_async(:characters, fn ->
{:ok, %{characters: characters}}
end)

View File

@@ -120,7 +120,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
do: socket
def handle_server_event(event, socket) do
Logger.warning(fn -> "unhandled map core event: #{inspect(event)}" end)
Logger.warning(fn -> "unhandled map core event: #{inspect(event)} #{inspect(socket)} " end)
socket
end
@@ -434,7 +434,19 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
|> handle_map_start_events(map_id, events)
map_characters = map_id |> WandererApp.Map.list_characters()
{:ok, options} =
map_id
|> WandererApp.Map.get_options()
user_permissions =
initial_data
|> Map.get(:user_permissions)
map_characters =
map_id
|> WandererApp.Map.list_characters()
|> filter_map_characters(user_character_eve_ids, user_permissions, options)
|> Enum.map(&MapCharactersEventHandler.map_ui_character/1)
socket
|> assign(
@@ -447,10 +459,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|> MapEventHandler.push_map_event(
"init",
initial_data
|> Map.put(
:characters,
map_characters |> Enum.map(&MapCharactersEventHandler.map_ui_character/1)
)
|> Map.put(:characters, map_characters)
)
|> push_event("js-exec", %{
to: "#map-loader",
@@ -500,13 +509,15 @@ defmodule WandererAppWeb.MapCoreEventHandler do
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
{:ok, connections} = map_id |> WandererApp.Map.list_connections()
{:ok, systems} = map_id |> WandererApp.Map.list_systems()
{:ok, options} = map_id |> WandererApp.Map.get_options()
%{
systems:
systems
|> Enum.map(fn system -> MapEventHandler.map_ui_system(system, include_static_data?) end),
hubs: hubs,
connections: connections |> Enum.map(&MapEventHandler.map_ui_connection/1)
connections: connections |> Enum.map(&MapEventHandler.map_ui_connection/1),
options: options
}
end
@@ -525,6 +536,26 @@ defmodule WandererAppWeb.MapCoreEventHandler do
end
end
defp filter_map_characters(
characters,
user_character_eve_ids,
%{
manage_map: manage_map_permission
} = _user_permissions,
options
) do
restrict_offline_showing =
options |> Map.get("restrict_offline_showing", "false") |> String.to_existing_atom()
show_offline? = not restrict_offline_showing or manage_map_permission
characters
|> Enum.filter(fn character ->
show_offline? || character.online ||
user_character_eve_ids |> Enum.member?(character.eve_id)
end)
end
defp map_system(
%{
solar_system_name: solar_system_name,

View File

@@ -312,7 +312,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
defp get_system_signatures(system_id),
def get_system_signatures(system_id),
do:
system_id
|> WandererApp.Api.MapSystemSignature.by_system_id!()
@@ -328,7 +328,8 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
:description,
:kind,
:group,
:type
:type,
:custom_info
])
|> Map.put(:linked_system, MapEventHandler.get_system_static_info(linked_system_id))
|> Map.put(:inserted_at, inserted_at |> Calendar.strftime("%Y/%m/%d %H:%M:%S"))
@@ -352,6 +353,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
kind: kind,
group: group,
type: Map.get(signature, "type"),
custom_info: Map.get(signature, "custom_info"),
character_eve_id: character_eve_id
}
end)

View File

@@ -66,7 +66,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
def handle_ui_event(
"add_system",
%{"system_id" => solar_system_id} = _event,
%{"system_id" => [solar_system_id]} = _event,
%{
assigns:
%{
@@ -217,7 +217,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
current_user: current_user,
tracked_character_ids: tracked_character_ids,
has_tracked_characters?: true,
user_permissions: %{update_system: true}
user_permissions: %{update_system: true} = user_permissions
}
} =
socket
@@ -244,23 +244,25 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
_ -> :none
end
apply(WandererApp.Map.Server, method_atom, [
map_id,
%{
solar_system_id: "#{solar_system_id}" |> String.to_integer()
}
|> Map.put_new(key_atom, value)
])
if can_update_system?(key_atom, user_permissions) do
apply(WandererApp.Map.Server, method_atom, [
map_id,
%{
solar_system_id: "#{solar_system_id}" |> String.to_integer()
}
|> Map.put_new(key_atom, value)
])
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: "#{solar_system_id}" |> String.to_integer(),
key: key_atom,
value: value
})
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: "#{solar_system_id}" |> String.to_integer(),
key: key_atom,
value: value
})
end
{:noreply, socket}
end
@@ -305,6 +307,9 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
defp can_update_system?(:locked, %{lock_system: false} = _user_permissions), do: false
defp can_update_system?(_key, _user_permissions), do: true
defp update_system_positions(_map_id, []), do: :ok
defp update_system_positions(map_id, [position | rest]) do

View File

@@ -234,6 +234,7 @@ defmodule WandererAppWeb.MapEventHandler do
def map_ui_system(
%{
id: system_id,
solar_system_id: solar_system_id,
name: name,
description: description,
@@ -249,12 +250,20 @@ defmodule WandererAppWeb.MapEventHandler do
) do
system_static_info = get_system_static_info(solar_system_id)
system_signatures =
system_id
|> WandererAppWeb.MapSignaturesEventHandler.get_system_signatures()
|> Enum.filter(fn signature ->
is_nil(signature.linked_system) && signature.group == "Wormhole"
end)
%{
id: "#{solar_system_id}",
position: %{x: position_x, y: position_y},
description: description,
name: name,
system_static_info: system_static_info,
system_signatures: system_signatures,
labels: labels,
locked: locked,
status: status,

View File

@@ -46,6 +46,7 @@
update_min_len={2}
available_option_class="w-full text-sm"
debounce={200}
mode={:tags}
>
<:option :let={option}>
<div class="gap-1 w-full flex flex-align-center p-autocomplete-item text-sm">
@@ -96,7 +97,7 @@
on_cancel={JS.push("hide_tracking")}
>
<.async_result :let={characters} assign={@characters}>
<:loading>Loading...</:loading>
<:loading><span class="loading loading-dots loading-xs" />.</:loading>
<:failed :let={reason}><%= reason %></:failed>
<.table
@@ -104,7 +105,6 @@
id="characters-tracking-table"
class="h-[400px] !overflow-y-auto"
rows={characters}
>
<:col :let={character} label="Tracked">
<label class="flex items-center gap-3">

View File

@@ -197,11 +197,13 @@ defmodule WandererAppWeb.MapsLive do
{:noreply, socket}
end
def handle_event("validate", %{"form" => params} = _params, socket) do
def handle_event("validate", %{"form" => form} = _params, socket) do
form =
AshPhoenix.Form.validate(
socket.assigns.form,
params |> Map.put("acls", params["acls"] || [])
form
|> Map.put("acls", form["acls"] || [])
|> Map.put("only_tracked_characters", form["only_tracked_characters"] || false)
)
{:noreply, socket |> assign(form: form)}
@@ -280,14 +282,17 @@ defmodule WandererAppWeb.MapsLive do
do:
{:noreply,
socket
|> assign(:amounts, [
%{label: "150M", value: 150_000_000},
%{label: "300M", value: 300_000_000},
%{label: "600M", value: 600_000_000},
%{label: "1.2B", value: 1_200_000_000},
%{label: "2.4B", value: 2_400_000_000},
%{label: "5B", value: 5_000_000_000}
])
|> assign(
:amounts,
[
{"150M", 150_000_000},
{"300M", 300_000_000},
{"600M", 600_000_000},
{"1.2B", 1_200_000_000},
{"2.4B", 2_400_000_000},
{"5B", 5_000_000_000}
]
)
|> assign(is_topping_up?: true)}
@impl true
@@ -654,7 +659,7 @@ defmodule WandererAppWeb.MapsLive do
) do
options =
options_form
|> Map.take(["layout", "store_custom_labels"])
|> Map.take(["layout", "store_custom_labels", "restrict_offline_showing"])
{:ok, updated_map} = WandererApp.MapRepo.update_options(map, options)

View File

@@ -139,14 +139,18 @@
<.input
type="select"
field={f[:owner_id]}
class="p-dropdown p-component p-inputwrapper mt-8"
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
wrapper_class="mt-2"
label="Map owner"
placeholder="Select a map owner"
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
/>
<.input
type="select"
field={f[:scope]}
class="p-dropdown p-component p-inputwrapper mt-8"
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
wrapper_class="mt-2"
label="Map scope"
placeholder="Select a map scope"
options={Enum.map(@scopes, fn scope -> {scope, scope} end)}
/>
@@ -182,203 +186,411 @@
<.modal
:if={@live_action in [:settings]}
title="Map Settings"
class="!w-[800px]"
class="!min-w-[700px]"
id="map-settings-modal"
show
on_cancel={JS.patch(~p"/maps")}
>
<div role="tablist" class="tabs tabs-bordered">
<a
role="tab"
phx-click="change_settings_tab"
phx-value-tab="general"
class={[
"tab",
classes("tab-active": @active_settings_tab == "general")
]}
>
<.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" />&nbsp;General
</a>
<a
role="tab"
phx-click="change_settings_tab"
phx-value-tab="import"
class={[
"tab",
classes("tab-active": @active_settings_tab == "import")
]}
>
<.icon name="hero-document-arrow-down-solid" class="w-4 h-4" />&nbsp;Import/Export
</a>
<a
:if={@map_subscriptions_enabled?}
role="tab"
phx-click="change_settings_tab"
phx-value-tab="balance"
class={[
"tab",
classes("tab-active": @active_settings_tab == "balance")
]}
>
<.icon name="hero-banknotes-solid" class="w-4 h-4" />&nbsp;Balance
</a>
<a
:if={@map_subscriptions_enabled?}
role="tab"
phx-click="change_settings_tab"
phx-value-tab="subscription"
class={[
"tab",
classes("tab-active": @active_settings_tab == "subscription")
]}
>
<.icon name="hero-check-badge-solid" class="w-4 h-4" />&nbsp;Subscription
</a>
</div>
<.header :if={@active_settings_tab == "general"} class="bordered border-1 border-zinc-800">
<:actions>
<.form
:let={f}
:if={assigns |> Map.get(:options_form, false)}
for={@options_form}
phx-change="update_options"
>
<div>
<div class="stat-title">Map systems layout</div>
<div class="stat-value text-white">
<.input
type="select"
field={f[:layout]}
class="p-dropdown p-component p-inputwrapper"
placeholder="Map default layout"
options={@layout_options}
/>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<div class="_verticalTabsContainer_1o01l_2">
<div class="p-tabview p-component" data-pc-name="tabview" data-pc-section="root">
<div class="p-tabview-nav-container" data-pc-section="navcontainer">
<div class="p-tabview-nav-content" data-pc-section="navcontent">
<ul class="p-tabview-nav" role="tablist" data-pc-section="nav">
<li
class={[
"p-unselectable-text",
classes("p-tabview-selected p-highlight": @active_settings_tab == "general")
]}
role="presentation"
data-pc-name=""
data-pc-section="header"
>
<a
role="tab"
class="p-tabview-nav-link flex p-[10px]"
tabindex="0"
aria-controls="pr_id_330_content"
aria-selected="true"
aria-disabled="false"
data-pc-section="headeraction"
phx-click="change_settings_tab"
phx-value-tab="general"
>
<span class="p-tabview-title" data-pc-section="headertitle">
<.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" />&nbsp;General
</span>
</a>
</li>
<li
:if={@map_subscriptions_enabled?}
class={[
"p-unselectable-text",
classes("p-tabview-selected p-highlight": @active_settings_tab == "balance")
]}
role="presentation"
data-pc-name=""
data-pc-section="header"
>
<a
role="tab"
class="p-tabview-nav-link flex p-[10px]"
tabindex="-1"
aria-controls="pr_id_332_content"
aria-selected="false"
aria-disabled="false"
data-pc-section="headeraction"
phx-click="change_settings_tab"
phx-value-tab="balance"
>
<span class="p-tabview-title" data-pc-section="headertitle">
<.icon name="hero-banknotes-solid" class="w-4 h-4" />&nbsp;Balance
</span>
</a>
</li>
<li
:if={@map_subscriptions_enabled?}
class={[
"p-unselectable-text",
classes(
"p-tabview-selected p-highlight": @active_settings_tab == "subscription"
)
]}
role="presentation"
data-pc-name=""
data-pc-section="header"
>
<a
role="tab"
class="p-tabview-nav-link flex p-[10px]"
tabindex="-1"
aria-controls="pr_id_334_content"
aria-selected="false"
aria-disabled="false"
data-pc-section="headeraction"
phx-click="change_settings_tab"
phx-value-tab="subscription"
>
<span class="p-tabview-title" data-pc-section="headertitle">
<.icon name="hero-check-badge-solid" class="w-4 h-4" />&nbsp;Subscription
</span>
</a>
</li>
<li
class={[
"p-unselectable-text",
classes("p-tabview-selected p-highlight": @active_settings_tab == "import")
]}
role="presentation"
data-pc-name=""
data-pc-section="header"
>
<a
role="tab"
class="p-tabview-nav-link flex p-[10px]"
tabindex="-1"
aria-controls="pr_id_331_content"
aria-selected="false"
aria-disabled="false"
data-pc-section="headeraction"
phx-click="change_settings_tab"
phx-value-tab="import"
>
<span class="p-tabview-title" data-pc-section="headertitle">
<.icon name="hero-document-arrow-down-solid" class="w-4 h-4" />&nbsp;Import/Export
</span>
</a>
</li>
<li
aria-hidden="true"
role="presentation"
class="p-tabview-ink-bar"
data-pc-section="inkbar"
style="width: 146px; left: 0px;"
>
</li>
</ul>
</div>
</div>
<div class="p-tabview-panels" data-pc-section="panelcontainer">
<div
id="pr_id_330_content"
class="p-tabview-panel"
role="tabpanel"
aria-labelledby="pr_id_33_header_0"
data-pc-name=""
data-pc-section="content"
>
<div :if={@active_settings_tab == "general"}>
<.form
:let={f}
:if={assigns |> Map.get(:options_form, false)}
for={@options_form}
phx-change="update_options"
>
<.input
type="select"
field={f[:layout]}
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
label="Map systems layout"
placeholder="Map default layout"
options={@layout_options}
/>
<.input
type="checkbox"
field={f[:store_custom_labels]}
label="Store system custom labels"
/>
<.input
type="checkbox"
field={f[:restrict_offline_showing]}
label="Show offline characters to admins & managers only"
/>
</.form>
</div>
<div :if={@active_settings_tab == "import"}>
<.form
:if={assigns |> Map.get(:import_form, false)}
for={@import_form}
phx-change="import"
>
<div phx-drop-target="{@uploads.settings.ref}">
<.live_file_input upload={@uploads.settings} />
</div>
</.form>
<progress :if={@importing} class="progress w-56"></progress>
<.button
id="export-settings-btn"
class="mt-8"
type="button"
disabled={@importing}
phx-hook="DownloadJson"
data-name={@map_slug}
data-content={Jason.encode!(assigns[:export_settings] || %{})}
>
<.icon name="hero-document-arrow-down-solid" class="w-4 h-4" /> Export Settings
</.button>
</div>
<div :if={@active_settings_tab == "balance"}>
<div class="stats w-full bg-primary text-primary-content">
<div class="stat">
<div class="stat-figure text-primary">
<.button
:if={not @is_topping_up?}
class="mt-2"
type="button"
phx-click="show_topup"
>
<.icon name="hero-banknotes-solid" class="w-4 h-4" /> Top Up
</.button>
</div>
<div class="stat-title">Map balance</div>
<div class="stat-value text-white">
ISK <%= @map_balance
|> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
</div>
<div class="stat-actions text-end"></div>
</div>
</div>
<.form
:let={f}
:if={@is_topping_up?}
for={@topup_form}
class="mt-2"
phx-change="validate_topup"
phx-submit="topup"
>
<.input
type="select"
field={f[:amount]}
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
label="Topup amount"
placeholder="Select topup amount"
options={@amounts}
/>
<div class="modal-action">
<.button class="mt-2" type="button" phx-click="hide_topup">
Cancel
</.button>
<.button class="mt-2" type="submit">
Top Up
</.button>
</div>
</.form>
</div>
<.table
:if={@active_settings_tab == "subscription"}
class="!max-h-[300px] !overflow-y-auto"
empty_label="No active subscriptions, using alpha plan by default."
id="active-subscriptions-tbl"
rows={@map_subscriptions}
>
<:col :let={subscription} label="Subscription Plan">
<%= subscription.plan %>
</:col>
<:col :let={subscription} label="Status">
<%= subscription.status %>
</:col>
<:col :let={subscription} label="Characters Limit">
<%= subscription.characters_limit %>
</:col>
<:col :let={subscription} label="Hubs Limit">
<%= subscription.hubs_limit %>
</:col>
<:col :let={subscription} label="Active Till">
<.local_time
:if={subscription.active_till}
id={"subscription-active-till-#{subscription.id}"}
at={subscription.active_till}
>
<%= subscription.active_till %>
</.local_time>
</:col>
<:col :let={subscription} label="Auto Renew">
<%= if subscription.auto_renew?, do: "Yes", else: "No" %>
</:col>
<:action :let={subscription}>
<div class="tooltip tooltip-left" data-tip="Edit subscription">
<button
:if={subscription.status == :active && subscription.plan != :alpha}
phx-click="edit-subscription"
phx-value-id={subscription.id}
>
<.icon name="hero-pencil-square-solid" class="w-4 h-4 hover:text-white" />
</button>
</div>
</:action>
<:action :let={subscription}>
<div class="tooltip tooltip-left" data-tip="Cancel subscription">
<button
:if={subscription.status == :active && subscription.plan != :alpha}
phx-click="cancel-subscription"
phx-value-id={subscription.id}
data={[confirm: "Please confirm to cancel subscription!"]}
>
<.icon name="hero-trash-solid" class="w-4 h-4 hover:text-white" />
</button>
</div>
</:action>
</.table>
<.header
:if={@active_settings_tab == "subscription" && @is_adding_subscription?}
class="bordered border-1 flex flex-col gap-4"
>
<div :if={is_nil(@selected_subscription)}>
Add subscription
<div class="badge badge-secondary">Limited time offer: 50%</div>
</div>
<div :if={not is_nil(@selected_subscription)}>
Edit subscription
<div class="badge badge-secondary">Limited time offer: 50%</div>
</div>
<.form
:let={f}
for={@subscription_form}
phx-change="validate_subscription"
phx-submit={
if is_nil(@selected_subscription),
do: "subscribe",
else: "update_subscription"
}
>
<.input
:if={is_nil(@selected_subscription)}
type="select"
field={f[:period]}
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
label="Subscription period"
options={@subscription_periods}
/>
<.input
field={f[:characters_limit]}
label="Characters limit"
show_value={true}
type="range"
min="300"
max="5000"
step="100"
class="range range-xs"
/>
<.input
field={f[:hubs_limit]}
label="Hubs limit"
show_value={true}
type="range"
min="20"
max="50"
step="10"
class="range range-xs"
/>
<.input field={f[:auto_renew?]} label="Auto Renew" type="checkbox" />
<div
:if={is_nil(@selected_subscription)}
class="stats w-full bg-primary text-primary-content mt-2"
>
<div class="stat">
<div class="stat-figure text-primary">
<.button type="submit">
Subscribe
</.button>
</div>
<div class="flex gap-8">
<div>
<div class="stat-title">Estimated price</div>
<div class="stat-value text-white">
ISK <%= (@estimated_price - @discount)
|> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
</div>
</div>
<div>
<div class="stat-title">Discount</div>
<div class="stat-value text-white relative">
ISK <%= @discount
|> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
<span class="absolute top-0 right-0 text-xs text-white discount" />
</div>
</div>
</div>
</div>
</div>
<div
:if={not is_nil(@selected_subscription)}
class="stats w-full bg-primary text-primary-content"
>
<div class="stat">
<div class="stat-figure text-primary">
<.button type="button" phx-click="cancel_edit_subscription">
Cancel
</.button>
<.button type="submit">
Update
</.button>
</div>
<div class="stat-title">Additional price (mounthly)</div>
<div class="stat-value text-white">
ISK <%= @additional_price
|> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
</div>
<div class="stat-actions text-end"></div>
</div>
</div>
</.form>
</.header>
</div>
</div>
</div>
<.input
type="checkbox"
field={f[:store_custom_labels]}
label="Store system custom labels"
/>
</.form>
</:actions>
</.header>
<.header :if={@active_settings_tab == "import"} class="bordered border-1 border-zinc-800">
Import/Export Map Settings
<:actions>
<.form :if={assigns |> Map.get(:import_form, false)} for={@import_form} phx-change="import">
<div phx-drop-target="{@uploads.settings.ref}">
<.live_file_input upload={@uploads.settings} />
</div>
</.form>
<progress :if={@importing} class="progress w-56"></progress>
<.button
id="export-settings-btn"
class="mt-8"
type="button"
disabled={@importing}
phx-hook="DownloadJson"
data-name={@map_slug}
data-content={Jason.encode!(assigns[:export_settings] || %{})}
>
<.icon name="hero-document-arrow-down-solid" class="w-4 h-4" /> Export Settings
</.button>
</:actions>
</.header>
<.header :if={@active_settings_tab == "balance"} class="bordered border-1 border-zinc-800">
<div class="stats w-full bg-primary text-primary-content">
<div class="stat">
<div class="stat-figure text-primary">
<.button :if={not @is_topping_up?} class="mt-2" type="button" phx-click="show_topup">
<.icon name="hero-banknotes-solid" class="w-4 h-4" /> Top Up
</.button>
</div>
<div class="stat-title">Map balance</div>
<div class="stat-value text-white">
ISK <%= @map_balance |> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
</div>
<div class="stat-actions text-end"></div>
</div>
</div>
<.form
:let={f}
:if={@is_topping_up?}
for={@topup_form}
phx-change="validate_topup"
phx-submit="topup"
>
<.live_select
field={f[:amount]}
update_min_len={0}
options={@amounts}
placeholder="Select topup amount"
/>
<div class="modal-action">
<.button class="mt-2" type="button" phx-click="hide_topup">
Cancel
</.button>
<.button class="mt-2" type="submit">
Top Up
</.button>
</div>
</.form>
</.header>
<.table
:if={@active_settings_tab == "subscription"}
class="!max-h-[20vh] !overflow-y-auto"
empty_label="No active subscriptions, using alpha plan by default."
id="active-subscriptions-tbl"
rows={@map_subscriptions}
>
<:col :let={subscription} label="Subscription Plan">
<%= subscription.plan %>
</:col>
<:col :let={subscription} label="Status">
<%= subscription.status %>
</:col>
<:col :let={subscription} label="Characters Limit">
<%= subscription.characters_limit %>
</:col>
<:col :let={subscription} label="Hubs Limit">
<%= subscription.hubs_limit %>
</:col>
<:col :let={subscription} label="Active Till">
<.local_time
:if={subscription.active_till}
id={"subscription-active-till-#{subscription.id}"}
at={subscription.active_till}
>
<%= subscription.active_till %>
</.local_time>
</:col>
<:col :let={subscription} label="Auto Renew">
<%= if subscription.auto_renew?, do: "Yes", else: "No" %>
</:col>
<:action :let={subscription}>
<div class="tooltip tooltip-left" data-tip="Edit subscription">
<button
:if={subscription.status == :active && subscription.plan != :alpha}
phx-click="edit-subscription"
phx-value-id={subscription.id}
>
<.icon name="hero-pencil-square-solid" class="w-4 h-4 hover:text-white" />
</button>
</div>
</:action>
<:action :let={subscription}>
<div class="tooltip tooltip-left" data-tip="Cancel subscription">
<button
:if={subscription.status == :active && subscription.plan != :alpha}
phx-click="cancel-subscription"
phx-value-id={subscription.id}
data={[confirm: "Please confirm to cancel subscription!"]}
>
<.icon name="hero-trash-solid" class="w-4 h-4 hover:text-white" />
</button>
</div>
</:action>
</.table>
</div>
<div class="modal-action">
<div
@@ -402,102 +614,4 @@
</.button>
</div>
</div>
<.header
:if={@active_settings_tab == "subscription" && @is_adding_subscription?}
class="bordered border-1 flex flex-col gap-4"
>
<div :if={is_nil(@selected_subscription)}>
Add subscription
<div class="badge badge-secondary">Limited time offer: 50%</div>
</div>
<div :if={not is_nil(@selected_subscription)}>
Edit subscription
<div class="badge badge-secondary">Limited time offer: 50%</div>
</div>
<.form
:let={f}
for={@subscription_form}
phx-change="validate_subscription"
phx-submit={if is_nil(@selected_subscription), do: "subscribe", else: "update_subscription"}
>
<.input
:if={is_nil(@selected_subscription)}
type="select"
field={f[:period]}
class="p-dropdown p-component p-inputwrapper"
placeholder="Subscription period"
options={@subscription_periods}
/>
<.input
field={f[:characters_limit]}
label="Characters limit"
show_value={true}
type="range"
min="300"
max="5000"
step="100"
class="range range-xs"
/>
<.input
field={f[:hubs_limit]}
label="Hubs limit"
show_value={true}
type="range"
min="20"
max="50"
step="10"
class="range range-xs"
/>
<.input field={f[:auto_renew?]} label="Auto Renew" type="checkbox" />
<div
:if={is_nil(@selected_subscription)}
class="stats w-full bg-primary text-primary-content"
>
<div class="stat">
<div class="stat-figure text-primary">
<.button type="submit">
Subscribe
</.button>
</div>
<div class="flex gap-8">
<div>
<div class="stat-title">Estimated price</div>
<div class="stat-value text-white">
ISK <%= (@estimated_price - @discount)
|> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
</div>
</div>
<div>
<div class="stat-title">Discount</div>
<div class="stat-value text-white relative">
ISK <%= @discount |> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
<span class="absolute top-0 right-0 text-xs text-white discount" />
</div>
</div>
</div>
</div>
</div>
<div
:if={not is_nil(@selected_subscription)}
class="stats w-full bg-primary text-primary-content"
>
<div class="stat">
<div class="stat-figure text-primary">
<.button type="button" phx-click="cancel_edit_subscription">
Cancel
</.button>
<.button type="submit">
Update
</.button>
</div>
<div class="stat-title">Additional price (mounthly)</div>
<div class="stat-value text-white">
ISK <%= @additional_price |> Number.to_human(units: ["", "K", "M", "B", "T", "P"]) %>
</div>
<div class="stat-actions text-end"></div>
</div>
</div>
</.form>
</.header>
</.modal>

View File

@@ -57,6 +57,7 @@ defmodule WandererAppWeb.Router do
@script_src,
~w('unsafe-inline'),
~w(https://unpkg.com),
~w(https://cdn.jsdelivr.net),
~w(https://w.appzi.io),
~w(https://www.googletagmanager.com),
~w(https://cdnjs.cloudflare.com)

View File

@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.20.1"
@version "1.29.5"
def project do
[
@@ -124,12 +124,12 @@ defmodule WandererApp.MixProject do
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.setup": [
"cmd npm install --prefix assets"
"cmd yarn install --cwd assets"
],
"assets.build": [],
"assets.deploy": [
"assets.setup",
"cmd --cd assets npm run build",
"cmd --cd assets yarn run build",
"phx.digest"
]
]

View File

@@ -31,10 +31,10 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
31,6,Shuttle,0,0,0,0,0,1
32,1,Alliance,None,0,0,0,0,0
38,7,Shield Extender,82,0,0,0,0,1
39,7,Shield Recharger,83,0,0,0,0,1
39,7,Shield Recharger,26451,0,0,0,0,1
40,7,Shield Booster,84,0,0,0,0,1
41,7,Remote Shield Booster,86,0,0,0,0,1
43,7,Capacitor Recharger,90,0,0,0,0,1
43,7,Capacitor Recharger,26457,0,0,0,0,1
46,7,Propulsion Module,96,0,0,0,0,1
47,7,Cargo Scanner,106,0,0,0,0,1
48,7,Ship Scanner,107,0,0,0,0,1
@@ -44,8 +44,8 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
54,7,Mining Laser,138,0,0,0,0,1
55,7,Projectile Weapon,384,0,0,0,0,1
56,7,Missile Launcher,168,0,0,0,0,0
57,7,Shield Power Relay,0,0,0,0,0,1
59,7,Gyrostabilizer,0,0,0,0,0,1
57,7,Shield Power Relay,26450,0,0,0,0,1
59,7,Gyrostabilizer,26452,0,0,0,0,1
60,7,Damage Control,0,0,0,0,0,1
61,7,Capacitor Battery,0,0,0,0,0,1
62,7,Armor Repair Unit,0,1,0,0,0,1
@@ -86,10 +86,10 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
110,9,Titan Blueprint,None,1,0,0,0,1
111,9,Shuttle Blueprint,0,1,0,0,0,1
118,9,Shield Extender Blueprint,82,1,0,0,0,1
119,9,Shield Recharger Blueprint,83,1,0,0,0,1
119,9,Shield Recharger Blueprint,26451,1,0,0,0,1
120,9,Shield Booster Blueprint,84,1,0,0,0,1
121,9,Remote Shield Booster Blueprint,86,1,0,0,0,1
123,9,Capacitor Recharger Blueprint,90,1,0,0,0,1
123,9,Capacitor Recharger Blueprint,26457,1,0,0,0,1
126,9,Propulsion Module Blueprint,96,1,0,0,0,1
127,9,Cargo Scanner Blueprint,106,1,0,0,0,1
128,9,Ship Scanner Blueprint,107,1,0,0,0,1
@@ -149,14 +149,14 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
201,7,ECM,0,0,0,0,0,1
202,7,ECCM,0,0,0,0,0,1
203,7,Sensor Backup Array,0,0,0,0,0,1
205,7,Heat Sink,0,0,0,0,0,1
205,7,Heat Sink,26453,0,0,0,0,1
208,7,Sensor Dampener,105,0,0,0,0,1
209,7,Remote Tracking Computer,3346,0,0,0,0,1
210,7,Signal Amplifier,0,0,0,0,0,1
211,7,Tracking Enhancer,0,0,0,0,0,1
212,7,Sensor Booster,74,0,0,0,0,1
213,7,Tracking Computer,3346,0,0,0,0,1
218,9,Heat Sink Blueprint,0,1,0,0,0,1
218,9,Heat Sink Blueprint,26453,1,0,0,0,1
223,9,Sensor Booster Blueprint,0,1,0,0,0,1
224,9,Tracking Computer Blueprint,0,1,0,0,0,1
225,7,Cheat Module Group,0,0,0,0,0,0
@@ -197,7 +197,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
299,18,Repair Drone,0,0,0,0,0,0
300,20,Cyberimplant,0,1,0,0,0,1
301,11,Concord Drone,0,0,0,0,0,0
302,7,Magnetic Field Stabilizer,0,0,0,0,0,1
302,7,Magnetic Field Stabilizer,26454,0,0,0,0,1
303,20,Booster,0,0,0,0,0,1
304,20,DNA Mutator,0,0,0,0,0,0
305,2,Comet,0,0,0,0,0,0
@@ -595,10 +595,10 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
764,7,Overdrive Injector System,0,0,0,0,0,1
765,7,Expanded Cargohold,0,0,0,0,0,1
766,7,Power Diagnostic System,0,0,0,0,0,1
767,7,Capacitor Power Relay,0,0,0,0,0,1
768,7,Capacitor Flux Coil,0,0,0,0,0,1
767,7,Capacitor Power Relay,26455,0,0,0,0,1
768,7,Capacitor Flux Coil,26456,0,0,0,0,1
769,7,Reactor Control Unit,0,0,0,0,0,1
770,7,Shield Flux Coil,0,0,0,0,0,1
770,7,Shield Flux Coil,26449,0,0,0,0,1
771,7,Missile Launcher Heavy Assault,3241,0,0,0,0,1
772,8,Heavy Assault Missile,3237,0,0,0,1,1
773,7,Rig Armor,0,0,0,0,0,1
@@ -1322,7 +1322,7 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
1962,66,Structure QA Modules,None,0,0,0,0,0
1964,17,Mutaplasmids,None,0,0,0,0,1
1966,66,Structure Capacitor Battery,None,0,0,0,0,1
1967,66,Structure Capacitor Power Relay,None,0,0,0,0,1
1967,66,Structure Capacitor Power Relay,26455,0,0,0,0,1
1968,66,Structure Armor Reinforcer,None,0,0,0,0,1
1969,7,Abyssal Modules,None,0,0,0,0,0
1971,2,Abyssal Hazards,None,0,0,0,0,0
@@ -1523,6 +1523,12 @@ groupID,categoryID,groupName,iconID,useBasePrice,anchored,anchorable,fittableNon
4802,11,Asteroid Sansha's Nation Officer Frigate,None,0,0,0,0,0
4803,11,Asteroid Serpentis Officer Cruiser,None,0,0,0,0,0
4804,11,Asteroid Serpentis Officer Frigate,None,0,0,0,0,0
4807,7,Breacher Pod Launchers,None,0,0,0,0,1
4808,8,SCARAB Breacher Pods,None,0,0,0,1,1
4810,22,Mercenary Den,None,0,0,0,0,1
4811,9,Mercenary Den Blueprint,None,1,0,0,0,1
4820,9,Mutaplasmid Blueprint,None,1,0,0,0,1
4821,17,Atavum,None,1,0,0,0,1
350858,350001,Infantry Weapons,None,1,0,0,0,0
351064,350001,Infantry Dropsuits,None,1,0,0,0,0
351121,350001,Infantry Modules,None,1,0,0,0,0
1 groupID categoryID groupName iconID useBasePrice anchored anchorable fittableNonSingleton published
31 31 6 Shuttle 0 0 0 0 0 1
32 32 1 Alliance None 0 0 0 0 0
33 38 7 Shield Extender 82 0 0 0 0 1
34 39 7 Shield Recharger 83 26451 0 0 0 0 1
35 40 7 Shield Booster 84 0 0 0 0 1
36 41 7 Remote Shield Booster 86 0 0 0 0 1
37 43 7 Capacitor Recharger 90 26457 0 0 0 0 1
38 46 7 Propulsion Module 96 0 0 0 0 1
39 47 7 Cargo Scanner 106 0 0 0 0 1
40 48 7 Ship Scanner 107 0 0 0 0 1
44 54 7 Mining Laser 138 0 0 0 0 1
45 55 7 Projectile Weapon 384 0 0 0 0 1
46 56 7 Missile Launcher 168 0 0 0 0 0
47 57 7 Shield Power Relay 0 26450 0 0 0 0 1
48 59 7 Gyrostabilizer 0 26452 0 0 0 0 1
49 60 7 Damage Control 0 0 0 0 0 1
50 61 7 Capacitor Battery 0 0 0 0 0 1
51 62 7 Armor Repair Unit 0 1 0 0 0 1
86 110 9 Titan Blueprint None 1 0 0 0 1
87 111 9 Shuttle Blueprint 0 1 0 0 0 1
88 118 9 Shield Extender Blueprint 82 1 0 0 0 1
89 119 9 Shield Recharger Blueprint 83 26451 1 0 0 0 1
90 120 9 Shield Booster Blueprint 84 1 0 0 0 1
91 121 9 Remote Shield Booster Blueprint 86 1 0 0 0 1
92 123 9 Capacitor Recharger Blueprint 90 26457 1 0 0 0 1
93 126 9 Propulsion Module Blueprint 96 1 0 0 0 1
94 127 9 Cargo Scanner Blueprint 106 1 0 0 0 1
95 128 9 Ship Scanner Blueprint 107 1 0 0 0 1
149 201 7 ECM 0 0 0 0 0 1
150 202 7 ECCM 0 0 0 0 0 1
151 203 7 Sensor Backup Array 0 0 0 0 0 1
152 205 7 Heat Sink 0 26453 0 0 0 0 1
153 208 7 Sensor Dampener 105 0 0 0 0 1
154 209 7 Remote Tracking Computer 3346 0 0 0 0 1
155 210 7 Signal Amplifier 0 0 0 0 0 1
156 211 7 Tracking Enhancer 0 0 0 0 0 1
157 212 7 Sensor Booster 74 0 0 0 0 1
158 213 7 Tracking Computer 3346 0 0 0 0 1
159 218 9 Heat Sink Blueprint 0 26453 1 0 0 0 1
160 223 9 Sensor Booster Blueprint 0 1 0 0 0 1
161 224 9 Tracking Computer Blueprint 0 1 0 0 0 1
162 225 7 Cheat Module Group 0 0 0 0 0 0
197 299 18 Repair Drone 0 0 0 0 0 0
198 300 20 Cyberimplant 0 1 0 0 0 1
199 301 11 Concord Drone 0 0 0 0 0 0
200 302 7 Magnetic Field Stabilizer 0 26454 0 0 0 0 1
201 303 20 Booster 0 0 0 0 0 1
202 304 20 DNA Mutator 0 0 0 0 0 0
203 305 2 Comet 0 0 0 0 0 0
595 764 7 Overdrive Injector System 0 0 0 0 0 1
596 765 7 Expanded Cargohold 0 0 0 0 0 1
597 766 7 Power Diagnostic System 0 0 0 0 0 1
598 767 7 Capacitor Power Relay 0 26455 0 0 0 0 1
599 768 7 Capacitor Flux Coil 0 26456 0 0 0 0 1
600 769 7 Reactor Control Unit 0 0 0 0 0 1
601 770 7 Shield Flux Coil 0 26449 0 0 0 0 1
602 771 7 Missile Launcher Heavy Assault 3241 0 0 0 0 1
603 772 8 Heavy Assault Missile 3237 0 0 0 1 1
604 773 7 Rig Armor 0 0 0 0 0 1
1322 1962 66 Structure QA Modules None 0 0 0 0 0
1323 1964 17 Mutaplasmids None 0 0 0 0 1
1324 1966 66 Structure Capacitor Battery None 0 0 0 0 1
1325 1967 66 Structure Capacitor Power Relay None 26455 0 0 0 0 1
1326 1968 66 Structure Armor Reinforcer None 0 0 0 0 1
1327 1969 7 Abyssal Modules None 0 0 0 0 0
1328 1971 2 Abyssal Hazards None 0 0 0 0 0
1523 4802 11 Asteroid Sansha's Nation Officer Frigate None 0 0 0 0 0
1524 4803 11 Asteroid Serpentis Officer Cruiser None 0 0 0 0 0
1525 4804 11 Asteroid Serpentis Officer Frigate None 0 0 0 0 0
1526 4807 7 Breacher Pod Launchers None 0 0 0 0 1
1527 4808 8 SCARAB Breacher Pods None 0 0 0 1 1
1528 4810 22 Mercenary Den None 0 0 0 0 1
1529 4811 9 Mercenary Den Blueprint None 1 0 0 0 1
1530 4820 9 Mutaplasmid Blueprint None 1 0 0 0 1
1531 4821 17 Atavum None 1 0 0 0 1
1532 350858 350001 Infantry Weapons None 1 0 0 0 0
1533 351064 350001 Infantry Dropsuits None 1 0 0 0 0
1534 351121 350001 Infantry Modules None 1 0 0 0 0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddSignatureCustomInfo do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_system_signatures_v1) do
add :custom_info, :text
end
end
def down do
alter table(:map_system_signatures_v1) do
remove :custom_info
end
end
end

View File

@@ -0,0 +1,197 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "eve_id",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "character_eve_id",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "type",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "linked_system_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "kind",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "group",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "custom_info",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated",
"type": "bigint"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_system_signatures_v1_system_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "map_system_v1"
},
"size": null,
"source": "system_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "75415502E71FD26E773B46F0EBFE6A645F7A521B4E1F06199FEFCF9F18F395BE",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "map_system_signatures_v1_uniq_system_eve_id_index",
"keys": [
{
"type": "atom",
"value": "system_id"
},
{
"type": "atom",
"value": "eve_id"
}
],
"name": "uniq_system_eve_id",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "map_system_signatures_v1"
}