mirror of
				https://github.com/wanderer-industries/wanderer
				synced 2025-10-30 14:07:03 +00:00 
			
		
		
		
	Compare commits
	
		
			155 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b7995f50de | ||
|   | 14997a2959 | ||
|   | 8fef6bcf82 | ||
|   | 1f82d23963 | ||
|   | 28317a2431 | ||
|   | 6aac496a57 | ||
|   | ac9306b713 | ||
|   | d55e804efa | ||
|   | 08407a5679 | ||
|   | c37d175bec | ||
|   | 69c5326e72 | ||
|   | 305f63e11d | ||
|   | 698fd5e083 | ||
|   | 1af8342d30 | ||
|   | 68b59da78e | ||
|   | e784a3f850 | ||
|   | a45e2f3fc2 | ||
|   | 8a3d920c31 | ||
|   | 996d7c47bd | ||
|   | 8d2b9db430 | ||
|   | 423ce343c7 | ||
|   | 1c17912d9f | ||
|   | 6714eb5d9b | ||
|   | 1620e1fd21 | ||
|   | 859014874f | ||
|   | ef44881f06 | ||
|   | b0532325fa | ||
|   | 2c00bd426e | ||
|   | 6eccf2ac67 | ||
|   | 973a1e54b3 | ||
|   | 2b42b637df | ||
|   | b950572818 | ||
|   | e470a210f1 | ||
|   | 71ec2d413c | ||
|   | 9122412558 | ||
|   | 0ba5c963b4 | ||
|   | 39a0ce284f | ||
|   | f9d580dbc0 | ||
|   | 5c41574328 | ||
|   | f17d74c8b7 | ||
|   | c88854c54c | ||
|   | f3779961d6 | ||
|   | d93fc29734 | ||
|   | c67918aca5 | ||
|   | a9f276c95a | ||
|   | 7cee4894a5 | ||
|   | edf8bef813 | ||
|   | 2081218398 | ||
|   | b100052453 | ||
|   | 71636e895e | ||
|   | 7ff9689b76 | ||
|   | 5a4d819622 | ||
|   | 3117d85648 | ||
|   | 114133ecd2 | ||
|   | bf8a1197e4 | ||
|   | 54c06a1fc0 | ||
|   | e77a42dfda | ||
|   | f83b4a2ba7 | ||
|   | d34e7b8d8a | ||
|   | fa0c7f3c66 | ||
|   | 5f58645b41 | ||
|   | 7ae0ec7573 | ||
|   | b1149cecaf | ||
|   | 8f28d2be65 | ||
|   | d758b54ef8 | ||
|   | 58293b4dc4 | ||
|   | f2083f4256 | ||
|   | 6c7bd5804e | ||
|   | 483ae21e89 | ||
|   | fc36d51e24 | ||
|   | f734565844 | ||
|   | 8c718ba181 | ||
|   | 8aaa2e7add | ||
|   | c8d8734601 | ||
|   | 5c757e8255 | ||
|   | 22f608f302 | ||
|   | 82f90ef759 | ||
|   | 167c8eea6b | ||
|   | d76079d4c7 | ||
|   | bf9c4cda02 | ||
|   | af00402546 | ||
|   | a245842ca4 | ||
|   | 8ddd672f13 | ||
|   | 92f471c0b0 | ||
|   | 9e2a2c5b44 | ||
|   | 5f5d3df003 | ||
|   | c66cc8868e | ||
|   | 0d6528ce4f | ||
|   | 34c385ac5f | ||
|   | b6d12e73a9 | ||
|   | 1118858120 | ||
|   | ae3a34d5bf | ||
|   | 43df42e49b | ||
|   | e670f3bf03 | ||
|   | c26a9404c5 | ||
|   | c0fad4ca92 | ||
|   | 16dbf9378b | ||
|   | 4001fe5eac | ||
|   | 2992dd8f8b | ||
|   | 98a03d1e59 | ||
|   | 2088393c79 | ||
|   | 093042b88a | ||
|   | e5ef35c186 | ||
|   | 1cd23d5efd | ||
|   | ead5818a3f | ||
|   | a5f66ada68 | ||
|   | 0919742853 | ||
|   | f3efffd259 | ||
|   | f85317983c | ||
|   | 76f709b768 | ||
|   | e3b2356302 | ||
|   | 3d810211ee | ||
|   | 7453795dc5 | ||
|   | 9de7cd99ee | ||
|   | 51489c1aa5 | ||
|   | 25dd6de770 | ||
|   | 9727405194 | ||
|   | 2a825f5a02 | ||
|   | 908d249eb9 | ||
|   | 6cd119e8f4 | ||
|   | 9a59c8eb75 | ||
|   | 452c022d41 | ||
|   | 27e9bab82a | ||
|   | edef860530 | ||
|   | 032cb63411 | ||
|   | a1791ba578 | ||
|   | 3a69fd7786 | ||
|   | 8a90723c2e | ||
|   | af2fc342c7 | ||
|   | 05ea2fcdbe | ||
|   | 6d4321fead | ||
|   | 3f6364c9ea | ||
|   | 0d11b12282 | ||
|   | 0796bcf7d0 | ||
|   | 0b5bec142a | ||
|   | a5020b58f2 | ||
|   | f039a74a8f | ||
|   | 0e6bb7390b | ||
|   | b52b4eecca | ||
|   | 8186977d1d | ||
|   | 86adcfe4d7 | ||
|   | ce2dd872c4 | ||
|   | aadc53c90e | ||
|   | cbc1b6b5c8 | ||
|   | 1aed7a9232 | ||
|   | b549189644 | ||
|   | 35279d17b4 | ||
|   | bb403aa0c5 | ||
|   | 04327c288b | ||
|   | 94d60e40d0 | ||
|   | 8505fcb6b7 | ||
|   | e0a37f7635 | ||
|   | 9aec57166d | ||
|   | a3739f2950 | ||
|   | 3d3b152758 | 
| @@ -6,3 +6,4 @@ export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>" | ||||
| export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>" | ||||
| export GIT_SHA="1111" | ||||
| export WANDERER_INVITES="false" | ||||
| export WANDERER_PUBLIC_API_DISABLED="false" | ||||
|   | ||||
							
								
								
									
										100
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										100
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -78,22 +78,23 @@ jobs: | ||||
|           fetch-depth: 0 | ||||
|       - name: 😅 Cache deps | ||||
|         id: cache-deps | ||||
|         uses: actions/cache@v3 | ||||
|         uses: actions/cache@v4 | ||||
|         env: | ||||
|           cache-name: cache-elixir-deps | ||||
|         with: | ||||
|           path: deps | ||||
|           key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} | ||||
|           path: | | ||||
|             deps | ||||
|           key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-mix-${{ env.cache-name }}- | ||||
|             ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}- | ||||
|       - name: 😅 Cache compiled build | ||||
|         id: cache-build | ||||
|         uses: actions/cache@v3 | ||||
|         uses: actions/cache@v4 | ||||
|         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') }}- | ||||
| @@ -122,6 +123,9 @@ 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 | ||||
| @@ -135,6 +139,7 @@ jobs: | ||||
|       matrix: | ||||
|         platform: | ||||
|           - linux/amd64 | ||||
|           - linux/arm64 | ||||
|     steps: | ||||
|       - name: Prepare | ||||
|         run: | | ||||
| @@ -183,15 +188,28 @@ jobs: | ||||
|           push: true | ||||
|           context: . | ||||
|           file: ./Dockerfile | ||||
|           tags: ${{ env.REGISTRY_IMAGE }}:latest,${{ env.REGISTRY_IMAGE }}:${{ steps.get-latest-tag.outputs.tag }} | ||||
|           cache-from: type=gha | ||||
|           cache-to: type=gha,mode=max | ||||
|           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: Image digest | ||||
|         run: echo ${{ steps.build.outputs.digest }} | ||||
|       - 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 | ||||
|  | ||||
|       - uses: markpatterson27/markdown-to-output@v1 | ||||
|         id: extract-changelog | ||||
| @@ -211,16 +229,54 @@ jobs: | ||||
|           maxLength: 500 | ||||
|           truncationSymbol: "…" | ||||
|  | ||||
|       - name: Discord Webhook Action | ||||
|         uses: tsickert/discord-webhook@v5.3.0 | ||||
|   merge: | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - docker | ||||
|     steps: | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@v4 | ||||
|         with: | ||||
|           webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} | ||||
|           content: ${{ steps.get-content.outputs.string }} | ||||
|           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 }} | ||||
|  | ||||
|   create-release: | ||||
|     name: 🏷 Create Release | ||||
|     runs-on: ubuntu-22.04 | ||||
|     needs: docker | ||||
|     needs: [docker, merge] | ||||
|     if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} | ||||
|     steps: | ||||
|       - name: ⬇️ Checkout repo | ||||
| @@ -228,17 +284,11 @@ 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: ${{ steps.get-latest-tag.outputs.tag }} | ||||
|           name: Release ${{ steps.get-latest-tag.outputs.tag }} | ||||
|           tag_name: ${{ needs.docker.outputs.release-tag }} | ||||
|           name: Release ${{ needs.docker.outputs.release-tag }} | ||||
|           body: | | ||||
|             ## Info | ||||
|             Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}). | ||||
| @@ -248,3 +298,9 @@ 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 }} | ||||
|   | ||||
							
								
								
									
										1529
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										1529
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -9,6 +9,9 @@ WORKDIR /app | ||||
| # set build ENV | ||||
| ENV MIX_ENV="prod" | ||||
|  | ||||
| # Set ERL_FLAGS for ARM compatibility | ||||
| ENV ERL_FLAGS="+JPperf true" | ||||
|  | ||||
| # install mix dependencies | ||||
| COPY mix.exs mix.lock ./ | ||||
| RUN rm -Rf _build deps && mix deps.get --only $MIX_ENV | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| @import 'primereact/resources/themes/arya-blue/theme.css' layer(primereact); | ||||
| /*@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css' layer(primereact);*/ | ||||
|  | ||||
| @import '../js/hooks/Mapper/components/map/styles/index.scss'; | ||||
|  | ||||
| @layer tailwind-base { | ||||
|   @tailwind base; | ||||
| } | ||||
|   | ||||
| @@ -108,3 +108,7 @@ | ||||
| .p-dropdown-empty-message { | ||||
|   padding: 0.25rem 0.5rem; | ||||
| } | ||||
|  | ||||
| .p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token { | ||||
|   margin-right: 0 !important; | ||||
| } | ||||
|   | ||||
| @@ -88,6 +88,23 @@ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseC | ||||
|     setSystem(undefined); | ||||
|   }, []); | ||||
|  | ||||
|   const onSystemTemporaryName = useCallback((temporaryName?: string) => { | ||||
|     const { system, outCommand } = ref.current; | ||||
|     if (!system) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     outCommand({ | ||||
|       type: OutCommand.updateSystemTemporaryName, | ||||
|       data: { | ||||
|         system_id: system, | ||||
|         value: temporaryName ?? '', | ||||
|       }, | ||||
|     }); | ||||
|     setSystem(undefined); | ||||
|   }, []); | ||||
|  | ||||
|  | ||||
|   const onSystemStatus = useCallback((status: number) => { | ||||
|     const { system, outCommand } = ref.current; | ||||
|     if (!system) { | ||||
| @@ -161,6 +178,7 @@ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseC | ||||
|     onLockToggle, | ||||
|     onHubToggle, | ||||
|     onSystemTag, | ||||
|     onSystemTemporaryName, | ||||
|     onSystemStatus, | ||||
|     onSystemLabels, | ||||
|     onOpenSettings, | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks'; | ||||
| import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components'; | ||||
| import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api'; | ||||
| import { UserPermission } from '@/hooks/Mapper/types/permissions.ts'; | ||||
| import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; | ||||
|  | ||||
| export const useContextMenuSystemItems = ({ | ||||
|   onDeleteSystem, | ||||
| @@ -44,6 +45,8 @@ export const useContextMenuSystemItems = ({ | ||||
|             <FastSystemActions | ||||
|               systemId={systemId} | ||||
|               systemName={system.system_static_info.solar_system_name} | ||||
|               regionName={system.system_static_info.region_name} | ||||
|               isWH={isWormholeSpace(system.system_static_info.system_class)} | ||||
|               showEdit | ||||
|               onOpenSettings={onOpenSettings} | ||||
|             /> | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/ty | ||||
| import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components'; | ||||
| import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks'; | ||||
| import { Route } from '@/hooks/Mapper/types/routes.ts'; | ||||
| import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; | ||||
|  | ||||
| export interface ContextMenuSystemInfoProps { | ||||
|   systemStatics: Map<number, SolarSystemStaticInfoRaw>; | ||||
| @@ -48,7 +49,6 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({ | ||||
|     if (!systemId || !system) { | ||||
|       return []; | ||||
|     } | ||||
|  | ||||
|     return [ | ||||
|       { | ||||
|         className: classes.FastActions, | ||||
| @@ -57,6 +57,8 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({ | ||||
|             <FastSystemActions | ||||
|               systemId={systemId} | ||||
|               systemName={system.solar_system_name} | ||||
|               regionName={system.region_name} | ||||
|               isWH={isWormholeSpace(system.system_class)} | ||||
|               onOpenSettings={onOpenSettings} | ||||
|             /> | ||||
|           ); | ||||
|   | ||||
| @@ -9,13 +9,22 @@ import { PrimeIcons } from 'primereact/api'; | ||||
| export interface FastSystemActionsProps { | ||||
|   systemId: string; | ||||
|   systemName: string; | ||||
|   regionName: string; | ||||
|   isWH: boolean; | ||||
|   showEdit?: boolean; | ||||
|   onOpenSettings(): void; | ||||
| } | ||||
|  | ||||
| export const FastSystemActions = ({ systemId, systemName, onOpenSettings, showEdit }: FastSystemActionsProps) => { | ||||
|   const ref = useRef({ systemId, systemName }); | ||||
|   ref.current = { systemId, systemName }; | ||||
| export const FastSystemActions = ({ | ||||
|   systemId, | ||||
|   systemName, | ||||
|   regionName, | ||||
|   isWH, | ||||
|   onOpenSettings, | ||||
|   showEdit, | ||||
| }: FastSystemActionsProps) => { | ||||
|   const ref = useRef({ systemId, systemName, regionName, isWH }); | ||||
|   ref.current = { systemId, systemName, regionName, isWH }; | ||||
|  | ||||
|   const handleOpenZKB = useCallback( | ||||
|     () => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'), | ||||
| @@ -27,10 +36,17 @@ export const FastSystemActions = ({ systemId, systemName, onOpenSettings, showEd | ||||
|     [], | ||||
|   ); | ||||
|  | ||||
|   const handleOpenDotlan = useCallback( | ||||
|     () => window.open(`https://evemaps.dotlan.net/system/${ref.current.systemName}`, '_blank'), | ||||
|     [], | ||||
|   ); | ||||
|   const handleOpenDotlan = useCallback(() => { | ||||
|     if (ref.current.isWH) { | ||||
|       window.open(`https://evemaps.dotlan.net/system/${ref.current.systemName}`, '_blank'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     return window.open( | ||||
|       `https://evemaps.dotlan.net/map/${ref.current.regionName.replace(/ /gim, '_')}/${ref.current.systemName}#jumps`, | ||||
|       '_blank', | ||||
|     ); | ||||
|   }, []); | ||||
|  | ||||
|   const copySystemNameToClipboard = useCallback(async () => { | ||||
|     try { | ||||
| @@ -43,9 +59,9 @@ export const FastSystemActions = ({ systemId, systemName, onOpenSettings, showEd | ||||
|   return ( | ||||
|     <LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}> | ||||
|       <div className={clsx('flex gap-2 items-center h-full', classes.Links)}> | ||||
|         <WdImgButton source={ZKB_ICON} onClick={handleOpenZKB} /> | ||||
|         <WdImgButton source={ANOIK_ICON} onClick={handleOpenAnoikis} /> | ||||
|         <WdImgButton source={DOTLAN_ICON} onClick={handleOpenDotlan} /> | ||||
|         <WdImgButton tooltip={{ content: 'Open zkillboard' }} source={ZKB_ICON} onClick={handleOpenZKB} /> | ||||
|         <WdImgButton tooltip={{ content: 'Open Anoikis' }} source={ANOIK_ICON} onClick={handleOpenAnoikis} /> | ||||
|         <WdImgButton tooltip={{ content: 'Open Dotlan' }} source={DOTLAN_ICON} onClick={handleOpenDotlan} /> | ||||
|       </div> | ||||
|  | ||||
|       <div className="flex gap-2 items-center pl-1"> | ||||
|   | ||||
| @@ -1,4 +1,11 @@ | ||||
| .MapRoot { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|  | ||||
|   background-color: var(--rf-bg-color, #0C0A09); | ||||
|  | ||||
|   &.BackgroundAlternateColor { | ||||
|     background-color: var(--rf-soft-bg-color, #171717); | ||||
|     --rf-node-bg-color: var(--rf-node-soft-bg-color, #202020); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect } from 'react'; | ||||
| import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react'; | ||||
| import ReactFlow, { | ||||
|   Background, | ||||
|   ConnectionMode, | ||||
|   Edge, | ||||
|   MiniMap, | ||||
|   Node, | ||||
| @@ -16,24 +15,24 @@ import ReactFlow, { | ||||
| } from 'reactflow'; | ||||
| import 'reactflow/dist/style.css'; | ||||
| import classes from './Map.module.scss'; | ||||
| import './styles/neon-theme.scss'; | ||||
| import './styles/eve-common.scss'; | ||||
| import { MapProvider, useMapState } from './MapProvider'; | ||||
| import { useNodesState, useEdgesState, useMapHandlers, useUpdateNodes } from './hooks'; | ||||
| import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks'; | ||||
| import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| import { | ||||
|   ContextMenuConnection, | ||||
|   ContextMenuRoot, | ||||
|   SolarSystemEdge, | ||||
|   SolarSystemNode, | ||||
|   useContextMenuConnectionHandlers, | ||||
|   useContextMenuRootHandlers, | ||||
| } from './components'; | ||||
| import { OnMapSelectionChange } from './map.types'; | ||||
| import { getBehaviorForTheme } from './helpers/getThemeBehavior'; | ||||
| import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types'; | ||||
| import { SESSION_KEY } from '@/hooks/Mapper/constants.ts'; | ||||
| import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types'; | ||||
| import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts'; | ||||
| import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts'; | ||||
| import clsx from 'clsx'; | ||||
| import { useBackgroundVars } from './hooks/useBackgroundVars'; | ||||
|  | ||||
| const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 }; | ||||
|  | ||||
| @@ -75,12 +74,6 @@ const initialEdges = [ | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const nodeTypes = { | ||||
|   // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||
|   // @ts-ignore | ||||
|   custom: SolarSystemNode, | ||||
| } as never; | ||||
|  | ||||
| const edgeTypes = { | ||||
|   floating: SolarSystemEdge, | ||||
| }; | ||||
| @@ -91,12 +84,16 @@ interface MapCompProps { | ||||
|   onSelectionChange: OnMapSelectionChange; | ||||
|   onManualDelete(systems: string[]): void; | ||||
|   onConnectionInfoClick?(e: SolarSystemConnection): void; | ||||
|   onAddSystem?: OnMapAddSystemCallback; | ||||
|   onSelectionContextMenu?: NodeSelectionMouseHandler; | ||||
|   minimapClasses?: string; | ||||
|   isShowMinimap?: boolean; | ||||
|   onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void; | ||||
|   showKSpaceBG?: boolean; | ||||
|   isThickConnections?: boolean; | ||||
|   isShowBackgroundPattern?: boolean; | ||||
|   isSoftBackground?: boolean; | ||||
|   theme?: string; | ||||
| } | ||||
|  | ||||
| const MapComp = ({ | ||||
| @@ -111,16 +108,29 @@ const MapComp = ({ | ||||
|   isShowMinimap, | ||||
|   showKSpaceBG, | ||||
|   isThickConnections, | ||||
|   isShowBackgroundPattern, | ||||
|   isSoftBackground, | ||||
|   theme, | ||||
|   onAddSystem, | ||||
| }: MapCompProps) => { | ||||
|   const { getNode } = useReactFlow(); | ||||
|   const { getNode, getNodes } = useReactFlow(); | ||||
|   const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes); | ||||
|   const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges); | ||||
|  | ||||
|   useMapHandlers(refn, onSelectionChange); | ||||
|   useUpdateNodes(nodes); | ||||
|   const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers(); | ||||
|   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 => { | ||||
| @@ -179,6 +189,12 @@ const MapComp = ({ | ||||
|     (changes: NodeChange[]) => { | ||||
|       const systemsIdsToRemove: string[] = []; | ||||
|  | ||||
|       // prevents single node deselection on background / same node click | ||||
|       // allows deseletion of all nodes if multiple are currently selected | ||||
|       if (changes.length === 1 && changes[0].type == 'select' && changes[0].selected === false) { | ||||
|         changes[0].selected = getNodes().filter(node => node.selected).length === 1; | ||||
|       } | ||||
|  | ||||
|       const nextChanges = changes.reduce((acc, change) => { | ||||
|         if (change.type !== 'remove') { | ||||
|           return [...acc, change]; | ||||
| @@ -203,7 +219,7 @@ const MapComp = ({ | ||||
|  | ||||
|       onNodesChange(nextChanges); | ||||
|     }, | ||||
|     [getNode, onManualDelete, onNodesChange], | ||||
|     [getNode, getNodes, onManualDelete, onNodesChange], | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
| @@ -216,7 +232,7 @@ const MapComp = ({ | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className={classes.MapRoot}> | ||||
|       <div className={clsx(classes.MapRoot, { [classes.BackgroundAlternateColor]: isSoftBackground })}> | ||||
|         <ReactFlow | ||||
|           nodes={nodes} | ||||
|           edges={edges} | ||||
| @@ -228,7 +244,7 @@ const MapComp = ({ | ||||
|           defaultViewport={getViewPortFromStore()} | ||||
|           edgeTypes={edgeTypes} | ||||
|           nodeTypes={nodeTypes} | ||||
|           connectionMode={ConnectionMode.Loose} | ||||
|           connectionMode={connectionMode} | ||||
|           snapToGrid | ||||
|           nodeDragThreshold={10} | ||||
|           onNodeDragStop={handleDragStop} | ||||
| @@ -236,6 +252,10 @@ 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) => { | ||||
| @@ -257,13 +277,19 @@ 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 | ||||
|           selectionMode={SelectionMode.Partial} | ||||
|         > | ||||
|           {isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />} | ||||
|           <Background /> | ||||
|           {isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />} | ||||
|         </ReactFlow> | ||||
|         {/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}> | ||||
|           Test // DON NOT REMOVE | ||||
|   | ||||
| @@ -9,6 +9,7 @@ export type MapData = MapUnionTypes & { | ||||
|   visibleNodes: Set<string>; | ||||
|   showKSpaceBG: boolean; | ||||
|   isThickConnections: boolean; | ||||
|   linkedSigEveId: string; | ||||
| }; | ||||
|  | ||||
| interface MapProviderProps { | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| @import '@/hooks/Mapper/components/map/styles/eve-common-variables'; | ||||
|  | ||||
| .ConnectionTimeEOL { | ||||
|   background-image: linear-gradient(207deg, transparent, #7452c3e3); | ||||
|   background-image: linear-gradient(207deg, transparent, var(--conn-time-eol)); | ||||
| } | ||||
|  | ||||
| .ConnectionFrigate { | ||||
|   background-image: linear-gradient(207deg, transparent, #325d88); | ||||
|   background-image: linear-gradient(207deg, transparent, var(--conn-frigate)); | ||||
| } | ||||
|  | ||||
| .ConnectionSave { | ||||
|   background-image: linear-gradient(207deg, transparent, rgba(155, 102, 45, 0.85)); | ||||
|   background-image: linear-gradient(207deg, transparent, var(--conn-save)); | ||||
| } | ||||
|  | ||||
| .SelectedItem { | ||||
|   background-color: rgba(98, 98, 98, 0.33); | ||||
|   background-color: var(--selected-item-bg); | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,14 @@ import { Edge } from '@reactflow/core/dist/esm/types/edges'; | ||||
| import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types'; | ||||
| import clsx from 'clsx'; | ||||
| import classes from './ContextMenuConnection.module.scss'; | ||||
| import { MASS_STATE_NAMES, MASS_STATE_NAMES_ORDER } from '@/hooks/Mapper/components/map/constants.ts'; | ||||
| import { | ||||
|   MASS_STATE_NAMES, | ||||
|   MASS_STATE_NAMES_ORDER, | ||||
|   SHIP_SIZES_NAMES, | ||||
|   SHIP_SIZES_NAMES_ORDER, | ||||
|   SHIP_SIZES_NAMES_SHORT, | ||||
|   SHIP_SIZES_SIZE, | ||||
| } from '@/hooks/Mapper/components/map/constants.ts'; | ||||
|  | ||||
| export interface ContextMenuConnectionProps { | ||||
|   contextMenuRef: RefObject<ContextMenu>; | ||||
| @@ -48,10 +55,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({ | ||||
|               icon: PrimeIcons.CLOCK, | ||||
|               command: onChangeTimeState, | ||||
|             }, | ||||
|           ] | ||||
|         : []), | ||||
|       ...(isWormhole | ||||
|         ? [ | ||||
|             { | ||||
|               label: `Frigate`, | ||||
|               className: clsx({ | ||||
| @@ -60,13 +63,9 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({ | ||||
|               icon: PrimeIcons.CLOUD, | ||||
|               command: () => | ||||
|                 onChangeShipSizeStatus( | ||||
|                   edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.normal : ShipSizeStatus.small, | ||||
|                   edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small, | ||||
|                 ), | ||||
|             }, | ||||
|           ] | ||||
|         : []), | ||||
|       ...(isWormhole | ||||
|         ? [ | ||||
|             { | ||||
|               label: `Save mass`, | ||||
|               className: clsx({ | ||||
| @@ -75,19 +74,40 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({ | ||||
|               icon: PrimeIcons.LOCK, | ||||
|               command: () => onToggleMassSave(!edge.data?.locked), | ||||
|             }, | ||||
|           ] | ||||
|         : []), | ||||
|       ...(isWormhole && !isFrigateSize | ||||
|         ? [ | ||||
|             ...(!isFrigateSize | ||||
|               ? [ | ||||
|                   { | ||||
|                     label: `Mass status`, | ||||
|                     icon: PrimeIcons.CHART_PIE, | ||||
|                     items: MASS_STATE_NAMES_ORDER.map(x => ({ | ||||
|                       label: MASS_STATE_NAMES[x], | ||||
|                       className: clsx({ | ||||
|                         [classes.SelectedItem]: edge.data?.mass_status === x, | ||||
|                       }), | ||||
|                       command: () => onChangeMassState(x), | ||||
|                     })), | ||||
|                   }, | ||||
|                 ] | ||||
|               : []), | ||||
|  | ||||
|             { | ||||
|               label: `Mass status`, | ||||
|               icon: PrimeIcons.CHART_PIE, | ||||
|               items: MASS_STATE_NAMES_ORDER.map(x => ({ | ||||
|                 label: MASS_STATE_NAMES[x], | ||||
|               label: `Ship Size`, | ||||
|               icon: PrimeIcons.CLOUD, | ||||
|               items: SHIP_SIZES_NAMES_ORDER.map(x => ({ | ||||
|                 label: ( | ||||
|                   <div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center"> | ||||
|                     <div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div> | ||||
|                     <div>{SHIP_SIZES_NAMES[x]}</div> | ||||
|                     <div></div> | ||||
|                     <div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500"> | ||||
|                       {SHIP_SIZES_SIZE[x]} t. | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 ) as unknown as string, // TODO my lovely kostyl | ||||
|                 className: clsx({ | ||||
|                   [classes.SelectedItem]: edge.data?.mass_status === x, | ||||
|                   [classes.SelectedItem]: edge.data?.ship_size_type === x, | ||||
|                 }), | ||||
|                 command: () => onChangeMassState(x), | ||||
|                 command: () => onChangeShipSizeStatus(x), | ||||
|               })), | ||||
|             }, | ||||
|           ] | ||||
| @@ -98,7 +118,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({ | ||||
|         command: onDeleteConnection, | ||||
|       }, | ||||
|     ]; | ||||
|   }, [edge, onChangeTimeState, onDeleteConnection, onChangeMassState, onChangeShipSizeStatus]); | ||||
|   }, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|   | ||||
| @@ -97,14 +97,16 @@ export const useContextMenuConnectionHandlers = () => { | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     outCommand({ | ||||
|       type: OutCommand.updateConnectionMassStatus, | ||||
|       data: { | ||||
|         source: edge.source, | ||||
|         target: edge.target, | ||||
|         value: MassState.normal, | ||||
|       }, | ||||
|     }); | ||||
|     if (status === ShipSizeStatus.small) { | ||||
|       outCommand({ | ||||
|         type: OutCommand.updateConnectionMassStatus, | ||||
|         data: { | ||||
|           source: edge.source, | ||||
|           target: edge.target, | ||||
|           value: MassState.normal, | ||||
|         }, | ||||
|       }); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const onToggleMassSave = useCallback((locked: boolean) => { | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| import { useReactFlow, XYPosition } from 'reactflow'; | ||||
| import React, { useRef, useState } from 'react'; | ||||
| import React, { useCallback, useRef, useState } from 'react'; | ||||
| import { ContextMenu } from 'primereact/contextmenu'; | ||||
| import { useMapState } from '../../MapProvider.tsx'; | ||||
| import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts'; | ||||
| import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts'; | ||||
|  | ||||
| export const useContextMenuRootHandlers = () => { | ||||
| type UseContextMenuRootHandlers = { | ||||
|   onAddSystem?: OnMapAddSystemCallback; | ||||
| }; | ||||
|  | ||||
| export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => { | ||||
|   const rf = useReactFlow(); | ||||
|   const contextMenuRef = useRef<ContextMenu | null>(null); | ||||
|   const { outCommand } = useMapState(); | ||||
|   const [position, setPosition] = useState<XYPosition | null>(null); | ||||
|  | ||||
|   const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => { | ||||
| @@ -18,14 +20,17 @@ export const useContextMenuRootHandlers = () => { | ||||
|     contextMenuRef.current?.show(e); | ||||
|   }; | ||||
|  | ||||
|   const onAddSystem = () => { | ||||
|     outCommand({ type: OutCommand.manualAddSystem, data: { coordinates: position } }); | ||||
|   }; | ||||
|   const ref = useRef({ onAddSystem, position }); | ||||
|   ref.current = { onAddSystem, position }; | ||||
|  | ||||
|   const onAddSystemCallback = useCallback(() => { | ||||
|     ref.current.onAddSystem?.({ coordinates: position }); | ||||
|   }, [position]); | ||||
|  | ||||
|   return { | ||||
|     handleRootContext, | ||||
|  | ||||
|     contextMenuRef, | ||||
|     onAddSystem, | ||||
|     onAddSystem: onAddSystemCallback, | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -1,23 +1,47 @@ | ||||
| @import "@/hooks/Mapper/components/map/styles/neon-variables"; | ||||
| @import '@/hooks/Mapper/components/map/styles/eve-common-variables'; | ||||
|  | ||||
| .react-flow__edge.selected { | ||||
|   .EdgePathBack { | ||||
|     stroke: $pastel-yellow; | ||||
| .EdgePathBack { | ||||
|   fill: none; | ||||
|   stroke: #80a5c5; | ||||
|   stroke-width: 3px; | ||||
|  | ||||
|   &.TimeCrit { | ||||
|     stroke: #f11ab2; | ||||
|     stroke-width: 4px; | ||||
|   } | ||||
|  | ||||
|   &.Hovered { | ||||
|     stroke: #b5c8d9; | ||||
|  | ||||
|     &.TimeCrit { | ||||
|       stroke: #ef7dce; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.Tick { | ||||
|     stroke-width: 5px; | ||||
|  | ||||
|     &.TimeCrit { | ||||
|       stroke-width: 6px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.Gate { | ||||
|     stroke: #9aff40; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .EdgePathFront { | ||||
|   fill: none; | ||||
|  | ||||
|   stroke: #2c3844; | ||||
|   stroke-width: 2px; | ||||
|  | ||||
|   &.MassVerge:not(&.Frigate) { | ||||
|     stroke: #af2900; | ||||
|     stroke: #af0000; | ||||
|   } | ||||
|  | ||||
|   &.MassHalf:not(&.Frigate) { | ||||
|     stroke: #a85f00; | ||||
|     stroke: #ffd700; | ||||
|   } | ||||
|  | ||||
|   &.Frigate { | ||||
| @@ -54,46 +78,29 @@ | ||||
|   } | ||||
| } | ||||
|  | ||||
| .EdgePathBack { | ||||
|   fill: none; | ||||
|  | ||||
|   stroke: #80a5c5; | ||||
|   stroke-width: 3px; | ||||
|  | ||||
|   &.TimeCrit { | ||||
|     stroke: #f11ab2; | ||||
|     stroke-width: 4px; | ||||
|   } | ||||
|  | ||||
|   &.Hovered { | ||||
|     stroke: #b5c8d9; | ||||
|  | ||||
|     &.TimeCrit { | ||||
|       stroke: #ef7dce; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.Tick { | ||||
|     stroke-width: 5px; | ||||
|  | ||||
|     &.TimeCrit { | ||||
|       stroke-width: 6px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.Gate { | ||||
|     stroke: #9aff40; | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| .ClickPath { | ||||
|   fill: none; | ||||
|   stroke: none; | ||||
|   stroke-width: 8px; | ||||
| } | ||||
|  | ||||
| .LinkLabel{ | ||||
| .Handle { | ||||
|   border: 1px solid var(--pastel-blue); | ||||
|   width: 5px; | ||||
|   height: 5px; | ||||
|   z-index: 1001; | ||||
|  | ||||
|   &.Tick { | ||||
|     width: 7px; | ||||
|     height: 7px; | ||||
|   } | ||||
|  | ||||
|   &.Right { | ||||
|     margin-left: 0px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .LinkLabel { | ||||
|   font-size: 9px; | ||||
|   line-height: 10px; | ||||
|   padding: 2px 4px; | ||||
| @@ -109,22 +116,3 @@ | ||||
|   height: 8px; | ||||
|   font-size: 8px; | ||||
| } | ||||
|  | ||||
| .Handle { | ||||
|   min-width: initial; | ||||
|   min-height: initial; | ||||
|   border: 1px solid #5a7d9a; | ||||
|   width: 5px; | ||||
|   height: 5px; | ||||
|   z-index: 1001; | ||||
|  | ||||
|   &.Tick { | ||||
|     width: 7px; | ||||
|     height: 7px; | ||||
|  | ||||
|     &.Right { | ||||
|       margin-left: 0px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeS | ||||
| import { PrimeIcons } from 'primereact/api'; | ||||
| import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; | ||||
| import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx'; | ||||
| import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts'; | ||||
|  | ||||
| const MAP_TRANSLATES: Record<string, string> = { | ||||
|   [Position.Top]: 'translate(-48%, 0%)', | ||||
| @@ -30,6 +31,14 @@ const MAP_OFFSETS: Record<string, { x: number; y: number }> = { | ||||
|   [Position.Right]: { x: 0, y: 0 }, | ||||
| }; | ||||
|  | ||||
| export const SHIP_SIZES_COLORS = { | ||||
|   [ShipSizeStatus.small]: 'bg-indigo-400', | ||||
|   [ShipSizeStatus.medium]: 'bg-cyan-500', | ||||
|   [ShipSizeStatus.large]: '', | ||||
|   [ShipSizeStatus.freight]: 'bg-lime-400', | ||||
|   [ShipSizeStatus.capital]: 'bg-red-400', | ||||
| }; | ||||
|  | ||||
| export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => { | ||||
|   const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source])); | ||||
|   const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target])); | ||||
| @@ -121,7 +130,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: | ||||
|         /> | ||||
|  | ||||
|         <div | ||||
|           className="absolute flex items-center gap-1" | ||||
|           className="absolute flex items-center gap-1 pointer-events-none" | ||||
|           style={{ | ||||
|             transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, | ||||
|           }} | ||||
| @@ -137,6 +146,19 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: | ||||
|               <span className={clsx(PrimeIcons.LOCK, classes.icon)} /> | ||||
|             </WdTooltipWrapper> | ||||
|           )} | ||||
|  | ||||
|           {isWormhole && data.ship_size_type !== ShipSizeStatus.large && ( | ||||
|             <WdTooltipWrapper | ||||
|               content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]} | ||||
|               className={clsx( | ||||
|                 classes.LinkLabel, | ||||
|                 'pointer-events-auto rounded opacity-100 cursor-auto text-neutral-900 font-bold', | ||||
|                 SHIP_SIZES_COLORS[data.ship_size_type], | ||||
|               )} | ||||
|             > | ||||
|               {SHIP_SIZES_NAMES_SHORT[data.ship_size_type]} | ||||
|             </WdTooltipWrapper> | ||||
|           )} | ||||
|         </div> | ||||
|       </EdgeLabelRenderer> | ||||
|     </> | ||||
|   | ||||
| @@ -1,324 +0,0 @@ | ||||
| import { memo, useMemo } from 'react'; | ||||
| import { Handle, Position, WrapNodeProps } from 'reactflow'; | ||||
| import { MapSolarSystemType } from '../../map.types'; | ||||
| import classes from './SolarSystemNode.module.scss'; | ||||
| import clsx from 'clsx'; | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
|  | ||||
| import { | ||||
|   EFFECT_BACKGROUND_STYLES, | ||||
|   LABELS_INFO, | ||||
|   LABELS_ORDER, | ||||
|   MARKER_BOOKMARK_BG_STYLES, | ||||
|   STATUS_CLASSES, | ||||
| } from '@/hooks/Mapper/components/map/constants.ts'; | ||||
| import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; | ||||
| import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp'; | ||||
| import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature'; | ||||
| import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx'; | ||||
| import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers'; | ||||
| import { sortWHClasses } from '@/hooks/Mapper/helpers'; | ||||
| import { PrimeIcons } from 'primereact/api'; | ||||
| import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts'; | ||||
| import { OutCommand } from '@/hooks/Mapper/types'; | ||||
| import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick.ts'; | ||||
| import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants'; | ||||
|  | ||||
| const SpaceToClass: Record<string, string> = { | ||||
|   [Spaces.Caldari]: classes.Caldaria, | ||||
|   [Spaces.Matar]: classes.Mataria, | ||||
|   [Spaces.Amarr]: classes.Amarria, | ||||
|   [Spaces.Gallente]: classes.Gallente, | ||||
| }; | ||||
|  | ||||
| const sortedLabels = (labels: string[]) => { | ||||
|   if (!labels) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]); | ||||
| }; | ||||
|  | ||||
| export const getActivityType = (count: number) => { | ||||
|   if (count <= 5) { | ||||
|     return 'activityNormal'; | ||||
|   } | ||||
|  | ||||
|   if (count <= 30) { | ||||
|     return 'activityWarn'; | ||||
|   } | ||||
|  | ||||
|   return 'activityDanger'; | ||||
| }; | ||||
|  | ||||
| // eslint-disable-next-line react/display-name | ||||
| export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarSystemType>) => { | ||||
|   const { interfaceSettings } = useMapRootState(); | ||||
|   const { isShowUnsplashedSignatures } = interfaceSettings; | ||||
|  | ||||
|   const { | ||||
|     system_class, | ||||
|     security, | ||||
|     class_title, | ||||
|     solar_system_id, | ||||
|     statics, | ||||
|     effect_name, | ||||
|     region_name, | ||||
|     region_id, | ||||
|     is_shattered, | ||||
|     solar_system_name, | ||||
|   } = data.system_static_info; | ||||
|  | ||||
|   const signatures = data.system_signatures; | ||||
|  | ||||
|   const { locked, name, tag, status, labels, id } = data || {}; | ||||
|  | ||||
|   const customName = solar_system_name !== name ? name : undefined; | ||||
|  | ||||
|   const { | ||||
|     data: { | ||||
|       characters, | ||||
|       presentCharacters, | ||||
|       wormholesData, | ||||
|       hubs, | ||||
|       kills, | ||||
|       userCharacters, | ||||
|       isConnecting, | ||||
|       hoverNodeId, | ||||
|       visibleNodes, | ||||
|       showKSpaceBG, | ||||
|       isThickConnections, | ||||
|     }, | ||||
|     outCommand, | ||||
|   } = useMapState(); | ||||
|  | ||||
|   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); | ||||
|     // eslint-disable-next-line | ||||
|   }, [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 lebM = useMemo(() => new LabelsManager(labels ?? ''), [labels]); | ||||
|   const labelsInfo = useMemo(() => sortedLabels(lebM.list), [lebM]); | ||||
|   const labelCustom = useMemo(() => lebM.customLabel, [lebM]); | ||||
|  | ||||
|   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 [unsplashedLeft, unsplashedRight] = useMemo(() => { | ||||
|     if (!isShowUnsplashedSignatures) { | ||||
|       return [[], []]; | ||||
|     } | ||||
|  | ||||
|     return prepareUnsplashedChunks( | ||||
|       signatures | ||||
|         .filter(s => s.group === 'Wormhole' && !s.linked_system) | ||||
|         .map(s => ({ | ||||
|           eve_id: s.eve_id, | ||||
|           type: s.type, | ||||
|           custom_info: s.custom_info, | ||||
|         })), | ||||
|     ); | ||||
|   }, [isShowUnsplashedSignatures, signatures]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {visible && ( | ||||
|         <div className={classes.Bookmarks}> | ||||
|           {labelCustom !== '' && ( | ||||
|             <div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}> | ||||
|               <span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {is_shattered && ( | ||||
|             <div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}> | ||||
|               <span className={clsx('pi pi-chart-pie', classes.icon)} /> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {killsCount && ( | ||||
|             <div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[getActivityType(killsCount)])}> | ||||
|               <div className={clsx(classes.BookmarkWithIcon)}> | ||||
|                 <span className={clsx(PrimeIcons.BOLT, classes.icon)} /> | ||||
|                 <span className={clsx(classes.text)}>{killsCount}</span> | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {labelsInfo.map(x => ( | ||||
|             <div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}> | ||||
|               {x.shortName} | ||||
|             </div> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <div | ||||
|         className={clsx(classes.RootCustomNode, regionClass, classes[STATUS_CLASSES[status]], { | ||||
|           [classes.selected]: selected, | ||||
|         })} | ||||
|       > | ||||
|         {visible && ( | ||||
|           <> | ||||
|             <div className={classes.HeadRow}> | ||||
|               <div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}> | ||||
|                 {class_title ?? '-'} | ||||
|               </div> | ||||
|               {tag != null && tag !== '' && ( | ||||
|                 <div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div> | ||||
|               )} | ||||
|               <div | ||||
|                 className={clsx( | ||||
|                   classes.classSystemName, | ||||
|                   '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans', | ||||
|                 )} | ||||
|               > | ||||
|                 {solar_system_name} | ||||
|               </div> | ||||
|  | ||||
|               {isWormhole && ( | ||||
|                 <div className={classes.statics}> | ||||
|                   {sortedStatics.map(x => ( | ||||
|                     <WormholeClassComp key={x} id={x} /> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {effect_name !== null && isWormhole && ( | ||||
|                 <div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effect_name])}></div> | ||||
|               )} | ||||
|             </div> | ||||
|  | ||||
|             <div className={clsx(classes.BottomRow, 'flex items-center justify-between')}> | ||||
|               {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"> | ||||
|                   {customName} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {!isWormhole && !customName && ( | ||||
|                 <div | ||||
|                   className={clsx( | ||||
|                     '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5', | ||||
|                   )} | ||||
|                 > | ||||
|                   {region_name} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {isWormhole && !customName && <div />} | ||||
|  | ||||
|               <div className="flex items-center justify-end"> | ||||
|                 <div className="flex gap-1 items-center"> | ||||
|                   {locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }}></i>} | ||||
|  | ||||
|                   {hubs.includes(solar_system_id.toString()) && ( | ||||
|                     <i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }}></i> | ||||
|                   )} | ||||
|  | ||||
|                   {charactersInSystem.length > 0 && ( | ||||
|                     <div className={clsx(classes.localCounter, { ['text-amber-300']: hasUserCharacters })}> | ||||
|                       <i className="pi pi-users" style={{ fontSize: '0.50rem' }}></i> | ||||
|                       <span className="font-sans">{charactersInSystem.length}</span> | ||||
|                     </div> | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
|  | ||||
|       {visible && isShowUnsplashedSignatures && ( | ||||
|         <div className={classes.Unsplashed}> | ||||
|           {unsplashedLeft.map(x => ( | ||||
|             <UnsplashedSignature key={x.sig_id} signature={x} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       {visible && isShowUnsplashedSignatures && ( | ||||
|         <div className={clsx([classes.Unsplashed, classes['Unsplashed--right']])}> | ||||
|           {unsplashedRight.map(x => ( | ||||
|             <UnsplashedSignature key={x.sig_id} signature={x} /> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <div onMouseDownCapture={dbClick} className={classes.Handlers}> | ||||
|         <Handle | ||||
|           type="source" | ||||
|           className={clsx(classes.Handle, classes.HandleTop, { | ||||
|             [classes.selected]: selected, | ||||
|             [classes.Tick]: isThickConnections, | ||||
|           })} | ||||
|           style={{ visibility: showHandlers ? 'visible' : 'hidden' }} | ||||
|           position={Position.Top} | ||||
|           id="a" | ||||
|         /> | ||||
|         <Handle | ||||
|           type="source" | ||||
|           className={clsx(classes.Handle, classes.HandleRight, { | ||||
|             [classes.selected]: selected, | ||||
|             [classes.Tick]: isThickConnections, | ||||
|           })} | ||||
|           style={{ visibility: showHandlers ? 'visible' : 'hidden' }} | ||||
|           position={Position.Right} | ||||
|           id="b" | ||||
|         /> | ||||
|         <Handle | ||||
|           type="source" | ||||
|           className={clsx(classes.Handle, classes.HandleBottom, { | ||||
|             [classes.selected]: selected, | ||||
|             [classes.Tick]: isThickConnections, | ||||
|           })} | ||||
|           style={{ visibility: showHandlers ? 'visible' : 'hidden' }} | ||||
|           position={Position.Bottom} | ||||
|           id="c" | ||||
|         /> | ||||
|         <Handle | ||||
|           type="source" | ||||
|           className={clsx(classes.Handle, classes.HandleLeft, { | ||||
|             [classes.selected]: selected, | ||||
|             [classes.Tick]: isThickConnections, | ||||
|           })} | ||||
|           style={{ visibility: showHandlers ? 'visible' : 'hidden' }} | ||||
|           position={Position.Left} | ||||
|           id="d" | ||||
|         /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }); | ||||
| @@ -6,7 +6,14 @@ $pastel-green: #88b04b; | ||||
| $pastel-yellow: #ffdd59; | ||||
| $dark-bg: #2d2d2d; | ||||
| $text-color: #ffffff; | ||||
| $tooltip-bg: #202020; // Dark background for tooltips | ||||
| $tooltip-bg: #202020; | ||||
| 
 | ||||
| $node-bg-color: #202020; | ||||
| $node-soft-bg-color: #202020; | ||||
| $text-color: #ffffff; | ||||
| $tag-color:  #38BDF8; | ||||
| $region-name: #D6D3D1; | ||||
| $custom-name: #93C5FD; | ||||
| 
 | ||||
| .RootCustomNode { | ||||
|   display: flex; | ||||
| @@ -85,7 +92,7 @@ $tooltip-bg: #202020; // Dark background for tooltips | ||||
|     box-shadow: 0 0 10px #9a1af1c2; | ||||
|   } | ||||
| 
 | ||||
|   .tooltip { | ||||
|   &.tooltip { | ||||
|     background-color: $tooltip-bg; | ||||
|     color: $text-color; | ||||
|     padding: 5px 10px; | ||||
| @@ -94,42 +101,59 @@ $tooltip-bg: #202020; // Dark background for tooltips | ||||
|   } | ||||
| 
 | ||||
|   &.eve-system-status-home { | ||||
|     border: 1px solid darken($eve-solar-system-status-color-home, 30%); | ||||
|     background-image: linear-gradient(275deg, $eve-solar-system-status-friendly, transparent); | ||||
| 
 | ||||
|     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: $eve-solar-system-status-color-home; | ||||
|       border-color: var(--eve-solar-system-status-color-home); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.eve-system-status-friendly { | ||||
|     border: 1px solid darken($eve-solar-system-status-color-friendly, 20%); | ||||
|     background-image: linear-gradient(275deg, darken($eve-solar-system-status-friendly, 30%), transparent); | ||||
| 
 | ||||
|     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: darken($eve-solar-system-status-color-friendly, 5%); | ||||
|       border-color: var(--eve-solar-system-status-color-friendly-dark5); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.eve-system-status-lookingFor { | ||||
|     border: 1px solid darken($eve-solar-system-status-color-lookingFor, 15%); | ||||
|     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, $eve-solar-system-status-warning, transparent); | ||||
|     background-image: linear-gradient( | ||||
|       275deg, | ||||
|       var(--eve-solar-system-status-warning), | ||||
|       transparent | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   &.eve-system-status-dangerous { | ||||
|     background-image: linear-gradient(275deg, $eve-solar-system-status-dangerous, transparent); | ||||
|     background-image: linear-gradient( | ||||
|       275deg, | ||||
|       var(--eve-solar-system-status-dangerous), | ||||
|       transparent | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   &.eve-system-status-target { | ||||
|     background-image: linear-gradient(275deg, $eve-solar-system-status-target, transparent); | ||||
|     background-image: linear-gradient( | ||||
|       275deg, | ||||
|       var(--eve-solar-system-status-target), | ||||
|       transparent | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @@ -242,10 +266,10 @@ $tooltip-bg: #202020; // Dark background for tooltips | ||||
| 
 | ||||
|   .TagTitle { | ||||
|     font-size: 11px; | ||||
|     font-weight: bold; | ||||
|     font-weight: medium; | ||||
|     text-shadow: 0 0 2px rgba(231, 146, 52, 0.73); | ||||
| 
 | ||||
|     color: #ffb01d; | ||||
|     color: var(--rf-tag-color, #38BDF8); | ||||
|   } | ||||
| 
 | ||||
|   /* Firefox kostyl */ | ||||
| @@ -0,0 +1,209 @@ | ||||
| import { memo } from 'react'; | ||||
| import { MapSolarSystemType } from '../../map.types'; | ||||
| import { Handle, Position, NodeProps } 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, | ||||
|   EFFECT_BACKGROUND_STYLES, | ||||
| } from '@/hooks/Mapper/components/map/constants'; | ||||
| 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); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {nodeVars.visible && ( | ||||
|         <div className={classes.Bookmarks}> | ||||
|           {nodeVars.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> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {nodeVars.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!])}> | ||||
|               <div className={clsx(classes.BookmarkWithIcon)}> | ||||
|                 <span className={clsx(PrimeIcons.BOLT, classes.icon)} /> | ||||
|                 <span className={clsx(classes.text)}>{nodeVars.killsCount}</span> | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {nodeVars.labelsInfo.map(x => ( | ||||
|             <div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}> | ||||
|               {x.shortName} | ||||
|             </div> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <div | ||||
|         className={clsx( | ||||
|           classes.RootCustomNode, | ||||
|           nodeVars.regionClass && classes[nodeVars.regionClass], | ||||
|           classes[STATUS_CLASSES[nodeVars.status]], | ||||
|           { | ||||
|             [classes.selected]: nodeVars.selected, | ||||
|           }, | ||||
|         )} | ||||
|       > | ||||
|         {nodeVars.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> | ||||
|  | ||||
|               {nodeVars.tag != null && nodeVars.tag !== '' && ( | ||||
|                 <div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div> | ||||
|               )} | ||||
|  | ||||
|               <div | ||||
|                 className={clsx( | ||||
|                   classes.classSystemName, | ||||
|                   '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans', | ||||
|                 )} | ||||
|               > | ||||
|                 {nodeVars.systemName} | ||||
|               </div> | ||||
|  | ||||
|               {nodeVars.isWormhole && ( | ||||
|                 <div className={classes.statics}> | ||||
|                   {nodeVars.sortedStatics.map(whClass => ( | ||||
|                     <WormholeClassComp key={whClass} id={whClass} /> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {nodeVars.effectName !== null && nodeVars.isWormhole && ( | ||||
|                 <div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} /> | ||||
|               )} | ||||
|             </div> | ||||
|  | ||||
|             <div className={clsx(classes.BottomRow, 'flex items-center justify-between')}> | ||||
|               {nodeVars.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} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {!nodeVars.isWormhole && !nodeVars.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} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {nodeVars.isWormhole && !nodeVars.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' }} /> | ||||
|                   )} | ||||
|  | ||||
|                   {nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && ( | ||||
|                     <i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} /> | ||||
|                   )} | ||||
|  | ||||
|                   {nodeVars.charactersInSystem.length > 0 && ( | ||||
|                     <div | ||||
|                       className={clsx(classes.localCounter, { | ||||
|                         ['text-amber-300']: nodeVars.hasUserCharacters, | ||||
|                       })} | ||||
|                     > | ||||
|                       <i className="pi pi-users" style={{ fontSize: '0.50rem' }} /> | ||||
|                       <span className="font-sans">{nodeVars.charactersInSystem.length}</span> | ||||
|                     </div> | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
|  | ||||
|       {nodeVars.visible && ( | ||||
|         <> | ||||
|           {nodeVars.unsplashedLeft.length > 0 && ( | ||||
|             <div className={classes.Unsplashed}> | ||||
|               {nodeVars.unsplashedLeft.map(sig => ( | ||||
|                 <UnsplashedSignature key={sig.sig_id} signature={sig} /> | ||||
|               ))} | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {nodeVars.unsplashedRight.length > 0 && ( | ||||
|             <div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}> | ||||
|               {nodeVars.unsplashedRight.map(sig => ( | ||||
|                 <UnsplashedSignature key={sig.sig_id} signature={sig} /> | ||||
|               ))} | ||||
|             </div> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
|       <div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}> | ||||
|         <Handle | ||||
|           type="source" | ||||
|           className={clsx(classes.Handle, classes.HandleTop, { | ||||
|             [classes.selected]: nodeVars.selected, | ||||
|             [classes.Tick]: nodeVars.isThickConnections, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.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, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.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, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.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, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }} | ||||
|           position={Position.Left} | ||||
|           id="d" | ||||
|         /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| SolarSystemNodeDefault.displayName = 'SolarSystemNodeDefault'; | ||||
| @@ -0,0 +1,91 @@ | ||||
| @import './SolarSystemNodeDefault.module.scss'; | ||||
|  | ||||
| /* --------------------------- | ||||
|    Only override what's different | ||||
| --------------------------- */ | ||||
|  | ||||
| /* 1) .RootCustomNode: | ||||
|    - new background-color using CSS var | ||||
|    - plus color, font-family, and font-weight */ | ||||
| .RootCustomNode { | ||||
|   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; | ||||
| } | ||||
|  | ||||
| /* 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; | ||||
|   } | ||||
|  | ||||
|   @-moz-document url-prefix() { | ||||
|     .classSystemName { | ||||
|       font-family: var(--rf-node-font-family, inherit) !important; | ||||
|       font-weight: var(--rf-node-font-weight, inherit) !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .classSystemName { | ||||
|     font-family: var(--rf-node-font-family, inherit) !important; | ||||
|     font-weight: var(--rf-node-font-weight, inherit) !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /* 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); | ||||
|   } | ||||
|  | ||||
|   .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; | ||||
|   } | ||||
|  | ||||
|   .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; | ||||
|   } | ||||
|  | ||||
|   .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; | ||||
|     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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,219 @@ | ||||
| import { memo } from 'react'; | ||||
| import { MapSolarSystemType } from '../../map.types'; | ||||
| import { Handle, Position, NodeProps } 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, | ||||
|   EFFECT_BACKGROUND_STYLES, | ||||
| } from '@/hooks/Mapper/components/map/constants'; | ||||
| 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); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {nodeVars.visible && ( | ||||
|         <div className={classes.Bookmarks}> | ||||
|           {nodeVars.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> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {nodeVars.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!])}> | ||||
|               <div className={clsx(classes.BookmarkWithIcon)}> | ||||
|                 <span className={clsx(PrimeIcons.BOLT, classes.icon)} /> | ||||
|                 <span className={clsx(classes.text)}>{nodeVars.killsCount}</span> | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {nodeVars.labelsInfo.map(x => ( | ||||
|             <div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}> | ||||
|               {x.shortName} | ||||
|             </div> | ||||
|           ))} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <div | ||||
|         className={clsx( | ||||
|           classes.RootCustomNode, | ||||
|           nodeVars.regionClass && classes[nodeVars.regionClass], | ||||
|           classes[STATUS_CLASSES[nodeVars.status]], | ||||
|           { | ||||
|             [classes.selected]: nodeVars.selected, | ||||
|           }, | ||||
|         )} | ||||
|       > | ||||
|         {nodeVars.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> | ||||
|  | ||||
|               {nodeVars.tag != null && nodeVars.tag !== '' && ( | ||||
|                 <div className={clsx(classes.TagTitle)}>{nodeVars.tag}</div> | ||||
|               )} | ||||
|  | ||||
|               <div | ||||
|                 className={clsx( | ||||
|                   classes.classSystemName, | ||||
|                   '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap', | ||||
|                 )} | ||||
|               > | ||||
|                 {nodeVars.systemName} | ||||
|               </div> | ||||
|  | ||||
|               {nodeVars.isWormhole && ( | ||||
|                 <div className={classes.statics}> | ||||
|                   {nodeVars.sortedStatics.map(whClass => ( | ||||
|                     <WormholeClassComp key={whClass} id={whClass} /> | ||||
|                   ))} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {nodeVars.effectName !== null && nodeVars.isWormhole && ( | ||||
|                 <div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} /> | ||||
|               )} | ||||
|             </div> | ||||
|  | ||||
|             <div className={clsx(classes.BottomRow, 'flex items-center justify-between')}> | ||||
|               {nodeVars.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} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {!nodeVars.isWormhole && !nodeVars.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} | ||||
|                 </div> | ||||
|               )} | ||||
|  | ||||
|               {nodeVars.isWormhole && !nodeVars.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' }} /> | ||||
|                   )} | ||||
|  | ||||
|                   {nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && ( | ||||
|                     <i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} /> | ||||
|                   )} | ||||
|  | ||||
|                   {nodeVars.charactersInSystem.length > 0 && ( | ||||
|                     <div | ||||
|                       className={clsx(classes.localCounter, { | ||||
|                         [classes.hasUserCharacters]: nodeVars.hasUserCharacters, | ||||
|                       })} | ||||
|                     > | ||||
|                       <i className="pi pi-users" style={{ fontSize: '0.50rem' }} /> | ||||
|                       <span className="font-sans">{nodeVars.charactersInSystem.length}</span> | ||||
|                     </div> | ||||
|                   )} | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
|  | ||||
|       {nodeVars.visible && ( | ||||
|         <> | ||||
|           {nodeVars.unsplashedLeft.length > 0 && ( | ||||
|             <div className={classes.Unsplashed}> | ||||
|               {nodeVars.unsplashedLeft.map(sig => ( | ||||
|                 <UnsplashedSignature key={sig.sig_id} signature={sig} /> | ||||
|               ))} | ||||
|             </div> | ||||
|           )} | ||||
|  | ||||
|           {nodeVars.unsplashedRight.length > 0 && ( | ||||
|             <div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}> | ||||
|               {nodeVars.unsplashedRight.map(sig => ( | ||||
|                 <UnsplashedSignature key={sig.sig_id} signature={sig} /> | ||||
|               ))} | ||||
|             </div> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
|       <div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}> | ||||
|         <Handle | ||||
|           type="source" | ||||
|           className={clsx(classes.Handle, classes.HandleTop, { | ||||
|             [classes.selected]: nodeVars.selected, | ||||
|             [classes.Tick]: nodeVars.isThickConnections, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.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, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.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, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.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, | ||||
|           })} | ||||
|           style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }} | ||||
|           position={Position.Left} | ||||
|           id="d" | ||||
|         /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| SolarSystemNodeTheme.displayName = 'SolarSystemNodeTheme'; | ||||
| @@ -1 +1,2 @@ | ||||
| export * from './SolarSystemNode'; | ||||
| export * from './SolarSystemNodeDefault'; | ||||
| export * from './SolarSystemNodeTheme'; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|     width: 13px; | ||||
|     height: 4px; | ||||
|     border-radius: 4px; | ||||
|     color: #ffffff; | ||||
|     color: var(--text-color); | ||||
|     font-size: 8px; | ||||
|     text-align: center; | ||||
|     font-weight: bolder; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { ConnectionType, MassState } from '@/hooks/Mapper/types'; | ||||
| import { ConnectionType, MassState, ShipSizeStatus } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| export enum SOLAR_SYSTEM_CLASS_IDS { | ||||
|   ccp1 = -1, | ||||
| @@ -727,16 +727,41 @@ export const MASS_STATE_NAMES = { | ||||
|   [MassState.verge]: 'Verge of collapse', | ||||
| }; | ||||
|  | ||||
| // export const SHIP_SIZES_NAMES_ORDER = [ | ||||
| //   ShipSizeStatus.small, | ||||
| //   ShipSizeStatus.normal, | ||||
| //   // ShipSizeStatus.large, | ||||
| //   // ShipSizeStatus.capital, | ||||
| // ]; | ||||
| // | ||||
| // export const SHIP_SIZES_NAMES = { | ||||
| //   [ShipSizeStatus.small]: 'Frigate', | ||||
| //   [ShipSizeStatus.normal]: 'Normal', | ||||
| //   // [ShipSizeStatus.large]: 'Normal', | ||||
| //   // [ShipSizeStatus.capital]: 'Normal', | ||||
| // }; | ||||
| export const SHIP_SIZES_NAMES_ORDER = [ | ||||
|   ShipSizeStatus.small, | ||||
|   ShipSizeStatus.medium, | ||||
|   ShipSizeStatus.large, | ||||
|   ShipSizeStatus.freight, | ||||
|   ShipSizeStatus.capital, | ||||
| ]; | ||||
|  | ||||
| export const SHIP_SIZES_NAMES = { | ||||
|   [ShipSizeStatus.small]: 'Frigate', | ||||
|   [ShipSizeStatus.medium]: 'Medium', | ||||
|   [ShipSizeStatus.large]: 'Normal', | ||||
|   [ShipSizeStatus.freight]: 'Huge', | ||||
|   [ShipSizeStatus.capital]: 'Capital', | ||||
| }; | ||||
| export const SHIP_SIZES_SIZE = { | ||||
|   [ShipSizeStatus.small]: '5K', | ||||
|   [ShipSizeStatus.medium]: '62K', | ||||
|   [ShipSizeStatus.large]: '375K', | ||||
|   [ShipSizeStatus.freight]: '1M', | ||||
|   [ShipSizeStatus.capital]: '2M', | ||||
| }; | ||||
|  | ||||
| export const SHIP_SIZES_DESCRIPTION = { | ||||
|   [ShipSizeStatus.small]: 'Frigate wormhole - up to Destroyer | 5K t.', | ||||
|   [ShipSizeStatus.medium]: 'Cruise wormhole - up to Battlecruiser | 62K t.', | ||||
|   [ShipSizeStatus.large]: 'Large wormhole - up to Battleship | 375K t.', | ||||
|   [ShipSizeStatus.freight]: 'Huge wormhole - up to Freighter | 1M t.', | ||||
|   [ShipSizeStatus.capital]: 'Capital wormhole - up to Capital | 2M t.', | ||||
| }; | ||||
|  | ||||
| export const SHIP_SIZES_NAMES_SHORT = { | ||||
|   [ShipSizeStatus.small]: 'S', | ||||
|   [ShipSizeStatus.medium]: 'M', | ||||
|   [ShipSizeStatus.large]: 'L', | ||||
|   [ShipSizeStatus.freight]: 'H', | ||||
|   [ShipSizeStatus.capital]: 'XL', | ||||
| }; | ||||
|   | ||||
| @@ -0,0 +1,32 @@ | ||||
| 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 }); | ||||
|       update({ systems: out }, true); | ||||
|     }, | ||||
|     [rf, update], | ||||
|   ); | ||||
|   | ||||
| @@ -0,0 +1,44 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { BackgroundVariant } from 'reactflow'; | ||||
|  | ||||
| export function useBackgroundVars(themeName?: string) { | ||||
|   const [variant, setVariant] = useState<BackgroundVariant>(BackgroundVariant.Dots); | ||||
|   const [gap, setGap] = useState<number>(16); | ||||
|   const [size, setSize] = useState<number>(1); | ||||
|   const [color, setColor] = useState('#81818b'); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     // match any element whose entire `class` attribute ends with "-theme" | ||||
|     let themeEl = document.querySelector('[class$="-theme"]'); | ||||
|  | ||||
|     // If none is found, fall back to the <html> element | ||||
|     if (!themeEl) { | ||||
|       themeEl = document.documentElement; | ||||
|     } | ||||
|  | ||||
|     const style = getComputedStyle(themeEl as HTMLElement); | ||||
|  | ||||
|     const rawVariant = style.getPropertyValue('--rf-bg-variant').replace(/['"]/g, '').trim().toLowerCase(); | ||||
|     let finalVariant: BackgroundVariant = BackgroundVariant.Dots; | ||||
|  | ||||
|     if (rawVariant === 'lines') { | ||||
|       finalVariant = BackgroundVariant.Lines; | ||||
|     } else if (rawVariant === 'cross') { | ||||
|       finalVariant = BackgroundVariant.Cross; | ||||
|     } | ||||
|  | ||||
|     const cssVarGap = style.getPropertyValue('--rf-bg-gap'); | ||||
|     const cssVarSize = style.getPropertyValue('--rf-bg-size'); | ||||
|     const cssColor = style.getPropertyValue('--rf-bg-pattern-color'); | ||||
|  | ||||
|     const gapNum = parseInt(cssVarGap, 10) || 16; | ||||
|     const sizeNum = parseInt(cssVarSize, 10) || 1; | ||||
|  | ||||
|     setVariant(finalVariant); | ||||
|     setGap(gapNum); | ||||
|     setSize(sizeNum); | ||||
|     setColor(cssColor); | ||||
|   }, [themeName]); | ||||
|  | ||||
|   return { variant, gap, size, color }; | ||||
| } | ||||
| @@ -0,0 +1,254 @@ | ||||
| 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'; | ||||
| import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick'; | ||||
| import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants'; | ||||
| import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace'; | ||||
| 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 { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants'; | ||||
|  | ||||
| function getActivityType(count: number) { | ||||
|   if (count <= 5) return 'activityNormal'; | ||||
|   if (count <= 30) return 'activityWarn'; | ||||
|   return 'activityDanger'; | ||||
| } | ||||
|  | ||||
| const SpaceToClass: Record<string, string> = { | ||||
|   [Spaces.Caldari]: 'Caldaria', | ||||
|   [Spaces.Matar]: 'Mataria', | ||||
|   [Spaces.Amarr]: 'Amarria', | ||||
|   [Spaces.Gallente]: 'Gallente', | ||||
| }; | ||||
|  | ||||
| function sortedLabels(labels: string[]) { | ||||
|   if (!labels) return []; | ||||
|   return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]); | ||||
| } | ||||
|  | ||||
| export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) { | ||||
|   const { id, data, selected } = props; | ||||
|   const { | ||||
|     system_static_info, | ||||
|     system_signatures, | ||||
|     locked, | ||||
|     name, | ||||
|     tag, | ||||
|     status, | ||||
|     labels, | ||||
|     temporary_name, | ||||
|     linked_sig_eve_id: linkedSigEveId = '', | ||||
|   } = data; | ||||
|  | ||||
|   const { | ||||
|     system_class, | ||||
|     security, | ||||
|     class_title, | ||||
|     solar_system_id, | ||||
|     statics, | ||||
|     effect_name, | ||||
|     region_name, | ||||
|     region_id, | ||||
|     is_shattered, | ||||
|     solar_system_name, | ||||
|   } = system_static_info; | ||||
|  | ||||
|   const { | ||||
|     interfaceSettings, | ||||
|     data: { systemSignatures: mapSystemSignatures }, | ||||
|   } = 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: { | ||||
|       characters, | ||||
|       presentCharacters, | ||||
|       wormholesData, | ||||
|       hubs, | ||||
|       kills, | ||||
|       userCharacters, | ||||
|       isConnecting, | ||||
|       hoverNodeId, | ||||
|       visibleNodes, | ||||
|       showKSpaceBG, | ||||
|       isThickConnections, | ||||
|     }, | ||||
|     outCommand, | ||||
|   } = useMapState(); | ||||
|  | ||||
|   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 | ||||
|   }, [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(() => { | ||||
|     if (isShowLinkedSigId && linkedSigPrefix) { | ||||
|       return labelsManager.customLabel ? `${linkedSigPrefix}・${labelsManager.customLabel}` : linkedSigPrefix; | ||||
|     } | ||||
|     return labelsManager.customLabel; | ||||
|   }, [linkedSigPrefix, isShowLinkedSigId, labelsManager]); | ||||
|  | ||||
|   const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]); | ||||
|   const killsActivityType = killsCount ? getActivityType(killsCount) : null; | ||||
|  | ||||
|   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 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 [unsplashedLeft, unsplashedRight] = useMemo(() => { | ||||
|     if (!isShowUnsplashedSignatures) { | ||||
|       return [[], []]; | ||||
|     } | ||||
|     return prepareUnsplashedChunks( | ||||
|       systemSignatures | ||||
|         .filter(s => s.group === 'Wormhole' && !s.linked_system) | ||||
|         .map(s => ({ | ||||
|           eve_id: s.eve_id, | ||||
|           type: s.type, | ||||
|           custom_info: s.custom_info, | ||||
|         })), | ||||
|     ); | ||||
|   }, [isShowUnsplashedSignatures, systemSignatures]); | ||||
|  | ||||
|   const nodeVars = { | ||||
|     id, | ||||
|     selected, | ||||
|  | ||||
|     visible, | ||||
|     isWormhole, | ||||
|     classTitleColor, | ||||
|     killsCount, | ||||
|     killsActivityType, | ||||
|     hasUserCharacters, | ||||
|     showHandlers, | ||||
|     regionClass, | ||||
|     systemName, | ||||
|     customName, | ||||
|     labelCustom, | ||||
|     isShattered: is_shattered, | ||||
|     tag, | ||||
|     status, | ||||
|     labelsInfo, | ||||
|     dbClick, | ||||
|     sortedStatics, | ||||
|     effectName: effect_name, | ||||
|     regionName: region_name, | ||||
|     solarSystemId: solar_system_id, | ||||
|     solarSystemName: solar_system_name, | ||||
|     locked, | ||||
|     hubs, | ||||
|     name: name, | ||||
|     isConnecting, | ||||
|     hoverNodeId, | ||||
|     charactersInSystem, | ||||
|     unsplashedLeft, | ||||
|     unsplashedRight, | ||||
|     isThickConnections, | ||||
|     classTitle: class_title, | ||||
|     temporaryName: temporary_name, | ||||
|   }; | ||||
|  | ||||
|   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; | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { SolarSystemRawType } from '@/hooks/Mapper/types/system'; | ||||
| import { SolarSystemConnection } from '@/hooks/Mapper/types'; | ||||
| import { XYPosition } from 'reactflow'; | ||||
|  | ||||
| export type MapSolarSystemType = Omit<SolarSystemRawType, 'position'>; | ||||
|  | ||||
| @@ -7,3 +8,5 @@ export type OnMapSelectionChange = (event: { | ||||
|   systems: string[]; | ||||
|   connections: Pick<SolarSystemConnection, 'source' | 'target'>[]; | ||||
| }) => void; | ||||
|  | ||||
| export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void; | ||||
|   | ||||
| @@ -0,0 +1,31 @@ | ||||
| @import './eve-common-variables'; | ||||
| @import './eve-common'; | ||||
|  | ||||
| .default-theme { | ||||
|   --rf-bg-color: #0C0A09; | ||||
|   --rf-soft-bg-color: #171717; | ||||
|  | ||||
|   --rf-node-bg-color: #202020; | ||||
|   --rf-node-soft-bg-color: #202020; | ||||
|   --rf-text-color: #ffffff; | ||||
|   --rf-tag-color:  #38BDF8; | ||||
|   --rf-region-name: #D6D3D1; | ||||
|   --rf-custom-name: #93C5FD; | ||||
|  | ||||
|  | ||||
|   --rf-bg-variant: "dots"; | ||||
|   --rf-bg-gap: 15; | ||||
|   --rf-bg-size: 1; | ||||
|   --rf-bg-pattern-color: #81818a; | ||||
|  | ||||
|   --pastel-blue: #5a7d9a; | ||||
|   --pastel-pink: #d291bc; | ||||
|   --pastel-green: #88b04b; | ||||
|   --pastel-yellow: #ffdd59; | ||||
|  | ||||
|   --dark-bg: #2d2d2d; | ||||
|   --text-color: #ffffff; | ||||
|   --tooltip-bg: #202020; | ||||
|  | ||||
|   --window-corner: #72716f; | ||||
| } | ||||
| @@ -1,78 +1,121 @@ | ||||
| $eve-link-color-default: #333; | ||||
| $eve-link-color-top-mass-0: #333; | ||||
| $eve-link-color-top-mass-1: #5a4520; | ||||
| $eve-link-color-top-mass-2: #672c2c; | ||||
| $eve-link-color-middle-mass-0: #333; | ||||
| $eve-link-color-middle-mass-1: #333; | ||||
| $eve-link-color-middle-mass-2: #333; | ||||
| $eve-link-color-middle-time-0: #5c5c5c; | ||||
| $eve-link-color-middle-time-1: #ff00cd; | ||||
| $eve-link-color-middle-time-1-border: #99f3ff; | ||||
|  | ||||
| $eve-link-color-top-mass-1-time-1: #796300; | ||||
| $eve-link-color-top-mass-2-time-1: #8c1717; | ||||
| $eve-link-color-temp: orange; | ||||
| $friendlyBase: #3bbd39;               | ||||
| $friendlyAlpha: #3bbd3952;            | ||||
| $friendlyDark20: darken($friendlyBase, 20%); | ||||
| $friendlyDark30: darken($friendlyBase, 30%); | ||||
| $friendlyDark5:  darken($friendlyBase, 5%); | ||||
|  | ||||
| $eve-effect-pulsar: #40aef5; | ||||
| $eve-effect-magnetar: #f058f8; | ||||
| $eve-effect-wolfRayet: #ef7843; | ||||
| $eve-effect-blackHole: #1b1b1b; | ||||
| $eve-effect-cataclysmicVariable: #ffea90; | ||||
| $eve-effect-redGiant: #fd3c3c; | ||||
| $eve-effect-dazhLiminalityLocus: #ff6464; | ||||
| $eve-effect-imperialStellarObservatory: #6991ce; | ||||
| $eve-effect-stateStellarObservatory: #6991ce; | ||||
| $eve-effect-republicStellarObservatory: #6991ce; | ||||
| $eve-effect-federalStellarObservatory: #6991ce; | ||||
| $lookingForBase: #43c2fd;             | ||||
| $lookingForAlpha: rgba(67, 176, 253, 0.48); | ||||
| $lookingForDark15: darken($lookingForBase, 15%); | ||||
|  | ||||
| $eve-wh-type-color-high: #5dffd2; | ||||
| $eve-wh-type-color-low: #f79400; | ||||
| $eve-wh-type-color-null: #fc3c3c; | ||||
| $eve-wh-type-color-c1: #69bfce; | ||||
| $eve-wh-type-color-c2: #6991ce; | ||||
| $eve-wh-type-color-c3: #a8cb70; | ||||
| $eve-wh-type-color-c4: #e39c68; | ||||
| $eve-wh-type-color-c5: #de8686; | ||||
| $eve-wh-type-color-c6: #e76363; | ||||
| $eve-wh-type-color-c13: #988cb5; | ||||
| $eve-wh-type-color-drifter: #ff44f6; | ||||
| $eve-wh-type-color-thera: #ffffff; | ||||
| $eve-wh-type-color-zarzakh: #212121; | ||||
| $homeBase: rgb(197, 253, 67); | ||||
| $homeAlpha: rgba(197, 253, 67, 0.32); | ||||
| $homeDark30: darken($homeBase, 30%); | ||||
|  | ||||
| $eve-security-color-10: #2c74df; | ||||
| $eve-security-color-09: #3998e8; | ||||
| $eve-security-color-08: #4dcbf5; | ||||
| $eve-security-color-07: #60d8a2; | ||||
| $eve-security-color-06: #71e454; | ||||
| $eve-security-color-05: #f2fc81; | ||||
| $eve-security-color-04: #d96c07; | ||||
| $eve-security-color-03: #cb440f; | ||||
| $eve-security-color-02: #b91117; | ||||
| $eve-security-color-01: #732020; | ||||
| $eve-security-color-00: #8b3263; | ||||
| $eve-security-color-m-01: #8b3263; | ||||
| $eve-security-color-m-02: #8b3263; | ||||
| $eve-security-color-m-03: #8b3263; | ||||
| $eve-security-color-m-04: #8b3263; | ||||
| $eve-security-color-m-05: #8b3263; | ||||
| $eve-security-color-m-06: #8b3263; | ||||
| $eve-security-color-m-07: #8b3263; | ||||
| $eve-security-color-m-08: #8b3263; | ||||
| $eve-security-color-m-09: #8b3263; | ||||
| $eve-security-color-m-10: #8b3263; | ||||
|  | ||||
| $eve-solar-system-status-unknown: transparent; | ||||
| $eve-solar-system-status-friendly: #3bbd3952; | ||||
| $eve-solar-system-status-warning: #906518a6; | ||||
| $eve-solar-system-status-target: #b439ff6b; | ||||
| $eve-solar-system-status-dangerous: #d54040; | ||||
| $eve-solar-system-status-lookingFor: rgba(67, 176, 253, 0.48); | ||||
| $eve-solar-system-status-home: rgb(197, 253, 67); | ||||
| :root { | ||||
|   --pastel-blue: #5a7d9a; | ||||
|   --pastel-pink: #d291bc; | ||||
|   --pastel-green: #88b04b; | ||||
|   --pastel-yellow: #ffdd59; | ||||
|   --dark-bg: #2d2d2d; | ||||
|   --text-color: #ffffff; | ||||
|   --tooltip-bg: #202020; | ||||
|  | ||||
| $eve-solar-system-status-color-unknown: transparent; | ||||
| $eve-solar-system-status-color-friendly: #3bbd39; | ||||
| $eve-solar-system-status-color-warning: #ffb93b; | ||||
| $eve-solar-system-status-color-target: #b439ff; | ||||
| $eve-solar-system-status-color-dangerous: #d54040; | ||||
| $eve-solar-system-status-color-lookingFor: #43c2fd; | ||||
| $eve-solar-system-status-color-home: rgb(197, 253, 67); | ||||
|   --pastel-blue-darken10: #4f6b86; | ||||
|   --pastel-blue-lighten10: #6da3af; | ||||
|   --pastel-pink-darken10: #bb7ca9; | ||||
|   --pastel-pink-lighten10: #e0a6cb; | ||||
|   --pastel-green-darken10: #79a244; | ||||
|   --pastel-green-lighten10: #99cf52; | ||||
|   --pastel-yellow-darken10: #e6c44f; | ||||
|   --pastel-yellow-lighten10: #ffe874; | ||||
|  | ||||
|   --eve-link-color-default: #333; | ||||
|   --eve-link-color-top-mass-0: #333; | ||||
|   --eve-link-color-top-mass-1: #5a4520; | ||||
|   --eve-link-color-top-mass-2: #672c2c; | ||||
|   --eve-link-color-middle-mass-0: #333; | ||||
|   --eve-link-color-middle-mass-1: #333; | ||||
|   --eve-link-color-middle-mass-2: #333; | ||||
|   --eve-link-color-middle-time-0: #5c5c5c; | ||||
|   --eve-link-color-middle-time-1: #ff00cd; | ||||
|   --eve-link-color-middle-time-1-border: #99f3ff; | ||||
|   --eve-link-color-top-mass-1-time-1: #796300; | ||||
|   --eve-link-color-top-mass-2-time-1: #8c1717; | ||||
|   --eve-link-color-temp: orange; | ||||
|  | ||||
|   --eve-effect-pulsar: #40aef5; | ||||
|   --eve-effect-magnetar: #f058f8; | ||||
|   --eve-effect-wolfRayet: #ef7843; | ||||
|   --eve-effect-blackHole: #1b1b1b; | ||||
|   --eve-effect-cataclysmicVariable: #ffea90; | ||||
|   --eve-effect-redGiant: #fd3c3c; | ||||
|   --eve-effect-dazhLiminalityLocus: #ff6464; | ||||
|   --eve-effect-imperialStellarObservatory: #6991ce; | ||||
|   --eve-effect-stateStellarObservatory: #6991ce; | ||||
|   --eve-effect-republicStellarObservatory: #6991ce; | ||||
|   --eve-effect-federalStellarObservatory: #6991ce; | ||||
|  | ||||
|   --eve-wh-type-color-high: #5dffd2; | ||||
|   --eve-wh-type-color-low: #f79400; | ||||
|   --eve-wh-type-color-null: #fc3c3c; | ||||
|   --eve-wh-type-color-c1: #69bfce; | ||||
|   --eve-wh-type-color-c2: #6991ce; | ||||
|   --eve-wh-type-color-c3: #a8cb70; | ||||
|   --eve-wh-type-color-c4: #e39c68; | ||||
|   --eve-wh-type-color-c5: #de8686; | ||||
|   --eve-wh-type-color-c6: #e76363; | ||||
|   --eve-wh-type-color-c13: #988cb5; | ||||
|   --eve-wh-type-color-drifter: #ff44f6; | ||||
|   --eve-wh-type-color-thera: #ffffff; | ||||
|   --eve-wh-type-color-zarzakh: #212121; | ||||
|  | ||||
|   --eve-security-color-10: #2c74df; | ||||
|   --eve-security-color-09: #3998e8; | ||||
|   --eve-security-color-08: #4dcbf5; | ||||
|   --eve-security-color-07: #60d8a2; | ||||
|   --eve-security-color-06: #71e454; | ||||
|   --eve-security-color-05: #f2fc81; | ||||
|   --eve-security-color-04: #d96c07; | ||||
|   --eve-security-color-03: #cb440f; | ||||
|   --eve-security-color-02: #b91117; | ||||
|   --eve-security-color-01: #732020; | ||||
|   --eve-security-color-00: #8b3263; | ||||
|   --eve-security-color-m-01: #8b3263; | ||||
|   --eve-security-color-m-02: #8b3263; | ||||
|   --eve-security-color-m-03: #8b3263; | ||||
|   --eve-security-color-m-04: #8b3263; | ||||
|   --eve-security-color-m-05: #8b3263; | ||||
|   --eve-security-color-m-06: #8b3263; | ||||
|   --eve-security-color-m-07: #8b3263; | ||||
|   --eve-security-color-m-08: #8b3263; | ||||
|   --eve-security-color-m-09: #8b3263; | ||||
|   --eve-security-color-m-10: #8b3263; | ||||
|  | ||||
|   --eve-solar-system-status-unknown: transparent; | ||||
|   --eve-solar-system-status-color-unknown: transparent; | ||||
|   --eve-solar-system-status-home:                #{$homeAlpha}; | ||||
|   --eve-solar-system-status-color-home:          #{$homeBase}; | ||||
|   --eve-solar-system-status-color-home-dark30:   #{$homeDark30}; | ||||
|   --eve-solar-system-status-friendly:            #{$friendlyAlpha}; | ||||
|   --eve-solar-system-status-color-friendly:      #{$friendlyBase}; | ||||
|   --eve-solar-system-status-friendly-dark30:     #{$friendlyDark30}; | ||||
|   --eve-solar-system-status-color-friendly-dark20: #{$friendlyDark20}; | ||||
|   --eve-solar-system-status-color-friendly-dark5:  #{$friendlyDark5}; | ||||
|   --eve-solar-system-status-lookingFor:              #{$lookingForAlpha}; | ||||
|   --eve-solar-system-status-color-lookingFor:        #{$lookingForBase}; | ||||
|   --eve-solar-system-status-color-lookingFor-dark15: #{$lookingForDark15}; | ||||
|   --eve-solar-system-status-warning: #906518a6; | ||||
|   --eve-solar-system-status-color-warning: #ffb93b; | ||||
|   --eve-solar-system-status-target: #b439ff6b; | ||||
|   --eve-solar-system-status-color-target: #b439ff; | ||||
|   --eve-solar-system-status-dangerous: #d54040; | ||||
|   --eve-solar-system-status-color-dangerous: #d54040; | ||||
|  | ||||
|   --conn-time-eol: #7452c3e3; | ||||
|   --conn-frigate: #325d88; | ||||
|   --conn-save: rgba(155, 102, 45, 0.85); | ||||
|   --selected-item-bg: rgba(98, 98, 98, 0.33); | ||||
| } | ||||
|   | ||||
| @@ -1,535 +1,504 @@ | ||||
| @import "eve-common-variables"; | ||||
| @import './eve-common-variables'; | ||||
|  | ||||
|  | ||||
| .eve-wh-effect-color-pulsar { | ||||
|   fill: $eve-effect-pulsar; | ||||
|   background-color: $eve-effect-pulsar; | ||||
|   fill: var(--eve-effect-pulsar); | ||||
|   background-color: var(--eve-effect-pulsar); | ||||
| } | ||||
|  | ||||
| .eve-wh-effect-color-magnetar { | ||||
|   fill: $eve-effect-magnetar; | ||||
|   background-color: $eve-effect-magnetar; | ||||
|   fill: var(--eve-effect-magnetar); | ||||
|   background-color: var(--eve-effect-magnetar); | ||||
| } | ||||
|  | ||||
| .eve-wh-effect-color-wolfRayet { | ||||
|   fill: $eve-effect-wolfRayet; | ||||
|   background-color: $eve-effect-wolfRayet; | ||||
|   fill: var(--eve-effect-wolfRayet); | ||||
|   background-color: var(--eve-effect-wolfRayet); | ||||
| } | ||||
|  | ||||
| .eve-wh-effect-color-blackHole { | ||||
|   fill: $eve-effect-blackHole; | ||||
|   background-color: $eve-effect-blackHole; | ||||
|   box-shadow: 0 0 8px rgba(255 255 255 / 33); | ||||
|   fill: var(--eve-effect-blackHole); | ||||
|   background-color: var(--eve-effect-blackHole); | ||||
|   box-shadow: 0 0 8px rgba(255, 255, 255, 0.33); | ||||
| } | ||||
|  | ||||
| .eve-wh-effect-color-cataclysmicVariable { | ||||
|   fill: $eve-effect-cataclysmicVariable; | ||||
|   background-color: $eve-effect-cataclysmicVariable; | ||||
|   fill: var(--eve-effect-cataclysmicVariable); | ||||
|   background-color: var(--eve-effect-cataclysmicVariable); | ||||
| } | ||||
|  | ||||
| .eve-wh-effect-color-redGiant { | ||||
|   fill: $eve-effect-redGiant; | ||||
|   background-color: $eve-effect-redGiant; | ||||
|   fill: var(--eve-effect-redGiant); | ||||
|   background-color: var(--eve-effect-redGiant); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-pulsar { | ||||
|   color: $eve-effect-pulsar; | ||||
|   color: var(--eve-effect-pulsar); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-magnetar { | ||||
|   color: $eve-effect-magnetar; | ||||
|   color: var(--eve-effect-magnetar); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-wolfRayet { | ||||
|   color: $eve-effect-wolfRayet; | ||||
|   color: var(--eve-effect-wolfRayet); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-blackHole { | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-cataclysmicVariable { | ||||
|   color: $eve-effect-cataclysmicVariable; | ||||
|   color: var(--eve-effect-cataclysmicVariable); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-redGiant { | ||||
|   color: $eve-effect-redGiant; | ||||
|   color: var(--eve-effect-redGiant); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-dazhLiminalityLocus { | ||||
|   color: $eve-effect-dazhLiminalityLocus; | ||||
|   color: var(--eve-effect-dazhLiminalityLocus); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-imperialStellarObservatory { | ||||
|   color: $eve-effect-imperialStellarObservatory; | ||||
|   color: var(--eve-effect-imperialStellarObservatory); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-stateStellarObservatory { | ||||
|   color: $eve-effect-stateStellarObservatory; | ||||
|   color: var(--eve-effect-stateStellarObservatory); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-republicStellarObservatory { | ||||
|   color: $eve-effect-republicStellarObservatory; | ||||
|   color: var(--eve-effect-republicStellarObservatory); | ||||
| } | ||||
|  | ||||
| .text-eve-wh-effect-color-federalStellarObservatory { | ||||
|   color: $eve-effect-federalStellarObservatory; | ||||
|   color: var(--eve-effect-federalStellarObservatory); | ||||
| } | ||||
|  | ||||
| /* Security color classes */ | ||||
| .eve-security-color-10 { | ||||
|   color: $eve-security-color-10 !important; | ||||
|   fill: $eve-security-color-10; | ||||
|   color: var(--eve-security-color-10) !important; | ||||
|   fill: var(--eve-security-color-10); | ||||
| } | ||||
|  | ||||
| .eve-security-color-09 { | ||||
|   color: $eve-security-color-09 !important; | ||||
|   fill: $eve-security-color-09; | ||||
|   color: var(--eve-security-color-09) !important; | ||||
|   fill: var(--eve-security-color-09); | ||||
| } | ||||
|  | ||||
| .eve-security-color-08 { | ||||
|   color: $eve-security-color-08 !important; | ||||
|   fill: $eve-security-color-08; | ||||
|   color: var(--eve-security-color-08) !important; | ||||
|   fill: var(--eve-security-color-08); | ||||
| } | ||||
|  | ||||
| .eve-security-color-07 { | ||||
|   color: $eve-security-color-07 !important; | ||||
|   fill: $eve-security-color-07; | ||||
|   color: var(--eve-security-color-07) !important; | ||||
|   fill: var(--eve-security-color-07); | ||||
| } | ||||
|  | ||||
| .eve-security-color-06 { | ||||
|   color: $eve-security-color-06 !important; | ||||
|   fill: $eve-security-color-06; | ||||
|   color: var(--eve-security-color-06) !important; | ||||
|   fill: var(--eve-security-color-06); | ||||
| } | ||||
|  | ||||
| .eve-security-color-05 { | ||||
|   color: $eve-security-color-05 !important; | ||||
|   fill: $eve-security-color-05; | ||||
|   color: var(--eve-security-color-05) !important; | ||||
|   fill: var(--eve-security-color-05); | ||||
| } | ||||
|  | ||||
| .eve-security-color-04 { | ||||
|   color: $eve-security-color-04 !important; | ||||
|   fill: $eve-security-color-04; | ||||
|   color: var(--eve-security-color-04) !important; | ||||
|   fill: var(--eve-security-color-04); | ||||
| } | ||||
|  | ||||
| .eve-security-color-03 { | ||||
|   color: $eve-security-color-03 !important; | ||||
|   fill: $eve-security-color-03; | ||||
|   color: var(--eve-security-color-03) !important; | ||||
|   fill: var(--eve-security-color-03); | ||||
| } | ||||
|  | ||||
| .eve-security-color-02 { | ||||
|   color: $eve-security-color-02 !important; | ||||
|   fill: $eve-security-color-02; | ||||
|   color: var(--eve-security-color-02) !important; | ||||
|   fill: var(--eve-security-color-02); | ||||
| } | ||||
|  | ||||
| .eve-security-color-01 { | ||||
|   color: $eve-security-color-01 !important; | ||||
|   fill: $eve-security-color-01; | ||||
|   color: var(--eve-security-color-01) !important; | ||||
|   fill: var(--eve-security-color-01); | ||||
| } | ||||
|  | ||||
| .eve-security-color-00 { | ||||
|   color: $eve-security-color-00 !important; | ||||
|   fill: $eve-security-color-00; | ||||
|   color: var(--eve-security-color-00) !important; | ||||
|   fill: var(--eve-security-color-00); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-01 { | ||||
|   color: $eve-security-color-m-01 !important; | ||||
|   fill: $eve-security-color-m-01; | ||||
|   color: var(--eve-security-color-m-01) !important; | ||||
|   fill: var(--eve-security-color-m-01); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-02 { | ||||
|   color: $eve-security-color-m-02 !important; | ||||
|   fill: $eve-security-color-m-02; | ||||
|   color: var(--eve-security-color-m-02) !important; | ||||
|   fill: var(--eve-security-color-m-02); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-03 { | ||||
|   color: $eve-security-color-m-03 !important; | ||||
|   fill: $eve-security-color-m-03; | ||||
|   color: var(--eve-security-color-m-03) !important; | ||||
|   fill: var(--eve-security-color-m-03); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-04 { | ||||
|   color: $eve-security-color-m-04 !important; | ||||
|   fill: $eve-security-color-m-04; | ||||
|   color: var(--eve-security-color-m-04) !important; | ||||
|   fill: var(--eve-security-color-m-04); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-05 { | ||||
|   color: $eve-security-color-m-05 !important; | ||||
|   fill: $eve-security-color-m-05; | ||||
|   color: var(--eve-security-color-m-05) !important; | ||||
|   fill: var(--eve-security-color-m-05); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-06 { | ||||
|   color: $eve-security-color-m-06 !important; | ||||
|   fill: $eve-security-color-m-06; | ||||
|   color: var(--eve-security-color-m-06) !important; | ||||
|   fill: var(--eve-security-color-m-06); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-07 { | ||||
|   color: $eve-security-color-m-07 !important; | ||||
|   fill: $eve-security-color-m-07; | ||||
|   color: var(--eve-security-color-m-07) !important; | ||||
|   fill: var(--eve-security-color-m-07); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-08 { | ||||
|   color: $eve-security-color-m-08 !important; | ||||
|   fill: $eve-security-color-m-08; | ||||
|   color: var(--eve-security-color-m-08) !important; | ||||
|   fill: var(--eve-security-color-m-08); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-09 { | ||||
|   color: $eve-security-color-m-09 !important; | ||||
|   fill: $eve-security-color-m-09; | ||||
|   color: var(--eve-security-color-m-09) !important; | ||||
|   fill: var(--eve-security-color-m-09); | ||||
| } | ||||
|  | ||||
| .eve-security-color-m-10 { | ||||
|   color: $eve-security-color-m-10 !important; | ||||
|   fill: $eve-security-color-m-10; | ||||
|   color: var(--eve-security-color-m-10) !important; | ||||
|   fill: var(--eve-security-color-m-10); | ||||
| } | ||||
|  | ||||
| /* Security backgrounds */ | ||||
| .eve-security-background-10 { | ||||
|   background-color: $eve-security-color-10; | ||||
|   fill: $eve-security-color-10; | ||||
|   background-color: var(--eve-security-color-10); | ||||
|   fill: var(--eve-security-color-10); | ||||
| } | ||||
|  | ||||
| .eve-security-background-09 { | ||||
|   background-color: $eve-security-color-09; | ||||
|   fill: $eve-security-color-09; | ||||
|   background-color: var(--eve-security-color-09); | ||||
|   fill: var(--eve-security-color-09); | ||||
| } | ||||
|  | ||||
| .eve-security-background-08 { | ||||
|   background-color: $eve-security-color-08; | ||||
|   fill: $eve-security-color-08; | ||||
|   background-color: var(--eve-security-color-08); | ||||
|   fill: var(--eve-security-color-08); | ||||
| } | ||||
|  | ||||
| .eve-security-background-07 { | ||||
|   background-color: $eve-security-color-07; | ||||
|   fill: $eve-security-color-07; | ||||
|   background-color: var(--eve-security-color-07); | ||||
|   fill: var(--eve-security-color-07); | ||||
| } | ||||
|  | ||||
| .eve-security-background-06 { | ||||
|   background-color: $eve-security-color-06; | ||||
|   fill: $eve-security-color-06; | ||||
|   background-color: var(--eve-security-color-06); | ||||
|   fill: var(--eve-security-color-06); | ||||
| } | ||||
|  | ||||
| .eve-security-background-05 { | ||||
|   background-color: $eve-security-color-05; | ||||
|   fill: $eve-security-color-05; | ||||
|   background-color: var(--eve-security-color-05); | ||||
|   fill: var(--eve-security-color-05); | ||||
| } | ||||
|  | ||||
| .eve-security-background-04 { | ||||
|   background-color: $eve-security-color-04; | ||||
|   fill: $eve-security-color-04; | ||||
|   background-color: var(--eve-security-color-04); | ||||
|   fill: var(--eve-security-color-04); | ||||
| } | ||||
|  | ||||
| .eve-security-background-03 { | ||||
|   background-color: $eve-security-color-03; | ||||
|   fill: $eve-security-color-03; | ||||
|   background-color: var(--eve-security-color-03); | ||||
|   fill: var(--eve-security-color-03); | ||||
| } | ||||
|  | ||||
| .eve-security-background-02 { | ||||
|   background-color: $eve-security-color-02; | ||||
|   fill: $eve-security-color-02; | ||||
|   background-color: var(--eve-security-color-02); | ||||
|   fill: var(--eve-security-color-02); | ||||
| } | ||||
|  | ||||
| .eve-security-background-01 { | ||||
|   background-color: $eve-security-color-01; | ||||
|   fill: $eve-security-color-01; | ||||
|   background-color: var(--eve-security-color-01); | ||||
|   fill: var(--eve-security-color-01); | ||||
| } | ||||
|  | ||||
| .eve-security-background-00 { | ||||
|   background-color: $eve-security-color-00; | ||||
|   fill: $eve-security-color-00; | ||||
|   background-color: var(--eve-security-color-00); | ||||
|   fill: var(--eve-security-color-00); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-01 { | ||||
|   background-color: $eve-security-color-m-01; | ||||
|   fill: $eve-security-color-m-01; | ||||
|   background-color: var(--eve-security-color-m-01); | ||||
|   fill: var(--eve-security-color-m-01); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-02 { | ||||
|   background-color: $eve-security-color-m-02; | ||||
|   fill: $eve-security-color-m-02; | ||||
|   background-color: var(--eve-security-color-m-02); | ||||
|   fill: var(--eve-security-color-m-02); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-03 { | ||||
|   background-color: $eve-security-color-m-03; | ||||
|   fill: $eve-security-color-m-03; | ||||
|   background-color: var(--eve-security-color-m-03); | ||||
|   fill: var(--eve-security-color-m-03); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-04 { | ||||
|   background-color: $eve-security-color-m-04; | ||||
|   fill: $eve-security-color-m-04; | ||||
|   background-color: var(--eve-security-color-m-04); | ||||
|   fill: var(--eve-security-color-m-04); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-05 { | ||||
|   background-color: $eve-security-color-m-05; | ||||
|   fill: $eve-security-color-m-05; | ||||
|   background-color: var(--eve-security-color-m-05); | ||||
|   fill: var(--eve-security-color-m-05); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-06 { | ||||
|   background-color: $eve-security-color-m-06; | ||||
|   fill: $eve-security-color-m-06; | ||||
|   background-color: var(--eve-security-color-m-06); | ||||
|   fill: var(--eve-security-color-m-06); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-07 { | ||||
|   background-color: $eve-security-color-m-07; | ||||
|   fill: $eve-security-color-m-07; | ||||
|   background-color: var(--eve-security-color-m-07); | ||||
|   fill: var(--eve-security-color-m-07); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-08 { | ||||
|   background-color: $eve-security-color-m-08; | ||||
|   fill: $eve-security-color-m-08; | ||||
|   background-color: var(--eve-security-color-m-08); | ||||
|   fill: var(--eve-security-color-m-08); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-09 { | ||||
|   background-color: $eve-security-color-m-09; | ||||
|   fill: $eve-security-color-m-09; | ||||
|   background-color: var(--eve-security-color-m-09); | ||||
|   fill: var(--eve-security-color-m-09); | ||||
| } | ||||
|  | ||||
| .eve-security-background-m-10 { | ||||
|   background-color: $eve-security-color-m-10; | ||||
|   fill: $eve-security-color-m-10; | ||||
|   background-color: var(--eve-security-color-m-10); | ||||
|   fill: var(--eve-security-color-m-10); | ||||
| } | ||||
|  | ||||
| /* WH Type color classes */ | ||||
| .eve-wh-type-color-high { | ||||
|   color: $eve-wh-type-color-high; | ||||
|   fill: $eve-wh-type-color-high; | ||||
|   color: var(--eve-wh-type-color-high) !important; | ||||
|   fill: var(--eve-wh-type-color-high); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-low { | ||||
|   color: $eve-wh-type-color-low; | ||||
|   fill: $eve-wh-type-color-low; | ||||
|   color: var(--eve-wh-type-color-low) !important; | ||||
|   fill: var(--eve-wh-type-color-low); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-null { | ||||
|   color: $eve-wh-type-color-null; | ||||
|   fill: $eve-wh-type-color-null; | ||||
|   color: var(--eve-wh-type-color-null) !important; | ||||
|   fill: var(--eve-wh-type-color-null); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c1 { | ||||
|   color: $eve-wh-type-color-c1 !important; | ||||
|   fill: $eve-wh-type-color-c1; | ||||
|   color: var(--eve-wh-type-color-c1) !important; | ||||
|   fill: var(--eve-wh-type-color-c1); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c2 { | ||||
|   color: $eve-wh-type-color-c2 !important; | ||||
|   fill: $eve-wh-type-color-c2; | ||||
|   color: var(--eve-wh-type-color-c2) !important; | ||||
|   fill: var(--eve-wh-type-color-c2); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c3 { | ||||
|   color: $eve-wh-type-color-c3 !important; | ||||
|   fill: $eve-wh-type-color-c3; | ||||
|   color: var(--eve-wh-type-color-c3) !important; | ||||
|   fill: var(--eve-wh-type-color-c3); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c4 { | ||||
|   color: $eve-wh-type-color-c4 !important; | ||||
|   fill: $eve-wh-type-color-c4; | ||||
|   color: var(--eve-wh-type-color-c4) !important; | ||||
|   fill: var(--eve-wh-type-color-c4); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c5 { | ||||
|   color: $eve-wh-type-color-c5 !important; | ||||
|   fill: $eve-wh-type-color-c5; | ||||
|   color: var(--eve-wh-type-color-c5) !important; | ||||
|   fill: var(--eve-wh-type-color-c5); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c6 { | ||||
|   color: $eve-wh-type-color-c6 !important; | ||||
|   fill: $eve-wh-type-color-c6; | ||||
|   color: var(--eve-wh-type-color-c6) !important; | ||||
|   fill: var(--eve-wh-type-color-c6); | ||||
|   font-weight: bold !important; | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-c13 { | ||||
|   color: $eve-wh-type-color-c13 !important; | ||||
|   fill: $eve-wh-type-color-c13; | ||||
|   color: var(--eve-wh-type-color-c13) !important; | ||||
|   fill: var(--eve-wh-type-color-c13); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-drifter { | ||||
|   color: $eve-wh-type-color-drifter !important; | ||||
|   fill: $eve-wh-type-color-drifter; | ||||
|   color: var(--eve-wh-type-color-drifter) !important; | ||||
|   fill: var(--eve-wh-type-color-drifter); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-color-thera { | ||||
|   color: $eve-wh-type-color-thera !important; | ||||
|   fill: $eve-wh-type-color-thera; | ||||
|   color: var(--eve-wh-type-color-thera) !important; | ||||
|   fill: var(--eve-wh-type-color-thera); | ||||
| } | ||||
|  | ||||
| /* WH Type backgrounds */ | ||||
| .eve-wh-type-background-high { | ||||
|   background-color: $eve-wh-type-color-high; | ||||
|   background-color: var(--eve-wh-type-color-high); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-low { | ||||
|   background-color: $eve-wh-type-color-low; | ||||
|   background-color: var(--eve-wh-type-color-low); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-null { | ||||
|   background-color: $eve-wh-type-color-null; | ||||
|   background-color: var(--eve-wh-type-color-null); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c1 { | ||||
|   background-color: $eve-wh-type-color-c1; | ||||
|   background-color: var(--eve-wh-type-color-c1); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c2 { | ||||
|   background-color: $eve-wh-type-color-c2; | ||||
|   background-color: var(--eve-wh-type-color-c2); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c3 { | ||||
|   background-color: $eve-wh-type-color-c3; | ||||
|   background-color: var(--eve-wh-type-color-c3); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c4 { | ||||
|   background-color: $eve-wh-type-color-c4; | ||||
|   background-color: var(--eve-wh-type-color-c4); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c5 { | ||||
|   background-color: $eve-wh-type-color-c5; | ||||
|   background-color: var(--eve-wh-type-color-c5); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c6 { | ||||
|   background-color: $eve-wh-type-color-c6; | ||||
|   background-color: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-c13 { | ||||
|   background-color: $eve-wh-type-color-c13; | ||||
|   background-color: var(--eve-wh-type-color-c13); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-drifter { | ||||
|   background-color: $eve-wh-type-color-drifter; | ||||
|   background-color: var(--eve-wh-type-color-drifter); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-thera { | ||||
|   background-color: $eve-wh-type-color-thera; | ||||
|   background-color: var(--eve-wh-type-color-thera); | ||||
| } | ||||
|  | ||||
| .eve-wh-type-background-zarzakh { | ||||
|   background-color: $eve-wh-type-color-zarzakh; | ||||
|   background-color: var(--eve-wh-type-color-zarzakh); | ||||
| } | ||||
|  | ||||
| /* Kind color classes */ | ||||
| .eve-kind-color-high { | ||||
|   color: $eve-wh-type-color-high; | ||||
|   fill: $eve-wh-type-color-high; | ||||
|   color: var(--eve-wh-type-color-high); | ||||
|   fill: var(--eve-wh-type-color-high); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-low { | ||||
|   color: $eve-wh-type-color-low; | ||||
|   fill: $eve-wh-type-color-low; | ||||
|   color: var(--eve-wh-type-color-low); | ||||
|   fill: var(--eve-wh-type-color-low); | ||||
|   font-weight: bold; | ||||
| } | ||||
|  | ||||
| .eve-kind-color-null { | ||||
|   color: $eve-wh-type-color-null; | ||||
|   fill: $eve-wh-type-color-null; | ||||
|   color: var(--eve-wh-type-color-null); | ||||
|   fill: var(--eve-wh-type-color-null); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-wh { | ||||
|   color: $eve-wh-type-color-c6; | ||||
|   fill: $eve-wh-type-color-c6; | ||||
|   color: var(--eve-wh-type-color-c6); | ||||
|   fill: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-thera { | ||||
|   color: $eve-wh-type-color-thera; | ||||
|   fill: $eve-wh-type-color-thera; | ||||
|   color: var(--eve-wh-type-color-thera); | ||||
|   fill: var(--eve-wh-type-color-thera); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-abyss { | ||||
|   color: $eve-wh-type-color-c6; | ||||
|   fill: $eve-wh-type-color-c6; | ||||
|   color: var(--eve-wh-type-color-c6); | ||||
|   fill: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-penalty { | ||||
|   color: $eve-wh-type-color-c6; | ||||
|   fill: $eve-wh-type-color-c6; | ||||
|   color: var(--eve-wh-type-color-c6); | ||||
|   fill: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-pochven { | ||||
|   color: $eve-wh-type-color-c6; | ||||
|   fill: $eve-wh-type-color-c6; | ||||
|   color: var(--eve-wh-type-color-c6); | ||||
|   fill: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-color-zarzakh { | ||||
|   color: $eve-wh-type-color-zarzakh; | ||||
|   fill: $eve-wh-type-color-zarzakh; | ||||
|   color: var(--eve-wh-type-color-zarzakh); | ||||
|   fill: var(--eve-wh-type-color-zarzakh); | ||||
| } | ||||
|  | ||||
| /* Kind backgrounds */ | ||||
| .eve-kind-background-high { | ||||
|   background-color: $eve-wh-type-color-high; | ||||
|   background-color: var(--eve-wh-type-color-high); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-low { | ||||
|   background-color: $eve-wh-type-color-low; | ||||
|   background-color: var(--eve-wh-type-color-low); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-null { | ||||
|   background-color: $eve-wh-type-color-null; | ||||
|   background-color: var(--eve-wh-type-color-null); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-wh { | ||||
|   background-color: $eve-wh-type-color-c6; | ||||
|   background-color: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-thera { | ||||
|   background-color: $eve-wh-type-color-thera; | ||||
|   background-color: var(--eve-wh-type-color-thera); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-abyss { | ||||
|   background-color: $eve-wh-type-color-c6; | ||||
|   background-color: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-penalty { | ||||
|   background-color: $eve-wh-type-color-c6; | ||||
|   background-color: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-pochven { | ||||
|   background-color: $eve-wh-type-color-c6; | ||||
|   background-color: var(--eve-wh-type-color-c6); | ||||
| } | ||||
|  | ||||
| .eve-kind-background-zarzakh { | ||||
|   background-color: $eve-wh-type-color-zarzakh; | ||||
|   background-color: var(--eve-wh-type-color-zarzakh); | ||||
| } | ||||
|  | ||||
| /* System status color classes */ | ||||
| .eve-system-status-color-clear { | ||||
|   color: $eve-solar-system-status-color-unknown; | ||||
|   color: var(--eve-solar-system-status-color-unknown); | ||||
| } | ||||
|  | ||||
| .eve-system-status-color-home { | ||||
|   color: $eve-solar-system-status-color-home; | ||||
|   color: var(--eve-solar-system-status-color-home); | ||||
| } | ||||
|  | ||||
| .eve-system-status-color-friendly { | ||||
|   color: $eve-solar-system-status-color-friendly; | ||||
|   color: var(--eve-solar-system-status-color-friendly); | ||||
| } | ||||
|  | ||||
| .eve-system-status-color-lookingFor { | ||||
|   color: $eve-solar-system-status-color-lookingFor; | ||||
|   color: var(--eve-solar-system-status-color-lookingFor); | ||||
| } | ||||
|  | ||||
| .eve-system-status-color-warning { | ||||
|   color: $eve-solar-system-status-color-warning; | ||||
|   color: var(--eve-solar-system-status-color-warning); | ||||
| } | ||||
|  | ||||
| .eve-system-status-color-target { | ||||
|   color: $eve-solar-system-status-color-target; | ||||
|   color: var(--eve-solar-system-status-color-target); | ||||
| } | ||||
| .eve-system-status-color-dangerous { | ||||
|   color: var(--eve-solar-system-status-color-dangerous); | ||||
| } | ||||
|  | ||||
| .eve-system-status-color-dangerous { | ||||
|   color: $eve-solar-system-status-color-dangerous; | ||||
| .eve-system-status-clear { | ||||
|   background-color: var(--eve-solar-system-status-unknown); | ||||
| } | ||||
| .eve-system-status-home { | ||||
|   background-color: var(--eve-solar-system-status-home); | ||||
| } | ||||
| .eve-system-status-friendly { | ||||
|   background-color: var(--eve-solar-system-status-friendly); | ||||
| } | ||||
| .eve-system-status-lookingFor { | ||||
|   background-color: var(--eve-solar-system-status-lookingFor); | ||||
| } | ||||
| .eve-system-status-warning { | ||||
|   background-color: var(--eve-solar-system-status-warning); | ||||
| } | ||||
| .eve-system-status-target { | ||||
|   background-color: var(--eve-solar-system-status-target); | ||||
| } | ||||
| .eve-system-status-dangerous { | ||||
|   background-color: var(--eve-solar-system-status-dangerous); | ||||
| } | ||||
|  | ||||
| .eve-system-status-clear { | ||||
|   background-color: var(--eve-solar-system-status-unknown); | ||||
|   color: var(--eve-solar-system-status-color-unknown); | ||||
| } | ||||
|  | ||||
| .eve-system-status-home { | ||||
|   background-color: var(--eve-solar-system-status-home); | ||||
|   color: var(--eve-solar-system-status-color-home); | ||||
| } | ||||
|  | ||||
| .eve-system-status-friendly { | ||||
|   background-color: var(--eve-solar-system-status-friendly); | ||||
|   color: var(--eve-solar-system-status-color-friendly); | ||||
| } | ||||
|  | ||||
| .eve-system-status-lookingFor { | ||||
|   background-color: var(--eve-solar-system-status-lookingFor); | ||||
|   color: var(--eve-solar-system-status-color-lookingFor); | ||||
| } | ||||
|  | ||||
| .eve-system-status-warning { | ||||
|   background-color: var(--eve-solar-system-status-warning); | ||||
|   color: var(--eve-solar-system-status-color-warning); | ||||
| } | ||||
|  | ||||
| .eve-system-status-target { | ||||
|   background-color: var(--eve-solar-system-status-target); | ||||
|   color: var(--eve-solar-system-status-color-target); | ||||
| } | ||||
|  | ||||
| .eve-system-status-dangerous { | ||||
|   background-color: var(--eve-solar-system-status-dangerous); | ||||
|   color: var(--eve-solar-system-status-color-dangerous); | ||||
| } | ||||
|  | ||||
| .wd-route-system-shape-triangle { | ||||
|   clip-path: polygon(50% 0, 0 100%, 100% 100%); | ||||
| } | ||||
|  | ||||
| .wd-route-system-shape-circle { | ||||
|   border-radius: 40%; | ||||
| } | ||||
|  | ||||
| /* Some additional background classes */ | ||||
| .wd-marker-bookmark-color-shattered { | ||||
|   background-color: #833ca4; | ||||
|   margin-top: 1px; | ||||
| } | ||||
|  | ||||
|  | ||||
| .wd-marker-bookmark-color-custom { | ||||
|   background-color: #282828; | ||||
|   border: 1px solid #4c4c4c; | ||||
| @@ -572,3 +541,49 @@ | ||||
| .wd-marker-bookmark-color-danger { | ||||
|   background-color: #d10600; | ||||
| } | ||||
|  | ||||
| .react-flow { | ||||
|   color: var(--text-color); | ||||
|  | ||||
|   &__pane { | ||||
|     cursor: auto; | ||||
|   } | ||||
|  | ||||
|   &__minimap { | ||||
|     background-color: rgba(66, 66, 66, 1); | ||||
|     opacity: 0.7; | ||||
|     border: 1px solid #2f2f2f; | ||||
|     border-radius: 4px; | ||||
|     overflow: hidden; | ||||
|   } | ||||
|  | ||||
|   &__minimap-mask { | ||||
|     fill: rgba(28, 28, 28, 0.75); | ||||
|   } | ||||
|  | ||||
|   &__controls { | ||||
|     filter: brightness(1.5); | ||||
|   } | ||||
|  | ||||
|   &__minimap-node { | ||||
|     fill: #ffb03a; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .context-menu-active { | ||||
|   background-color: rgba(131, 131, 131, 0.33); | ||||
| } | ||||
|  | ||||
| .p-dialog { | ||||
|   .p-dialog-header { | ||||
|     height: 40px; | ||||
|     padding: 1rem; | ||||
|     padding-right: 10px !important; | ||||
|   } | ||||
|   .p-dialog-title { | ||||
|     font-size: 1rem !important; | ||||
|   } | ||||
|   .p-dialog-header-icons { | ||||
|     align-self: initial !important; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										2
									
								
								assets/js/hooks/Mapper/components/map/styles/index.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								assets/js/hooks/Mapper/components/map/styles/index.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| @import './default-theme.scss';  | ||||
| @import './pathfinder-theme.scss';  | ||||
| @@ -1,74 +0,0 @@ | ||||
| $pastel-blue: #5a7d9a; | ||||
| $pastel-pink: #d291bc; | ||||
| $pastel-green: #88b04b; | ||||
| $pastel-yellow: #ffdd59; | ||||
| $dark-bg: #2d2d2d; | ||||
| $text-color: #ffffff; | ||||
| $tooltip-bg: #202020; | ||||
|  | ||||
| .react-flow { | ||||
|   // background-color: $dark-bg; | ||||
|   color: $text-color; | ||||
|  | ||||
|   &__node { | ||||
|     //cursor: auto; | ||||
|   } | ||||
|  | ||||
|   &__pane { | ||||
|     cursor: auto; | ||||
|   } | ||||
|   //&__edge { | ||||
|   //  stroke: $pastel-pink; | ||||
|   //  stroke-width: 2px; | ||||
|   // | ||||
|   //  &.selected { | ||||
|   //    stroke: $pastel-yellow; | ||||
|   //  } | ||||
|   //} | ||||
|  | ||||
|   &__handle { | ||||
|     //background-color: $pastel-green; | ||||
|     //box-shadow: 0 0 5px rgba($pastel-green, 0.5); | ||||
|   } | ||||
|  | ||||
|   &__minimap { | ||||
|     background-color: rgba(66, 66, 66, 1); | ||||
|     opacity: 0.7; | ||||
|     //backdrop-filter: blur(5px); | ||||
|     border: 1px solid #2f2f2f; | ||||
|     border-radius: 4px; | ||||
|     overflow: hidden; | ||||
|   } | ||||
|  | ||||
|   &__minimap-mask { | ||||
|     fill: rgba(28, 28, 28, 0.75); | ||||
|   } | ||||
|  | ||||
|   &__controls { | ||||
|     filter: brightness(1.5); | ||||
|   } | ||||
|  | ||||
|   &__minimap-node { | ||||
|     fill: #ffb03a; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .context-menu-active { | ||||
|   background-color: rgba(131, 131, 131, 0.33); | ||||
| } | ||||
|  | ||||
| .p-dialog { | ||||
|   .p-dialog-header { | ||||
|     height: 40px; | ||||
|     padding: 1rem; | ||||
|     padding-right: 10px !important; | ||||
|   } | ||||
|  | ||||
|   .p-dialog-title { | ||||
|     font-size: 1rem !important; | ||||
|   } | ||||
|  | ||||
|   .p-dialog-header-icons { | ||||
|     align-self: initial !important; | ||||
|   } | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| $pastel-blue: #5a7d9a; | ||||
| $pastel-pink: #d291bc; | ||||
| $pastel-green: #88b04b; | ||||
| $pastel-yellow: #ffdd59; | ||||
| $dark-bg: #2d2d2d; | ||||
| $text-color: #ffffff; | ||||
| $tooltip-bg: #202020; | ||||
| @@ -0,0 +1,51 @@ | ||||
| @import './eve-common-variables'; | ||||
| @import './eve-common'; | ||||
| @import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap'); | ||||
|  | ||||
| .pathfinder-theme { | ||||
|   --rf-bg-color: #000000; | ||||
|   --rf-soft-bg-color: #282828; | ||||
|  | ||||
|   --rf-node-bg-color: #202020; | ||||
|   --rf-node-soft-bg-color: #313335; | ||||
|   --rf-node-font-weight: bold; | ||||
|   --rf-text-color: #adadad; | ||||
|   --rf-region-name: var(--rf-text-color); | ||||
|   --rf-custom-name: var(--rf-text-color); | ||||
|  | ||||
|   --tooltip-bg: #202020; | ||||
|  | ||||
|   --rf-bg-variant: "lines"; | ||||
|   --rf-bg-gap: 32; | ||||
|   --rf-bg-size: 1; | ||||
|   --rf-bg-pattern-color: #313131; | ||||
|  | ||||
|   --eve-effect-pulsar: #428bca; | ||||
|   --eve-effect-magnetar: #e06fdf; | ||||
|   --eve-effect-wolfRayet: #e28a0d; | ||||
|   --eve-effect-blackHole: #000000; | ||||
|   --eve-effect-cataclysmicVariable: #ffffbb; | ||||
|   --eve-effect-redGiant: #d9534f; | ||||
|  | ||||
|   --eve-wh-type-color-high: #5cb85c; | ||||
|   --eve-wh-type-color-low: #e28a0d; | ||||
|   --eve-wh-type-color-null: #d9534f; | ||||
|   --eve-wh-type-color-c1: #428bca; | ||||
|   --eve-wh-type-color-c2: #428bca; | ||||
|   --eve-wh-type-color-c3: #e28a0d; | ||||
|   --eve-wh-type-color-c4: #e28a0d; | ||||
|   --eve-wh-type-color-c5: #d9534f; | ||||
|   --eve-wh-type-color-c6: #d9534f; | ||||
|   --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; | ||||
|   --rf-node-text-color: var(--pf-text-color); | ||||
|  | ||||
|   --rf-tag-color: #fbbf24; | ||||
|   --rf-has-user-characters: #5cb85c; | ||||
|  | ||||
|   --window-corner: #72716f; | ||||
| } | ||||
| @@ -1,78 +1,25 @@ | ||||
| import 'react-grid-layout/css/styles.css'; | ||||
| import 'react-resizable/css/styles.css'; | ||||
| 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; | ||||
| }; | ||||
| 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'; | ||||
|  | ||||
| export const MapInterface = () => { | ||||
|   const [items, setItems] = useState<WidgetGridItem[]>(restoreWindowsFromLS); | ||||
|   // const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS); | ||||
|   const { windowsSettings, updateWidgetSettings } = useMapRootState(); | ||||
|  | ||||
|   return ( | ||||
|     <WidgetsGrid | ||||
|       items={items} | ||||
|       onChange={x => { | ||||
|         saveWindowsToLS(x); | ||||
|         setItems(x); | ||||
|       }} | ||||
|     /> | ||||
|   ); | ||||
|   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} />; | ||||
| }; | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| .SearchItem { | ||||
|   & > * { | ||||
|     font-size: 13px !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .SearchItemEffect { | ||||
|   font-weight: initial !important; | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,203 @@ | ||||
| import { Dialog } from 'primereact/dialog'; | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { useCallback, useRef, useState } from 'react'; | ||||
| import { Button } from 'primereact/button'; | ||||
| import { IconField } from 'primereact/iconfield'; | ||||
| import { AutoComplete } from 'primereact/autocomplete'; | ||||
| import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types'; | ||||
| import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit'; | ||||
| import classes from './AddSystemDialog.module.scss'; | ||||
|  | ||||
| import clsx from 'clsx'; | ||||
| import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; | ||||
| import { sortWHClasses } from '@/hooks/Mapper/helpers'; | ||||
|  | ||||
| export type SearchOnSubmitCallback = (item: SearchSystemItem) => void; | ||||
|  | ||||
| interface AddSystemDialogProps { | ||||
|   title?: string; | ||||
|   visible: boolean; | ||||
|   setVisible: (visible: boolean) => void; | ||||
|   onSubmit?: SearchOnSubmitCallback; | ||||
|   excludedSystems?: number[]; | ||||
| } | ||||
|  | ||||
| export const AddSystemDialog = ({ | ||||
|   title = 'Add system', | ||||
|   visible, | ||||
|   setVisible, | ||||
|   onSubmit, | ||||
|   excludedSystems = [], | ||||
| }: AddSystemDialogProps) => { | ||||
|   const { | ||||
|     outCommand, | ||||
|     data: { wormholesData }, | ||||
|   } = useMapRootState(); | ||||
|  | ||||
|   const inputRef = useRef<any>(); | ||||
|   const onShow = useCallback(() => { | ||||
|     inputRef.current?.focus(); | ||||
|   }, []); | ||||
|  | ||||
|   const [filteredItems, setFilteredItems] = useState<SearchSystemItem[]>([]); | ||||
|   const [selectedItem, setSelectedItem] = useState<SearchSystemItem[] | null>(null); | ||||
|  | ||||
|   const searchItems = useCallback( | ||||
|     async (event: { query: string }) => { | ||||
|       if (event.query.length < 2) { | ||||
|         setFilteredItems([]); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const query = event.query; | ||||
|  | ||||
|       if (query.length === 0) { | ||||
|         setFilteredItems([]); | ||||
|       } else { | ||||
|         try { | ||||
|           const result = await outCommand({ | ||||
|             type: OutCommand.searchSystems, | ||||
|             data: { | ||||
|               text: query, | ||||
|             }, | ||||
|           }); | ||||
|  | ||||
|           let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => { | ||||
|             const amatch = a.label.indexOf(query); | ||||
|             const bmatch = b.label.indexOf(query); | ||||
|             return amatch - bmatch; | ||||
|           }); | ||||
|  | ||||
|           if (excludedSystems) { | ||||
|             prepared = prepared.filter(x => !excludedSystems.includes(x.system_static_info.solar_system_id)); | ||||
|           } | ||||
|  | ||||
|           setFilteredItems(prepared); | ||||
|         } catch (error) { | ||||
|           console.error('Error fetching data:', error); | ||||
|           setFilteredItems([]); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     [excludedSystems, outCommand], | ||||
|   ); | ||||
|  | ||||
|   const ref = useRef({ onSubmit, selectedItem }); | ||||
|   ref.current = { onSubmit, selectedItem }; | ||||
|  | ||||
|   const handleSubmit = useCallback(() => { | ||||
|     const { onSubmit, selectedItem } = ref.current; | ||||
|     setFilteredItems([]); | ||||
|     setSelectedItem([]); | ||||
|  | ||||
|     if (!selectedItem) { | ||||
|       setVisible(false); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     onSubmit?.(selectedItem[0]); | ||||
|     setVisible(false); | ||||
|   }, [setVisible]); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog | ||||
|       header={title} | ||||
|       visible={visible} | ||||
|       draggable={false} | ||||
|       style={{ width: '520px' }} | ||||
|       onShow={onShow} | ||||
|       onHide={() => { | ||||
|         if (!visible) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         setVisible(false); | ||||
|       }} | ||||
|     > | ||||
|       <div className="flex flex-col gap-3 px-1.5"> | ||||
|         <div className="flex flex-col gap-2 py-3.5"> | ||||
|           <div className="flex flex-col gap-1"> | ||||
|             <IconField> | ||||
|               <AutoComplete | ||||
|                 ref={inputRef} | ||||
|                 multiple | ||||
|                 showEmptyMessage | ||||
|                 scrollHeight="300px" | ||||
|                 value={selectedItem} | ||||
|                 suggestions={filteredItems} | ||||
|                 completeMethod={searchItems} | ||||
|                 onChange={e => { | ||||
|                   setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]); | ||||
|                 }} | ||||
|                 emptyMessage="Not found any system..." | ||||
|                 placeholder="Type here..." | ||||
|                 field="label" | ||||
|                 id="value" | ||||
|                 className="w-full" | ||||
|                 itemTemplate={(item: SearchSystemItem) => { | ||||
|                   const { security, system_class, effect_power, effect_name, statics } = item.system_static_info; | ||||
|                   const sortedStatics = sortWHClasses(wormholesData, statics); | ||||
|                   const isWH = isWormholeSpace(system_class); | ||||
|  | ||||
|                   return ( | ||||
|                     <div className={clsx('flex gap-1.5', classes.SearchItem)}> | ||||
|                       <SystemViewStandalone | ||||
|                         security={security} | ||||
|                         system_class={system_class} | ||||
|                         solar_system_id={item.value} | ||||
|                         class_title={item.class_title} | ||||
|                         solar_system_name={item.label} | ||||
|                         region_name={item.region_name} | ||||
|                       /> | ||||
|  | ||||
|                       {effect_name && isWH && ( | ||||
|                         <WHEffectView | ||||
|                           effectName={effect_name} | ||||
|                           effectPower={effect_power} | ||||
|                           className={classes.SearchItemEffect} | ||||
|                         /> | ||||
|                       )} | ||||
|  | ||||
|                       {isWH && ( | ||||
|                         <div className="flex gap-1 grow justify-between"> | ||||
|                           <div></div> | ||||
|                           <div className="flex gap-1"> | ||||
|                             {sortedStatics.map(x => ( | ||||
|                               <WHClassView key={x} whClassName={x} /> | ||||
|                             ))} | ||||
|                           </div> | ||||
|                         </div> | ||||
|                       )} | ||||
|                     </div> | ||||
|                   ); | ||||
|                 }} | ||||
|                 selectedItemTemplate={(item: SearchSystemItem) => ( | ||||
|                   <SystemViewStandalone | ||||
|                     security={item.system_static_info.security} | ||||
|                     system_class={item.system_static_info.system_class} | ||||
|                     solar_system_id={item.value} | ||||
|                     class_title={item.class_title} | ||||
|                     solar_system_name={item.label} | ||||
|                     region_name={item.region_name} | ||||
|                   /> | ||||
|                 )} | ||||
|               /> | ||||
|             </IconField> | ||||
|  | ||||
|             <span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div className="flex gap-2 justify-end"> | ||||
|           <Button | ||||
|             onClick={handleSubmit} | ||||
|             outlined | ||||
|             disabled={!selectedItem || selectedItem.length !== 1} | ||||
|             size="small" | ||||
|             label="Submit" | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1 @@ | ||||
| export * from './AddSystemDialog'; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { useCallback, useRef } from 'react'; | ||||
| import { useCallback, useEffect, 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={false} | ||||
|       draggable={true} | ||||
|       style={{ width: '500px' }} | ||||
|       onHide={handleHide} | ||||
|       contentClassName="!p-0" | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { InputTextarea } from 'primereact/inputtextarea'; | ||||
| import { Dialog } from 'primereact/dialog'; | ||||
| import { getSystemById } from '@/hooks/Mapper/helpers'; | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api'; | ||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | ||||
| import { Button } from 'primereact/button'; | ||||
| import { OutCommand } from '@/hooks/Mapper/types'; | ||||
| @@ -22,30 +23,21 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe | ||||
|     outCommand, | ||||
|   } = useMapRootState(); | ||||
|  | ||||
|   const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true'; | ||||
|  | ||||
|   const system = getSystemById(systems, systemId); | ||||
|  | ||||
|   const [name, setName] = useState(''); | ||||
|   const [label, setLabel] = useState(''); | ||||
|   const [temporaryName, setTemporaryName] = useState(''); | ||||
|   const [description, setDescription] = useState(''); | ||||
|   const inputRef = useRef<HTMLInputElement>(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!system) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const labels = new LabelsManager(system.labels || ''); | ||||
|  | ||||
|     setName(system.name || ''); | ||||
|     setLabel(labels.customLabel); | ||||
|     setDescription(system.description || ''); | ||||
|   }, [system]); | ||||
|  | ||||
|   const ref = useRef({ name, description, label, outCommand, systemId, system }); | ||||
|   ref.current = { name, description, label, outCommand, systemId, system }; | ||||
|   const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system }); | ||||
|   ref.current = { name, description, label, temporaryName, outCommand, systemId, system }; | ||||
|  | ||||
|   const handleSave = useCallback(() => { | ||||
|     const { name, description, label, outCommand, systemId, system } = ref.current; | ||||
|     const { name, description, label, temporaryName, outCommand, systemId, system } = ref.current; | ||||
|  | ||||
|     const outLabel = new LabelsManager(system?.labels ?? ''); | ||||
|     outLabel.updateCustomLabel(label); | ||||
| @@ -58,6 +50,14 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     outCommand({ | ||||
|       type: OutCommand.updateSystemTemporaryName, | ||||
|       data: { | ||||
|         system_id: systemId, | ||||
|         value: temporaryName, | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     outCommand({ | ||||
|       type: OutCommand.updateSystemName, | ||||
|       data: { | ||||
| @@ -93,6 +93,21 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe | ||||
|     e.target.value = e.target.value.toUpperCase().replace(/[^A-Z0-9\-[\](){}]/g, ''); | ||||
|   }, []); | ||||
|  | ||||
|   // Attention: this effect should be call only on mount. | ||||
|   useEffect(() => { | ||||
|     const { system } = ref.current; | ||||
|     if (!system) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const labels = new LabelsManager(system.labels || ''); | ||||
|  | ||||
|     setName(system.name || ''); | ||||
|     setLabel(labels.customLabel); | ||||
|     setTemporaryName(system.temporary_name || ''); | ||||
|     setDescription(system.description || ''); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog | ||||
|       header="System settings" | ||||
| @@ -167,6 +182,35 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe | ||||
|               </IconField> | ||||
|             </div> | ||||
|  | ||||
|             {isTempSystemNameEnabled && ( | ||||
|               <div className="flex flex-col gap-1"> | ||||
|                 <label htmlFor="username">Temporary Name</label> | ||||
|  | ||||
|                 <IconField> | ||||
|                   {temporaryName !== '' && ( | ||||
|                     <WdImgButton | ||||
|                       className="pi pi-trash text-red-400" | ||||
|                       textSize={WdImageSize.large} | ||||
|                       tooltip={{ | ||||
|                         content: 'Remove temporary name', | ||||
|                         className: 'pi p-input-icon', | ||||
|                         position: TooltipPosition.top, | ||||
|                       }} | ||||
|                       onClick={() => setTemporaryName('')} | ||||
|                     /> | ||||
|                   )} | ||||
|                   <InputText | ||||
|                     id="temporaryName" | ||||
|                     aria-describedby="temporaryName" | ||||
|                     autoComplete="off" | ||||
|                     value={temporaryName} | ||||
|                     maxLength={10} | ||||
|                     onChange={e => setTemporaryName(e.target.value)} | ||||
|                   /> | ||||
|                 </IconField> | ||||
|               </div> | ||||
|             )} | ||||
|  | ||||
|             <div className="flex flex-col gap-1"> | ||||
|               <label htmlFor="username">Description</label> | ||||
|               <InputTextarea | ||||
|   | ||||
| @@ -1,37 +0,0 @@ | ||||
| .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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -1,196 +0,0 @@ | ||||
| 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -1 +0,0 @@ | ||||
| export * from './WidgetsGrid'; | ||||
| @@ -1,5 +1,4 @@ | ||||
| export * from './Widget'; | ||||
| export * from './WidgetsGrid'; | ||||
| export * from './SystemSettingsDialog'; | ||||
| export * from './SystemCustomLabelDialog'; | ||||
| export * from './SystemLinkSignatureDialog'; | ||||
|   | ||||
							
								
								
									
										92
									
								
								assets/js/hooks/Mapper/components/mapInterface/constants.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								assets/js/hooks/Mapper/components/mapInterface/constants.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| 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,4 +1,4 @@ | ||||
| import { useCallback, useMemo } from 'react'; | ||||
| import { useCallback, useMemo, useRef } from 'react'; | ||||
| import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller'; | ||||
| @@ -10,6 +10,8 @@ import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/s | ||||
| import useLocalStorageState from 'use-local-storage-state'; | ||||
| import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api'; | ||||
| import { UserPermission } from '@/hooks/Mapper/types/permissions.ts'; | ||||
| import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts'; | ||||
| import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; | ||||
|  | ||||
| type CharItemProps = { | ||||
|   compact: boolean; | ||||
| @@ -92,21 +94,26 @@ export const LocalCharacters = () => { | ||||
|   const isNotSelectedSystem = selectedSystems.length !== 1; | ||||
|   const showList = sorted.length > 0 && selectedSystems.length === 1; | ||||
|  | ||||
|   const ref = useRef<HTMLDivElement>(null); | ||||
|   const compact = useMaxWidth(ref, 145); | ||||
|  | ||||
|   return ( | ||||
|     <Widget | ||||
|       label={ | ||||
|         <div className="flex justify-between items-center text-xs w-full"> | ||||
|         <div className="flex justify-between items-center text-xs w-full" ref={ref}> | ||||
|           <span className="select-none">Local{showList ? ` [${sorted.length}]` : ''}</span> | ||||
|           <LayoutEventBlocker className="flex items-center gap-2"> | ||||
|             {showOffline && ( | ||||
|               <WdCheckbox | ||||
|                 size="xs" | ||||
|                 labelSide="left" | ||||
|                 label={'Show offline'} | ||||
|                 value={settings.showOffline} | ||||
|                 classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300" | ||||
|                 onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))} | ||||
|               /> | ||||
|               <WdTooltipWrapper content="Show offline characters in system"> | ||||
|                 <WdCheckbox | ||||
|                   size="xs" | ||||
|                   labelSide="left" | ||||
|                   label={compact ? '' : 'Show offline'} | ||||
|                   value={settings.showOffline} | ||||
|                   classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300" | ||||
|                   onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))} | ||||
|                 /> | ||||
|               </WdTooltipWrapper> | ||||
|             )} | ||||
|  | ||||
|             <span | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
| .RouteSystem { | ||||
|   width: 8px; | ||||
|   height: 8px; | ||||
|   background: #ffffff; | ||||
|  | ||||
|   cursor: pointer; | ||||
|   transition: opacity 200ms; | ||||
|   | ||||
| @@ -19,6 +19,13 @@ import { PrimeIcons } from 'primereact/api'; | ||||
| import { RoutesSettingsDialog } from './RoutesSettingsDialog'; | ||||
| import { RoutesProvider, useRouteProvider } from './RoutesProvider.tsx'; | ||||
| import { ContextMenuSystemInfo, useContextMenuSystemInfoHandlers } from '@/hooks/Mapper/components/contexts'; | ||||
| import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts'; | ||||
| import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; | ||||
| import { | ||||
|   AddSystemDialog, | ||||
|   SearchOnSubmitCallback, | ||||
| } from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog'; | ||||
| import { OutCommand } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| const sortByDist = (a: Route, b: Route) => { | ||||
|   const distA = a.has_connection ? a.systems?.length || 0 : Infinity; | ||||
| @@ -161,6 +168,12 @@ export const RoutesWidgetContent = () => { | ||||
| export const RoutesWidgetComp = () => { | ||||
|   const [routeSettingsVisible, setRouteSettingsVisible] = useState(false); | ||||
|   const { data, update } = useRouteProvider(); | ||||
|   const { | ||||
|     data: { hubs = [] }, | ||||
|     outCommand, | ||||
|   } = useMapRootState(); | ||||
|  | ||||
|   const preparedHubs = useMemo(() => hubs.map(x => parseInt(x)), [hubs]); | ||||
|  | ||||
|   const isSecure = data.path_type === 'secure'; | ||||
|   const handleSecureChange = useCallback(() => { | ||||
| @@ -170,27 +183,70 @@ export const RoutesWidgetComp = () => { | ||||
|     }); | ||||
|   }, [data, update]); | ||||
|  | ||||
|   const ref = useRef<HTMLDivElement>(null); | ||||
|   const compact = useMaxWidth(ref, 155); | ||||
|   const [openAddSystem, setOpenAddSystem] = useState<boolean>(false); | ||||
|  | ||||
|   const onAddSystem = useCallback(() => setOpenAddSystem(true), []); | ||||
|  | ||||
|   const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback( | ||||
|     async item => { | ||||
|       if (preparedHubs.includes(item.value)) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       await outCommand({ | ||||
|         type: OutCommand.addHub, | ||||
|         data: { system_id: item.value }, | ||||
|       }); | ||||
|     }, | ||||
|     [hubs, outCommand], | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Widget | ||||
|       label={ | ||||
|         <div className="flex justify-between items-center text-xs w-full"> | ||||
|         <div className="flex justify-between items-center text-xs w-full" ref={ref}> | ||||
|           <span className="select-none">Routes</span> | ||||
|           <LayoutEventBlocker className="flex items-center gap-2"> | ||||
|             <WdCheckbox | ||||
|               size="xs" | ||||
|               labelSide="left" | ||||
|               label={'Show shortest'} | ||||
|               value={!isSecure} | ||||
|               onChange={handleSecureChange} | ||||
|               classNameLabel={clsx('text-red-400')} | ||||
|             <WdImgButton | ||||
|               className={PrimeIcons.PLUS_CIRCLE} | ||||
|               onClick={onAddSystem} | ||||
|               tooltip={{ | ||||
|                 content: 'Click here to add new system to routes', | ||||
|               }} | ||||
|             /> | ||||
|  | ||||
|             <WdTooltipWrapper content="Show shortest route"> | ||||
|               <WdCheckbox | ||||
|                 size="xs" | ||||
|                 labelSide="left" | ||||
|                 label={compact ? '' : 'Show shortest'} | ||||
|                 value={!isSecure} | ||||
|                 onChange={handleSecureChange} | ||||
|                 classNameLabel={clsx('text-red-400')} | ||||
|               /> | ||||
|             </WdTooltipWrapper> | ||||
|             <WdImgButton | ||||
|               className={PrimeIcons.SLIDERS_H} | ||||
|               onClick={() => setRouteSettingsVisible(true)} | ||||
|               tooltip={{ | ||||
|                 content: 'Click here to open Routes settings', | ||||
|               }} | ||||
|             /> | ||||
|             <WdImgButton className={PrimeIcons.SLIDERS_H} onClick={() => setRouteSettingsVisible(true)} /> | ||||
|           </LayoutEventBlocker> | ||||
|         </div> | ||||
|       } | ||||
|     > | ||||
|       <RoutesWidgetContent /> | ||||
|       <RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} /> | ||||
|  | ||||
|       <AddSystemDialog | ||||
|         title="Add system to routes" | ||||
|         visible={openAddSystem} | ||||
|         setVisible={() => setOpenAddSystem(false)} | ||||
|         onSubmit={handleSubmitAddSystem} | ||||
|       /> | ||||
|     </Widget> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -2,30 +2,33 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; | ||||
| import { | ||||
|   InfoDrawer, | ||||
|   LayoutEventBlocker, | ||||
|   SystemView, | ||||
|   TooltipPosition, | ||||
|   WdImgButton, | ||||
|   WdCheckbox, | ||||
|   WdImgButton, | ||||
| } from '@/hooks/Mapper/components/ui-kit'; | ||||
| import { SystemSignaturesContent } from './SystemSignaturesContent'; | ||||
| import { | ||||
|   Setting, | ||||
|   SystemSignatureSettingsDialog, | ||||
|   COSMIC_SIGNATURE, | ||||
|   COSMIC_ANOMALY, | ||||
|   COSMIC_SIGNATURE, | ||||
|   DEPLOYABLE, | ||||
|   STRUCTURE, | ||||
|   STARBASE, | ||||
|   SHIP, | ||||
|   DRONE, | ||||
|   Setting, | ||||
|   SHIP, | ||||
|   STARBASE, | ||||
|   STRUCTURE, | ||||
|   SystemSignatureSettingsDialog, | ||||
| } from './SystemSignatureSettingsDialog'; | ||||
| import { SignatureGroup } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| import React, { useCallback, useEffect, useState, useMemo } from 'react'; | ||||
| import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||||
|  | ||||
| import { PrimeIcons } from 'primereact/api'; | ||||
|  | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { CheckboxChangeEvent } from 'primereact/checkbox'; | ||||
| import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts'; | ||||
| import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; | ||||
|  | ||||
| const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_2'; | ||||
| export const SHOW_DESCRIPTION_COLUMN_SETTING = 'show_description_column_setting'; | ||||
| @@ -96,21 +99,33 @@ export const SystemSignatures = () => { | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const ref = useRef<HTMLDivElement>(null); | ||||
|   const compact = useMaxWidth(ref, 260); | ||||
|  | ||||
|   return ( | ||||
|     <Widget | ||||
|       label={ | ||||
|         <div className="flex justify-between items-center text-xs w-full h-full"> | ||||
|           <div className="flex gap-1">System Signatures</div> | ||||
|         <div className="flex justify-between items-center text-xs w-full h-full" ref={ref}> | ||||
|           <div className="flex justify-between items-center gap-1"> | ||||
|             {!compact && ( | ||||
|               <div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400"> | ||||
|                 Signatures {isNotSelectedSystem ? '' : 'in'} | ||||
|               </div> | ||||
|             )} | ||||
|             {!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />} | ||||
|           </div> | ||||
|  | ||||
|           <LayoutEventBlocker className="flex gap-2.5"> | ||||
|             <WdCheckbox | ||||
|               size="xs" | ||||
|               labelSide="left" | ||||
|               label={'Lazy delete'} | ||||
|               value={lazyDeleteValue} | ||||
|               classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300" | ||||
|               onChange={(event: CheckboxChangeEvent) => handleLazyDeleteChange(!!event.checked)} | ||||
|             /> | ||||
|             <WdTooltipWrapper content="Enable Lazy delete"> | ||||
|               <WdCheckbox | ||||
|                 size="xs" | ||||
|                 labelSide="left" | ||||
|                 label={compact ? '' : 'Lazy delete'} | ||||
|                 value={lazyDeleteValue} | ||||
|                 classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300 whitespace-nowrap text-ellipsis overflow-hidden" | ||||
|                 onChange={(event: CheckboxChangeEvent) => handleLazyDeleteChange(!!event.checked)} | ||||
|               /> | ||||
|             </WdTooltipWrapper> | ||||
|  | ||||
|             <WdImgButton | ||||
|               className={PrimeIcons.QUESTION_CIRCLE} | ||||
|   | ||||
| @@ -2,7 +2,10 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { parseSignatures } from '@/hooks/Mapper/helpers'; | ||||
| import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit'; | ||||
| import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; | ||||
| import { | ||||
|   getGroupIdByRawGroup, | ||||
|   GROUPS_LIST, | ||||
| } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; | ||||
|  | ||||
| import { DataTable, DataTableRowClickEvent, DataTableRowMouseEvent, SortOrder } from 'primereact/datatable'; | ||||
| import { Column } from 'primereact/column'; | ||||
| @@ -122,13 +125,14 @@ export const SystemSignaturesContent = ({ | ||||
|         } | ||||
|  | ||||
|         const isCosmicSignature = x.kind === COSMIC_SIGNATURE; | ||||
|         const preparedGroup = getGroupIdByRawGroup(x.group); | ||||
|  | ||||
|         if (isCosmicSignature) { | ||||
|           const showCosmicSignatures = settings.find(y => y.key === COSMIC_SIGNATURE)?.value; | ||||
|           if (showCosmicSignatures) { | ||||
|             return !x.group || groupSettings.find(y => y.key === x.group)?.value; | ||||
|             return !x.group || groupSettings.find(y => y.key === preparedGroup)?.value; | ||||
|           } else { | ||||
|             return !!x.group && groupSettings.find(y => y.key === x.group)?.value; | ||||
|             return !!x.group && groupSettings.find(y => y.key === preparedGroup)?.value; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,12 @@ | ||||
| import { GroupType, SignatureGroup } from '@/hooks/Mapper/types'; | ||||
| import { | ||||
|   GroupType, | ||||
|   SignatureGroup, | ||||
|   SignatureGroupENG, | ||||
|   SignatureGroupRU, | ||||
|   SignatureKind, | ||||
|   SignatureKindENG, | ||||
|   SignatureKindRU, | ||||
| } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| export const TIME_ONE_MINUTE = 1000 * 60; | ||||
| export const TIME_TEN_MINUTES = 1000 * 60 * 10; | ||||
| @@ -24,3 +32,43 @@ export const GROUPS: Record<SignatureGroup, GroupType> = { | ||||
|   [SignatureGroup.Wormhole]: { id: SignatureGroup.Wormhole, icon: '/icons/brackets/wormhole.png', ...wh }, | ||||
|   [SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 }, | ||||
| }; | ||||
|  | ||||
| export const MAPPING_GROUP_TO_ENG = { | ||||
|   // ENGLISH | ||||
|   [SignatureGroupENG.GasSite]: SignatureGroup.GasSite, | ||||
|   [SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite, | ||||
|   [SignatureGroupENG.DataSite]: SignatureGroup.DataSite, | ||||
|   [SignatureGroupENG.OreSite]: SignatureGroup.OreSite, | ||||
|   [SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite, | ||||
|   [SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole, | ||||
|   [SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature, | ||||
|  | ||||
|   // RUSSIAN | ||||
|   [SignatureGroupRU.GasSite]: SignatureGroup.GasSite, | ||||
|   [SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite, | ||||
|   [SignatureGroupRU.DataSite]: SignatureGroup.DataSite, | ||||
|   [SignatureGroupRU.OreSite]: SignatureGroup.OreSite, | ||||
|   [SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite, | ||||
|   [SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole, | ||||
|   [SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature, | ||||
| }; | ||||
|  | ||||
| export const MAPPING_TYPE_TO_ENG = { | ||||
|   // ENGLISH | ||||
|   [SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature, | ||||
|   [SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly, | ||||
|   [SignatureKindENG.Structure]: SignatureKind.Structure, | ||||
|   [SignatureKindENG.Ship]: SignatureKind.Ship, | ||||
|   [SignatureKindENG.Deployable]: SignatureKind.Deployable, | ||||
|   [SignatureKindENG.Drone]: SignatureKind.Drone, | ||||
|  | ||||
|   // RUSSIAN | ||||
|   [SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature, | ||||
|   [SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly, | ||||
|   [SignatureKindRU.Structure]: SignatureKind.Structure, | ||||
|   [SignatureKindRU.Ship]: SignatureKind.Ship, | ||||
|   [SignatureKindRU.Deployable]: SignatureKind.Deployable, | ||||
|   [SignatureKindRU.Drone]: SignatureKind.Drone, | ||||
| }; | ||||
|  | ||||
| export const getGroupIdByRawGroup = (val: string) => MAPPING_GROUP_TO_ENG[val as SignatureGroup]; | ||||
|   | ||||
| @@ -0,0 +1,113 @@ | ||||
| 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 press Ctrl+C, | ||||
|                     <br /> | ||||
|                     then click on this widget and press Ctrl+V | ||||
|                   </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 use the | ||||
|                     <span className="text-blue-500"> blue </span> | ||||
|                     add timer 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,18 @@ | ||||
|   .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; | ||||
|   } | ||||
|    | ||||
| @@ -0,0 +1,118 @@ | ||||
| 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,31 @@ | ||||
|  | ||||
|    .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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -0,0 +1,216 @@ | ||||
| import React, { useEffect, useState, useCallback } from 'react'; | ||||
| import { Dialog } from 'primereact/dialog'; | ||||
| import { Button } from 'primereact/button'; | ||||
| import { AutoComplete } from 'primereact/autocomplete'; | ||||
| 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) => { | ||||
|     // 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; | ||||
|       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 | ||||
|       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>End Time:</span> | ||||
|             <input | ||||
|               type="datetime-local" | ||||
|               className="p-inputtext p-component" | ||||
|               value={editData.endTime ? editData.endTime.replace('Z', '').slice(0, 16) : ''} | ||||
|               onChange={e => handleChange('endTime', e.target.value)} | ||||
|             /> | ||||
|           </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> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,4 @@ | ||||
| export * from './parserHelper'; | ||||
| export * from './pasteParser'; | ||||
| export * from './structureTypes'; | ||||
| export * from './structureUtils'; | ||||
| @@ -0,0 +1,92 @@ | ||||
| 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, | ||||
|   }; | ||||
| } | ||||
| @@ -0,0 +1,56 @@ | ||||
| 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]; | ||||
| } | ||||
| @@ -0,0 +1,32 @@ | ||||
| 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", | ||||
| }; | ||||
| @@ -0,0 +1,59 @@ | ||||
| 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; | ||||
| } | ||||
| @@ -0,0 +1,85 @@ | ||||
| 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 }; | ||||
| } | ||||
| @@ -0,0 +1 @@ | ||||
| export * from './SystemStructures'; | ||||
| @@ -0,0 +1,50 @@ | ||||
| // 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); | ||||
| @@ -0,0 +1,36 @@ | ||||
| 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,3 +2,4 @@ export * from './LocalCharacters'; | ||||
| export * from './SystemInfo'; | ||||
| export * from './RoutesWidget'; | ||||
| export * from './SystemSignatures'; | ||||
| export * from './SystemStructures'; | ||||
|   | ||||
| @@ -16,6 +16,8 @@ export const MapRootContent = ({}: MapRootContentProps) => { | ||||
|   const { interfaceSettings } = useMapRootState(); | ||||
|   const { isShowMenu } = interfaceSettings; | ||||
|  | ||||
|   const themeClass = `${interfaceSettings.theme ?? 'default'}-theme`; | ||||
|  | ||||
|   const [showOnTheMap, setShowOnTheMap] = useState(false); | ||||
|   const [showMapSettings, setShowMapSettings] = useState(false); | ||||
|   const mapInterface = <MapInterface />; | ||||
| @@ -26,27 +28,29 @@ export const MapRootContent = ({}: MapRootContentProps) => { | ||||
|   useSkipContextMenu(); | ||||
|  | ||||
|   return ( | ||||
|     <Layout map={<MapWrapper />}> | ||||
|       {!isShowMenu ? ( | ||||
|         <div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none"> | ||||
|           <div className="absolute top-0 left-0 w-[calc(100%-3.5rem)] h-full pointer-events-none"> | ||||
|             <Topbar /> | ||||
|     <div className={themeClass}> | ||||
|       <Layout map={<MapWrapper />}> | ||||
|         {!isShowMenu ? ( | ||||
|           <div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none"> | ||||
|             <div className="absolute top-0 left-0 w-[calc(100%-3.5rem)] h-full pointer-events-none"> | ||||
|               <Topbar /> | ||||
|               {mapInterface} | ||||
|             </div> | ||||
|             <div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto"> | ||||
|               <RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} /> | ||||
|             </div> | ||||
|           </div> | ||||
|         ) : ( | ||||
|           <div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none"> | ||||
|             <Topbar> | ||||
|               <MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} /> | ||||
|             </Topbar> | ||||
|             {mapInterface} | ||||
|           </div> | ||||
|           <div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto"> | ||||
|             <RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} /> | ||||
|           </div> | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none"> | ||||
|           <Topbar> | ||||
|             <MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} /> | ||||
|           </Topbar> | ||||
|           {mapInterface} | ||||
|         </div> | ||||
|       )} | ||||
|       <OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} /> | ||||
|       <MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} /> | ||||
|     </Layout> | ||||
|         )} | ||||
|         <OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} /> | ||||
|         <MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} /> | ||||
|       </Layout> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
|     .p-tabview-panels { | ||||
|       padding: 6px 1rem !important; | ||||
|       flex-grow: 1; | ||||
|       height: 100%; | ||||
|     } | ||||
|  | ||||
|     .p-tabview-nav-container { | ||||
|   | ||||
| @@ -3,8 +3,10 @@ import { Dialog } from 'primereact/dialog'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | ||||
| import { TabPanel, TabView } from 'primereact/tabview'; | ||||
| import { PrettySwitchbox } from './components'; | ||||
| import { InterfaceStoredSettings, InterfaceStoredSettingsProps, useMapRootState } 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', | ||||
| @@ -37,43 +39,101 @@ export interface MapSettingsProps { | ||||
|   onHide: () => void; | ||||
| } | ||||
|  | ||||
| type CheckboxesList = { | ||||
| type SettingsListItem = { | ||||
|   prop: keyof UserSettings; | ||||
|   label: string; | ||||
| }[]; | ||||
|   type: 'checkbox' | 'dropdown'; | ||||
|   options?: { label: string; value: string }[]; | ||||
| }; | ||||
|  | ||||
| const COMMON_CHECKBOXES_PROPS: CheckboxesList = [ | ||||
|   { prop: InterfaceStoredSettingsProps.isShowMinimap, label: 'Show Minimap' }, | ||||
| const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [ | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isShowMinimap, | ||||
|     label: 'Show Minimap', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const SYSTEMS_CHECKBOXES_PROPS: CheckboxesList = [ | ||||
|   { prop: InterfaceStoredSettingsProps.isShowKSpace, label: 'Highlight Low/High-security systems' }, | ||||
|   { prop: UserSettingsRemoteProps.select_on_spash, label: 'Auto-select splashed' }, | ||||
| const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [ | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isShowKSpace, | ||||
|     label: 'Highlight Low/High-security systems', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
|   { | ||||
|     prop: UserSettingsRemoteProps.select_on_spash, | ||||
|     label: 'Auto-select splashed', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const SIGNATURES_CHECKBOXES_PROPS: CheckboxesList = [ | ||||
|   { prop: UserSettingsRemoteProps.link_signature_on_splash, label: 'Link signature on splash' }, | ||||
|   { prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures, label: 'Show unsplashed signatures' }, | ||||
| const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [ | ||||
|   { | ||||
|     prop: UserSettingsRemoteProps.link_signature_on_splash, | ||||
|     label: 'Link signature on splash', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures, | ||||
|     label: 'Show unsplashed signatures', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const CONNECTIONS_CHECKBOXES_PROPS: CheckboxesList = [ | ||||
|   { prop: UserSettingsRemoteProps.delete_connection_with_sigs, label: 'Delete connections to linked signatures' }, | ||||
| const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [ | ||||
|   { | ||||
|     prop: UserSettingsRemoteProps.delete_connection_with_sigs, | ||||
|     label: 'Delete connections to linked signatures', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isThickConnections, | ||||
|     label: 'Thicker connections', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const UI_CHECKBOXES_PROPS: CheckboxesList = [ | ||||
|   { prop: InterfaceStoredSettingsProps.isShowMenu, label: 'Enable compact map menu bar' }, | ||||
|   { prop: InterfaceStoredSettingsProps.isThickConnections, label: 'Thicker connections' }, | ||||
| const UI_CHECKBOXES_PROPS: SettingsListItem[] = [ | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isShowMenu, | ||||
|     label: 'Enable compact map menu bar', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isShowBackgroundPattern, | ||||
|     label: 'Show background pattern', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
|   { | ||||
|     prop: InterfaceStoredSettingsProps.isSoftBackground, | ||||
|     label: 'Enable soft background', | ||||
|     type: 'checkbox', | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const THEME_OPTIONS = [ | ||||
|   { label: 'Default', value: 'default' }, | ||||
|   { label: 'Pathfinder', value: 'pathfinder' }, | ||||
| ]; | ||||
|  | ||||
| const THEME_SETTING: SettingsListItem = { | ||||
|   prop: 'theme', | ||||
|   label: 'Theme', | ||||
|   type: 'dropdown', | ||||
|   options: THEME_OPTIONS, | ||||
| }; | ||||
|  | ||||
| export const MapSettings = ({ show, onHide }: MapSettingsProps) => { | ||||
|   const [activeIndex, setActiveIndex] = useState(0); | ||||
|   const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState(); | ||||
|   const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({ ...DEFAULT_REMOTE_SETTINGS }); | ||||
|   const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({ | ||||
|     ...DEFAULT_REMOTE_SETTINGS, | ||||
|   }); | ||||
|  | ||||
|   const mergedSettings = useMemo(() => { | ||||
|     return { | ||||
|       ...interfaceSettings, | ||||
|       ...userRemoteSettings, | ||||
|       ...interfaceSettings, | ||||
|     }; | ||||
|   }, [userRemoteSettings, interfaceSettings]); | ||||
|  | ||||
| @@ -82,49 +142,67 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => { | ||||
|       type: OutCommand.getUserSettings, | ||||
|       data: null, | ||||
|     }); | ||||
|  | ||||
|     setUserRemoteSettings({ | ||||
|       ...user_settings, | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleChangeChecked = useCallback( | ||||
|     (prop: keyof UserSettings) => async (checked: boolean) => { | ||||
|       // @ts-ignore | ||||
|       if (UserSettingsRemoteList.includes(prop)) { | ||||
|   const handleSettingChange = useCallback( | ||||
|     async (prop: keyof UserSettings, value: boolean | string) => { | ||||
|       if (UserSettingsRemoteList.includes(prop as any)) { | ||||
|         const newRemoteSettings = { | ||||
|           ...userRemoteSettings, | ||||
|           [prop]: checked, | ||||
|           [prop]: value, | ||||
|         }; | ||||
|  | ||||
|         await outCommand({ | ||||
|           type: OutCommand.updateUserSettings, | ||||
|           data: newRemoteSettings, | ||||
|         }); | ||||
|  | ||||
|         setUserRemoteSettings(newRemoteSettings); | ||||
|         return; | ||||
|       } else { | ||||
|         setInterfaceSettings({ | ||||
|           ...interfaceSettings, | ||||
|           [prop]: value, | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       setInterfaceSettings({ | ||||
|         ...interfaceSettings, | ||||
|         [prop]: checked, | ||||
|       }); | ||||
|     }, | ||||
|     [interfaceSettings, outCommand, setInterfaceSettings, userRemoteSettings], | ||||
|     [userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings], | ||||
|   ); | ||||
|  | ||||
|   const renderCheckboxesList = (list: CheckboxesList) => { | ||||
|     return list.map(x => { | ||||
|   const renderSettingItem = (item: SettingsListItem) => { | ||||
|     const currentValue = mergedSettings[item.prop]; | ||||
|  | ||||
|     if (item.type === 'checkbox') { | ||||
|       return ( | ||||
|         <PrettySwitchbox | ||||
|           key={x.prop} | ||||
|           label={x.label} | ||||
|           checked={mergedSettings[x.prop]} | ||||
|           setChecked={handleChangeChecked(x.prop)} | ||||
|           key={item.prop} | ||||
|           label={item.label} | ||||
|           checked={!!currentValue} | ||||
|           setChecked={checked => handleSettingChange(item.prop, checked)} | ||||
|         /> | ||||
|       ); | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     if (item.type === 'dropdown' && item.options) { | ||||
|       return ( | ||||
|         <div key={item.prop} className="flex items-center gap-2 mt-2"> | ||||
|           <label className="text-sm">{item.label}:</label> | ||||
|           <Dropdown | ||||
|             className="text-sm" | ||||
|             value={currentValue} | ||||
|             options={item.options} | ||||
|             onChange={e => handleSettingChange(item.prop, e.value)} | ||||
|             placeholder="Select a theme" | ||||
|           /> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   }; | ||||
|  | ||||
|   const renderSettingsList = (list: SettingsListItem[]) => { | ||||
|     return list.map(renderSettingItem); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -135,10 +213,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => { | ||||
|       style={{ width: '550px' }} | ||||
|       onShow={handleShow} | ||||
|       onHide={() => { | ||||
|         if (!show) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (!show) return; | ||||
|         setActiveIndex(0); | ||||
|         onHide(); | ||||
|       }} | ||||
| @@ -146,27 +221,33 @@ 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)} | ||||
|               className={styles.verticalTabView} | ||||
|             > | ||||
|             <TabView activeIndex={activeIndex} onTabChange={e => setActiveIndex(e.index)}> | ||||
|               <TabPanel header="Common" headerClassName={styles.verticalTabHeader}> | ||||
|                 <div className="w-full h-full flex flex-col gap-1">{renderCheckboxesList(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"> | ||||
|                   {renderCheckboxesList(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}> | ||||
|                 {renderCheckboxesList(CONNECTIONS_CHECKBOXES_PROPS)} | ||||
|                 {renderSettingsList(CONNECTIONS_CHECKBOXES_PROPS)} | ||||
|               </TabPanel> | ||||
|  | ||||
|               <TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}> | ||||
|                 {renderCheckboxesList(SIGNATURES_CHECKBOXES_PROPS)} | ||||
|                 {renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)} | ||||
|               </TabPanel> | ||||
|  | ||||
|               <TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}> | ||||
|                 {renderCheckboxesList(UI_CHECKBOXES_PROPS)} | ||||
|                 {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> | ||||
|             </TabView> | ||||
|           </div> | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -17,7 +17,17 @@ const getPossibleWormholes = (systemStatic: SolarSystemStaticInfoRaw, wormholes: | ||||
|  | ||||
|   // @ts-ignore | ||||
|   const spawnClassGroup = SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS[whType]; | ||||
|   const possibleWHTypes = wormholes.filter(x => x.src.includes(spawnClassGroup)); | ||||
|   const possibleWHTypes = wormholes.filter(x => { | ||||
|     return x.src.some(x => { | ||||
|       const [group, type] = x.split('-'); | ||||
|  | ||||
|       if (type === 'shattered') { | ||||
|         return systemStatic.is_shattered && group === spawnClassGroup; | ||||
|       } | ||||
|  | ||||
|       return group === spawnClassGroup; | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|     statics: possibleWHTypes | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { Map } from '@/hooks/Mapper/components/map/Map.tsx'; | ||||
| import { useCallback, useRef, useState } from 'react'; | ||||
| import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types'; | ||||
| import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts'; | ||||
| import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts'; | ||||
| import isEqual from 'lodash.isequal'; | ||||
| import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts'; | ||||
| import { | ||||
| @@ -14,14 +14,18 @@ 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 { Node } from 'reactflow'; | ||||
| import { Node, XYPosition } from 'reactflow'; | ||||
|  | ||||
| import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| import { useMapEventListener } from '@/hooks/Mapper/events'; | ||||
| import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events'; | ||||
|  | ||||
| import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider'; | ||||
| import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks'; | ||||
| import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts'; | ||||
| import { | ||||
|   AddSystemDialog, | ||||
|   SearchOnSubmitCallback, | ||||
| } from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog'; | ||||
|  | ||||
| // TODO: INFO - this component needs for abstract work with Map instance | ||||
| export const MapWrapper = () => { | ||||
| @@ -34,6 +38,9 @@ export const MapWrapper = () => { | ||||
|       isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap, | ||||
|       isShowKSpace, | ||||
|       isThickConnections, | ||||
|       isShowBackgroundPattern, | ||||
|       isSoftBackground, | ||||
|       theme, | ||||
|     }, | ||||
|   } = useMapRootState(); | ||||
|   const { deleteSystems } = useDeleteSystems(); | ||||
| @@ -45,6 +52,7 @@ export const MapWrapper = () => { | ||||
|   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, deleteSystems }); | ||||
| @@ -85,9 +93,6 @@ export const MapWrapper = () => { | ||||
|         case OutCommand.openSettings: | ||||
|           setOpenSettings(event.data.system_id); | ||||
|           break; | ||||
|         case OutCommand.linkSignatureToSystem: | ||||
|           setOpenLinkSignatures(event.data); | ||||
|           break; | ||||
|         default: | ||||
|           return outCommand(event); | ||||
|       } | ||||
| @@ -121,6 +126,28 @@ export const MapWrapper = () => { | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const onAddSystem: OnMapAddSystemCallback = useCallback(({ coordinates }) => { | ||||
|     setOpenAddSystem(coordinates); | ||||
|   }, []); | ||||
|  | ||||
|   const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback( | ||||
|     async item => { | ||||
|       if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) { | ||||
|         emitMapEvent({ | ||||
|           name: Commands.centerSystem, | ||||
|           data: item.value.toString(), | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       await outCommand({ | ||||
|         type: OutCommand.manualAddSystem, | ||||
|         data: { coordinates: openAddSystem, solar_system_id: item.value }, | ||||
|       }); | ||||
|     }, | ||||
|     [openAddSystem, outCommand], | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Map | ||||
| @@ -135,6 +162,10 @@ export const MapWrapper = () => { | ||||
|         showKSpaceBG={isShowKSpace} | ||||
|         onManualDelete={handleManualDelete} | ||||
|         isThickConnections={isThickConnections} | ||||
|         isShowBackgroundPattern={isShowBackgroundPattern} | ||||
|         isSoftBackground={isSoftBackground} | ||||
|         theme={theme} | ||||
|         onAddSystem={onAddSystem} | ||||
|       /> | ||||
|  | ||||
|       {openSettings != null && ( | ||||
| @@ -149,6 +180,12 @@ export const MapWrapper = () => { | ||||
|         <SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} /> | ||||
|       )} | ||||
|  | ||||
|       <AddSystemDialog | ||||
|         visible={!!openAddSystem} | ||||
|         setVisible={() => setOpenAddSystem(null)} | ||||
|         onSubmit={handleSubmitAddSystem} | ||||
|       /> | ||||
|  | ||||
|       <Connections selectedConnection={selectedConnection} onHide={() => setSelectedConnection(null)} /> | ||||
|  | ||||
|       <ContextMenuSystem | ||||
|   | ||||
| @@ -18,7 +18,7 @@ const Topbar = ({ children }: WithChildren) => { | ||||
|     <nav | ||||
|       className={clsx( | ||||
|         'px-2 flex items-center justify-center min-w-0 h-12 pointer-events-auto', | ||||
|         'border-b border-gray-900 bg-gray-800 bg-opacity-5', | ||||
|         'border-b border-stone-800 bg-gray-800 bg-opacity-5', | ||||
|         'bg-opacity-70 bg-neutral-900', | ||||
|       )} | ||||
|     > | ||||
|   | ||||
| @@ -31,12 +31,15 @@ const prepareEffects = (effects: Record<string, EffectRaw>, effectName: string, | ||||
|   return out; | ||||
| }; | ||||
|  | ||||
| let counter = 0; | ||||
|  | ||||
| export interface WHEffectViewProps { | ||||
|   effectName: string; | ||||
|   effectPower: number; | ||||
|   className?: string; | ||||
| } | ||||
|  | ||||
| export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) => { | ||||
| export const WHEffectView = ({ effectName, effectPower, className }: WHEffectViewProps) => { | ||||
|   const { | ||||
|     data: { effects }, | ||||
|   } = useMapRootState(); | ||||
| @@ -49,7 +52,7 @@ export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) => | ||||
|     [effectName, effectPower, effects], | ||||
|   ); | ||||
|  | ||||
|   const targetClass = `wh-effect-name${effectInfo.id}`; | ||||
|   const targetClass = useMemo(() => `wh-effect-name${effectInfo.id}-${counter++}`, []); | ||||
|  | ||||
|   return ( | ||||
|     <div className={classes.WHEffectViewRoot}> | ||||
| @@ -84,7 +87,7 @@ export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) => | ||||
|         </div> | ||||
|       </FixedTooltip> | ||||
|  | ||||
|       <div className={clsx('font-bold select-none cursor-help w-min-content', effectClass, targetClass)}> | ||||
|       <div className={clsx('font-bold select-none cursor-help w-min-content', effectClass, targetClass, className)}> | ||||
|         {effectName} | ||||
|       </div> | ||||
|     </div> | ||||
|   | ||||
| @@ -0,0 +1,137 @@ | ||||
| .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; | ||||
| } | ||||
| @@ -0,0 +1,419 @@ | ||||
| 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1 @@ | ||||
| export * from './WindowManager'; | ||||
| @@ -0,0 +1,10 @@ | ||||
| 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; | ||||
| }; | ||||
| @@ -0,0 +1,13 @@ | ||||
| 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,6 +1,7 @@ | ||||
| export enum SESSION_KEY { | ||||
|   viewPort = 'viewPort', | ||||
|   windows = 'windows', | ||||
|   windowsVisible = 'windowsVisible', | ||||
|   routes = 'routes', | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog'; | ||||
| import { SystemSignature } from '@/hooks/Mapper/types'; | ||||
| import { SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types'; | ||||
| import { MAPPING_TYPE_TO_ENG } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; | ||||
|  | ||||
| export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => { | ||||
|   const outArr: SystemSignature[] = []; | ||||
| @@ -14,10 +14,12 @@ export const parseSignatures = (value: string, availableKeys: string[]): SystemS | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     const kind = MAPPING_TYPE_TO_ENG[sigArrInfo[1] as SignatureKind]; | ||||
|  | ||||
|     outArr.push({ | ||||
|       eve_id: sigArrInfo[0], | ||||
|       kind: availableKeys.includes(sigArrInfo[1]) ? sigArrInfo[1] : COSMIC_SIGNATURE, | ||||
|       group: sigArrInfo[2], | ||||
|       kind: availableKeys.includes(kind) ? kind : SignatureKind.CosmicSignature, | ||||
|       group: sigArrInfo[2] as SignatureGroup, | ||||
|       name: sigArrInfo[3], | ||||
|       type: '', | ||||
|     }); | ||||
|   | ||||
| @@ -1,9 +1,15 @@ | ||||
| import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils'; | ||||
| import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext } from 'react'; | ||||
| import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext, useEffect } 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'; | ||||
|  | ||||
| export type MapRootData = MapUnionTypes & { | ||||
|   selectedSystems: string[]; | ||||
| @@ -18,6 +24,7 @@ const INITIAL_DATA: MapRootData = { | ||||
|   userCharacters: [], | ||||
|   presentCharacters: [], | ||||
|   systems: [], | ||||
|   systemSignatures: {}, | ||||
|   hubs: [], | ||||
|   routes: undefined, | ||||
|   kills: [], | ||||
| @@ -35,6 +42,9 @@ export enum InterfaceStoredSettingsProps { | ||||
|   isShowKSpace = 'isShowKSpace', | ||||
|   isThickConnections = 'isThickConnections', | ||||
|   isShowUnsplashedSignatures = 'isShowUnsplashedSignatures', | ||||
|   isShowBackgroundPattern = 'isShowBackgroundPattern', | ||||
|   isSoftBackground = 'isSoftBackground', | ||||
|   theme = 'theme', | ||||
| } | ||||
|  | ||||
| export type InterfaceStoredSettings = { | ||||
| @@ -43,6 +53,9 @@ export type InterfaceStoredSettings = { | ||||
|   isShowKSpace: boolean; | ||||
|   isThickConnections: boolean; | ||||
|   isShowUnsplashedSignatures: boolean; | ||||
|   isShowBackgroundPattern: boolean; | ||||
|   isSoftBackground: boolean; | ||||
|   theme: string; | ||||
| }; | ||||
|  | ||||
| export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = { | ||||
| @@ -51,6 +64,9 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = { | ||||
|   isShowKSpace: false, | ||||
|   isThickConnections: false, | ||||
|   isShowUnsplashedSignatures: false, | ||||
|   isShowBackgroundPattern: true, | ||||
|   isSoftBackground: false, | ||||
|   theme: 'default', | ||||
| }; | ||||
|  | ||||
| export interface MapRootContextProps { | ||||
| @@ -59,6 +75,10 @@ export interface MapRootContextProps { | ||||
|   outCommand: OutCommandHandler; | ||||
|   interfaceSettings: InterfaceStoredSettings; | ||||
|   setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>; | ||||
|   windowsSettings: WindowStoreInfo; | ||||
|   toggleWidgetVisibility: ToggleWidgetVisibility; | ||||
|   updateWidgetSettings: UpdateWidgetSettingsFunc; | ||||
|   resetWidgets: () => void; | ||||
| } | ||||
|  | ||||
| const MapRootContext = createContext<MapRootContextProps>({ | ||||
| @@ -92,6 +112,25 @@ 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 | ||||
| @@ -101,6 +140,10 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide | ||||
|         outCommand: outCommand, | ||||
|         setInterfaceSettings, | ||||
|         interfaceSettings, | ||||
|         windowsSettings, | ||||
|         updateWidgetSettings, | ||||
|         toggleWidgetVisibility, | ||||
|         resetWidgets, | ||||
|       }} | ||||
|     > | ||||
|       <MapRootHandlers ref={fwdRef}>{children}</MapRootHandlers> | ||||
|   | ||||
| @@ -8,14 +8,14 @@ import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| export const useCommandsSystems = () => { | ||||
|   const { | ||||
|     update, | ||||
|     data: { systems }, | ||||
|     data: { systems, systemSignatures }, | ||||
|     outCommand, | ||||
|   } = useMapRootState(); | ||||
|  | ||||
|   const { addSystemStatic } = useLoadSystemStatic({ systems: [] }); | ||||
|  | ||||
|   const ref = useRef({ systems, update, addSystemStatic }); | ||||
|   ref.current = { systems, update, addSystemStatic }; | ||||
|   const ref = useRef({ systems, systemSignatures, update, addSystemStatic }); | ||||
|   ref.current = { systems, systemSignatures, update, addSystemStatic }; | ||||
|  | ||||
|   const addSystems = useCallback((systemsToAdd: CommandAddSystems) => { | ||||
|     const { update, addSystemStatic, systems } = ref.current; | ||||
| @@ -57,28 +57,19 @@ export const useCommandsSystems = () => { | ||||
|     }); | ||||
|  | ||||
|     update({ systems: out }, true); | ||||
|  | ||||
|     emitMapEvent({ name: Commands.updateSystems, data: out }); | ||||
|   }, []); | ||||
|  | ||||
|   const updateSystemSignatures = useCallback( | ||||
|     async (systemId: string) => { | ||||
|       const { update, systems } = ref.current; | ||||
|  | ||||
|       const { update, systemSignatures } = ref.current; | ||||
|       const { signatures } = await outCommand({ | ||||
|         type: OutCommand.getSignatures, | ||||
|         data: { system_id: `${systemId}` }, | ||||
|       }); | ||||
|  | ||||
|       const out = systems.map(current => { | ||||
|         if (current.id === `${systemId}`) { | ||||
|           return { ...current, system_signatures: signatures }; | ||||
|         } | ||||
|  | ||||
|         return current; | ||||
|       }); | ||||
|  | ||||
|       update({ systems: out }, true); | ||||
|  | ||||
|       emitMapEvent({ name: Commands.updateSystems, data: out }); | ||||
|       const out = { ...systemSignatures, [`${systemId}`]: signatures }; | ||||
|       update({ systemSignatures: out }, true); | ||||
|     }, | ||||
|     [outCommand], | ||||
|   ); | ||||
|   | ||||
							
								
								
									
										118
									
								
								assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| 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, | ||||
|   }; | ||||
| }; | ||||
| @@ -14,16 +14,12 @@ export enum TimeStatus { | ||||
|   eol, | ||||
| } | ||||
|  | ||||
| // export enum ShipSizeStatus { | ||||
| //   small, // frigates, destroyers - less than 5K t | ||||
| //   medium, // less than 20K t | ||||
| //   large, // less than 375K t | ||||
| //   capital, // less than 1.8M t | ||||
| // } | ||||
|  | ||||
| export enum ShipSizeStatus { | ||||
|   small, // frigates, destroyers - less than 5K t | ||||
|   normal, | ||||
|   small = 0, // frigates, destroyers - less than 5K t | ||||
|   medium = 1, // less than 62K t | ||||
|   large = 2, // less than 375K t | ||||
|   freight = 3, // less than 1M t | ||||
|   capital = 4, // less than 1.8M t | ||||
| } | ||||
|  | ||||
| export type SolarSystemConnection = { | ||||
|   | ||||
| @@ -118,6 +118,7 @@ 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', | ||||
| @@ -127,8 +128,10 @@ 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', | ||||
|   updateSystemDescription = 'update_system_description', | ||||
|   updateSystemLabels = 'update_system_labels', | ||||
|   updateSystemLocked = 'update_system_locked', | ||||
| @@ -146,6 +149,8 @@ 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', | ||||
| @@ -153,6 +158,7 @@ export enum OutCommand { | ||||
|   getUserSettings = 'get_user_settings', | ||||
|   updateUserSettings = 'update_user_settings', | ||||
|   unlinkSignature = 'unlink_signature', | ||||
|   searchSystems = 'search_systems', | ||||
| } | ||||
|  | ||||
| export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ 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>; | ||||
| @@ -15,6 +16,7 @@ export type MapUnionTypes = { | ||||
|   presentCharacters: string[]; | ||||
|   hubs: string[]; | ||||
|   systems: SolarSystemRawType[]; | ||||
|   systemSignatures: Record<string, SystemSignature[]>; | ||||
|   routes?: RoutesList; | ||||
|   kills: Record<number, number>; | ||||
|   connections: SolarSystemConnection[]; | ||||
|   | ||||
| @@ -10,6 +10,15 @@ export enum SignatureGroup { | ||||
|   CombatSite = 'Combat Site', | ||||
| } | ||||
|  | ||||
| export enum SignatureKind { | ||||
|   CosmicSignature = 'Cosmic Signature', | ||||
|   CosmicAnomaly = 'Cosmic Anomaly', | ||||
|   Structure = 'Structure', | ||||
|   Ship = 'Ship', | ||||
|   Deployable = 'Deployable', | ||||
|   Drone = 'Drone', | ||||
| } | ||||
|  | ||||
| export type GroupType = { | ||||
|   id: string; | ||||
|   icon: string; | ||||
| @@ -19,7 +28,7 @@ export type GroupType = { | ||||
|  | ||||
| export type SystemSignature = { | ||||
|   eve_id: string; | ||||
|   kind: string; | ||||
|   kind: SignatureKind; | ||||
|   name: string; | ||||
|   custom_info?: string; | ||||
|   description?: string; | ||||
| @@ -30,3 +39,41 @@ export type SystemSignature = { | ||||
|   inserted_at?: string; | ||||
|   updated_at?: string; | ||||
| }; | ||||
|  | ||||
| export enum SignatureKindENG { | ||||
|   CosmicSignature = 'Cosmic Signature', | ||||
|   CosmicAnomaly = 'Cosmic Anomaly', | ||||
|   Structure = 'Structure', | ||||
|   Ship = 'Ship', | ||||
|   Deployable = 'Deployable', | ||||
|   Drone = 'Drone', | ||||
| } | ||||
|  | ||||
| export enum SignatureKindRU { | ||||
|   CosmicSignature = 'Скрытый сигнал', | ||||
|   CosmicAnomaly = 'Космическая аномалия', | ||||
|   Structure = 'Сооружение', | ||||
|   Ship = 'Корабль', | ||||
|   Deployable = 'Полевые блоки', | ||||
|   Drone = 'Дрон', | ||||
| } | ||||
|  | ||||
| export enum SignatureGroupENG { | ||||
|   CosmicSignature = 'Cosmic Signature', | ||||
|   Wormhole = 'Wormhole', | ||||
|   GasSite = 'Gas Site', | ||||
|   RelicSite = 'Relic Site', | ||||
|   DataSite = 'Data Site', | ||||
|   OreSite = 'Ore Site', | ||||
|   CombatSite = 'Combat Site', | ||||
| } | ||||
|  | ||||
| export enum SignatureGroupRU { | ||||
|   CosmicSignature = 'Скрытый сигнал', | ||||
|   Wormhole = 'Червоточина', | ||||
|   GasSite = 'Газовый район', | ||||
|   RelicSite = 'Археологический район', | ||||
|   DataSite = 'Информационный район', | ||||
|   OreSite = 'Астероидный район', | ||||
|   CombatSite = 'Боевой район', | ||||
| } | ||||
|   | ||||
| @@ -116,7 +116,18 @@ export type SolarSystemRawType = { | ||||
|   tag: string | null; | ||||
|   status: number; | ||||
|   name: string | null; | ||||
|   temporary_name: string | null; | ||||
|   linked_sig_eve_id: string | null; | ||||
|  | ||||
|   system_static_info: SolarSystemStaticInfoRaw; | ||||
|   system_signatures: SystemSignature[]; | ||||
| }; | ||||
|  | ||||
| export type SearchSystemItem = { | ||||
|   class_title: string; | ||||
|   constellation_name: string; | ||||
|   label: string; | ||||
|   region_name: string; | ||||
|   system_static_info: SolarSystemStaticInfoRaw; | ||||
|   value: number; | ||||
| }; | ||||
|   | ||||
| @@ -1,17 +1,14 @@ | ||||
| export default { | ||||
|   mounted() { | ||||
|     const hook = this; | ||||
|     const url = hook.el.dataset.url; | ||||
|     const button = hook.el; | ||||
|     const button = this.el; | ||||
|  | ||||
|     button.addEventListener('click', function () { | ||||
|       // Get the URL from the data attribute | ||||
|  | ||||
|       button.classList.remove('copied'); | ||||
|  | ||||
|       // Copy the URL to the clipboard | ||||
|       navigator.clipboard | ||||
|         .writeText(url) | ||||
|         .writeText(button.dataset.url) | ||||
|         .then(() => { | ||||
|           button.classList.add('copied'); | ||||
|         }) | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import CopyToClipboard from './copyToClipboard'; | ||||
| import DownloadJson from './downloadJson'; | ||||
| import NewVersionUpdate from './newVersionUpdate'; | ||||
| import MapAction from './maps/mapAction'; | ||||
| import ShowCharactersAddAlert from './showCharactersAddAlert'; | ||||
|  | ||||
| export default { | ||||
|   DownloadJson, | ||||
| @@ -20,4 +21,5 @@ export default { | ||||
|   Ping, | ||||
|   CopyToClipboard, | ||||
|   NewVersionUpdate, | ||||
|   ShowCharactersAddAlert, | ||||
| }; | ||||
|   | ||||
							
								
								
									
										11
									
								
								assets/js/hooks/showCharactersAddAlert.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								assets/js/hooks/showCharactersAddAlert.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| export default { | ||||
|   mounted() { | ||||
|     this.pushEvent('restore_show_characters_add_alert', { | ||||
|       value: localStorage.getItem('wanderer:hide_characters_add_alert') !== 'true', | ||||
|     }); | ||||
|  | ||||
|     document.getElementById('characters-add-alert-hide')?.addEventListener('click', e => { | ||||
|       localStorage.setItem('wanderer:hide_characters_add_alert', 'true'); | ||||
|     }); | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/static/images/news/01-05-map-public-api/generate-key.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/static/images/news/01-05-map-public-api/generate-key.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 30 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.1 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 13 KiB | 
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user