Compare commits

...

9 Commits

Author SHA1 Message Date
calli c37578c4e5 add drag for characters inside an account card. also minimize for easier reorder 2026-04-14 22:14:39 +03:00
calli 48da721980 fix launchpad storage calculations 2026-04-14 22:08:26 +03:00
calli bf31a7e2cb add matrix space link 2026-02-24 19:47:18 +02:00
calli 6b47b34ddf add buy me a beer button 2026-01-07 09:02:16 +02:00
calli e8f69b15a4 fix per cycle calculation 2026-01-07 08:59:34 +02:00
calli ebd39243b2 fix hourly averages 2026-01-02 21:10:58 +02:00
calli 0ee129b3ca remove planet cache logging 2026-01-02 20:53:09 +02:00
calli 7a09503ffa add a setting to alert if extraction is under desired level 2025-12-30 17:49:25 +02:00
calli 9de91f9982 Add partner code info 2025-12-27 22:44:59 +02:00
13 changed files with 370 additions and 62 deletions
+11 -1
View File
@@ -2,7 +2,17 @@
Simple tool to track your PI planet extractors. Login with your characters and enjoy the PI! Simple tool to track your PI planet extractors. Login with your characters and enjoy the PI!
Any questions, feedback or suggestions are welcome at [EVE PI Discord](https://discord.gg/bCdXzU8PHK) Any questions, feedback or suggestions are welcome at
[EVE PI Matrix](https://matrix.to/#/#eve-pi:calli.fi)
[EVE PI Discord](https://discord.gg/bCdXzU8PHK)
## Partner code
Consider using EVE partner code to support the project:
```
CALLIEVE
```
## [Hosted PI tool](https://pi.calli.fi) ## [Hosted PI tool](https://pi.calli.fi)
+181 -37
View File
@@ -7,11 +7,14 @@ import { useContext, useState, useEffect } from "react";
import { PlanRow } from "./PlanRow"; import { PlanRow } from "./PlanRow";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { DragDropContext, Droppable, Draggable, DropResult } from "@hello-pangea/dnd";
import { planetCalculations } from "@/planets"; import { planetCalculations } from "@/planets";
import { EvePraisalResult } from "@/eve-praisal"; import { EvePraisalResult } from "@/eve-praisal";
import { STORAGE_IDS, PI_SCHEMATICS, PI_PRODUCT_VOLUMES, STORAGE_CAPACITIES } from "@/const"; import { STORAGE_IDS, PI_SCHEMATICS, PI_PRODUCT_VOLUMES, STORAGE_CAPACITIES } from "@/const";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { PlanetCalculations, AlertState, StorageContent, StorageInfo } from "@/types/planet"; import { PlanetCalculations, AlertState, StorageContent, StorageInfo } from "@/types/planet";
import { getProgramOutputPrediction } from "../PlanetaryInteraction/ExtractionSimulation";
interface AccountTotals { interface AccountTotals {
monthlyEstimate: number; monthlyEstimate: number;
@@ -22,15 +25,17 @@ interface AccountTotals {
totalExtractors: number; totalExtractors: number;
} }
const calculateAlertState = (planetDetails: PlanetCalculations): AlertState => { const calculateAlertState = (planetDetails: PlanetCalculations, minExtractionRate: number): AlertState => {
const hasLowStorage = planetDetails.storageInfo.some(storage => storage.fillRate > 60); const hasLowStorage = planetDetails.storageInfo.some(storage => storage.fillRate > 60);
const hasLowImports = planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24); const hasLowImports = planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24);
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
return { return {
expired: planetDetails.expired, expired: planetDetails.expired,
hasLowStorage, hasLowStorage,
hasLowImports, hasLowImports,
hasLargeExtractorDifference: planetDetails.hasLargeExtractorDifference hasLargeExtractorDifference: planetDetails.hasLargeExtractorDifference,
hasLowExtractionRate
}; };
}; };
@@ -47,18 +52,27 @@ const calculatePlanetDetails = (planet: PlanetWithInfo, piPrices: EvePraisalResu
])); ]));
// Calculate extractor averages and check for large differences // Calculate extractor averages and check for large differences
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
const extractorAverages = extractors const extractorAverages = extractors
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) .filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
.map(e => { .map(e => {
const cycleTime = e.extractor_details?.cycle_time || 3600; const installDate = new Date(e.install_time ?? "");
const expiryDate = new Date(e.expiry_time ?? "");
const programDuration = (expiryDate.getTime() - installDate.getTime()) / 1000;
const cycles = Math.floor(programDuration / CYCLE_TIME);
const qtyPerCycle = e.extractor_details?.qty_per_cycle || 0; const qtyPerCycle = e.extractor_details?.qty_per_cycle || 0;
const prediction = getProgramOutputPrediction(qtyPerCycle, CYCLE_TIME, cycles);
const totalOutput = prediction.reduce((sum, val) => sum + val, 0);
const averagePerHour = totalOutput / cycles * 2;
return { return {
typeId: e.extractor_details!.product_type_id!, typeId: e.extractor_details!.product_type_id!,
averagePerHour: (qtyPerCycle * 3600) / cycleTime averagePerHour
}; };
}); });
const hasLargeExtractorDifference = extractorAverages.length === 2 && const hasLargeExtractorDifference = extractorAverages.length === 2 &&
Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold; Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold;
// Calculate storage info // Calculate storage info
@@ -87,6 +101,7 @@ const calculatePlanetDetails = (planet: PlanetWithInfo, piPrices: EvePraisalResu
const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0; const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0;
return { return {
pin_id: storage.pin_id,
type: storageType, type: storageType,
type_id: storage.type_id, type_id: storage.type_id,
capacity: storageCapacity, capacity: storageCapacity,
@@ -228,7 +243,70 @@ const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalR
export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { characters: AccessToken[], isCollapsed?: boolean }) => { export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { characters: AccessToken[], isCollapsed?: boolean }) => {
const theme = useTheme(); const theme = useTheme();
const [localIsCollapsed, setLocalIsCollapsed] = useState(false); const [localIsCollapsed, setLocalIsCollapsed] = useState(false);
const { planMode, piPrices, alertMode, balanceThreshold } = useContext(SessionContext); const { planMode, piPrices, alertMode, balanceThreshold, minExtractionRate } = useContext(SessionContext);
const accountName = characters.length > 0 ? (characters[0].account ?? "-") : "-";
const [characterOrder, setCharacterOrder] = useState<number[]>(() => {
try {
const saved = localStorage.getItem(`characterOrder-${accountName}`);
if (saved) {
const parsed: number[] = JSON.parse(saved);
const ids = characters.map(c => c.character.characterId);
const valid = parsed.filter(id => ids.includes(id));
const newIds = ids.filter(id => !valid.includes(id));
return [...valid, ...newIds];
}
} catch {}
return characters.map(c => c.character.characterId);
});
useEffect(() => {
const ids = characters.map(c => c.character.characterId);
setCharacterOrder(prev => {
const valid = prev.filter(id => ids.includes(id));
const newIds = ids.filter(id => !valid.includes(id));
return [...valid, ...newIds];
});
}, [characters]);
useEffect(() => {
if (characterOrder.length > 0) {
localStorage.setItem(`characterOrder-${accountName}`, JSON.stringify(characterOrder));
}
}, [characterOrder, accountName]);
const [collapsedCharacters, setCollapsedCharacters] = useState<Set<number>>(() => {
try {
const saved = localStorage.getItem(`collapsedCharacters-${accountName}`);
if (saved) return new Set<number>(JSON.parse(saved));
} catch {}
return new Set<number>();
});
const toggleCharacterCollapsed = (characterId: number) => {
setCollapsedCharacters(prev => {
const next = new Set(prev);
if (next.has(characterId)) next.delete(characterId);
else next.add(characterId);
localStorage.setItem(`collapsedCharacters-${accountName}`, JSON.stringify(Array.from(next)));
return next;
});
};
const orderedCharacters = characterOrder
.map(id => characters.find(c => c.character.characterId === id))
.filter((c): c is AccessToken => c !== undefined);
const handleCharacterDragEnd = (result: DropResult) => {
if (!result.destination) return;
const items = Array.from(characterOrder);
const [moved] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, moved);
setCharacterOrder(items);
};
const DragDropContextComponent = DragDropContext as any;
const DroppableComponent = Droppable as any;
const DraggableComponent = Draggable as any;
const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices); const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices);
// Calculate planet details and alert states for each planet // Calculate planet details and alert states for each planet
@@ -237,7 +315,7 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
const details = calculatePlanetDetails(planet, piPrices, balanceThreshold); const details = calculatePlanetDetails(planet, piPrices, balanceThreshold);
acc[`${character.character.characterId}-${planet.planet_id}`] = { acc[`${character.character.characterId}-${planet.planet_id}`] = {
...details, ...details,
alertState: calculateAlertState(details) alertState: calculateAlertState(details, minExtractionRate)
}; };
}); });
return acc; return acc;
@@ -254,16 +332,18 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
if (alertState.hasLowStorage) return 'visible'; if (alertState.hasLowStorage) return 'visible';
if (alertState.hasLowImports) return 'visible'; if (alertState.hasLowImports) return 'visible';
if (alertState.hasLargeExtractorDifference) return 'visible'; if (alertState.hasLargeExtractorDifference) return 'visible';
if (alertState.hasLowExtractionRate) return 'visible';
return 'hidden'; return 'hidden';
}; };
// Check if any planet in the account has alerts // Check if any planet in the account has alerts
const hasAnyAlerts = Object.values(planetDetails).some(details => { const hasAnyAlerts = Object.values(planetDetails).some(details => {
const alertState = calculateAlertState(details); const alertState = calculateAlertState(details, minExtractionRate);
return alertState.expired || return alertState.expired ||
alertState.hasLowStorage || alertState.hasLowStorage ||
alertState.hasLowImports || alertState.hasLowImports ||
alertState.hasLargeExtractorDifference; alertState.hasLargeExtractorDifference ||
alertState.hasLowExtractionRate;
}); });
// If in alert mode and no alerts, hide the entire card // If in alert mode and no alerts, hide the entire card
@@ -406,30 +486,94 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
{localIsCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} {localIsCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton> </IconButton>
</Box> </Box>
{!localIsCollapsed && characters.map((c) => ( {!localIsCollapsed && (
<Stack <DragDropContextComponent onDragEnd={handleCharacterDragEnd}>
key={c.character.characterId} <DroppableComponent droppableId={`characters-${accountName}`} direction="vertical">
direction="row" {(provided: any) => (
alignItems="flex-start" <Box ref={provided.innerRef} {...provided.droppableProps}>
> {orderedCharacters.map((c, index) => (
<CharacterRow character={c} /> <DraggableComponent
{planMode ? ( key={c.character.characterId}
<PlanRow character={c} /> draggableId={`char-${c.character.characterId}`}
) : ( index={index}
<PlanetaryInteractionRow >
character={c} {(provided: any, snapshot: any) => (
planetDetails={c.planets.reduce((acc, planet) => { <Stack
const details = planetDetails[`${c.character.characterId}-${planet.planet_id}`]; ref={provided.innerRef}
acc[planet.planet_id] = { {...provided.draggableProps}
...details, direction="row"
visibility: getAlertVisibility(details.alertState) alignItems="flex-start"
}; sx={{
return acc; opacity: snapshot.isDragging ? 0.8 : 1,
}, {} as Record<number, PlanetCalculations & { visibility: string }>)} backgroundColor: snapshot.isDragging ? theme.palette.action.hover : 'transparent',
/> borderRadius: 1,
)} }}
</Stack> >
))} <Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
pt: 1,
gap: 0.5,
}}
>
<Box
{...provided.dragHandleProps}
sx={{
display: 'flex',
alignItems: 'center',
cursor: 'grab',
px: 0.5,
color: theme.palette.text.disabled,
'&:hover': { color: theme.palette.text.secondary },
'&:active': { cursor: 'grabbing' },
}}
>
</Box>
<IconButton
size="small"
onClick={() => toggleCharacterCollapsed(c.character.characterId)}
sx={{
p: 0.25,
color: theme.palette.text.disabled,
'&:hover': { color: theme.palette.text.secondary },
transform: collapsedCharacters.has(c.character.characterId) ? 'rotate(0deg)' : 'rotate(90deg)',
transition: 'transform 0.2s ease-in-out',
}}
>
<ChevronRightIcon fontSize="small" />
</IconButton>
</Box>
<CharacterRow character={c} />
{!collapsedCharacters.has(c.character.characterId) && (
planMode ? (
<PlanRow character={c} />
) : (
<PlanetaryInteractionRow
character={c}
planetDetails={c.planets.reduce((acc, planet) => {
const details = planetDetails[`${c.character.characterId}-${planet.planet_id}`];
acc[planet.planet_id] = {
...details,
visibility: getAlertVisibility(details.alertState)
};
return acc;
}, {} as Record<number, PlanetCalculations & { visibility: string }>)}
/>
)
)}
</Stack>
)}
</DraggableComponent>
))}
{provided.placeholder}
</Box>
)}
</DroppableComponent>
</DragDropContextComponent>
)}
</Box> </Box>
</Paper> </Paper>
); );
+11 -1
View File
@@ -15,7 +15,9 @@ import { CCPButton } from "../CCP/CCPButton";
import { DiscordButton } from "../Discord/DiscordButton"; import { DiscordButton } from "../Discord/DiscordButton";
import { GitHubButton } from "../Github/GitHubButton"; import { GitHubButton } from "../Github/GitHubButton";
import { LoginButton } from "../Login/LoginButton"; import { LoginButton } from "../Login/LoginButton";
import { PartnerCodeButton } from "../PartnerCode/PartnerCodeButton";
import { SettingsButton } from "../Settings/SettingsButtons"; import { SettingsButton } from "../Settings/SettingsButtons";
import { BuyMeCoffeeButton } from "../BuyMeCoffee/BuyMeCoffeeButton";
import { import {
Button, Button,
Dialog, Dialog,
@@ -128,6 +130,12 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<CCPButton /> <CCPButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<PartnerCodeButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<BuyMeCoffeeButton />
</MenuItem>
</Menu> </Menu>
</Box> </Box>
<PublicIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} /> <PublicIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} />
@@ -162,12 +170,14 @@ function ResponsiveAppBar() {
<UploadButton /> <UploadButton />
<DiscordButton /> <DiscordButton />
<GitHubButton /> <GitHubButton />
<SettingsButton /> <SettingsButton />
<Button onClick={() => setFaqOpen(true)} color="inherit"> <Button onClick={() => setFaqOpen(true)} color="inherit">
FAQ FAQ
</Button> </Button>
<CCPButton /> <CCPButton />
<PartnerCodeButton />
<BuyMeCoffeeButton />
</Box> </Box>
</Toolbar> </Toolbar>
</Container> </Container>
@@ -0,0 +1,17 @@
import { Box, Button, Tooltip } from "@mui/material";
export const BuyMeCoffeeButton = () => {
return (
<Box>
<Tooltip title="Support the development of this tool">
<Button
href="https://buymeacoffee.com/evepi"
target="_blank"
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
>
By me a beer
</Button>
</Tooltip>
</Box>
);
};
@@ -0,0 +1,32 @@
import { Box, Button, Tooltip } from "@mui/material";
import { useState } from "react";
export const PartnerCodeButton = () => {
const [copied, setCopied] = useState(false);
const handleClick = () => {
navigator.clipboard.writeText("CALLIEVE");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Box>
<Tooltip
title={
copied
? "Copied to clipboard!"
: "Click to copy partner code - Use for CCP purchases to support this project"
}
>
<Button
onClick={handleClick}
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
>
Partner Code: CALLIEVE
</Button>
</Tooltip>
</Box>
);
};
@@ -1,8 +1,9 @@
import React from 'react'; import React, { useContext } from 'react';
import { Box, Paper, Typography, Stack } from '@mui/material'; import { Box, Paper, Typography, Stack } from '@mui/material';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { getProgramOutputPrediction } from './ExtractionSimulation'; import { getProgramOutputPrediction } from './ExtractionSimulation';
import { PI_TYPES_MAP } from '@/const'; import { PI_TYPES_MAP } from '@/const';
import { SessionContext } from '@/app/context/Context';
import { import {
Chart as ChartJS, Chart as ChartJS,
CategoryScale, CategoryScale,
@@ -41,6 +42,7 @@ interface ExtractionSimulationTooltipProps {
export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipProps> = ({ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipProps> = ({
extractors extractors
}) => { }) => {
const { minExtractionRate } = useContext(SessionContext);
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
// Calculate program duration and cycles for each extractor // Calculate program duration and cycles for each extractor
@@ -133,7 +135,7 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
</Box> </Box>
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Stack spacing={1}> <Stack spacing={1}>
{extractorPrograms.map(({ typeId, cycleTime, cycles, installTime, expiryTime }, idx) => { {extractorPrograms.map(({ typeId, cycleTime, cycles, expiryTime }, idx) => {
const prediction = getProgramOutputPrediction( const prediction = getProgramOutputPrediction(
extractors[idx].baseValue, extractors[idx].baseValue,
CYCLE_TIME, CYCLE_TIME,
@@ -159,8 +161,15 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
<Typography variant="body2"> <Typography variant="body2">
Average per Cycle: {(totalOutput / cycles).toFixed(1)} units Average per Cycle: {(totalOutput / cycles).toFixed(1)} units
</Typography> </Typography>
<Typography variant="body2"> <Typography
Average per hour: {(totalOutput / cycles * 2).toFixed(1)} units variant="body2"
color={
minExtractionRate > 0 && (extractors[idx].baseValue * 3600) / extractors[idx].cycleTime < minExtractionRate
? 'error'
: 'inherit'
}
>
Average per hour: {(totalOutput / cycles * 2).toFixed(1)} units
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
Expires in: <Countdown overtime={true} date={DateTime.fromISO(expiryTime).toMillis()} /> Expires in: <Countdown overtime={true} date={DateTime.fromISO(expiryTime).toMillis()} />
@@ -176,17 +185,24 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
</Typography> </Typography>
<Stack spacing={0.5}> <Stack spacing={0.5}>
{extractors.map((extractor, index) => { {extractors.map((extractor, index) => {
const averagePerHour = (extractor.baseValue * 3600) / extractor.cycleTime; const prediction = getProgramOutputPrediction(
extractor.baseValue,
CYCLE_TIME,
extractorPrograms[index].cycles
);
const totalOutput = prediction.reduce((sum, val) => sum + val, 0);
const cycles = extractorPrograms[index].cycles;
const averagePerHour = totalOutput / cycles * 2;
return ( return (
<Typography key={index} variant="body2"> <Typography key={index} variant="body2">
{PI_TYPES_MAP[extractor.typeId]?.name}: {averagePerHour.toFixed(1)} u/h {PI_TYPES_MAP[extractor.typeId]?.name}: {averagePerHour.toFixed(1)} u/h
</Typography> </Typography>
); );
})} })}
<Typography <Typography
variant="body2" variant="body2"
color="error" color="error"
sx={{ sx={{
mt: 1, mt: 1,
fontWeight: 'bold', fontWeight: 'bold',
borderTop: '1px solid', borderTop: '1px solid',
@@ -194,10 +210,27 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
pt: 1 pt: 1
}} }}
> >
Difference: {Math.abs( Difference: {(() => {
(extractors[0].baseValue * 3600 / extractors[0].cycleTime) - const prediction0 = getProgramOutputPrediction(
(extractors[1].baseValue * 3600 / extractors[1].cycleTime) extractors[0].baseValue,
).toFixed(1)} u/h CYCLE_TIME,
extractorPrograms[0].cycles
);
const totalOutput0 = prediction0.reduce((sum, val) => sum + val, 0);
const cycles0 = extractorPrograms[0].cycles;
const avg0 = totalOutput0 / cycles0 * 2;
const prediction1 = getProgramOutputPrediction(
extractors[1].baseValue,
CYCLE_TIME,
extractorPrograms[1].cycles
);
const totalOutput1 = prediction1.reduce((sum, val) => sum + val, 0);
const cycles1 = extractorPrograms[1].cycles;
const avg1 = totalOutput1 / cycles1 * 2;
return Math.abs(avg0 - avg1).toFixed(1);
})()} u/h
</Typography> </Typography>
</Stack> </Stack>
</Paper> </Paper>
@@ -8,7 +8,7 @@ import { PlanetCalculations } from "@/types/planet";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import Countdown from "react-countdown"; import Countdown from "react-countdown";
import { ColorContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip"; import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip";
import { timeColor } from "./alerts"; import { timeColor } from "./alerts";
@@ -40,6 +40,7 @@ export const PlanetCard = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { colors } = useContext(ColorContext); const { colors } = useContext(ColorContext);
const { minExtractionRate } = useContext(SessionContext);
const extractorConfigs: ExtractorConfig[] = planetDetails.extractors const extractorConfigs: ExtractorConfig[] = planetDetails.extractors
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) .filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
@@ -51,6 +52,8 @@ export const PlanetCard = ({
expiryTime: e.expiry_time ?? "" expiryTime: e.expiry_time ?? ""
})); }));
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
return ( return (
<Tooltip <Tooltip
title={ title={
@@ -114,9 +117,30 @@ export const PlanetCard = ({
/> />
)} )}
<div style={{ position: "absolute", top: 5, left: 10 }}> <div style={{ position: "absolute", top: 5, left: 10 }}>
<Typography fontSize={theme.custom.smallText}> <Typography
fontSize={theme.custom.smallText}
color={(planetDetails.hasLargeExtractorDifference || hasLowExtractionRate) ? 'error' : 'inherit'}
>
{planet.infoUniverse?.name} {planet.infoUniverse?.name}
</Typography> </Typography>
{planetDetails.hasLargeExtractorDifference && (
<Typography
fontSize={theme.custom.smallText}
color="error"
sx={{ opacity: 0.7 }}
>
off-balance
</Typography>
)}
{hasLowExtractionRate && (
<Typography
fontSize={theme.custom.smallText}
color="error"
sx={{ opacity: 0.7 }}
>
low-extraction
</Typography>
)}
{planetDetails.extractors.map((e, idx) => { {planetDetails.extractors.map((e, idx) => {
const average = planetDetails.extractorAverages[idx]; const average = planetDetails.extractorAverages[idx];
return ( return (
@@ -55,7 +55,7 @@ export const PlanetTableRow = ({
planetDetails: PlanetCalculations; planetDetails: PlanetCalculations;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { showProductIcons, extractionTimeMode, alertMode } = useContext(SessionContext); const { showProductIcons, extractionTimeMode, alertMode, minExtractionRate } = useContext(SessionContext);
const { colors } = useContext(ColorContext); const { colors } = useContext(ColorContext);
const [planetRenderOpen, setPlanetRenderOpen] = useState(false); const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
@@ -103,11 +103,13 @@ export const PlanetTableRow = ({
}; };
// Check if there are any alerts // Check if there are any alerts
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
const hasAlerts = alertMode && ( const hasAlerts = alertMode && (
planetDetails.expired || planetDetails.expired ||
planetDetails.storageInfo.some(storage => storage.fillRate > 60) || planetDetails.storageInfo.some(storage => storage.fillRate > 60) ||
planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24) || planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24) ||
planetDetails.hasLargeExtractorDifference planetDetails.hasLargeExtractorDifference ||
hasLowExtractionRate
); );
// If in alert mode and no alerts, hide the row // If in alert mode and no alerts, hide the row
@@ -225,14 +227,14 @@ export const PlanetTableRow = ({
}} }}
> >
<Stack spacing={0}> <Stack spacing={0}>
<Typography <Typography
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
color={planetDetails.hasLargeExtractorDifference ? 'error' : 'inherit'} color={(planetDetails.hasLargeExtractorDifference || hasLowExtractionRate) ? 'error' : 'inherit'}
> >
{planetInfoUniverse?.name} {planetInfoUniverse?.name}
</Typography> </Typography>
{planetDetails.hasLargeExtractorDifference && ( {planetDetails.hasLargeExtractorDifference && (
<Typography <Typography
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
color="error" color="error"
sx={{ opacity: 0.7 }} sx={{ opacity: 0.7 }}
@@ -240,6 +242,15 @@ export const PlanetTableRow = ({
off-balance off-balance
</Typography> </Typography>
)} )}
{hasLowExtractionRate && (
<Typography
fontSize={theme.custom.smallText}
color="error"
sx={{ opacity: 0.7 }}
>
low-extraction
</Typography>
)}
</Stack> </Stack>
</Tooltip> </Tooltip>
</div> </div>
@@ -427,7 +438,7 @@ export const PlanetTableRow = ({
.map((storage, idx) => { .map((storage, idx) => {
const fillRate = storage.fillRate; const fillRate = storage.fillRate;
const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit'; const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit';
const contents = planet.info.pins.find(p => p.type_id === storage.type_id)?.contents || []; const contents = planet.info.pins.find(p => p.pin_id === storage.pin_id)?.contents || [];
return ( return (
<React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}> <React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}>
@@ -21,7 +21,7 @@ import React, { useState, useContext } from "react";
export const SettingsButton = () => { export const SettingsButton = () => {
const { colors, setColors } = useContext(ColorContext); const { colors, setColors } = useContext(ColorContext);
const { balanceThreshold, setBalanceThreshold, showProductIcons, setShowProductIcons } = useContext(SessionContext); const { balanceThreshold, setBalanceThreshold, minExtractionRate, setMinExtractionRate, showProductIcons, setShowProductIcons } = useContext(SessionContext);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@@ -51,6 +51,13 @@ export const SettingsButton = () => {
setShowProductIcons(event.target.checked); setShowProductIcons(event.target.checked);
}; };
const handleMinExtractionRateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value);
if (!isNaN(value) && value >= 0 && value <= 100000) {
setMinExtractionRate(value);
}
};
return ( return (
<Tooltip title="Toggle settings dialog"> <Tooltip title="Toggle settings dialog">
<> <>
@@ -93,6 +100,19 @@ export const SettingsButton = () => {
error={balanceThreshold < 0 || balanceThreshold > 100000} error={balanceThreshold < 0 || balanceThreshold > 100000}
/> />
</Box> </Box>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Minimum Extraction Rate</Typography>
<TextField
type="number"
value={minExtractionRate}
onChange={handleMinExtractionRateChange}
fullWidth
margin="normal"
inputProps={{ min: 0, max: 100000 }}
helperText="Alert if extraction per hour is below this value (0-100,000, 0 = disabled)"
error={minExtractionRate < 0 || minExtractionRate > 100000}
/>
</Box>
{Object.keys(colors).map((key) => { {Object.keys(colors).map((key) => {
return ( return (
<div key={`color-row-${key}`}> <div key={`color-row-${key}`}>
+4
View File
@@ -39,6 +39,8 @@ export const SessionContext = createContext<{
}) => PlanetConfig; }) => PlanetConfig;
balanceThreshold: number; balanceThreshold: number;
setBalanceThreshold: Dispatch<SetStateAction<number>>; setBalanceThreshold: Dispatch<SetStateAction<number>>;
minExtractionRate: number;
setMinExtractionRate: Dispatch<SetStateAction<number>>;
showProductIcons: boolean; showProductIcons: boolean;
setShowProductIcons: (show: boolean) => void; setShowProductIcons: (show: boolean) => void;
}>({ }>({
@@ -68,6 +70,8 @@ export const SessionContext = createContext<{
}, },
balanceThreshold: 1000, balanceThreshold: 1000,
setBalanceThreshold: () => {}, setBalanceThreshold: () => {},
minExtractionRate: 0,
setMinExtractionRate: () => {},
showProductIcons: false, showProductIcons: false,
setShowProductIcons: () => {}, setShowProductIcons: () => {},
}); });
+3
View File
@@ -46,6 +46,7 @@ const Home = () => {
undefined, undefined,
); );
const [balanceThreshold, setBalanceThreshold] = useState(1000); const [balanceThreshold, setBalanceThreshold] = useState(1000);
const [minExtractionRate, setMinExtractionRate] = useState(0);
const [showProductIcons, setShowProductIcons] = useState(false); const [showProductIcons, setShowProductIcons] = useState(false);
const [extractionTimeMode, setExtractionTimeMode] = useState(false); const [extractionTimeMode, setExtractionTimeMode] = useState(false);
@@ -305,6 +306,8 @@ const Home = () => {
readPlanetConfig, readPlanetConfig,
balanceThreshold, balanceThreshold,
setBalanceThreshold, setBalanceThreshold,
minExtractionRate,
setMinExtractionRate,
showProductIcons, showProductIcons,
setShowProductIcons, setShowProductIcons,
}} }}
-2
View File
@@ -62,11 +62,9 @@ export const getPlanet = async (
const cached = planetCache.get(cacheKey); const cached = planetCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION_MS) { if (cached && Date.now() - cached.timestamp < CACHE_DURATION_MS) {
console.log(`[Cache HIT] Planet ${planet.planet_id} for character ${character.character.characterId}`);
return cached.data; return cached.data;
} }
console.log(`[Cache MISS] Fetching planet ${planet.planet_id} for character ${character.character.characterId}`);
const api = new Api(); const api = new Api();
const planetInfo = ( const planetInfo = (
await api.v3.getCharactersCharacterIdPlanetsPlanetId( await api.v3.getCharactersCharacterIdPlanetsPlanetId(
+2
View File
@@ -6,6 +6,7 @@ export interface StorageContent {
} }
export interface StorageInfo { export interface StorageInfo {
pin_id: number;
type: string; type: string;
type_id: number; type_id: number;
capacity: number; capacity: number;
@@ -32,6 +33,7 @@ export interface AlertState {
hasLowStorage: boolean; hasLowStorage: boolean;
hasLowImports: boolean; hasLowImports: boolean;
hasLargeExtractorDifference: boolean; hasLargeExtractorDifference: boolean;
hasLowExtractionRate: boolean;
} }
export interface ExtractorAverage { export interface ExtractorAverage {