mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-08 08:45:37 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d35500e2f | ||
|
|
5dad5d8e03 | ||
|
|
fd4d5b90e2 | ||
|
|
1ee9f26b34 | ||
|
|
09880a54e9 | ||
|
|
0f6847b16d | ||
|
|
e457d94df8 | ||
|
|
e9583c928e | ||
|
|
89c14628e1 | ||
|
|
7a82b2c102 | ||
|
|
2db2a47186 | ||
|
|
eabb0e8470 | ||
|
|
c65b8e5ebd | ||
|
|
bfed1480ae | ||
|
|
5ff902f185 | ||
|
|
8d38345c7f | ||
|
|
14be9dbb8a | ||
|
|
720c26db94 | ||
|
|
6d0b8b845d | ||
|
|
b2767e000e | ||
|
|
169f23c2ca | ||
|
|
81f70eafff | ||
|
|
8b6f600989 | ||
|
|
fe3617b39f | ||
|
|
7fb8d24d73 | ||
|
|
f03448007d | ||
|
|
c317a8bff9 | ||
|
|
618cca39a4 | ||
|
|
fe7a98098f | ||
|
|
df49939990 | ||
|
|
f23f2776f4 | ||
|
|
4419c86164 | ||
|
|
9848f49b49 |
89
CHANGELOG.md
89
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1
assets/js/hooks/Mapper/components/helpers/index.ts
Normal file
1
assets/js/hooks/Mapper/components/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './parseMapUserSettings.ts';
|
||||
@@ -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);
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -0,0 +1 @@
|
||||
export * from './KillsCounter.tsx';
|
||||
@@ -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(() => {
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LocalCounter';
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
71
assets/js/hooks/Mapper/constants/signatures.ts
Normal file
71
assets/js/hooks/Mapper/constants/signatures.ts
Normal 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,
|
||||
};
|
||||
11
assets/js/hooks/Mapper/helpers/getFormattedTime.ts
Normal file
11
assets/js/hooks/Mapper/helpers/getFormattedTime.ts
Normal 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}`;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './useActualizeSettings';
|
||||
export * from './useClipboard';
|
||||
export * from './useHotkey';
|
||||
export * from './usePageVisibility';
|
||||
|
||||
@@ -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
|
||||
}, []);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
1
assets/js/hooks/Mapper/mapRootProvider/helpers/index.ts
Normal file
1
assets/js/hooks/Mapper/mapRootProvider/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './actualizeSettings';
|
||||
@@ -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 });
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum PingType {
|
||||
}
|
||||
|
||||
export type PingData = {
|
||||
id: string;
|
||||
inserted_at: number;
|
||||
character_eve_id: string;
|
||||
solar_system_id: string;
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './contextStore';
|
||||
export * from './getQueryVariable';
|
||||
export * from './loadTextFile';
|
||||
export * from './saveToFile';
|
||||
|
||||
27
assets/js/hooks/Mapper/utils/loadTextFile.ts
Normal file
27
assets/js/hooks/Mapper/utils/loadTextFile.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
33
assets/js/hooks/Mapper/utils/saveToFile.ts
Normal file
33
assets/js/hooks/Mapper/utils/saveToFile.ts
Normal 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();
|
||||
}
|
||||
BIN
assets/static/images/pochven.webp
Normal file
BIN
assets/static/images/pochven.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...")
|
||||
|
||||
[
|
||||
|
||||
@@ -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")
|
||||
[]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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 ------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(%{
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<main class="bg-stone-950">
|
||||
<%= @inner_content %>
|
||||
{@inner_content}
|
||||
</main>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user