mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-02 22:12:55 +00:00
Compare commits
4 Commits
v1.43.2
...
show-sig-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ce7ca928a | ||
|
|
16249950eb | ||
|
|
0087bab3a2 | ||
|
|
34149799df |
99
.github/workflows/build.yml
vendored
99
.github/workflows/build.yml
vendored
@@ -78,23 +78,22 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-elixir-deps
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
|
||||
path: deps
|
||||
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
|
||||
${{ runner.os }}-mix-${{ env.cache-name }}-
|
||||
- name: 😅 Cache compiled build
|
||||
id: cache-build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-compiled-build
|
||||
with:
|
||||
path: |
|
||||
_build
|
||||
**/_build
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
|
||||
@@ -123,9 +122,6 @@ jobs:
|
||||
name: 🛠 Build Docker Images
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
release-notes: ${{ steps.get-content.outputs.string }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
@@ -188,28 +184,15 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ env.REGISTRY_IMAGE }}:latest,${{ env.REGISTRY_IMAGE }}:${{ steps.get-latest-tag.outputs.tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
MIX_ENV=prod
|
||||
BUILD_METADATA=${{ steps.meta.outputs.json }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.build.outputs.digest }}
|
||||
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
@@ -229,54 +212,16 @@ jobs:
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
create-release:
|
||||
name: 🏷 Create Release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [docker, merge]
|
||||
needs: docker
|
||||
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
@@ -284,11 +229,17 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: 🏷 Create Draft Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.docker.outputs.release-tag }}
|
||||
name: Release ${{ needs.docker.outputs.release-tag }}
|
||||
tag_name: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
name: Release ${{ steps.get-latest-tag.outputs.tag }}
|
||||
body: |
|
||||
## Info
|
||||
Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}).
|
||||
@@ -298,9 +249,3 @@ jobs:
|
||||
## How to Promote?
|
||||
In order to promote this to prod, edit the draft and press **"Publish release"**.
|
||||
draft: true
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ needs.docker.outputs.release-notes }}
|
||||
|
||||
212
CHANGELOG.md
212
CHANGELOG.md
@@ -2,218 +2,6 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.43.2](https://github.com/wanderer-industries/wanderer/compare/v1.43.1...v1.43.2) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* prevent constraint error for follow/toggle (#132)
|
||||
|
||||
## [v1.43.1](https://github.com/wanderer-industries/wanderer/compare/v1.43.0...v1.43.1) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.0](https://github.com/wanderer-industries/wanderer/compare/v1.42.5...v1.43.0) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add news post for structures widget (#131)
|
||||
|
||||
## [v1.42.5](https://github.com/wanderer-industries/wanderer/compare/v1.42.4...v1.42.5) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix link signatures on splash. Fix deleting connection on locked system remove.
|
||||
|
||||
## [v1.42.4](https://github.com/wanderer-industries/wanderer/compare/v1.42.3...v1.42.4) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fix system statics list (required EVE DB data update). Add system name to signature added/removed audit log
|
||||
|
||||
## [v1.42.3](https://github.com/wanderer-industries/wanderer/compare/v1.42.2...v1.42.3) (2025-01-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* change structure tooltip to avoid paste confusion (#125)
|
||||
|
||||
* change structure tooltip to avoid paste confusion
|
||||
|
||||
* clarify use of evetime and use primereact calendar
|
||||
|
||||
## [v1.42.2](https://github.com/wanderer-industries/wanderer/compare/v1.42.1...v1.42.2) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.42.1](https://github.com/wanderer-industries/wanderer/compare/v1.42.0...v1.42.1) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Remove linked sig ID if system containing signature removed from map
|
||||
|
||||
## [v1.42.0](https://github.com/wanderer-industries/wanderer/compare/v1.41.0...v1.42.0) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Audit: Add 'Signatures added/removed' map audit events
|
||||
|
||||
## [v1.41.0](https://github.com/wanderer-industries/wanderer/compare/v1.40.7...v1.41.0) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Audit: Add 'ACL added/removed' map audit events
|
||||
|
||||
## [v1.40.7](https://github.com/wanderer-industries/wanderer/compare/v1.40.6...v1.40.7) (2025-01-15)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.40.6](https://github.com/wanderer-industries/wanderer/compare/v1.40.5...v1.40.6) (2025-01-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix follow mode
|
||||
|
||||
* center system is not selected text for structures (#122)
|
||||
|
||||
* Map: Fix system revert issues
|
||||
|
||||
* Map: Fix issues with splashing signatures select & sig ID in temp names
|
||||
|
||||
## [v1.40.5](https://github.com/wanderer-industries/wanderer/compare/v1.40.4...v1.40.5) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix follow mode
|
||||
|
||||
## [v1.40.4](https://github.com/wanderer-industries/wanderer/compare/v1.40.3...v1.40.4) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* center system is not selected text for structures (#122)
|
||||
|
||||
## [v1.40.3](https://github.com/wanderer-industries/wanderer/compare/v1.40.2...v1.40.3) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix system revert issues
|
||||
|
||||
## [v1.40.2](https://github.com/wanderer-industries/wanderer/compare/v1.40.1...v1.40.2) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix issues with splashing signatures select & sig ID in temp names
|
||||
|
||||
## [v1.40.1](https://github.com/wanderer-industries/wanderer/compare/v1.40.0...v1.40.1) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.40.0](https://github.com/wanderer-industries/wanderer/compare/v1.39.3...v1.40.0) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add structure widget with timer and associated api
|
||||
|
||||
## [v1.39.3](https://github.com/wanderer-industries/wanderer/compare/v1.39.2...v1.39.3) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add style of corners for windows. Add ability to reset widgets. A lot of refactoring
|
||||
|
||||
## [v1.39.2](https://github.com/wanderer-industries/wanderer/compare/v1.39.1...v1.39.2) (2025-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.39.1](https://github.com/wanderer-industries/wanderer/compare/v1.39.0...v1.39.1) (2025-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: New windows systems
|
||||
|
||||
* Map: Add new windows system and removed old
|
||||
|
||||
* Map: First prototype of windows
|
||||
|
||||
## [v1.39.0](https://github.com/wanderer-industries/wanderer/compare/v1.38.7...v1.39.0) (2025-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Map: Added option to show signature ID as system temporary name part
|
||||
|
||||
## [v1.38.7](https://github.com/wanderer-industries/wanderer/compare/v1.38.6...v1.38.7) (2025-01-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.38.6](https://github.com/wanderer-industries/wanderer/compare/v1.38.5...v1.38.6) (2025-01-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.38.5](https://github.com/wanderer-industries/wanderer/compare/v1.38.4...v1.38.5) (2025-01-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.38.4](https://github.com/wanderer-industries/wanderer/compare/v1.38.3...v1.38.4) (2025-01-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.38.3](https://github.com/wanderer-industries/wanderer/compare/v1.38.2...v1.38.3) (2025-01-12)
|
||||
|
||||
|
||||
|
||||
181
assets/js/hooks/Mapper/components/hooks/useSolarSystemNode.ts
Normal file
181
assets/js/hooks/Mapper/components/hooks/useSolarSystemNode.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
// feel free to rename these imports or the file path as you see fit
|
||||
import { useMemo } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick.ts';
|
||||
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { LABELS_INFO, LABELS_ORDER, getActivityType } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
|
||||
const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Caldari]: 'Caldaria',
|
||||
[Spaces.Matar]: 'Mataria',
|
||||
[Spaces.Amarr]: 'Amarria',
|
||||
[Spaces.Gallente]: 'Gallente',
|
||||
};
|
||||
|
||||
const sortedLabels = (labels: string[]) => {
|
||||
if (!labels) return [];
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
|
||||
};
|
||||
|
||||
interface UseSolarSystemNodeParams {
|
||||
data: MapSolarSystemType;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export function useSolarSystemNode({ data, selected }: UseSolarSystemNodeParams) {
|
||||
// 1) Bring in relevant global state
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
const { isShowUnsplashedSignatures } = interfaceSettings;
|
||||
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
|
||||
const isShowLinkedSigId = useMapGetOption('show_linked_signature_id') === 'true';
|
||||
|
||||
const {
|
||||
data: {
|
||||
characters,
|
||||
presentCharacters,
|
||||
wormholesData,
|
||||
hubs,
|
||||
kills,
|
||||
userCharacters,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
},
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
|
||||
// 2) Extract data from the node
|
||||
const {
|
||||
system_class,
|
||||
security,
|
||||
class_title,
|
||||
solar_system_id,
|
||||
statics,
|
||||
effect_name,
|
||||
region_name,
|
||||
region_id,
|
||||
is_shattered,
|
||||
solar_system_name,
|
||||
} = data.system_static_info;
|
||||
|
||||
const {
|
||||
locked,
|
||||
name,
|
||||
tag,
|
||||
status,
|
||||
labels,
|
||||
id,
|
||||
temporary_name: temporaryName,
|
||||
linked_sig_eve_id: linkedSigEveId = '',
|
||||
} = data || {};
|
||||
const signatures = data.system_signatures;
|
||||
|
||||
// 3) Compute derived values
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const charactersInSystem = useMemo(() => {
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
|
||||
}, [characters, presentCharacters, solar_system_id]);
|
||||
|
||||
const isWormhole = isWormholeSpace(system_class);
|
||||
|
||||
const classTitleColor = useMemo(
|
||||
() => getSystemClassStyles({ systemClass: system_class, security }),
|
||||
[security, system_class],
|
||||
);
|
||||
|
||||
const sortedStatics = useMemo(() => sortWHClasses(wormholesData, statics), [wormholesData, statics]);
|
||||
|
||||
const linkedSigPrefix = useMemo(() => (linkedSigEveId ? linkedSigEveId.split('-')[0] : null), [linkedSigEveId]);
|
||||
|
||||
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
|
||||
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
|
||||
const labelCustom = useMemo(
|
||||
() =>
|
||||
isShowLinkedSigId && linkedSigPrefix
|
||||
? `${linkedSigPrefix}・${labelsManager.customLabel}`
|
||||
: labelsManager.customLabel,
|
||||
[linkedSigPrefix, isShowLinkedSigId, labelsManager],
|
||||
);
|
||||
|
||||
const killsCount = useMemo(() => {
|
||||
const systemKills = kills[solar_system_id];
|
||||
if (!systemKills) return null;
|
||||
return systemKills;
|
||||
}, [kills, solar_system_id]);
|
||||
|
||||
const hasUserCharacters = useMemo(() => {
|
||||
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
|
||||
}, [charactersInSystem, userCharacters]);
|
||||
|
||||
const dbClick = useDoubleClick(() => {
|
||||
outCommand({
|
||||
type: OutCommand.openSettings,
|
||||
data: {
|
||||
system_id: solar_system_id.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const showHandlers = isConnecting || hoverNodeId === id;
|
||||
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
|
||||
const systemName = (isTempSystemNameEnabled && temporaryName) || solar_system_name;
|
||||
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name);
|
||||
|
||||
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
return [[], []];
|
||||
}
|
||||
return prepareUnsplashedChunks(
|
||||
signatures
|
||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||
.map(s => ({
|
||||
eve_id: s.eve_id,
|
||||
type: s.type,
|
||||
custom_info: s.custom_info,
|
||||
})),
|
||||
);
|
||||
}, [isShowUnsplashedSignatures, signatures]);
|
||||
|
||||
return {
|
||||
selected,
|
||||
visible,
|
||||
isWormhole,
|
||||
classTitleColor,
|
||||
killsCount,
|
||||
hasUserCharacters,
|
||||
showHandlers,
|
||||
regionClass,
|
||||
systemName,
|
||||
customName,
|
||||
labelCustom,
|
||||
is_shattered,
|
||||
tag,
|
||||
status,
|
||||
labelsInfo,
|
||||
dbClick,
|
||||
sortedStatics,
|
||||
effect_name,
|
||||
region_name,
|
||||
solar_system_id,
|
||||
locked,
|
||||
hubs,
|
||||
charactersInSystem,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
isThickConnections,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeChange,
|
||||
@@ -17,16 +17,18 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
import classes from './Map.module.scss';
|
||||
import { MapProvider, useMapState } from './MapProvider';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { useNodesState, useEdgesState, useMapHandlers, useUpdateNodes } from './hooks';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
SolarSystemEdge,
|
||||
SolarSystemNodeDefault,
|
||||
SolarSystemNodeTheme,
|
||||
useContextMenuConnectionHandlers,
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { wrapNode } from './utils/wrapNode';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
@@ -75,6 +77,9 @@ const initialEdges = [
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
const edgeTypes = {
|
||||
floating: SolarSystemEdge,
|
||||
};
|
||||
@@ -84,7 +89,6 @@ interface MapCompProps {
|
||||
onCommand: OutCommandHandler;
|
||||
onSelectionChange: OnMapSelectionChange;
|
||||
onManualDelete(systems: string[]): void;
|
||||
canRemoveConnection?(connectionId: string): boolean;
|
||||
onConnectionInfoClick?(e: SolarSystemConnection): void;
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onSelectionContextMenu?: NodeSelectionMouseHandler;
|
||||
@@ -114,26 +118,28 @@ const MapComp = ({
|
||||
isSoftBackground,
|
||||
theme,
|
||||
onAddSystem,
|
||||
canRemoveConnection,
|
||||
}: MapCompProps) => {
|
||||
const { getEdge, getNode, getNodes } = useReactFlow();
|
||||
const { getNode, getNodes } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
|
||||
|
||||
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom:
|
||||
theme !== '' && theme !== 'default'
|
||||
? wrapNode(SolarSystemNodeTheme)
|
||||
: wrapNode(SolarSystemNodeDefault),
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
|
||||
useMapHandlers(refn, onSelectionChange);
|
||||
useUpdateNodes(nodes);
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||
|
||||
// You can create nodeTypes dynamically based on the node component
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom: nodeComponent,
|
||||
};
|
||||
}, [nodeComponent]);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
params => {
|
||||
@@ -222,41 +228,7 @@ const MapComp = ({
|
||||
|
||||
onNodesChange(nextChanges);
|
||||
},
|
||||
[getNode, getNodes, onManualDelete, onNodesChange],
|
||||
);
|
||||
|
||||
const handleEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
const nextChanges = changes.reduce((acc, change) => {
|
||||
if (change.type !== 'remove') {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (canRemoveConnection?.(change.id)) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const edge = getEdge(change.id);
|
||||
if (!edge) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const sourceNode = getNode(edge.source);
|
||||
const targetNode = getNode(edge.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (sourceNode.data.locked || targetNode.data.locked) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, change];
|
||||
}, [] as EdgeChange[]);
|
||||
|
||||
onEdgesChange(nextChanges);
|
||||
},
|
||||
[getEdge, getNode, onEdgesChange],
|
||||
[getNode, onManualDelete, onNodesChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -274,14 +246,14 @@ const MapComp = ({
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
// TODO we need save into session all of this
|
||||
// and on any action do either
|
||||
defaultViewport={getViewPortFromStore()}
|
||||
edgeTypes={edgeTypes}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={connectionMode}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
snapToGrid
|
||||
nodeDragThreshold={10}
|
||||
onNodeDragStop={handleDragStop}
|
||||
@@ -289,10 +261,6 @@ const MapComp = ({
|
||||
onConnectStart={() => update({ isConnecting: true })}
|
||||
onConnectEnd={() => update({ isConnecting: false })}
|
||||
onNodeMouseEnter={(_, node) => update({ hoverNodeId: node.id })}
|
||||
onPaneClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
// onKeyUp=
|
||||
onNodeMouseLeave={() => update({ hoverNodeId: null })}
|
||||
onEdgeClick={(_, t) => {
|
||||
@@ -314,12 +282,6 @@ const MapComp = ({
|
||||
maxZoom={1.5}
|
||||
elevateNodesOnSelect
|
||||
deleteKeyCode={['Delete']}
|
||||
{...(isPanAndDrag
|
||||
? {
|
||||
selectionOnDrag: true,
|
||||
panOnDrag: [2],
|
||||
}
|
||||
: {})}
|
||||
// TODO need create clear example with problem with that flag
|
||||
// if system is not visible edge not drawing (and any render in Custom node is not happening)
|
||||
// onlyRenderVisibleElements
|
||||
|
||||
@@ -9,7 +9,6 @@ export type MapData = MapUnionTypes & {
|
||||
visibleNodes: Set<string>;
|
||||
showKSpaceBG: boolean;
|
||||
isThickConnections: boolean;
|
||||
linkedSigEveId: string;
|
||||
};
|
||||
|
||||
interface MapProviderProps {
|
||||
|
||||
@@ -6,14 +6,7 @@ $pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020;
|
||||
|
||||
$node-bg-color: #202020;
|
||||
$node-soft-bg-color: #202020;
|
||||
$text-color: #ffffff;
|
||||
$tag-color: #38BDF8;
|
||||
$region-name: #D6D3D1;
|
||||
$custom-name: #93C5FD;
|
||||
$tooltip-bg: #202020; // Dark background for tooltips
|
||||
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
|
||||
import {
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
@@ -13,35 +15,64 @@ import {
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
export const SolarSystemNodeDefault = memo(props => {
|
||||
const {
|
||||
charactersInSystem,
|
||||
classTitle,
|
||||
classTitleColor,
|
||||
customName,
|
||||
effectName,
|
||||
hasUserCharacters,
|
||||
hubs,
|
||||
visible,
|
||||
labelCustom,
|
||||
labelsInfo,
|
||||
locked,
|
||||
isShattered,
|
||||
isThickConnections,
|
||||
isWormhole,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
regionClass,
|
||||
regionName,
|
||||
status,
|
||||
selected,
|
||||
tag,
|
||||
showHandlers,
|
||||
systemName,
|
||||
sortedStatics,
|
||||
solarSystemId,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
dbClick: handleDbClick,
|
||||
} = useSolarSystemNode(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nodeVars.visible && (
|
||||
{visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
{labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.isShattered && (
|
||||
{isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
||||
{killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
<span className={clsx(classes.text)}>{killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
{labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
@@ -50,30 +81,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
)}
|
||||
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
|
||||
[classes.selected]: selected,
|
||||
})}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
{visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classTitle,
|
||||
nodeVars.classTitleColor,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
|
||||
)}
|
||||
>
|
||||
{nodeVars.classTitle ?? '-'}
|
||||
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
|
||||
{classTitle ?? '-'}
|
||||
</div>
|
||||
|
||||
{nodeVars.tag != null && nodeVars.tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
|
||||
{tag != null && tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
@@ -82,55 +102,53 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
|
||||
)}
|
||||
>
|
||||
{nodeVars.systemName}
|
||||
{systemName}
|
||||
</div>
|
||||
|
||||
{nodeVars.isWormhole && (
|
||||
{isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
{sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.effectName !== null && nodeVars.isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
|
||||
{effectName !== null && isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{nodeVars.customName && (
|
||||
{customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.customName}
|
||||
{customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
{!isWormhole && !customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.regionName}
|
||||
{regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
{isWormhole && !customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
|
||||
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
{hubs.includes(solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.charactersInSystem.length > 0 && (
|
||||
{charactersInSystem.length > 0 && (
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
['text-amber-300']: nodeVars.hasUserCharacters,
|
||||
['text-amber-300']: hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
||||
<span className="font-sans">{charactersInSystem.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -140,19 +158,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nodeVars.visible && (
|
||||
{visible && (
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
{unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
{unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
{unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
{unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
@@ -160,44 +178,44 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
|
||||
@@ -1,91 +1,407 @@
|
||||
@import './SolarSystemNodeDefault.module.scss';
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
/* ---------------------------
|
||||
Only override what's different
|
||||
--------------------------- */
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: #d291bc;
|
||||
$pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020; // Dark background for tooltips
|
||||
|
||||
/* 1) .RootCustomNode:
|
||||
- new background-color using CSS var
|
||||
- plus color, font-family, and font-weight */
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
height: 34px;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
|
||||
background-color: var(--rf-node-bg-color, #202020) !important;
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
/* 2) .Bookmarks:
|
||||
- add var-based font family/weight
|
||||
*/
|
||||
.Bookmarks {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
/* 3) .HeadRow, .classTitle, .classSystemName:
|
||||
- add new references to var-based font family/weight
|
||||
*/
|
||||
.HeadRow {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
.classTitle {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
&.Mataria,
|
||||
&.Amarria,
|
||||
&.Gallente,
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Mataria {
|
||||
&::before {
|
||||
background-image: url('/images/mataria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
background-image: url('/images/caldaria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Amarria {
|
||||
&::before {
|
||||
opacity: 0.45;
|
||||
background-image: url('/images/amarr-180.png');
|
||||
background-position-x: 0;
|
||||
background-position-y: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Gallente {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
background-image: url('/images/gallente-180.png');
|
||||
background-position-x: 1px;
|
||||
background-position-y: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
}
|
||||
|
||||
&.tooltip {
|
||||
background-color: $tooltip-bg;
|
||||
color: $text-color;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $pastel-pink;
|
||||
}
|
||||
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-home);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-friendly {
|
||||
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-friendly-dark30),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-friendly-dark5);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-lookingFor {
|
||||
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
|
||||
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-warning {
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-warning),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&.eve-system-status-dangerous {
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-dangerous),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&.eve-system-status-target {
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-target),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.Bookmarks {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
left: 4px;
|
||||
|
||||
& > .Bookmark {
|
||||
min-width: 13px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -13px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
//background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.BookmarkWithIcon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -2px;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
|
||||
padding-right: 2px;
|
||||
|
||||
& > .icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin-top: 1px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Unsplashed {
|
||||
position: absolute;
|
||||
width: calc(50% - 4px);
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
left: 2px;
|
||||
|
||||
&--right {
|
||||
left: calc(50% + 6px);
|
||||
}
|
||||
|
||||
& > .Signature {
|
||||
width: 13px;
|
||||
height: 4px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
display: block;
|
||||
|
||||
background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.HeadRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
.classTitle {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
|
||||
}
|
||||
|
||||
.TagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: medium;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
|
||||
color: var(--rf-tag-color, #38BDF8);
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
.classSystemName {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.classSystemName {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.solarSystemName {
|
||||
}
|
||||
}
|
||||
|
||||
/* 4) .BottomRow:
|
||||
- introduces .tagTitle, .regionName, .customName, .localCounter
|
||||
referencing new CSS variables */
|
||||
.BottomRow {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
.tagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: medium;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
color: var(--rf-tag-color, #38BDF8);
|
||||
}
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
|
||||
.regionName {
|
||||
color: var(--rf-region-name, #D6D3D1);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
color: var(--rf-region-name, #D6D3D1)
|
||||
}
|
||||
|
||||
.customName {
|
||||
color: var(--rf-custom-name, #93C5FD);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
color: var(--rf-custom-name, #93C5FD)
|
||||
}
|
||||
|
||||
.localCounter {
|
||||
display: flex;
|
||||
color: var(--rf-has-user-characters, #fbbf24);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
//align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters, #fbbf24);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: 500;
|
||||
//margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.effect {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -2px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.statics {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
font-size: 8px;
|
||||
|
||||
& > * {
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Handle {
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
|
||||
&.HandleTop {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -2px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -2px;
|
||||
}
|
||||
|
||||
&.Tick {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
|
||||
&.HandleTop {
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -3px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
|
||||
import {
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
@@ -13,35 +15,64 @@ import {
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
export const SolarSystemNodeTheme = memo(props => {
|
||||
const {
|
||||
charactersInSystem,
|
||||
classTitle,
|
||||
classTitleColor,
|
||||
customName,
|
||||
effectName,
|
||||
hasUserCharacters,
|
||||
hubs,
|
||||
visible,
|
||||
labelCustom,
|
||||
labelsInfo,
|
||||
locked,
|
||||
isShattered,
|
||||
isThickConnections,
|
||||
isWormhole,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
regionClass,
|
||||
regionName,
|
||||
status,
|
||||
selected,
|
||||
tag,
|
||||
showHandlers,
|
||||
systemName,
|
||||
sortedStatics,
|
||||
solarSystemId,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
dbClick: handleDbClick,
|
||||
} = useSolarSystemNode(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nodeVars.visible && (
|
||||
{visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
{labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.isShattered && (
|
||||
{isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
||||
{killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
<span className={clsx(classes.text)}>{killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
{labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
@@ -50,31 +81,18 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
)}
|
||||
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
|
||||
[classes.selected]: selected,
|
||||
})}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
{visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classTitle,
|
||||
nodeVars.classTitleColor,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
|
||||
)}
|
||||
>
|
||||
{nodeVars.classTitle ?? '-'}
|
||||
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
|
||||
{classTitle ?? '-'}
|
||||
</div>
|
||||
|
||||
{nodeVars.tag != null && nodeVars.tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle)}>{nodeVars.tag}</div>
|
||||
)}
|
||||
{tag != null && tag !== '' && <div className={clsx(classes.TagTitle)}>{tag}</div>}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -82,65 +100,63 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap',
|
||||
)}
|
||||
>
|
||||
{nodeVars.systemName}
|
||||
{systemName}
|
||||
</div>
|
||||
|
||||
{nodeVars.isWormhole && (
|
||||
{isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
{sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.effectName !== null && nodeVars.isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
|
||||
{effectName !== null && isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{nodeVars.customName && (
|
||||
{customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CustomName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
{nodeVars.customName}
|
||||
{customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
{!isWormhole && !customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RegionName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
{nodeVars.regionName}
|
||||
{regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
{isWormhole && !customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
|
||||
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
{hubs.includes(solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.charactersInSystem.length > 0 && (
|
||||
{charactersInSystem.length > 0 && (
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
||||
<span className="font-sans">{charactersInSystem.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -150,19 +166,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nodeVars.visible && (
|
||||
{visible && (
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
{unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
{unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
{unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
{unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
@@ -170,44 +186,44 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { SolarSystemNodeDefault, SolarSystemNodeTheme } from '../components/SolarSystemNode';
|
||||
import type { NodeProps } from 'reactflow';
|
||||
import type { ComponentType } from 'react';
|
||||
import { MapSolarSystemType } from '../map.types';
|
||||
import { ConnectionMode } from 'reactflow';
|
||||
|
||||
export type SolarSystemNodeComponent = ComponentType<NodeProps<MapSolarSystemType>>;
|
||||
|
||||
interface ThemeBehavior {
|
||||
isPanAndDrag: boolean;
|
||||
nodeComponent: SolarSystemNodeComponent;
|
||||
connectionMode: ConnectionMode;
|
||||
}
|
||||
|
||||
const THEME_BEHAVIORS: {
|
||||
[key: string]: ThemeBehavior;
|
||||
} = {
|
||||
default: {
|
||||
isPanAndDrag: false,
|
||||
nodeComponent: SolarSystemNodeDefault,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
},
|
||||
pathfinder: {
|
||||
isPanAndDrag: true,
|
||||
nodeComponent: SolarSystemNodeTheme,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
},
|
||||
};
|
||||
|
||||
export function getBehaviorForTheme(themeName: string) {
|
||||
return THEME_BEHAVIORS[themeName] ?? THEME_BEHAVIORS.default;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export const useMapUpdateSystems = () => {
|
||||
return newSystem;
|
||||
});
|
||||
|
||||
update({ systems: out }, true);
|
||||
update({ systems: out });
|
||||
},
|
||||
[rf, update],
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
setTimeout(() => addConnections(data as CommandAddConnections), 100);
|
||||
break;
|
||||
case Commands.removeConnections:
|
||||
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.charactersUpdated:
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { MapSolarSystemType } from '../map.types';
|
||||
import { NodeProps } from 'reactflow';
|
||||
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
|
||||
@@ -10,7 +9,7 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
|
||||
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
|
||||
import { CharacterTypeRaw, OutCommand } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
||||
|
||||
function getActivityType(count: number) {
|
||||
@@ -31,8 +30,8 @@ function sortedLabels(labels: string[]) {
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
|
||||
}
|
||||
|
||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const { id, data, selected } = props;
|
||||
export function useSolarSystemNode(props: any) {
|
||||
const { data, selected, id } = props;
|
||||
const {
|
||||
system_static_info,
|
||||
system_signatures,
|
||||
@@ -58,15 +57,11 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
solar_system_name,
|
||||
} = system_static_info;
|
||||
|
||||
const {
|
||||
interfaceSettings,
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
} = useMapRootState();
|
||||
|
||||
// Global map state
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
const { isShowUnsplashedSignatures } = interfaceSettings;
|
||||
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
|
||||
const isShowLinkedSigId = useMapGetOption('show_linked_signature_id') === 'true';
|
||||
const isShowLinkedSigIdTempName = useMapGetOption('show_linked_signature_id_temp_name') === 'true';
|
||||
|
||||
const {
|
||||
data: {
|
||||
@@ -85,13 +80,9 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
|
||||
// logic
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const systemSignatures = useMemo(
|
||||
() => mapSystemSignatures[solar_system_id] || system_signatures,
|
||||
[system_signatures, solar_system_id, mapSystemSignatures],
|
||||
);
|
||||
|
||||
const charactersInSystem = useMemo(() => {
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
|
||||
// eslint-disable-next-line
|
||||
@@ -110,12 +101,13 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
|
||||
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
|
||||
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
|
||||
const labelCustom = useMemo(() => {
|
||||
if (isShowLinkedSigId && linkedSigPrefix) {
|
||||
return labelsManager.customLabel ? `${linkedSigPrefix}・${labelsManager.customLabel}` : linkedSigPrefix;
|
||||
}
|
||||
return labelsManager.customLabel;
|
||||
}, [linkedSigPrefix, isShowLinkedSigId, labelsManager]);
|
||||
const labelCustom = useMemo(
|
||||
() =>
|
||||
isShowLinkedSigId && linkedSigPrefix
|
||||
? `${linkedSigPrefix}・${labelsManager.customLabel}`
|
||||
: labelsManager.customLabel,
|
||||
[linkedSigPrefix, isShowLinkedSigId, labelsManager],
|
||||
);
|
||||
|
||||
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
|
||||
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
|
||||
@@ -136,33 +128,15 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
|
||||
const temporaryName = useMemo(() => {
|
||||
if (!isTempSystemNameEnabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
}
|
||||
|
||||
return temporary_name;
|
||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
||||
|
||||
const systemName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && temporaryName) {
|
||||
return temporaryName;
|
||||
}
|
||||
return solar_system_name;
|
||||
}, [isTempSystemNameEnabled, solar_system_name, temporaryName]);
|
||||
|
||||
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name) || null;
|
||||
const systemName = (isTempSystemNameEnabled && temporary_name) || solar_system_name;
|
||||
const customName = (isTempSystemNameEnabled && temporary_name && name) || (solar_system_name !== name && name);
|
||||
|
||||
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
return [[], []];
|
||||
}
|
||||
return prepareUnsplashedChunks(
|
||||
systemSignatures
|
||||
system_signatures
|
||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||
.map(s => ({
|
||||
eve_id: s.eve_id,
|
||||
@@ -170,12 +144,13 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
custom_info: s.custom_info,
|
||||
})),
|
||||
);
|
||||
}, [isShowUnsplashedSignatures, systemSignatures]);
|
||||
}, [isShowUnsplashedSignatures, system_signatures]);
|
||||
|
||||
const nodeVars = {
|
||||
// original props
|
||||
id,
|
||||
selected,
|
||||
|
||||
// computed
|
||||
visible,
|
||||
isWormhole,
|
||||
classTitleColor,
|
||||
@@ -212,43 +187,3 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
|
||||
return nodeVars;
|
||||
}
|
||||
|
||||
export interface SolarSystemNodeVars {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
visible: boolean;
|
||||
isWormhole: boolean;
|
||||
classTitleColor: string | null;
|
||||
killsCount: number | null;
|
||||
killsActivityType: string | null;
|
||||
hasUserCharacters: boolean;
|
||||
showHandlers: boolean;
|
||||
regionClass: string | null;
|
||||
systemName: string;
|
||||
customName?: string | null;
|
||||
labelCustom: string | null;
|
||||
isShattered: boolean;
|
||||
tag?: string | null;
|
||||
status?: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
labelsInfo: Array<any>;
|
||||
dbClick: (event?: void) => void;
|
||||
sortedStatics: Array<string | number>;
|
||||
effectName: string | null;
|
||||
regionName: string | null;
|
||||
solarSystemId: number;
|
||||
solarSystemName: string | null;
|
||||
locked: boolean;
|
||||
hubs: string[] | number[];
|
||||
name: string | null;
|
||||
isConnecting: boolean;
|
||||
hoverNodeId: string | null;
|
||||
charactersInSystem: Array<CharacterTypeRaw>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
unsplashedLeft: Array<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
unsplashedRight: Array<any>;
|
||||
isThickConnections: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
}
|
||||
|
||||
@@ -27,5 +27,5 @@
|
||||
--text-color: #ffffff;
|
||||
--tooltip-bg: #202020;
|
||||
|
||||
--window-corner: #72716f;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
@import './eve-common-variables';
|
||||
@import './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
@@ -39,6 +40,7 @@
|
||||
--eve-wh-type-color-c13: #7986cb;
|
||||
--eve-wh-type-color-drifter: #44aa82;
|
||||
|
||||
|
||||
--rf-node-font-weight: bold;
|
||||
--rf-node-line-height: normal;
|
||||
--rf-node-font-family: 'Oxygen', sans-serif;
|
||||
@@ -46,6 +48,4 @@
|
||||
|
||||
--rf-tag-color: #fbbf24;
|
||||
--rf-has-user-characters: #5cb85c;
|
||||
|
||||
--window-corner: #72716f;
|
||||
}
|
||||
}
|
||||
11
assets/js/hooks/Mapper/components/map/utils/wrapNode.tsx
Normal file
11
assets/js/hooks/Mapper/components/map/utils/wrapNode.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// wrapNode.ts
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { SolarSystemNodeProps } from '../components/SolarSystemNode';
|
||||
|
||||
export function wrapNode<T>(
|
||||
SolarSystemNode: React.FC<SolarSystemNodeProps<T>>
|
||||
): React.FC<NodeProps<T>> {
|
||||
return function NodeAdapter(props) {
|
||||
return <SolarSystemNode {...props} />;
|
||||
};
|
||||
}
|
||||
@@ -1,25 +1,78 @@
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useMemo } from 'react';
|
||||
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WidgetGridItem, WidgetsGrid } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import {
|
||||
LocalCharacters,
|
||||
RoutesWidget,
|
||||
SystemInfo,
|
||||
SystemSignatures,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { useState } from 'react';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
// import { debounce } from 'lodash/debounce';
|
||||
|
||||
const DEFAULT_WINDOWS = [
|
||||
{
|
||||
name: 'info',
|
||||
rightOffset: 5,
|
||||
width: 5,
|
||||
height: 4,
|
||||
item: () => <SystemInfo />,
|
||||
},
|
||||
{
|
||||
name: 'local',
|
||||
rightOffset: 5,
|
||||
topOffset: 4,
|
||||
width: 5,
|
||||
height: 4,
|
||||
item: () => <LocalCharacters />,
|
||||
},
|
||||
{ name: 'signatures', width: 8, height: 4, topOffset: 8, rightOffset: 12, item: () => <SystemSignatures /> },
|
||||
{
|
||||
name: 'routes',
|
||||
rightOffset: 0,
|
||||
topOffset: 8,
|
||||
width: 5,
|
||||
height: 6,
|
||||
item: () => <RoutesWidget />,
|
||||
},
|
||||
];
|
||||
|
||||
const saveWindowsToLS = (toSaveItems: WidgetGridItem[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const out = toSaveItems.map(({ item, ...rest }) => rest);
|
||||
localStorage.setItem(SESSION_KEY.windows, JSON.stringify(out));
|
||||
};
|
||||
|
||||
const restoreWindowsFromLS = (): WidgetGridItem[] => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const raw = localStorage.getItem(SESSION_KEY.windows);
|
||||
if (!raw) {
|
||||
console.warn('No windows found in local storage!!');
|
||||
return DEFAULT_WINDOWS;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-debugger
|
||||
const out = (JSON.parse(raw) as Omit<WidgetGridItem, 'item'>[])
|
||||
.filter(x => DEFAULT_WINDOWS.find(def => def.name === x.name))
|
||||
.map(x => {
|
||||
const windowItem = DEFAULT_WINDOWS.find(def => def.name === x.name)?.item;
|
||||
return { ...x, item: windowItem! };
|
||||
});
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
export const MapInterface = () => {
|
||||
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
|
||||
const { windowsSettings, updateWidgetSettings } = useMapRootState();
|
||||
const [items, setItems] = useState<WidgetGridItem[]>(restoreWindowsFromLS);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return windowsSettings.windows
|
||||
.map(x => {
|
||||
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;
|
||||
return {
|
||||
...x,
|
||||
content: content!,
|
||||
};
|
||||
})
|
||||
.filter(x => windowsSettings.visible.some(j => x.id === j));
|
||||
}, [windowsSettings]);
|
||||
|
||||
return <WindowManager windows={items} dragSelector=".react-grid-dragHandleExample" onChange={updateWidgetSettings} />;
|
||||
return (
|
||||
<WidgetsGrid
|
||||
items={items}
|
||||
onChange={x => {
|
||||
saveWindowsToLS(x);
|
||||
setItems(x);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
@@ -58,7 +58,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
<Dialog
|
||||
header="Select signature to link"
|
||||
visible
|
||||
draggable={true}
|
||||
draggable={false}
|
||||
style={{ width: '500px' }}
|
||||
onHide={handleHide}
|
||||
contentClassName="!p-0"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.GridLayoutWrapper {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.GridLayout {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
pointer-events: none;
|
||||
|
||||
& > div {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
:global {
|
||||
.react-resizable-handle::after {
|
||||
border-color: #696969 !important;
|
||||
}
|
||||
|
||||
.react-grid-placeholder {
|
||||
background-color: rgba(147, 147, 147, 0.3);
|
||||
//filter: blur(5px);
|
||||
border: 2px dashed #b6b6b6;
|
||||
}
|
||||
|
||||
.react-grid-item {
|
||||
transition-property: none !important;
|
||||
}
|
||||
|
||||
.react-grid-item.cssTransforms {
|
||||
transition-property: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import classes from './WidgetsGrid.module.scss';
|
||||
import { ItemCallback, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
|
||||
import clsx from 'clsx';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
const colSize = 50;
|
||||
const initState = { breakpoints: 100, cols: 2 };
|
||||
|
||||
export type WidgetGridItem = {
|
||||
rightOffset?: number;
|
||||
leftOffset?: number;
|
||||
topOffset?: number;
|
||||
width: number;
|
||||
height: number;
|
||||
name: string;
|
||||
item: () => React.ReactNode;
|
||||
};
|
||||
|
||||
export interface WidgetsGridProps {
|
||||
items: WidgetGridItem[];
|
||||
onChange: (items: WidgetGridItem[]) => void;
|
||||
}
|
||||
|
||||
export const WidgetsGrid = ({ items, onChange }: WidgetsGridProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [, setKey] = useState(0);
|
||||
const [callRerenderOfGrid, setCallRerenderOfGrid] = useState(0);
|
||||
|
||||
const isTabVisible = usePageVisibility();
|
||||
|
||||
const refAll = useRef({
|
||||
isReady: false,
|
||||
layouts: {
|
||||
lg: [
|
||||
// { i: 'a', w: 4, h: 16, x: 22, y: 0 },
|
||||
// { i: 'b', w: 5, h: 10, x: 17, y: 0 },
|
||||
],
|
||||
} as Layouts,
|
||||
breakpoints: { lg: 100, md: 0, sm: 0, xs: 0, xxs: 0 },
|
||||
cols: { lg: 26, md: 0, sm: 0, xs: 0, xxs: 0 },
|
||||
containerWidth: 0,
|
||||
colsPrev: 26,
|
||||
needPostProcess: false,
|
||||
items: [...items],
|
||||
});
|
||||
|
||||
// TODO
|
||||
// 1. onLayoutChange (original) not calling when we change x of any widget
|
||||
// 2. setKey need no call rerender for update props
|
||||
const onLayoutChange: ItemCallback = (newItems, _, newItem) => {
|
||||
const updatedItems = newItems.map(item => {
|
||||
const toLeft = (item.x + item.w / 2) / refAll.current.cols.lg <= 0.5;
|
||||
const original = refAll.current.items.find(x => x.name === item.i)!;
|
||||
|
||||
return {
|
||||
...original,
|
||||
width: item.w,
|
||||
height: item.h,
|
||||
leftOffset: toLeft ? item.x : undefined,
|
||||
rightOffset: !toLeft ? refAll.current.cols.lg - (item.x + item.w) : undefined,
|
||||
topOffset: item.y,
|
||||
};
|
||||
});
|
||||
|
||||
const sortedItems = [
|
||||
...updatedItems.filter(x => x.name !== newItem.i),
|
||||
updatedItems.find(x => x.name === newItem.i)!,
|
||||
];
|
||||
|
||||
refAll.current.layouts = {
|
||||
lg: [...newItems.filter(x => x.i !== newItem.i), newItem],
|
||||
};
|
||||
|
||||
onChange(sortedItems);
|
||||
setKey(x => x + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refAll.current.items = [...items];
|
||||
setKey(x => x + 1);
|
||||
}, [items]);
|
||||
|
||||
// TODO
|
||||
// 1. Unknown why but if we set layout and cols both instantly it not help...
|
||||
// 1.2 it means that we should make report... until we will send new key on window resize
|
||||
useEffect(() => {
|
||||
const updateItems = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width } = containerRef.current.getBoundingClientRect();
|
||||
const newColsCount = (width - (width % colSize)) / colSize;
|
||||
|
||||
refAll.current.layouts = {
|
||||
lg: refAll.current.items.map(({ name, width, height, rightOffset, leftOffset, topOffset = 0 }) => {
|
||||
return {
|
||||
i: name,
|
||||
x: rightOffset != null ? newColsCount - width - rightOffset : leftOffset ?? 0,
|
||||
y: topOffset,
|
||||
w: width,
|
||||
h: height,
|
||||
};
|
||||
}),
|
||||
};
|
||||
refAll.current.cols = { lg: newColsCount, md: 0, sm: 0, xs: 0, xxs: 0 };
|
||||
};
|
||||
|
||||
const updateContainerWidth = () => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width } = containerRef.current.getBoundingClientRect();
|
||||
|
||||
refAll.current.containerWidth = width;
|
||||
const newColsCount = (width - (width % colSize)) / colSize;
|
||||
|
||||
if (width <= 100 || refAll.current.cols.lg === newColsCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!refAll.current.isReady) {
|
||||
updateItems();
|
||||
setCallRerenderOfGrid(x => x + 1);
|
||||
refAll.current.isReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
refAll.current.layouts = {
|
||||
lg: refAll.current.layouts.lg.map(lgEl => {
|
||||
const toLeft = (lgEl.x + lgEl.w / 2) / refAll.current.cols.lg <= 0.5;
|
||||
const next = {
|
||||
...lgEl,
|
||||
x: toLeft ? lgEl.x : newColsCount - (refAll.current.cols.lg - lgEl.x),
|
||||
};
|
||||
return next;
|
||||
}),
|
||||
};
|
||||
|
||||
refAll.current.cols = { lg: newColsCount, md: 0, sm: 0, xs: 0, xxs: 0 };
|
||||
setCallRerenderOfGrid(x => x + 1);
|
||||
};
|
||||
|
||||
setTimeout(() => updateContainerWidth(), 100);
|
||||
|
||||
const withRerender = () => {
|
||||
updateContainerWidth();
|
||||
setCallRerenderOfGrid(x => x + 1);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', withRerender);
|
||||
return () => {
|
||||
window.removeEventListener('resize', withRerender);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isNotSet = initState.cols === refAll.current.cols.lg;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={clsx(classes.GridLayoutWrapper, 'relative p-4')}>
|
||||
{!isNotSet && isTabVisible && (
|
||||
<ResponsiveGridLayout
|
||||
key={callRerenderOfGrid}
|
||||
className={classes.GridLayout}
|
||||
layouts={refAll.current.layouts}
|
||||
breakpoints={refAll.current.breakpoints}
|
||||
cols={refAll.current.cols}
|
||||
rowHeight={30}
|
||||
width={refAll.current.containerWidth}
|
||||
preventCollision={true}
|
||||
compactType={null}
|
||||
allowOverlap
|
||||
onDragStop={onLayoutChange}
|
||||
onResizeStop={onLayoutChange}
|
||||
// onResizeStart={onLayoutChange}
|
||||
// onDragStart={onLayoutChange}
|
||||
isBounded
|
||||
containerPadding={[0, 0]}
|
||||
resizeHandles={['sw', 'se']}
|
||||
draggableHandle=".react-grid-dragHandleExample"
|
||||
>
|
||||
{refAll.current.items.map(x => (
|
||||
<div key={x.name} className="grid-item">
|
||||
{x.item()}
|
||||
</div>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WidgetsGrid';
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Widget';
|
||||
export * from './WidgetsGrid';
|
||||
export * from './SystemSettingsDialog';
|
||||
export * from './SystemCustomLabelDialog';
|
||||
export * from './SystemLinkSignatureDialog';
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
import {
|
||||
LocalCharacters,
|
||||
RoutesWidget,
|
||||
SystemInfo,
|
||||
SystemSignatures,
|
||||
SystemStructures,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
export const CURRENT_WINDOWS_VERSION = 8;
|
||||
export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2';
|
||||
|
||||
export enum WidgetsIds {
|
||||
info = 'info',
|
||||
signatures = 'signatures',
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
structures = 'structures',
|
||||
}
|
||||
|
||||
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
|
||||
WidgetsIds.info,
|
||||
WidgetsIds.local,
|
||||
WidgetsIds.routes,
|
||||
WidgetsIds.signatures,
|
||||
];
|
||||
|
||||
export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
{
|
||||
id: WidgetsIds.info,
|
||||
position: { x: 10, y: 10 },
|
||||
size: { width: 250, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemInfo />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.signatures,
|
||||
position: { x: 10, y: 220 },
|
||||
size: { width: 250, height: 300 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemSignatures />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.local,
|
||||
position: { x: 270, y: 10 },
|
||||
size: { width: 250, height: 510 },
|
||||
zIndex: 0,
|
||||
content: () => <LocalCharacters />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routes,
|
||||
position: { x: 10, y: 530 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <RoutesWidget />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.structures,
|
||||
position: { x: 10, y: 730 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemStructures />,
|
||||
},
|
||||
];
|
||||
|
||||
type WidgetsCheckboxesType = {
|
||||
id: WidgetsIds;
|
||||
label: string;
|
||||
}[];
|
||||
|
||||
export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
{
|
||||
id: WidgetsIds.info,
|
||||
label: 'System Info',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.signatures,
|
||||
label: 'Signatures',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.local,
|
||||
label: 'Local',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routes,
|
||||
label: 'Routes',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.structures,
|
||||
label: 'Structures',
|
||||
},
|
||||
];
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { useCallback, ClipboardEvent, useRef } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
WdImgButton,
|
||||
TooltipPosition,
|
||||
InfoDrawer,
|
||||
SystemView,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
|
||||
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
|
||||
import { useSystemStructures } from './hooks/useSystemStructures';
|
||||
import { processSnippetText } from './helpers';
|
||||
|
||||
export const SystemStructures: React.FC = () => {
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
const [systemId] = selectedSystems;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
|
||||
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(labelRef, 260);
|
||||
|
||||
const processClipboard = useCallback(
|
||||
(text: string) => {
|
||||
const updated = processSnippetText(text, structures);
|
||||
handleUpdateStructures(updated);
|
||||
},
|
||||
[structures, handleUpdateStructures],
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(e: ClipboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
processClipboard(e.clipboardData.getData('text'));
|
||||
},
|
||||
[processClipboard],
|
||||
);
|
||||
|
||||
const handlePasteTimer = useCallback(async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
processClipboard(text);
|
||||
} catch (err) {
|
||||
console.error('Clipboard read error:', err);
|
||||
}
|
||||
}, [processClipboard]);
|
||||
|
||||
function renderWidgetLabel() {
|
||||
return (
|
||||
<div className="flex justify-between items-center text-xs w-full h-full" ref={labelRef}>
|
||||
<div className="flex justify-between items-center gap-1">
|
||||
{!isCompact && (
|
||||
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
|
||||
Structures
|
||||
{!isNotSelectedSystem && ' in'}
|
||||
</div>
|
||||
)}
|
||||
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2.5">
|
||||
<WdImgButton
|
||||
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
|
||||
onClick={handlePasteTimer}
|
||||
/>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.QUESTION_CIRCLE}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: (
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add/update structures?</b>}>
|
||||
In game, select one or more structures in D-Scan and then
|
||||
<br />
|
||||
use the blue add structure data button
|
||||
</InfoDrawer>
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add a timer?</b>}>
|
||||
In game, select a structure with an active timer, right click to copy, and then
|
||||
<span className="text-blue-500"> blue </span>
|
||||
use the blue add structure data button
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div tabIndex={0} onPaste={handlePaste} className="h-full flex flex-col" style={{ outline: 'none' }}>
|
||||
<Widget label={renderWidgetLabel()}>
|
||||
{isNotSelectedSystem ? (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
System is not selected
|
||||
</div>
|
||||
) : (
|
||||
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
|
||||
)}
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
.TableRowCompact {
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
|
||||
.Tooltip {
|
||||
white-space: pre-line; // or pre-wrap
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { DataTable, DataTableRowClickEvent } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SystemStructuresDialog } from '../SystemStructuresDialog/SystemStructuresDialog';
|
||||
import { StructureItem } from '../helpers/structureTypes';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import classes from './SystemStructuresContent.module.scss';
|
||||
import { renderOwnerCell, renderTypeCell, renderTimerCell } from '../renders/cellRenders';
|
||||
|
||||
interface SystemStructuresContentProps {
|
||||
structures: StructureItem[];
|
||||
onUpdateStructures: (newList: StructureItem[]) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresContent: React.FC<SystemStructuresContentProps> = ({ structures, onUpdateStructures }) => {
|
||||
const [selectedRow, setSelectedRow] = useState<StructureItem | null>(null);
|
||||
const [editingItem, setEditingItem] = useState<StructureItem | null>(null);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const handleRowClick = (e: DataTableRowClickEvent) => {
|
||||
const row = e.data as StructureItem;
|
||||
setSelectedRow(prev => (prev?.id === row.id ? null : row));
|
||||
};
|
||||
|
||||
const handleRowDoubleClick = (e: DataTableRowClickEvent) => {
|
||||
setEditingItem(e.data as StructureItem);
|
||||
setShowEditDialog(true);
|
||||
};
|
||||
|
||||
// Press Delete => remove selected row
|
||||
const handleDeleteSelected = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!selectedRow) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newList = structures.filter(s => s.id !== selectedRow.id);
|
||||
onUpdateStructures(newList);
|
||||
setSelectedRow(null);
|
||||
},
|
||||
[selectedRow, structures, onUpdateStructures],
|
||||
);
|
||||
|
||||
useHotkey(false, ['Delete', 'Backspace'], handleDeleteSelected);
|
||||
|
||||
const visibleStructures = useMemo(() => {
|
||||
return structures;
|
||||
}, [structures]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2 text-xs text-stone-200 h-full">
|
||||
{visibleStructures.length === 0 ? (
|
||||
<div className="flex-1 flex justify-center items-center text-stone-400/80 text-sm">No structures</div>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
<DataTable
|
||||
value={visibleStructures}
|
||||
dataKey="id"
|
||||
className={clsx(classes.Table, 'w-full select-none h-full')}
|
||||
size="small"
|
||||
sortMode="single"
|
||||
rowHover
|
||||
onRowClick={handleRowClick}
|
||||
onRowDoubleClick={handleRowDoubleClick}
|
||||
rowClassName={rowData => {
|
||||
const isSelected = selectedRow?.id === rowData.id;
|
||||
return clsx(
|
||||
classes.TableRowCompact,
|
||||
'transition-colors duration-200 cursor-pointer',
|
||||
isSelected ? 'bg-amber-500/50 hover:bg-amber-500/70' : 'hover:bg-purple-400/20',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Column header="Type" body={renderTypeCell} style={{ width: '160px' }} />
|
||||
<Column field="name" header="Name" style={{ width: '120px' }} />
|
||||
<Column header="Owner" body={renderOwnerCell} style={{ width: '120px' }} />
|
||||
<Column field="status" header="Status" style={{ width: '100px' }} />
|
||||
<Column header="Timer" body={renderTimerCell} style={{ width: '110px' }} />
|
||||
<Column
|
||||
body={(rowData: StructureItem) => (
|
||||
<i
|
||||
className={clsx(PrimeIcons.PENCIL, 'text-[14px] cursor-pointer')}
|
||||
title="Edit"
|
||||
onClick={() => {
|
||||
setEditingItem(rowData);
|
||||
setShowEditDialog(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{ width: '40px', textAlign: 'center' }}
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEditDialog && editingItem && (
|
||||
<SystemStructuresDialog
|
||||
visible={showEditDialog}
|
||||
structure={editingItem}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onSave={(updatedItem: StructureItem) => {
|
||||
const newList = structures.map(s => (s.id === updatedItem.id ? updatedItem : s));
|
||||
onUpdateStructures(newList);
|
||||
setShowEditDialog(false);
|
||||
}}
|
||||
onDelete={(deleteId: string) => {
|
||||
const newList = structures.filter(s => s.id !== deleteId);
|
||||
onUpdateStructures(newList);
|
||||
setShowEditDialog(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
.systemStructureDialog {
|
||||
|
||||
.p-dialog-content {
|
||||
background-color: var(--surface-800) !important;
|
||||
}
|
||||
|
||||
.p-dialog-header {
|
||||
background-color: var(--surface-700);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icon,
|
||||
.p-dialog-header-title {
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.p-inputtext {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ddd !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
.p-button {
|
||||
font-size: 12px !important;
|
||||
padding: 0.3rem 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { StructureItem, StructureStatus, statusesRequiringTimer, formatToISO } from '../helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
|
||||
interface StructuresEditDialogProps {
|
||||
visible: boolean;
|
||||
structure?: StructureItem;
|
||||
onClose: () => void;
|
||||
onSave: (updatedItem: StructureItem) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
visible,
|
||||
structure,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
}) => {
|
||||
const [editData, setEditData] = useState<StructureItem | null>(null);
|
||||
const [ownerInput, setOwnerInput] = useState('');
|
||||
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (structure) {
|
||||
setEditData(structure);
|
||||
setOwnerInput(structure.ownerName ?? '');
|
||||
} else {
|
||||
setEditData(null);
|
||||
setOwnerInput('');
|
||||
}
|
||||
}, [structure]);
|
||||
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item =>
|
||||
item.label.toLowerCase().includes(newQuery.toLowerCase()),
|
||||
);
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch owners:', err);
|
||||
setOwnerSuggestions([]);
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
);
|
||||
|
||||
const handleChange = (field: keyof StructureItem, val: string | Date) => {
|
||||
// If we want to forbid changing structureTypeId or structureType from the dialog, do so here:
|
||||
if (field === 'structureTypeId' || field === 'structureType') return;
|
||||
|
||||
setEditData(prev => {
|
||||
if (!prev) return null;
|
||||
|
||||
// If this is the endTime (Date from Calendar), we store as ISO or string:
|
||||
if (field === 'endTime' && val instanceof Date) {
|
||||
return { ...prev, endTime: val.toISOString() };
|
||||
}
|
||||
|
||||
return { ...prev, [field]: val };
|
||||
});
|
||||
};
|
||||
|
||||
// when user picks a corp from auto-complete
|
||||
const handleSelectOwner = (selected: { label: string; value: string }) => {
|
||||
setOwnerInput(selected.label);
|
||||
setEditData(prev =>
|
||||
prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null,
|
||||
);
|
||||
};
|
||||
|
||||
const handleStatusChange = (val: string) => {
|
||||
setEditData(prev => {
|
||||
if (!prev) return null;
|
||||
const newStatus = val as StructureStatus;
|
||||
// If new status doesn't require a timer, we clear out endTime
|
||||
const newEndTime = statusesRequiringTimer.includes(newStatus) ? prev.endTime : '';
|
||||
return { ...prev, status: newStatus, endTime: newEndTime };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
if (!editData) return;
|
||||
|
||||
// If status doesn't require a timer, clear endTime
|
||||
if (!statusesRequiringTimer.includes(editData.status)) {
|
||||
editData.endTime = '';
|
||||
} else if (editData.endTime) {
|
||||
// convert to full ISO if not already
|
||||
editData.endTime = formatToISO(editData.endTime);
|
||||
}
|
||||
|
||||
// fetch corporation ticker if we have an ownerId
|
||||
if (editData.ownerId) {
|
||||
try {
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: editData.ownerId },
|
||||
});
|
||||
editData.ownerTicker = ticker ?? '';
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ticker:', err);
|
||||
editData.ownerTicker = '';
|
||||
}
|
||||
}
|
||||
|
||||
onSave(editData);
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
if (!editData) return;
|
||||
onDelete(editData.id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!editData) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onHide={onClose}
|
||||
header={`Edit Structure - ${editData.name ?? ''}`}
|
||||
className={clsx('myStructuresDialog', 'text-stone-200 w-full max-w-md')}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-[14px]">
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Type:</span>
|
||||
<input
|
||||
readOnly
|
||||
className="p-inputtext p-component cursor-not-allowed"
|
||||
value={editData.structureType ?? ''}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Name:</span>
|
||||
<input
|
||||
className="p-inputtext p-component"
|
||||
value={editData.name ?? ''}
|
||||
onChange={e => handleChange('name', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Owner:</span>
|
||||
<AutoComplete
|
||||
id="owner"
|
||||
value={ownerInput}
|
||||
suggestions={ownerSuggestions}
|
||||
completeMethod={searchOwners}
|
||||
minLength={3}
|
||||
delay={400}
|
||||
field="label"
|
||||
placeholder="Corporation name..."
|
||||
onChange={e => setOwnerInput(e.value)}
|
||||
onSelect={e => handleSelectOwner(e.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Status:</span>
|
||||
<select
|
||||
className="p-inputtext p-component"
|
||||
value={editData.status}
|
||||
onChange={e => handleStatusChange(e.target.value)}
|
||||
>
|
||||
<option value="Powered">Powered</option>
|
||||
<option value="Anchoring">Anchoring</option>
|
||||
<option value="Unanchoring">Unanchoring</option>
|
||||
<option value="Low Power">Low Power</option>
|
||||
<option value="Abandoned">Abandoned</option>
|
||||
<option value="Reinforced">Reinforced</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{statusesRequiringTimer.includes(editData.status) && (
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Timer <br /> (Eve Time):</span>
|
||||
<Calendar
|
||||
value={editData.endTime ? new Date(editData.endTime) : undefined}
|
||||
onChange={(e) => handleChange('endTime', e.value ?? '')}
|
||||
showTime
|
||||
hourFormat="24"
|
||||
dateFormat="yy-mm-dd"
|
||||
showIcon
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
|
||||
<span className="mt-1">Notes:</span>
|
||||
<textarea
|
||||
className="p-inputtext p-component resize-none h-24"
|
||||
value={editData.notes ?? ''}
|
||||
onChange={e => handleChange('notes', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 mt-4">
|
||||
<Button label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
|
||||
<Button label="Save" className="p-button-sm" onClick={handleSaveClick} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './parserHelper';
|
||||
export * from './pasteParser';
|
||||
export * from './structureTypes';
|
||||
export * from './structureUtils';
|
||||
@@ -1,92 +0,0 @@
|
||||
import { StructureStatus, StructureItem, STRUCTURE_TYPE_MAP } from './structureTypes';
|
||||
import { formatToISO } from './structureUtils';
|
||||
|
||||
// Up to you if you'd like to keep a separate constant here or not
|
||||
export const statusesRequiringTimer: StructureStatus[] = ['Anchoring', 'Reinforced'];
|
||||
|
||||
/**
|
||||
* parseFormatOneLine(line):
|
||||
* - Splits by tabs
|
||||
* - First col => structureTypeId
|
||||
* - Second col => rawName
|
||||
* - Third col => structureTypeName
|
||||
*/
|
||||
export function parseFormatOneLine(line: string): StructureItem | null {
|
||||
const columns = line
|
||||
.split('\t')
|
||||
.map(c => c.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// Expecting e.g. "35832 J214811 - SomeName Astrahus"
|
||||
if (columns.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [rawTypeId, rawName, rawTypeName] = columns;
|
||||
|
||||
if (columns.length != 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!STRUCTURE_TYPE_MAP[rawTypeId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawTypeName != STRUCTURE_TYPE_MAP[rawTypeId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = rawName.replace(/^J\d{6}\s*-\s*/, '').trim();
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
structureTypeId: rawTypeId,
|
||||
structureType: rawTypeName,
|
||||
name,
|
||||
ownerName: '',
|
||||
notes: '',
|
||||
status: 'Powered', // Default
|
||||
endTime: '', // No timer by default
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesThreeLineSnippet(lines: string[]): boolean {
|
||||
if (lines.length < 3) return false;
|
||||
return /until\s+\d{4}\.\d{2}\.\d{2}/i.test(lines[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseThreeLineSnippet:
|
||||
* - Example lines:
|
||||
* line1: "J214811 - Folgers"
|
||||
* line2: "1,475 km"
|
||||
* line3: "Reinforced until 2025.01.13 23:51"
|
||||
*/
|
||||
export function parseThreeLineSnippet(lines: string[]): StructureItem {
|
||||
const [line1, , line3] = lines;
|
||||
|
||||
let status: StructureStatus = 'Reinforced';
|
||||
let endTime: string | undefined;
|
||||
|
||||
// e.g. "Reinforced until 2025.01.13 23:27"
|
||||
const match = line3.match(/^(?<stat>\w+)\s+until\s+(?<dateTime>[\d.]+\s+[\d:]+)/i);
|
||||
|
||||
if (match?.groups?.stat) {
|
||||
const candidateStatus = match.groups.stat as StructureStatus;
|
||||
if (statusesRequiringTimer.includes(candidateStatus)) {
|
||||
status = candidateStatus;
|
||||
}
|
||||
}
|
||||
if (match?.groups?.dateTime) {
|
||||
let dt = match.groups.dateTime.trim().replace(/\./g, '-'); // "2025-01-13 23:27"
|
||||
dt = dt.replace(' ', 'T'); // "2025-01-13T23:27"
|
||||
endTime = formatToISO(dt); // => "2025-01-13T23:27:00Z"
|
||||
}
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: line1.replace(/^J\d{6}\s*-\s*/, '').trim(),
|
||||
status,
|
||||
endTime,
|
||||
};
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { StructureItem } from './structureTypes';
|
||||
import { parseThreeLineSnippet, parseFormatOneLine, matchesThreeLineSnippet } from './parserHelper';
|
||||
|
||||
export function processSnippetText(rawText: string, existingStructures: StructureItem[]): StructureItem[] {
|
||||
if (!rawText) {
|
||||
return existingStructures.slice();
|
||||
}
|
||||
|
||||
const lines = rawText
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (lines.length === 3 && matchesThreeLineSnippet(lines)) {
|
||||
return applyThreeLineSnippet(lines, existingStructures);
|
||||
} else {
|
||||
return applySingleLineParse(lines, existingStructures);
|
||||
}
|
||||
}
|
||||
|
||||
function applyThreeLineSnippet(snippetLines: string[], existingStructures: StructureItem[]): StructureItem[] {
|
||||
const updatedList = [...existingStructures];
|
||||
const snippetItem = parseThreeLineSnippet(snippetLines);
|
||||
|
||||
const existingIndex = updatedList.findIndex(s => s.name.trim() === snippetItem.name.trim());
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existing = updatedList[existingIndex];
|
||||
updatedList[existingIndex] = {
|
||||
...existing,
|
||||
status: snippetItem.status,
|
||||
endTime: snippetItem.endTime,
|
||||
};
|
||||
}
|
||||
|
||||
return updatedList;
|
||||
}
|
||||
|
||||
function applySingleLineParse(lines: string[], existingStructures: StructureItem[]): StructureItem[] {
|
||||
const updatedList = [...existingStructures];
|
||||
const newItems: StructureItem[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const item = parseFormatOneLine(line);
|
||||
if (!item) continue;
|
||||
|
||||
const isDuplicate = updatedList.some(
|
||||
s => s.structureTypeId === item.structureTypeId && s.name.trim() === item.name.trim(),
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [...updatedList, ...newItems];
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
export type StructureStatus = 'Powered' | 'Anchoring' | 'Unanchoring' | 'Low Power' | 'Abandoned' | 'Reinforced';
|
||||
|
||||
export interface StructureItem {
|
||||
id: string;
|
||||
systemId?: string;
|
||||
structureTypeId?: string;
|
||||
structureType?: string;
|
||||
name: string;
|
||||
ownerName?: string;
|
||||
ownerId?: string;
|
||||
ownerTicker?: string;
|
||||
notes?: string;
|
||||
status: StructureStatus;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
export const STRUCTURE_TYPE_MAP: Record<string, string> = {
|
||||
'35825': 'Raitaru',
|
||||
'35826': 'Azbel',
|
||||
'35827': 'Sotiyo',
|
||||
'35832': 'Astrahus',
|
||||
'35833': 'Fortizar',
|
||||
'35834': 'Keepstar',
|
||||
'35835': 'Athanor',
|
||||
'35836': 'Tatara',
|
||||
'40340': 'Upwell Palatine Keepstar',
|
||||
'47512': "'Moreau' Fortizar",
|
||||
'47513': "'Draccous' Fortizar",
|
||||
'47514': "'Horizon' Fortizar",
|
||||
'47515': "'Marginis' Fortizar",
|
||||
'47516': "'Prometheus' Fortizar",
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { StructureItem } from './structureTypes';
|
||||
|
||||
export function getActualStructures(oldList: StructureItem[], newList: StructureItem[]) {
|
||||
const oldMap = new Map(oldList.map(s => [s.id, s]));
|
||||
const newMap = new Map(newList.map(s => [s.id, s]));
|
||||
|
||||
const added: StructureItem[] = [];
|
||||
const updated: StructureItem[] = [];
|
||||
const removed: StructureItem[] = [];
|
||||
|
||||
for (const newItem of newList) {
|
||||
const oldItem = oldMap.get(newItem.id);
|
||||
if (!oldItem) {
|
||||
added.push(newItem);
|
||||
} else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
|
||||
updated.push(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldItem of oldList) {
|
||||
if (!newMap.has(oldItem.id)) {
|
||||
removed.push(oldItem);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, updated, removed };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function mapServerStructure(serverData: any): StructureItem {
|
||||
const { owner_id, owner_ticker, structure_type_id, structure_type, owner_name, end_time, system_id, ...rest } =
|
||||
serverData;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
ownerId: owner_id,
|
||||
ownerTicker: owner_ticker,
|
||||
ownerName: owner_name,
|
||||
structureType: structure_type,
|
||||
structureTypeId: structure_type_id,
|
||||
endTime: end_time ?? '',
|
||||
systemId: system_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatToISO(datetimeLocal: string): string {
|
||||
if (!datetimeLocal) return '';
|
||||
|
||||
// If missing seconds, add :00
|
||||
let iso = datetimeLocal;
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(iso)) {
|
||||
iso += ':00';
|
||||
}
|
||||
// Ensure trailing 'Z'
|
||||
if (!iso.endsWith('Z')) {
|
||||
iso += 'Z';
|
||||
}
|
||||
return iso;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { mapServerStructure, getActualStructures, StructureItem, statusesRequiringTimer } from '../helpers';
|
||||
|
||||
interface UseSystemStructuresProps {
|
||||
systemId: string | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
outCommand: (payload: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export function useSystemStructures({ systemId, outCommand }: UseSystemStructuresProps) {
|
||||
const [structures, setStructures] = useState<StructureItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchStructures = useCallback(async () => {
|
||||
if (!systemId) {
|
||||
setStructures([]);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const { structures: fetched = [] } = await outCommand({
|
||||
type: OutCommand.getStructures,
|
||||
data: { system_id: systemId },
|
||||
});
|
||||
|
||||
const mappedStructures = fetched.map(mapServerStructure);
|
||||
setStructures(mappedStructures);
|
||||
} catch (err) {
|
||||
console.error('Failed to get structures:', err);
|
||||
setError('Error fetching structures');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [systemId, outCommand]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStructures();
|
||||
}, [fetchStructures]);
|
||||
|
||||
const sanitizeEndTimers = useCallback((item: StructureItem) => {
|
||||
if (!statusesRequiringTimer.includes(item.status)) {
|
||||
item.endTime = '';
|
||||
}
|
||||
return item;
|
||||
}, []);
|
||||
|
||||
const sanitizeIds = useCallback((item: StructureItem) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, ...rest } = item;
|
||||
return rest;
|
||||
}, []);
|
||||
|
||||
const handleUpdateStructures = useCallback(
|
||||
async (newList: StructureItem[]) => {
|
||||
const { added, updated, removed } = getActualStructures(structures, newList);
|
||||
|
||||
const sanitizedAdded = added.map(sanitizeIds);
|
||||
const sanitizedUpdated = updated.map(sanitizeEndTimers);
|
||||
|
||||
try {
|
||||
const { structures: updatedStructures = [] } = await outCommand({
|
||||
type: OutCommand.updateStructures,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
added: sanitizedAdded,
|
||||
updated: sanitizedUpdated,
|
||||
removed,
|
||||
},
|
||||
});
|
||||
|
||||
const finalStructures = updatedStructures.map(mapServerStructure);
|
||||
setStructures(finalStructures);
|
||||
} catch (err) {
|
||||
console.error('Failed to update structures:', err);
|
||||
}
|
||||
},
|
||||
[structures, systemId, outCommand, sanitizeIds, sanitizeEndTimers],
|
||||
);
|
||||
|
||||
return { structures, handleUpdateStructures, isLoading, error };
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SystemStructures';
|
||||
@@ -1,50 +0,0 @@
|
||||
// File: TimerCell.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StructureStatus } from '../helpers/structureTypes';
|
||||
import { statusesRequiringTimer } from '../helpers';
|
||||
|
||||
interface TimerCellProps {
|
||||
endTime?: string;
|
||||
status: StructureStatus;
|
||||
}
|
||||
|
||||
function TimerCellImpl({ endTime, status }: TimerCellProps) {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (!endTime || !statusesRequiringTimer.includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setNow(Date.now());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [endTime, status]);
|
||||
|
||||
if (!statusesRequiringTimer.includes(status)) {
|
||||
return <span className="text-stone-400"></span>;
|
||||
}
|
||||
if (!endTime) {
|
||||
return <span className="text-sky-400">Set Timer</span>;
|
||||
}
|
||||
|
||||
const msLeft = new Date(endTime).getTime() - now;
|
||||
if (msLeft <= 0) {
|
||||
return <span className="text-red-500">00:00:00</span>;
|
||||
}
|
||||
|
||||
const sec = Math.floor(msLeft / 1000) % 60;
|
||||
const min = Math.floor(msLeft / (1000 * 60)) % 60;
|
||||
const hr = Math.floor(msLeft / (1000 * 3600));
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return (
|
||||
<span className="text-sky-400">
|
||||
{pad(hr)}:{pad(min)}:{pad(sec)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export const TimerCell = React.memo(TimerCellImpl);
|
||||
@@ -1,36 +0,0 @@
|
||||
import { StructureItem } from '../helpers';
|
||||
import { TimerCell } from './TimerCell';
|
||||
|
||||
export function renderTimerCell(row: StructureItem) {
|
||||
return <TimerCell endTime={row.endTime} status={row.status} />;
|
||||
}
|
||||
|
||||
export function renderOwnerCell(row: StructureItem) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{row.ownerId && (
|
||||
<img
|
||||
src={`https://images.evetech.net/corporations/${row.ownerId}/logo?size=32`}
|
||||
alt="corp icon"
|
||||
className="w-5 h-5 object-contain"
|
||||
/>
|
||||
)}
|
||||
<span>{row.ownerTicker || row.ownerName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderTypeCell(row: StructureItem) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{row.structureTypeId && (
|
||||
<img
|
||||
src={`https://images.evetech.net/types/${row.structureTypeId}/icon`}
|
||||
alt="icon"
|
||||
className="w-5 h-5 object-contain"
|
||||
/>
|
||||
)}
|
||||
<span>{row.structureType ?? ''}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,4 +2,3 @@ export * from './LocalCharacters';
|
||||
export * from './SystemInfo';
|
||||
export * from './RoutesWidget';
|
||||
export * from './SystemSignatures';
|
||||
export * from './SystemStructures';
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
.p-tabview-panels {
|
||||
padding: 6px 1rem !important;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav-container {
|
||||
|
||||
@@ -3,10 +3,13 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from './components';
|
||||
import { InterfaceStoredSettingsProps, useMapRootState, InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
InterfaceStoredSettingsProps,
|
||||
useMapRootState,
|
||||
InterfaceStoredSettings,
|
||||
} from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { WidgetsSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/WidgetsSettings/WidgetsSettings.tsx';
|
||||
|
||||
export enum UserSettingsRemoteProps {
|
||||
link_signature_on_splash = 'link_signature_on_splash',
|
||||
@@ -137,6 +140,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
};
|
||||
}, [userRemoteSettings, interfaceSettings]);
|
||||
|
||||
|
||||
const handleShow = async () => {
|
||||
const { user_settings } = await outCommand({
|
||||
type: OutCommand.getUserSettings,
|
||||
@@ -178,7 +182,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
key={item.prop}
|
||||
label={item.label}
|
||||
checked={!!currentValue}
|
||||
setChecked={checked => handleSettingChange(item.prop, checked)}
|
||||
setChecked={(checked) => handleSettingChange(item.prop, checked)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -191,7 +195,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
className="text-sm"
|
||||
value={currentValue}
|
||||
options={item.options}
|
||||
onChange={e => handleSettingChange(item.prop, e.value)}
|
||||
onChange={(e) => handleSettingChange(item.prop, e.value)}
|
||||
placeholder="Select a theme"
|
||||
/>
|
||||
</div>
|
||||
@@ -221,13 +225,21 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className={styles.verticalTabsContainer}>
|
||||
<TabView activeIndex={activeIndex} onTabChange={e => setActiveIndex(e.index)}>
|
||||
<TabView
|
||||
activeIndex={activeIndex}
|
||||
onTabChange={(e) => setActiveIndex(e.index)}
|
||||
className={styles.verticalTabView}
|
||||
>
|
||||
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
{renderSettingsList(COMMON_CHECKBOXES_PROPS)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}</div>
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Connections" headerClassName={styles.verticalTabHeader}>
|
||||
@@ -242,10 +254,6 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
{renderSettingsList(UI_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
|
||||
<WidgetsSettings />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingItem(THEME_SETTING)}
|
||||
</TabPanel>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Button } from 'primereact/button';
|
||||
|
||||
export interface WidgetsSettingsProps {}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
|
||||
const { windowsSettings, toggleWidgetVisibility, resetWidgets } = useMapRootState();
|
||||
|
||||
const handleWidgetSettingsChange = useCallback(
|
||||
(widget: WidgetsIds) => toggleWidgetVisibility(widget),
|
||||
[toggleWidgetVisibility],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2">
|
||||
<div>
|
||||
{WIDGETS_CHECKBOXES_PROPS.map(widget => (
|
||||
<PrettySwitchbox
|
||||
key={widget.id}
|
||||
label={widget.label}
|
||||
checked={windowsSettings.visible.some(x => x === widget.id)}
|
||||
setChecked={() => handleWidgetSettingsChange(widget.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto]">
|
||||
<div />
|
||||
<Button className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets"></Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,10 +14,9 @@ import classes from './MapWrapper.module.scss';
|
||||
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
|
||||
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { Node, XYPosition } from 'reactflow';
|
||||
|
||||
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
|
||||
|
||||
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
|
||||
@@ -33,7 +32,7 @@ export const MapWrapper = () => {
|
||||
const {
|
||||
update,
|
||||
outCommand,
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, connections, linkSignatureToSystem },
|
||||
data: { selectedConnections, selectedSystems, hubs, systems },
|
||||
interfaceSettings: {
|
||||
isShowMenu,
|
||||
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
|
||||
@@ -47,19 +46,25 @@ export const MapWrapper = () => {
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
const { mapRef, runCommand } = useCommonMapEventProcessor();
|
||||
|
||||
const { updateLinkSignatureToSystem } = useCommandsSystems();
|
||||
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
|
||||
const { handleSystemMultipleContext, ...systemMultipleCtxProps } = useContextMenuSystemMultipleHandlers();
|
||||
|
||||
const [openSettings, setOpenSettings] = useState<string | null>(null);
|
||||
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
|
||||
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
|
||||
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
|
||||
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
|
||||
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems };
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
|
||||
|
||||
useMapEventListener(event => {
|
||||
switch (event.name) {
|
||||
case Commands.linkSignatureToSystem:
|
||||
setOpenLinkSignatures(event.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
runCommand(event);
|
||||
});
|
||||
|
||||
@@ -88,6 +93,9 @@ export const MapWrapper = () => {
|
||||
case OutCommand.openSettings:
|
||||
setOpenSettings(event.data.system_id);
|
||||
break;
|
||||
case OutCommand.linkSignatureToSystem:
|
||||
setOpenLinkSignatures(event.data);
|
||||
break;
|
||||
default:
|
||||
return outCommand(event);
|
||||
}
|
||||
@@ -125,11 +133,6 @@ export const MapWrapper = () => {
|
||||
setOpenAddSystem(coordinates);
|
||||
}, []);
|
||||
|
||||
const canRemoveConnection = useCallback((connectionId: string) => {
|
||||
const { connections } = ref.current;
|
||||
return !connections.some(x => x.id === connectionId);
|
||||
}, []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => {
|
||||
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
|
||||
@@ -166,7 +169,6 @@ export const MapWrapper = () => {
|
||||
isSoftBackground={isSoftBackground}
|
||||
theme={theme}
|
||||
onAddSystem={onAddSystem}
|
||||
canRemoveConnection={canRemoveConnection}
|
||||
/>
|
||||
|
||||
{openSettings != null && (
|
||||
@@ -177,8 +179,8 @@ export const MapWrapper = () => {
|
||||
<SystemCustomLabelDialog systemId={openCustomLabel} visible setVisible={() => setOpenCustomLabel(null)} />
|
||||
)}
|
||||
|
||||
{linkSignatureToSystem != null && (
|
||||
<SystemLinkSignatureDialog data={linkSignatureToSystem} setVisible={() => updateLinkSignatureToSystem(null)} />
|
||||
{openLinkSignatures != null && (
|
||||
<SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} />
|
||||
)}
|
||||
|
||||
<AddSystemDialog
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
.windowContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window {
|
||||
position: absolute;
|
||||
//background: #fff;
|
||||
//border: 1px solid #000;
|
||||
//box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
user-select: none;
|
||||
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
position: absolute;
|
||||
//background: rgba(0, 0, 0, 0.2);
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.topRight,
|
||||
.bottomLeft {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.topLeft,
|
||||
.bottomRight {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.topLeft {
|
||||
top: -7.5px;
|
||||
left: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 7.5px;
|
||||
left: 7.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-left: 1px solid var(--window-corner);
|
||||
border-top: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.topRight {
|
||||
top: -7.5px;
|
||||
right: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 7.5px;
|
||||
right: -2.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 1px solid var(--window-corner);
|
||||
border-top: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomLeft {
|
||||
bottom: -7.5px;
|
||||
left: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 2.5px;
|
||||
left: 7.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-left: 1px solid var(--window-corner);
|
||||
border-bottom: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomRight {
|
||||
bottom: -7.5px;
|
||||
right: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 2.5px;
|
||||
right: -2.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 1px solid var(--window-corner);
|
||||
border-bottom: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
top: -5px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -5px;
|
||||
width: 10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: -5px;
|
||||
width: 10px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import styles from './WindowManager.module.scss';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
|
||||
const MIN_WINDOW_SIZE = 100;
|
||||
const SNAP_THRESHOLD = 10;
|
||||
export const SNAP_GAP = 10;
|
||||
|
||||
export enum ActionType {
|
||||
Drag = 'drag',
|
||||
Resize = 'resize',
|
||||
}
|
||||
|
||||
export const DefaultWindowState = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
function getWindowsBySides(windows: WindowProps[], containerWidth: number, containerHeight: number) {
|
||||
const centerX = containerWidth / 2;
|
||||
const centerY = containerHeight / 2;
|
||||
|
||||
const top = windows.filter(window => window.position.y + window.size.height / 2 < centerY);
|
||||
const bottom = windows.filter(window => window.position.y + window.size.height / 2 >= centerY);
|
||||
const left = windows.filter(window => window.position.x + window.size.width / 2 < centerX);
|
||||
const right = windows.filter(window => window.position.x + window.size.width / 2 >= centerX);
|
||||
|
||||
return { top, bottom, left, right };
|
||||
}
|
||||
|
||||
export type WindowWrapperProps = {
|
||||
onDrag: (e: React.MouseEvent, windowId: string | number) => void;
|
||||
onResize: (e: React.MouseEvent, windowId: string | number, resizeDirection: string) => void;
|
||||
} & WindowProps;
|
||||
|
||||
export const WindowWrapper = ({ onResize, onDrag, ...window }: WindowWrapperProps) => {
|
||||
const handleMouseDownRoot = (e: React.MouseEvent) => {
|
||||
onDrag(e, window.id);
|
||||
};
|
||||
|
||||
const { handleResizeTL, handleResizeTR, handleResizeBL, handleResizeBR } = useMemo(() => {
|
||||
const handleResizeTL = (e: React.MouseEvent) => onResize(e, window.id, 'top left');
|
||||
const handleResizeTR = (e: React.MouseEvent) => onResize(e, window.id, 'top right');
|
||||
const handleResizeBL = (e: React.MouseEvent) => onResize(e, window.id, 'bottom left');
|
||||
const handleResizeBR = (e: React.MouseEvent) => onResize(e, window.id, 'bottom right');
|
||||
|
||||
return {
|
||||
handleResizeTL,
|
||||
handleResizeTR,
|
||||
handleResizeBL,
|
||||
handleResizeBR,
|
||||
};
|
||||
}, [window]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={window.id}
|
||||
className={`drag-handle ${styles.window}`}
|
||||
style={{
|
||||
width: window.size.width,
|
||||
height: window.size.height,
|
||||
top: window.position.y,
|
||||
left: window.position.x,
|
||||
zIndex: window.zIndex,
|
||||
}}
|
||||
onMouseDown={handleMouseDownRoot}
|
||||
>
|
||||
{window.content(window)}
|
||||
<div className={styles.resizeHandle + ' ' + styles.topLeft} onMouseDown={handleResizeTL} />
|
||||
<div className={styles.resizeHandle + ' ' + styles.topRight} onMouseDown={handleResizeTR} />
|
||||
<div className={styles.resizeHandle + ' ' + styles.bottomLeft} onMouseDown={handleResizeBL} />
|
||||
<div className={styles.resizeHandle + ' ' + styles.bottomRight} onMouseDown={handleResizeBR} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type WindowManagerProps = {
|
||||
windows: WindowProps[];
|
||||
dragSelector?: string;
|
||||
onChange?(windows: WindowProps[]): void;
|
||||
};
|
||||
|
||||
export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWindows, dragSelector, onChange }) => {
|
||||
const [windows, setWindows] = useState(
|
||||
initialWindows.map((window, index) => ({
|
||||
...window,
|
||||
zIndex: index + 1,
|
||||
})),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setWindows(initialWindows.slice(0));
|
||||
}, [initialWindows]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const activeWindowIdRef = useRef<string | number | null>(null);
|
||||
const actionTypeRef = useRef<ActionType | null>(null);
|
||||
const resizeDirectionRef = useRef<string | null>(null);
|
||||
const startMousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const startWindowStateRef = useRef<{ x: number; y: number; width: number; height: number }>(DefaultWindowState);
|
||||
|
||||
const ref = useRef({ windows, onChange });
|
||||
ref.current = { windows, onChange };
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
|
||||
const onDebouncedChange = useMemo(() => {
|
||||
return debounce(() => {
|
||||
ref.current.onChange?.(ref.current.windows);
|
||||
}, 20);
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent,
|
||||
windowId: string | number,
|
||||
actionType: ActionType,
|
||||
resizeDirection?: string,
|
||||
) => {
|
||||
if (dragSelector && actionType === ActionType.Drag && !(e.target as HTMLElement).closest(dragSelector)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
activeWindowIdRef.current = windowId;
|
||||
actionTypeRef.current = actionType;
|
||||
resizeDirectionRef.current = resizeDirection || null;
|
||||
startMousePositionRef.current = { x: e.clientX, y: e.clientY };
|
||||
const targetWindow = windows.find(win => win.id === windowId);
|
||||
if (targetWindow) {
|
||||
startWindowStateRef.current = {
|
||||
x: targetWindow.position.x,
|
||||
y: targetWindow.position.y,
|
||||
width: targetWindow.size.width,
|
||||
height: targetWindow.size.height,
|
||||
};
|
||||
}
|
||||
|
||||
// Bring window to front by updating zIndex
|
||||
setWindows(prevWindows => {
|
||||
const maxZIndex = Math.max(...prevWindows.map(w => w.zIndex));
|
||||
return prevWindows.map(window => (window.id === windowId ? { ...window, zIndex: maxZIndex + 1 } : window));
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (activeWindowIdRef.current !== null && actionTypeRef.current) {
|
||||
const deltaX = e.clientX - startMousePositionRef.current.x;
|
||||
const deltaY = e.clientY - startMousePositionRef.current.y;
|
||||
const container = containerRef.current;
|
||||
|
||||
setWindows(prevWindows =>
|
||||
prevWindows.map(window => {
|
||||
if (window.id === activeWindowIdRef.current) {
|
||||
let newX = startWindowStateRef.current.x;
|
||||
let newY = startWindowStateRef.current.y;
|
||||
let newWidth = startWindowStateRef.current.width;
|
||||
let newHeight = startWindowStateRef.current.height;
|
||||
|
||||
if (actionTypeRef.current === ActionType.Drag) {
|
||||
newX += deltaX;
|
||||
newY += deltaY;
|
||||
|
||||
// Ensure the window stays within the container boundaries
|
||||
if (container) {
|
||||
newX = Math.max(SNAP_GAP, Math.min(container.clientWidth - window.size.width - SNAP_GAP, newX));
|
||||
newY = Math.max(SNAP_GAP, Math.min(container.clientHeight - window.size.height - SNAP_GAP, newY));
|
||||
}
|
||||
|
||||
// Snap to other windows with or without gap
|
||||
prevWindows.forEach(otherWindow => {
|
||||
if (otherWindow.id === window.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Snap vertically (top and bottom)
|
||||
if (Math.abs(newY - otherWindow.position.y) < SNAP_THRESHOLD) {
|
||||
newY = otherWindow.position.y; // Align top without gap
|
||||
} else if (Math.abs(newY + window.size.height - otherWindow.position.y) < SNAP_THRESHOLD) {
|
||||
newY = otherWindow.position.y - window.size.height - SNAP_GAP; // Bottom aligns to top
|
||||
} else if (Math.abs(newY - (otherWindow.position.y + otherWindow.size.height)) < SNAP_THRESHOLD) {
|
||||
newY = otherWindow.position.y + otherWindow.size.height + SNAP_GAP; // Align bottom without gap
|
||||
} else if (
|
||||
Math.abs(newY + window.size.height - (otherWindow.position.y + otherWindow.size.height)) <
|
||||
SNAP_THRESHOLD
|
||||
) {
|
||||
newY = otherWindow.position.y + otherWindow.size.height - window.size.height; // Bottom aligns bottom
|
||||
}
|
||||
|
||||
// Snap horizontally (left and right)
|
||||
if (Math.abs(newX - otherWindow.position.x) < SNAP_THRESHOLD) {
|
||||
newX = otherWindow.position.x; // Align left without gap
|
||||
} else if (Math.abs(newX + window.size.width - otherWindow.position.x) < SNAP_THRESHOLD) {
|
||||
newX = otherWindow.position.x - window.size.width - SNAP_GAP; // Right aligns to left
|
||||
} else if (Math.abs(newX - (otherWindow.position.x + otherWindow.size.width)) < SNAP_THRESHOLD) {
|
||||
newX = otherWindow.position.x + otherWindow.size.width + SNAP_GAP; // Align right without gap
|
||||
} else if (
|
||||
Math.abs(newX + window.size.width - (otherWindow.position.x + otherWindow.size.width)) <
|
||||
SNAP_THRESHOLD
|
||||
) {
|
||||
newX = otherWindow.position.x + otherWindow.size.width - window.size.width; // Right aligns right
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (actionTypeRef.current === ActionType.Resize && resizeDirectionRef.current) {
|
||||
if (resizeDirectionRef.current.includes('right')) {
|
||||
newWidth = Math.max(MIN_WINDOW_SIZE, startWindowStateRef.current.width + deltaX);
|
||||
|
||||
// Снап для правой границы с отступом SNAP_THRESHOLD
|
||||
prevWindows.forEach(otherWindow => {
|
||||
if (otherWindow.id !== window.id) {
|
||||
// Правая граница текущего окна к левой границе другого окна
|
||||
const snapRightToLeft =
|
||||
otherWindow.position.x - (startWindowStateRef.current.x + newWidth) - SNAP_THRESHOLD;
|
||||
if (Math.abs(snapRightToLeft) < SNAP_THRESHOLD) {
|
||||
newWidth = otherWindow.position.x - startWindowStateRef.current.x - SNAP_THRESHOLD;
|
||||
}
|
||||
|
||||
// Правая граница текущего окна к правой границе другого окна
|
||||
const snapRightToRight =
|
||||
otherWindow.position.x + otherWindow.size.width - (startWindowStateRef.current.x + newWidth);
|
||||
if (Math.abs(snapRightToRight) < SNAP_THRESHOLD) {
|
||||
newWidth = otherWindow.position.x + otherWindow.size.width - startWindowStateRef.current.x;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (resizeDirectionRef.current.includes('left')) {
|
||||
newWidth = Math.max(MIN_WINDOW_SIZE, startWindowStateRef.current.width - deltaX);
|
||||
newX = startWindowStateRef.current.x + (startWindowStateRef.current.width - newWidth);
|
||||
|
||||
// Снап для левой границы с отступом SNAP_THRESHOLD
|
||||
prevWindows.forEach(otherWindow => {
|
||||
if (otherWindow.id !== window.id) {
|
||||
// Левая граница текущего окна к правой границе другого окна
|
||||
const snapLeftToRight = newX - (otherWindow.position.x + otherWindow.size.width + SNAP_THRESHOLD);
|
||||
if (Math.abs(snapLeftToRight) < SNAP_THRESHOLD) {
|
||||
newX = otherWindow.position.x + otherWindow.size.width + SNAP_THRESHOLD;
|
||||
newWidth = startWindowStateRef.current.width + startWindowStateRef.current.x - newX;
|
||||
}
|
||||
|
||||
// Левая граница текущего окна к левой границе другого окна
|
||||
const snapLeftToLeft = newX - otherWindow.position.x;
|
||||
if (Math.abs(snapLeftToLeft) < SNAP_THRESHOLD) {
|
||||
newX = otherWindow.position.x;
|
||||
newWidth = startWindowStateRef.current.width + startWindowStateRef.current.x - newX;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (resizeDirectionRef.current.includes('bottom')) {
|
||||
newHeight = Math.max(MIN_WINDOW_SIZE, startWindowStateRef.current.height + deltaY);
|
||||
|
||||
// Снап для нижней границы с отступом SNAP_THRESHOLD
|
||||
prevWindows.forEach(otherWindow => {
|
||||
if (otherWindow.id !== window.id) {
|
||||
// Нижняя граница текущего окна к верхней границе другого окна
|
||||
const snapBottomToTop =
|
||||
otherWindow.position.y - (startWindowStateRef.current.y + newHeight) - SNAP_THRESHOLD;
|
||||
if (Math.abs(snapBottomToTop) < SNAP_THRESHOLD) {
|
||||
newHeight = otherWindow.position.y - startWindowStateRef.current.y - SNAP_THRESHOLD;
|
||||
}
|
||||
|
||||
// Нижняя граница текущего окна к нижней границе другого окна
|
||||
const snapBottomToBottom =
|
||||
otherWindow.position.y + otherWindow.size.height - (startWindowStateRef.current.y + newHeight);
|
||||
if (Math.abs(snapBottomToBottom) < SNAP_THRESHOLD) {
|
||||
newHeight = otherWindow.position.y + otherWindow.size.height - startWindowStateRef.current.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (resizeDirectionRef.current.includes('top')) {
|
||||
newHeight = Math.max(MIN_WINDOW_SIZE, startWindowStateRef.current.height - deltaY);
|
||||
newY = startWindowStateRef.current.y + (startWindowStateRef.current.height - newHeight);
|
||||
|
||||
// Снап для верхней границы с отступом SNAP_THRESHOLD
|
||||
prevWindows.forEach(otherWindow => {
|
||||
if (otherWindow.id !== window.id) {
|
||||
// Верхняя граница текущего окна к нижней границе другого окна
|
||||
const snapTopToBottom = newY - (otherWindow.position.y + otherWindow.size.height + SNAP_THRESHOLD);
|
||||
if (Math.abs(snapTopToBottom) < SNAP_THRESHOLD) {
|
||||
newY = otherWindow.position.y + otherWindow.size.height + SNAP_THRESHOLD;
|
||||
newHeight = startWindowStateRef.current.height + startWindowStateRef.current.y - newY;
|
||||
}
|
||||
|
||||
// Верхняя граница текущего окна к верхней границе другого окна
|
||||
const snapTopToTop = newY - otherWindow.position.y;
|
||||
if (Math.abs(snapTopToTop) < SNAP_THRESHOLD) {
|
||||
newY = otherWindow.position.y;
|
||||
newHeight = startWindowStateRef.current.height + startWindowStateRef.current.y - newY;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the window stays within the container boundaries
|
||||
if (container) {
|
||||
newX = Math.max(0 + SNAP_GAP, Math.min(container.clientWidth - newWidth - SNAP_GAP, newX));
|
||||
newY = Math.max(0 + SNAP_GAP, Math.min(container.clientHeight - newHeight - SNAP_GAP, newY));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...window,
|
||||
position: { x: newX, y: newY },
|
||||
size: { width: newWidth, height: newHeight },
|
||||
};
|
||||
}
|
||||
return window;
|
||||
}),
|
||||
);
|
||||
|
||||
onDebouncedChange();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
activeWindowIdRef.current = null;
|
||||
actionTypeRef.current = null;
|
||||
resizeDirectionRef.current = null;
|
||||
|
||||
onDebouncedChange();
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
}, []);
|
||||
|
||||
// Handle resize of the container and reposition windows
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
refPrevSize.current = { w: containerRef.current.clientWidth, h: containerRef.current.clientHeight };
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
const container = containerRef.current;
|
||||
const { windows } = ref.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = container.clientWidth - refPrevSize.current.w;
|
||||
const deltaY = container.clientHeight - refPrevSize.current.h;
|
||||
|
||||
const { bottom, right } = getWindowsBySides(windows, refPrevSize.current.w, refPrevSize.current.h);
|
||||
|
||||
setWindows(w => {
|
||||
return w.map(x => {
|
||||
let next = { ...x };
|
||||
|
||||
if (right.some(r => r.id === x.id)) {
|
||||
next = {
|
||||
...next,
|
||||
position: {
|
||||
...next.position,
|
||||
x: next.position.x + deltaX,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (bottom.some(r => r.id === x.id)) {
|
||||
next = {
|
||||
...next,
|
||||
position: {
|
||||
...next.position,
|
||||
y: next.position.y + deltaY,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (next.position.x + next.size.width > container.clientWidth - SNAP_GAP) {
|
||||
next.position.x = container.clientWidth - next.size.width - SNAP_GAP;
|
||||
}
|
||||
|
||||
if (next.position.y + next.size.height > container.clientHeight - SNAP_GAP) {
|
||||
next.position.y = container.clientHeight - next.size.height - SNAP_GAP;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
onDebouncedChange();
|
||||
|
||||
refPrevSize.current = { w: container.clientWidth, h: container.clientHeight };
|
||||
};
|
||||
|
||||
const tid = setTimeout(handleResize, 10);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
clearTimeout(tid);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDrag = (e: React.MouseEvent, windowId: string | number) => {
|
||||
handleMouseDown(e, windowId, ActionType.Drag);
|
||||
};
|
||||
|
||||
const handleResize = (e: React.MouseEvent, windowId: string | number, resizeDirection: string) => {
|
||||
handleMouseDown(e, windowId, ActionType.Resize, resizeDirection);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.windowContainer}>
|
||||
{windows.map(window => (
|
||||
<WindowWrapper key={window.id} onDrag={handleDrag} onResize={handleResize} {...window} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './WindowManager';
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export type WindowProps = {
|
||||
id: string | number;
|
||||
content: (w: WindowProps) => React.ReactNode;
|
||||
position: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
zIndex: number;
|
||||
visible?: boolean;
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/WindowManager.tsx';
|
||||
|
||||
export function getWindowsBySides(windows: WindowProps[], containerWidth: number, containerHeight: number) {
|
||||
const centerX = containerWidth / 2;
|
||||
const centerY = containerHeight / 2;
|
||||
|
||||
const top = windows.filter(window => window.position.y + window.size.height / 2 < centerY);
|
||||
const bottom = windows.filter(window => window.position.y + window.size.height / 2 >= centerY);
|
||||
const left = windows.filter(window => window.position.x + window.size.width / 2 < centerX);
|
||||
const right = windows.filter(window => window.position.x + window.size.width / 2 >= centerX);
|
||||
|
||||
return { top, bottom, left, right };
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
export enum SESSION_KEY {
|
||||
viewPort = 'viewPort',
|
||||
windows = 'windows',
|
||||
windowsVisible = 'windowsVisible',
|
||||
routes = 'routes',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext, useEffect } from 'react';
|
||||
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext } from 'react';
|
||||
import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
|
||||
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import {
|
||||
ToggleWidgetVisibility,
|
||||
UpdateWidgetSettingsFunc,
|
||||
useStoreWidgets,
|
||||
WindowStoreInfo,
|
||||
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
|
||||
import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types';
|
||||
|
||||
export type MapRootData = MapUnionTypes & {
|
||||
selectedSystems: string[];
|
||||
selectedConnections: Pick<SolarSystemConnection, 'source' | 'target'>[];
|
||||
linkSignatureToSystem: CommandLinkSignatureToSystem | null;
|
||||
};
|
||||
|
||||
const INITIAL_DATA: MapRootData = {
|
||||
@@ -26,7 +18,6 @@ const INITIAL_DATA: MapRootData = {
|
||||
userCharacters: [],
|
||||
presentCharacters: [],
|
||||
systems: [],
|
||||
systemSignatures: {},
|
||||
hubs: [],
|
||||
routes: undefined,
|
||||
kills: [],
|
||||
@@ -36,7 +27,6 @@ const INITIAL_DATA: MapRootData = {
|
||||
selectedConnections: [],
|
||||
userPermissions: {},
|
||||
options: {},
|
||||
linkSignatureToSystem: null,
|
||||
};
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
@@ -70,7 +60,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
isShowBackgroundPattern: true,
|
||||
isSoftBackground: false,
|
||||
theme: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
export interface MapRootContextProps {
|
||||
update: ContextStoreDataUpdate<MapRootData>;
|
||||
@@ -78,10 +68,6 @@ export interface MapRootContextProps {
|
||||
outCommand: OutCommandHandler;
|
||||
interfaceSettings: InterfaceStoredSettings;
|
||||
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
|
||||
windowsSettings: WindowStoreInfo;
|
||||
toggleWidgetVisibility: ToggleWidgetVisibility;
|
||||
updateWidgetSettings: UpdateWidgetSettingsFunc;
|
||||
resetWidgets: () => void;
|
||||
}
|
||||
|
||||
const MapRootContext = createContext<MapRootContextProps>({
|
||||
@@ -115,25 +101,6 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
|
||||
defaultValue: STORED_INTERFACE_DEFAULT_VALUES,
|
||||
},
|
||||
);
|
||||
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
|
||||
|
||||
useEffect(() => {
|
||||
let foundNew = false;
|
||||
const newVals = Object.keys(STORED_INTERFACE_DEFAULT_VALUES).reduce((acc, x) => {
|
||||
if (Object.keys(acc).includes(x)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
foundNew = true;
|
||||
|
||||
// @ts-ignore
|
||||
return { ...acc, [x]: STORED_INTERFACE_DEFAULT_VALUES[x] };
|
||||
}, interfaceSettings);
|
||||
|
||||
if (foundNew) {
|
||||
setInterfaceSettings(newVals);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MapRootContext.Provider
|
||||
@@ -143,10 +110,6 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
|
||||
outCommand: outCommand,
|
||||
setInterfaceSettings,
|
||||
interfaceSettings,
|
||||
windowsSettings,
|
||||
updateWidgetSettings,
|
||||
toggleWidgetVisibility,
|
||||
resetWidgets,
|
||||
}}
|
||||
>
|
||||
<MapRootHandlers ref={fwdRef}>{children}</MapRootHandlers>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
CommandAddSystems,
|
||||
CommandRemoveSystems,
|
||||
CommandUpdateSystems,
|
||||
CommandLinkSignatureToSystem,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { CommandAddSystems, CommandRemoveSystems, CommandUpdateSystems } from '@/hooks/Mapper/types';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
@@ -13,14 +8,14 @@ import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
export const useCommandsSystems = () => {
|
||||
const {
|
||||
update,
|
||||
data: { systems, systemSignatures },
|
||||
data: { systems },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
|
||||
|
||||
const ref = useRef({ systems, systemSignatures, update, addSystemStatic });
|
||||
ref.current = { systems, systemSignatures, update, addSystemStatic };
|
||||
const ref = useRef({ systems, update, addSystemStatic });
|
||||
ref.current = { systems, update, addSystemStatic };
|
||||
|
||||
const addSystems = useCallback((systemsToAdd: CommandAddSystems) => {
|
||||
const { update, addSystemStatic, systems } = ref.current;
|
||||
@@ -62,27 +57,31 @@ export const useCommandsSystems = () => {
|
||||
});
|
||||
|
||||
update({ systems: out }, true);
|
||||
|
||||
emitMapEvent({ name: Commands.updateSystems, data: out });
|
||||
}, []);
|
||||
|
||||
const updateSystemSignatures = useCallback(
|
||||
async (systemId: string) => {
|
||||
const { update, systemSignatures } = ref.current;
|
||||
const { update, systems } = ref.current;
|
||||
|
||||
const { signatures } = await outCommand({
|
||||
type: OutCommand.getSignatures,
|
||||
data: { system_id: `${systemId}` },
|
||||
});
|
||||
const out = { ...systemSignatures, [`${systemId}`]: signatures };
|
||||
update({ systemSignatures: out }, true);
|
||||
|
||||
const out = systems.map(current => {
|
||||
if (current.id === `${systemId}`) {
|
||||
return { ...current, system_signatures: signatures };
|
||||
}
|
||||
|
||||
return current;
|
||||
});
|
||||
|
||||
update({ systems: out }, true);
|
||||
|
||||
emitMapEvent({ name: Commands.updateSystems, data: out });
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const updateLinkSignatureToSystem = useCallback(async (command: CommandLinkSignatureToSystem) => {
|
||||
const { update } = ref.current;
|
||||
update({ linkSignatureToSystem: command }, true);
|
||||
}, []);
|
||||
|
||||
return { addSystems, removeSystems, updateSystems, updateSystemSignatures, updateLinkSignatureToSystem };
|
||||
return { addSystems, removeSystems, updateSystems, updateSystemSignatures };
|
||||
};
|
||||
|
||||
@@ -32,8 +32,7 @@ import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapInit = useMapInit();
|
||||
const { addSystems, removeSystems, updateSystems, updateSystemSignatures, updateLinkSignatureToSystem } =
|
||||
useCommandsSystems();
|
||||
const { addSystems, removeSystems, updateSystems, updateSystemSignatures } = useCommandsSystems();
|
||||
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
|
||||
const { charactersUpdated, characterAdded, characterRemoved, characterUpdated, presentCharacters } =
|
||||
useCommandsCharacters();
|
||||
@@ -94,9 +93,7 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
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 { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { SNAP_GAP } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
|
||||
export type StoredWindowProps = Omit<WindowProps, 'content'>;
|
||||
export type WindowStoreInfo = {
|
||||
version: number;
|
||||
windows: StoredWindowProps[];
|
||||
visible: WidgetsIds[];
|
||||
};
|
||||
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(),
|
||||
});
|
||||
|
||||
const ref = useRef({ windowsSettings, setWindowsSettings });
|
||||
ref.current = { windowsSettings, setWindowsSettings };
|
||||
|
||||
const updateWidgetSettings: UpdateWidgetSettingsFunc = useCallback(newWindows => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
setWindowsSettings(({ version, visible /*, windows*/ }: WindowStoreInfo) => {
|
||||
return {
|
||||
version,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
windows: DEFAULT_WIDGETS.map(({ content, ...x }) => {
|
||||
const windowProp = newWindows.find(j => j.id === x.id);
|
||||
if (windowProp) {
|
||||
return windowProp;
|
||||
}
|
||||
|
||||
return x;
|
||||
}),
|
||||
visible,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleWidgetVisibility: ToggleWidgetVisibility = useCallback(widgetId => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
setWindowsSettings(({ visible, windows, ...x }) => {
|
||||
const isCheckedPrev = visible.includes(widgetId);
|
||||
if (!isCheckedPrev) {
|
||||
const maxZIndex = Math.max(...windows.map(w => w.zIndex));
|
||||
return {
|
||||
...x,
|
||||
windows: windows.map(wnd => {
|
||||
if (wnd.id === widgetId) {
|
||||
return { ...wnd, position: { x: SNAP_GAP, y: SNAP_GAP }, zIndex: maxZIndex + 1 };
|
||||
}
|
||||
|
||||
return wnd;
|
||||
}),
|
||||
visible: [...visible, widgetId],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...x,
|
||||
windows,
|
||||
visible: visible.filter(x => x !== widgetId),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
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 } = 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,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
|
||||
|
||||
return {
|
||||
windowsSettings,
|
||||
updateWidgetSettings,
|
||||
toggleWidgetVisibility,
|
||||
resetWidgets,
|
||||
};
|
||||
};
|
||||
@@ -118,7 +118,6 @@ export enum OutCommand {
|
||||
deleteHub = 'delete_hub',
|
||||
getRoutes = 'get_routes',
|
||||
getCharacterJumps = 'get_character_jumps',
|
||||
getStructures = 'get_structures',
|
||||
getSignatures = 'get_signatures',
|
||||
getSystemStaticInfos = 'get_system_static_infos',
|
||||
getConnectionInfo = 'get_connection_info',
|
||||
@@ -128,7 +127,6 @@ export enum OutCommand {
|
||||
updateConnectionShipSizeType = 'update_connection_ship_size_type',
|
||||
updateConnectionLocked = 'update_connection_locked',
|
||||
updateConnectionCustomInfo = 'update_connection_custom_info',
|
||||
updateStructures = 'update_structures',
|
||||
updateSignatures = 'update_signatures',
|
||||
updateSystemName = 'update_system_name',
|
||||
updateSystemTemporaryName = 'update_system_temporary_name',
|
||||
@@ -149,8 +147,6 @@ export enum OutCommand {
|
||||
openUserSettings = 'open_user_settings',
|
||||
getPassages = 'get_passages',
|
||||
linkSignatureToSystem = 'link_signature_to_system',
|
||||
getCorporationNames = 'get_corporation_names',
|
||||
getCorporationTicker = 'get_corporation_ticker',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
|
||||
export type MapUnionTypes = {
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
@@ -16,7 +15,6 @@ export type MapUnionTypes = {
|
||||
presentCharacters: string[];
|
||||
hubs: string[];
|
||||
systems: SolarSystemRawType[];
|
||||
systemSignatures: Record<string, SystemSignature[]>;
|
||||
routes?: RoutesList;
|
||||
kills: Record<number, number>;
|
||||
connections: SolarSystemConnection[];
|
||||
|
||||
@@ -117,7 +117,6 @@ export type SolarSystemRawType = {
|
||||
status: number;
|
||||
name: string | null;
|
||||
temporary_name: string | null;
|
||||
linked_sig_eve_id: string | null;
|
||||
|
||||
system_static_info: SolarSystemStaticInfoRaw;
|
||||
system_signatures: SystemSignature[];
|
||||
@@ -131,3 +130,4 @@ export type SearchSystemItem = {
|
||||
system_static_info: SolarSystemStaticInfoRaw;
|
||||
value: number;
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -64,19 +64,7 @@ map_subscription_characters_limit =
|
||||
|
||||
map_subscription_hubs_limit =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 10)
|
||||
|
||||
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 =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_100_PRICE", 50_000_000)
|
||||
|
||||
map_subscription_extra_hubs_10_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 100)
|
||||
|
||||
map_connection_auto_expire_hours =
|
||||
config_dir
|
||||
@@ -88,7 +76,7 @@ map_connection_auto_eol_hours =
|
||||
|
||||
map_connection_eol_expire_timeout_mins =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_EOL_EXPIRE_TIMEOUT_MINS", 60)
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_EOL_EXPIRE_TIMEOUT_MINS", 30)
|
||||
|
||||
wallet_tracking_enabled =
|
||||
config_dir
|
||||
@@ -129,16 +117,16 @@ config :wanderer_app,
|
||||
},
|
||||
%{
|
||||
id: "omega",
|
||||
characters_limit: map_subscription_characters_limit * 2,
|
||||
hubs_limit: map_subscription_hubs_limit * 2,
|
||||
base_price: map_subscription_base_price,
|
||||
characters_limit: 300,
|
||||
hubs_limit: 20,
|
||||
base_price: 250_000_000,
|
||||
month_3_discount: 0.2,
|
||||
month_6_discount: 0.4,
|
||||
month_12_discount: 0.5
|
||||
}
|
||||
],
|
||||
extra_characters_100: map_subscription_extra_characters_100_price,
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||
extra_characters_100: 75_000_000,
|
||||
extra_hubs_10: 25_000_000
|
||||
}
|
||||
|
||||
config :ueberauth, Ueberauth,
|
||||
|
||||
@@ -16,7 +16,6 @@ defmodule WandererApp.Api do
|
||||
resource WandererApp.Api.MapState
|
||||
resource WandererApp.Api.MapSystem
|
||||
resource WandererApp.Api.MapSystemSignature
|
||||
resource WandererApp.Api.MapSystemStructure
|
||||
resource WandererApp.Api.MapCharacterSettings
|
||||
resource WandererApp.Api.MapSubscription
|
||||
resource WandererApp.Api.MapTransaction
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
defmodule WandererApp.Api.MapSystemStructure do
|
||||
@moduledoc """
|
||||
Ash resource representing a structure in a given map system.
|
||||
|
||||
"""
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_system_structures_v1")
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define(:all_active, action: :all_active)
|
||||
define(:create, action: :create)
|
||||
define(:update, action: :update)
|
||||
|
||||
define(:by_id,
|
||||
get_by: [:id],
|
||||
action: :read
|
||||
)
|
||||
|
||||
define(:by_system_id,
|
||||
action: :by_system_id,
|
||||
args: [:system_id]
|
||||
)
|
||||
end
|
||||
|
||||
actions do
|
||||
default_accept [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
read :all_active do
|
||||
prepare build(sort: [updated_at: :desc])
|
||||
end
|
||||
|
||||
read :by_system_id do
|
||||
argument :system_id, :string, allow_nil?: false
|
||||
filter(expr(system_id == ^arg(:system_id)))
|
||||
end
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
accept [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:system_id, :system,
|
||||
on_lookup: :relate,
|
||||
on_no_match: nil
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
update :update do
|
||||
primary? true
|
||||
require_atomic? false
|
||||
|
||||
accept [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :structure_type_id, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :structure_type, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :solar_system_name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :solar_system_id, :integer do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :owner_name, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :owner_ticker, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :owner_id, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :status, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :end_time, :utc_datetime_usec do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
update_timestamp :updated_at
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :system, WandererApp.Api.MapSystem do
|
||||
attribute_writable? true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -95,9 +95,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
:map_acl_member_updated,
|
||||
:map_connection_added,
|
||||
:map_connection_updated,
|
||||
:map_connection_removed,
|
||||
:signatures_added,
|
||||
:signatures_removed
|
||||
:map_connection_removed
|
||||
]
|
||||
)
|
||||
|
||||
@@ -110,6 +108,8 @@ defmodule WandererApp.Api.UserActivity do
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
|
||||
|
||||
relationships do
|
||||
belongs_to :character, WandererApp.Api.Character do
|
||||
allow_nil? true
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
|
||||
def get_characters_limit(map_id),
|
||||
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 50)}
|
||||
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 100)}
|
||||
|
||||
def is_subscription_active?(map_id) do
|
||||
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
||||
|
||||
@@ -326,10 +326,6 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
options |> Map.get("store_custom_labels", "false") |> String.to_existing_atom(),
|
||||
show_linked_signature_id:
|
||||
options |> Map.get("show_linked_signature_id", "false") |> String.to_existing_atom(),
|
||||
show_linked_signature_id_temp_name:
|
||||
options
|
||||
|> Map.get("show_linked_signature_id_temp_name", "false")
|
||||
|> String.to_existing_atom(),
|
||||
show_temp_system_name:
|
||||
options |> Map.get("show_temp_system_name", "false") |> String.to_existing_atom(),
|
||||
restrict_offline_showing:
|
||||
@@ -206,24 +206,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
user_id,
|
||||
character_id
|
||||
) do
|
||||
filtered_ids =
|
||||
removed_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id})
|
||||
end)
|
||||
|> Enum.filter(fn system -> not is_nil(system) && not system.locked end)
|
||||
|> Enum.map(&{&1.solar_system_id, &1.id})
|
||||
|
||||
solar_system_ids_to_remove =
|
||||
filtered_ids
|
||||
|> Enum.map(fn {solar_system_id, _} -> solar_system_id end)
|
||||
|
||||
system_ids_to_remove =
|
||||
filtered_ids
|
||||
|> Enum.map(fn {_, system_id} -> system_id end)
|
||||
|
||||
connections_to_remove =
|
||||
solar_system_ids_to_remove
|
||||
removed_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Map.find_connections(map_id, solar_system_id)
|
||||
end)
|
||||
@@ -231,9 +215,9 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
|
||||
:ok = WandererApp.Map.remove_connections(map_id, connections_to_remove)
|
||||
:ok = WandererApp.Map.remove_systems(map_id, solar_system_ids_to_remove)
|
||||
:ok = WandererApp.Map.remove_systems(map_id, removed_ids)
|
||||
|
||||
solar_system_ids_to_remove
|
||||
removed_ids
|
||||
|> Enum.each(fn solar_system_id ->
|
||||
map_id
|
||||
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|
||||
@@ -253,7 +237,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
WandererApp.MapConnectionRepo.destroy(map_id, connection)
|
||||
end)
|
||||
|
||||
solar_system_ids_to_remove
|
||||
removed_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
@@ -266,28 +250,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
|
||||
end)
|
||||
|
||||
linked_system_ids =
|
||||
system_ids_to_remove
|
||||
|> Enum.map(fn system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_system_id!(system_id)
|
||||
|> Enum.filter(fn s -> not is_nil(s.linked_system_id) end)
|
||||
|> Enum.map(fn s -> s.linked_system_id end)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
linked_system_ids
|
||||
|> Enum.each(fn linked_system_id ->
|
||||
WandererApp.Map.Server.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
end)
|
||||
|
||||
@ddrt.delete(solar_system_ids_to_remove, rtree_name)
|
||||
@ddrt.delete(removed_ids, rtree_name)
|
||||
|
||||
Impl.broadcast!(map_id, :remove_connections, connections_to_remove)
|
||||
Impl.broadcast!(map_id, :systems_removed, solar_system_ids_to_remove)
|
||||
Impl.broadcast!(map_id, :systems_removed, removed_ids)
|
||||
|
||||
case not is_nil(user_id) do
|
||||
true ->
|
||||
@@ -296,12 +262,12 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
character_id: character_id,
|
||||
user_id: user_id,
|
||||
map_id: map_id,
|
||||
solar_system_ids: solar_system_ids_to_remove
|
||||
solar_system_ids: removed_ids
|
||||
})
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :systems, :remove],
|
||||
%{count: solar_system_ids_to_remove |> Enum.count()}
|
||||
%{count: removed_ids |> Enum.count()}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
@@ -55,33 +55,18 @@ defmodule WandererApp.Maps do
|
||||
|
||||
def get_available_maps(current_user) do
|
||||
case WandererApp.Api.Map.available(%{}, actor: current_user) do
|
||||
{:ok, maps} -> {:ok, maps |> filter_blocked_maps(current_user)}
|
||||
{:ok, maps} -> {:ok, maps |> _filter_blocked_maps(current_user)}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
def get_tracked_map_characters(map_id, current_user) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(
|
||||
map_id,
|
||||
current_user.characters |> Enum.map(& &1.id)
|
||||
) do
|
||||
{:ok, settings} ->
|
||||
{:ok,
|
||||
settings
|
||||
|> Enum.map(fn s -> s |> Ash.load!(:character) |> Map.get(:character) end)}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
def load_characters(map, character_settings, user_id) do
|
||||
{:ok, user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
characters =
|
||||
map
|
||||
|> get_map_available_characters(user_characters)
|
||||
|> _get_map_available_characters(user_characters)
|
||||
|> Enum.map(fn c ->
|
||||
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
|
||||
end)
|
||||
@@ -161,7 +146,7 @@ defmodule WandererApp.Maps do
|
||||
}}
|
||||
end
|
||||
|
||||
defp get_map_available_characters(map, user_characters) do
|
||||
defp _get_map_available_characters(map, user_characters) do
|
||||
{:ok,
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
@@ -179,7 +164,7 @@ defmodule WandererApp.Maps do
|
||||
end)
|
||||
end
|
||||
|
||||
defp filter_blocked_maps(maps, current_user) do
|
||||
defp _filter_blocked_maps(maps, current_user) do
|
||||
user_character_ids = current_user.characters |> Enum.map(& &1.id)
|
||||
user_character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ defmodule WandererApp.MapRepo do
|
||||
"layout" => "left_to_right",
|
||||
"store_custom_labels" => "false",
|
||||
"show_linked_signature_id" => "false",
|
||||
"show_linked_signature_id_temp_name" => "false",
|
||||
"show_temp_system_name" => "false",
|
||||
"restrict_offline_showing" => "false"
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
defmodule WandererApp.Structure do
|
||||
@moduledoc """
|
||||
Encapsulates the logic for parsing and updating system structures.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Api.MapSystemStructure
|
||||
alias WandererApp.Character
|
||||
|
||||
def update_structures(system, added, updated, removed, user_characters) do
|
||||
first_char_eve_id = List.first(user_characters)
|
||||
|
||||
added_structs =
|
||||
parse_structures(added, first_char_eve_id, system)
|
||||
|> Enum.map(&Map.delete(&1, :id))
|
||||
|
||||
updated_structs = parse_structures(updated, first_char_eve_id, system)
|
||||
removed_structs = parse_structures(removed, first_char_eve_id, system)
|
||||
|
||||
remove_structures(system.id, Enum.map(removed_structs, & &1.id))
|
||||
update_structures_in_db(system.id, updated_structs, Enum.map(updated_structs, & &1.id))
|
||||
add_structures(added_structs)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def search_corporation_names([], _search), do: {:ok, []}
|
||||
|
||||
def search_corporation_names([first_char | _], search) when is_binary(search) do
|
||||
Character.search(first_char.id, params: [search: search, categories: "corporation"])
|
||||
end
|
||||
|
||||
def search_corporation_names(_user_chars, _search), do: {:ok, []}
|
||||
|
||||
defp parse_structures(list_of_maps, character_eve_id, system) do
|
||||
Logger.debug(fn ->
|
||||
"[Structure] parse_structures =>\n" <> inspect(list_of_maps, pretty: true)
|
||||
end)
|
||||
|
||||
Enum.map(list_of_maps, fn item ->
|
||||
%{
|
||||
id: Map.get(item, "id"),
|
||||
|
||||
system_id: system.id,
|
||||
solar_system_id: system.solar_system_id,
|
||||
solar_system_name: system.name,
|
||||
|
||||
structure_type_id: Map.get(item, "structureTypeId") || "???",
|
||||
structure_type: Map.get(item, "structureType"),
|
||||
character_eve_id: character_eve_id,
|
||||
name: Map.get(item, "name"),
|
||||
notes: Map.get(item, "notes"),
|
||||
owner_name: Map.get(item, "ownerName"),
|
||||
owner_ticker: Map.get(item, "ownerTicker"),
|
||||
owner_id: Map.get(item, "ownerId"),
|
||||
status: Map.get(item, "status"),
|
||||
|
||||
end_time: parse_end_time(Map.get(item, "endTime"))
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_end_time(str) when is_binary(str) do
|
||||
# Log everything we can about the incoming string
|
||||
Logger.debug("[parse_end_time] raw input => #{inspect(str)} (length=#{String.length(str)})")
|
||||
|
||||
if String.trim(str) == "" do
|
||||
Logger.debug("[parse_end_time] It's empty (or whitespace only). Returning nil.")
|
||||
nil
|
||||
else
|
||||
# Attempt to parse
|
||||
case DateTime.from_iso8601(str) do
|
||||
{:ok, dt, _offset} ->
|
||||
Logger.debug("[parse_end_time] Successfully parsed => #{inspect(dt)}")
|
||||
dt
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[parse_end_time] Invalid ISO string: #{inspect(str)}, reason: #{inspect(reason)}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_end_time(other) do
|
||||
Logger.error("[parse_end_time] Received non-string => #{inspect(other)}. Returning nil.")
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
defp remove_structures(system_id, removed_ids) do
|
||||
MapSystemStructure.by_system_id!(system_id)
|
||||
|> Enum.filter(fn s -> s.id in removed_ids end)
|
||||
|> Enum.each(&Ash.destroy!/1)
|
||||
end
|
||||
|
||||
defp update_structures_in_db(system_id, updated_structs, updated_ids) do
|
||||
existing_records = MapSystemStructure.by_system_id!(system_id)
|
||||
|
||||
Enum.each(existing_records, fn existing ->
|
||||
if existing.id in updated_ids do
|
||||
updated_data = Enum.find(updated_structs, fn u -> u.id == existing.id end)
|
||||
|
||||
if updated_data do
|
||||
Logger.debug(fn ->
|
||||
"[Structure] about to update =>\n" <>
|
||||
inspect(updated_data, pretty: true)
|
||||
end)
|
||||
|
||||
updated_data = Map.delete(updated_data, :id) # remove PK so Ash doesn't treat it as a new record
|
||||
|
||||
new_record = MapSystemStructure.update(existing, updated_data)
|
||||
Logger.debug(fn ->
|
||||
"[Structure] updated record =>\n" <> inspect(new_record, pretty: true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_structures(added_structs) do
|
||||
Enum.each(added_structs, fn struct_map ->
|
||||
Logger.debug(fn ->
|
||||
"[Structure] Creating structure =>\n" <> inspect(struct_map, pretty: true)
|
||||
end)
|
||||
|
||||
MapSystemStructure.create!(struct_map)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -74,7 +74,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
</p>
|
||||
|
||||
<p class="text-sm text-[var(--color-gray-4)] w-[15%]">
|
||||
<%= get_event_name(@activity.event_type) %>
|
||||
<%= _get_event_name(@activity.event_type) %>
|
||||
</p>
|
||||
<.activity_event event_type={@activity.event_type} event_data={@activity.event_data} />
|
||||
|
||||
@@ -115,7 +115,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
<div class="w-[40%]">
|
||||
<div class="flex items-center gap-1">
|
||||
<h6 class="text-base leading-[150%] font-semibold dark:text-white">
|
||||
<%= get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"])) %>
|
||||
<%= _get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"])) %>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,128 +129,107 @@ defmodule WandererAppWeb.UserActivity do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp get_event_name(:hub_added), do: "Hub Added"
|
||||
defp get_event_name(:hub_removed), do: "Hub Removed"
|
||||
defp get_event_name(:map_connection_added), do: "Connection Added"
|
||||
defp get_event_name(:map_connection_updated), do: "Connection Updated"
|
||||
defp get_event_name(:map_connection_removed), do: "Connection Removed"
|
||||
defp get_event_name(:map_acl_added), do: "Acl Added"
|
||||
defp get_event_name(:map_acl_removed), do: "Acl Removed"
|
||||
defp get_event_name(:system_added), do: "System Added"
|
||||
defp get_event_name(:system_updated), do: "System Updated"
|
||||
defp get_event_name(:systems_removed), do: "System(s) Removed"
|
||||
defp get_event_name(:signatures_added), do: "Signatures Added"
|
||||
defp get_event_name(:signatures_removed), do: "Signatures Removed"
|
||||
defp get_event_name(name), do: name
|
||||
defp _get_event_name(:hub_added), do: "Hub Added"
|
||||
defp _get_event_name(:hub_removed), do: "Hub Removed"
|
||||
defp _get_event_name(:map_connection_added), do: "Connection Added"
|
||||
defp _get_event_name(:map_connection_updated), do: "Connection Updated"
|
||||
defp _get_event_name(:map_connection_removed), do: "Connection Removed"
|
||||
defp _get_event_name(:map_acl_added), do: "Acl Added"
|
||||
defp _get_event_name(:map_acl_removed), do: "Acl Removed"
|
||||
defp _get_event_name(:system_added), do: "System Added"
|
||||
defp _get_event_name(:system_updated), do: "System Updated"
|
||||
defp _get_event_name(:systems_removed), do: "System(s) Removed"
|
||||
defp _get_event_name(name), do: name
|
||||
|
||||
defp get_event_data(:map_acl_added, %{"acl_id" => acl_id}) do
|
||||
{:ok, acl} = WandererApp.AccessListRepo.get(acl_id)
|
||||
"#{acl.name}"
|
||||
end
|
||||
# defp _get_event_data(:hub_added, data), do: Jason.encode!(data)
|
||||
# defp _get_event_data(:hub_removed, data), do: data
|
||||
|
||||
defp get_event_data(:map_acl_removed, %{"acl_id" => acl_id}) do
|
||||
{:ok, acl} = WandererApp.AccessListRepo.get(acl_id)
|
||||
"#{acl.name}"
|
||||
end
|
||||
|
||||
# defp get_event_data(:map_acl_removed, data), do: data
|
||||
# defp get_event_data(:system_added, data), do: data
|
||||
# defp _get_event_data(:map_acl_added, data), do: data
|
||||
# defp _get_event_data(:map_acl_removed, data), do: data
|
||||
# defp _get_event_data(:system_added, data), do: data
|
||||
#
|
||||
|
||||
defp get_event_data(:system_updated, %{
|
||||
defp _get_event_data(:system_updated, %{
|
||||
"key" => "labels",
|
||||
"solar_system_id" => solar_system_id,
|
||||
"value" => value
|
||||
}) do
|
||||
system_name = get_system_name(solar_system_id)
|
||||
system_name = _get_system_name(solar_system_id)
|
||||
|
||||
try do
|
||||
%{"customLabel" => customLabel, "labels" => labels} = Jason.decode!(value)
|
||||
|
||||
"#{system_name}: labels - #{inspect(labels)}, customLabel - #{customLabel}"
|
||||
"#{system_name} labels - #{inspect(labels)}, customLabel - #{customLabel}"
|
||||
rescue
|
||||
_ ->
|
||||
"#{system_name}: labels - #{inspect(value)}"
|
||||
"#{system_name} labels - #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_event_data(:system_added, %{
|
||||
defp _get_event_data(:system_added, %{
|
||||
"solar_system_id" => solar_system_id
|
||||
}),
|
||||
do: get_system_name(solar_system_id)
|
||||
do: _get_system_name(solar_system_id)
|
||||
|
||||
defp get_event_data(:hub_added, %{
|
||||
defp _get_event_data(:hub_added, %{
|
||||
"solar_system_id" => solar_system_id
|
||||
}),
|
||||
do: get_system_name(solar_system_id)
|
||||
do: _get_system_name(solar_system_id)
|
||||
|
||||
defp get_event_data(:hub_removed, %{
|
||||
defp _get_event_data(:hub_removed, %{
|
||||
"solar_system_id" => solar_system_id
|
||||
}),
|
||||
do: get_system_name(solar_system_id)
|
||||
do: _get_system_name(solar_system_id)
|
||||
|
||||
defp get_event_data(:system_updated, %{
|
||||
defp _get_event_data(:system_updated, %{
|
||||
"key" => key,
|
||||
"solar_system_id" => solar_system_id,
|
||||
"value" => value
|
||||
}) do
|
||||
system_name = get_system_name(solar_system_id)
|
||||
"#{system_name}: #{key} - #{inspect(value)}"
|
||||
system_name = _get_system_name(solar_system_id)
|
||||
"#{system_name} #{key} - #{inspect(value)}"
|
||||
end
|
||||
|
||||
defp get_event_data(:systems_removed, %{
|
||||
defp _get_event_data(:systems_removed, %{
|
||||
"solar_system_ids" => solar_system_ids
|
||||
}),
|
||||
do:
|
||||
solar_system_ids
|
||||
|> Enum.map(&get_system_name/1)
|
||||
|> Enum.map(&_get_system_name/1)
|
||||
|> Enum.join(", ")
|
||||
|
||||
defp get_event_data(signatures_event, %{
|
||||
"solar_system_id" => solar_system_id,
|
||||
"signatures" => signatures
|
||||
})
|
||||
when signatures_event in [:signatures_added, :signatures_removed],
|
||||
do: "#{get_system_name(solar_system_id)}: #{signatures |> Enum.join(", ")}"
|
||||
|
||||
defp get_event_data(signatures_event, %{
|
||||
"signatures" => signatures
|
||||
})
|
||||
when signatures_event in [:signatures_added, :signatures_removed],
|
||||
do: signatures |> Enum.join(", ")
|
||||
|
||||
defp get_event_data(:map_connection_added, %{
|
||||
defp _get_event_data(:map_connection_added, %{
|
||||
"solar_system_source_id" => solar_system_source_id,
|
||||
"solar_system_target_id" => solar_system_target_id
|
||||
}) do
|
||||
source_system_name = get_system_name(solar_system_source_id)
|
||||
target_system_name = get_system_name(solar_system_target_id)
|
||||
source_system_name = _get_system_name(solar_system_source_id)
|
||||
target_system_name = _get_system_name(solar_system_target_id)
|
||||
"[#{source_system_name}:#{target_system_name}]"
|
||||
end
|
||||
|
||||
defp get_event_data(:map_connection_removed, %{
|
||||
defp _get_event_data(:map_connection_removed, %{
|
||||
"solar_system_source_id" => solar_system_source_id,
|
||||
"solar_system_target_id" => solar_system_target_id
|
||||
}) do
|
||||
source_system_name = get_system_name(solar_system_source_id)
|
||||
target_system_name = get_system_name(solar_system_target_id)
|
||||
source_system_name = _get_system_name(solar_system_source_id)
|
||||
target_system_name = _get_system_name(solar_system_target_id)
|
||||
"[#{source_system_name}:#{target_system_name}]"
|
||||
end
|
||||
|
||||
defp get_event_data(:map_connection_updated, %{
|
||||
defp _get_event_data(:map_connection_updated, %{
|
||||
"key" => key,
|
||||
"solar_system_source_id" => solar_system_source_id,
|
||||
"solar_system_target_id" => solar_system_target_id,
|
||||
"value" => value
|
||||
}) do
|
||||
source_system_name = get_system_name(solar_system_source_id)
|
||||
target_system_name = get_system_name(solar_system_target_id)
|
||||
source_system_name = _get_system_name(solar_system_source_id)
|
||||
target_system_name = _get_system_name(solar_system_target_id)
|
||||
"[#{source_system_name}:#{target_system_name}] #{key} - #{inspect(value)}"
|
||||
end
|
||||
|
||||
defp get_event_data(_name, data), do: Jason.encode!(data)
|
||||
defp _get_event_data(_name, data), do: Jason.encode!(data)
|
||||
|
||||
defp get_system_name(solar_system_id) do
|
||||
defp _get_system_name(solar_system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, nil} ->
|
||||
solar_system_id
|
||||
|
||||
@@ -44,46 +44,51 @@ defmodule WandererAppWeb.APIController do
|
||||
end
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Map
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
GET /api/map/systems
|
||||
@doc """
|
||||
GET /api/map/systems
|
||||
|
||||
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
|
||||
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
|
||||
|
||||
If `?all=true` is provided, **all** systems are returned.
|
||||
Otherwise, only "visible" systems are returned.
|
||||
If `?all=true` is provided, **all** systems are returned.
|
||||
Otherwise, only "visible" systems are returned.
|
||||
|
||||
Examples:
|
||||
GET /api/map/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
|
||||
GET /api/map/systems?slug=my-unique-wormhole-map
|
||||
GET /api/map/systems?map_id=<UUID>&all=true
|
||||
"""
|
||||
def list_systems(conn, params) do
|
||||
with {:ok, map_id} <- fetch_map_id(params) do
|
||||
repo_fun =
|
||||
if params["all"] == "true" do
|
||||
&MapSystemRepo.get_all_by_map/1
|
||||
else
|
||||
&MapSystemRepo.get_visible_by_map/1
|
||||
end
|
||||
|
||||
case repo_fun.(map_id) do
|
||||
{:ok, systems} ->
|
||||
data = Enum.map(systems, &map_system_to_json/1)
|
||||
json(conn, %{data: data})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Could not fetch systems for map_id=#{map_id}: #{inspect(reason)}"})
|
||||
Examples:
|
||||
GET /api/map/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
|
||||
GET /api/map/systems?slug=my-unique-wormhole-map
|
||||
GET /api/map/systems?map_id=<UUID>&all=true
|
||||
"""
|
||||
def list_systems(conn, params) do
|
||||
with {:ok, map_id} <- fetch_map_id(params) do
|
||||
# Decide which function to call based on the "all" param
|
||||
repo_fun =
|
||||
if params["all"] == "true" do
|
||||
&MapSystemRepo.get_all_by_map/1
|
||||
else
|
||||
&MapSystemRepo.get_visible_by_map/1
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
|
||||
case repo_fun.(map_id) do
|
||||
{:ok, systems} ->
|
||||
data = Enum.map(systems, &map_system_to_json/1)
|
||||
json(conn, %{data: data})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: msg})
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Could not fetch systems for map_id=#{map_id}: #{inspect(reason)}"})
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: msg})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
GET /api/map/system
|
||||
@@ -175,97 +180,6 @@ defmodule WandererAppWeb.APIController do
|
||||
end
|
||||
end
|
||||
|
||||
def show_structure_timers(conn, params) do
|
||||
with {:ok, map_id} <- fetch_map_id(params) do
|
||||
system_id_str = params["system_id"]
|
||||
|
||||
case system_id_str do
|
||||
nil ->
|
||||
handle_all_structure_timers(conn, map_id)
|
||||
|
||||
_ ->
|
||||
case parse_int(system_id_str) do
|
||||
{:ok, system_id} ->
|
||||
handle_single_structure_timers(conn, map_id, system_id)
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "system_id must be int: #{reason}"})
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: msg})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_all_structure_timers(conn, map_id) do
|
||||
case MapSystemRepo.get_visible_by_map(map_id) do
|
||||
{:ok, systems} ->
|
||||
all_timers =
|
||||
systems
|
||||
|> Enum.flat_map(&get_timers_for_system/1)
|
||||
|
||||
json(conn, %{data: all_timers})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Could not fetch visible systems for map_id=#{map_id}: #{inspect(reason)}"})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_single_structure_timers(conn, map_id, system_id) do
|
||||
case MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
|
||||
{:ok, map_system} ->
|
||||
timers = get_timers_for_system(map_system)
|
||||
json(conn, %{data: timers})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "No system with solar_system_id=#{system_id} in map=#{map_id}"})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Failed to retrieve system: #{inspect(reason)}"})
|
||||
end
|
||||
end
|
||||
|
||||
defp get_timers_for_system(map_system) do
|
||||
structures = WandererApp.Api.MapSystemStructure.by_system_id!(map_system.id)
|
||||
|
||||
structures
|
||||
|> Enum.filter(&timer_needed?/1)
|
||||
|> Enum.map(&structure_to_timer_json/1)
|
||||
end
|
||||
|
||||
defp timer_needed?(structure) do
|
||||
structure.status in ["Anchoring", "Reinforced"] and not is_nil(structure.end_time)
|
||||
end
|
||||
|
||||
defp structure_to_timer_json(s) do
|
||||
Map.take(s, [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
])
|
||||
end
|
||||
|
||||
defp get_tracked_by_map_ids(map_id) do
|
||||
case MapCharacterSettingsRepo.get_tracked_by_map_all(map_id) do
|
||||
{:ok, settings_list} ->
|
||||
@@ -300,8 +214,7 @@ defmodule WandererAppWeb.APIController do
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(_),
|
||||
do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
|
||||
defp require_param(params, key) do
|
||||
case params[key] do
|
||||
|
||||
@@ -156,7 +156,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
{:ok, map_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
|
||||
{:ok, map_characters} = get_tracked_map_characters(map_id, current_user)
|
||||
|
||||
user_character_eve_ids = map_characters |> Enum.map(& &1.eve_id)
|
||||
|
||||
@@ -204,7 +204,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
|
||||
# Find and filter user's characters
|
||||
{:ok, user_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
|
||||
{:ok, user_characters} = get_tracked_map_characters(map_id, current_user)
|
||||
user_char_ids = Enum.map(user_characters, & &1.id)
|
||||
|
||||
my_settings =
|
||||
@@ -213,7 +213,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
s.character_id in user_char_ids
|
||||
end)
|
||||
|
||||
existing = Enum.find(all_settings, &(&1.character_id == clicked_char_id))
|
||||
existing = Enum.find(my_settings, &(&1.character_id == clicked_char_id))
|
||||
|
||||
{:ok, target_setting} =
|
||||
if not is_nil(existing) do
|
||||
@@ -229,7 +229,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
|
||||
# If the target_setting is already followed => unfollow it
|
||||
if target_setting.followed do
|
||||
{:ok, _updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
|
||||
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
|
||||
else
|
||||
# Only unfollow other rows from the current user
|
||||
for s <- my_settings, s.id != target_setting.id, s.followed == true do
|
||||
@@ -245,13 +245,13 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
:ok = add_characters([char], map_id, true)
|
||||
end
|
||||
|
||||
{:ok, _updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
|
||||
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
|
||||
end
|
||||
|
||||
# re-fetch or re-map to confirm final results in UI
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
|
||||
{:ok, tracked_characters} = get_tracked_map_characters(map_id, current_user)
|
||||
user_eve_ids = Enum.map(tracked_characters, & &1.eve_id)
|
||||
|
||||
{:ok, final_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
|
||||
@@ -316,6 +316,21 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
def has_tracked_characters?([]), do: false
|
||||
def has_tracked_characters?(_user_characters), do: true
|
||||
|
||||
def get_tracked_map_characters(map_id, current_user) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(
|
||||
map_id,
|
||||
current_user.characters |> Enum.map(& &1.id)
|
||||
) do
|
||||
{:ok, settings} ->
|
||||
{:ok,
|
||||
settings
|
||||
|> Enum.map(fn s -> s |> Ash.load!(:character) |> Map.get(:character) end)}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
def map_ui_character(character),
|
||||
do:
|
||||
character
|
||||
|
||||
@@ -119,10 +119,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
),
|
||||
do: socket
|
||||
|
||||
def handle_server_event(%{event: :structures_updated, payload: _solar_system_id}, socket) do
|
||||
socket
|
||||
end
|
||||
|
||||
def handle_server_event(event, socket) do
|
||||
Logger.warning(fn -> "unhandled map core event: #{inspect(event)} #{inspect(socket)} " end)
|
||||
socket
|
||||
@@ -270,8 +266,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
current_user.characters |> Enum.map(& &1.id)
|
||||
)
|
||||
|
||||
{:ok, map_user_settings} = WandererApp.MapUserSettingsRepo.get(map_id, current_user.id)
|
||||
|
||||
{:ok, character_settings} =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
@@ -304,7 +298,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
socket
|
||||
|> assign(
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings,
|
||||
page_title: map_name,
|
||||
user_permissions: user_permissions,
|
||||
tracked_character_ids: tracked_character_ids,
|
||||
@@ -337,8 +330,9 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
} = socket
|
||||
) do
|
||||
with {:ok, _} <- current_user |> WandererApp.Api.User.update_last_map(%{last_map_id: map_id}),
|
||||
{:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user.id),
|
||||
{:ok, tracked_map_characters} <-
|
||||
WandererApp.Maps.get_tracked_map_characters(map_id, current_user),
|
||||
MapCharactersEventHandler.get_tracked_map_characters(map_id, current_user),
|
||||
{:ok, characters_limit} <- map_id |> WandererApp.Map.get_characters_limit(),
|
||||
{:ok, present_character_ids} <-
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", []),
|
||||
@@ -416,6 +410,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
socket
|
||||
|> map_start(%{
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings,
|
||||
user_characters: user_character_eve_ids,
|
||||
initial_data: initial_data,
|
||||
events: events
|
||||
@@ -438,6 +433,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
socket,
|
||||
%{
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings,
|
||||
user_characters: user_character_eve_ids,
|
||||
initial_data: initial_data,
|
||||
events: events
|
||||
@@ -468,6 +464,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
socket
|
||||
|> assign(
|
||||
map_loaded?: true,
|
||||
map_user_settings: map_user_settings,
|
||||
user_characters: user_character_eve_ids,
|
||||
has_tracked_characters?: has_tracked_characters?
|
||||
)
|
||||
@@ -541,6 +538,21 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
}
|
||||
end
|
||||
|
||||
defp get_tracked_map_characters(map_id, current_user) do
|
||||
case WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(
|
||||
map_id,
|
||||
current_user.characters |> Enum.map(& &1.id)
|
||||
) do
|
||||
{:ok, settings} ->
|
||||
{:ok,
|
||||
settings
|
||||
|> Enum.map(fn s -> s |> Ash.load!(:character) |> Map.get(:character) end)}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_map_characters(
|
||||
characters,
|
||||
user_character_eve_ids,
|
||||
|
||||
@@ -81,7 +81,6 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
},
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings,
|
||||
user_characters: user_characters,
|
||||
@@ -135,14 +134,6 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
})
|
||||
end
|
||||
|
||||
if not is_nil(s.linked_system_id) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
|
||||
solar_system_id: s.linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
end
|
||||
|
||||
s
|
||||
|> Ash.destroy!()
|
||||
end)
|
||||
@@ -162,40 +153,10 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
end)
|
||||
|
||||
added_signatures
|
||||
|> Enum.each(fn s ->
|
||||
|> Enum.map(fn s ->
|
||||
s |> WandererApp.Api.MapSystemSignature.create!()
|
||||
end)
|
||||
|
||||
added_signatures_eve_ids =
|
||||
added_signatures
|
||||
|> Enum.map(fn s -> s.eve_id end)
|
||||
|
||||
first_tracked_character =
|
||||
current_user.characters
|
||||
|> Enum.find(fn c -> c.eve_id === first_character_eve_id end)
|
||||
|
||||
if not is_nil(first_tracked_character) &&
|
||||
not (added_signatures_eve_ids |> Enum.empty?()) do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_added, %{
|
||||
character_id: first_tracked_character.id,
|
||||
user_id: current_user.id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: added_signatures_eve_ids
|
||||
})
|
||||
end
|
||||
|
||||
if not is_nil(first_tracked_character) &&
|
||||
not (removed_signatures_eve_ids |> Enum.empty?()) do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
|
||||
character_id: first_tracked_character.id,
|
||||
user_id: current_user.id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: removed_signatures_eve_ids
|
||||
})
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :signatures_updated,
|
||||
payload: system.solar_system_id
|
||||
@@ -273,19 +234,11 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
})
|
||||
end)
|
||||
|
||||
map_system =
|
||||
WandererApp.Map.find_system_by_location(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_target}
|
||||
)
|
||||
|
||||
if not is_nil(map_system) && is_nil(map_system.linked_sig_eve_id) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
|
||||
solar_system_id: solar_system_target,
|
||||
linked_sig_eve_id: signature_eve_id
|
||||
})
|
||||
end
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
|
||||
solar_system_id: solar_system_target,
|
||||
linked_sig_eve_id: signature_eve_id
|
||||
})
|
||||
|
||||
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :signatures_updated,
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
defmodule WandererAppWeb.MapStructuresEventHandler do
|
||||
use WandererAppWeb, :live_component
|
||||
use Phoenix.Component
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.MapEventHandler
|
||||
alias WandererApp.Api.MapSystem
|
||||
alias WandererApp.Structure
|
||||
|
||||
def handle_ui_event("get_structures", %{"system_id" => solar_system_id}, %{assigns: %{map_id: map_id}} = socket) do
|
||||
case MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: String.to_integer(solar_system_id)
|
||||
}) do
|
||||
{:ok, system} ->
|
||||
{:reply, %{structures: get_system_structures(system.id)}, socket}
|
||||
|
||||
_ ->
|
||||
{:reply, %{structures: []}, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"update_structures",
|
||||
%{
|
||||
"system_id" => solar_system_id,
|
||||
"added" => added_structures,
|
||||
"updated" => updated_structures,
|
||||
"removed" => removed_structures
|
||||
},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
user_characters: user_characters,
|
||||
user_permissions: %{update_system: true}
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
with {:ok, system} <- get_map_system(map_id, solar_system_id),
|
||||
:ok <- ensure_user_has_tracked_character(user_characters) do
|
||||
Logger.debug(fn ->
|
||||
"[handle_ui_event:update_structures] loaded map_system =>\n" <>
|
||||
inspect(system, pretty: true)
|
||||
end)
|
||||
|
||||
Structure.update_structures(
|
||||
system,
|
||||
added_structures,
|
||||
updated_structures,
|
||||
removed_structures,
|
||||
user_characters
|
||||
)
|
||||
|
||||
broadcast_structures_updated(system, map_id)
|
||||
|
||||
{:reply, %{structures: get_system_structures(system.id)}, socket}
|
||||
else
|
||||
:no_tracked_character ->
|
||||
{:reply,
|
||||
%{structures: []},
|
||||
put_flash(socket, :error, "You must have at least one tracked character to work with structures.")}
|
||||
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event("get_corporation_names", %{"search" => search}, %{assigns: %{current_user: current_user}} = socket) do
|
||||
user_chars = current_user.characters
|
||||
|
||||
case Structure.search_corporation_names(user_chars, search) do
|
||||
{:ok, results} ->
|
||||
{:reply, %{results: results}, socket}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[MapStructuresEventHandler] corp search failed: #{inspect(reason)}")
|
||||
{:reply, %{results: []}, socket}
|
||||
|
||||
_ ->
|
||||
{:reply, %{results: []}, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event("get_corporation_ticker", %{"corp_id" => corp_id}, socket) do
|
||||
case WandererApp.Esi.get_corporation_info(corp_id) do
|
||||
{:ok, %{"ticker" => ticker}} ->
|
||||
{:reply, %{ticker: ticker}, socket}
|
||||
|
||||
_ ->
|
||||
{:reply, %{ticker: nil}, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_system(map_id, solar_system_id) do
|
||||
case MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: String.to_integer(solar_system_id)
|
||||
}) do
|
||||
{:ok, system} -> {:ok, system}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_user_has_tracked_character(user_characters) do
|
||||
if Enum.empty?(user_characters) or is_nil(List.first(user_characters)) do
|
||||
:no_tracked_character
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp broadcast_structures_updated(system, map_id) do
|
||||
Phoenix.PubSub.broadcast!(
|
||||
WandererApp.PubSub,
|
||||
map_id,
|
||||
%{event: :structures_updated, payload: system.solar_system_id}
|
||||
)
|
||||
end
|
||||
|
||||
def get_system_structures(system_id) do
|
||||
results =
|
||||
WandererApp.Api.MapSystemStructure.by_system_id!(system_id)
|
||||
|> Enum.map(fn record ->
|
||||
record
|
||||
|> Map.take([
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:solar_system_name,
|
||||
:structure_type_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time,
|
||||
:inserted_at,
|
||||
:updated_at,
|
||||
:structure_type
|
||||
])
|
||||
|> Map.update!(:inserted_at, &Calendar.strftime(&1, "%Y/%m/%d %H:%M:%S"))
|
||||
|> Map.update!(:updated_at, &Calendar.strftime(&1, "%Y/%m/%d %H:%M:%S"))
|
||||
end)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[get_system_structures] => returning:\n" <> inspect(results, pretty: true)
|
||||
end)
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
@@ -21,63 +21,57 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
|> MapEventHandler.push_map_event("remove_systems", solar_system_ids)
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
event: :maybe_select_system,
|
||||
payload: %{
|
||||
character_id: character_id,
|
||||
solar_system_id: solar_system_id
|
||||
}
|
||||
},
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
is_user_character =
|
||||
current_user.characters
|
||||
|> Enum.map(& &1.id)
|
||||
|> Enum.member?(character_id)
|
||||
%{
|
||||
event: :maybe_select_system,
|
||||
payload: %{
|
||||
character_id: character_id,
|
||||
solar_system_id: solar_system_id
|
||||
}
|
||||
},
|
||||
%{assigns: %{current_user: current_user, map_id: map_id, map_user_settings: map_user_settings}} = socket
|
||||
) do
|
||||
|
||||
is_select_on_spash =
|
||||
map_user_settings
|
||||
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|
||||
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
|
||||
is_user_character =
|
||||
current_user.characters
|
||||
|> Enum.map(& &1.id)
|
||||
|> Enum.member?(character_id)
|
||||
|
||||
is_followed =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
|
||||
{:ok, setting} -> setting.followed == true
|
||||
_ -> false
|
||||
end
|
||||
is_select_on_spash =
|
||||
map_user_settings
|
||||
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|
||||
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
|
||||
|
||||
must_select? = is_user_character && (is_select_on_spash || is_followed)
|
||||
is_followed =
|
||||
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
|
||||
{:ok, setting} -> setting.followed == true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
if not must_select? do
|
||||
socket
|
||||
else
|
||||
# Check if we already selected this exact system for this char:
|
||||
last_selected =
|
||||
WandererApp.Cache.lookup!(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
nil
|
||||
)
|
||||
|
||||
if last_selected == solar_system_id do
|
||||
# same system => skip
|
||||
must_select? = is_user_character && (is_select_on_spash || is_followed)
|
||||
if not must_select? do
|
||||
socket
|
||||
else
|
||||
# new system => update cache + push event
|
||||
WandererApp.Cache.put(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
# Check if we already selected this exact system for this char:
|
||||
last_selected =
|
||||
WandererApp.Cache.lookup!(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
nil
|
||||
)
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
if last_selected == solar_system_id do
|
||||
# same system => skip
|
||||
socket
|
||||
else
|
||||
# new system => update cache + push event
|
||||
WandererApp.Cache.put(
|
||||
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_server_event(%{event: :kills_updated, payload: kills}, socket) do
|
||||
|
||||
@@ -171,9 +171,7 @@ defmodule WandererAppWeb.MapAuditLive do
|
||||
{"ACL Removed", :map_acl_removed},
|
||||
{"Connection Added", :map_connection_added},
|
||||
{"Connection Updated", :map_connection_updated},
|
||||
{"Connection Removed", :map_connection_removed},
|
||||
{"Signatures Added", :signatures_added},
|
||||
{"Signatures Removed", :signatures_removed}
|
||||
{"Connection Removed", :map_connection_removed}
|
||||
])
|
||||
|> load_activity(1)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
<main
|
||||
id="map-events-list"
|
||||
class="pt-20 w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 pb-20 overflow-auto"
|
||||
>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<.live_component
|
||||
module={UserActivity}
|
||||
id="user-activity"
|
||||
notify_to={self()}
|
||||
can_undo_types={@can_undo_types}
|
||||
stream={@streams.activity}
|
||||
page={@page}
|
||||
end_of_stream?={@end_of_stream?}
|
||||
event_name="activity_event"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<nav class="fixed top-0 z-100 px-6 pl-20 flex items-center justify-between w-full h-12 pointer-events-auto border-b border-stone-800 bg-opacity-70 bg-neutral-900">
|
||||
<span className="w-full font-medium text-sm">
|
||||
<.link navigate={~p"/#{@map_slug}"} class="text-neutral-100">
|
||||
@@ -96,20 +113,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main
|
||||
id="map-events-list"
|
||||
class="pt-20 w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 pb-20 overflow-auto"
|
||||
>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<.live_component
|
||||
module={UserActivity}
|
||||
id="user-activity"
|
||||
notify_to={self()}
|
||||
can_undo_types={@can_undo_types}
|
||||
stream={@streams.activity}
|
||||
page={@page}
|
||||
end_of_stream?={@end_of_stream?}
|
||||
event_name="activity_event"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -10,8 +10,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
MapCoreEventHandler,
|
||||
MapRoutesEventHandler,
|
||||
MapSignaturesEventHandler,
|
||||
MapSystemsEventHandler,
|
||||
MapStructuresEventHandler,
|
||||
MapSystemsEventHandler
|
||||
}
|
||||
|
||||
@map_characters_events [
|
||||
@@ -104,17 +103,6 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
"unlink_signature"
|
||||
]
|
||||
|
||||
@map_structures_events [
|
||||
:structures_updated,
|
||||
]
|
||||
|
||||
@map_structures_ui_events [
|
||||
"update_structures",
|
||||
"get_structures",
|
||||
"get_corporation_names",
|
||||
"get_corporation_ticker",
|
||||
]
|
||||
|
||||
def handle_event(socket, %{event: event_name} = event)
|
||||
when event_name in @map_characters_events,
|
||||
do: MapCharactersEventHandler.handle_server_event(event, socket)
|
||||
@@ -135,10 +123,6 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
when event_name in @map_routes_events,
|
||||
do: MapRoutesEventHandler.handle_server_event(event, socket)
|
||||
|
||||
def handle_event(socket, %{event: event_name} = event)
|
||||
when event_name in @map_structures_events,
|
||||
do: MapSignaturesEventHandler.handle_server_event(event, socket)
|
||||
|
||||
def handle_event(socket, %{event: event_name} = event)
|
||||
when event_name in @map_signatures_events,
|
||||
do: MapSignaturesEventHandler.handle_server_event(event, socket)
|
||||
@@ -191,10 +175,6 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
when event in @map_signatures_ui_events,
|
||||
do: MapSignaturesEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
def handle_ui_event(event, body, socket)
|
||||
when event in @map_structures_ui_events,
|
||||
do: MapStructuresEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
def handle_ui_event(event, body, socket)
|
||||
when event in @map_activity_ui_events,
|
||||
do: MapActivityEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
@@ -112,7 +112,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
subscription_form = %{
|
||||
"plan" => "omega",
|
||||
"period" => "1",
|
||||
"characters_limit" => "100",
|
||||
"characters_limit" => "300",
|
||||
"hubs_limit" => "10",
|
||||
"auto_renew?" => true
|
||||
}
|
||||
@@ -636,33 +636,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
{:map_acl_updated, added_acls, removed_acls}
|
||||
)
|
||||
|
||||
{:ok, tracked_characters} =
|
||||
WandererApp.Maps.get_tracked_map_characters(map.id, current_user)
|
||||
|
||||
first_tracked_character_id = Enum.map(tracked_characters, & &1.id) |> List.first()
|
||||
|
||||
added_acls
|
||||
|> Enum.each(fn acl_id ->
|
||||
{:ok, _} =
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_acl_added, %{
|
||||
character_id: first_tracked_character_id,
|
||||
user_id: current_user.id,
|
||||
map_id: map.id,
|
||||
acl_id: acl_id
|
||||
})
|
||||
end)
|
||||
|
||||
removed_acls
|
||||
|> Enum.each(fn acl_id ->
|
||||
{:ok, _} =
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_acl_removed, %{
|
||||
character_id: first_tracked_character_id,
|
||||
user_id: current_user.id,
|
||||
map_id: map.id,
|
||||
acl_id: acl_id
|
||||
})
|
||||
end)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_async(:maps, fn ->
|
||||
@@ -711,7 +684,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
"layout",
|
||||
"store_custom_labels",
|
||||
"show_linked_signature_id",
|
||||
"show_linked_signature_id_temp_name",
|
||||
"show_temp_system_name",
|
||||
"restrict_offline_showing"
|
||||
])
|
||||
|
||||
@@ -373,11 +373,6 @@
|
||||
field={f[:store_custom_labels]}
|
||||
label="Store system custom labels"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_temp_system_name]}
|
||||
label="Allow temporary system names"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_linked_signature_id]}
|
||||
@@ -385,8 +380,8 @@
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_linked_signature_id_temp_name]}
|
||||
label="Show linked signature ID as temporary name part"
|
||||
field={f[:show_temp_system_name]}
|
||||
label="Allow temporary system names"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
@@ -580,9 +575,11 @@
|
||||
>
|
||||
<div :if={is_nil(@selected_subscription)}>
|
||||
Add subscription
|
||||
<div class="badge badge-secondary">Limited time offer: 50%</div>
|
||||
</div>
|
||||
<div :if={not is_nil(@selected_subscription)}>
|
||||
Edit subscription
|
||||
<div class="badge badge-secondary">Limited time offer: 50%</div>
|
||||
</div>
|
||||
<.form
|
||||
:let={f}
|
||||
@@ -607,7 +604,7 @@
|
||||
label="Characters limit"
|
||||
show_value={true}
|
||||
type="range"
|
||||
min="100"
|
||||
min="300"
|
||||
max="5000"
|
||||
step="100"
|
||||
class="range range-xs"
|
||||
|
||||
@@ -129,9 +129,6 @@ scope "/api/map", WandererAppWeb do
|
||||
|
||||
# GET /api/map/characters?map_id=... or slug=...
|
||||
get "/characters", APIController, :tracked_characters_with_info
|
||||
|
||||
# GET /api/map/structure-timers?map_id=... or slug=... and optionally ?system_id=...
|
||||
get "/structure-timers", APIController, :show_structure_timers
|
||||
end
|
||||
|
||||
scope "/api/common", WandererAppWeb do
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.43.2"
|
||||
@version "1.38.3"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
%{
|
||||
title: "How to Add a Custom Theme",
|
||||
author: "Wanderer Community",
|
||||
cover_image_uri: "/images/news/01-13-theme-swap/theme-selector.png",
|
||||
tags: ~w(themes),
|
||||
description: "",
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# How to Add a Custom Theme
|
||||
|
||||
Adding a custom theme to your map is a great way to give it a unique look and feel. In this guide, we’ll walk you through the necessary steps to create and enable a brand-new theme, from updating the theme selector to creating custom SCSS files.
|
||||
|
||||
---
|
||||
|
||||
1. Add Your Theme to the Theme Selector
|
||||
|
||||
|
||||
Open the file:
|
||||
|
||||
assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx
|
||||
|
||||
|
||||
In this file, you’ll find an array called `THEME_OPTIONS`. Simply add your new theme to this array, giving it both a `label` and a `value`. For example:
|
||||
|
||||
```ts
|
||||
const THEME_OPTIONS = [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Pathfinder', value: 'pathfinder' },
|
||||
{ label: 'YourTheme', value: 'yourtheme' },
|
||||
];
|
||||
```
|
||||
|
||||
This ensures your new theme will appear in the theme selection menu.
|
||||
|
||||
---
|
||||
|
||||
2. Create the SCSS File for Your Theme
|
||||
|
||||
Next, you’ll need to create a new SCSS file to define your theme’s custom styles. Navigate to:
|
||||
|
||||
```
|
||||
assets/js/hooks/Mapper/components/map/styles
|
||||
```
|
||||
|
||||
and add a new file. You can use `pathfinder-theme.scss` as a reference. The **filename must be in the format**:
|
||||
|
||||
```
|
||||
yourthemename-theme.scss
|
||||
```
|
||||
|
||||
> **Why the specific format?**
|
||||
> The system looks for theme files following this naming pattern. If you choose a different format, it will not load correctly.
|
||||
|
||||
# 2.1. Define Your CSS Variables
|
||||
|
||||
Inside your theme SCSS file, you can override the variables below to customize colors, backgrounds, patterns, text, and more. For example:
|
||||
|
||||
```scss
|
||||
// yourtheme-theme.scss
|
||||
|
||||
:root {
|
||||
/* Main pane background color */
|
||||
--rf-bg-color: #222;
|
||||
--rf-soft-bg-color: #333;
|
||||
|
||||
/* Background pattern settings */
|
||||
--rf-bg-variant: lines;
|
||||
--rf-bg-gap: 10px;
|
||||
--rf-bg-size: 1px;
|
||||
--rf-bg-pattern-color: rgba(255, 255, 255, 0.15);
|
||||
|
||||
/* Node (system) appearance */
|
||||
--rf-node-bg-color: #444;
|
||||
--rf-node-soft-bg-color: #555;
|
||||
--rf-node-font-family: "Roboto", sans-serif;
|
||||
--rf-text-color: #f5f5f5;
|
||||
--rf-region-name: #a3e4d7;
|
||||
--rf-custom-name: #d7bde2;
|
||||
--rf-tag-color: #e59866;
|
||||
--rf-has-user-character: #f9e79f;
|
||||
|
||||
/* Eve-specific overrides */
|
||||
--eve-effect-nullsec: #ff0000;
|
||||
--eve-wh-type-color-C1: #aaffaa;
|
||||
/* ...etc... */
|
||||
}
|
||||
```
|
||||
|
||||
> **Tip:** Feel free to rename or add new custom variables as necessary, but keep in mind the defaults and naming conventions used throughout the existing code.
|
||||
|
||||
---
|
||||
|
||||
3. Customize Node-Related Styles
|
||||
|
||||
If you want to override more specific aspects of the node styling, review the file:
|
||||
|
||||
```
|
||||
assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.module.scss
|
||||
```
|
||||
|
||||
This file shows which variables are already set up for styling through CSS variables. If the element you want to style already has a variable reference, you can simply override that variable in your SCSS theme file.
|
||||
|
||||
---
|
||||
|
||||
4. Update Theme Behavior (Optional)
|
||||
|
||||
Finally, if your theme requires special interactions, you can update the theme behavior in:
|
||||
|
||||
```
|
||||
assets/js/hooks/Mapper/components/map/helpers/getThemeBehavior.ts
|
||||
```
|
||||
|
||||
By default, some overrides are already set up. For example:
|
||||
|
||||
- `isPanAndDrag: true` sets left-click to select, and right-click to pan. (When `false`, it uses the default behavior)
|
||||
- `nodeComponent: SolarSystemNodeTheme` specifies a special node component that uses theme CSS overrides -- you could also provide your own node component here
|
||||
- `connectionMode: ConnectionMode.Loose` allows you to control how strict the connection handles are.
|
||||
|
||||
If your theme needs custom logic—like a different node component or a unique interaction pattern—this is where you’d implement it.
|
||||
|
||||
---
|
||||
|
||||
Summary
|
||||
|
||||
1. **Add your theme** to `THEME_OPTIONS` in `MapSettings.tsx`.
|
||||
2. **Create a custom SCSS file** with the pattern `yourtheme-theme.scss` and override any relevant variables.
|
||||
3. **Check for additional styling** in `SolarSystemNodeTheme.module.scss` to see if there are more elements you’d like to override.
|
||||
4. (Optional) **Modify the theme behavior** in `getThemeBehavior.ts` if you want your theme to have unique interaction patterns or different default behaviors.
|
||||
|
||||
By following these steps, you’ll be able to quickly add your own themed experience to the map. If you need to make further changes (like adding new variables or hooking into different node components), just follow the same pattern and refer to existing examples in the codebase. Happy theming!
|
||||
|
||||
---
|
||||
|
||||
### Example of heavily customize node component and theme
|
||||
|
||||

|
||||
@@ -1,151 +0,0 @@
|
||||
%{
|
||||
title: "Managing Upwell Structures & Timers with the Structures Widget",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/structure-widget/cover.png",
|
||||
tags: ~w(interface guide map structures),
|
||||
description: "Learn how to track structure information using the Structures Widget."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### Introduction
|
||||
|
||||
Upwell structures like **Astrahus**, **Athanor**, and more are key strategic points in EVE Online. Staying informed about their statuses—whether they’re anchoring, powered, or reinforced—helps you plan defenses, coordinate attacks, and align with allies. Our **Structures Widget** simplifies the process by allowing you to:
|
||||
|
||||
- Copy structure information directly from the in-game Directional Scanner (`D-Scan`) and paste it into the widget.
|
||||
- Keep track of **anchoring** or **reinforced** timers, including exact vulnerability windows.
|
||||
- Share real-time data across the map with your corporation or alliance, ensuring everyone is on the same page.
|
||||
|
||||
In this guide, we’ll explore how to enable the Structures Widget, manage structure data, and make use of the built-in API for remote structure updates.
|
||||
|
||||
---
|
||||
|
||||
### 1. Enabling the Structure Widget
|
||||
|
||||

|
||||
|
||||
1. **Open the Map:**
|
||||
2. **Locate the Widget Settings:** By default, the structure widget panel is not visible. Enable it by going to menu -> map settings -> widgets.
|
||||
3. **Add the Structures Widget:** Click the checkbox for **Structures** from the list of available widgets.
|
||||
|
||||
> **Tip:** Rearrange your widgets by dragging them around the panel to suit your workflow.
|
||||
|
||||
---
|
||||
|
||||
### 2. Overview of the Structures Widget
|
||||
|
||||

|
||||
|
||||
Once enabled, the **Structures Widget** appears in the map. It shows:
|
||||
|
||||
- **Structure Type** (Astrahus, Fortizar, etc.)
|
||||
- **Structure Name** (auto-detected if you paste from D-Scan)
|
||||
- **Owner** (Corporation ticker)
|
||||
- **Status** (Powered, Anchoring, Low Power, Reinforced, etc.)
|
||||
- **Timer** (Reinforced or anchoring end time)
|
||||
|
||||
You can **click** or **double-click** on an entry to edit details like the structure’s owner or add notes about the structure’s purpose or location.
|
||||
|
||||
---
|
||||
|
||||
### 3. Adding Structures via Copy & Paste
|
||||
|
||||
A fast way to add structure data is by copying from in-game D-Scan or show-info panels:
|
||||
|
||||
1. **In EVE Online:** Open the D-Scan window or structure context menu, select the relevant lines of text, and press **Ctrl + C**.
|
||||
2. **In the Widget:** Focus on the Structures Widget, click in the widget area, and press **Ctrl + V** to paste or use the **blue** add structure info button.
|
||||
3. The widget automatically parses the structure names and types. You can also add owners and notes manually.
|
||||
|
||||
This eliminates manual typing and reduces the chance of errors, especially useful when scanning multiple systems.
|
||||
|
||||
---
|
||||
|
||||
### 4. Tracking Reinforced Timers
|
||||
|
||||
When a structure is in a **Reinforced** or **Anchoring** state, we have a timer to note when it becomes vulnerable or completes anchoring:
|
||||
|
||||
- **Timer Field:** If the structure’s status is set to “Reinforced” or “Anchoring,” the widget enables a **Calendar** pop-up where you can set the _end time_.
|
||||
|
||||
Keep your fleet prepared by referencing this schedule. When the timer hits zero, the structure becomes vulnerable (or finishes anchoring).
|
||||
|
||||
---
|
||||
|
||||
### 5. Editing and Deleting Structures
|
||||
|
||||
1. **Single-click** a structure entry to select it.
|
||||
2. Press **Delete** (or **Backspace**) to remove it entirely—useful when clearing out old data or removing outdated structures.
|
||||
3. **Double-click** to open the **Edit Dialog**:
|
||||
- Change **Name**, **Owner**, or **Status**.
|
||||
- Update or remove **Reinforced** timers.
|
||||
- Add or edit **Notes**.
|
||||
|
||||
Any changes made here are immediately visible to other map users.
|
||||
|
||||
---
|
||||
|
||||
### 6. API Integration for Automated Timers
|
||||
|
||||
Beyond the in-app widget, there is a dedicated API endpoint to fetch or update structure timers programmatically. This allows advanced users and third-party applications to seamlessly incorporate structure data.
|
||||
|
||||
**Example API Request/Response**:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
"https://wanderer.yourdomain.space/api/map/structure-timers?slug=yourmap"
|
||||
|
||||
"data": [
|
||||
{
|
||||
"name": "Overlook Hotel",
|
||||
"status": "Reinforced",
|
||||
"notes": null,
|
||||
"owner_id": null,
|
||||
"solar_system_id": 31000515,
|
||||
"solar_system_name": "J114942",
|
||||
"character_eve_id": "2122839817",
|
||||
"system_id": "4865aec4-b69d-4524-91d3-250b0556322b",
|
||||
"end_time": "2025-01-22T23:42:03.000000Z",
|
||||
"owner_name": null,
|
||||
"owner_ticker": null,
|
||||
"structure_type": "Astrahus",
|
||||
"structure_type_id": "35832"
|
||||
},
|
||||
{
|
||||
"name": "Some Structure",
|
||||
"status": "Reinforced",
|
||||
"notes": null,
|
||||
"owner_id": null,
|
||||
"solar_system_id": 3100229,
|
||||
"solar_system_name": "somecustomname",
|
||||
"character_eve_id": "some name",
|
||||
"system_id": "ae779ed6-92b3-4349-899d-f1bdf299082f",
|
||||
"end_time": "2025-01-16T03:04:00.000000Z",
|
||||
"owner_name": null,
|
||||
"owner_ticker": null,
|
||||
"structure_type": "Athanor",
|
||||
"structure_type_id": "35835"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
With this API, you could, for example, build automated pings on Slack/Discord when timers are about to expire or display status updates on a custom web dashboard.
|
||||
|
||||
> **Note:** Ensure your API token (`Bearer YOUR_API_TOKEN`) matches the api key generated for you map.
|
||||
|
||||
---
|
||||
|
||||
### 7. Best Practices & Tips
|
||||
|
||||
- **Keep Data Fresh:** Update timers as soon as possible after a structure enters reinforcement. This keeps your corporation or alliance fully informed.
|
||||
- **Use Notes Effectively:** Add details such as final reinforcement phases or relevant system intel (e.g., known hostiles, safe spots) to help allies plan more effectively.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The **Structures Widget** is your central hub for monitoring, updating, and sharing information about Upwell structures across New Eden. From real-time timer tracking to simple copy-and-paste integration with D-Scan, this widget streamlines group operations and cuts down on manual data entry.
|
||||
|
||||
Whether you’re a solo explorer managing a personal citadel network or a fleet commander overseeing multiple staging systems, the Structures Widget and its accompanying API ensure you’ll always have up-to-date intel on the structures that matter most.
|
||||
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
@@ -222,7 +222,7 @@
|
||||
{
|
||||
"mass_regen": 500000000,
|
||||
"dest": "hs",
|
||||
"src": ["c3", "c4-shattered"],
|
||||
"src": ["c3"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"lifetime": "24",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddSystemStructures do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:map_system_structures_v1, primary_key: false) do
|
||||
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
|
||||
add :structure_type_id, :text, null: false
|
||||
add :structure_type, :text, null: false
|
||||
add :character_eve_id, :text, null: false
|
||||
add :solar_system_name, :text, null: false
|
||||
add :solar_system_id, :bigint, null: false
|
||||
add :name, :text, null: false
|
||||
add :notes, :text
|
||||
add :owner_name, :text
|
||||
add :owner_ticker, :text
|
||||
add :owner_id, :text
|
||||
add :status, :text
|
||||
add :end_time, :utc_datetime_usec
|
||||
|
||||
add :inserted_at, :utc_datetime_usec,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
|
||||
add :updated_at, :utc_datetime_usec,
|
||||
null: false,
|
||||
default: fragment("(now() AT TIME ZONE 'utc')")
|
||||
|
||||
add :system_id,
|
||||
references(:map_system_v1,
|
||||
column: :id,
|
||||
name: "map_system_structures_v1_system_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop constraint(:map_system_structures_v1, "map_system_structures_v1_system_id_fkey")
|
||||
|
||||
drop table(:map_system_structures_v1)
|
||||
end
|
||||
end
|
||||
@@ -1,198 +0,0 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "structure_type_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "structure_type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "character_eve_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "solar_system_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "solar_system_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "notes",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "owner_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "owner_ticker",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "owner_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "status",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "end_time",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_system_structures_v1_system_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "map_system_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "system_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "B9DA704034C53F0EC20C28EED99D579A34034655225EDC3BC7E57719B276F83F",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_system_structures_v1"
|
||||
}
|
||||
Reference in New Issue
Block a user