mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-04 23:05:35 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b079505c3 | ||
|
|
5b972b03e5 | ||
|
|
79b284c46d | ||
|
|
b29e57b3a4 | ||
|
|
c6f4baeee3 | ||
|
|
6d341be072 | ||
|
|
2437ec9c84 | ||
|
|
7e692b5805 | ||
|
|
01b7370ecd | ||
|
|
20ad8b07d7 | ||
|
|
cab1880fb0 | ||
|
|
78eefcd6a7 | ||
|
|
eec78d38a8 | ||
|
|
73f8b1f06b | ||
|
|
f96cb01860 | ||
|
|
6800be1bb6 | ||
|
|
143f0a5b3a | ||
|
|
b6495504f8 | ||
|
|
2f07ec1b74 |
85
CHANGELOG.md
85
CHANGELOG.md
@@ -2,6 +2,91 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.47.3](https://github.com/wanderer-industries/wanderer/compare/v1.47.2...v1.47.3) (2025-02-11)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.47.2](https://github.com/wanderer-industries/wanderer/compare/v1.47.1...v1.47.2) (2025-02-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* lazy load kills widget (#157)
|
||||
|
||||
* lazy load kills widget
|
||||
|
||||
* updates for eslint and pr feedback
|
||||
|
||||
## [v1.47.1](https://github.com/wanderer-industries/wanderer/compare/v1.47.0...v1.47.1) (2025-02-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Connections: Fixed connections auto-refresh after update
|
||||
|
||||
## [v1.47.0](https://github.com/wanderer-industries/wanderer/compare/v1.46.1...v1.47.0) (2025-02-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Map: Added check for active map subscription to using Map APIs
|
||||
|
||||
## [v1.46.1](https://github.com/wanderer-industries/wanderer/compare/v1.46.0...v1.46.1) (2025-02-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed a lot of design and architect issues after last milli⦠(#154)
|
||||
|
||||
* Map: Fixed a lot of design and architect issues after last million PRs
|
||||
|
||||
* Map: removed unnecessary hooks styles
|
||||
|
||||
## [v1.46.0](https://github.com/wanderer-industries/wanderer/compare/v1.45.5...v1.46.0) (2025-02-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Added WANDERER_RESTRICT_MAPS_CREATION env support
|
||||
|
||||
## [v1.45.5](https://github.com/wanderer-industries/wanderer/compare/v1.45.4...v1.45.5) (2025-02-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* restore styling for local characters list (#152)
|
||||
|
||||
## [v1.45.4](https://github.com/wanderer-industries/wanderer/compare/v1.45.3...v1.45.4) (2025-02-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove snap to grid customization (#153)
|
||||
|
||||
## [v1.45.3](https://github.com/wanderer-industries/wanderer/compare/v1.45.2...v1.45.3) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* color and formatting fixes for local character (#150)
|
||||
|
||||
## [v1.45.2](https://github.com/wanderer-industries/wanderer/compare/v1.45.1...v1.45.2) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useSystemInfo';
|
||||
export * from './useGetOwnOnlineCharacters';
|
||||
export * from './useElementWidth';
|
||||
|
||||
43
assets/js/hooks/Mapper/components/hooks/useElementWidth.ts
Normal file
43
assets/js/hooks/Mapper/components/hooks/useElementWidth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState, useLayoutEffect, RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* useElementWidth
|
||||
*
|
||||
* A custom hook that accepts a ref to an HTML element and returns its current width.
|
||||
* It uses a ResizeObserver and window resize listener to update the width when necessary.
|
||||
*
|
||||
* @param ref - A RefObject pointing to an HTML element.
|
||||
* @returns The current width of the element.
|
||||
*/
|
||||
export function useElementWidth<T extends HTMLElement>(ref: RefObject<T>): number {
|
||||
const [width, setWidth] = useState<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (ref.current) {
|
||||
const newWidth = ref.current.getBoundingClientRect().width;
|
||||
if (newWidth > 0) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth(); // Initial measurement
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
const id = setTimeout(updateWidth, 100);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", updateWidth);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -84,7 +84,6 @@ interface MapCompProps {
|
||||
onCommand: OutCommandHandler;
|
||||
onSelectionChange: OnMapSelectionChange;
|
||||
onManualDelete(systems: string[]): void;
|
||||
canRemoveConnection?(connectionId: string): boolean;
|
||||
onConnectionInfoClick?(e: SolarSystemConnection): void;
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onSelectionContextMenu?: NodeSelectionMouseHandler;
|
||||
@@ -114,9 +113,8 @@ const MapComp = ({
|
||||
isSoftBackground,
|
||||
theme,
|
||||
onAddSystem,
|
||||
canRemoveConnection,
|
||||
}: MapCompProps) => {
|
||||
const { getEdge, getNode, getNodes } = useReactFlow();
|
||||
const { getNode, getNodes } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
|
||||
|
||||
@@ -125,7 +123,7 @@ const MapComp = ({
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color, snapSize } = useBackgroundVars(theme);
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||
|
||||
const nodeTypes = useMemo(() => {
|
||||
@@ -224,40 +222,6 @@ const MapComp = ({
|
||||
[getNode, getNodes, onManualDelete, onNodesChange],
|
||||
);
|
||||
|
||||
const handleEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
const nextChanges = changes.reduce((acc, change) => {
|
||||
if (change.type !== 'remove') {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (canRemoveConnection?.(change.id)) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const edge = getEdge(change.id);
|
||||
if (!edge) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const sourceNode = getNode(edge.source);
|
||||
const targetNode = getNode(edge.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (sourceNode.data.locked || targetNode.data.locked) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, change];
|
||||
}, [] as EdgeChange[]);
|
||||
|
||||
onEdgesChange(nextChanges);
|
||||
},
|
||||
[canRemoveConnection, getEdge, getNode, onEdgesChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
update(x => ({
|
||||
...x,
|
||||
@@ -273,7 +237,7 @@ const MapComp = ({
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
// TODO we need save into session all of this
|
||||
// and on any action do either
|
||||
@@ -282,7 +246,6 @@ const MapComp = ({
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={connectionMode}
|
||||
snapToGrid
|
||||
snapGrid={[snapSize, snapSize]}
|
||||
nodeDragThreshold={10}
|
||||
onNodeDragStop={handleDragStop}
|
||||
onSelectionDragStop={handleSelectionDragStop}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.TooltipActive {
|
||||
pointer-events: auto !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.localCounter {
|
||||
mix-blend-mode: screen;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: var(--rf-node-local-counter);
|
||||
|
||||
&.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters);
|
||||
}
|
||||
|
||||
& > i {
|
||||
font-size: 9px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Pathfinder {
|
||||
.localCounter {
|
||||
@-moz-document url-prefix() {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,23 @@ import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||
import { LocalCharactersList, CharItemProps } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
|
||||
import classes from './SolarSystemLocalCounter.module.scss';
|
||||
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
|
||||
|
||||
interface LocalCounterProps {
|
||||
localCounterCharacters: Array<CharItemProps>;
|
||||
classes: { [key: string]: string };
|
||||
hasUserCharacters: boolean;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
export function LocalCounter({
|
||||
localCounterCharacters,
|
||||
hasUserCharacters,
|
||||
classes,
|
||||
showIcon = true,
|
||||
}: LocalCounterProps) {
|
||||
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
|
||||
const [settings] = useLocalCharacterWidgetSettings();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
const theme = useTheme();
|
||||
|
||||
const pilotTooltipContent = useMemo(() => {
|
||||
return (
|
||||
@@ -42,23 +40,26 @@ export function LocalCounter({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.LocalCounterLayer} style={{ zIndex: 9999 }}>
|
||||
<div
|
||||
className={clsx(classes.TooltipActive, {
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
>
|
||||
<WdTooltipWrapper
|
||||
// @ts-ignore
|
||||
content={pilotTooltipContent}
|
||||
position={TooltipPosition.right}
|
||||
offset={180}
|
||||
interactive={true}
|
||||
offset={0}
|
||||
>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
{showIcon && <i className="pi pi-users" style={{ fontSize: '0.50rem' }} />}
|
||||
{showIcon && <i className="pi pi-users" />}
|
||||
<span>{localCounterCharacters.length}</span>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ $tooltip-bg: #202020;
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
&.Mataria,
|
||||
@@ -91,8 +91,8 @@ $tooltip-bg: #202020;
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
45deg,
|
||||
var(--eve-solar-system-status-color-background),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
@@ -273,7 +273,7 @@ $tooltip-bg: #202020;
|
||||
height: 19px;
|
||||
|
||||
.hasLocalCounter {
|
||||
margin-right: 1.25rem;
|
||||
margin-right: 2px;
|
||||
&.countAbove9 {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
@@ -319,7 +319,8 @@ $tooltip-bg: #202020;
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -332,6 +333,7 @@ $tooltip-bg: #202020;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
pointer-events: auto;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
@@ -375,38 +377,3 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
}
|
||||
|
||||
.LocalCounterLayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
padding: 8;
|
||||
|
||||
.localCounter {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
top: 10.5px;
|
||||
right: 8px;
|
||||
mix-blend-mode: screen;
|
||||
gap: 2px;
|
||||
color: var(--rf-node-local-counter, #5cb85c);
|
||||
|
||||
&.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters, #fbbf24);
|
||||
}
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
@@ -120,18 +121,18 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div
|
||||
className={clsx('flex items-center gap-1', {
|
||||
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -158,7 +159,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
@@ -200,11 +201,6 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
classes={classes}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,3 +4,17 @@
|
||||
Only override what's different from the base
|
||||
Currently none required
|
||||
---------------------------------------------- */
|
||||
|
||||
.RootCustomNode {
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-home);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
<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>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
@@ -130,18 +131,18 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div
|
||||
className={clsx('flex items-center gap-1', {
|
||||
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -168,7 +169,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
@@ -210,11 +211,6 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
classes={classes}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,5 +10,6 @@ export const convertSystem2Node = (sys: SolarSystemRawType): Node => {
|
||||
position: sys.position,
|
||||
data: sys,
|
||||
draggable: !sys.locked,
|
||||
deletable: !sys.locked,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,6 +31,6 @@
|
||||
--window-corner: #72716f;
|
||||
|
||||
--rf-local-counter-font-weight: 500;
|
||||
--rf-node-local-counter: #5cb85c;
|
||||
--rf-has-user-characters: #fbbf24;
|
||||
--rf-node-local-counter: inherit;
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyDark20: darken($friendlyBase, 20%);
|
||||
$friendlyDark30: darken($friendlyBase, 30%);
|
||||
$friendlyDark5: darken($friendlyBase, 5%);
|
||||
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForAlpha: rgba(67, 176, 253, 0.48);
|
||||
$lookingForDark15: darken($lookingForBase, 15%);
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeBase: rgb(179, 253, 67);
|
||||
$homeAlpha: rgba(186, 248, 48, 0.32);
|
||||
$homeBackground: #a0fa5636;
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
|
||||
|
||||
:root {
|
||||
--pastel-blue: #5a7d9a;
|
||||
--pastel-pink: #d291bc;
|
||||
@@ -98,6 +98,7 @@ $homeDark30: darken($homeBase, 30%);
|
||||
--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-background: #{$homeBackground};
|
||||
--eve-solar-system-status-color-home-dark30: #{$homeDark30};
|
||||
--eve-solar-system-status-friendly: #{$friendlyAlpha};
|
||||
--eve-solar-system-status-color-friendly: #{$friendlyBase};
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
@import './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
|
||||
.pathfinder-theme {
|
||||
/* -- Override values from the default theme -- */
|
||||
--rf-bg-color: #000000;
|
||||
@@ -41,4 +45,10 @@
|
||||
--eve-wh-type-color-c6: #d9534f;
|
||||
--eve-wh-type-color-c13: #7986cb;
|
||||
--eve-wh-type-color-drifter: #44aa82;
|
||||
|
||||
--rf-node-local-counter: #5cb85c;
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
|
||||
--eve-solar-system-status-home: #{$homeAlpha};
|
||||
--eve-solar-system-status-color-home: #{$homeBase};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.VirtualScroller {
|
||||
height: 100% !important;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import clsx from 'clsx';
|
||||
import { LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
|
||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters';
|
||||
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';
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions';
|
||||
import { LocalCharactersList } from './components/LocalCharactersList';
|
||||
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
|
||||
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
|
||||
import classes from './LocalCharacters.module.scss';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const LocalCharacters = () => {
|
||||
const {
|
||||
@@ -18,11 +17,9 @@ export const LocalCharacters = () => {
|
||||
} = useMapRootState();
|
||||
|
||||
const [settings, setSettings] = useLocalCharacterWidgetSettings();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
||||
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
||||
|
||||
const showOffline = useMemo(
|
||||
() => !restrictOfflineShowing || isAdminOrManager,
|
||||
[isAdminOrManager, restrictOfflineShowing],
|
||||
@@ -42,7 +39,6 @@ export const LocalCharacters = () => {
|
||||
if (!showOffline || !settings.showOffline) {
|
||||
return filtered.filter(c => c.online);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [
|
||||
characters,
|
||||
@@ -58,64 +54,18 @@ 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);
|
||||
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex w-full items-center" ref={ref}>
|
||||
<div className="flex-shrink-0 select-none mr-2">
|
||||
Local{showList ? ` [${sorted.length}]` : ''}
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden">
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system">
|
||||
<div className={clsx("min-w-0", { "max-w-[100px]": compact })}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show offline"
|
||||
value={settings.showOffline}
|
||||
classNameLabel={clsx("whitespace-nowrap", { "truncate": compact })}
|
||||
onChange={() =>
|
||||
setSettings(prev => ({ ...prev, showOffline: !prev.showOffline }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{settings.compact && (
|
||||
<WdTooltipWrapper content="Show ship name in compact rows">
|
||||
<div className={clsx("min-w-0", { "max-w-[100px]": compact })}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show ship name"
|
||||
value={settings.showShipName}
|
||||
classNameLabel={clsx("whitespace-nowrap", { "truncate": compact })}
|
||||
onChange={() =>
|
||||
setSettings(prev => ({ ...prev, showShipName: !prev.showShipName }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={clsx("w-4 h-4 cursor-pointer", {
|
||||
"hero-bars-2": settings.compact,
|
||||
"hero-bars-3": !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings(prev => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
</div>
|
||||
<LocalCharactersHeader
|
||||
sortedCount={sorted.length}
|
||||
showList={showList}
|
||||
showOffline={showOffline}
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isNotSelectedSystem && (
|
||||
@@ -123,19 +73,20 @@ export const LocalCharacters = () => {
|
||||
System is not selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isNobodyHere && !isNotSelectedSystem && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
|
||||
Nobody here
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showList && (
|
||||
<LocalCharactersList
|
||||
items={sorted}
|
||||
itemSize={settings.compact ? 26 : 41}
|
||||
itemTemplate={itemTemplate}
|
||||
containerClassName="w-full h-full overflow-x-hidden overflow-y-auto"
|
||||
containerClassName={clsx(
|
||||
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
|
||||
classes.VirtualScroller,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// .VirtualScroller {
|
||||
// height: 100% !important;
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface LocalCharactersHeaderProps {
|
||||
sortedCount: number;
|
||||
showList: boolean;
|
||||
showOffline: boolean;
|
||||
settings: {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
showShipName: boolean;
|
||||
};
|
||||
setSettings: (fn: (prev: any) => any) => void;
|
||||
}
|
||||
|
||||
export const LocalCharactersHeader: React.FC<LocalCharactersHeaderProps> = ({
|
||||
sortedCount,
|
||||
showList,
|
||||
showOffline,
|
||||
settings,
|
||||
setSettings,
|
||||
}) => {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const compactOffline = useMaxWidth(headerRef, 145);
|
||||
const compactShipName = useMaxWidth(headerRef, 195);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center text-xs justify-between" ref={headerRef}>
|
||||
<div className="flex-shrink-0 select-none mr-2">Local{showList ? ` [${sortedCount}]` : ''}</div>
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compactOffline ? '' : 'Offline'}
|
||||
value={settings.showOffline}
|
||||
onChange={() => setSettings((prev: any) => ({ ...prev, showOffline: !prev.showOffline }))}
|
||||
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
|
||||
truncate: compactOffline,
|
||||
})}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{settings.compact && (
|
||||
<WdTooltipWrapper content="Show ship name in compact rows" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compactShipName ? '' : 'Ship name'}
|
||||
value={settings.showShipName}
|
||||
onChange={() => setSettings((prev: any) => ({ ...prev, showShipName: !prev.showShipName }))}
|
||||
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
|
||||
truncate: compactShipName,
|
||||
})}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
<WdTooltipWrapper content="Enable compact mode" position={TooltipPosition.top}>
|
||||
<span
|
||||
className={clsx('w-4 h-4 min-w-[1rem] cursor-pointer', {
|
||||
'hero-bars-2': settings.compact,
|
||||
'hero-bars-3': !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings((prev: any) => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,6 @@
|
||||
.VirtualScroller {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.CharacterRow {
|
||||
//border-left-width: 1px;
|
||||
|
||||
&.CardBorderLeftIsOwn {
|
||||
border-left-color: rgb(251 146 60 / 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import classes from './LocalCharactersItemTemplate.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
|
||||
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
|
||||
VirtualScrollerTemplateOptions;
|
||||
|
||||
export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalCharactersItemTemplateProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CharacterRow,
|
||||
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px]',
|
||||
{
|
||||
'surface-hover': options.odd,
|
||||
'border-b border-gray-600 border-opacity-20': !options.last,
|
||||
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': options.online,
|
||||
},
|
||||
)}
|
||||
style={{ height: `${options.props.itemSize}px` }}
|
||||
>
|
||||
<CharacterCard showShipName={showShipName} {...options} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LocalCharactersItemTemplate.tsx';
|
||||
@@ -5,23 +5,24 @@ import { CharItemProps } from './types';
|
||||
|
||||
type LocalCharactersListProps = {
|
||||
items: Array<CharItemProps>;
|
||||
|
||||
itemSize: number;
|
||||
|
||||
itemTemplate: (char: CharItemProps, options: VirtualScrollerTemplateOptions) => React.ReactNode;
|
||||
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function LocalCharactersList({ items, itemSize, itemTemplate, containerClassName }: LocalCharactersListProps) {
|
||||
export const LocalCharactersList = ({
|
||||
items,
|
||||
itemSize,
|
||||
itemTemplate,
|
||||
containerClassName,
|
||||
}: LocalCharactersListProps) => {
|
||||
return (
|
||||
<VirtualScroller
|
||||
items={items}
|
||||
itemSize={itemSize}
|
||||
orientation="vertical"
|
||||
className={clsx('w-full h-full', containerClassName)}
|
||||
autoSize={false}
|
||||
itemTemplate={itemTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './LocalCharactersItemTemplate';
|
||||
export * from './LocalCharactersList';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import classes from './useLocalCharacters.module.scss';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharItemProps } from '../components';
|
||||
import { CharItemProps, LocalCharactersItemTemplate } from '../components';
|
||||
|
||||
export function useLocalCharactersItemTemplate(showShipName: boolean) {
|
||||
return useCallback(
|
||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterRow, 'box-border flex items-center', {
|
||||
'surface-hover': options.odd,
|
||||
'border-b border-gray-600 border-opacity-20': !options.last,
|
||||
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': char.online,
|
||||
})}
|
||||
style={{
|
||||
height: `${options.props.itemSize}px`,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<CharacterCard showShipName={showShipName} {...char} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => (
|
||||
<LocalCharactersItemTemplate {...char} {...options} showShipName={showShipName} />
|
||||
),
|
||||
[showShipName],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classes from './RoutesList.module.scss';
|
||||
import { Route, SystemStaticInfoShort } from '@/hooks/Mapper/types/routes.ts';
|
||||
import clsx from 'clsx';
|
||||
import { SystemViewStandalone, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemViewStandalone, TooltipPosition, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getBackgroundClass, getShapeClass } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { MouseEvent, useCallback, useRef, useState } from 'react';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
@@ -46,9 +46,11 @@ export const RouteSystem = ({
|
||||
<>
|
||||
<WdTooltip
|
||||
ref={tooltipRef}
|
||||
position={TooltipPosition.top}
|
||||
// targetSelector={`.tooltip-route-sys_${destination}_${solar_system_id}`}
|
||||
content={() => (
|
||||
<SystemViewStandalone
|
||||
className="mx-[4px]"
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
class_title={class_title}
|
||||
@@ -63,8 +65,8 @@ export const RouteSystem = ({
|
||||
tooltipRef.current?.show(e);
|
||||
onMouseEnter?.(solar_system_id);
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
tooltipRef.current?.hide(e);
|
||||
onMouseLeave={() => {
|
||||
tooltipRef.current?.hide();
|
||||
onMouseLeave?.();
|
||||
}}
|
||||
onContextMenu={handleContext}
|
||||
|
||||
@@ -184,7 +184,7 @@ export const RoutesWidgetComp = () => {
|
||||
}, [data, update]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 155);
|
||||
const compact = useMaxWidth(ref, 170);
|
||||
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
|
||||
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
@@ -217,14 +217,14 @@ export const RoutesWidgetComp = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route">
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show shortest'}
|
||||
value={!isSecure}
|
||||
onChange={handleSecureChange}
|
||||
classNameLabel={clsx('text-red-400')}
|
||||
classNameLabel="text-red-400 whitespace-nowrap"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
<WdImgButton
|
||||
|
||||
@@ -15,7 +15,6 @@ export const SystemKills: React.FC = () => {
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
|
||||
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
|
||||
|
||||
const systemNameMap = useMemo(() => {
|
||||
@@ -41,7 +40,9 @@ export const SystemKills: React.FC = () => {
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settings.whOnly || !visible) return kills;
|
||||
return kills.filter(kill => {
|
||||
const system = systems.find(sys => sys.system_static_info.solar_system_id === kill.solar_system_id);
|
||||
const system = systems.find(
|
||||
sys => sys.system_static_info.solar_system_id === kill.solar_system_id
|
||||
);
|
||||
if (!system) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
return false;
|
||||
@@ -55,60 +56,62 @@ export const SystemKills: React.FC = () => {
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<Widget
|
||||
label={
|
||||
<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />
|
||||
<KillsHeader
|
||||
systemId={systemId}
|
||||
onOpenSettings={() => setSettingsDialogVisible(true)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!isSubscriptionActive && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</div>
|
||||
)}
|
||||
{isSubscriptionActive && (
|
||||
<>
|
||||
{isNothingSelected && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
<div className="relative h-full">
|
||||
{!isSubscriptionActive ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</span>
|
||||
</div>
|
||||
) : isNothingSelected ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No system selected (or toggle “Show all systems”)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && showLoading && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
</span>
|
||||
</div>
|
||||
) : showLoading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Loading Kills...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && error && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-red-400 text-sm">
|
||||
</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected &&
|
||||
!showLoading &&
|
||||
!error &&
|
||||
(!filteredKills || filteredKills.length === 0) && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
No kills found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && !error && (
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SystemKillsContent
|
||||
key={settings.compact ? 'compact' : 'normal'}
|
||||
kills={filteredKills}
|
||||
systemNameMap={systemNameMap}
|
||||
compact={settings.compact}
|
||||
onlyOneSystem={!visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : !filteredKills || filteredKills.length === 0 ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No kills found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<SystemKillsContent
|
||||
key={settings.compact ? 'compact' : 'normal'}
|
||||
kills={filteredKills}
|
||||
systemNameMap={systemNameMap}
|
||||
compact={settings.compact}
|
||||
onlyOneSystem={!visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
|
||||
<KillsSettingsDialog visible={settingsDialogVisible} setVisible={setSettingsDialogVisible} />
|
||||
<KillsSettingsDialog
|
||||
visible={settingsDialogVisible}
|
||||
setVisible={setSettingsDialogVisible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
|
||||
interface AttackerRowSubInfoProps {
|
||||
finalBlowCharId: number | null | undefined;
|
||||
finalBlowCharName?: string;
|
||||
attackerPortraitUrl: string | null;
|
||||
|
||||
finalBlowCorpId: number | null | undefined;
|
||||
finalBlowCorpName?: string;
|
||||
attackerCorpLogoUrl: string | null;
|
||||
|
||||
finalBlowAllianceId: number | null | undefined;
|
||||
finalBlowAllianceName?: string;
|
||||
attackerAllianceLogoUrl: string | null;
|
||||
|
||||
containerHeight?: number;
|
||||
}
|
||||
|
||||
export const AttackerRowSubInfo: React.FC<AttackerRowSubInfoProps> = ({
|
||||
finalBlowCharId = 0,
|
||||
finalBlowCharName,
|
||||
attackerPortraitUrl,
|
||||
containerHeight = 8,
|
||||
}) => {
|
||||
if (!attackerPortraitUrl || finalBlowCharId === null || finalBlowCharId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerClass = `h-${containerHeight}`;
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-start gap-1', containerClass)}>
|
||||
<div className="relative shrink-0 w-auto h-full overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', finalBlowCharId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt={finalBlowCharName || 'AttackerPortrait'}
|
||||
className={clsx(classes.killRowImage, 'h-full w-auto object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -29,8 +29,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
|
||||
// Victim
|
||||
// Victim data
|
||||
victim_char_name = '',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
@@ -41,8 +40,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
victim_ship_type_id = 0,
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
|
||||
// Attacker
|
||||
// Attacker data
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
@@ -53,14 +51,12 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_ship_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
|
||||
total_value = 0,
|
||||
kill_time = '',
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
const victimAffiliation =
|
||||
victim_alliance_ticker || victim_corp_ticker || null;
|
||||
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker || null;
|
||||
const attackerAffiliation = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
@@ -69,7 +65,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
|
||||
// Victim images, now also pulling victimShipUrl
|
||||
// Build victim images
|
||||
const {
|
||||
victimPortraitUrl,
|
||||
victimCorpLogoUrl,
|
||||
@@ -81,7 +77,8 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
// Attacker images
|
||||
|
||||
// Build attacker images
|
||||
const {
|
||||
attackerPortraitUrl,
|
||||
attackerCorpLogoUrl,
|
||||
@@ -92,7 +89,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
// Primary corp/alliance logo for victim
|
||||
// Primary image for victim
|
||||
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
@@ -102,7 +99,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
'Victim'
|
||||
);
|
||||
|
||||
// Primary image for attacker => NPC => ship, else corp/alliance
|
||||
// Primary image for attacker
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
@@ -119,34 +116,15 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
classes.killRowContainer,
|
||||
'h-18 w-full justify-between items-start text-sm py-[4px]'
|
||||
'w-full text-sm py-1 px-2',
|
||||
'flex flex-col sm:flex-row'
|
||||
)}
|
||||
>
|
||||
{/* ---------------- Victim Side ---------------- */}
|
||||
<div className="flex items-start gap-1 min-w-0 h-full">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{victimPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<div className="w-full flex flex-col sm:flex-row items-start gap-2">
|
||||
{/* Victim Section */}
|
||||
<div className="flex items-start gap-1 min-w-0">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
@@ -154,109 +132,136 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryImageUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<VictimRowSubInfo
|
||||
victimCharName={victim_char_name}
|
||||
victimCharacterId={victim_char_id}
|
||||
victimPortraitUrl={victimPortraitUrl}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
|
||||
<div className="truncate">
|
||||
<span className="font-semibold">{victim_char_name}</span>
|
||||
{victimAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">
|
||||
{killValueFormatted}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName && <span>{systemName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1 min-w-0 h-full">
|
||||
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
|
||||
{!attackerIsNpc && (
|
||||
)}
|
||||
{victimPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryImageUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
<VictimRowSubInfo
|
||||
victimCharName={victim_char_name}
|
||||
victimCharacterId={victim_char_id}
|
||||
victimPortraitUrl={victimPortraitUrl}
|
||||
/>
|
||||
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
|
||||
<div className="truncate font-semibold">
|
||||
{final_blow_char_name}
|
||||
{attackerAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
|
||||
{victim_char_name}
|
||||
{victimAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!attackerIsNpc && final_blow_ship_name && (
|
||||
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
|
||||
)}
|
||||
<div className="truncate text-red-400">{killTimeAgo}</div>
|
||||
</div>
|
||||
|
||||
{!attackerIsNpc && attackerPortraitUrl && final_blow_char_id && final_blow_char_id > 0 && (
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', final_blow_char_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt="AttackerPortrait"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName && <span>{systemName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
</div>
|
||||
{/* Attacker Section */}
|
||||
<div className="flex items-start gap-1 min-w-0 sm:ml-auto">
|
||||
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
|
||||
{!attackerIsNpc && (
|
||||
<div className="truncate font-semibold">
|
||||
{final_blow_char_name}
|
||||
{attackerAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!attackerIsNpc && final_blow_ship_name && (
|
||||
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
|
||||
)}
|
||||
<div className="truncate text-red-400">{killTimeAgo}</div>
|
||||
</div>
|
||||
{(!attackerIsNpc && attackerPortraitUrl && final_blow_char_id > 0) && (
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
href={zkillLink('character', final_blow_char_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
src={attackerPortraitUrl}
|
||||
alt="AttackerPortrait"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
attackerSubscript.cssClass,
|
||||
classes.attackerCountLabel
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
)}
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
attackerSubscript.cssClass,
|
||||
classes.attackerCountLabel
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
SystemView,
|
||||
TooltipPosition,
|
||||
WdCheckbox,
|
||||
WdImgButton,
|
||||
TooltipPosition,
|
||||
SystemView,
|
||||
WdTooltipWrapper,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
|
||||
interface KillsWidgetHeaderProps {
|
||||
interface KillsHeaderProps {
|
||||
systemId?: string;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
const [settings, setSettings] = useKillsWidgetSettings();
|
||||
const { showAll } = settings;
|
||||
|
||||
@@ -22,8 +24,11 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
|
||||
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
|
||||
};
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(headerRef, 150);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center text-xs w-full">
|
||||
<div className="flex w-full items-center justify-between text-xs" ref={headerRef}>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-stone-400">
|
||||
Kills
|
||||
@@ -32,24 +37,28 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
|
||||
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2 items-center">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show all systems"
|
||||
value={showAll}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={onToggleShowAllVisible}
|
||||
/>
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<WdTooltipWrapper content="Show all systems" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? 'All' : 'Show all systems'}
|
||||
value={showAll}
|
||||
onChange={onToggleShowAllVisible}
|
||||
classNameLabel="whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.SLIDERS_H}
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{
|
||||
content: 'Open Kills Settings',
|
||||
position: TooltipPosition.left,
|
||||
}}
|
||||
/>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.SLIDERS_H}
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{
|
||||
content: 'Open Kills Settings',
|
||||
position: TooltipPosition.top,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// VictimSubRowInfo.tsx
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
@@ -15,13 +14,13 @@ export const VictimRowSubInfo: React.FC<VictimRowSubInfoProps> = ({
|
||||
victimPortraitUrl,
|
||||
victimCharName,
|
||||
}) => {
|
||||
if (!victimPortraitUrl || victimCharacterId === null || victimCharacterId <= 0) {
|
||||
if (!victimPortraitUrl || !victimCharacterId || victimCharacterId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-1 h-14">
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', victimCharacterId)}
|
||||
target="_blank"
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from './components';
|
||||
import { InterfaceStoredSettingsProps, useMapRootState, InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
InterfaceStoredSettingsProps,
|
||||
useMapRootState,
|
||||
InterfaceStoredSettings,
|
||||
AvailableThemes
|
||||
} 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';
|
||||
@@ -112,8 +117,8 @@ const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
];
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Pathfinder', value: 'pathfinder' },
|
||||
{ label: 'Default', value: AvailableThemes.default },
|
||||
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
|
||||
];
|
||||
|
||||
const THEME_SETTING: SettingsListItem = {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const MapWrapper = () => {
|
||||
const {
|
||||
update,
|
||||
outCommand,
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, connections, linkSignatureToSystem },
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem },
|
||||
interfaceSettings: {
|
||||
isShowMenu,
|
||||
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
|
||||
@@ -56,8 +56,8 @@ export const MapWrapper = () => {
|
||||
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
|
||||
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
|
||||
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems };
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
|
||||
|
||||
useMapEventListener(event => {
|
||||
runCommand(event);
|
||||
@@ -125,11 +125,6 @@ export const MapWrapper = () => {
|
||||
setOpenAddSystem(coordinates);
|
||||
}, []);
|
||||
|
||||
const canRemoveConnection = useCallback((connectionId: string) => {
|
||||
const { connections } = ref.current;
|
||||
return !connections.some(x => x.id === connectionId);
|
||||
}, []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => {
|
||||
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
|
||||
@@ -166,7 +161,6 @@ export const MapWrapper = () => {
|
||||
isSoftBackground={isSoftBackground}
|
||||
theme={theme}
|
||||
onAddSystem={onAddSystem}
|
||||
canRemoveConnection={canRemoveConnection}
|
||||
/>
|
||||
|
||||
{openSettings != null && (
|
||||
|
||||
@@ -26,12 +26,6 @@ export const getShipName = (name: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => (
|
||||
<span className="mx-1 text-gray-400" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
);
|
||||
|
||||
export const CharacterCard = ({
|
||||
compact = false,
|
||||
isOwn,
|
||||
@@ -50,6 +44,7 @@ export const CharacterCard = ({
|
||||
const shipNameText = char.ship?.ship_name ? getShipName(char.ship.ship_name) : '';
|
||||
const tickerText = char.alliance_id ? char.alliance_ticker : char.corporation_ticker;
|
||||
const shipType = char.ship?.ship_type_info?.name;
|
||||
const locationShown = showSystem && char.location?.solar_system_id;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
@@ -71,18 +66,19 @@ export const CharacterCard = ({
|
||||
/>
|
||||
<div className="flex flex-grow overflow-hidden text-left" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="text-gray-200">{char.name}</span>
|
||||
<Divider />
|
||||
{showShipName && shipNameText ? (
|
||||
<span className="text-indigo-300">{shipNameText}</span>
|
||||
) : (
|
||||
<span className="text-indigo-300">[{tickerText}]</span>
|
||||
)}
|
||||
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
|
||||
{char.name}
|
||||
</span>{" "}
|
||||
<span className="text-gray-400">
|
||||
{(!locationShown && showShipName && shipNameText)
|
||||
? `- ${shipNameText}`
|
||||
: `[${tickerText}]`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{shipType && (
|
||||
<div
|
||||
className="text-yellow-400 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
|
||||
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
|
||||
style={{ maxWidth: '120px' }}
|
||||
title={shipType}
|
||||
>
|
||||
@@ -93,8 +89,6 @@ export const CharacterCard = ({
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const locationShown = showSystem && char.location?.solar_system_id;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
@@ -113,14 +107,15 @@ export const CharacterCard = ({
|
||||
/>
|
||||
<div className="flex flex-col flex-grow overflow-hidden" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="text-gray-200">{char.name}</span>
|
||||
<Divider />
|
||||
<span className="text-indigo-300">[{tickerText}]</span>
|
||||
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
|
||||
{char.name}
|
||||
</span>{" "}
|
||||
<span className="text-gray-400">[{tickerText}]</span>
|
||||
</div>
|
||||
{locationShown ? (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<SystemView
|
||||
systemId={char?.location?.solar_system_id?.toString() || '' }
|
||||
systemId={char?.location?.solar_system_id?.toString() || ''}
|
||||
useSystemsCache={useSystemsCache}
|
||||
/>
|
||||
</div>
|
||||
@@ -132,25 +127,15 @@ export const CharacterCard = ({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{((shipType) || (locationShown && shipNameText)) && (
|
||||
{shipType && (
|
||||
<div className="flex-shrink-0 self-start">
|
||||
{shipType && (
|
||||
<div
|
||||
className="text-yellow-400 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
style={{ maxWidth: '200px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
)}
|
||||
{locationShown && shipNameText && (
|
||||
<div
|
||||
className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap text-right"
|
||||
style={{ maxWidth: '200px' }}
|
||||
>
|
||||
{shipNameText}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
style={{ maxWidth: '200px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,12 @@ import classes from './WdCheckbox.module.scss';
|
||||
import { Checkbox, CheckboxChangeEvent } from 'primereact/checkbox';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export interface WdCheckboxProps {
|
||||
label: string;
|
||||
label: React.ReactNode | string;
|
||||
classNameLabel?: string;
|
||||
value: boolean;
|
||||
labelSide?: 'left' | 'right';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
/**
|
||||
* Display modes for the responsive checkbox.
|
||||
*
|
||||
* - "full": show the full label (e.g. "Show offline" or "Show ship name")
|
||||
* - "abbr": show the abbreviated label (e.g. "Offline" or "Ship name")
|
||||
* - "checkbox": show only the checkbox (no text)
|
||||
* - "hide": do not render the checkbox at all
|
||||
*/
|
||||
export type WdDisplayMode = 'full' | 'abbr' | 'checkbox' | 'hide';
|
||||
|
||||
export interface WdResponsiveCheckboxProps {
|
||||
tooltipContent: string;
|
||||
size: 'xs' | 'normal' | 'm';
|
||||
labelFull: string;
|
||||
labelAbbreviated: string;
|
||||
value: boolean;
|
||||
onChange: () => void;
|
||||
classNameLabel?: string;
|
||||
containerClassName?: string;
|
||||
labelSide?: 'left' | 'right';
|
||||
displayMode: WdDisplayMode;
|
||||
}
|
||||
|
||||
export const WdResponsiveCheckbox: React.FC<WdResponsiveCheckboxProps> = ({
|
||||
tooltipContent,
|
||||
size,
|
||||
labelFull,
|
||||
labelAbbreviated,
|
||||
value,
|
||||
onChange,
|
||||
classNameLabel,
|
||||
containerClassName,
|
||||
labelSide = 'left',
|
||||
displayMode,
|
||||
}) => {
|
||||
if (displayMode === 'hide') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
displayMode === 'full'
|
||||
? labelFull
|
||||
: displayMode === 'abbr'
|
||||
? labelAbbreviated
|
||||
: displayMode === 'checkbox'
|
||||
? ''
|
||||
: labelFull;
|
||||
|
||||
const checkbox = (
|
||||
<div className={clsx('min-w-0', containerClassName)}>
|
||||
<WdCheckbox
|
||||
size={size}
|
||||
labelSide={labelSide}
|
||||
label={label}
|
||||
value={value}
|
||||
classNameLabel={classNameLabel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return tooltipContent ? <WdTooltipWrapper content={tooltipContent}>{checkbox}</WdTooltipWrapper> : checkbox;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WdResponsiveCheckbox';
|
||||
@@ -1,13 +1,4 @@
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash.debounce';
|
||||
@@ -58,11 +49,11 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
ref: ForwardedRef<WdTooltipHandlers>
|
||||
ref: ForwardedRef<WdTooltipHandlers>,
|
||||
) {
|
||||
// Always initialize position so we never have a null value.
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<OffsetPosition>({ left: 0, top: 0 });
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
@@ -80,8 +71,13 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
|
||||
if (newLeft < 0) newLeft = 10;
|
||||
if (newTop < 0) newTop = 10;
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 10;
|
||||
}
|
||||
|
||||
const rightEdge = newLeft + tooltipWidth + 10;
|
||||
if (rightEdge > window.innerWidth) {
|
||||
@@ -99,11 +95,13 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (!interactive) {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
return;
|
||||
}
|
||||
if (!hideTimeoutRef.current) {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
}, [interactive]);
|
||||
@@ -130,12 +128,12 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
// Recalculate position once the tooltip element has been rendered.
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !triggerInfo) return;
|
||||
|
||||
const tooltipEl = tooltipRef.current;
|
||||
@@ -147,30 +145,36 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
x = rect.left - tooltipBounds.width - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default case: use stored coordinates.
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
|
||||
@@ -184,6 +188,7 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerEl = targetEl.closest(targetSelector);
|
||||
const insideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
||||
|
||||
@@ -196,6 +201,7 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
@@ -206,30 +212,26 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
let y = evt.clientY;
|
||||
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left: {
|
||||
case TooltipPosition.left:
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.right: {
|
||||
case TooltipPosition.right:
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.top: {
|
||||
case TooltipPosition.top:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.bottom: {
|
||||
case TooltipPosition.bottom:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
@@ -237,13 +239,10 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
};
|
||||
|
||||
const debounced = debounce(handleMouseMove, 15);
|
||||
const listener = (evt: Event) => {
|
||||
debounced(evt as MouseEvent);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', listener);
|
||||
document.addEventListener('mousemove', debounced);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', listener);
|
||||
document.removeEventListener('mousemove', debounced);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
@@ -265,11 +264,12 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
className
|
||||
className,
|
||||
pos === null ? 'invisible' : '',
|
||||
)}
|
||||
style={{
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
@@ -289,7 +289,7 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>,
|
||||
document.body
|
||||
document.body,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, HTMLProps, ReactNode } from 'react';
|
||||
import { forwardRef, HTMLProps, ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltip, WdTooltipHandlers, TooltipProps } from '@/hooks/Mapper/components/ui-kit';
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
@@ -13,11 +13,8 @@ export type WdTooltipWrapperProps = {
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
(
|
||||
{ className, children, content, offset, position, targetSelector, interactive = false, size, ...props },
|
||||
forwardedRef,
|
||||
) => {
|
||||
const suffix = Math.random().toString(36).slice(2, 7);
|
||||
({ className, children, content, offset, position, targetSelector, interactive, size, ...props }, forwardedRef) => {
|
||||
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
|
||||
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
|
||||
const finalTargetSelector = targetSelector || `.${autoClass}`;
|
||||
|
||||
|
||||
@@ -11,3 +11,5 @@ export * from './WdImgButton';
|
||||
export * from './WdTooltip';
|
||||
export * from './WdCheckbox';
|
||||
export * from './TimeAgo';
|
||||
export * from './WdTooltipWrapper';
|
||||
export * from './WdResponsiveCheckBox';
|
||||
|
||||
7
assets/js/hooks/Mapper/hooks/useTheme.ts
Normal file
7
assets/js/hooks/Mapper/hooks/useTheme.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AvailableThemes, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const useTheme = (): AvailableThemes => {
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
|
||||
return interfaceSettings.theme;
|
||||
};
|
||||
@@ -46,6 +46,11 @@ const INITIAL_DATA: MapRootData = {
|
||||
linkSignatureToSystem: null,
|
||||
};
|
||||
|
||||
export enum AvailableThemes {
|
||||
default = 'default',
|
||||
pathfinder = 'pathfinder',
|
||||
}
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
isShowMenu = 'isShowMenu',
|
||||
isShowMinimap = 'isShowMinimap',
|
||||
@@ -65,7 +70,7 @@ export type InterfaceStoredSettings = {
|
||||
isShowUnsplashedSignatures: boolean;
|
||||
isShowBackgroundPattern: boolean;
|
||||
isSoftBackground: boolean;
|
||||
theme: string;
|
||||
theme: AvailableThemes;
|
||||
};
|
||||
|
||||
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
@@ -76,7 +81,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
isShowUnsplashedSignatures: false,
|
||||
isShowBackgroundPattern: true,
|
||||
isSoftBackground: false,
|
||||
theme: 'default',
|
||||
theme: AvailableThemes.default,
|
||||
};
|
||||
|
||||
export interface MapRootContextProps {
|
||||
|
||||
@@ -107,6 +107,11 @@ admins =
|
||||
admins -> admins |> String.split(",")
|
||||
end
|
||||
|
||||
restrict_maps_creation =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_RESTRICT_MAPS_CREATION", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
config :wanderer_app,
|
||||
web_app_url: web_app_url,
|
||||
git_sha: System.get_env("GIT_SHA", "111"),
|
||||
@@ -124,6 +129,7 @@ config :wanderer_app,
|
||||
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
|
||||
map_connection_eol_expire_timeout_mins: map_connection_eol_expire_timeout_mins,
|
||||
wallet_tracking_enabled: wallet_tracking_enabled,
|
||||
restrict_maps_creation: restrict_maps_creation,
|
||||
subscription_settings: %{
|
||||
plans: [
|
||||
%{
|
||||
|
||||
@@ -238,7 +238,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"corporation",
|
||||
{:transactions, character.corporation_id, transactions}
|
||||
{:transactions, character.corporation_id, transactions |> Enum.sort_by(& &1.date, :desc)}
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ defmodule WandererApp.Env do
|
||||
def corp_eve_id, do: get_key(:corp_id, -1)
|
||||
def subscription_settings, do: get_key(:subscription_settings)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
)
|
||||
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-auto-expire-hours"
|
||||
|
||||
@@ -13,27 +13,19 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
|
||||
case header do
|
||||
"Bearer " <> incoming_token ->
|
||||
case fetch_map_id(conn.query_params) do
|
||||
{:ok, map_id} ->
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, map} ->
|
||||
if map.public_api_key == incoming_token do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> send_resp(401, "Unauthorized (invalid token for map)")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> send_resp(404, "Map not found")
|
||||
|> halt()
|
||||
case fetch_map(conn.query_params) do
|
||||
{:ok, map} ->
|
||||
if map.public_api_key == incoming_token do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> send_resp(401, "Unauthorized (invalid token for map)")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
{:error, msg} ->
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> send_resp(400, msg)
|
||||
|> send_resp(404, "Map not found")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
@@ -44,6 +36,19 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map(query_params) do
|
||||
case fetch_map_id(query_params) do
|
||||
{:ok, {:map, map}} ->
|
||||
{:ok, map}
|
||||
|
||||
{:ok, map_id} ->
|
||||
WandererApp.Api.Map.by_id(map_id)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
@@ -51,7 +56,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} ->
|
||||
{:ok, map.id}
|
||||
{:ok, {:map, map}}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:error, "No map found for slug=#{slug}"}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckMapSubscription do
|
||||
@moduledoc """
|
||||
A plug that checks the Map has active subscription
|
||||
Halts with 401 if no active subscription.
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
case fetch_map_id(conn.query_params) do
|
||||
{:ok, map_id} ->
|
||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||
|
||||
if is_subscription_active do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> send_resp(401, "Unauthorized (map subscription not active)")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
{:error, msg} ->
|
||||
conn
|
||||
|> send_resp(400, msg)
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} ->
|
||||
{:ok, map.id}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:error, "No map found for slug=#{slug}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
end
|
||||
@@ -62,7 +62,8 @@ defmodule WandererAppWeb.AdminLive do
|
||||
user_character_ids: user_character_ids,
|
||||
user_id: user_id,
|
||||
invite_link: nil,
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?()
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
|
||||
restrict_maps_creation?: WandererApp.Env.restrict_maps_creation?()
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -207,6 +208,22 @@ defmodule WandererAppWeb.AdminLive do
|
||||
|> put_flash(:info, "Character unlinked.")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"create-map",
|
||||
_params,
|
||||
socket
|
||||
) do
|
||||
WandererApp.Cache.put(
|
||||
"create_map_once",
|
||||
true
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_navigate(to: ~p"/maps/new")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(event, body, socket) do
|
||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 2xl:grid-cols-4 pb-6">
|
||||
<div :if={@restrict_maps_creation?} class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<.button class="mt-2" type="button" phx-click="create-map">
|
||||
Create Map
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
<div :if={@map_subscriptions_enabled?} class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<div class="col-span-6">
|
||||
|
||||
@@ -13,7 +13,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
%{"user_id" => user_id} = _session,
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(user_id) do
|
||||
when not is_nil(user_id) and is_connected?(socket) do
|
||||
{:ok, active_characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
user_characters =
|
||||
@@ -26,6 +26,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
characters: user_characters,
|
||||
importing: false,
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
|
||||
restrict_maps_creation?: WandererApp.Env.restrict_maps_creation?(),
|
||||
acls: [],
|
||||
location: nil
|
||||
)
|
||||
@@ -40,8 +41,16 @@ defmodule WandererAppWeb.MapsLive do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params, url)}
|
||||
def handle_params(params, url, socket) when is_connected?(socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:is_connected?, true)
|
||||
|> apply_action(socket.assigns.live_action, params, url)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(_params, _url, socket) do
|
||||
{:noreply, socket |> assign(:is_connected?, false)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params, _url) do
|
||||
@@ -51,24 +60,32 @@ defmodule WandererAppWeb.MapsLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :create, _params, url) do
|
||||
socket
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Create")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.Map, :new,
|
||||
forms: [
|
||||
auto?: true
|
||||
],
|
||||
prepare_source: fn form ->
|
||||
form
|
||||
|> Map.put("scope", "wormholes")
|
||||
end
|
||||
)
|
||||
)
|
||||
|> load_access_lists()
|
||||
allow_map_creation()
|
||||
|> case do
|
||||
true ->
|
||||
socket
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Create")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.Map, :new,
|
||||
forms: [
|
||||
auto?: true
|
||||
],
|
||||
prepare_source: fn form ->
|
||||
form
|
||||
|> Map.put("scope", "wormholes")
|
||||
end
|
||||
)
|
||||
)
|
||||
|> load_access_lists()
|
||||
|
||||
_ ->
|
||||
socket
|
||||
|> push_patch(to: ~p"/maps")
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"slug" => map_slug} = _params, url) do
|
||||
@@ -166,6 +183,9 @@ defmodule WandererAppWeb.MapsLive do
|
||||
)
|
||||
end
|
||||
|
||||
defp allow_map_creation(),
|
||||
do: not WandererApp.Env.restrict_maps_creation?() || WandererApp.Cache.take("create_map_once")
|
||||
|
||||
@impl true
|
||||
def handle_event("set-default", %{"id" => id}, socket) do
|
||||
send_update(LiveSelect.Component, options: socket.assigns.characters, id: id)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<%= if @maps != [] do %>
|
||||
<div class="gap-4 grid grid-cols-2 lg:grid-cols-5 md:grid-cols-4 sm:grid-cols-3 ">
|
||||
<.link
|
||||
:if={not @restrict_maps_creation?}
|
||||
class="card h-[250px] rounded-none bg-gradient-to-l from-stone-950 to-stone-900 hover:text-white transform transition duration-500"
|
||||
patch={~p"/maps/new"}
|
||||
>
|
||||
@@ -125,7 +126,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<.modal
|
||||
:if={@live_action in [:create, :edit]}
|
||||
:if={@is_connected? && @live_action in [:create, :edit]}
|
||||
title={"#{(@live_action == :create && "Create") || "Edit"} Map"}
|
||||
class="!w-[500px]"
|
||||
id="add_map_modal"
|
||||
|
||||
@@ -24,7 +24,6 @@ defmodule WandererAppWeb.Router do
|
||||
@font_src ~w('self' https://fonts.gstatic.com data: https://web.ccpgamescdn.com https://w.appzi.io )
|
||||
@script_src ~w('self' )
|
||||
|
||||
|
||||
pipeline :admin_bauth do
|
||||
plug :admin_basic_auth
|
||||
end
|
||||
@@ -112,6 +111,7 @@ defmodule WandererAppWeb.Router do
|
||||
|
||||
pipeline :api_map do
|
||||
plug WandererAppWeb.Plugs.CheckMapApiKey
|
||||
plug WandererAppWeb.Plugs.CheckMapSubscription
|
||||
end
|
||||
|
||||
pipeline :api_kills do
|
||||
@@ -145,7 +145,6 @@ defmodule WandererAppWeb.Router do
|
||||
|
||||
# GET /api/common/system-static-info?id=...
|
||||
get "/system-static-info", CommonAPIController, :show_system_static
|
||||
|
||||
end
|
||||
|
||||
scope "/", WandererAppWeb do
|
||||
|
||||
Reference in New Issue
Block a user