9 Commits

7 changed files with 346 additions and 14 deletions

1
.gitignore vendored
View File

@@ -127,3 +127,4 @@ gitversion
# vscode settings # vscode settings
.vscode .vscode
eve.db

28
build.sh Normal file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
set -e
echo "Building pyfa binary..."
# Ensure we're using the local venv
if [ ! -d ".venv" ]; then
echo "Creating virtual environment..."
uv venv
fi
# Install dependencies
echo "Installing dependencies..."
uv pip install -r requirements.txt
uv pip install pyinstaller
# Clean previous builds
echo "Cleaning previous builds..."
rm -rf build dist
# Build the binary
echo "Building binary with PyInstaller..."
uv run pyinstaller pyfa.spec
echo ""
echo "Build complete! Binary is located at: dist/pyfa/pyfa.exe"
echo "You can run it with: dist/pyfa/pyfa.exe"

View File

@@ -5,6 +5,7 @@ import gui.builtinMarketBrowser.pfSearchBox as SBox
import gui.globalEvents as GE import gui.globalEvents as GE
from config import slotColourMap, slotColourMapDark from config import slotColourMap, slotColourMapDark
from eos.saveddata.module import Module from eos.saveddata.module import Module
from eos.const import FittingSlot
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES, CHARGES_FOR_FIT
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
from gui.display import Display from gui.display import Display
@@ -153,19 +154,22 @@ class ItemView(Display):
# skip the event so the other handlers also get called # skip the event so the other handlers also get called
event.Skip() event.Skip()
if self.marketBrowser.mode != 'charges':
return
activeFitID = self.mainFrame.getActiveFit() activeFitID = self.mainFrame.getActiveFit()
# if it was not the active fitting that was changed, do not do anything # if it was not the active fitting that was changed, do not do anything
if activeFitID is not None and activeFitID not in event.fitIDs: if activeFitID is not None and activeFitID not in event.fitIDs:
return return
items = self.getChargesForActiveFit() # Handle charges mode
if self.marketBrowser.mode == 'charges':
items = self.getChargesForActiveFit()
# update the UI
self.updateItemStore(items)
self.filterItemStore()
return
# update the UI # If "Fits" filter is active, re-filter the current view
self.updateItemStore(items) if self.marketBrowser.getFitsFilter():
self.filterItemStore() self.filterItemStore()
def updateItemStore(self, items): def updateItemStore(self, items):
self.unfilteredStore = items self.unfilteredStore = items
@@ -197,13 +201,115 @@ class ItemView(Display):
if btn.userSelected: if btn.userSelected:
selectedMetas.update(sMkt.META_MAP[btn.metaName]) selectedMetas.update(sMkt.META_MAP[btn.metaName])
filteredItems = sMkt.filterItemsByMeta(self.unfilteredStore, selectedMetas) filteredItems = sMkt.filterItemsByMeta(self.unfilteredStore, selectedMetas)
# Apply slot/fits filters - works IDENTICALLY to meta buttons (filters CURRENT VIEW only)
activeSlotFilters = []
fitsFilterActive = False
for btn in self.marketBrowser.slotButtons:
if btn.userSelected:
if btn.filterType == "fits":
fitsFilterActive = True
elif btn.filterType == "slot":
activeSlotFilters.append(btn.slotType)
# Apply fits filter
if fitsFilterActive:
filteredItems = self._filterByFits(filteredItems)
# Apply slot filters
if activeSlotFilters:
filteredItems = [item for item in filteredItems if Module.calculateSlot(item) in activeSlotFilters]
return filteredItems return filteredItems
def _filterByFits(self, items):
"""Filter items by remaining CPU/PG - filters CURRENT VIEW only"""
fitId = self.mainFrame.getActiveFit()
if fitId is None:
return []
fit = self.sFit.getFit(fitId)
# Get remaining CPU and power grid
cpuOutput = fit.ship.getModifiedItemAttr("cpuOutput")
powerOutput = fit.ship.getModifiedItemAttr("powerOutput")
cpuUsed = fit.cpuUsed
pgUsed = fit.pgUsed
cpuRemaining = cpuOutput - cpuUsed
pgRemaining = powerOutput - pgUsed
# Get remaining calibration (for rigs)
calibrationCapacity = fit.ship.getModifiedItemAttr("upgradeCapacity")
calibrationUsed = fit.calibrationUsed
calibrationRemaining = None
if calibrationCapacity is not None and calibrationCapacity > 0:
calibrationRemaining = calibrationCapacity - calibrationUsed
fittingItems = []
for item in items:
# Check if item is a module (has a slot)
slot = Module.calculateSlot(item)
if slot is None:
continue
# Rigs don't use CPU/power, they use calibration - check rig size and calibration
if slot == FittingSlot.RIG:
# Check if item can fit on the ship
if not fit.canFit(item):
continue
# Check rig size compatibility with ship
shipRigSize = fit.ship.getModifiedItemAttr("rigSize")
itemRigSize = item.attributes.get("rigSize")
if shipRigSize is not None and itemRigSize is not None:
if shipRigSize != itemRigSize.value:
continue
# Check calibration requirement
if calibrationRemaining is not None and calibrationRemaining > 0:
itemCalibration = item.attributes.get("upgradeCost")
if itemCalibration is not None:
itemCalibrationValue = itemCalibration.value
if itemCalibrationValue > calibrationRemaining:
continue
fittingItems.append(item)
continue
# For non-rigs, check CPU and power requirements
itemCpu = item.attributes.get("cpu")
itemPower = item.attributes.get("power")
# Skip items without CPU or power (not modules)
if itemCpu is None and itemPower is None:
continue
# Check CPU requirement
if itemCpu is not None:
itemCpuValue = itemCpu.value
if itemCpuValue > cpuRemaining:
continue
# Check power requirement
if itemPower is not None:
itemPowerValue = itemPower.value
if itemPowerValue > pgRemaining:
continue
# Check if item can fit on the ship (most expensive check, do last)
if not fit.canFit(item):
continue
fittingItems.append(item)
return fittingItems
def setToggles(self): def setToggles(self):
metaIDs = set() metaIDs = set()
slotIDs = set()
sMkt = self.sMkt sMkt = self.sMkt
for item in self.unfilteredStore: for item in self.unfilteredStore:
metaIDs.add(sMkt.getMetaGroupIdByItem(item)) metaIDs.add(sMkt.getMetaGroupIdByItem(item))
slot = Module.calculateSlot(item)
if slot is not None:
slotIDs.add(slot)
for btn in self.marketBrowser.metaButtons: for btn in self.marketBrowser.metaButtons:
btn.reset() btn.reset()
@@ -213,6 +319,20 @@ class ItemView(Display):
else: else:
btn.setMetaAvailable(False) btn.setMetaAvailable(False)
# Set toggles for slot/fits buttons
for btn in self.marketBrowser.slotButtons:
btn.reset()
if btn.filterType == "fits":
# Fits button is available if there's an active fit
fitId = self.mainFrame.getActiveFit()
btn.setMetaAvailable(fitId is not None)
elif btn.filterType == "slot":
# Slot button is available if items with that slot exist in current view
if btn.slotType in slotIDs:
btn.setMetaAvailable(True)
else:
btn.setMetaAvailable(False)
def scheduleSearch(self, event=None): def scheduleSearch(self, event=None):
self.searchTimer.Stop() # Cancel any pending timers self.searchTimer.Stop() # Cancel any pending timers
search = self.marketBrowser.search.GetLineText(0) search = self.marketBrowser.search.GetLineText(0)

