feat: add selectable sig deletion timing, and color options (#208)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions

This commit is contained in:
guarzo
2025-03-04 14:51:10 -05:00
committed by GitHub
parent 2a0d7654e7
commit 6c22e6554d
19 changed files with 478 additions and 112 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import { useCallback, useRef, useMemo } from 'react';
import { Dialog } from 'primereact/dialog';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
@@ -14,6 +14,15 @@ import {
import { SignatureGroup } from '@/hooks/Mapper/types';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import {
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
interface SystemLinkSignatureDialogProps {
data: CommandLinkSignatureToSystem;
@@ -26,6 +35,13 @@ const signatureSettings: Setting[] = [
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: true, isFilter: false },
];
// Extend the SignatureCustomInfo type to include k162Type
interface ExtendedSignatureCustomInfo {
k162Type?: string;
isEOL?: boolean;
[key: string]: unknown;
}
export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignatureDialogProps) => {
const {
outCommand,
@@ -35,10 +51,74 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
const ref = useRef({ outCommand });
ref.current = { outCommand };
// Get system info for the target system
const { staticInfo: targetSystemInfo } = useSystemInfo({ systemId: `${data.solar_system_target}` });
// Get the system class group for the target system
const targetSystemClassGroup = useMemo(() => {
if (!targetSystemInfo) return null;
const systemClassId = targetSystemInfo.system_class;
const systemClassKey = Object.keys(SOLAR_SYSTEM_CLASS_IDS).find(
key => SOLAR_SYSTEM_CLASS_IDS[key as keyof typeof SOLAR_SYSTEM_CLASS_IDS] === systemClassId,
);
if (!systemClassKey) return null;
return (
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS[systemClassKey as keyof typeof SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS] || null
);
}, [targetSystemInfo]);
const handleHide = useCallback(() => {
setVisible(false);
}, [setVisible]);
const filterSignature = useCallback(
(signature: SystemSignature) => {
if (signature.group !== SignatureGroup.Wormhole || !targetSystemClassGroup) {
return true;
}
if (!signature.type) {
return true;
}
if (signature.type === K162_SIGNATURE_TYPE) {
// Parse the custom info to see if the user has specified what class this K162 leads to
const customInfo = parseSignatureCustomInfo(signature.custom_info) as ExtendedSignatureCustomInfo;
// If the user has specified a k162Type for this K162
if (customInfo.k162Type) {
// Get the K162 type information
const k162TypeInfo = K162_TYPES_MAP[customInfo.k162Type];
if (k162TypeInfo) {
// Check if the k162Type matches our target system class
return customInfo.k162Type === targetSystemClassGroup;
}
}
// If no k162Type is specified or we couldn't find type info, allow it
return true;
}
// Find the wormhole data for this signature type
const wormholeData = wormholes.find(wh => wh.name === signature.type);
if (!wormholeData) {
return true; // If we don't know the destination, don't filter it out
}
// Get the destination system class from the wormhole data
const destinationClass = wormholeData.dest;
// Check if the destination class matches the target system class
const isMatch = destinationClass === targetSystemClassGroup;
return isMatch;
},
[targetSystemClassGroup, wormholes],
);
const handleSelect = useCallback(
async (signature: SystemSignature) => {
if (!signature) {
@@ -80,7 +160,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
setVisible(false);
},
[data, setVisible],
[data, setVisible, wormholes],
);
return (
@@ -98,6 +178,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
settings={signatureSettings}
onSelect={handleSelect}
selectable={true}
filterSignature={filterSignature}
/>
</Dialog>
);

View File

@@ -1,5 +1,5 @@
const ZKILL_URL = 'https://zkillboard.com';
const BASE_IMAGE_URL = 'https://images.evetech.net';
import { getEveImageUrl } from '@/hooks/Mapper/helpers';
export function zkillLink(type: 'kill' | 'character' | 'corporation' | 'alliance', id?: number | null): string {
if (!id) return `${ZKILL_URL}`;
@@ -10,21 +10,7 @@ export function zkillLink(type: 'kill' | 'character' | 'corporation' | 'alliance
return `${ZKILL_URL}`;
}
export function eveImageUrl(
category: 'characters' | 'corporations' | 'alliances' | 'types',
id?: number | null,
variation: string = 'icon',
size?: number,
): string | null {
if (!id || id <= 0) {
return null;
}
let url = `${BASE_IMAGE_URL}/${category}/${id}/${variation}`;
if (size) {
url += `?size=${size}`;
}
return url;
}
export const eveImageUrl = getEveImageUrl;
export function buildVictimImageUrls(args: {
victim_char_id?: number | null;

View File

@@ -1,15 +1,36 @@
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { renderIcon } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { getCharacterPortraitUrl } from '@/hooks/Mapper/helpers';
export interface SignatureViewProps {}
export interface SignatureViewProps {
signature: SystemSignature;
showCharacterPortrait?: boolean;
}
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
const isWormhole = signature?.group === SignatureGroup.Wormhole;
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature;
const characterName = signature.character_name || 'Unknown character';
export const SignatureView = (sig: SignatureViewProps & SystemSignature) => {
return (
<div className="flex gap-2 items-center">
{renderIcon(sig)}
<div>{sig?.eve_id}</div>
<div>{sig?.group ?? SignatureGroup.CosmicSignature}</div>
<div>{sig?.name}</div>
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
{renderIcon(signature)}
<div>{signature?.eve_id}</div>
<div>{groupDisplay}</div>
{!isWormhole && <div>{signature?.name}</div>}
{hasCharacterInfo && (
<div className="flex items-center gap-1 ml-2 pl-2 border-l border-stone-700">
<img
src={getCharacterPortraitUrl(signature.character_eve_id)}
alt={characterName}
className="w-5 h-5 rounded-sm border border-stone-700"
/>
<div className="text-xs text-stone-300">{characterName}</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -13,6 +13,7 @@ export type HeaderProps = {
lazyDeleteValue: boolean;
onLazyDeleteChange: (checked: boolean) => void;
pendingCount: number;
pendingTimeRemaining?: number; // Time remaining in ms
onUndoClick: () => void;
onSettingsClick: () => void;
};
@@ -25,9 +26,17 @@ function HeaderImpl({
lazyDeleteValue,
onLazyDeleteChange,
pendingCount,
pendingTimeRemaining,
onUndoClick,
onSettingsClick,
}: HeaderProps) {
// Format time remaining as seconds
const formatTimeRemaining = () => {
if (!pendingTimeRemaining) return '';
const seconds = Math.ceil(pendingTimeRemaining / 1000);
return ` (${seconds}s remaining)`;
};
return (
<div className="flex justify-between items-center text-xs w-full h-full">
<div className="flex justify-between items-center gap-1">
@@ -55,7 +64,10 @@ function HeaderImpl({
<WdImgButton
className={PrimeIcons.UNDO}
style={{ color: 'red' }}
tooltip={{ content: `Undo pending changes (${pendingCount})` }}
tooltip={{
content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}`,
position: TooltipPosition.top,
}}
onClick={onUndoClick}
/>
)}

View File

@@ -4,8 +4,15 @@ import { Button } from 'primereact/button';
import { TabPanel, TabView } from 'primereact/tabview';
import styles from './SystemSignatureSettingsDialog.module.scss';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
export type Setting = { key: string; name: string; value: boolean; isFilter?: boolean };
export type Setting = {
key: string;
name: string;
value: boolean | number;
isFilter?: boolean;
options?: { label: string; value: number }[];
};
export const COSMIC_SIGNATURE = 'Cosmic Signature';
export const COSMIC_ANOMALY = 'Cosmic Anomaly';
@@ -33,13 +40,49 @@ export const SystemSignatureSettingsDialog = ({
const userSettings = settings.filter(setting => !setting.isFilter);
const handleSettingsChange = (key: string) => {
setSettings(prevState => prevState.map(item => (item.key === key ? { ...item, value: !item.value } : item)));
setSettings(prevState =>
prevState.map(item =>
item.key === key ? { ...item, value: typeof item.value === 'boolean' ? !item.value : item.value } : item,
),
);
};
const handleDropdownChange = (key: string, value: number) => {
setSettings(prevState => prevState.map(item => (item.key === key ? { ...item, value } : item)));
};
const handleSave = useCallback(() => {
onSave(settings);
}, [onSave, settings]);
const renderSetting = (setting: Setting) => {
if (setting.options) {
return (
<div key={setting.key} className="flex items-center justify-between gap-2 mb-2">
<label className="text-[#b8b8b8] text-[13px] select-none">{setting.name}</label>
<Dropdown
value={setting.value}
options={setting.options.map(opt => ({
...opt,
label: opt.label.split(' ')[0], // Just take the first part (e.g., "0s" from "Immediate (0s)")
}))}
onChange={e => handleDropdownChange(setting.key, e.value)}
className="w-40"
/>
</div>
);
}
return (
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={!!setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
};
return (
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg h-[500px]">
<div className="flex flex-col gap-3 justify-between h-full">
@@ -51,31 +94,15 @@ export const SystemSignatureSettingsDialog = ({
className={styles.verticalTabView}
>
<TabPanel header="Filters" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{filterSettings.map(setting => {
return (
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
</div>
<div className="w-full h-full flex flex-col gap-1">{filterSettings.map(renderSetting)}</div>
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{userSettings.map(setting => {
return (
<PrettySwitchbox
key={setting.key}
label={setting.name}
checked={setting.value}
setChecked={() => handleSettingsChange(setting.key)}
/>
);
})}
{userSettings.filter(setting => !setting.options).map(renderSetting)}
{userSettings.some(setting => setting.options) && (
<div className="my-2 border-t border-stone-700/50"></div>
)}
{userSettings.filter(setting => setting.options).map(renderSetting)}
</div>
</TabPanel>
</TabView>

View File

@@ -16,7 +16,13 @@ import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { COMPACT_MAX_WIDTH } from './constants';
import {
COMPACT_MAX_WIDTH,
DELETION_TIMING_DEFAULT,
DELETION_TIMING_EXTENDED,
DELETION_TIMING_IMMEDIATE,
DELETION_TIMING_SETTING_KEY,
} from './constants';
import { renderHeaderLabel } from './renders';
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_5';
@@ -26,13 +32,35 @@ export const SHOW_UPDATED_COLUMN_SETTING = 'SHOW_UPDATED_COLUMN_SETTING';
export const SHOW_CHARACTER_COLUMN_SETTING = 'SHOW_CHARACTER_COLUMN_SETTING';
export const LAZY_DELETE_SIGNATURES_SETTING = 'LAZY_DELETE_SIGNATURES_SETTING';
export const KEEP_LAZY_DELETE_SETTING = 'KEEP_LAZY_DELETE_ENABLED_SETTING';
// eslint-disable-next-line react-refresh/only-export-components
export const DELETION_TIMING_SETTING = DELETION_TIMING_SETTING_KEY;
export const COLOR_BY_TYPE_SETTING = 'COLOR_BY_TYPE_SETTING';
export const SHOW_CHARACTER_PORTRAIT_SETTING = 'SHOW_CHARACTER_PORTRAIT_SETTING';
const SETTINGS: Setting[] = [
// Extend the Setting type to include options for dropdown settings
type ExtendedSetting = Setting & {
options?: { label: string; value: number }[];
};
const SETTINGS: ExtendedSetting[] = [
{ key: SHOW_UPDATED_COLUMN_SETTING, name: 'Show Updated Column', value: false, isFilter: false },
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: false, isFilter: false },
{ key: SHOW_CHARACTER_COLUMN_SETTING, name: 'Show Character Column', value: false, isFilter: false },
{ key: SHOW_CHARACTER_PORTRAIT_SETTING, name: 'Show Character Portrait in Tooltip', value: false, isFilter: false },
{ key: LAZY_DELETE_SIGNATURES_SETTING, name: 'Lazy Delete Signatures', value: false, isFilter: false },
{ key: KEEP_LAZY_DELETE_SETTING, name: 'Keep "Lazy Delete" Enabled', value: false, isFilter: false },
{ key: COLOR_BY_TYPE_SETTING, name: 'Color Signatures by Type', value: false, isFilter: false },
{
key: DELETION_TIMING_SETTING,
name: 'Deletion Timing',
value: DELETION_TIMING_DEFAULT,
isFilter: false,
options: [
{ label: '0s', value: DELETION_TIMING_IMMEDIATE },
{ label: '10s', value: DELETION_TIMING_DEFAULT },
{ label: '30s', value: DELETION_TIMING_EXTENDED },
],
},
{ key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true, isFilter: true },
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true, isFilter: true },
@@ -49,10 +77,36 @@ const SETTINGS: Setting[] = [
{ key: SignatureGroup.CombatSite, name: 'Show Combat Sites', value: true, isFilter: true },
];
function getDefaultSettings(): Setting[] {
function getDefaultSettings(): ExtendedSetting[] {
return [...SETTINGS];
}
function getInitialSettings(): ExtendedSetting[] {
const stored = localStorage.getItem(SIGNATURE_SETTINGS_KEY);
if (stored) {
try {
const parsedSettings = JSON.parse(stored) as ExtendedSetting[];
// Merge stored settings with default settings to ensure new settings are included
const defaultSettings = getDefaultSettings();
const mergedSettings = defaultSettings.map(defaultSetting => {
const storedSetting = parsedSettings.find(s => s.key === defaultSetting.key);
if (storedSetting) {
// Keep the stored value but ensure options are from default settings
return {
...defaultSetting,
value: storedSetting.value,
};
}
return defaultSetting;
});
return mergedSettings;
} catch (error) {
console.error('Error parsing stored settings', error);
}
}
return getDefaultSettings();
}
export const SystemSignatures: React.FC = () => {
const {
data: { selectedSystems },
@@ -60,17 +114,7 @@ export const SystemSignatures: React.FC = () => {
const [visible, setVisible] = useState(false);
const [currentSettings, setCurrentSettings] = useState<Setting[]>(() => {
const stored = localStorage.getItem(SIGNATURE_SETTINGS_KEY);
if (stored) {
try {
return JSON.parse(stored) as Setting[];
} catch (error) {
console.error('Error parsing stored settings', error);
}
}
return getDefaultSettings();
});
const [currentSettings, setCurrentSettings] = useState<ExtendedSetting[]>(getInitialSettings);
useEffect(() => {
localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(currentSettings));
@@ -78,6 +122,7 @@ export const SystemSignatures: React.FC = () => {
const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [minPendingTimeRemaining, setMinPendingTimeRemaining] = useState<number | undefined>(undefined);
const undoPendingFnRef = useRef<() => void>(() => {});
@@ -88,13 +133,23 @@ export const SystemSignatures: React.FC = () => {
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
const lazyDeleteValue = useMemo(
() => currentSettings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)?.value || false,
[currentSettings],
);
const lazyDeleteValue = useMemo(() => {
const setting = currentSettings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING);
return typeof setting?.value === 'boolean' ? setting.value : false;
}, [currentSettings]);
const deletionTimingValue = useMemo(() => {
const setting = currentSettings.find(setting => setting.key === DELETION_TIMING_SETTING);
return typeof setting?.value === 'number' ? setting.value : DELETION_TIMING_IMMEDIATE;
}, [currentSettings]);
const colorByTypeValue = useMemo(() => {
const setting = currentSettings.find(setting => setting.key === COLOR_BY_TYPE_SETTING);
return typeof setting?.value === 'boolean' ? setting.value : false;
}, [currentSettings]);
const handleSettingsChange = useCallback((newSettings: Setting[]) => {
setCurrentSettings(newSettings);
setCurrentSettings(newSettings as ExtendedSetting[]);
setVisible(false);
}, []);
@@ -113,12 +168,14 @@ export const SystemSignatures: React.FC = () => {
event.stopPropagation();
undoPendingFnRef.current();
setPendingSigs([]);
setMinPendingTimeRemaining(undefined);
}
});
const handleUndoClick = useCallback(() => {
undoPendingFnRef.current();
setPendingSigs([]);
setMinPendingTimeRemaining(undefined);
}, []);
const handleSettingsButtonClick = useCallback(() => {
@@ -135,6 +192,32 @@ export const SystemSignatures: React.FC = () => {
undoPendingFnRef.current = newUndo;
}, []);
// Calculate the minimum time remaining for any pending signature
useEffect(() => {
if (pendingSigs.length === 0) {
setMinPendingTimeRemaining(undefined);
return;
}
const calculateTimeRemaining = () => {
const now = Date.now();
let minTime: number | undefined = undefined;
pendingSigs.forEach(sig => {
const extendedSig = sig as unknown as { pendingUntil?: number };
if (extendedSig.pendingUntil && (minTime === undefined || extendedSig.pendingUntil - now < minTime)) {
minTime = extendedSig.pendingUntil - now;
}
});
setMinPendingTimeRemaining(minTime && minTime > 0 ? minTime : undefined);
};
calculateTimeRemaining();
const interval = setInterval(calculateTimeRemaining, 1000);
return () => clearInterval(interval);
}, [pendingSigs]);
return (
<Widget
label={
@@ -146,6 +229,7 @@ export const SystemSignatures: React.FC = () => {
sigCount,
lazyDeleteValue,
pendingCount: pendingSigs.length,
pendingTimeRemaining: minPendingTimeRemaining,
onLazyDeleteChange: handleLazyDeleteChange,
onUndoClick: handleUndoClick,
onSettingsClick: handleSettingsButtonClick,
@@ -165,6 +249,8 @@ export const SystemSignatures: React.FC = () => {
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={handlePendingChange}
deletionTiming={deletionTimingValue}
colorByType={colorByTypeValue}
/>
)}
{visible && (

View File

@@ -20,6 +20,7 @@ import {
SHOW_UPDATED_COLUMN_SETTING,
SHOW_CHARACTER_COLUMN_SETTING,
SIGNATURE_WINDOW_ID,
SHOW_CHARACTER_PORTRAIT_SETTING,
} from '../SystemSignatures';
import { COSMIC_SIGNATURE } from '../SystemSignatureSettingsDialog';
@@ -48,13 +49,16 @@ const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
interface SystemSignaturesContentProps {
systemId: string;
settings: { key: string; value: boolean }[];
settings: { key: string; value: boolean | number }[];
hideLinkedSignatures?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void;
deletionTiming?: number;
colorByType?: boolean;
filterSignature?: (signature: SystemSignature) => boolean;
}
const headerInlineStyle = { padding: '2px', fontSize: '12px', lineHeight: '1.333' };
@@ -68,6 +72,9 @@ export function SystemSignaturesContent({
onLazyDeleteChange,
onCountChange,
onPendingChange,
deletionTiming,
colorByType,
filterSignature,
}: SystemSignaturesContentProps) {
const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } =
useSystemSignaturesData({
@@ -76,6 +83,7 @@ export function SystemSignaturesContent({
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
});
const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>(
@@ -98,7 +106,7 @@ export function SystemSignaturesContent({
handlePaste(clipboardContent.text);
setClipboardContent(null);
}, [selectable, clipboardContent]);
}, [selectable, clipboardContent, handlePaste, setClipboardContent]);
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
@@ -152,6 +160,7 @@ export function SystemSignaturesContent({
const showDescriptionColumn = settings.find(s => s.key === SHOW_DESCRIPTION_COLUMN_SETTING)?.value;
const showUpdatedColumn = settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value;
const showCharacterColumn = settings.find(s => s.key === SHOW_CHARACTER_COLUMN_SETTING)?.value;
const showCharacterPortrait = settings.find(s => s.key === SHOW_CHARACTER_PORTRAIT_SETTING)?.value;
const enabledGroups = settings
.filter(s => GROUPS_LIST.includes(s.key as SignatureGroup) && s.value === true)
@@ -159,6 +168,10 @@ export function SystemSignaturesContent({
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
return signatures.filter(sig => {
if (filterSignature && !filterSignature(sig)) {
return false;
}
if (hideLinkedSignatures && sig.linked_system) {
return false;
}
@@ -176,7 +189,7 @@ export function SystemSignaturesContent({
return settings.find(y => y.key === sig.kind)?.value;
}
});
}, [signatures, hideLinkedSignatures, settings, enabledGroups]);
}, [signatures, hideLinkedSignatures, settings, enabledGroups, filterSignature]);
return (
<div ref={tableRef} className="h-full">
@@ -201,23 +214,17 @@ export function SystemSignaturesContent({
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={e => setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder })}
onRowMouseEnter={
isCompact || isMedium
? (e: DataTableRowMouseEvent) => {
setHoveredSignature(filteredSignatures[e.index]);
tooltipRef.current?.show(e.originalEvent);
}
: undefined
onRowMouseEnter={(e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature);
tooltipRef.current?.show(e.originalEvent);
}}
onRowMouseLeave={() => {
setHoveredSignature(null);
tooltipRef.current?.hide();
}}
rowClassName={rowData =>
getSignatureRowClass(rowData as ExtendedSystemSignature, selectedSignatures, colorByType)
}
onRowMouseLeave={
isCompact || isMedium
? () => {
setHoveredSignature(null);
tooltipRef.current?.hide();
}
: undefined
}
rowClassName={rowData => getSignatureRowClass(rowData as ExtendedSystemSignature, selectedSignatures)}
>
<Column
field="icon"
@@ -318,7 +325,11 @@ export function SystemSignaturesContent({
<WdTooltip
className="bg-stone-900/95 text-slate-50"
ref={tooltipRef}
content={hoveredSignature ? <SignatureView {...hoveredSignature} /> : null}
content={
hoveredSignature ? (
<SignatureView signature={hoveredSignature} showCharacterPortrait={!!showCharacterPortrait} />
) : null
}
/>
{showSignatureSettings && (

View File

@@ -14,6 +14,12 @@ export const TIME_ONE_DAY = 24 * 60 * 60 * 1000;
export const TIME_ONE_WEEK = 7 * TIME_ONE_DAY;
export const FINAL_DURATION_MS = 10000;
// Signature deletion timing options
export const DELETION_TIMING_IMMEDIATE = 0;
export const DELETION_TIMING_DEFAULT = 10000;
export const DELETION_TIMING_EXTENDED = 30000;
export const DELETION_TIMING_SETTING_KEY = 'DELETION_TIMING_SETTING';
export const COMPACT_MAX_WIDTH = 260;
export const MEDIUM_MAX_WIDTH = 380;
export const OTHER_COLUMNS_WIDTH = 276;

View File

@@ -4,7 +4,7 @@ import { getState } from './getState';
/**
* Compare two lists of signatures and return which are added, updated, or removed.
*
*
* @param oldSignatures existing signatures (in memory or from server)
* @param newSignatures newly parsed or incoming signatures from user input
* @param updateOnly if true, do NOT remove old signatures not found in newSignatures

View File

@@ -12,11 +12,11 @@ export const getRowBackgroundColor = (date: Date | undefined): string => {
const diff = currentDate.getTime() + currentDate.getTimezoneOffset() * TIME_ONE_MINUTE - date.getTime();
if (diff < TIME_ONE_MINUTE) {
return 'bg-lime-600/50 transition hover:bg-lime-600/60';
return 'bg-lime-600/40 transition hover:bg-lime-600/50';
}
if (diff < TIME_TEN_MINUTES) {
return 'bg-lime-700/40 transition hover:bg-lime-700/50';
return 'bg-lime-700/30 transition hover:bg-lime-700/40';
}
return '';

View File

@@ -1,17 +1,19 @@
.pendingDeletion {
background-color: #f87171;
background-color: rgba(248, 113, 113, 0.4);
transition: background-color 0.2s ease;
}
.pendingDeletion td {
background-color: #f87171;
background-color: rgba(248, 113, 113, 0.4);
transition: background-color 0.2s ease;
}
.pendingDeletion {
background-color: #f87171;
transition: background-color 0.2s ease;
.pendingDeletion:hover {
background-color: rgba(248, 113, 113, 0.5);
}
.pendingDeletion:hover td {
background-color: rgba(248, 113, 113, 0.5);
}
.Table thead tr {

View File

@@ -1,4 +1,5 @@
import clsx from 'clsx';
import { SignatureGroup } from '@/hooks/Mapper/types';
import { ExtendedSystemSignature } from './contentHelpers';
import { getRowBackgroundColor } from './getRowBackgroundColor';
import classes from './rowStyles.module.scss';
@@ -6,15 +7,67 @@ import classes from './rowStyles.module.scss';
export function getSignatureRowClass(
row: ExtendedSystemSignature,
selectedSignatures: ExtendedSystemSignature[],
colorByType?: boolean,
): string {
const isSelected = selectedSignatures.some(s => s.eve_id === row.eve_id);
if (isSelected) {
return clsx(
classes.TableRowCompact,
'p-selectable-row',
'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200 text-xs',
);
}
if (row.pendingDeletion) {
return clsx(classes.TableRowCompact, 'p-selectable-row', classes.pendingDeletion);
}
// Apply color by type styling if enabled
if (colorByType) {
if (row.group === SignatureGroup.Wormhole) {
return clsx(
classes.TableRowCompact,
'p-selectable-row',
'bg-blue-400/20 hover:bg-blue-400/20 transition duration-200 text-xs',
);
}
if (row.group === SignatureGroup.CosmicSignature) {
return clsx(
classes.TableRowCompact,
'p-selectable-row',
'bg-red-400/20 hover:bg-red-400/20 transition duration-200 text-xs',
);
}
if (
row.group === SignatureGroup.RelicSite ||
row.group === SignatureGroup.DataSite ||
row.group === SignatureGroup.GasSite ||
row.group === SignatureGroup.OreSite ||
row.group === SignatureGroup.CombatSite
) {
return clsx(
classes.TableRowCompact,
'p-selectable-row',
'bg-green-400/20 hover:bg-green-400/20 transition duration-200 text-xs',
);
}
// Default for color by type - apply same color as CosmicSignature (red) and small text size
return clsx(
classes.TableRowCompact,
'p-selectable-row',
'bg-red-400/20 hover:bg-red-400/20 transition duration-200 text-xs',
);
}
// Original styling when color by type is disabled
return clsx(
classes.TableRowCompact,
'p-selectable-row',
isSelected && 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200',
!isSelected && row.pendingDeletion && classes.pendingDeletion,
!isSelected && getRowBackgroundColor(row.inserted_at ? new Date(row.inserted_at) : undefined),
'hover:bg-purple-400/20 transition duration-200',
!row.pendingDeletion && getRowBackgroundColor(row.inserted_at ? new Date(row.inserted_at) : undefined),
!row.pendingDeletion && 'hover:bg-purple-400/20 transition duration-200',
);
}

View File

@@ -2,11 +2,12 @@ import { ExtendedSystemSignature } from '../helpers/contentHelpers';
export interface UseSystemSignaturesDataProps {
systemId: string;
settings: { key: string; value: boolean }[];
settings: { key: string; value: boolean | number }[];
hideLinkedSignatures?: boolean;
onCountChange?: (count: number) => void;
onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void;
onLazyDeleteChange?: (value: boolean) => void;
deletionTiming?: number;
}
export interface UseFetchingParams {
@@ -19,8 +20,10 @@ export interface UseFetchingParams {
export interface UsePendingDeletionParams {
systemId: string;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
deletionTiming?: number;
}
export interface UsePendingAdditionParams {
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
deletionTiming?: number;
}

View File

@@ -3,33 +3,49 @@ import { ExtendedSystemSignature, schedulePendingAdditionForSig } from '../helpe
import { UsePendingAdditionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
export function usePendingAdditions({ setSignatures }: UsePendingAdditionParams) {
export function usePendingAdditions({ setSignatures, deletionTiming }: UsePendingAdditionParams) {
const [pendingUndoAdditions, setPendingUndoAdditions] = useState<ExtendedSystemSignature[]>([]);
const pendingAdditionMapRef = useRef<Record<string, { finalUntil: number; finalTimeoutId: number }>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processAddedSignatures = useCallback(
(added: ExtendedSystemSignature[]) => {
if (!added.length) return;
// If duration is 0, don't show pending state
if (finalDuration === 0) {
setSignatures(prev => [
...prev,
...added.map(sig => ({
...sig,
pendingAddition: false,
})),
]);
return;
}
const now = Date.now();
setSignatures(prev => [
...prev,
...added.map(sig => ({
...sig,
pendingAddition: true,
pendingUntil: now + FINAL_DURATION_MS,
pendingUntil: now + finalDuration,
})),
]);
added.forEach(sig => {
schedulePendingAdditionForSig(
sig,
FINAL_DURATION_MS,
finalDuration,
setSignatures,
pendingAdditionMapRef,
setPendingUndoAdditions,
);
});
},
[setSignatures],
[setSignatures, finalDuration],
);
const clearPendingAdditions = useCallback(() => {

View File

@@ -5,13 +5,16 @@ import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export function usePendingDeletions({ systemId, setSignatures }: UsePendingDeletionParams) {
export function usePendingDeletions({ systemId, setSignatures, deletionTiming }: UsePendingDeletionParams) {
const { outCommand } = useMapRootState();
const [localPendingDeletions, setLocalPendingDeletions] = useState<ExtendedSystemSignature[]>([]);
const [pendingDeletionMap, setPendingDeletionMap] = useState<
Record<string, { finalUntil: number; finalTimeoutId: number }>
>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
@@ -19,12 +22,22 @@ export function usePendingDeletions({ systemId, setSignatures }: UsePendingDelet
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
// If deletion timing is 0, immediately delete without pending state
if (finalDuration === 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
return;
}
const now = Date.now();
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingAddition: false,
pendingUntil: now + FINAL_DURATION_MS,
pendingUntil: now + finalDuration,
}));
setLocalPendingDeletions(prev => [...prev, ...processedRemoved]);
@@ -36,7 +49,7 @@ export function usePendingDeletions({ systemId, setSignatures }: UsePendingDelet
setSignatures(prev =>
prev.map(sig => {
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
return { ...sig, pendingDeletion: true, pendingUntil: now + FINAL_DURATION_MS };
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
}
return sig;
}),
@@ -53,10 +66,10 @@ export function usePendingDeletions({ systemId, setSignatures }: UsePendingDelet
setLocalPendingDeletions(prev => prev.filter(x => x.eve_id !== sig.eve_id));
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
},
FINAL_DURATION_MS,
finalDuration,
);
},
[systemId, outCommand, setSignatures],
[systemId, outCommand, setSignatures, finalDuration],
);
const clearPendingDeletions = useCallback(() => {

View File

@@ -21,6 +21,7 @@ export function useSystemSignaturesData({
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
}: UseSystemSignaturesDataProps) {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
@@ -30,10 +31,12 @@ export function useSystemSignaturesData({
usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
});
const { pendingUndoAdditions, setPendingUndoAdditions, processAddedSignatures, clearPendingAdditions } =
usePendingAdditions({
setSignatures,
deletionTiming,
});
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({