Compare commits

..

33 Commits

Author SHA1 Message Date
CI
8d35500e2f chore: release version v1.74.13
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-29 15:15:15 +00:00
Dmitry Popov
5dad5d8e03 fix(Core): Fixed issue with callback url 2025-07-29 17:11:28 +02:00
CI
fd4d5b90e2 chore: release version v1.74.12
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-22 11:37:44 +00:00
Dmitry Popov
1ee9f26b34 chore: fixed .tool-versions 2025-07-22 13:37:06 +02:00
CI
09880a54e9 chore: release version v1.74.11
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-18 11:41:22 +00:00
Dmitry Popov
0f6847b16d fix(Map): Fixed remove pings for removed systems 2025-07-18 13:39:36 +02:00
CI
e457d94df8 chore: release version v1.74.10
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-15 12:49:00 +00:00
Dmitry Popov
e9583c928e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-07-15 14:48:23 +02:00
Dmitry Popov
89c14628e1 chore: mix format 2025-07-15 14:48:20 +02:00
CI
7a82b2c102 chore: release version v1.74.9
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-13 17:51:56 +00:00
Dmitry Popov
2db2a47186 Merge pull request #460 from wanderer-industries/fast-forward-bug
fix(Map): Trying to fix problem with fast forwarding after page are i…
2025-07-13 21:51:32 +04:00
DanSylvest
eabb0e8470 fix(Map): Trying to fix problem with fast forwarding after page are inactive some time. 2025-07-13 15:20:33 +03:00
CI
c65b8e5ebd chore: release version v1.74.8
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-11 08:19:12 +00:00
Dmitry Popov
bfed1480ae Merge pull request #453 from wanderer-industries/unified-settings
Unified settings
2025-07-11 12:18:41 +04:00
DanSylvest
5ff902f185 fix(Map): removed comments 2025-07-09 21:01:30 +03:00
DanSylvest
8d38345c7f fix(Map): Fixed conflict 2025-07-09 20:23:18 +03:00
DanSylvest
14be9dbb8a Merge branch 'main' into unified-settings
# Conflicts:
#	assets/js/hooks/Mapper/components/map/components/LocalCounter/LocalCounter.tsx
2025-07-09 19:55:29 +03:00
CI
720c26db94 chore: release version v1.74.7
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-09 15:43:43 +00:00
Dmitry Popov
6d0b8b845d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-07-09 17:43:17 +02:00
Dmitry Popov
b2767e000e chore: release version v1.74.5 2025-07-09 17:43:14 +02:00
CI
169f23c2ca chore: release version v1.74.6
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-09 14:33:43 +00:00
Dmitry Popov
81f70eafff chore: release version v1.74.5 2025-07-09 16:33:04 +02:00
CI
8b6f600989 chore: release version v1.74.5 2025-07-09 08:19:58 +00:00
Dmitry Popov
fe3617b39f Merge pull request #454 from wanderer-industries/gate-connections
fix(Map): Add background for Pochven's systems. Changed from Region n…
2025-07-09 12:19:32 +04:00
DanSylvest
7fb8d24d73 fix(Map): Add background for Pochven's systems. Changed from Region name to constellation name for pochven systems. Changed connection style for gates (display like common connection). Changed behaviour of connections. 2025-07-08 13:17:03 +03:00
DanSylvest
f03448007d Merge branch 'main' into unified-settings 2025-07-07 17:22:03 +03:00
CI
c317a8bff9 chore: release version v1.74.4
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-07 13:59:56 +00:00
Dmitry Popov
618cca39a4 fix(Core): Fixed issue with update system positions 2025-07-07 15:59:23 +02:00
DanSylvest
fe7a98098f fix(Map): Unified settings. Second part: Import/Export 2025-07-07 16:57:06 +03:00
DanSylvest
df49939990 fix(Map): Unified settings. First part: add one place for storing settings 2025-07-06 18:59:40 +03:00
Dmitry Popov
f23f2776f4 chore: release version v1.72.1 2025-07-06 11:33:29 +02:00
CI
4419c86164 chore: release version v1.74.3
Some checks failed
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-07-06 08:55:48 +00:00
Dmitry Popov
9848f49b49 fix(Core): Fixed issues with map subscription component 2025-07-06 10:55:21 +02:00
150 changed files with 3747 additions and 1727 deletions

View File

@@ -2,6 +2,95 @@
<!-- changelog -->
## [v1.74.13](https://github.com/wanderer-industries/wanderer/compare/v1.74.12...v1.74.13) (2025-07-29)
### Bug Fixes:
* Core: Fixed issue with callback url
## [v1.74.12](https://github.com/wanderer-industries/wanderer/compare/v1.74.11...v1.74.12) (2025-07-22)
## [v1.74.11](https://github.com/wanderer-industries/wanderer/compare/v1.74.10...v1.74.11) (2025-07-18)
### Bug Fixes:
* Map: Fixed remove pings for removed systems
## [v1.74.10](https://github.com/wanderer-industries/wanderer/compare/v1.74.9...v1.74.10) (2025-07-15)
## [v1.74.9](https://github.com/wanderer-industries/wanderer/compare/v1.74.8...v1.74.9) (2025-07-13)
### Bug Fixes:
* Map: Trying to fix problem with fast forwarding after page are inactive some time.
## [v1.74.8](https://github.com/wanderer-industries/wanderer/compare/v1.74.7...v1.74.8) (2025-07-11)
### Bug Fixes:
* Map: removed comments
* Map: Fixed conflict
* Map: Unified settings. Second part: Import/Export
* Map: Unified settings. First part: add one place for storing settings
## [v1.74.7](https://github.com/wanderer-industries/wanderer/compare/v1.74.6...v1.74.7) (2025-07-09)
## [v1.74.6](https://github.com/wanderer-industries/wanderer/compare/v1.74.5...v1.74.6) (2025-07-09)
## [v1.74.5](https://github.com/wanderer-industries/wanderer/compare/v1.74.4...v1.74.5) (2025-07-09)
### Bug Fixes:
* Map: Add background for Pochven's systems. Changed from Region name to constellation name for pochven systems. Changed connection style for gates (display like common connection). Changed behaviour of connections.
## [v1.74.4](https://github.com/wanderer-industries/wanderer/compare/v1.74.3...v1.74.4) (2025-07-07)
### Bug Fixes:
* Core: Fixed issue with update system positions
## [v1.74.3](https://github.com/wanderer-industries/wanderer/compare/v1.74.2...v1.74.3) (2025-07-06)
### Bug Fixes:
* Core: Fixed issues with map subscription component
## [v1.74.2](https://github.com/wanderer-industries/wanderer/compare/v1.74.1...v1.74.2) (2025-06-30)

View File