View File

@@ -64,22 +64,61 @@ class MarketBrowser(wx.Panel):
# Same fix as for search box on macs, # Same fix as for search box on macs,
# need some pixels of extra space or everything clips and is ugly # need some pixels of extra space or everything clips and is ugly
p = wx.Panel(self) p = wx.Panel(self)
box = wx.BoxSizer(wx.HORIZONTAL) vbox_panel = wx.BoxSizer(wx.VERTICAL)
p.SetSizer(box) p.SetSizer(vbox_panel)
vbox.Add(p, 0, wx.EXPAND) vbox.Add(p, 0, wx.EXPAND)
# First row: meta buttons
metaBox = wx.BoxSizer(wx.HORIZONTAL)
vbox_panel.Add(metaBox, 0, wx.EXPAND)
self.metaButtons = [] self.metaButtons = []
btn = None btn = None
for name in list(self.sMkt.META_MAP.keys()): for name in list(self.sMkt.META_MAP.keys()):
btn = MetaButton(p, wx.ID_ANY, name.capitalize(), style=wx.BU_EXACTFIT) btn = MetaButton(p, wx.ID_ANY, name.capitalize(), style=wx.BU_EXACTFIT)
setattr(self, name, btn) setattr(self, name, btn)
box.Add(btn, 1, wx.ALIGN_CENTER) metaBox.Add(btn, 1, wx.ALIGN_CENTER)
btn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleMetaButton) btn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleMetaButton)
btn.metaName = name btn.metaName = name
self.metaButtons.append(btn) self.metaButtons.append(btn)
# Second row: slot/fits filter buttons (BELOW meta buttons)
slotBox = wx.BoxSizer(wx.HORIZONTAL)
vbox_panel.Add(slotBox, 0, wx.EXPAND)
self.slotButtons = []
from eos.const import FittingSlot
# Fits button
fitsBtn = MetaButton(p, wx.ID_ANY, "Fits", style=wx.BU_EXACTFIT)
setattr(self, "fits", fitsBtn)
slotBox.Add(fitsBtn, 1, wx.ALIGN_CENTER)
fitsBtn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleSlotButton)
fitsBtn.filterType = "fits"
# Fits button starts deselected (checkbox, off by default)
fitsBtn.setUserSelection(False)
self.slotButtons.append(fitsBtn)
# High, Med, Low, Rig buttons
slotMap = {
FittingSlot.HIGH: "High",
FittingSlot.MED: "Med",
FittingSlot.LOW: "Low",
FittingSlot.RIG: "Rig"
}
for slot, label in slotMap.items():
slotBtn = MetaButton(p, wx.ID_ANY, label, style=wx.BU_EXACTFIT)
setattr(self, "slot_%s" % label.lower(), slotBtn)
slotBox.Add(slotBtn, 1, wx.ALIGN_CENTER)
slotBtn.Bind(wx.EVT_TOGGLEBUTTON, self.toggleSlotButton)
slotBtn.filterType = "slot"
slotBtn.slotType = slot
# Slot buttons start deselected (unlike meta buttons which start selected)
slotBtn.setUserSelection(False)
self.slotButtons.append(slotBtn)
# Make itemview to set toggles according to list contents # Make itemview to set toggles according to list contents
self.itemView.setToggles() self.itemView.setToggles()
p.SetMinSize((wx.SIZE_AUTO_WIDTH, btn.GetSize()[1] + 5)) p.SetMinSize((wx.SIZE_AUTO_WIDTH, btn.GetSize()[1] * 2 + 10))
def toggleMetaButton(self, event): def toggleMetaButton(self, event):
"""Process clicks on toggle buttons""" """Process clicks on toggle buttons"""
@@ -100,6 +139,42 @@ class MarketBrowser(wx.Panel):
self.itemView.filterItemStore() self.itemView.filterItemStore()
def toggleSlotButton(self, event):
"""Process clicks on slot/fits filter buttons"""
clickedBtn = event.EventObject
# Fits button works as a checkbox (independent toggle)
if clickedBtn.filterType == "fits":
clickedBtn.setUserSelection(clickedBtn.GetValue())
self.itemView.filterItemStore()
return
# Slot buttons (High/Med/Low) work as radio buttons (mutually exclusive)
if wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL:
# Get only slot buttons (not fits)
activeSlotBtns = [btn for btn in self.slotButtons if btn.GetValue() and btn.filterType == "slot"]
if activeSlotBtns:
clickedBtn.setUserSelection(clickedBtn.GetValue())
self.itemView.filterItemStore()
else:
# Do 'nothing' if we're trying to turn last active button off
# Keep button in the same state
clickedBtn.setUserSelection(True)
else:
# Deselect all slot buttons, then select clicked one
for btn in self.slotButtons:
if btn.filterType == "slot":
btn.setUserSelection(btn == clickedBtn)
self.itemView.filterItemStore()
def getFitsFilter(self):
"""Check if Fits button is active"""
for btn in self.slotButtons:
if btn.filterType == "fits" and btn.userSelected:
return True
return False
def jump(self, item): def jump(self, item):
self.mode = 'normal' self.mode = 'normal'
self.marketView.jump(item) self.marketView.jump(item)
@@ -157,4 +232,3 @@ class MarketBrowser(wx.Panel):
for btn in self.metaButtons: for btn in self.metaButtons:
btn.setUserSelection(True) btn.setUserSelection(True)
self.__mode = newMode self.__mode = newMode

