Compare commits

...

5 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
7 changed files with 183 additions and 28 deletions
+3 -1
View File
@@ -2,7 +2,9 @@
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 ## Partner code
+154 -24
View File
@@ -7,6 +7,8 @@ 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";
@@ -99,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,
@@ -241,6 +244,69 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
const theme = useTheme(); const theme = useTheme();
const [localIsCollapsed, setLocalIsCollapsed] = useState(false); const [localIsCollapsed, setLocalIsCollapsed] = useState(false);
const { planMode, piPrices, alertMode, balanceThreshold, minExtractionRate } = 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
@@ -420,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>
); );
+5
View File
@@ -17,6 +17,7 @@ import { GitHubButton } from "../Github/GitHubButton";
import { LoginButton } from "../Login/LoginButton"; import { LoginButton } from "../Login/LoginButton";
import { PartnerCodeButton } from "../PartnerCode/PartnerCodeButton"; 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,
@@ -132,6 +133,9 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<PartnerCodeButton /> <PartnerCodeButton />
</MenuItem> </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 }} />
@@ -173,6 +177,7 @@ function ResponsiveAppBar() {
</Button> </Button>
<CCPButton /> <CCPButton />
<PartnerCodeButton /> <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>
);
};
@@ -135,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,7 +159,7 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
Program Cycles: {cycles} Program Cycles: {cycles}
</Typography> </Typography>
<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 <Typography
variant="body2" variant="body2"
@@ -438,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}`}>
+1
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;