@@ -212,3 +212,75 @@
.p-inputtext:enabled:hover {
border-color: #335c7e;
}
// --------------- TOAST
.p-toast .p-toast-message {
background-color: #1a1a1a;
color: #e0e0e0;
border-left: 4px solid transparent;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
.p-toast .p-toast-message .p-toast-summary {
color: #ffffff;
font-weight: 600;
}
.p-toast .p-toast-message .p-toast-detail {
color: #c0c0c0;
font-size: 13px;
}
.p-toast .p-toast-icon-close {
color: #ffaa00;
transition: background 0.2s;
}
.p-toast .p-toast-icon-close:hover {
background: #333;
color: #fff;
}
.p-toast-message-success {
border-left-color: #f1c40f;
}
.p-toast-message-error {
border-left-color: #e74c3c;
}
.p-toast-message-info {
border-left-color: #3498db;
}
.p-toast-message-warn {
border-left-color: #e67e22;
}
.p-toast-message-success .p-toast-message-icon {
color: #f1c40f;
}
.p-toast-message-error .p-toast-message-icon {
color: #e74c3c;
}
.p-toast-message-info .p-toast-message-icon {
color: #3498db;
}
.p-toast-message-warn .p-toast-message-icon {
color: #e67e22;
}
.p-toast-message-success .p-toast-message-content {
border-left-color: #f1c40f;
}
.p-toast-message-error .p-toast-message-content {
border-left-color: #e74c3c;
}
.p-toast-message-info .p-toast-message-content {
border-left-color: #3498db;
}
.p-toast-message-warn .p-toast-message-content {
border-left-color: #e67e22;
}

View File

@@ -64,9 +64,9 @@ body .p-dialog {
}
.p-dialog-footer {
padding: 1rem;
border-top: 1px solid #ddd;
background: #f4f4f4;
padding: .75rem 1rem;
border-top: none !important;
//background: #f4f4f4;
}
.p-dialog-header-close {

View File

@@ -0,0 +1 @@
export * from './parseMapUserSettings.ts';

View File

@@ -0,0 +1,67 @@
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
const REQUIRED_KEYS = [
'widgets',
'interface',
'onTheMap',
'routes',
'localWidget',
'signaturesWidget',
'killsWidget',
] as const;
type RequiredKeys = (typeof REQUIRED_KEYS)[number];
/** Custom error for any parsing / validation issue */
export class MapUserSettingsParseError extends Error {
constructor(msg: string) {
super(`MapUserSettings parse error: ${msg}`);
}
}
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
typeof v === 'object' && v !== null && REQUIRED_KEYS.every(k => k in v);
/* ------------------------------ Main parser ------------------------------- */
/**
* Parses and validates a JSON string as `MapUserSettings`.
*
* @throws `MapUserSettingsParseError` если строка не JSON или нарушена структура
*/
export const parseMapUserSettings = (json: unknown): MapUserSettings => {
if (typeof json !== 'string') throw new MapUserSettingsParseError('Input must be a JSON string');
let data: unknown;
try {
data = JSON.parse(json);
} catch (e) {
throw new MapUserSettingsParseError(`Invalid JSON: ${(e as Error).message}`);
}
if (!hasAllRequiredKeys(data)) {
const missing = REQUIRED_KEYS.filter(k => !(k in (data as any)));
throw new MapUserSettingsParseError(`Missing top-level field(s): ${missing.join(', ')}`);
}
for (const key of REQUIRED_KEYS) {
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
}
}
// Everything passes, so cast is safe
return data as MapUserSettings;
};
/* ------------------------------ Usage example ----------------------------- */
// const raw = fetchFromServer(); // string
// const settings = parseMapUserSettings(raw);

View File

@@ -98,6 +98,7 @@ interface MapCompProps {
theme?: string;
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
}
const MapComp = ({
@@ -117,6 +118,7 @@ const MapComp = ({
onAddSystem,
pings,
minimapPlacement = 'bottom-right',
localShowShipName = false,
}: MapCompProps) => {
const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
@@ -212,8 +214,9 @@ const MapComp = ({
showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections,
pings,
localShowShipName,
}));
}, [showKSpaceBG, isThickConnections, pings, update]);
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
return (
<>

View File

@@ -10,6 +10,7 @@ export type MapData = MapUnionTypes & {
showKSpaceBG: boolean;
isThickConnections: boolean;
linkedSigEveId: string;
localShowShipName: boolean;
};
interface MapProviderProps {
@@ -42,6 +43,7 @@ const INITIAL_DATA: MapData = {
followingCharacterEveId: null,
userHubs: [],
pings: [],
localShowShipName: false,
};
export interface MapContextProps {

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { useKillsCounter } from '../../hooks/useKillsCounter.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import {
KILLS_ROW_HEIGHT,
SystemKillsList,

View File

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

View File

@@ -3,11 +3,11 @@ import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
import classes from './SolarSystemLocalCounter.module.scss';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
import classes from './LocalCounter.module.scss';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useLocalCharactersItemTemplate } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters.tsx';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
@@ -16,8 +16,10 @@ interface LocalCounterProps {
}
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const [settings] = useLocalCharacterWidgetSettings();
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
const {
data: { localShowShipName },
} = useMapState();
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
const theme = useTheme();
const pilotTooltipContent = useMemo(() => {

View File

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

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
import clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
@@ -51,11 +51,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const [hovered, setHovered] = useState(false);
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode!, targetNode!);
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const method = isWormhole ? getBezierPath : getBezierPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,

View File

@@ -40,6 +40,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
z-index: 3;
overflow: hidden;
&.Pochven,
&.Mataria,
&.Amarria,
&.Gallente,
@@ -95,6 +96,15 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
}
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -13px;
}
}
&.selected {
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;

View File

@@ -12,11 +12,11 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { Tag } from 'primereact/tag';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {

View File

@@ -12,10 +12,10 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {

View File

@@ -5,7 +5,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { Regions, REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
@@ -65,6 +65,7 @@ const SpaceToClass: Record<string, string> = {
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
@@ -112,6 +113,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
region_id,
is_shattered,
solar_system_name,
constellation_name,
} = systemStaticInfo;
const { isShowUnsplashedSignatures } = interfaceSettings;
@@ -195,10 +197,18 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const isRally = useMemo(
() => pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
() => !!pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
[pings, solar_system_id],
);
const regionName = useMemo(() => {
if (region_id === Regions.Pochven) {
return constellation_name;
}
return region_name;
}, [constellation_name, region_id, region_name]);
const nodeVars: SolarSystemNodeVars = {
id,
selected,
@@ -233,7 +243,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isThickConnections,
classTitle: class_title,
temporaryName: computedTemporaryName,
regionName: region_name,
regionName,
solarSystemName: solar_system_name,
isRally,
};

View File

@@ -1,37 +1,48 @@
import { Position, internalsSymbol } from 'reactflow';
import { Position, internalsSymbol, Node } from 'reactflow';
// returns the position (top,right,bottom or right) passed node compared to
function getParams(nodeA, nodeB) {
type Coords = [number, number];
type CoordsWithPosition = [number, number, Position];
function segmentsIntersect(a1: number, a2: number, b1: number, b2: number): boolean {
const [minA, maxA] = a1 < a2 ? [a1, a2] : [a2, a1];
const [minB, maxB] = b1 < b2 ? [b1, b2] : [b2, b1];
return maxA >= minB && maxB >= minA;
}
function getParams(nodeA: Node, nodeB: Node): CoordsWithPosition {
const centerA = getNodeCenter(nodeA);
const centerB = getNodeCenter(nodeB);
const horizontalDiff = Math.abs(centerA.x - centerB.x);
const verticalDiff = Math.abs(centerA.y - centerB.y);
let position: Position;
// when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
if (horizontalDiff > verticalDiff) {
position = centerA.x > centerB.x ? Position.Left : Position.Right;
} else {
// here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
if (
segmentsIntersect(
nodeA.positionAbsolute!.x - 10,
nodeA.positionAbsolute!.x - 10 + nodeA.width! + 20,
nodeB.positionAbsolute!.x,
nodeB.positionAbsolute!.x + nodeB.width!,
)
) {
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
} else {
position = centerA.x > centerB.x ? Position.Left : Position.Right;
}
const [x, y] = getHandleCoordsByPosition(nodeA, position);
return [x, y, position];
}
function getHandleCoordsByPosition(node, handlePosition) {
// all handles are from type source, that's why we use handleBounds.source here
const handle = node[internalsSymbol].handleBounds.source.find(h => h.position === handlePosition);
function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords {
const handle = node[internalsSymbol]!.handleBounds!.source!.find(h => h.position === handlePosition);
if (!handle) {
throw new Error(`Handle with position ${handlePosition} not found on node ${node.id}`);
}
let offsetX = handle.width / 2;
let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible.
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
switch (handlePosition) {
case Position.Left:
offsetX = 0;
@@ -47,21 +58,20 @@ function getHandleCoordsByPosition(node, handlePosition) {
break;
}
const x = node.positionAbsolute.x + handle.x + offsetX;
const y = node.positionAbsolute.y + handle.y + offsetY;
const x = node.positionAbsolute!.x + handle.x + offsetX;
const y = node.positionAbsolute!.y + handle.y + offsetY;
return [x, y];
}
function getNodeCenter(node) {
function getNodeCenter(node: Node): { x: number; y: number } {
return {
x: node.positionAbsolute.x + node.width / 2,
y: node.positionAbsolute.y + node.height / 2,
x: node.positionAbsolute!.x + node.width! / 2,
y: node.positionAbsolute!.y + node.height! / 2,
};
}
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source, target) {
export function getEdgeParams(source: Node, target: Node) {
const [sx, sy, sourcePos] = getParams(source, target);
const [tx, ty, targetPos] = getParams(target, source);

View File

@@ -1,9 +1,4 @@
import { Button } from 'primereact/button';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import {
CharacterCardById,
SystemView,
@@ -12,12 +7,17 @@ import {
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
import useRefState from 'react-usestateref';
import { PrimeIcons } from 'primereact/api';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Toast } from 'primereact/toast';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useRefState from 'react-usestateref';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
@@ -119,7 +119,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
await outCommand({
type: OutCommand.cancelPing,
data: { type: ping.type, solar_system_id: ping.solar_system_id },
data: { type: ping.type, id: ping.id },
});
}, [outCommand, ping]);

View File

@@ -7,10 +7,6 @@ import {
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts';
import {
SETTINGS_KEYS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
@@ -18,6 +14,7 @@ import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureC
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;

View File

@@ -6,7 +6,6 @@ import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootP
import { UserPermission } from '@/hooks/Mapper/types/permissions';
import { LocalCharactersList } from './components/LocalCharactersList';
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
import classes from './LocalCharacters.module.scss';
import clsx from 'clsx';
@@ -14,9 +13,9 @@ import clsx from 'clsx';
export const LocalCharacters = () => {
const {
data: { characters, userCharacters, selectedSystems },
storedSettings: { settingsLocal, settingsLocalUpdate },
} = useMapRootState();
const [settings, setSettings] = useLocalCharacterWidgetSettings();
const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
@@ -31,12 +30,12 @@ export const LocalCharacters = () => {
.map(x => ({
...x,
isOwn: userCharacters.includes(x.eve_id),
compact: settings.compact,
showShipName: settings.showShipName,
compact: settingsLocal.compact,
showShipName: settingsLocal.showShipName,
}))
.sort(sortCharacters);
if (!showOffline || !settings.showOffline) {
if (!showOffline || !settingsLocal.showOffline) {
return filtered.filter(c => c.online);
}
return filtered;
@@ -44,9 +43,9 @@ export const LocalCharacters = () => {
characters,
systemId,
userCharacters,
settings.compact,
settings.showOffline,
settings.showShipName,
settingsLocal.compact,
settingsLocal.showOffline,
settingsLocal.showShipName,
showOffline,
]);
@@ -54,7 +53,7 @@ export const LocalCharacters = () => {
const isNotSelectedSystem = selectedSystems.length !== 1;
const showList = sorted.length > 0 && selectedSystems.length === 1;
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
const itemTemplate = useLocalCharactersItemTemplate(settingsLocal.showShipName);
return (
<Widget
@@ -63,8 +62,8 @@ export const LocalCharacters = () => {
sortedCount={sorted.length}
showList={showList}
showOffline={showOffline}
settings={settings}
setSettings={setSettings}
settings={settingsLocal}
setSettings={settingsLocalUpdate}
/>
}
>
@@ -81,7 +80,7 @@ export const LocalCharacters = () => {
{showList && (
<LocalCharactersList
items={sorted}
itemSize={settings.compact ? 26 : 41}
itemSize={settingsLocal.compact ? 26 : 41}
itemTemplate={itemTemplate}
containerClassName={clsx(
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',

View File

@@ -1,21 +0,0 @@
import useLocalStorageState from 'use-local-storage-state';
export interface LocalCharacterWidgetSettings {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
}
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export function useLocalCharacterWidgetSettings() {
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
});
}

View File

@@ -8,8 +8,8 @@ import {
Setting,
SettingsTypes,
SIGNATURE_SETTINGS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
interface SystemSignatureSettingsDialogProps {
settings: SignatureSettingsType;

View File

@@ -1,21 +1,14 @@
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import useLocalStorageState from 'use-local-storage-state';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import {
SETTINGS_KEYS,
SETTINGS_VALUES,
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
/**
* Custom hook for managing pending signature deletions and undo countdown.
@@ -126,20 +119,14 @@ export const SystemSignatures = () => {
const {
data: { selectedSystems },
outCommand,
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
SIGNATURE_SETTING_STORE_KEY,
{
defaultValue: SETTINGS_VALUES,
},
);
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
currentSettings,
settingsSignatures,
outCommand,
);
@@ -157,20 +144,20 @@ export const SystemSignatures = () => {
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
settingsSignaturesUpdate(newSettings);
setVisible(false);
},
[setCurrentSettings],
[settingsSignaturesUpdate],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
setCurrentSettings(prev => ({
settingsSignaturesUpdate(prev => ({
...prev,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
}));
},
[setCurrentSettings],
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setVisible(true), []);
@@ -180,7 +167,7 @@ export const SystemSignatures = () => {
label={
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingIds.size}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
@@ -197,7 +184,7 @@ export const SystemSignatures = () => {
) : (
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
settings={settingsSignatures}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
@@ -207,7 +194,7 @@ export const SystemSignatures = () => {
{visible && (
<SystemSignatureSettingsDialog
settings={currentSettings}
settings={settingsSignatures}
onCancel={() => setVisible(false)}
onSave={handleSettingsSave}
/>

View File

@@ -8,7 +8,6 @@ import {
SortOrder,
} from 'primereact/datatable';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
import {
@@ -17,9 +16,6 @@ import {
GROUPS_LIST,
MEDIUM_MAX_WIDTH,
OTHER_COLUMNS_WIDTH,
SETTINGS_KEYS,
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
@@ -36,19 +32,11 @@ import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
type SystemSignaturesSortSettings = {
sortField: string;
sortOrder: SortOrder;
};
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
sortField: 'inserted_at',
sortOrder: -1,
};
interface SystemSignaturesContentProps {
systemId: string;
settings: SignatureSettingsType;
@@ -79,6 +67,10 @@ export const SystemSignaturesContent = ({
const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null);
const {
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState();
const tableRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<WdTooltipHandlers>(null);
@@ -87,11 +79,6 @@ export const SystemSignaturesContent = ({
const { clipboardContent, setClipboardContent } = useClipboard();
const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>(
'window:signatures:sort',
{ defaultValue: SORT_DEFAULT_VALUES },
);
const {
signatures,
selectedSignatures,
@@ -246,8 +233,8 @@ export const SystemSignaturesContent = ({
tooltipRef.current?.hide();
}, []);
const refVars = useRef({ settings, selectedSignatures, setSortSettings });
refVars.current = { settings, selectedSignatures, setSortSettings };
const refVars = useRef({ settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate });
refVars.current = { settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate };
// @ts-ignore
const getRowClassName = useCallback(rowData => {
@@ -263,7 +250,12 @@ export const SystemSignaturesContent = ({
}, []);
const handleSortSettings = useCallback(
(e: DataTableStateEvent) => refVars.current.setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder }),
(e: DataTableStateEvent) =>
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
}),
[],
);
@@ -295,8 +287,8 @@ export const SystemSignaturesContent = ({
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
sortField={settingsSignatures[SETTINGS_KEYS.SORT_FIELD] as string}
sortOrder={settingsSignatures[SETTINGS_KEYS.SORT_ORDER] as SortOrder}
onSort={handleSortSettings}
onRowMouseEnter={onRowMouseEnter}
onRowMouseLeave={onRowMouseLeave}

View File

@@ -11,6 +11,7 @@ import {
SignatureKindFR,
SignatureKindRU,
} from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
@@ -96,44 +97,11 @@ export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined =>
return MAPPING_GROUP_TO_ENG[val] || undefined;
};
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
export enum SETTINGS_KEYS {
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
SHOW_CHARACTER_COLUMN = 'show_character_column',
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
DELETION_TIMING = 'deletion_timing',
COLOR_BY_TYPE = 'color_by_type',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
// From SignatureKind
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
COSMIC_SIGNATURE = SignatureKind.CosmicSignature,
DEPLOYABLE = SignatureKind.Deployable,
STRUCTURE = SignatureKind.Structure,
STARBASE = SignatureKind.Starbase,
SHIP = SignatureKind.Ship,
DRONE = SignatureKind.Drone,
// From SignatureGroup
WORMHOLE = SignatureGroup.Wormhole,
RELIC_SITE = SignatureGroup.RelicSite,
DATA_SITE = SignatureGroup.DataSite,
ORE_SITE = SignatureGroup.OreSite,
GAS_SITE = SignatureGroup.GasSite,
COMBAT_SITE = SignatureGroup.CombatSite,
}
export enum SettingsTypes {
flag,
dropdown,
}
export type SignatureSettingsType = { [key in SETTINGS_KEYS]?: unknown };
export type Setting = {
key: SETTINGS_KEYS;
name: string;
@@ -142,12 +110,6 @@ export type Setting = {
options?: { label: string; value: number | string | boolean }[];
};
export enum SIGNATURES_DELETION_TIMING {
IMMEDIATE,
DEFAULT,
EXTENDED,
}
// Now use a stricter type: every timing key maps to a number
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>;
@@ -194,32 +156,6 @@ export const SIGNATURE_SETTINGS = {
],
};
export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
[SETTINGS_KEYS.COSMIC_ANOMALY]: true,
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
[SETTINGS_KEYS.DEPLOYABLE]: true,
[SETTINGS_KEYS.STRUCTURE]: true,
[SETTINGS_KEYS.STARBASE]: true,
[SETTINGS_KEYS.SHIP]: true,
[SETTINGS_KEYS.DRONE]: true,
[SETTINGS_KEYS.WORMHOLE]: true,
[SETTINGS_KEYS.RELIC_SITE]: true,
[SETTINGS_KEYS.DATA_SITE]: true,
[SETTINGS_KEYS.ORE_SITE]: true,
[SETTINGS_KEYS.GAS_SITE]: true,
[SETTINGS_KEYS.COMBAT_SITE]: true,
};
// Now this map is strongly typed as “number” for each timing enum
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,

View File

@@ -1,5 +1,5 @@
import { SignatureSettingsType } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export interface UseSystemSignaturesDataProps {
systemId: string;

View File

@@ -5,15 +5,13 @@ import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import {
SETTINGS_KEYS,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
export const useSystemSignaturesData = ({
systemId,

View File

@@ -3,7 +3,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemKillsList } from './SystemKillsList';
import { KillsHeader } from './components/SystemKillsHeader';
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
import { useSystemKills } from './hooks/useSystemKills';
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
@@ -13,27 +12,25 @@ const SystemKillsContent = () => {
const {
data: { selectedSystems, isSubscriptionActive },
outCommand,
storedSettings: { settingsKills },
} = useMapRootState();
const [systemId] = selectedSystems || [];
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const [settings] = useKillsWidgetSettings();
const visible = settings.showAll;
const { kills, isLoading, error } = useSystemKills({
systemId,
outCommand,
showAllVisible: visible,
sinceHours: settings.timeRange,
showAllVisible: settingsKills.showAll,
sinceHours: settingsKills.timeRange,
});
const isNothingSelected = !systemId && !visible;
const isNothingSelected = !systemId && !settingsKills.showAll;
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settings.whOnly || !visible) return kills;
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
return kills.filter(kill => {
if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
@@ -41,7 +38,7 @@ const SystemKillsContent = () => {
}
return isWormholeSpace(systemStaticInfo.system_class);
});
}, [kills, settings.whOnly, systemStaticInfo, visible]);
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
if (!isSubscriptionActive) {
return (
@@ -87,7 +84,9 @@ const SystemKillsContent = () => {
);
}
return <SystemKillsList kills={filteredKills} onlyOneSystem={!visible} timeRange={settings.timeRange} />;
return (
<SystemKillsList kills={filteredKills} onlyOneSystem={!settingsKills.showAll} timeRange={settingsKills.timeRange} />
);
};
export const WSystemKills = () => {

View File

@@ -7,9 +7,9 @@ import {
WdImgButton,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import { PrimeIcons } from 'primereact/api';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface KillsHeaderProps {
systemId?: string;
@@ -17,11 +17,14 @@ interface KillsHeaderProps {
}
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
const [settings, setSettings] = useKillsWidgetSettings();
const { showAll } = settings;
const {
storedSettings: { settingsKills, settingsKillsUpdate },
} = useMapRootState();
const { showAll } = settingsKills;
const onToggleShowAllVisible = () => {
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
settingsKillsUpdate(prev => ({ ...prev, showAll: !prev.showAll }));
};
const headerRef = useRef<HTMLDivElement>(null);

View File

@@ -3,12 +3,12 @@ import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface KillsSettingsDialogProps {
visible: boolean;
@@ -16,12 +16,15 @@ interface KillsSettingsDialogProps {
}
export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visible, setVisible }) => {
const [globalSettings, setGlobalSettings] = useKillsWidgetSettings();
const {
storedSettings: { settingsKills, settingsKillsUpdate },
} = useMapRootState();
const localRef = useRef({
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
showAll: settingsKills.showAll,
whOnly: settingsKills.whOnly,
excludedSystems: settingsKills.excludedSystems || [],
timeRange: settingsKills.timeRange,
});
const [, forceRender] = useState(0);
@@ -30,14 +33,14 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
useEffect(() => {
if (visible) {
localRef.current = {
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
showAll: settingsKills.showAll,
whOnly: settingsKills.whOnly,
excludedSystems: settingsKills.excludedSystems || [],
timeRange: settingsKills.timeRange,
};
forceRender(n => n + 1);
}
}, [visible, globalSettings]);
}, [visible, settingsKills]);
const handleWHChange = useCallback((checked: boolean) => {
localRef.current = {
@@ -75,12 +78,12 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
}, []);
const handleApply = useCallback(() => {
setGlobalSettings(prev => ({
settingsKillsUpdate(prev => ({
...prev,
...localRef.current,
}));
setVisible(false);
}, [setGlobalSettings, setVisible]);
}, [settingsKillsUpdate, setVisible]);
const handleHide = useCallback(() => {
setVisible(false);

View File

@@ -1,53 +0,0 @@
import { useMemo, useCallback } from 'react';
import useLocalStorageState from 'use-local-storage-state';
export interface KillsWidgetSettings {
showAll: boolean;
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
}
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
showAll: false,
whOnly: true,
excludedSystems: [],
version: 2,
timeRange: 4,
};
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {
if (!settings) {
return DEFAULT_KILLS_WIDGET_SETTINGS;
}
return {
...DEFAULT_KILLS_WIDGET_SETTINGS,
...settings,
excludedSystems: Array.isArray(settings.excludedSystems) ? settings.excludedSystems : [],
};
}
export function useKillsWidgetSettings() {
const [rawValue, setRawValue] = useLocalStorageState<KillsWidgetSettings | undefined>('kills:widget:settings');
const value = useMemo<KillsWidgetSettings>(() => {
return mergeWithDefaults(rawValue);
}, [rawValue]);
const setValue = useCallback(
(newVal: KillsWidgetSettings | ((prev: KillsWidgetSettings) => KillsWidgetSettings)) => {
setRawValue(prev => {
const mergedPrev = mergeWithDefaults(prev);
const nextUnmerged = typeof newVal === 'function' ? newVal(mergedPrev) : newVal;
return mergeWithDefaults(nextUnmerged);
});
},
[setRawValue],
);
return [value, setValue] as const;
}

View File

@@ -3,7 +3,6 @@ import debounce from 'lodash.debounce';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
interface UseSystemKillsProps {
systemId?: string;
@@ -26,10 +25,12 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[]): Detai
}
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
const { data, update } = useMapRootState();
const { detailedKills = {}, systems = [] } = data;
const [settings] = useKillsWidgetSettings();
const excludedSystems = settings.excludedSystems;
const {
data: { detailedKills = {}, systems = [] },
update,
storedSettings: { settingsKills },
} = useMapRootState();
const { excludedSystems } = settingsKills;
const effectiveSinceHours = sinceHours;

View File

@@ -14,13 +14,14 @@ import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/compone
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx';
export interface MapRootContentProps {}
// eslint-disable-next-line no-empty-pattern
export const MapRootContent = ({}: MapRootContentProps) => {
const {
storedSettings: { interfaceSettings },
storedSettings: { interfaceSettings, isReady, hasOldSettings },
data,
} = useMapRootState();
const { isShowMenu } = interfaceSettings;
@@ -34,7 +35,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
/* Important Notice - this solution needs for use one instance of MapInterface */
const mapInterface = <MapInterface />;
const mapInterface = isReady ? <MapInterface /> : null;
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
@@ -90,6 +91,8 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)}
{hasOldSettings && <OldSettingsDialog />}
</Layout>
</div>
);

View File

@@ -12,6 +12,7 @@ import {
import { WidgetsSettings } from './components/WidgetsSettings';
import { CommonSettings } from './components/CommonSettings';
import { SettingsListItem } from './types.ts';
import { ImportExport } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx';
export interface MapSettingsProps {
visible: boolean;
@@ -87,6 +88,10 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings />
</TabPanel>
<TabPanel header="Import/Export" className="h-full" headerClassName={styles.verticalTabHeader}>
<ImportExport />
</TabPanel>
</TabView>
</div>
</div>

View File

@@ -22,6 +22,7 @@ import { OutCommand } from '@/hooks/Mapper/types';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
type MapSettingsContextType = {
renderSettingItem: (item: SettingsListItem) => ReactNode;
@@ -30,7 +31,7 @@ type MapSettingsContextType = {
const MapSettingsContext = createContext<MapSettingsContextType | undefined>(undefined);
export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
export const MapSettingsProvider = ({ children }: WithChildren) => {
const {
outCommand,
storedSettings: { interfaceSettings, setInterfaceSettings },

View File

@@ -0,0 +1,202 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useMemo, useRef } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
import { SplitButton } from 'primereact/splitbutton';
import { loadTextFile } from '@/hooks/Mapper/utils';
export const ImportExport = () => {
const {
storedSettings: { getSettingsForExport, applySettings },
data: { map_slug },
} = useMapRootState();
const toast = useRef<Toast | null>(null);
const handleImportFromClipboard = useCallback(async () => {
const text = await navigator.clipboard.readText();
if (text == null || text == '') {
return;
}
try {
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
detail: 'Map settings was imported successfully.',
life: 3000,
});
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 100);
return;
}
toast.current?.show({
severity: 'warn',
summary: 'Warning',
detail: 'Settings already imported. Or something went wrong.',
life: 3000,
});
} catch (error) {
console.error(`Import from clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on import from Clipboard, check console log.',
life: 3000,
});
}
}, [applySettings]);
const handleImportFromFile = useCallback(async () => {
try {
const text = await loadTextFile();
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
detail: 'Map settings was imported successfully.',
life: 3000,
});
return;
}
toast.current?.show({
severity: 'warn',
summary: 'Warning',
detail: 'Settings already imported. Or something went wrong.',
life: 3000,
});
} catch (error) {
console.error(`Import from file Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on import from File, check console log.',
life: 3000,
});
}
}, [applySettings]);
const handleExportToClipboard = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
return;
}
try {
await navigator.clipboard.writeText(settings);
toast.current?.show({
severity: 'success',
summary: 'Export',
detail: 'Map settings copied into clipboard',
life: 3000,
});
} catch (error) {
console.error(`Export to clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on copying to clipboard, check console log.',
life: 3000,
});
}
}, [getSettingsForExport]);
const handleExportToFile = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
return;
}
try {
saveTextFile(`map_settings_${map_slug}.json`, settings);
toast.current?.show({
severity: 'success',
summary: 'Export to File',
detail: 'Map settings successfully saved to file',
life: 3000,
});
} catch (error) {
console.error(`Export to cliboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on saving to file, check console log.',
life: 3000,
});
}
}, [getSettingsForExport, map_slug]);
const importItems = useMemo(
() => [
{
label: 'Import from File',
icon: 'pi pi-file-import',
command: handleImportFromFile,
},
],
[handleImportFromFile],
);
const exportItems = useMemo(
() => [
{
label: 'Export as File',
icon: 'pi pi-file-export',
command: handleExportToFile,
},
],
[handleExportToFile],
);
return (
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<SplitButton
onClick={handleImportFromClipboard}
icon="pi pi-download"
size="small"
severity="warning"
label="Import from Clipboard"
className="py-[4px]"
model={importItems}
/>
</div>
<span className="text-stone-500 text-[12px]">
*Will read map settings from clipboard. Be careful it could overwrite current settings.
</span>
</div>
<div className="flex flex-col gap-1">
<div>
<SplitButton
onClick={handleExportToClipboard}
icon="pi pi-upload"
size="small"
label="Export to Clipboard"
className="py-[4px]"
model={exportItems}
/>
</div>
<span className="text-stone-500 text-[12px]">*Will save map settings to clipboard.</span>
</div>
<Toast ref={toast} />
</div>
);
};

View File

@@ -0,0 +1,206 @@
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useCallback, useRef, useState } from 'react';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { Toast } from 'primereact/toast';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { saveTextFile } from '@/hooks/Mapper/utils';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return {
version: -1,
settings: lsSettings ? JSON.parse(lsSettings) : defaultValues,
};
};
export const OldSettingsDialog = () => {
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const toast = useRef<Toast | null>(null);
const {
storedSettings: { checkOldSettings },
data: { map_slug },
} = useMapRootState();
const handleExport = useCallback(
async (asFile?: boolean) => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
const widgetRoutes = localStorage.getItem('window:interface:routes');
const widgetLocal = localStorage.getItem('window:interface:local');
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_5');
const out: MapUserSettings = {
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createSettings(widgetLocal, DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createSettings(widgetsOld, getDefaultWidgetProps()),
routes: createSettings(widgetRoutes, DEFAULT_ROUTES_SETTINGS),
onTheMap: createSettings(onTheMapOld, DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createSettings(signatures, DEFAULT_SIGNATURE_SETTINGS),
interface: createSettings(interfaceSettings, STORED_INTERFACE_DEFAULT_VALUES),
};
if (asFile) {
if (!out) {
return;
}
try {
saveTextFile(`map_settings_${map_slug}.json`, JSON.stringify(out));
toast.current?.show({
severity: 'success',
summary: 'Export to File',
detail: 'Map settings successfully saved to file',
life: 3000,
});
} catch (error) {
console.error(`Export to cliboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on saving to file, check console log.',
life: 3000,
});
return;
}
return;
}
try {
await navigator.clipboard.writeText(JSON.stringify(out));
toast.current?.show({
severity: 'success',
summary: 'Export to clipboard',
detail: 'Map settings was export successfully.',
life: 3000,
});
} catch (error) {
console.error(`Export to clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on copying to clipboard, check console log.',
life: 3000,
});
}
},
[map_slug],
);
const handleExportClipboard = useCallback(async () => {
await handleExport();
}, [handleExport]);
const handleExportAsFile = useCallback(async () => {
await handleExport(true);
}, [handleExport]);
const handleProceed = useCallback(() => {
localStorage.removeItem('window:interface:settings');
localStorage.removeItem('window:interface:routes');
localStorage.removeItem('window:interface:local');
localStorage.removeItem('kills:widget:settings');
localStorage.removeItem('window:onTheMap:settings');
localStorage.removeItem('windows:settings:v2');
localStorage.removeItem('wanderer_system_signature_settings_v6_5');
checkOldSettings();
}, [checkOldSettings]);
return (
<>
<Dialog
header={
<div className="dialog-header">
<span className="pointer-events-none">Old settings detected!</span>
</div>
}
draggable={false}
resizable={false}
closable={false}
visible
onHide={() => null}
className="w-[640px] h-[400px] text-text-color min-h-0"
footer={
<div className="flex items-center justify-end">
<Button
// @ts-ignore
ref={cpRemoveBtnRef}
onClick={handleShowCP}
icon="pi pi-exclamation-triangle"
size="small"
severity="warning"
label="Proceed"
/>
</div>
}
>
<div className="w-full h-full flex flex-col gap-1 items-center justify-center text-stone-400 text-[15px]">
<span>
We detected <span className="text-orange-400">deprecated</span> settings saved in your browser.
</span>
<span>
Now we will give you ability to make <span className="text-orange-400">export</span> your old settings.
</span>
<span>
After click: all settings will saved in your <span className="text-orange-400">clipboard</span>.
</span>
<span>
Then you need to go into <span className="text-orange-400">Map Settings</span> and click{' '}
<span className="text-orange-400">Import from clipboard</span>
</span>
<div className="h-[30px]"></div>
<div className="flex items-center gap-3">
<Button
onClick={handleExportClipboard}
icon="pi pi-copy"
size="small"
severity="info"
label="Export to Clipboard"
/>
<Button
onClick={handleExportAsFile}
icon="pi pi-download"
size="small"
severity="info"
label="Export as File"
/>
</div>
<span className="text-stone-600 text-[12px]">*You will see this dialog until click Export.</span>
</div>
</Dialog>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="After click dialog will disappear. Ready?"
icon="pi pi-exclamation-triangle"
accept={handleProceed}
/>
<Toast ref={toast} />
</>
);
};

View File

@@ -7,24 +7,11 @@ import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virt
import clsx from 'clsx';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { CharacterCard, TooltipPosition, WdCheckbox, WdImageSize, WdImgButton } 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';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
type WindowLocalSettingsType = {
compact: boolean;
hideOffline: boolean;
version: number;
};
const STORED_DEFAULT_VALUES: WindowLocalSettingsType = {
compact: true,
hideOffline: false,
version: 0,
};
const itemTemplate = (item: CharacterTypeRaw & WithIsOwnCharacter, options: VirtualScrollerTemplateOptions) => {
return (
<div
@@ -48,14 +35,11 @@ export interface OnTheMapProps {
export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
const {
data: { characters, userCharacters },
storedSettings: { settingsOnTheMap, settingsOnTheMapUpdate },
} = useMapRootState();
const [searchVal, setSearchVal] = useState('');
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:onTheMap:settings', {
defaultValue: STORED_DEFAULT_VALUES,
});
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
@@ -107,12 +91,12 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
});
}
if (showOffline && !settings.hideOffline) {
if (showOffline && !settingsOnTheMap.hideOffline) {
return out;
}
return out.filter(x => x.online);
}, [showOffline, searchVal, characters, settings.hideOffline, userCharacters]);
}, [showOffline, searchVal, characters, settingsOnTheMap.hideOffline, userCharacters]);
return (
<Sidebar
@@ -153,9 +137,11 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
size="m"
labelSide="left"
label={'Hide offline'}
value={settings.hideOffline}
value={settingsOnTheMap.hideOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, hideOffline: !settings.hideOffline }))}
onChange={() =>
settingsOnTheMapUpdate(() => ({ ...settingsOnTheMap, hideOffline: !settingsOnTheMap.hideOffline }))
}
/>
)}
</div>

View File

@@ -0,0 +1,49 @@
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import useLocalStorageState from 'use-local-storage-state';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const DebugComponent = () => {
const { outCommand } = useMapRootState();
const [record, setRecord] = useLocalStorageState<boolean>('record', {
defaultValue: false,
});
// @ts-ignore
const [recordsList] = useLocalStorageState<{ type; data }[]>('recordsList', {
defaultValue: [],
});
const handleRunSavedEvents = () => {
recordsList.forEach(record => outCommand(record));
};
return (
<>
<WdTooltipWrapper content="Run saved events" 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={handleRunSavedEvents}
disabled={recordsList.length === 0 || record}
>
<i className="pi pi-forward"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Record" 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={() => setRecord(x => !x)}
>
{!record ? (
<i className="pi pi-play-circle text-green-500"></i>
) : (
<i className="pi pi-stop-circle text-red-500"></i>
)}
</button>
</WdTooltipWrapper>
</>
);
};

View File

@@ -7,6 +7,7 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
// import { DebugComponent } from '@/hooks/Mapper/components/mapRootContent/components/RightBar/DebugComponent.tsx';
interface RightBarProps {
onShowOnTheMap?: () => void;
@@ -79,6 +80,9 @@ export const RightBar = ({
</div>
<div className="flex flex-col items-center mb-2 gap-1">
{/* TODO - do not delete this code needs for debug */}
{/*<DebugComponent />*/}
<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"

View File

@@ -48,7 +48,7 @@ export const MapWrapper = () => {
linkSignatureToSystem,
systemSignatures,
},
storedSettings: { interfaceSettings },
storedSettings: { interfaceSettings, settingsLocal },
} = useMapRootState();
const {
@@ -254,6 +254,7 @@ export const MapWrapper = () => {
pings={pings}
onAddSystem={onAddSystem}
minimapPlacement={minimapPosition}
localShowShipName={settingsLocal.showShipName}
/>
{openSettings != null && (

View File

@@ -33,6 +33,7 @@ export enum Regions {
Solitude = 10000044,
TashMurkon = 10000020,
VergeVendor = 10000068,
Pochven = 10000070,
}
export enum Spaces {
@@ -40,6 +41,7 @@ export enum Spaces {
'Gallente' = 'Gallente',
'Matar' = 'Matar',
'Amarr' = 'Amarr',
'Pochven' = 'Pochven',
}
export const REGIONS_MAP: Record<number, Spaces> = {
@@ -66,6 +68,7 @@ export const REGIONS_MAP: Record<number, Spaces> = {
[Regions.Solitude]: Spaces.Gallente,
[Regions.TashMurkon]: Spaces.Amarr,
[Regions.VergeVendor]: Spaces.Gallente,
[Regions.Pochven]: Spaces.Pochven,
};
export type K162Type = {

View File

@@ -0,0 +1,71 @@
import { SignatureGroup, SignatureKind } from '@/hooks/Mapper/types';
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export enum SIGNATURES_DELETION_TIMING {
IMMEDIATE,
DEFAULT,
EXTENDED,
}
export enum SETTINGS_KEYS {
SORT_FIELD = 'sortField',
SORT_ORDER = 'sortOrder',
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
SHOW_CHARACTER_COLUMN = 'show_character_column',
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
DELETION_TIMING = 'deletion_timing',
COLOR_BY_TYPE = 'color_by_type',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
// From SignatureKind
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
COSMIC_SIGNATURE = SignatureKind.CosmicSignature,
DEPLOYABLE = SignatureKind.Deployable,
STRUCTURE = SignatureKind.Structure,
STARBASE = SignatureKind.Starbase,
SHIP = SignatureKind.Ship,
DRONE = SignatureKind.Drone,
// From SignatureGroup
WORMHOLE = SignatureGroup.Wormhole,
RELIC_SITE = SignatureGroup.RelicSite,
DATA_SITE = SignatureGroup.DataSite,
ORE_SITE = SignatureGroup.OreSite,
GAS_SITE = SignatureGroup.GasSite,
COMBAT_SITE = SignatureGroup.CombatSite,
}
export type SignatureSettingsType = { [key in SETTINGS_KEYS]?: unknown };
export const DEFAULT_SIGNATURE_SETTINGS: SignatureSettingsType = {
[SETTINGS_KEYS.SORT_FIELD]: 'inserted_at',
[SETTINGS_KEYS.SORT_ORDER]: -1,
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
[SETTINGS_KEYS.COSMIC_ANOMALY]: true,
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
[SETTINGS_KEYS.DEPLOYABLE]: true,
[SETTINGS_KEYS.STRUCTURE]: true,
[SETTINGS_KEYS.STARBASE]: true,
[SETTINGS_KEYS.SHIP]: true,
[SETTINGS_KEYS.DRONE]: true,
[SETTINGS_KEYS.WORMHOLE]: true,
[SETTINGS_KEYS.RELIC_SITE]: true,
[SETTINGS_KEYS.DATA_SITE]: true,
[SETTINGS_KEYS.ORE_SITE]: true,
[SETTINGS_KEYS.GAS_SITE]: true,
[SETTINGS_KEYS.COMBAT_SITE]: true,
};

View File

@@ -0,0 +1,11 @@
export function getFormattedTime() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds() + 1000).slice(1);
return `${hours}:${minutes}:${seconds} ${ms}`;
}

View File

@@ -1,4 +1,3 @@
export * from './useActualizeSettings';
export * from './useClipboard';
export * from './useHotkey';
export * from './usePageVisibility';

View File

@@ -1,23 +0,0 @@
import { useEffect } from 'react';
type Settings = Record<string, unknown>;
export const useActualizeSettings = <T extends Settings>(defaultVals: T, vals: T, setVals: (newVals: T) => void) => {
useEffect(() => {
let foundNew = false;
const newVals = Object.keys(defaultVals).reduce((acc, x) => {
if (Object.keys(acc).includes(x)) {
return acc;
}
foundNew = true;
// @ts-ignore
return { ...acc, [x]: defaultVals[x] };
}, vals);
if (foundNew) {
setVals(newVals);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

View File

@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react';
function usePageVisibility() {
const [isVisible, setIsVisible] = useState(!document.hidden);
const getIsVisible = () => !document.hidden;
const [isVisible, setIsVisible] = useState(getIsVisible());
useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(!document.hidden);
setIsVisible(getIsVisible());
};
document.addEventListener('visibilitychange', handleVisibilityChange);

View File

@@ -19,10 +19,24 @@ import {
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { DetailedKill } from '../types/kills';
import { InterfaceStoredSettings, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_SETTINGS, STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import {
InterfaceStoredSettings,
KillsWidgetSettings,
LocalWidgetSettings,
MapUserSettings,
OnTheMapSettingsType,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useMapUserSettings } from '@/hooks/Mapper/mapRootProvider/hooks/useMapUserSettings.ts';
import { useGlobalHooks } from '@/hooks/Mapper/mapRootProvider/hooks/useGlobalHooks.ts';
import { DEFAULT_SIGNATURE_SETTINGS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export type MapRootData = MapUnionTypes & {
selectedSystems: string[];
@@ -36,6 +50,7 @@ export type MapRootData = MapUnionTypes & {
};
trackingCharactersData: TrackingCharacter[];
loadingPublicRoutes: boolean;
map_slug: string | null;
};
const INITIAL_DATA: MapRootData = {
@@ -70,6 +85,7 @@ const INITIAL_DATA: MapRootData = {
followingCharacterEveId: null,
pings: [],
loadingPublicRoutes: false,
map_slug: null,
};
export enum InterfaceStoredSettingsProps {
@@ -103,6 +119,19 @@ export interface MapRootContextProps {
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
settingsRoutes: RoutesType;
settingsRoutesUpdate: Dispatch<SetStateAction<RoutesType>>;
settingsLocal: LocalWidgetSettings;
settingsLocalUpdate: Dispatch<SetStateAction<LocalWidgetSettings>>;
settingsSignatures: SignatureSettingsType;
settingsSignaturesUpdate: Dispatch<SetStateAction<SignatureSettingsType>>;
settingsOnTheMap: OnTheMapSettingsType;
settingsOnTheMapUpdate: Dispatch<SetStateAction<OnTheMapSettingsType>>;
settingsKills: KillsWidgetSettings;
settingsKillsUpdate: Dispatch<SetStateAction<KillsWidgetSettings>>;
isReady: boolean;
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
checkOldSettings(): void;
};
}
@@ -134,6 +163,19 @@ const MapRootContext = createContext<MapRootContextProps>({
setInterfaceSettings: () => null,
settingsRoutes: DEFAULT_ROUTES_SETTINGS,
settingsRoutesUpdate: () => null,
settingsLocal: DEFAULT_WIDGET_LOCAL_SETTINGS,
settingsLocalUpdate: () => null,
settingsSignatures: DEFAULT_SIGNATURE_SETTINGS,
settingsSignaturesUpdate: () => null,
settingsOnTheMap: DEFAULT_ON_THE_MAP_SETTINGS,
settingsOnTheMapUpdate: () => null,
settingsKills: DEFAULT_KILLS_WIDGET_SETTINGS,
settingsKillsUpdate: () => null,
isReady: false,
hasOldSettings: false,
getSettingsForExport: () => '',
applySettings: () => false,
checkOldSettings: () => null,
},
});
@@ -154,9 +196,11 @@ const MapRootHandlers = forwardRef(({ children }: WithChildren, fwdRef: Forwarde
export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProviderProps) => {
const { update, ref } = useContextStore<MapRootData>({ ...INITIAL_DATA });
const storedSettings = useMapUserSettings();
const storedSettings = useMapUserSettings(ref);
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } =
useStoreWidgets(storedSettings);
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
const comments = useComments({ outCommand });
const charactersCache = useCharactersCache({ outCommand });

View File

@@ -1,10 +1,18 @@
import {
AvailableThemes,
InterfaceStoredSettings,
KillsWidgetSettings,
LocalWidgetSettings,
MiniMapPlacement,
OnTheMapSettingsType,
PingsPlacement,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
STORED_VISIBLE_WIDGETS_DEFAULT,
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMenu: false,
@@ -31,3 +39,29 @@ export const DEFAULT_ROUTES_SETTINGS: RoutesType = {
avoid_triglavian: false,
avoid: [],
};
export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
hideOffline: false,
version: 0,
};
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
showAll: false,
whOnly: true,
excludedSystems: [],
version: 2,
timeRange: 4,
};
export const getDefaultWidgetProps = () => ({
version: CURRENT_WINDOWS_VERSION,
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
windows: DEFAULT_WIDGETS,
});

View File

@@ -0,0 +1,22 @@
type Settings = Record<string, unknown>;
export const actualizeSettings = <T extends Settings>(defaultVals: T, vals: T, setVals: (newVals: T) => void) => {
let foundNew = false;
const newVals = Object.keys(defaultVals).reduce((acc, key) => {
if (key in acc) {
return acc;
}
foundNew = true;
return {
...acc,
[key]: defaultVals[key],
};
}, vals);
if (foundNew) {
setVals(newVals);
}
};

View File

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

View File

@@ -14,8 +14,8 @@ export const useCommandPings = () => {
ref.current.update({ pings });
}, []);
const pingCancelled = useCallback(({ type, solar_system_id }: CommandPingCancelled) => {
const newPings = ref.current.pings.filter(x => x.solar_system_id !== solar_system_id && x.type !== type);
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => {
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type);
ref.current.update({ pings: newPings });
}, []);

View File

@@ -27,6 +27,7 @@ export const useMapInit = () => {
main_character_eve_id,
following_character_eve_id,
user_hubs,
map_slug,
} = props;
const updateData: Partial<MapRootData> = {};
@@ -98,6 +99,10 @@ export const useMapInit = () => {
updateData.followingCharacterEveId = following_character_eve_id;
}
if ('map_slug' in props) {
updateData.map_slug = map_slug;
}
update(updateData);
},
[update, addSystemStatic],

View File

@@ -1,39 +1,222 @@
import useLocalStorageState from 'use-local-storage-state';
import { InterfaceStoredSettings, RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_ROUTES_SETTINGS, STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useActualizeSettings } from '@/hooks/Mapper/hooks';
import { useEffect } from 'react';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { MapUserSettings, MapUserSettingsStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useCallback, useEffect, useRef, useState } from 'react';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures';
import { MapRootData } from '@/hooks/Mapper/mapRootProvider';
import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/useSettingsValueAndSetter.ts';
import fastDeepEqual from 'fast-deep-equal';
export const useMigrationRoutesSettingsV1 = (update: (upd: RoutesType) => void) => {
//TODO if current Date is more than 01.01.2026 - remove this hook.
// import { actualizeSettings } from '@/hooks/Mapper/mapRootProvider/helpers';
useEffect(() => {
const items = localStorage.getItem(SESSION_KEY.routes);
if (items) {
update(JSON.parse(items));
localStorage.removeItem(SESSION_KEY.routes);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// TODO - we need provide and compare version
const createWidgetSettingsWithVersion = <T>(settings: T) => {
return {
version: 0,
settings,
};
};
export const useMapUserSettings = () => {
const [interfaceSettings, setInterfaceSettings] = useLocalStorageState<InterfaceStoredSettings>(
'window:interface:settings',
{
defaultValue: STORED_INTERFACE_DEFAULT_VALUES,
},
);
const createDefaultWidgetSettings = (): MapUserSettings => {
return {
killsWidget: createWidgetSettingsWithVersion(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettingsWithVersion(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettingsWithVersion(getDefaultWidgetProps()),
routes: createWidgetSettingsWithVersion(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettingsWithVersion(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettingsWithVersion(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettingsWithVersion(STORED_INTERFACE_DEFAULT_VALUES),
};
};
const [settingsRoutes, settingsRoutesUpdate] = useLocalStorageState<RoutesType>('window:interface:routes', {
defaultValue: DEFAULT_ROUTES_SETTINGS,
const EMPTY_OBJ = {};
export const useMapUserSettings = ({ map_slug }: MapRootData) => {
const [isReady, setIsReady] = useState(false);
const [hasOldSettings, setHasOldSettings] = useState(false);
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>('map-user-settings', {
defaultValue: EMPTY_OBJ,
});
useActualizeSettings(STORED_INTERFACE_DEFAULT_VALUES, interfaceSettings, setInterfaceSettings);
useActualizeSettings(DEFAULT_ROUTES_SETTINGS, settingsRoutes, settingsRoutesUpdate);
const ref = useRef({ mapUserSettings, setMapUserSettings, map_slug });
ref.current = { mapUserSettings, setMapUserSettings, map_slug };
useMigrationRoutesSettingsV1(settingsRoutesUpdate);
useEffect(() => {
const { mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug === null) {
return;
}
return { interfaceSettings, setInterfaceSettings, settingsRoutes, settingsRoutesUpdate };
if (!(map_slug in mapUserSettings)) {
setMapUserSettings({
...mapUserSettings,
[map_slug]: createDefaultWidgetSettings(),
});
}
}, [map_slug]);
const [interfaceSettings, setInterfaceSettings] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'interface',
);
const [settingsRoutes, settingsRoutesUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'routes',
);
const [settingsLocal, settingsLocalUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'localWidget',
);
const [settingsSignatures, settingsSignaturesUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'signaturesWidget',
);
const [settingsOnTheMap, settingsOnTheMapUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'onTheMap',
);
const [settingsKills, settingsKillsUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'killsWidget',
);
const [windowsSettings, setWindowsSettings] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'widgets',
);
// HERE we MUST work with migrations
useEffect(() => {
if (isReady) {
return;
}
if (map_slug === null) {
return;
}
if (mapUserSettings[map_slug] == null) {
return;
}
// TODO !!!! FROM this date 06.07.2025 - we must work only with migrations
// actualizeSettings(STORED_INTERFACE_DEFAULT_VALUES, interfaceSettings, setInterfaceSettings);
// actualizeSettings(DEFAULT_ROUTES_SETTINGS, settingsRoutes, settingsRoutesUpdate);
// actualizeSettings(DEFAULT_WIDGET_LOCAL_SETTINGS, settingsLocal, settingsLocalUpdate);
// actualizeSettings(DEFAULT_SIGNATURE_SETTINGS, settingsSignatures, settingsSignaturesUpdate);
// actualizeSettings(DEFAULT_ON_THE_MAP_SETTINGS, settingsOnTheMap, settingsOnTheMapUpdate);
// actualizeSettings(DEFAULT_KILLS_WIDGET_SETTINGS, settingsKills, settingsKillsUpdate);
setIsReady(true);
}, [
map_slug,
mapUserSettings,
interfaceSettings,
setInterfaceSettings,
settingsRoutes,
settingsRoutesUpdate,
settingsLocal,
settingsLocalUpdate,
settingsSignatures,
settingsSignaturesUpdate,
settingsOnTheMap,
settingsOnTheMapUpdate,
settingsKills,
settingsKillsUpdate,
isReady,
]);
const checkOldSettings = useCallback(() => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
const widgetRoutes = localStorage.getItem('window:interface:routes');
const widgetLocal = localStorage.getItem('window:interface:local');
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
setHasOldSettings(!!(widgetsOld || interfaceSettings || widgetRoutes || widgetLocal || widgetKills || onTheMapOld));
}, []);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
const getSettingsForExport = useCallback(() => {
const { map_slug } = ref.current;
if (map_slug == null) {
return;
}
return JSON.stringify(ref.current.mapUserSettings[map_slug]);
}, []);
const applySettings = useCallback((settings: MapUserSettings) => {
const { map_slug, mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug == null) {
return false;
}
if (fastDeepEqual(settings, mapUserSettings[map_slug])) {
return false;
}
setMapUserSettings(old => ({
...old,
[map_slug]: settings,
}));
return true;
}, []);
return {
isReady,
hasOldSettings,
interfaceSettings,
setInterfaceSettings,
settingsRoutes,
settingsRoutesUpdate,
settingsLocal,
settingsLocalUpdate,
settingsSignatures,
settingsSignaturesUpdate,
settingsOnTheMap,
settingsOnTheMapUpdate,
settingsKills,
settingsKillsUpdate,
windowsSettings,
setWindowsSettings,
getSettingsForExport,
applySettings,
checkOldSettings,
};
};

View File

@@ -0,0 +1,60 @@
import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from 'react';
import {
MapUserSettings,
MapUserSettingsStructure,
SettingsWithVersion,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
type ExtractSettings<S extends keyof MapUserSettings> =
MapUserSettings[S] extends SettingsWithVersion<infer U> ? U : never;
type Setter<S extends keyof MapUserSettings> = (
value: Partial<ExtractSettings<S>> | ((prev: ExtractSettings<S>) => Partial<ExtractSettings<S>>),
) => void;
type GenerateSettingsReturn<S extends keyof MapUserSettings> = [ExtractSettings<S>, Setter<S>];
export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
settings: MapUserSettingsStructure,
setSettings: Dispatch<SetStateAction<MapUserSettingsStructure>>,
mapId: string | null,
setting: S,
): GenerateSettingsReturn<S> => {
const data = useMemo<ExtractSettings<S>>(() => {
if (!mapId) return {} as ExtractSettings<S>;
const mapSettings = settings[mapId];
return (mapSettings?.[setting]?.settings ?? ({} as ExtractSettings<S>)) as ExtractSettings<S>;
}, [mapId, setting, settings]);
const refData = useRef({ mapId, setting, setSettings });
refData.current = { mapId, setting, setSettings };
const setter = useCallback<Setter<S>>(value => {
const { mapId, setting, setSettings } = refData.current;
if (!mapId) return;
setSettings(all => {
const currentMap = all[mapId];
const prev = currentMap[setting].settings as ExtractSettings<S>;
const version = currentMap[setting].version;
const patch =
typeof value === 'function' ? (value as (p: ExtractSettings<S>) => Partial<ExtractSettings<S>>)(prev) : value;
return {
...all,
[mapId]: {
...currentMap,
[setting]: {
version,
settings: { ...(prev as any), ...patch } as ExtractSettings<S>,
},
},
};
});
}, []);
return [data, setter];
};

View File

@@ -1,14 +1,8 @@
import useLocalStorageState from 'use-local-storage-state';
import {
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
STORED_VISIBLE_WIDGETS_DEFAULT,
WidgetsIds,
WINDOWS_LOCAL_STORE_KEY,
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { DEFAULT_WIDGETS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import { useCallback, useEffect, useRef } from 'react';
import { /*SNAP_GAP,*/ WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { Dispatch, SetStateAction, useCallback, useRef } from 'react';
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { getDefaultWidgetProps } from '@/hooks/Mapper/mapRootProvider/constants.ts';
export type StoredWindowProps = Omit<WindowProps, 'content'>;
export type WindowStoreInfo = {
@@ -20,17 +14,12 @@ export type WindowStoreInfo = {
// export type UpdateWidgetSettingsFunc = (widgets: WindowProps[]) => void;
export type ToggleWidgetVisibility = (widgetId: WidgetsIds) => void;
export const getDefaultWidgetProps = () => ({
version: CURRENT_WINDOWS_VERSION,
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
windows: DEFAULT_WIDGETS,
});
export const useStoreWidgets = () => {
const [windowsSettings, setWindowsSettings] = useLocalStorageState<WindowStoreInfo>(WINDOWS_LOCAL_STORE_KEY, {
defaultValue: getDefaultWidgetProps(),
});
interface UseStoreWidgetsProps {
windowsSettings: WindowStoreInfo;
setWindowsSettings: Dispatch<SetStateAction<WindowStoreInfo>>;
}
export const useStoreWidgets = ({ windowsSettings, setWindowsSettings }: UseStoreWidgetsProps) => {
const ref = useRef({ windowsSettings, setWindowsSettings });
ref.current = { windowsSettings, setWindowsSettings };
@@ -83,33 +72,6 @@ export const useStoreWidgets = () => {
});
}, []);
useEffect(() => {
const { setWindowsSettings } = ref.current;
const raw = localStorage.getItem(WINDOWS_LOCAL_STORE_KEY);
if (!raw) {
console.warn('No windows found in local storage!!');
setWindowsSettings(getDefaultWidgetProps());
return;
}
const { version, windows, visible, viewPort } = JSON.parse(raw) as WindowStoreInfo;
if (!version || CURRENT_WINDOWS_VERSION > version) {
setWindowsSettings(getDefaultWidgetProps());
}
// eslint-disable-next-line no-debugger
const out = windows.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id));
setWindowsSettings({
version: CURRENT_WINDOWS_VERSION,
windows: out as WindowProps[],
visible,
viewPort,
});
}, []);
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
return {

View File

@@ -1,3 +1,6 @@
import { WindowStoreInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export enum AvailableThemes {
default = 'default',
pathfinder = 'pathfinder',
@@ -43,3 +46,42 @@ export type RoutesType = {
avoid_triglavian: boolean;
avoid: number[];
};
export type LocalWidgetSettings = {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
};
export type OnTheMapSettingsType = {
hideOffline: boolean;
version: number;
};
export type KillsWidgetSettings = {
showAll: boolean;
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
};
export type SettingsWithVersion<T> = {
version: number;
settings: T;
};
export type MapUserSettings = {
widgets: SettingsWithVersion<WindowStoreInfo>;
interface: SettingsWithVersion<InterfaceStoredSettings>;
onTheMap: SettingsWithVersion<OnTheMapSettingsType>;
routes: SettingsWithVersion<RoutesType>;
localWidget: SettingsWithVersion<LocalWidgetSettings>;
signaturesWidget: SettingsWithVersion<SignatureSettingsType>;
killsWidget: SettingsWithVersion<KillsWidgetSettings>;
};
export type MapUserSettingsStructure = {
[mapId: string]: MapUserSettings;
};

View File

@@ -97,6 +97,7 @@ export type CommandInit = {
is_subscription_active?: boolean;
main_character_eve_id?: string | null;
following_character_eve_id?: string | null;
map_slug?: string;
};
export type CommandAddSystems = SolarSystemRawType[];
@@ -150,7 +151,7 @@ export type CommandUpdateTracking = {
follow: boolean;
};
export type CommandPingAdded = PingData[];
export type CommandPingCancelled = Pick<PingData, 'type' | 'solar_system_id'>;
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
export interface UserSettings {
primaryCharacterId?: string;

View File

@@ -4,6 +4,7 @@ export enum PingType {
}
export type PingData = {
id: string;
inserted_at: number;
character_eve_id: string;
solar_system_id: string;

View File

@@ -1,27 +1,121 @@
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { RefObject, useCallback } from 'react';
import { RefObject, useCallback, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
// const inIndex = 0;
// const prevEventTime = +new Date();
const LAST_VERSION_KEY = 'wandererLastVersion';
// @ts-ignore
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
const visible = usePageVisibility();
const wasHiddenOnce = useRef(false);
const visibleRef = useRef(visible);
visibleRef.current = visible;
// TODO - do not delete THIS code it needs for debug
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
// defaultValue: false,
// });
// const [recordsList, setRecordsList] = useLocalStorageState<{ type; data }[]>('recordsList', {
// defaultValue: [],
// });
//
// const ref = useRef({ record, setRecord, recordsList, setRecordsList });
// ref.current = { record, setRecord, recordsList, setRecordsList };
//
// const recordBufferRef = useRef<{ type; data }[]>([]);
// useEffect(() => {
// if (record || recordBufferRef.current.length === 0) {
// return;
// }
//
// ref.current.setRecordsList([...recordBufferRef.current]);
// recordBufferRef.current = [];
// }, [record]);
const handleCommand = useCallback(
// @ts-ignore
async ({ type, data }) => {
if (!hooksRef.current) {
return;
}
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `OUT`, ref.current.record, { type, data });
// if (ref.current.record) {
// recordBufferRef.current.push({ type, data });
// }
// 'ui_loaded'
return await hooksRef.current.pushEventAsync(type, data);
},
[hooksRef.current],
);
const handleMapEvent = useCallback(({ type, body }) => {
handlerRefs.forEach(ref => {
if (!ref.current) {
// @ts-ignore
const eventsBufferRef = useRef<{ type; body }[]>([]);
const eventTick = useCallback(
debounce(() => {
if (eventsBufferRef.current.length === 0) {
return;
}
ref.current?.command(type, body);
});
const { type, body } = eventsBufferRef.current.shift()!;
handlerRefs.forEach(ref => {
if (!ref.current) {
return;
}
ref.current?.command(type, body);
});
// TODO - do not delete THIS code it needs for debug
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
if (eventsBufferRef.current.length > 0) {
eventTick();
}
}, 10),
[],
);
const eventTickRef = useRef(eventTick);
eventTickRef.current = eventTick;
// @ts-ignore
const handleMapEvent = useCallback(({ type, body }) => {
// TODO - do not delete THIS code it needs for debug
// const currentTime = +new Date();
// const timeDiff = currentTime - prevEventTime;
// prevEventTime = currentTime;
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
if (!eventTickRef.current || !visibleRef.current) {
return;
}
eventsBufferRef.current.push({ type, body });
eventTickRef.current();
}, []);
useEffect(() => {
if (!visible && !wasHiddenOnce.current) {
wasHiddenOnce.current = true;
return;
}
if (!wasHiddenOnce.current) {
return;
}
if (!visible) {
return;
}
hooksRef.current.pushEventAsync('ui_loaded', { version: localStorage.getItem(LAST_VERSION_KEY) });
}, [hooksRef.current, visible]);
return { handleCommand, handleMapEvent };
};

View File

@@ -1,2 +1,4 @@
export * from './contextStore';
export * from './getQueryVariable';
export * from './loadTextFile';
export * from './saveToFile';

View File

@@ -0,0 +1,27 @@
export function loadTextFile(): Promise<string> {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsText(file);
};
input.click();
});
}

View File

@@ -0,0 +1,33 @@
export function saveTextFile(filename: string, content: string) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export async function saveTextFileInteractive(filename: string, content: string) {
if (!('showSaveFilePicker' in window)) {
throw new Error('File System Access API is not supported in this browser.');
}
const handle = await (window as any).showSaveFilePicker({
suggestedName: filename,
types: [
{
description: 'Text Files',
accept: { 'text/plain': ['.txt', '.json'] },
},
],
});
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -84,9 +84,9 @@ map_subscription_base_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_BASE_PRICE", 100_000_000)
map_subscription_extra_characters_100_price =
map_subscription_extra_characters_50_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_100_PRICE", 50_000_000)
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_50_PRICE", 50_000_000)
map_subscription_extra_hubs_10_price =
config_dir
@@ -167,7 +167,7 @@ config :wanderer_app,
month_12_discount: 0.5
}
],
extra_characters_100: map_subscription_extra_characters_100_price,
extra_characters_50: map_subscription_extra_characters_50_price,
extra_hubs_10: map_subscription_extra_hubs_10_price
}

View File

@@ -14,6 +14,11 @@ defmodule WandererApp.Api.MapPing do
define(:new, action: :new)
define(:destroy, action: :destroy)
define(:by_id,
get_by: [:id],
action: :read
)
define(:by_map,
action: :by_map
)

View File

@@ -5,25 +5,24 @@ defmodule WandererApp.Api.MapSystemStructure do
"""
@derive {Jason.Encoder,
only: [
:id,
:system_id,
:solar_system_id,
:solar_system_name,
:structure_type_id,
:structure_type,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time,
:inserted_at,
:updated_at
]
}
only: [
:id,
:system_id,
:solar_system_id,
:solar_system_name,
:structure_type_id,
:structure_type,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time,
:inserted_at,
:updated_at
]}
use Ash.Resource,
domain: WandererApp.Api,
@@ -100,10 +99,9 @@ defmodule WandererApp.Api.MapSystemStructure do
argument :system_id, :uuid, allow_nil?: false
change manage_relationship(:system_id, :system,
on_lookup: :relate,
on_no_match: nil
)
on_lookup: :relate,
on_no_match: nil
)
end
update :update do
@@ -125,9 +123,7 @@ defmodule WandererApp.Api.MapSystemStructure do
:status,
:end_time
]
end
end
attributes do

View File

@@ -93,7 +93,7 @@ defmodule WandererApp.Application do
wanderer_kills_enabled =
Application.get_env(:wanderer_app, :wanderer_kills_service_enabled, false)
if wanderer_kills_enabled in [true, :true, "true"] do
if wanderer_kills_enabled in [true, true, "true"] do
Logger.info("Starting WandererKills service integration...")
[

View File

@@ -50,8 +50,8 @@ defmodule WandererApp.Character.Activity do
def process_character_activity(map_id, current_user) do
with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
raw_activity <- WandererApp.Map.get_character_activity(map_id),
{:ok, user_characters} <- WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
{:ok, user_characters} <-
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
result = process_activity_data(raw_activity, map_user_settings, user_characters)
result
end
@@ -61,6 +61,7 @@ defmodule WandererApp.Character.Activity do
case WandererApp.MapUserSettingsRepo.get(map_id, user_id) do
{:ok, settings} when not is_nil(settings) ->
{:ok, settings}
_ ->
{:ok, %{main_character_eve_id: nil}}
end
@@ -98,17 +99,24 @@ defmodule WandererApp.Character.Activity do
|> sort_by_timestamp()
end
defp process_user_activity(user_id, user_activities, %{user_id: user_id, main_character_eve_id: main_id} = _map_user_settings, all_characters)
defp process_user_activity(
user_id,
user_activities,
%{user_id: user_id, main_character_eve_id: main_id} = _map_user_settings,
all_characters
)
when not is_nil(main_id) do
# Group activities by character
activities_by_character = group_activities_by_character(user_activities)
main_id_str = to_string(main_id)
display_character = case Enum.find(all_characters, &(to_string(&1.eve_id) == main_id_str)) do
nil -> find_most_active_character_details(activities_by_character) # Fall back to most active
main_char -> main_char
end
display_character =
case Enum.find(all_characters, &(to_string(&1.eve_id) == main_id_str)) do
# Fall back to most active
nil -> find_most_active_character_details(activities_by_character)
main_char -> main_char
end
build_activity_entry_if_valid(display_character, activities_by_character, user_id)
end
@@ -147,7 +155,8 @@ defmodule WandererApp.Character.Activity do
# Find the details of the most active character
defp find_most_active_character_details(activities_by_character) do
with most_active_id when not is_nil(most_active_id) <- find_most_active_character(activities_by_character),
with most_active_id when not is_nil(most_active_id) <-
find_most_active_character(activities_by_character),
most_active_activities <- Map.get(activities_by_character, most_active_id, []),
[first_activity | _] <- most_active_activities,
character when not is_nil(character) <- Map.get(first_activity, :character) do
@@ -168,13 +177,15 @@ defmodule WandererApp.Character.Activity do
# Only create entry if there's at least some activity
if all_passages + all_connections + all_signatures > 0 do
[%{
character: character,
passages: all_passages,
connections: all_connections,
signatures: all_signatures,
timestamp: get_latest_timestamp(activities_by_character)
}]
[
%{
character: character,
passages: all_passages,
connections: all_connections,
signatures: all_signatures,
timestamp: get_latest_timestamp(activities_by_character)
}
]
else
Logger.warning("Character has no activity, not creating entry")
[]

View File

@@ -71,7 +71,10 @@ defmodule WandererApp.Character.TransactionsTracker do
@impl true
def handle_info(:shutdown, %Impl{} = state) do
Logger.debug(fn -> "Shutting down character transaction tracker: #{inspect(state.character_id)}" end)
Logger.debug(fn ->
"Shutting down character transaction tracker: #{inspect(state.character_id)}"
end)
{:stop, :normal, state}
end

View File

@@ -26,7 +26,6 @@ defmodule WandererApp.Esi do
defdelegate get_killmail(killmail_id, killmail_hash, opts \\ []), to: WandererApp.Esi.ApiClient
defdelegate set_autopilot_waypoint(
add_to_beginning,
clear_other_waypoints,

View File

@@ -1,55 +1,61 @@
defmodule WandererApp.Kills do
@moduledoc """
Main interface for the WandererKills integration subsystem.
Provides high-level functions for monitoring and managing the kills
data pipeline, including connection status, health metrics, and
system subscriptions.
"""
alias WandererApp.Kills.{Client, Storage}
@doc """
Gets comprehensive status of the kills subsystem.
"""
@spec get_status() :: {:ok, map()} | {:error, term()}
def get_status do
with {:ok, client_status} <- Client.get_status() do
{:ok, %{
enabled: Application.get_env(:wanderer_app, :wanderer_kills_service_enabled, false),
client: client_status,
websocket_url: Application.get_env(:wanderer_app, :wanderer_kills_base_url, "ws://wanderer-kills:4004")
}}
{:ok,
%{
enabled: Application.get_env(:wanderer_app, :wanderer_kills_service_enabled, false),
client: client_status,
websocket_url:
Application.get_env(
:wanderer_app,
:wanderer_kills_base_url,
"ws://wanderer-kills:4004"
)
}}
end
end
@doc """
Subscribes to killmail updates for specified systems.
"""
@spec subscribe_systems([integer()]) :: :ok | {:error, term()}
defdelegate subscribe_systems(system_ids), to: Client, as: :subscribe_to_systems
@doc """
Unsubscribes from killmail updates for specified systems.
"""
@spec unsubscribe_systems([integer()]) :: :ok | {:error, term()}
defdelegate unsubscribe_systems(system_ids), to: Client, as: :unsubscribe_from_systems
@doc """
Gets kill count for a specific system.
"""
@spec get_system_kill_count(integer()) :: {:ok, non_neg_integer()} | {:error, :not_found}
defdelegate get_system_kill_count(system_id), to: Storage, as: :get_kill_count
@doc """
Gets recent kills for a specific system.
"""
@spec get_system_kills(integer()) :: {:ok, list(map())} | {:error, :not_found}
defdelegate get_system_kills(system_id), to: Storage
@doc """
Manually triggers a reconnection attempt.
"""
@spec reconnect() :: :ok | {:error, term()}
defdelegate reconnect(), to: Client
end
end

View File

@@ -15,8 +15,10 @@ defmodule WandererApp.Kills.Client do
# Simple retry configuration - inline like character module
@retry_delays [5_000, 10_000, 30_000, 60_000]
@max_retries 10
@health_check_interval :timer.seconds(30) # Check every 30 seconds
@message_timeout :timer.minutes(15) # No messages timeout
# Check every 30 seconds
@health_check_interval :timer.seconds(30)
# No messages timeout
@message_timeout :timer.minutes(15)
defstruct [
:socket_pid,
@@ -165,7 +167,8 @@ defmodule WandererApp.Kills.Client do
| connected: true,
connecting: false,
socket_pid: socket_pid,
retry_count: 0, # Reset retry count only on successful connection
# Reset retry count only on successful connection
retry_count: 0,
last_error: nil,
last_message_time: System.system_time(:millisecond)
}
@@ -181,68 +184,78 @@ defmodule WandererApp.Kills.Client do
end
def handle_info({:disconnected, reason}, state) do
Logger.warning("[Client] WebSocket disconnected: #{inspect(reason)} (was connected: #{state.connected}, was connecting: #{state.connecting})")
Logger.warning(
"[Client] WebSocket disconnected: #{inspect(reason)} (was connected: #{state.connected}, was connecting: #{state.connecting})"
)
# Cancel connection timeout if pending
state = cancel_connection_timeout(state)
state =
%{state |
connected: false,
connecting: false,
socket_pid: nil,
last_error: reason
}
%{state | connected: false, connecting: false, socket_pid: nil, last_error: reason}
if should_retry?(state) do
{:noreply, schedule_retry(state)}
else
Logger.error("[Client] Max retry attempts (#{@max_retries}) reached. Will not retry automatically.")
Logger.error(
"[Client] Max retry attempts (#{@max_retries}) reached. Will not retry automatically."
)
{:noreply, state}
end
end
def handle_info(:health_check, state) do
health_status = check_health(state)
new_state = case health_status do
:healthy ->
state
:needs_reconnect ->
Logger.warning("[Client] Connection unhealthy, triggering reconnect (retry count: #{state.retry_count})")
# Don't reset retry count during health check failures
if state.connected or state.connecting do
send(self(), {:disconnected, :health_check_failed})
%{state | connected: false, connecting: false, socket_pid: nil}
else
# Already disconnected, just maintain state
new_state =
case health_status do
:healthy ->
state
end
:needs_reconnect_with_timestamp ->
Logger.warning("[Client] Health check triggering reconnect (retry count: #{state.retry_count})")
new_state = %{state | last_health_reconnect_attempt: System.system_time(:millisecond)}
if state.connected or state.connecting do
send(self(), {:disconnected, :health_check_failed})
%{new_state | connected: false, connecting: false, socket_pid: nil}
else
# Already disconnected, trigger reconnect
send(self(), :connect)
new_state
end
:needs_reconnect ->
Logger.warning(
"[Client] Connection unhealthy, triggering reconnect (retry count: #{state.retry_count})"
)
:needs_reconnect_reset_retries ->
Logger.warning("[Client] Health check resetting retry count and triggering reconnect")
new_state = %{state | retry_count: 0, last_retry_cycle_end: nil}
if state.connected or state.connecting do
send(self(), {:disconnected, :health_check_failed})
%{new_state | connected: false, connecting: false, socket_pid: nil}
else
# Already disconnected, trigger immediate reconnect with reset count
send(self(), :connect)
new_state
end
end
# Don't reset retry count during health check failures
if state.connected or state.connecting do
send(self(), {:disconnected, :health_check_failed})
%{state | connected: false, connecting: false, socket_pid: nil}
else
# Already disconnected, just maintain state
state
end
:needs_reconnect_with_timestamp ->
Logger.warning(
"[Client] Health check triggering reconnect (retry count: #{state.retry_count})"
)
new_state = %{state | last_health_reconnect_attempt: System.system_time(:millisecond)}
if state.connected or state.connecting do
send(self(), {:disconnected, :health_check_failed})
%{new_state | connected: false, connecting: false, socket_pid: nil}
else
# Already disconnected, trigger reconnect
send(self(), :connect)
new_state
end
:needs_reconnect_reset_retries ->
Logger.warning("[Client] Health check resetting retry count and triggering reconnect")
new_state = %{state | retry_count: 0, last_retry_cycle_end: nil}
if state.connected or state.connecting do
send(self(), {:disconnected, :health_check_failed})
%{new_state | connected: false, connecting: false, socket_pid: nil}
else
# Already disconnected, trigger immediate reconnect with reset count
send(self(), :connect)
new_state
end
end
schedule_health_check()
{:noreply, new_state}
@@ -261,7 +274,9 @@ defmodule WandererApp.Kills.Client do
end
def handle_info({:connection_timeout, socket_pid}, %{socket_pid: socket_pid} = state) do
Logger.error("[Client] Connection timeout - socket process failed to connect within 10s (retry #{state.retry_count}/#{@max_retries})")
Logger.error(
"[Client] Connection timeout - socket process failed to connect within 10s (retry #{state.retry_count}/#{@max_retries})"
)
# Kill the socket process if it's still alive
if socket_alive?(socket_pid) do
@@ -303,9 +318,9 @@ defmodule WandererApp.Kills.Client do
Logger.debug(fn ->
"[Client] Subscribing to #{length(to_subscribe)} new systems. " <>
"Total subscribed: #{MapSet.size(updated_systems)}. " <>
"Map breakdown: #{inspect(map_info)}" end
)
"Total subscribed: #{MapSet.size(updated_systems)}. " <>
"Map breakdown: #{inspect(map_info)}"
end)
end
if length(to_subscribe) > 0 and state.socket_pid do
@@ -318,6 +333,7 @@ defmodule WandererApp.Kills.Client do
def handle_cast({:unsubscribe_systems, system_ids}, state) do
{updated_systems, to_unsubscribe} =
Manager.unsubscribe_systems(state.subscribed_systems, system_ids)
if length(to_unsubscribe) > 0 and state.socket_pid do
Manager.sync_with_server(state.socket_pid, [], to_unsubscribe)
end
@@ -354,7 +370,8 @@ defmodule WandererApp.Kills.Client do
| connected: false,
connecting: false,
socket_pid: nil,
retry_count: 0, # Manual reconnect resets retry count
# Manual reconnect resets retry count
retry_count: 0,
last_error: nil
}
@@ -378,10 +395,12 @@ defmodule WandererApp.Kills.Client do
defp connect_to_server do
url = Config.server_url()
systems =
case MapIntegration.get_tracked_system_ids() do
{:ok, system_list} ->
system_list
{:error, reason} ->
Logger.warning(
"[Client] Failed to get tracked system IDs for initial subscription: #{inspect(reason)}, will retry after connection"
@@ -402,9 +421,11 @@ defmodule WandererApp.Kills.Client do
# GenSocketClient expects transport_opts to be wrapped in a specific format
opts = [
transport_opts: [
timeout: 10_000, # 10 second connection timeout
# 10 second connection timeout
timeout: 10_000,
tcp_opts: [
connect_timeout: 10_000, # TCP connection timeout
# TCP connection timeout
connect_timeout: 10_000,
send_timeout: 5_000,
recv_timeout: 5_000
]
@@ -430,6 +451,7 @@ defmodule WandererApp.Kills.Client do
defp should_retry?(_), do: true
defp should_start_new_retry_cycle?(%{last_retry_cycle_end: nil}), do: true
defp should_start_new_retry_cycle?(%{last_retry_cycle_end: end_time}) do
System.system_time(:millisecond) - end_time >= @message_timeout
end
@@ -437,8 +459,9 @@ defmodule WandererApp.Kills.Client do
# Prevent health check from triggering reconnects too frequently
# Allow health check reconnects only every 2 minutes to avoid spam
@health_check_reconnect_cooldown :timer.minutes(2)
defp should_health_check_reconnect?(%{last_health_reconnect_attempt: nil}), do: true
defp should_health_check_reconnect?(%{last_health_reconnect_attempt: last_attempt}) do
System.system_time(:millisecond) - last_attempt >= @health_check_reconnect_cooldown
end
@@ -449,14 +472,15 @@ defmodule WandererApp.Kills.Client do
# Increment retry count first
new_retry_count = state.retry_count + 1
# If we've hit max retries, mark the end of this retry cycle
state = if new_retry_count >= @max_retries do
%{state | last_retry_cycle_end: System.system_time(:millisecond)}
else
state
end
state =
if new_retry_count >= @max_retries do
%{state | last_retry_cycle_end: System.system_time(:millisecond)}
else
state
end
delay = Enum.at(@retry_delays, min(state.retry_count, length(@retry_delays) - 1))
timer_ref = Process.send_after(self(), :retry_connection, delay)
@@ -478,11 +502,13 @@ defmodule WandererApp.Kills.Client do
end
defp check_health(%{connecting: true} = _state) do
:healthy # Don't interfere with ongoing connection attempts
# Don't interfere with ongoing connection attempts
:healthy
end
defp check_health(%{connected: false, retry_timer_ref: ref} = _state) when not is_nil(ref) do
:healthy # Don't interfere with scheduled retries
# Don't interfere with scheduled retries
:healthy
end
defp check_health(%{connected: false} = state) do
@@ -491,7 +517,8 @@ defmodule WandererApp.Kills.Client do
if should_health_check_reconnect?(state) do
:needs_reconnect_with_timestamp
else
:healthy # Recent health check reconnect attempt
# Recent health check reconnect attempt
:healthy
end
else
# Max retries reached, check if 15 minutes have passed since last retry cycle
@@ -499,7 +526,8 @@ defmodule WandererApp.Kills.Client do
Logger.info("[Client] 15 minutes elapsed since max retries, starting new retry cycle")
:needs_reconnect_reset_retries
else
:healthy # Still within 15-minute cooldown period
# Still within 15-minute cooldown period
:healthy
end
end
end
@@ -515,17 +543,21 @@ defmodule WandererApp.Kills.Client do
end
end
defp check_health(%{socket_pid: pid, last_message_time: last_msg_time} = state) when not is_nil(pid) and not is_nil(last_msg_time) do
defp check_health(%{socket_pid: pid, last_message_time: last_msg_time} = state)
when not is_nil(pid) and not is_nil(last_msg_time) do
cond do
not socket_alive?(pid) ->
Logger.warning("[Client] Health check: Socket process #{inspect(pid)} is dead")
:needs_reconnect
# Check if we haven't received a message in the configured timeout
System.system_time(:millisecond) - last_msg_time > @message_timeout ->
Logger.warning("[Client] Health check: No messages received for 15+ minutes, reconnecting")
Logger.warning(
"[Client] Health check: No messages received for 15+ minutes, reconnecting"
)
:needs_reconnect
true ->
:healthy
end
@@ -571,7 +603,6 @@ defmodule WandererApp.Kills.Client do
send(self(), {:disconnected, :connection_lost})
end
# Handler module for WebSocket events
defmodule Handler do
@moduledoc """
@@ -591,8 +622,10 @@ defmodule WandererApp.Kills.Client do
# Configure with heartbeat interval (Phoenix default is 30s)
params = [
{"vsn", "2.0.0"},
{"heartbeat", "30000"} # 30 second heartbeat
# 30 second heartbeat
{"heartbeat", "30000"}
]
{:connect, ws_url, params, state}
end
@@ -644,7 +677,7 @@ defmodule WandererApp.Kills.Client do
{"killmails:lobby", "killmail_update"} ->
# Notify parent that we received a message
send(state.parent, {:message_received, :killmail_update})
# Use supervised task to handle failures gracefully
Task.Supervisor.start_child(
WandererApp.Kills.TaskSupervisor,
@@ -654,7 +687,7 @@ defmodule WandererApp.Kills.Client do
{"killmails:lobby", "kill_count_update"} ->
# Notify parent that we received a message
send(state.parent, {:message_received, :kill_count_update})
# Use supervised task to handle failures gracefully
Task.Supervisor.start_child(
WandererApp.Kills.TaskSupervisor,
@@ -677,6 +710,7 @@ defmodule WandererApp.Kills.Client do
case push_to_channel(transport, "subscribe_systems", %{"systems" => system_ids}) do
:ok ->
Logger.debug(fn -> "[Handler] Successfully pushed subscribe_systems event" end)
error ->
Logger.error("[Handler] Failed to push subscribe_systems: #{inspect(error)}")
end
@@ -689,6 +723,7 @@ defmodule WandererApp.Kills.Client do
case push_to_channel(transport, "unsubscribe_systems", %{"systems" => system_ids}) do
:ok ->
Logger.debug(fn -> "[Handler] Successfully pushed unsubscribe_systems event" end)
error ->
Logger.error("[Handler] Failed to push unsubscribe_systems: #{inspect(error)}")
end
@@ -720,12 +755,15 @@ defmodule WandererApp.Kills.Client do
end
defp push_to_channel(transport, event, payload) do
Logger.debug(fn -> "[Handler] Pushing event '#{event}' with payload: #{inspect(payload)}" end)
Logger.debug(fn ->
"[Handler] Pushing event '#{event}' with payload: #{inspect(payload)}"
end)
case GenSocketClient.push(transport, "killmails:lobby", event, payload) do
{:ok, ref} ->
Logger.debug(fn -> "[Handler] Push successful, ref: #{inspect(ref)}" end)
:ok
error ->
Logger.error("[Handler] Push failed: #{inspect(error)}")
error
@@ -775,6 +813,7 @@ defmodule WandererApp.Kills.Client do
system_ids
|> Enum.reduce(%{}, fn system_id, acc ->
maps = WandererApp.Kills.Subscription.SystemMapIndex.get_maps_for_system(system_id)
Enum.reduce(maps, acc, fn map_id, inner_acc ->
Map.update(inner_acc, map_id, 1, &(&1 + 1))
end)

View File

@@ -71,7 +71,10 @@ defmodule WandererApp.Kills.MapEventListener do
end
def handle_info({:systems_removed, system_ids}, state) do
Logger.debug(fn -> "[MapEventListener] Systems removed (alt format): #{length(system_ids)} systems" end)
Logger.debug(fn ->
"[MapEventListener] Systems removed (alt format): #{length(system_ids)} systems"
end)
# Track pending removals so we can handle them immediately
new_pending_removals = MapSet.union(state.pending_removals, MapSet.new(system_ids))
{:noreply, schedule_subscription_update(%{state | pending_removals: new_pending_removals})}
@@ -106,18 +109,21 @@ defmodule WandererApp.Kills.MapEventListener do
def handle_info(:resubscribe_to_maps, state) do
running_maps = WandererApp.Map.RegistryHelper.list_all_maps()
current_running_map_ids = MapSet.new(Enum.map(running_maps, & &1.id))
Logger.debug(fn ->
"[MapEventListener] Resubscribing to maps. Running maps: #{MapSet.size(current_running_map_ids)}"
end)
# Unsubscribe from maps no longer running
maps_to_unsubscribe = MapSet.difference(state.subscribed_maps, current_running_map_ids)
Enum.each(maps_to_unsubscribe, fn map_id ->
Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id)
end)
# Subscribe to new running maps
maps_to_subscribe = MapSet.difference(current_running_map_ids, state.subscribed_maps)
Enum.each(maps_to_subscribe, fn map_id ->
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
end)
@@ -232,9 +238,10 @@ defmodule WandererApp.Kills.MapEventListener do
defp apply_subscription_changes(current_systems, pending_removals) do
current_set = MapSet.new(current_systems)
Logger.debug(fn ->
"[MapEventListener] Current subscriptions: #{MapSet.size(current_set)} systems, " <>
"Pending removals: #{MapSet.size(pending_removals)} systems"
"Pending removals: #{MapSet.size(pending_removals)} systems"
end)
# Use get_tracked_system_ids to get only systems from running maps
@@ -252,9 +259,10 @@ defmodule WandererApp.Kills.MapEventListener do
# Remove pending removals from tracked_systems since DB might not be updated yet
tracked_systems_adjusted = MapSet.difference(tracked_systems_set, pending_removals)
Logger.debug(fn ->
"[MapEventListener] Tracked systems from maps: #{MapSet.size(tracked_systems_set)}, " <>
"After removing pending: #{MapSet.size(tracked_systems_adjusted)}"
"After removing pending: #{MapSet.size(tracked_systems_adjusted)}"
end)
# Use the existing MapIntegration logic to determine changes
@@ -266,12 +274,18 @@ defmodule WandererApp.Kills.MapEventListener do
# Apply the changes
if to_subscribe != [] do
Logger.debug(fn -> "[MapEventListener] Triggering subscription for #{length(to_subscribe)} systems" end)
Logger.debug(fn ->
"[MapEventListener] Triggering subscription for #{length(to_subscribe)} systems"
end)
Client.subscribe_to_systems(to_subscribe)
end
if to_unsubscribe != [] do
Logger.debug(fn -> "[MapEventListener] Triggering unsubscription for #{length(to_unsubscribe)} systems" end)
Logger.debug(fn ->
"[MapEventListener] Triggering unsubscription for #{length(to_unsubscribe)} systems"
end)
Client.unsubscribe_from_systems(to_unsubscribe)
end
end

View File

@@ -187,13 +187,11 @@ defmodule WandererApp.Kills.MessageHandler do
# Pattern match on flat format - already adapted
defp adapt_kill_data(%{"victim_char_id" => _} = kill) do
validated_kill = validate_flat_format_kill(kill)
if map_size(validated_kill) > 0 do
{:ok, validated_kill}
else
Logger.warning(
"[MessageHandler] Invalid flat format kill: #{inspect(kill["killmail_id"])}"
)
Logger.warning("[MessageHandler] Invalid flat format kill: #{inspect(kill["killmail_id"])}")
{:error, :invalid_data}
end
end
@@ -219,7 +217,7 @@ defmodule WandererApp.Kills.MessageHandler do
|> Map.delete("system_id")
adapted_kill = adapt_nested_format_kill(normalized_kill)
if map_size(adapted_kill) > 0 do
{:ok, adapted_kill}
else
@@ -410,6 +408,7 @@ defmodule WandererApp.Kills.MessageHandler do
defp get_character_name(data) when is_map(data) do
# Try multiple possible field names
field_names = ["attacker_name", "victim_name", "character_name", "name"]
extract_field(data, field_names) ||
case Map.get(data, "character") do
%{"name" => name} when is_binary(name) -> name
@@ -420,33 +419,38 @@ defmodule WandererApp.Kills.MessageHandler do
defp get_character_name(_), do: nil
@spec get_corp_ticker(map() | any()) :: String.t() | nil
defp get_corp_ticker(data) when is_map(data) do
defp get_corp_ticker(data) when is_map(data) do
extract_field(data, ["corporation_ticker", "corp_ticker"])
end
defp get_corp_ticker(_), do: nil
@spec get_corp_name(map() | any()) :: String.t() | nil
defp get_corp_name(data) when is_map(data) do
extract_field(data, ["corporation_name", "corp_name"])
end
defp get_corp_name(_), do: nil
@spec get_alliance_ticker(map() | any()) :: String.t() | nil
defp get_alliance_ticker(data) when is_map(data) do
extract_field(data, ["alliance_ticker"])
end
defp get_alliance_ticker(_), do: nil
@spec get_alliance_name(map() | any()) :: String.t() | nil
defp get_alliance_name(data) when is_map(data) do
extract_field(data, ["alliance_name"])
end
defp get_alliance_name(_), do: nil
@spec get_ship_name(map() | any()) :: String.t() | nil
defp get_ship_name(data) when is_map(data) do
extract_field(data, ["ship_name", "ship_type_name"])
end
defp get_ship_name(_), do: nil
defp get_and_validate_system_id(kill) do

View File

@@ -70,7 +70,8 @@ defmodule WandererApp.Map.Audit do
def track_acl_event(
event_type,
%{user_id: user_id, acl_id: acl_id} = metadata
) when not is_nil(user_id) and not is_nil(acl_id),
)
when not is_nil(user_id) and not is_nil(acl_id),
do:
WandererApp.Api.UserActivity.new(%{
user_id: user_id,
@@ -85,7 +86,8 @@ defmodule WandererApp.Map.Audit do
def track_map_event(
event_type,
%{character_id: character_id, user_id: user_id, map_id: map_id} = metadata
) when not is_nil(character_id) and not is_nil(user_id) and not is_nil(map_id),
)
when not is_nil(character_id) and not is_nil(user_id) and not is_nil(map_id),
do:
WandererApp.Api.UserActivity.new(%{
character_id: character_id,

View File

@@ -161,7 +161,6 @@ defmodule WandererApp.Map.Manager do
case MapSystemSignature.by_deleted_and_updated_before!(true, delete_after_date) do
{:ok, deleted_signatures} ->
Enum.each(deleted_signatures, fn sig ->
Ash.destroy!(sig)
end)
@@ -174,17 +173,16 @@ defmodule WandererApp.Map.Manager do
end
end
defp cleanup_expired_pings() do
delete_after_date = DateTime.utc_now() |> DateTime.add(-1 * @pings_expire_minutes, :minute)
case WandererApp.MapPingsRepo.get_by_inserted_before(delete_after_date) do
{:ok, pings} ->
Enum.each(pings, fn %{map_id: map_id, type: type} = ping ->
Enum.each(pings, fn %{id: ping_id, map_id: map_id, type: type} = ping ->
{:ok, %{system: system}} = ping |> Ash.load([:system])
WandererApp.Map.Server.Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: system.solar_system_id,
type: type
})

View File

@@ -91,7 +91,8 @@ defmodule WandererApp.Map.Operations do
defdelegate delete_connection(map_id, src_id, tgt_id), to: Connections
@doc "Get a connection by source and target system IDs"
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
@spec get_connection_by_systems(String.t(), integer(), integer()) ::
{:ok, map()} | {:error, String.t()}
defdelegate get_connection_by_systems(map_id, source, target), to: Connections
# -- Structures ------------------------------------------------------------

View File

@@ -97,7 +97,7 @@ defmodule WandererApp.Map.SubscriptionManager do
) do
%{
plans: plans,
extra_characters_100: extra_characters_100,
extra_characters_50: extra_characters_50,
extra_hubs_10: extra_hubs_10
} = WandererApp.Env.subscription_settings()
@@ -113,7 +113,7 @@ defmodule WandererApp.Map.SubscriptionManager do
case characters_limit > plan_characters_limit do
true ->
estimated_price +
(characters_limit - plan_characters_limit) / 100 * extra_characters_100
(characters_limit - plan_characters_limit) / 50 * extra_characters_50
_ ->
estimated_price
@@ -153,7 +153,7 @@ defmodule WandererApp.Map.SubscriptionManager do
) do
%{
plans: plans,
extra_characters_100: extra_characters_100,
extra_characters_50: extra_characters_50,
extra_hubs_10: extra_hubs_10
} = WandererApp.Env.subscription_settings()
@@ -170,7 +170,7 @@ defmodule WandererApp.Map.SubscriptionManager do
case characters_limit > sub_characters_limit do
true ->
additional_price +
(characters_limit - sub_characters_limit) / 100 * extra_characters_100
(characters_limit - sub_characters_limit) / 50 * extra_characters_50
_ ->
additional_price

View File

@@ -39,6 +39,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
# Update detailed kills for maps with active subscriptions
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
if is_subscription_active do
update_detailed_map_kills(map_id)
end
@@ -89,18 +90,25 @@ defmodule WandererApp.Map.ZkbDataFetcher do
cache_key_ids = "map:#{map_id}:zkb:ids"
cache_key_details = "map:#{map_id}:zkb:detailed_kills"
old_ids_map = case WandererApp.Cache.get(cache_key_ids) do
map when is_map(map) -> map
_ -> %{}
end
old_ids_map =
case WandererApp.Cache.get(cache_key_ids) do
map when is_map(map) -> map
_ -> %{}
end
old_details_map = case WandererApp.Cache.get(cache_key_details) do
map when is_map(map) -> map
_ ->
# Initialize with empty map and store it
WandererApp.Cache.insert(cache_key_details, %{}, ttl: :timer.hours(@killmail_ttl_hours))
%{}
end
old_details_map =
case WandererApp.Cache.get(cache_key_details) do
map when is_map(map) ->
map
_ ->
# Initialize with empty map and store it
WandererApp.Cache.insert(cache_key_details, %{},
ttl: :timer.hours(@killmail_ttl_hours)
)
%{}
end
# Build current killmail ID map from cache
new_ids_map =
@@ -117,7 +125,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
old_set = MapSet.new(Map.get(old_ids_map, system_id, []))
old_details = Map.get(old_details_map, system_id, [])
# Update if IDs changed OR if we have IDs but no detailed kills
not MapSet.equal?(new_ids_set, old_set) or
not MapSet.equal?(new_ids_set, old_set) or
(MapSet.size(new_ids_set) > 0 and old_details == [])
end)
|> Enum.map(&elem(&1, 0))
@@ -132,7 +140,8 @@ defmodule WandererApp.Map.ZkbDataFetcher do
:ok
else
# Build new details for each changed system
updated_details_map = build_updated_details_map(changed_systems, old_details_map, new_ids_map)
updated_details_map =
build_updated_details_map(changed_systems, old_details_map, new_ids_map)
# Update the ID map cache
updated_ids_map = build_updated_ids_map(changed_systems, old_ids_map, new_ids_map)
@@ -198,7 +207,10 @@ defmodule WandererApp.Map.ZkbDataFetcher do
defp maybe_initialize_empty_details_map(%{}, systems, cache_key_details) do
# First time initialization - create empty structure
initial_map = Enum.into(systems, %{}, fn {system_id, _} -> {system_id, []} end)
WandererApp.Cache.insert(cache_key_details, initial_map, ttl: :timer.hours(@killmail_ttl_hours))
WandererApp.Cache.insert(cache_key_details, initial_map,
ttl: :timer.hours(@killmail_ttl_hours)
)
end
defp maybe_initialize_empty_details_map(_old_details_map, _systems, _cache_key_details), do: :ok

View File

@@ -15,9 +15,9 @@ defmodule WandererApp.Map.Operations.Connections do
@connection_type_stargate 1
# Ship size constants
@small_ship_size 0
@small_ship_size 0
@medium_ship_size 1
@large_ship_size 2
@large_ship_size 2
@xlarge_ship_size 3
# System class constants
@@ -40,13 +40,15 @@ defmodule WandererApp.Map.Operations.Connections do
build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info)
else
{:error, reason} -> handle_precondition_error(reason, attrs)
{:ok, []} -> {:error, :inconsistent_state}
other -> {:error, :unexpected_precondition_error, other}
{:ok, []} -> {:error, :inconsistent_state}
other -> {:error, :unexpected_precondition_error, other}
end
end
defp build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) do
Logger.debug("[Connections] build_and_add_connection called with src_info: #{inspect(src_info)}, tgt_info: #{inspect(tgt_info)}")
Logger.debug(
"[Connections] build_and_add_connection called with src_info: #{inspect(src_info)}, tgt_info: #{inspect(tgt_info)}"
)
# Guard against nil info
if is_nil(src_info) or is_nil(tgt_info) do
@@ -61,12 +63,22 @@ defmodule WandererApp.Map.Operations.Connections do
}
case Server.add_connection(map_id, info) do
:ok -> {:ok, :created}
{:ok, []} -> log_warn_and(:inconsistent_state, info)
:ok ->
{:ok, :created}
{:ok, []} ->
log_warn_and(:inconsistent_state, info)
{:error, %Invalid{errors: errs}} = err ->
if Enum.any?(errs, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
{:error, _} = err -> Logger.error("[add_connection] #{inspect(err)}"); {:error, :server_error}
other -> Logger.error("[add_connection] unexpected: #{inspect(other)}"); {:error, :unexpected_error}
{:error, _} = err ->
Logger.error("[add_connection] #{inspect(err)}")
{:error, :server_error}
other ->
Logger.error("[add_connection] unexpected: #{inspect(other)}")
{:error, :unexpected_error}
end
end
end
@@ -75,46 +87,55 @@ defmodule WandererApp.Map.Operations.Connections do
type = parse_type(attrs["type"])
if type == @connection_type_wormhole and
(src_info.system_class == @c1_system_class or
tgt_info.system_class == @c1_system_class) do
(src_info.system_class == @c1_system_class or
tgt_info.system_class == @c1_system_class) do
@medium_ship_size
else
parse_ship_size(attrs["ship_size_type"], @large_ship_size)
end
end
defp parse_ship_size(nil, default), do: default
defp parse_ship_size(nil, default), do: default
defp parse_ship_size(val, _default) when is_integer(val), do: val
defp parse_ship_size(val, default) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
:error -> default
end
end
defp parse_ship_size(_, default), do: default
defp parse_ship_size(_, default), do: default
defp parse_type(nil), do: @connection_type_wormhole
defp parse_type(val) when is_integer(val), do: val
defp parse_type(val) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
:error -> @connection_type_wormhole
end
end
defp parse_type(_), do: @connection_type_wormhole
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
defp parse_int(val, _) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> {:ok, i}
:error -> {:error, :invalid_integer}
end
end
defp parse_int(_, field), do: {:error, {:invalid_field, field}}
defp handle_precondition_error(reason, attrs) do
Logger.warning("[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}")
Logger.warning(
"[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}"
)
{:error, :precondition_failed, reason}
end
@@ -134,6 +155,7 @@ defmodule WandererApp.Map.Operations.Connections do
{:error, err} ->
Logger.warning("[list_connections] Repo error: #{inspect(err)}")
{:error, :repo_error}
other ->
Logger.error("[list_connections] Unexpected repo result: #{inspect(other)}")
{:error, :unexpected_repo_result}
@@ -157,28 +179,33 @@ defmodule WandererApp.Map.Operations.Connections do
end
@spec update_connection(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def update_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, conn_id, attrs) do
def update_connection(
%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn,
conn_id,
attrs
) do
with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id),
result <- (
try do
_allowed_keys = [
:mass_status,
:ship_size_type,
:type
]
_update_map =
attrs
|> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end)
|> Enum.map(fn {k, v} -> {String.to_atom(k), v} end)
|> Enum.into(%{})
res = apply_connection_updates(map_id, conn_struct, attrs, char_id)
res
rescue
error ->
Logger.error("[update_connection] Exception: #{inspect(error)}")
{:error, :exception}
end
),
result <-
(try do
_allowed_keys = [
:mass_status,
:ship_size_type,
:type
]
_update_map =
attrs
|> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end)
|> Enum.map(fn {k, v} -> {String.to_atom(k), v} end)
|> Enum.into(%{})
res = apply_connection_updates(map_id, conn_struct, attrs, char_id)
res
rescue
error ->
Logger.error("[update_connection] Exception: #{inspect(error)}")
{:error, :exception}
end),
:ok <- result,
{:ok, updated_conn} <- MapConnectionRepo.get_by_id(map_id, conn_id) do
{:ok, updated_conn}
@@ -187,29 +214,46 @@ defmodule WandererApp.Map.Operations.Connections do
_ -> {:error, :unexpected_error}
end
end
def update_connection(_conn, _conn_id, _attrs), do: {:error, :missing_params}
@spec delete_connection(Plug.Conn.t(), integer(), integer()) :: :ok | {:error, atom()}
def delete_connection(%{assigns: %{map_id: map_id}} = _conn, src, tgt) do
case Server.delete_connection(map_id, %{solar_system_source_id: src, solar_system_target_id: tgt}) do
:ok -> :ok
case Server.delete_connection(map_id, %{
solar_system_source_id: src,
solar_system_target_id: tgt
}) do
:ok ->
:ok
{:error, :not_found} ->
Logger.warning("[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}")
Logger.warning(
"[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}"
)
{:error, :not_found}
{:error, _} = err ->
Logger.error("[delete_connection] Server error: #{inspect(err)}")
{:error, :server_error}
_ ->
Logger.error("[delete_connection] Unknown error")
{:error, :unknown}
end
end
def delete_connection(_conn, _src, _tgt), do: {:error, :missing_params}
@doc "Batch upsert for connections"
@spec upsert_batch(Plug.Conn.t(), [map()]) :: %{created: integer(), updated: integer(), skipped: integer()}
@spec upsert_batch(Plug.Conn.t(), [map()]) :: %{
created: integer(),
updated: integer(),
skipped: integer()
}
def upsert_batch(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conns) do
_assigns = %{map_id: map_id, char_id: char_id}
Enum.reduce(conns, %{created: 0, updated: 0, skipped: 0}, fn conn_attrs, acc ->
case upsert_single(conn, conn_attrs) do
{:ok, :created} -> %{acc | created: acc.created + 1}
@@ -218,6 +262,7 @@ defmodule WandererApp.Map.Operations.Connections do
end
end)
end
def upsert_batch(_conn, _conns), do: %{created: 0, updated: 0, skipped: 0}
@doc "Upsert a single connection"
@@ -225,6 +270,7 @@ defmodule WandererApp.Map.Operations.Connections do
def upsert_single(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conn_data) do
source = conn_data["solar_system_source"] || conn_data[:solar_system_source]
target = conn_data["solar_system_target"] || conn_data[:solar_system_target]
with {:ok, %{} = existing_conn} <- get_connection_by_systems(map_id, source, target),
{:ok, _} <- update_connection(conn, existing_conn.id, conn_data) do
{:ok, :updated}
@@ -235,18 +281,22 @@ defmodule WandererApp.Map.Operations.Connections do
{:skip, :exists} -> {:ok, :updated}
err -> {:error, err}
end
{:error, _} = err ->
Logger.warning("[upsert_single] Connection lookup error: #{inspect(err)}")
{:error, :lookup_error}
err ->
Logger.error("[upsert_single] Update failed: #{inspect(err)}")
{:error, :unexpected_error}
end
end
def upsert_single(_conn, _conn_data), do: {:error, :missing_params}
@doc "Get a connection by source and target system IDs"
@spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()}
@spec get_connection_by_systems(String.t(), integer(), integer()) ::
{:ok, map()} | {:error, String.t()}
def get_connection_by_systems(map_id, source, target) do
with {:ok, conn} <- WandererApp.Map.find_connection(map_id, source, target) do
if conn, do: {:ok, conn}, else: WandererApp.Map.find_connection(map_id, target, source)
@@ -266,6 +316,7 @@ defmodule WandererApp.Map.Operations.Connections do
"type" -> maybe_update_type(map_id, conn, val)
_ -> :ok
end
if result == :ok do
{:cont, :ok}
else
@@ -279,6 +330,7 @@ defmodule WandererApp.Map.Operations.Connections do
end
defp maybe_update_mass_status(_map_id, _conn, nil), do: :ok
defp maybe_update_mass_status(map_id, conn, value) do
Server.update_connection_mass_status(map_id, %{
solar_system_source_id: conn.solar_system_source,
@@ -288,6 +340,7 @@ defmodule WandererApp.Map.Operations.Connections do
end
defp maybe_update_ship_size_type(_map_id, _conn, nil), do: :ok
defp maybe_update_ship_size_type(map_id, conn, value) do
Server.update_connection_ship_size_type(map_id, %{
solar_system_source_id: conn.solar_system_source,
@@ -297,6 +350,7 @@ defmodule WandererApp.Map.Operations.Connections do
end
defp maybe_update_type(_map_id, _conn, nil), do: :ok
defp maybe_update_type(map_id, conn, value) do
Server.update_connection_type(map_id, %{
solar_system_source_id: conn.solar_system_source,
@@ -306,15 +360,16 @@ defmodule WandererApp.Map.Operations.Connections do
end
@doc "Creates a connection between two systems"
@spec create_connection(String.t(), map(), String.t()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
@spec create_connection(String.t(), map(), String.t()) ::
{:ok, :created} | {:skip, :exists} | {:error, atom()}
def create_connection(map_id, attrs, char_id) do
do_create(attrs, map_id, char_id)
end
@doc "Creates a connection between two systems from a Plug.Conn"
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
@spec create_connection(Plug.Conn.t(), map()) ::
{:ok, :created} | {:skip, :exists} | {:error, atom()}
def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do
do_create(attrs, map_id, char_id)
end
end

View File

@@ -12,10 +12,12 @@ defmodule WandererApp.Map.Operations.Owner do
MapUserSettingsRepo,
Cache
}
alias WandererApp.Character
alias WandererApp.Character.TrackingUtils
@spec get_owner_character_id(String.t()) :: {:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
@spec get_owner_character_id(String.t()) ::
{:ok, %{id: term(), user_id: term()}} | {:error, String.t()}
def get_owner_character_id(map_id) do
cache_key = "map_#{map_id}:owner_info"
@@ -25,7 +27,8 @@ defmodule WandererApp.Map.Operations.Owner do
{:ok, char_ids} <- fetch_character_ids(map_id),
{:ok, characters} <- load_characters(char_ids),
{:ok, user_settings} <- MapUserSettingsRepo.get(map_id, owner.id),
{:ok, main} <- TrackingUtils.get_main_character(user_settings, characters, characters) do
{:ok, main} <-
TrackingUtils.get_main_character(user_settings, characters, characters) do
result = %{id: main.id, user_id: main.user_id}
Cache.insert(cache_key, result, ttl: @owner_info_cache_ttl)
{:ok, result}

View File

@@ -11,6 +11,7 @@ defmodule WandererApp.Map.Operations.Signatures do
@spec list_signatures(String.t()) :: [map()]
def list_signatures(map_id) do
systems = Operations.list_systems(map_id)
if systems != [] do
systems
|> Enum.flat_map(fn sys ->
@@ -28,18 +29,25 @@ defmodule WandererApp.Map.Operations.Signatures do
end
@spec create_signature(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
def create_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do
def create_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
%{"solar_system_id" => _solar_system_id} = params
) do
attrs = Map.put(params, "character_eve_id", char_id)
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: params["solar_system_id"],
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok -> {:ok, attrs}
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: params["solar_system_id"],
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
{:ok, attrs}
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
@@ -49,7 +57,12 @@ defmodule WandererApp.Map.Operations.Signatures do
def create_signature(_conn, _params), do: {:error, :missing_params}
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def update_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id, params) do
def update_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
sig_id,
params
) do
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
{:ok, system} <- MapSystem.by_id(sig.system_id) do
base = %{
@@ -63,16 +76,20 @@ defmodule WandererApp.Map.Operations.Signatures do
"description" => sig.description,
"linked_system_id" => sig.linked_system_id
}
attrs = Map.merge(base, params)
:ok = Server.update_signatures(map_id, %{
added_signatures: [],
updated_signatures: [attrs],
removed_signatures: [],
solar_system_id: system.solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
})
:ok =
Server.update_signatures(map_id, %{
added_signatures: [],
updated_signatures: [attrs],
removed_signatures: [],
solar_system_id: system.solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
})
{:ok, attrs}
else
err ->
@@ -84,24 +101,33 @@ defmodule WandererApp.Map.Operations.Signatures do
def update_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
@spec delete_signature(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
def delete_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id) do
def delete_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
sig_id
) do
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
{:ok, system} <- MapSystem.by_id(sig.system_id) do
removed = [%{
"eve_id" => sig.eve_id,
"name" => sig.name,
"kind" => sig.kind,
"group" => sig.group
}]
:ok = Server.update_signatures(map_id, %{
added_signatures: [],
updated_signatures: [],
removed_signatures: removed,
solar_system_id: system.solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
})
removed = [
%{
"eve_id" => sig.eve_id,
"name" => sig.name,
"kind" => sig.kind,
"group" => sig.group
}
]
:ok =
Server.update_signatures(map_id, %{
added_signatures: [],
updated_signatures: [],
removed_signatures: removed,
solar_system_id: system.solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
})
:ok
else
err ->

View File

@@ -11,13 +11,12 @@ defmodule WandererApp.Map.Operations.Structures do
@spec list_structures(String.t()) :: [map()]
def list_structures(map_id) do
with systems when is_list(systems) and systems != [] <- (
case Operations.list_systems(map_id) do
{:ok, systems} -> systems
systems when is_list(systems) -> systems
_ -> []
end
) do
with systems when is_list(systems) and systems != [] <-
(case Operations.list_systems(map_id) do
{:ok, systems} -> systems
systems when is_list(systems) -> systems
_ -> []
end) do
systems
|> Enum.flat_map(fn sys ->
with {:ok, structs} <- MapSystemStructure.by_system_id(sys.id) do
@@ -32,8 +31,16 @@ defmodule WandererApp.Map.Operations.Structures do
end
@spec create_structure(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
def create_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do
with {:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: params["solar_system_id"]}),
def create_structure(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
%{"solar_system_id" => _solar_system_id} = params
) do
with {:ok, system} <-
MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: params["solar_system_id"]
}),
attrs <- Map.put(prepare_attrs(params), "system_id", system.id),
:ok <- Structure.update_structures(system, [attrs], [], [], char_id, user_id),
name = Map.get(attrs, "name"),
@@ -46,6 +53,7 @@ defmodule WandererApp.Map.Operations.Structures do
nil ->
Logger.warning("[create_structure] Structure not found after creation")
{:error, :structure_not_found}
err ->
Logger.error("[create_structure] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
@@ -55,13 +63,25 @@ defmodule WandererApp.Map.Operations.Structures do
def create_structure(_conn, _params), do: {:error, "missing params"}
@spec update_structure(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def update_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id, params) do
def update_structure(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
struct_id,
params
) do
with {:ok, struct} <- MapSystemStructure.by_id(struct_id),
{:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: struct.solar_system_id}) do
{:ok, system} <-
MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: struct.solar_system_id
}) do
attrs = Map.merge(prepare_attrs(params), %{"id" => struct_id})
:ok = Structure.update_structures(system, [], [attrs], [], char_id, user_id)
case MapSystemStructure.by_id(struct_id) do
{:ok, updated} -> {:ok, updated}
{:ok, updated} ->
{:ok, updated}
err ->
Logger.error("[update_structure] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
@@ -76,7 +96,11 @@ defmodule WandererApp.Map.Operations.Structures do
def update_structure(_conn, _struct_id, _params), do: {:error, "missing params"}
@spec delete_structure(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()}
def delete_structure(%{assigns: %{map_id: _map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id) do
def delete_structure(
%{assigns: %{map_id: _map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
struct_id
) do
with {:ok, struct} <- MapSystemStructure.by_id(struct_id),
{:ok, system} <- MapSystem.by_id(struct.system_id) do
:ok = Structure.update_structures(system, [], [], [%{"id" => struct_id}], char_id, user_id)

View File

@@ -23,9 +23,14 @@ defmodule WandererApp.Map.Operations.Systems do
end
@spec create_system(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()}
def create_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, params) do
def create_system(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
params
) do
do_create_system(map_id, user_id, char_id, params)
end
def create_system(_conn, _params), do: {:error, :missing_params}
# Private helper for batch upsert
@@ -36,13 +41,20 @@ defmodule WandererApp.Map.Operations.Systems do
defp do_create_system(map_id, user_id, char_id, params) do
with {:ok, system_id} <- fetch_system_id(params),
coords <- normalize_coordinates(params),
:ok <- Server.add_system(map_id, %{solar_system_id: system_id, coordinates: coords}, user_id, char_id),
:ok <-
Server.add_system(
map_id,
%{solar_system_id: system_id, coordinates: coords},
user_id,
char_id
),
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
{:ok, system}
else
{:error, reason} when is_binary(reason) ->
Logger.warning("[do_create_system] Expected error: #{inspect(reason)}")
{:error, :expected_error}
_ ->
Logger.error("[do_create_system] Unexpected error")
{:error, :unexpected_error}
@@ -64,15 +76,21 @@ defmodule WandererApp.Map.Operations.Systems do
{:error, reason} when is_binary(reason) ->
Logger.warning("[update_system] Expected error: #{inspect(reason)}")
{:error, :expected_error}
_ ->
Logger.error("[update_system] Unexpected error")
{:error, :unexpected_error}
end
end
def update_system(_conn, _system_id, _attrs), do: {:error, :missing_params}
@spec delete_system(Plug.Conn.t(), integer()) :: {:ok, integer()} | {:error, atom()}
def delete_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, system_id) do
def delete_system(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
system_id
) do
with {:ok, _} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
:ok <- Server.delete_systems(map_id, [system_id], user_id, char_id) do
{:ok, 1}
@@ -80,17 +98,27 @@ defmodule WandererApp.Map.Operations.Systems do
{:error, :not_found} ->
Logger.warning("[delete_system] System not found: #{inspect(system_id)}")
{:error, :not_found}
_ ->
Logger.error("[delete_system] Unexpected error")
{:error, :unexpected_error}
end
end
def delete_system(_conn, _system_id), do: {:error, :missing_params}
@spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) :: {:ok, map()} | {:error, atom()}
def upsert_systems_and_connections(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn, systems, connections) do
@spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) ::
{:ok, map()} | {:error, atom()}
def upsert_systems_and_connections(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn,
systems,
connections
) do
assigns = %{map_id: map_id, user_id: user_id, char_id: char_id}
{created_s, updated_s, _skipped_s} = upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0)
{created_s, updated_s, _skipped_s} =
upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0)
conn_results =
connections
|> Enum.reduce(%{created: 0, updated: 0, skipped: 0}, fn conn_data, acc ->
@@ -100,33 +128,44 @@ defmodule WandererApp.Map.Operations.Systems do
_ -> %{acc | skipped: acc.skipped + 1}
end
end)
{:ok, %{
systems: %{created: created_s, updated: updated_s},
connections: %{created: conn_results.created, updated: conn_results.updated}
}}
{:ok,
%{
systems: %{created: created_s, updated: updated_s},
connections: %{created: conn_results.created, updated: conn_results.updated}
}}
end
def upsert_systems_and_connections(_conn, _systems, _connections), do: {:error, :missing_params}
# -- Internal Helpers -------------------------------------------------------
defp fetch_system_id(%{"solar_system_id" => id}), do: parse_int(id, "solar_system_id")
defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id), do: parse_int(id, "solar_system_id")
defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id),
do: parse_int(id, "solar_system_id")
defp fetch_system_id(_), do: {:error, "Missing system identifier (id)"}
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
defp parse_int(val, field) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> {:ok, i}
_ -> {:error, "Invalid #{field}: #{val}"}
end
end
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}}) when is_number(x) and is_number(y),
do: %{x: x, y: y}
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}})
when is_number(x) and is_number(y),
do: %{x: x, y: y}
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y),
do: %{x: x, y: y}
defp normalize_coordinates(params) do
%{
x: params |> Map.get("position_x", Map.get(params, :position_x, 0)),
@@ -135,10 +174,23 @@ defmodule WandererApp.Map.Operations.Systems do
end
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
with :ok <- Server.update_system_position(map_id, %{solar_system_id: system_id, position_x: round(x), position_y: round(y)}) do
with :ok <-
Server.update_system_position(map_id, %{
solar_system_id: system_id,
position_x: round(x),
position_y: round(y)
}) do
attrs
|> Map.drop([:coordinates, :position_x, :position_y, :solar_system_id,
"coordinates", "position_x", "position_y", "solar_system_id"])
|> Map.drop([
:coordinates,
:position_x,
:position_y,
:solar_system_id,
"coordinates",
"position_x",
"position_y",
"solar_system_id"
])
|> Enum.reduce_while(:ok, fn {key, val}, _ ->
case update_system_field(map_id, system_id, to_string(key), val) do
:ok -> {:cont, :ok}
@@ -150,21 +202,43 @@ defmodule WandererApp.Map.Operations.Systems do
defp update_system_field(map_id, system_id, field, val) do
case field do
"status" -> Server.update_system_status(map_id, %{solar_system_id: system_id, status: convert_status(val)})
"description" -> Server.update_system_description(map_id, %{solar_system_id: system_id, description: val})
"tag" -> Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val})
"status" ->
Server.update_system_status(map_id, %{
solar_system_id: system_id,
status: convert_status(val)
})
"description" ->
Server.update_system_description(map_id, %{solar_system_id: system_id, description: val})
"tag" ->
Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val})
"locked" ->
bool = val in [true, "true", 1, "1"]
Server.update_system_locked(map_id, %{solar_system_id: system_id, locked: bool})
f when f in ["label", "labels"] ->
labels = cond do
is_list(val) -> val
is_binary(val) -> String.split(val, ",", trim: true)
true -> []
end
Server.update_system_labels(map_id, %{solar_system_id: system_id, labels: Enum.join(labels, ",")})
"temporary_name" -> Server.update_system_temporary_name(map_id, %{solar_system_id: system_id, temporary_name: val})
_ -> :ok
labels =
cond do
is_list(val) -> val
is_binary(val) -> String.split(val, ",", trim: true)
true -> []
end
Server.update_system_labels(map_id, %{
solar_system_id: system_id,
labels: Enum.join(labels, ",")
})
"temporary_name" ->
Server.update_system_temporary_name(map_id, %{
solar_system_id: system_id,
temporary_name: val
})
_ ->
:ok
end
end
@@ -175,15 +249,18 @@ defmodule WandererApp.Map.Operations.Systems do
defp convert_status("TIME_CRITICAL"), do: 4
defp convert_status("REINFORCED"), do: 5
defp convert_status(i) when is_integer(i), do: i
defp convert_status(s) when is_binary(s) do
case Integer.parse(s) do
{i, _} -> i
_ -> 0
end
end
defp convert_status(_), do: 0
defp upsert_each([], _fun, c, u, d), do: {c, u, d}
defp upsert_each([item | rest], fun, c, u, d) do
case fun.(item) do
{:ok, _} -> upsert_each(rest, fun, c + 1, u, d)

View File

@@ -359,13 +359,15 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
{:ok, target_system_info} = get_system_static_info(location.solar_system_id)
# Set ship size type to medium only for wormhole connections involving C1 systems
ship_size_type = if connection_type == @connection_type_wormhole and
(source_system_info.system_class == @c1 or
target_system_info.system_class == @c1) do
@medium_ship_size
else
2 # Default to large for non-wormhole or non-C1 connections
end
ship_size_type =
if connection_type == @connection_type_wormhole and
(source_system_info.system_class == @c1 or
target_system_info.system_class == @c1) do
@medium_ship_size
else
# Default to large for non-wormhole or non-C1 connections
2
end
{:ok, connection} =
WandererApp.MapConnectionRepo.create(%{

View File

@@ -52,7 +52,7 @@ defmodule WandererApp.Map.Server.PingsImpl do
def cancel_ping(
%{map_id: map_id} = state,
%{
solar_system_id: solar_system_id,
id: ping_id,
character_id: character_id,
user_id: user_id,
type: type
@@ -60,14 +60,13 @@ defmodule WandererApp.Map.Server.PingsImpl do
) do
{:ok, character} = WandererApp.Character.get_character(character_id)
system =
WandererApp.Map.find_system_by_location(map_id, %{
solar_system_id: solar_system_id |> String.to_integer()
})
{:ok, %{system: %{solar_system_id: solar_system_id}} = ping} =
WandererApp.MapPingsRepo.get_by_id(ping_id)
:ok = WandererApp.MapPingsRepo.destroy(map_id, system.id)
:ok = WandererApp.MapPingsRepo.destroy(ping)
Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: solar_system_id,
type: type
})

View File

@@ -4,7 +4,7 @@ defmodule WandererApp.MapPingsRepo do
require Logger
def get_by_id(ping_id),
do: WandererApp.Api.MapPing.by_id(ping_id)
do: WandererApp.Api.MapPing.by_id!(ping_id) |> Ash.load([:system])
def get_by_map(map_id),
do: WandererApp.Api.MapPing.by_map!(%{map_id: map_id}) |> Ash.load([:character, :system])
@@ -18,20 +18,12 @@ defmodule WandererApp.MapPingsRepo do
def create(ping), do: ping |> WandererApp.Api.MapPing.new()
def create!(ping), do: ping |> WandererApp.Api.MapPing.new!()
def destroy(map_id, system_id) when is_binary(map_id) and is_binary(system_id) do
{:ok, pings} =
WandererApp.Api.MapPing.by_map_and_system(%{
map_id: map_id,
system_id: system_id
})
pings
|> Enum.each(fn ping ->
WandererApp.Api.MapPing.destroy!(ping)
end)
def destroy(ping) do
ping
|> WandererApp.Api.MapPing.destroy!()
:ok
end
def destroy(_ping), do: :ok
def destroy(_ping_id), do: :ok
end

View File

@@ -45,17 +45,18 @@ defmodule WandererApp.Ueberauth.Strategy.Eve.OAuth do
"""
def authorize_url!(params \\ [], opts \\ []) do
opts
|> Keyword.put(:redirect_uri, "#{WandererApp.Env.base_url()}/auth/eve/callback")
|> client
|> OAuth2.Client.authorize_url!(params)
end
def get(token, url, headers \\ [], opts \\ []) do
[token: token]
|> Keyword.put(:redirect_uri, "#{WandererApp.Env.base_url()}/auth/eve/callback")
|> client
|> put_param("response_type", "code")
|> put_param("client_id", client().client_id)
|> put_param("state", "ccp_auth_response")
|> put_param("redirect_uri", "#{WandererApp.Env.base_url()}/auth/eve/callback")
|> OAuth2.Client.get(url, headers, opts)
end

View File

@@ -18,8 +18,13 @@ defmodule WandererApp.Utils.EVEUtil do
"https://images.evetech.net/characters/12345678/portrait?size=128"
"""
def get_portrait_url(eve_id, size \\ 64)
def get_portrait_url(nil, size), do: "https://images.evetech.net/characters/0/portrait?size=#{size}"
def get_portrait_url("", size), do: "https://images.evetech.net/characters/0/portrait?size=#{size}"
def get_portrait_url(nil, size),
do: "https://images.evetech.net/characters/0/portrait?size=#{size}"
def get_portrait_url("", size),
do: "https://images.evetech.net/characters/0/portrait?size=#{size}"
def get_portrait_url(eve_id, size) do
"https://images.evetech.net/characters/#{eve_id}/portrait?size=#{size}"
end

View File

@@ -88,7 +88,7 @@ defmodule WandererAppWeb.CoreComponents do
]}
>
<h3 class="p-dialog-header font-bold text-base">
<div><%= @title %></div>
<div>{@title}</div>
<div class="absolute right-4">
<button
phx-click={JS.exec("data-cancel", to: "##{@id}")}
@@ -101,7 +101,7 @@ defmodule WandererAppWeb.CoreComponents do
</div>
</h3>
<div id={"#{@id}-content"} class="p-dialog-content !overflow-visible">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</div>
</.focus_wrap>
</div>
@@ -129,7 +129,7 @@ defmodule WandererAppWeb.CoreComponents do
</div>
<div class="ml-3 flex items-center">
<p class="text-sm font-medium text-red-800" role="alert">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</p>
</div>
</div>
@@ -208,7 +208,7 @@ defmodule WandererAppWeb.CoreComponents do
class="h-5 !w-[50px] text-orange-500"
/>
<.icon :if={@kind == :error} name="hero-x-circle" class="h-5 !w-[50px] text-red-500" />
<span :if={@kind == :loading} class="loading loading-ring loading-md"></span> <%= msg %>
<span :if={@kind == :loading} class="loading loading-ring loading-md"></span> {msg}
</div>
</div>
<button type="button" class="flex items-center" aria-label={gettext("close")}>
@@ -266,9 +266,9 @@ defmodule WandererAppWeb.CoreComponents do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="w-full space-y-8">
<%= render_slot(@inner_block, f) %>
{render_slot(@inner_block, f)}
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
{render_slot(action, f)}
</div>
</div>
</.form>
@@ -299,7 +299,7 @@ defmodule WandererAppWeb.CoreComponents do
]}
{@rest}
>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</button>
"""
end
@@ -379,7 +379,7 @@ defmodule WandererAppWeb.CoreComponents do
~H"""
<div phx-feedback-for={@name} class="form-control mt-2">
<label class="inputContainer" for={@name}>
<span><%= @label %></span>
<span>{@label}</span>
<div></div>
<div class="smallInputSwitch">
<div class="flex items-center">
@@ -443,9 +443,9 @@ defmodule WandererAppWeb.CoreComponents do
<div phx-feedback-for={@name}>
<div class="form-control w-full">
<.label for={@id}>
<span><%= @label %></span>
<span>{@label}</span>
<div></div>
<%= @value %>
{@value}
</.label>
<div>
@@ -463,7 +463,7 @@ defmodule WandererAppWeb.CoreComponents do
/>
</div>
</div>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
"""
end
@@ -477,7 +477,7 @@ defmodule WandererAppWeb.CoreComponents do
@wrapper_class
]}
>
<.label :if={@label} for={@id}><%= @label %></.label>
<.label :if={@label} for={@id}>{@label}</.label>
<div :if={@label}></div>
<select
id={@id}
@@ -489,10 +489,10 @@ defmodule WandererAppWeb.CoreComponents do
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, @value)}
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
"""
end
@@ -500,7 +500,7 @@ defmodule WandererAppWeb.CoreComponents do
def input(%{type: "textarea"} = assigns) do
~H"""
<label phx-feedback-for={@name} class="form-control">
<.label for={@id}><span class="label-text"><%= @label %></span></.label>
<.label for={@id}><span class="label-text">{@label}</span></.label>
<textarea
id={@id}
name={@name}
@@ -511,7 +511,7 @@ defmodule WandererAppWeb.CoreComponents do
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</label>
"""
end
@@ -520,7 +520,7 @@ defmodule WandererAppWeb.CoreComponents do
def input(assigns) do
~H"""
<label class="form-control w-full" phx-feedback-for={@name}>
<.label for={@id}><span class="label-text"><%= @label %></span></.label>
<.label for={@id}><span class="label-text">{@label}</span></.label>
<div class="join">
<input :if={@prefix} class="p-inputtext bg-neutral-700 join-item" disabled value={@prefix} />
<input
@@ -538,7 +538,7 @@ defmodule WandererAppWeb.CoreComponents do
</div>
<div class="label">
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</label>
"""
@@ -553,7 +553,7 @@ defmodule WandererAppWeb.CoreComponents do
def label(assigns) do
~H"""
<label for={@for} class="inputContainer">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</label>
"""
end
@@ -567,7 +567,7 @@ defmodule WandererAppWeb.CoreComponents do
~H"""
<p class="label-text-alt text-rose-600 phx-no-feedback:hidden">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</p>
"""
end
@@ -589,13 +589,13 @@ defmodule WandererAppWeb.CoreComponents do
]}>
<div>
<h1 class="text-lg font-semibold leading-8">
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6">
<%= render_slot(@subtitle) %>
{render_slot(@subtitle)}
</p>
</div>
<div class="flex-none"><%= render_slot(@actions) %></div>
<div class="flex-none">{render_slot(@actions)}</div>
</header>
"""
end
@@ -641,16 +641,16 @@ defmodule WandererAppWeb.CoreComponents do
<table class="table overflow-y-auto">
<thead>
<tr>
<th :for={col <- @col}><%= col[:label] %></th>
<th :for={col <- @col}>{col[:label]}</th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span>
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
<tbody id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}>
<tr :if={@rows |> Enum.empty?()}>
<td colspan={@col |> Enum.count()}>
<%= @empty_label %>
{@empty_label}
</td>
</tr>
<tr
@@ -660,13 +660,13 @@ defmodule WandererAppWeb.CoreComponents do
class={"hover #{if @row_selected && @row_selected.(row), do: "!bg-slate-600", else: ""} #{if @row_click, do: "cursor-pointer", else: ""}"}
>
<td :for={{col, _index} <- Enum.with_index(@col)}>
<%= render_slot(col, @row_item.(row)) %>
{render_slot(col, @row_item.(row))}
</td>
<td :if={@action != []}>
<div class="relative whitespace-nowrap text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
<span :for={action <- @action} class="relative pl-4 font-semibold leading-6">
<%= render_slot(action, @row_item.(row)) %>
{render_slot(action, @row_item.(row))}
</span>
</div>
</td>
@@ -696,8 +696,8 @@ defmodule WandererAppWeb.CoreComponents do
<div class="mt-14">
<dl class="-my-4 divide-y divide-zinc-100">
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
<dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
<dd class="text-zinc-700"><%= render_slot(item) %></dd>
<dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
<dd class="text-zinc-700">{render_slot(item)}</dd>
</div>
</dl>
</div>
@@ -757,10 +757,10 @@ defmodule WandererAppWeb.CoreComponents do
text_input_selected_class="p-inputtext"
{@live_select_opts}
>
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</LiveSelect.live_select>
<div for="form_description" class="label">
<.error :for={msg <- @errors}><%= msg %></.error>
<.error :for={msg <- @errors}>{msg}</.error>
</div>
</div>
"""
@@ -788,7 +788,7 @@ defmodule WandererAppWeb.CoreComponents do
]}
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
<%= render_slot(@inner_block) %>
{render_slot(@inner_block)}
</.link>
</div>
"""
@@ -836,7 +836,7 @@ defmodule WandererAppWeb.CoreComponents do
def local_time(assigns) do
~H"""
<time phx-hook="LocalTime" id={"time-#{@id}"} class="invisible"><%= @at %></time>
<time phx-hook="LocalTime" id={"time-#{@id}"} class="invisible">{@at}</time>
"""
end
@@ -845,7 +845,7 @@ defmodule WandererAppWeb.CoreComponents do
def client_time(assigns) do
~H"""
<time phx-hook="ClientTime" id={"client-time-#{@id}"} class="invisible"><%= @at %></time>
<time phx-hook="ClientTime" id={"client-time-#{@id}"} class="invisible">{@at}</time>
"""
end

View File

@@ -1,3 +1,3 @@
<main class="bg-stone-950">
<%= @inner_content %>
{@inner_content}
</main>

View File

@@ -39,7 +39,7 @@
<div class="navbar-end"></div>
</navbar>
<div class="!z-10 min-h-[calc(100vh-7rem)]">
<%= @inner_content %>
{@inner_content}
</div>
<!--Footer-->
<footer class="!z-10 w-full pb-4 text-sm text-center fade-in">

View File

@@ -8,6 +8,6 @@
class="main flex-1 relative z-0 overflow-hidden focus:outline-none transition-all duration-500 opacity-0 phx-page-loading:opacity-0 bg-stone-950 maps_bg ccp-font"
phx-mounted={JS.remove_class("opacity-0")}
>
<%= @inner_content %>
{@inner_content}
</main>
</div>

View File

@@ -7,7 +7,7 @@
class="main flex-1 relative z-0 overflow-hidden focus:outline-none transition-all duration-500 opacity-0 phx-page-loading:opacity-0"
phx-mounted={JS.remove_class("opacity-0")}
>
<%= @inner_content %>
{@inner_content}
</main>
<aside class="h-full w-14 left-0 absolute bg-gray-400 bg-opacity-5 text-gray-200 shadow-lg border-r border-stone-800 bg-opacity-70 bg-neutral-900">
<.sidebar_nav_links
@@ -23,9 +23,9 @@
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
</div>
<%= live_render(@socket, WandererAppWeb.ServerStatusLive,
{live_render(@socket, WandererAppWeb.ServerStatusLive,
container: {:div, class: ""},
id: "server-status"
) %>
)}
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Wanderer">
<%= assigns[:page_title] || "Welcome" %>
{assigns[:page_title] || "Welcome"}
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
@@ -49,6 +49,6 @@
</script>
</head>
<body>
<%= @inner_content %>
{@inner_content}
</body>
</html>

View File

@@ -198,9 +198,10 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Lists the ACLs for a given map.
"""
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :index,
operation(:index,
summary: "List ACLs for a Map",
description: "Lists the ACLs for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
description:
"Lists the ACLs for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
parameters: [
map_id: [
in: :query,
@@ -217,13 +218,17 @@ defmodule WandererAppWeb.MapAccessListAPIController do
],
responses: [
ok: {"List of ACLs", "application/json", @acl_index_response_schema},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{error: %OpenApiSpex.Schema{type: :string}},
required: ["error"],
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
}}
bad_request:
{"Error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{error: %OpenApiSpex.Schema{type: :string}},
required: ["error"],
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
}}
]
)
def index(conn, params) do
case APIUtils.fetch_map_id(params) do
{:ok, map_identifier} ->
@@ -235,7 +240,9 @@ defmodule WandererAppWeb.MapAccessListAPIController do
{:error, :map_not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "Map not found. Please provide a valid map_id or slug as a query parameter."})
|> json(%{
error: "Map not found. Please provide a valid map_id or slug as a query parameter."
})
{:error, error} ->
conn
@@ -256,9 +263,10 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Creates a new ACL for a map.
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :create,
operation(:create,
summary: "Create ACL for a Map",
description: "Creates a new ACL for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
description:
"Creates a new ACL for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
parameters: [
map_id: [
in: :query,
@@ -276,13 +284,17 @@ defmodule WandererAppWeb.MapAccessListAPIController do
request_body: {"ACL parameters", "application/json", @acl_create_request_schema},
responses: [
created: {"Created ACL", "application/json", @acl_create_response_schema},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{error: %OpenApiSpex.Schema{type: :string}},
required: ["error"],
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
}}
bad_request:
{"Error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{error: %OpenApiSpex.Schema{type: :string}},
required: ["error"],
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
}}
]
)
def create(conn, params) do
with {:ok, map_identifier} <- APIUtils.fetch_map_id(params),
{:ok, map} <- get_map(map_identifier),
@@ -303,7 +315,9 @@ defmodule WandererAppWeb.MapAccessListAPIController do
{:error, :map_not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "Map not found. Please provide a valid map_id or slug as a query parameter."})
|> json(%{
error: "Map not found. Please provide a valid map_id or slug as a query parameter."
})
{:error, "Must provide either ?map_id=UUID or ?slug=SLUG"} ->
conn
@@ -318,7 +332,10 @@ defmodule WandererAppWeb.MapAccessListAPIController do
{:error, "owner_eve_id does not match any existing character"} = _error ->
conn
|> put_status(:bad_request)
|> json(%{error: "Character not found: The provided owner_eve_id does not match any existing character"})
|> json(%{
error:
"Character not found: The provided owner_eve_id does not match any existing character"
})
%{} ->
conn
@@ -338,7 +355,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Shows a specific ACL (with its members).
"""
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :show,
operation(:show,
summary: "Get ACL details",
description: "Retrieves details for a specific ACL by its ID.",
parameters: [
@@ -356,27 +373,33 @@ defmodule WandererAppWeb.MapAccessListAPIController do
"application/json",
@acl_show_response_schema
},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "ACL not found"
}
}},
internal_server_error: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Failed to load ACL members: reason"
}
}}
not_found:
{"Error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "ACL not found"
}
}},
internal_server_error:
{"Error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Failed to load ACL members: reason"
}
}}
]
)
def show(conn, %{"id" => id}) do
query =
AccessList
@@ -413,7 +436,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Updates an ACL.
"""
@spec update(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :update,
operation(:update,
summary: "Update an ACL",
description: "Updates an existing ACL by its ID.",
parameters: [
@@ -436,27 +459,33 @@ defmodule WandererAppWeb.MapAccessListAPIController do
"application/json",
@acl_update_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Failed to update ACL: invalid parameters"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "ACL not found"
}
}}
bad_request:
{"Error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Failed to update ACL: invalid parameters"
}
}},
not_found:
{"Error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "ACL not found"
}
}}
]
)
def update(conn, %{"id" => id, "acl" => acl_params}) do
with {:ok, acl} <- AccessList.by_id(id),
{:ok, updated_acl} <- AccessList.update(acl, acl_params),
@@ -559,12 +588,13 @@ defmodule WandererAppWeb.MapAccessListAPIController do
new_acl_id = if is_binary(new_acl), do: new_acl, else: new_acl.id
# Extract IDs from current ACLs to ensure we're working with UUIDs only
current_acl_ids = loaded_map.acls
|> Kernel.||([])
|> Enum.map(fn
acl when is_binary(acl) -> acl
acl -> acl.id
end)
current_acl_ids =
loaded_map.acls
|> Kernel.||([])
|> Enum.map(fn
acl when is_binary(acl) -> acl
acl -> acl.id
end)
updated_acls = current_acl_ids ++ [new_acl_id]

View File

@@ -1,5 +1,5 @@
<article class="prose prose-lg ccp-font w-full max-w-3xl mx-auto">
<div class="w-full px-4 md:px-6 text-xl leading-normal ccp-font">
<%= raw(@file.body) %>
{raw(@file.body)}
</div>
</article>

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