9
pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[project]
name = "pyfa"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"ruff>=0.14.8",
]

60
release.sh Normal file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
set -e
echo "Figuring out the tag..."
TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
# Get the latest tag
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null || echo "v0.0.0")
# Remove 'v' prefix if present
LATEST_TAG=${LATEST_TAG#v}
# Increment the patch version
IFS='.' read -r -a VERSION_PARTS <<< "$LATEST_TAG"
VERSION_PARTS[2]=$((VERSION_PARTS[2]+1))
TAG="v${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}"
# Create a new tag
git tag $TAG
git push origin $TAG
fi
echo "Tag: $TAG"
echo "Building the binary..."
./build.sh
echo "Creating release zip..."
ZIP="pyfa-${TAG}-win.zip"
cd dist/pyfa
zip -r "../../${ZIP}" .
cd ../..
echo "Creating a release..."
TOKEN="$GITEA_API_KEY"
GITEA="https://git.site.quack-lab.dev"
REPO="dave/pyfa"
# Create a release
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "'"$TAG"'",
"name": "'"$TAG"'",
"draft": false,
"prerelease": false
}' \
$GITEA/api/v1/repos/$REPO/releases)
# Extract the release ID
echo $RELEASE_RESPONSE
RELEASE_ID=$(echo $RELEASE_RESPONSE | awk -F'"id":' '{print $2+0; exit}')
echo "Release ID: $RELEASE_ID"
echo "Uploading the zip..."
curl -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@${ZIP}" \
"$GITEA/api/v1/repos/$REPO/releases/${RELEASE_ID}/assets?name=${ZIP}"
echo "Release complete! ${ZIP} uploaded to ${TAG}"

40
uv.lock generated Normal file
View File

@@ -0,0 +1,40 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "pyfa"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [{ name = "ruff", specifier = ">=0.14.8" }]
[[package]]
name = "ruff"
version = "0.14.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
]