Compare commits

...

69 Commits

Author SHA1 Message Date
f03ffa85d8 Update pyproject.toml 2026-01-01 21:08:41 +01:00
8d6ae56f33 Implement copy skills to clipboard 2026-01-01 21:08:41 +01:00
64e339fb46 Mirror Pyfa-Mod for darkmode dll 2025-12-30 14:15:37 +01:00
29ee808337 Add oleacc for darkmode 2025-12-30 14:15:25 +01:00
72d65e6118 Prevent the slot filters from filtering items that fit into no slots (like drones) 2025-12-16 00:53:55 +01:00
135fdd8812 Make the slot filters a toggle instead of radio 2025-12-15 23:12:17 +01:00
6bb0938be0 Fix ammo filtering 2025-12-15 22:47:12 +01:00
8a37ee810a Use 7z LIKE GOD INTENDED IT instead of fucking zip 2025-12-15 22:47:12 +01:00
4d1320161a Add a release script 2025-12-15 22:47:12 +01:00
b5d6211ae0 Add a build script 2025-12-15 22:47:12 +01:00
fa05cd625f Add rig filter button 2025-12-15 22:47:12 +01:00
d18ebb6dc0 Filter rigs by calibration fits as well 2025-12-15 22:47:12 +01:00
f3a89157ca Have fits filter rigs by size 2025-12-15 22:47:12 +01:00
766d45dd17 Make fits a check instead of radio button 2025-12-15 22:47:12 +01:00
457bbc0dc3 Fix rigs never passing fitting check because no cpu 2025-12-15 22:47:12 +01:00
4ddf1733e4 Refresh market filter when fit changed 2025-12-15 22:47:12 +01:00
ca2a80cc85 Hallucinte some more buttons below market 2025-12-15 22:47:11 +01:00
DarkPhoenix
c7074f499f Add a guard against situation where fit is empty for some reason 2025-12-15 12:18:52 +01:00
DarkPhoenix
f6f3a69be4 Fail build if image tool couldn't be fetched 2025-12-11 19:21:03 +01:00
DarkPhoenix
23e09729f7 CCP mistype is actually a mistype on my part 2025-12-11 18:50:14 +01:00
DarkPhoenix
0aca05704f Bump version 2025-12-11 18:03:49 +01:00
DarkPhoenix
b08894e984 Anhinga effect changes 2025-12-11 18:03:32 +01:00
DarkPhoenix
a1bc8742c9 Add new renders 2025-12-11 15:58:02 +01:00
DarkPhoenix
6472cabc05 Update static data and make some changes to support new AT ships 2025-12-11 15:54:11 +01:00
DarkPhoenix
56bb8217d3 Do not fail whole app when ESI access object fails instantiation 2025-12-10 19:22:52 +01:00
DarkPhoenix
17f9071317 Bump version 2025-12-09 15:28:26 +01:00
DarkPhoenix
50eda1f4db Add option to copy condensed skill list 2025-12-09 15:26:17 +01:00
DarkPhoenix
84fbc0a46c Fix clipboard on my wayland installation 2025-12-09 15:08:38 +01:00
Anton Vorobyov
9551195078 Merge pull request #2700 from skyride/master
Fixed bug with Clipboard on Wayland
2025-12-09 17:49:24 +04:00
DarkPhoenix
f01949d892 Add expedition hold display 2025-12-09 14:44:43 +01:00
DarkPhoenix
26b4c05b6f Stacking penalize residue probability attribute 2025-12-09 14:41:40 +01:00
Anton Vorobyov
6ecab03fd8 Merge pull request #2683 from cryonox/dev/refresh_tokens
Refresh tokens when pyfa start to avoid expiry
2025-12-09 17:35:55 +04:00
DarkPhoenix
edc0418d9a Add perserverance effects 2025-12-09 14:31:05 +01:00
DarkPhoenix
1d413595b9 Update icons 2025-12-09 14:17:49 +01:00
DarkPhoenix
ce5a593f7b Add missing wightstorm booster effects 2025-12-09 14:15:35 +01:00
DarkPhoenix
faea6a97f0 Update static data 2025-12-09 14:13:14 +01:00
Amy Findlay
b92913cbf9 Fixed bug with Clipboard on Wayland 2025-11-22 19:57:55 +01:00
DarkPhoenix
1af2e7f94b Bump version & update static data 2025-11-18 12:38:44 +01:00
DarkPhoenix
dbb61a8a37 Change presentation of mining info 2025-11-17 17:25:25 +01:00
DarkPhoenix
b12adcae3d Add slowdown modifier to effects of grappler 2025-11-15 21:04:56 +01:00
DarkPhoenix
72567a7155 Add new mining charge effects 2025-11-15 15:16:01 +01:00
DarkPhoenix
0d4c2551c1 Update effects, add new ISA jargon entry and make attribute cap to use modified value 2025-11-15 14:57:39 +01:00
DarkPhoenix
ce10aeb55e Change version name back to dev 2025-11-15 13:26:07 +01:00
DarkPhoenix
7b14266f0d Update icons 2025-11-15 13:25:51 +01:00
DarkPhoenix
b436f6ec89 Change renamed items 2025-11-15 13:18:28 +01:00
DarkPhoenix
e7e3f4e626 Try non-dev name 2025-11-15 10:32:55 +01:00
DarkPhoenix
ae8405d132 Update static data and remove some now-unused data 2025-11-14 22:52:00 +01:00
DarkPhoenix
977cf61ff5 Merge branch 'master' into singularity
# Conflicts:
#	eos/effects.py
#	staticdata/fsd_built/dogmaeffects.0.json
#	staticdata/fsd_built/iconids.0.json
#	staticdata/fsd_built/marketgroups.0.json
#	staticdata/fsd_built/types.4.json
#	staticdata/fsd_built/types.5.json
#	staticdata/phobos/metadata.0.json
#	version.yml
2025-11-14 22:13:20 +01:00
DarkPhoenix
7cc4f6ec27 Add new skill effects 2025-10-30 13:00:38 +01:00
DarkPhoenix
0e6b9b48f1 Add erratic mining crystals to ammo picker 2025-10-30 12:51:51 +01:00
DarkPhoenix
a00a80b4e4 Implement effects and buffs related to expedition bursts 2025-10-30 12:44:08 +01:00
DarkPhoenix
6843373283 Implement odysseus effects 2025-10-30 12:06:24 +01:00
DarkPhoenix
408da2e344 Add outrider effects 2025-10-30 11:09:10 +01:00
DarkPhoenix
dc8ecae0f1 Implement pioneer effects 2025-10-30 10:41:05 +01:00
DarkPhoenix
50e6ed516f Implement mining burst scanner upgrade buff & faction venture effects 2025-10-30 10:23:25 +01:00
DarkPhoenix
42a6bb92a7 Implement mining crit effect for modules 2025-10-30 02:29:27 +01:00
DarkPhoenix
521a58d77b Add survey scanner effects 2025-10-30 02:19:44 +01:00
DarkPhoenix
6546f32971 Add renames 2025-10-30 02:00:31 +01:00
DarkPhoenix
ad3019debe Icon update 2025-10-30 01:56:52 +01:00
DarkPhoenix
fe9fa8b4fe Bump version 2025-10-30 01:50:52 +01:00
DarkPhoenix
92fce5be96 Update static data 2025-10-28 15:53:35 +01:00
DarkPhoenix
d271515060 Bump version 2025-10-28 14:01:28 +01:00
DarkPhoenix
343e52e556 Update static data and effects 2025-10-28 14:01:05 +01:00
DarkPhoenix
7949bc60dc Bump version 2025-10-07 08:22:34 +02:00
DarkPhoenix
bfbd3526cd Add icons 2025-10-07 08:21:36 +02:00
DarkPhoenix
c363f7359d Update effects 2025-10-07 08:18:52 +02:00
DarkPhoenix
3a7ece6a5b Update static data 2025-10-07 07:59:41 +02:00
DarkPhoenix
e0e1c1ce03 Fix BCS group name 2025-09-11 11:17:15 +02:00
cryonox
edec81f4b8 Refresh tokens when pyfa start to avoid expiry 2025-09-10 12:39:34 +08:00
220 changed files with 113417 additions and 36278 deletions

View File

@@ -32,7 +32,7 @@ for:
- sh: export PYFA_VERSION="$(python3 -B scripts/dump_version.py)" - sh: export PYFA_VERSION="$(python3 -B scripts/dump_version.py)"
- sh: mkdir build - sh: mkdir build
# Download packaging tool # Download packaging tool
- sh: curl -o $APPIMAGE_TOOL -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - sh: curl --fail-with-body -o $APPIMAGE_TOOL -L https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
- sh: chmod +x $APPIMAGE_TOOL - sh: chmod +x $APPIMAGE_TOOL
build_script: build_script:
- sh: mkdir -p AppDir/opt/pyfa - sh: mkdir -p AppDir/opt/pyfa

2
.gitattributes vendored
View File

@@ -33,4 +33,4 @@ pyfa.py text eol=lf
*.jpg binary *.jpg binary
*.icns binary *.icns binary
*.ico binary *.ico binary
*.dll filter=lfs diff=lfs merge=lfs -text

3
.gitignore vendored
View File

@@ -126,4 +126,5 @@ gitversion
/locale/progress.json /locale/progress.json
# vscode settings # vscode settings
.vscode .vscode
eve.db

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "Pyfa-Mod"]
path = Pyfa-Mod
url = https://github.com/Eivonz/Pyfa-Mod

1
Pyfa-Mod Submodule

Submodule Pyfa-Mod added at ccebbf9708

30
build.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/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
cp oleacc* dist/pyfa/
echo ""
echo "Build complete! Binary is located at: dist/pyfa/pyfa.exe"
echo "You can run it with: dist/pyfa/pyfa.exe"

File diff suppressed because it is too large Load Diff

View File

@@ -324,7 +324,7 @@ class ModifiedAttributeDict(MutableMapping):
cappingAttrKeyCache[key] = cappingKey cappingAttrKeyCache[key] = cappingKey
if cappingKey: if cappingKey:
cappingValue = self.original.get(cappingKey, self.__calculateValue(cappingKey)) cappingValue = self[cappingKey]
cappingValue = cappingValue.value if hasattr(cappingValue, "value") else cappingValue cappingValue = cappingValue.value if hasattr(cappingValue, "value") else cappingValue
else: else:
cappingValue = None cappingValue = None

View File

@@ -82,7 +82,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
self.__baseVolley = None self.__baseVolley = None
self.__baseRRAmount = None self.__baseRRAmount = None
self.__miningYield = None self.__miningYield = None
self.__miningWaste = None self.__miningDrain = None
self.__ehp = None self.__ehp = None
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self._item.attributes self.__itemModifiedAttributes.original = self._item.attributes
@@ -240,15 +240,15 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
if not ignoreState and self.amountActive <= 0: if not ignoreState and self.amountActive <= 0:
return 0 return 0
if self.__miningYield is None: if self.__miningYield is None:
self.__miningYield, self.__miningWaste = self.__calculateMining() self.__miningYield, self.__miningDrain = self.__calculateMining()
return self.__miningYield return self.__miningYield
def getMiningWPS(self, ignoreState=False): def getMiningDPS(self, ignoreState=False):
if not ignoreState and self.amountActive <= 0: if not ignoreState and self.amountActive <= 0:
return 0 return 0
if self.__miningWaste is None: if self.__miningDrain is None:
self.__miningYield, self.__miningWaste = self.__calculateMining() self.__miningYield, self.__miningDrain = self.__calculateMining()
return self.__miningWaste return self.__miningDrain
def __calculateMining(self): def __calculateMining(self):
if self.mines is True: if self.mines is True:
@@ -262,8 +262,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
yps = yield_ / (cycleTime / 1000.0) yps = yield_ / (cycleTime / 1000.0)
wasteChance = self.getModifiedItemAttr("miningWasteProbability") wasteChance = self.getModifiedItemAttr("miningWasteProbability")
wasteMult = self.getModifiedItemAttr("miningWastedVolumeMultiplier") wasteMult = self.getModifiedItemAttr("miningWastedVolumeMultiplier")
wps = yps * max(0, min(1, wasteChance / 100)) * wasteMult dps = yps * (1 + max(0, min(1, wasteChance / 100)) * wasteMult)
return yps, wps return yps, dps
else: else:
return 0, 0 return 0, 0
@@ -335,7 +335,7 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, Mu
self.__baseVolley = None self.__baseVolley = None
self.__baseRRAmount = None self.__baseRRAmount = None
self.__miningYield = None self.__miningYield = None
self.__miningWaste = None self.__miningDrain = None
self.__ehp = None self.__ehp = None
self.itemModifiedAttributes.clear() self.itemModifiedAttributes.clear()
self.chargeModifiedAttributes.clear() self.chargeModifiedAttributes.clear()

View File

@@ -140,8 +140,8 @@ class Fit:
self.__remoteRepMap = {} self.__remoteRepMap = {}
self.__minerYield = None self.__minerYield = None
self.__droneYield = None self.__droneYield = None
self.__minerWaste = None self.__minerDrain = None
self.__droneWaste = None self.__droneDrain = None
self.__droneDps = None self.__droneDps = None
self.__droneVolley = None self.__droneVolley = None
self.__sustainableTank = None self.__sustainableTank = None
@@ -378,11 +378,11 @@ class Fit:
return self.__minerYield return self.__minerYield
@property @property
def minerWaste(self): def minerDrain(self):
if self.__minerWaste is None: if self.__minerDrain is None:
self.calculatemining() self.calculatemining()
return self.__minerWaste return self.__minerDrain
@property @property
def droneYield(self): def droneYield(self):
@@ -392,19 +392,19 @@ class Fit:
return self.__droneYield return self.__droneYield
@property @property
def droneWaste(self): def droneDrain(self):
if self.__droneWaste is None: if self.__droneDrain is None:
self.calculatemining() self.calculatemining()
return self.__droneWaste return self.__droneDrain
@property @property
def totalYield(self): def totalYield(self):
return self.droneYield + self.minerYield return self.droneYield + self.minerYield
@property @property
def totalWaste(self): def totalDrain(self):
return self.droneWaste + self.minerWaste return self.droneDrain + self.minerDrain
@property @property
def maxTargets(self): def maxTargets(self):
@@ -518,8 +518,8 @@ class Fit:
self.__remoteRepMap = {} self.__remoteRepMap = {}
self.__minerYield = None self.__minerYield = None
self.__droneYield = None self.__droneYield = None
self.__minerWaste = None self.__minerDrain = None
self.__droneWaste = None self.__droneDrain = None
self.__effectiveSustainableTank = None self.__effectiveSustainableTank = None
self.__sustainableTank = None self.__sustainableTank = None
self.__droneDps = None self.__droneDps = None
@@ -702,15 +702,12 @@ class Fit:
mod.item.requiresSkill("High Speed Maneuvering"), mod.item.requiresSkill("High Speed Maneuvering"),
"speedFactor", value, stackingPenalties=True) "speedFactor", value, stackingPenalties=True)
if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining/Survey Range if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining Range
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
mod.item.requiresSkill("Ice Harvesting") or mod.item.requiresSkill("Ice Harvesting") or
mod.item.requiresSkill("Gas Cloud Harvesting"), mod.item.requiresSkill("Gas Cloud Harvesting"),
"maxRange", value, stackingPenalties=True) "maxRange", value, stackingPenalties=True)
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("CPU Management"),
"surveyScanRange", value, stackingPenalties=True)
if warfareBuffID == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration if warfareBuffID == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
mod.item.requiresSkill("Ice Harvesting") or mod.item.requiresSkill("Ice Harvesting") or
@@ -925,6 +922,36 @@ class Fit:
lambda mod: (mod.item.requiresSkill("Repair Systems") lambda mod: (mod.item.requiresSkill("Repair Systems")
or mod.item.requiresSkill("Capital Repair Systems")), or mod.item.requiresSkill("Capital Repair Systems")),
"armorDamageAmount", value, stackingPenalties=True) "armorDamageAmount", value, stackingPenalties=True)
if warfareBuffID == 2464: # Expedition Burst: Probe Strength
self.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Astrometrics'),
'baseSensorStrength', value, stackingPenalties=True)
if warfareBuffID == 2465: # Expedition Burst: Directional Scanner, Hacking and Salvager Range
self.ship.boostItemAttr("maxDirectionalScanRange", value)
self.modules.filteredItemBoost(
lambda mod: mod.item.group.name in ("Data Miners", "Salvager"), "maxRange", value, stackingPenalties=True)
if warfareBuffID == 2466: # Expedition Burst: Maximum Scan Deviation Modifier
self.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Astrometrics'),
'baseMaxScanDeviation', value, stackingPenalties=True)
if warfareBuffID == 2468: # Expedition Burst: Virus Coherence
self.modules.filteredItemIncrease(
lambda mod: mod.item.group.name == "Data Miners", "virusCoherence", value)
if warfareBuffID == 2474: # Mining burst charges
self.ship.forceItemAttr("miningScannerUpgrade", value)
if warfareBuffID == 2481: # Expedition Burst: Salvager duration bonus
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Salvaging"), "duration", value)
if warfareBuffID == 2516: # Mining Burst: Mining Crit Chance
self.modules.filteredItemBoost(
lambda mod: mod.item.requiresSkill("Mining") or mod.item.requiresSkill("Ice Harvesting"),
"miningCritChance", value)
if warfareBuffID == 2517: # Mining Burst: Mining Residue Chance Reduction
self.modules.filteredItemBoost(
lambda mod: (
mod.item.requiresSkill("Mining")
or mod.item.requiresSkill("Ice Harvesting")
or mod.item.requiresSkill("Gas Cloud Harvesting")),
"miningWasteProbability", value, stackingPenalties=True)
del self.commandBonuses[warfareBuffID] del self.commandBonuses[warfareBuffID]
@@ -1707,21 +1734,21 @@ class Fit:
def calculatemining(self): def calculatemining(self):
minerYield = 0 minerYield = 0
minerWaste = 0 minerDrain = 0
droneYield = 0 droneYield = 0
droneWaste = 0 droneDrain = 0
for mod in self.modules: for mod in self.modules:
minerYield += mod.getMiningYPS() minerYield += mod.getMiningYPS()
minerWaste += mod.getMiningWPS() minerDrain += mod.getMiningDPS()
for drone in self.drones: for drone in self.drones:
droneYield += drone.getMiningYPS() droneYield += drone.getMiningYPS()
droneWaste += drone.getMiningWPS() droneDrain += drone.getMiningDPS()
self.__minerYield = minerYield self.__minerYield = minerYield
self.__minerWaste = minerWaste self.__minerDrain = minerDrain
self.__droneYield = droneYield self.__droneYield = droneYield
self.__droneWaste = droneWaste self.__droneDrain = droneDrain
def calculateWeaponDmgStats(self, spoolOptions): def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes.default() weaponVolley = DmgTypes.default()

View File

@@ -127,7 +127,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
self.__baseVolley = None self.__baseVolley = None
self.__baseRRAmount = None self.__baseRRAmount = None
self.__miningYield = None self.__miningYield = None
self.__miningWaste = None self.__miningDrain = None
self.__reloadTime = None self.__reloadTime = None
self.__reloadForce = None self.__reloadForce = None
self.__chargeCycles = None self.__chargeCycles = None
@@ -418,17 +418,17 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
if not ignoreState and self.state < FittingModuleState.ACTIVE: if not ignoreState and self.state < FittingModuleState.ACTIVE:
return 0 return 0
if self.__miningYield is None: if self.__miningYield is None:
self.__miningYield, self.__miningWaste = self.__calculateMining() self.__miningYield, self.__miningDrain = self.__calculateMining()
return self.__miningYield return self.__miningYield
def getMiningWPS(self, ignoreState=False): def getMiningDPS(self, ignoreState=False):
if self.isEmpty: if self.isEmpty:
return 0 return 0
if not ignoreState and self.state < FittingModuleState.ACTIVE: if not ignoreState and self.state < FittingModuleState.ACTIVE:
return 0 return 0
if self.__miningWaste is None: if self.__miningDrain is None:
self.__miningYield, self.__miningWaste = self.__calculateMining() self.__miningYield, self.__miningDrain = self.__calculateMining()
return self.__miningWaste return self.__miningDrain
def __calculateMining(self): def __calculateMining(self):
yield_ = self.getModifiedItemAttr("miningAmount") yield_ = self.getModifiedItemAttr("miningAmount")
@@ -443,8 +443,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
yps = 0 yps = 0
wasteChance = self.getModifiedItemAttr("miningWasteProbability") wasteChance = self.getModifiedItemAttr("miningWasteProbability")
wasteMult = self.getModifiedItemAttr("miningWastedVolumeMultiplier") wasteMult = self.getModifiedItemAttr("miningWastedVolumeMultiplier")
wps = yps * max(0, min(1, wasteChance / 100)) * wasteMult dps = yps * (1 + max(0, min(1, wasteChance / 100)) * wasteMult)
return yps, wps critChance = self.getModifiedItemAttr("miningCritChance")
critBonusMult = self.getModifiedItemAttr("miningCritBonusYield")
yps += yps * critChance * critBonusMult
return yps, dps
def isDealingDamage(self, ignoreState=False): def isDealingDamage(self, ignoreState=False):
volleyParams = self.getVolleyParameters(ignoreState=ignoreState) volleyParams = self.getVolleyParameters(ignoreState=ignoreState)
@@ -894,7 +897,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut, M
self.__baseVolley = None self.__baseVolley = None
self.__baseRRAmount = None self.__baseRRAmount = None
self.__miningYield = None self.__miningYield = None
self.__miningWaste = None self.__miningDrain = None
self.__reloadTime = None self.__reloadTime = None
self.__reloadForce = None self.__reloadForce = None
self.__chargeCycles = None self.__chargeCycles = None

View File

@@ -126,7 +126,7 @@ class Ship(ItemAttrShortcut, HandledItem):
valid Item objects, not the Mode objects. Returns None if not a valid Item objects, not the Mode objects. Returns None if not a
t3 dessy t3 dessy
""" """
if self.item.group.name != "Tactical Destroyer": if self.item.group.name != "Tactical Destroyer" and self.item.name != "Anhinga":
return None return None
items = [] items = []

View File

@@ -47,6 +47,8 @@ class AddCurrentlyOpenFit(ContextMenuUnconditional):
if isinstance(page, BlankPage): if isinstance(page, BlankPage):
continue continue
fit = sFit.getFit(page.activeFitID, basic=True) fit = sFit.getFit(page.activeFitID, basic=True)
if fit is None:
continue
id = ContextMenuUnconditional.nextID() id = ContextMenuUnconditional.nextID()
mitem = wx.MenuItem(rootMenu, id, "{}: {}".format(fit.ship.item.name, fit.name)) mitem = wx.MenuItem(rootMenu, id, "{}: {}".format(fit.ship.item.name, fit.name))
bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem) bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem)

View File

@@ -39,6 +39,7 @@ class ChangeModuleAmmo(ContextMenuCombined):
('r16', _t('Moon Uncommon')), ('r16', _t('Moon Uncommon')),
('r32', _t('Moon Rare')), ('r32', _t('Moon Rare')),
('r64', _t('Moon Exceptional')), ('r64', _t('Moon Exceptional')),
('err', _t('Erratic')),
('misc', _t('Misc'))]) ('misc', _t('Misc'))])
def display(self, callingWindow, srcContext, mainItem, selection): def display(self, callingWindow, srcContext, mainItem, selection):

View File

@@ -17,7 +17,10 @@ class ChangeShipTacticalMode(ContextMenuUnconditional):
self.modeMap = { self.modeMap = {
'Defense': _t('Defense'), 'Defense': _t('Defense'),
'Propulsion': _t('Propulsion'), 'Propulsion': _t('Propulsion'),
'Sharpshooter': _t('Sharpshooter') 'Sharpshooter': _t('Sharpshooter'),
'Primary': _t('Primary'),
'Secondary': _t('Secondary'),
'Tertiary': _t('Tertiary'),
} }
def display(self, callingWindow, srcContext): def display(self, callingWindow, srcContext):

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 and btn.IsEnabled():
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()
@@ -212,6 +318,23 @@ class ItemView(Display):
btn.setMetaAvailable(True) btn.setMetaAvailable(True)
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()
isAvailable = fitId is not None
btn.setMetaAvailable(isAvailable)
if not isAvailable:
btn.setUserSelection(False)
elif btn.filterType == "slot":
# Slot button is available if items with that slot exist in current view
isAvailable = btn.slotType in slotIDs
btn.setMetaAvailable(isAvailable)
if not isAvailable:
btn.setUserSelection(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

View File

@@ -130,9 +130,9 @@ class MiningYieldViewFull(StatsView):
def refreshPanel(self, fit): def refreshPanel(self, fit):
# If we did anything intresting, we'd update our labels to reflect the new fit's stats here # If we did anything intresting, we'd update our labels to reflect the new fit's stats here
stats = (("labelFullminingyieldMiner", lambda: fit.minerYield, lambda: fit.minerWaste, 3, 0, 0, "{}{} m\u00B3/s", None), stats = (("labelFullminingyieldMiner", lambda: fit.minerYield, lambda: fit.minerDrain, 3, 0, 0, "{} m\u00B3/s", None),
("labelFullminingyieldDrone", lambda: fit.droneYield, lambda: fit.droneWaste, 3, 0, 0, "{}{} m\u00B3/s", None), ("labelFullminingyieldDrone", lambda: fit.droneYield, lambda: fit.droneDrain, 3, 0, 0, "{} m\u00B3/s", None),
("labelFullminingyieldTotal", lambda: fit.totalYield, lambda: fit.totalWaste, 3, 0, 0, "{}{} m\u00B3/s", None)) ("labelFullminingyieldTotal", lambda: fit.totalYield, lambda: fit.totalDrain, 3, 0, 0, "{} m\u00B3/s", None))
def processValue(value): def processValue(value):
value = value() if fit is not None else 0 value = value() if fit is not None else 0
@@ -140,23 +140,26 @@ class MiningYieldViewFull(StatsView):
return value return value
counter = 0 counter = 0
for labelName, yieldValue, wasteValue, prec, lowest, highest, valueFormat, altFormat in stats: for labelName, yieldValue, drainValue, prec, lowest, highest, valueFormat, altFormat in stats:
label = getattr(self, labelName) label = getattr(self, labelName)
yieldValue = processValue(yieldValue) yieldValue = processValue(yieldValue)
wasteValue = processValue(wasteValue) drainValue = processValue(drainValue)
if self._cachedValues[counter] != (yieldValue, wasteValue): if self._cachedValues[counter] != (yieldValue, drainValue):
try:
efficiency = '{}%'.format(formatAmount(yieldValue / drainValue * 100, 4, 0, 0))
except ZeroDivisionError:
efficiency = '0%'
yps = formatAmount(yieldValue, prec, lowest, highest) yps = formatAmount(yieldValue, prec, lowest, highest)
yph = formatAmount(yieldValue * 3600, prec, lowest, highest) yph = formatAmount(yieldValue * 3600, prec, lowest, highest)
wps = formatAmount(wasteValue, prec, lowest, highest) dps = formatAmount(drainValue, prec, lowest, highest)
wph = formatAmount(wasteValue * 3600, prec, lowest, highest) dph = formatAmount(drainValue * 3600, prec, lowest, highest)
wasteSuffix = '\u02b7' if wasteValue > 0 else '' label.SetLabel(valueFormat.format(yps))
label.SetLabel(valueFormat.format(yps, wasteSuffix))
tipLines = [] tipLines = []
tipLines.append("{} m\u00B3 mining yield per second ({} m\u00B3 per hour)".format(yps, yph)) tipLines.append("{} m\u00B3 yield per second ({} m\u00B3 per hour)".format(yps, yph))
if wasteValue > 0: tipLines.append("{} m\u00B3 drain per second ({} m\u00B3 per hour)".format(dps, dph))
tipLines.append("{} m\u00B3 mining waste per second ({} m\u00B3 per hour)".format(wps, wph)) tipLines.append(f'{efficiency} efficiency')
label.SetToolTip(wx.ToolTip('\n'.join(tipLines))) label.SetToolTip(wx.ToolTip('\n'.join(tipLines)))
self._cachedValues[counter] = (yieldValue, wasteValue) self._cachedValues[counter] = (yieldValue, drainValue)
counter += 1 counter += 1
self.panel.Layout() self.panel.Layout()
self.headerPanel.Layout() self.headerPanel.Layout()

View File

@@ -130,6 +130,7 @@ class TargetingMiscViewMinimal(StatsView):
("specialPlanetaryCommoditiesHoldCapacity", _t("Planetary goods hold")), ("specialPlanetaryCommoditiesHoldCapacity", _t("Planetary goods hold")),
("specialQuafeHoldCapacity", _t("Quafe hold")), ("specialQuafeHoldCapacity", _t("Quafe hold")),
("specialMobileDepotHoldCapacity", _t("Mobile depot hold")), ("specialMobileDepotHoldCapacity", _t("Mobile depot hold")),
("specialExpeditionHoldCapacity", _t("Expedition hold")),
)) ))
cargoValues = { cargoValues = {
@@ -154,6 +155,7 @@ class TargetingMiscViewMinimal(StatsView):
"specialPlanetaryCommoditiesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialPlanetaryCommoditiesHoldCapacity"), "specialPlanetaryCommoditiesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialPlanetaryCommoditiesHoldCapacity"),
"specialQuafeHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialQuafeHoldCapacity"), "specialQuafeHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialQuafeHoldCapacity"),
"specialMobileDepotHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialMobileDepotHoldCapacity"), "specialMobileDepotHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialMobileDepotHoldCapacity"),
"specialExpeditionHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialExpeditionHoldCapacity"),
} }
stats = (("labelTargets", {"main": lambda: fit.maxTargets}, 3, 0, 0, ""), stats = (("labelTargets", {"main": lambda: fit.maxTargets}, 3, 0, 0, ""),

View File

@@ -27,6 +27,7 @@ from gui.viewColumn import ViewColumn
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount from gui.utils.numberFormatter import formatAmount
from gui.utils.listFormatter import formatList from gui.utils.listFormatter import formatList
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.spoolSupport import SpoolType, SpoolOptions
import eos.config import eos.config
@@ -195,7 +196,7 @@ class Miscellanea(ViewColumn):
tooltip = "Warp core strength modification" tooltip = "Warp core strength modification"
return text, tooltip return text, tooltip
elif ( elif (
itemGroup in ("Stasis Web", "Stasis Webifying Drone", "Structure Stasis Webifier") or itemGroup in ("Stasis Web", "Stasis Grappler", "Stasis Webifying Drone", "Structure Stasis Webifier") or
(itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEWeb" in item.effects) (itemGroup in ("Structure Burst Projector", "Burst Projectors") and "doomsdayAOEWeb" in item.effects)
): ):
speedFactor = stuff.getModifiedItemAttr("speedFactor") speedFactor = stuff.getModifiedItemAttr("speedFactor")
@@ -291,7 +292,7 @@ class Miscellanea(ViewColumn):
"Gyrostabilizer", "Gyrostabilizer",
"Magnetic Field Stabilizer", "Magnetic Field Stabilizer",
"Heat Sink", "Heat Sink",
"Ballistic Control system", "Ballistic Control System",
"Structure Weapon Upgrade", "Structure Weapon Upgrade",
"Entropic Radiation Sink", "Entropic Radiation Sink",
"Vorton Projector Upgrade" "Vorton Projector Upgrade"
@@ -300,7 +301,7 @@ class Miscellanea(ViewColumn):
"Gyrostabilizer": ("damageMultiplier", "speedMultiplier", "Projectile weapon"), "Gyrostabilizer": ("damageMultiplier", "speedMultiplier", "Projectile weapon"),
"Magnetic Field Stabilizer": ("damageMultiplier", "speedMultiplier", "Hybrid weapon"), "Magnetic Field Stabilizer": ("damageMultiplier", "speedMultiplier", "Hybrid weapon"),
"Heat Sink": ("damageMultiplier", "speedMultiplier", "Energy weapon"), "Heat Sink": ("damageMultiplier", "speedMultiplier", "Energy weapon"),
"Ballistic Control system": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"), "Ballistic Control System": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Structure Weapon Upgrade": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"), "Structure Weapon Upgrade": ("missileDamageMultiplierBonus", "speedMultiplier", "Missile"),
"Entropic Radiation Sink": ("damageMultiplier", "speedMultiplier", "Precursor weapon"), "Entropic Radiation Sink": ("damageMultiplier", "speedMultiplier", "Precursor weapon"),
"Vorton Projector Upgrade": ("damageMultiplier", "speedMultiplier", "Vorton projector")} "Vorton Projector Upgrade": ("damageMultiplier", "speedMultiplier", "Vorton projector")}
@@ -547,18 +548,24 @@ class Miscellanea(ViewColumn):
if not yps: if not yps:
return "", None return "", None
yph = yps * 3600 yph = yps * 3600
wps = stuff.getMiningWPS(ignoreState=True) dps = stuff.getMiningDPS(ignoreState=True)
wph = wps * 3600 dph = dps * 3600
try:
efficiency = yps / dps
except ZeroDivisionError:
efficiency = 0
textParts = [] textParts = []
textParts.append(formatAmount(yps, 3, 0, 3))
tipLines = [] tipLines = []
textParts.append('{} m\u00B3/s'.format(formatAmount(yps, 3, 0, 3)))
tipLines.append("{} m\u00B3 mining yield per second ({} m\u00B3 per hour)".format( tipLines.append("{} m\u00B3 mining yield per second ({} m\u00B3 per hour)".format(
formatAmount(yps, 3, 0, 3), formatAmount(yph, 3, 0, 3))) formatAmount(yps, 3, 0, 3), formatAmount(yph, 3, 0, 3)))
if wps > 0: tipLines.append("{} m\u00B3 mining drain per second ({} m\u00B3 per hour)".format(
textParts.append(formatAmount(wps, 3, 0, 3)) formatAmount(dps, 3, 0, 3), formatAmount(dph, 3, 0, 3)))
tipLines.append("{} m\u00B3 mining waste per second ({} m\u00B3 per hour)".format( if floatUnerr(efficiency) != 1:
formatAmount(wps, 3, 0, 3), formatAmount(wph, 3, 0, 3))) eff_text = '{}%'.format(formatAmount(efficiency * 100, 4, 0, 0))
text = '{} m\u00B3/s'.format('+'.join(textParts)) textParts.append(eff_text)
tipLines.append(f"{eff_text} mining efficiency")
text = '{}'.format(' | '.join(textParts))
tooltip = '\n'.join(tipLines) tooltip = '\n'.join(tipLines)
return text, tooltip return text, tooltip
elif itemGroup == "Logistic Drone": elif itemGroup == "Logistic Drone":
@@ -701,7 +708,7 @@ class Miscellanea(ViewColumn):
formatAmount(itemArmorResistanceShiftHardenerExp, 3, 0, 3), formatAmount(itemArmorResistanceShiftHardenerExp, 3, 0, 3),
) )
return text, tooltip return text, tooltip
elif itemGroup in ("Cargo Scanner", "Ship Scanner", "Survey Scanner"): elif itemGroup in ("Cargo Scanner", "Ship Scanner"):
duration = stuff.getModifiedItemAttr("duration") duration = stuff.getModifiedItemAttr("duration")
if not duration: if not duration:
return "", None return "", None
@@ -766,15 +773,36 @@ class Miscellanea(ViewColumn):
elif buffId == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase elif buffId == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%") textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("AB/MWD speed increase") tooltipSections.append("AB/MWD speed increase")
elif buffId == 23: # Mining Burst: Mining Laser Field Enhancement: Mining/Survey Range elif buffId == 23: # Mining Burst: Mining Laser Field Enhancement: Mining Range
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%") textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("mining/survey module range") tooltipSections.append("mining module range")
elif buffId == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration elif buffId == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%") textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("mining module duration & capacitor use") tooltipSections.append("mining module duration & capacitor use")
elif buffId == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility elif buffId == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%") textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("mining crystal volatility") tooltipSections.append("mining crystal volatility")
elif buffId == 2464: # Expedition Burst: Probe Strength
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("scan probe strength")
elif buffId == 2465: # Expedition Burst: Directional Scanner, Hacking and Salvager Range
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("dscan, hacking & salvaging range")
elif buffId == 2466: # Expedition Burst: Maximum Scan Deviation Modifier
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("scan probe deviation")
elif buffId == 2468: # Expedition Burst: Virus Coherence
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}")
tooltipSections.append("virus coherence")
elif buffId == 2481: # Expedition Burst: Salvager duration bonus
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("salvager cycle time")
elif buffId == 2516: # Mining Burst: Mining Crit Chance
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("crit chance")
elif buffId == 2517: # Mining Burst: Mining Residue Chance Reduction
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("waste chance")
if not textSections: if not textSections:
return '', None return '', None
text = ' | '.join(textSections) text = ' | '.join(textSections)

View File

@@ -106,6 +106,9 @@ class CharacterSelection(wx.Panel):
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills")) exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem) self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (condensed)"))
self.Bind(wx.EVT_MENU, self.exportSkillsCondensed, exportItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)")) exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)"))
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem) self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem)
@@ -268,6 +271,15 @@ class CharacterSelection(wx.Panel):
toClipboard(list) toClipboard(list)
def exportSkillsCondensed(self, evt):
skillsMap = self._buildSkillsTooltipSuperCondensed(self.reqs, skillsMap={})
list = ""
for key in sorted(skillsMap):
list += "%s %d\n" % (key, skillsMap[key][0])
toClipboard(list)
def exportSkillsEveMon(self, evt): def exportSkillsEveMon(self, evt):
skillsMap = self._buildSkillsTooltipCondensed(self.reqs, skillsMap={}) skillsMap = self._buildSkillsTooltipCondensed(self.reqs, skillsMap={})

View File

@@ -18,6 +18,7 @@
# ============================================================================= # =============================================================================
import datetime import datetime
import itertools
import os.path import os.path
import threading import threading
import time import time
@@ -60,8 +61,12 @@ from gui.shipBrowser import ShipBrowser
from gui.statsPane import StatsPane from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditor from gui.targetProfileEditor import TargetProfileEditor
from gui.updateDialog import UpdateDialog from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard from gui.utils.clipboard import fromClipboard, toClipboard
from gui.utils.progressHelper import ProgressHelper from gui.utils.progressHelper import ProgressHelper
from eos.const import FittingSlot as es_Slot
from eos.saveddata.character import Skill
from eos.saveddata.fighter import Fighter as es_Fighter
from eos.saveddata.module import Module as es_Module
from service.character import Character from service.character import Character
from service.esi import Esi from service.esi import Esi
from service.fit import Fit from service.fit import Fit
@@ -522,6 +527,8 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.backupToXml, id=menuBar.backupFitsId) self.Bind(wx.EVT_MENU, self.backupToXml, id=menuBar.backupFitsId)
# Export skills needed # Export skills needed
self.Bind(wx.EVT_MENU, self.exportSkillsNeeded, id=menuBar.exportSkillsNeededId) self.Bind(wx.EVT_MENU, self.exportSkillsNeeded, id=menuBar.exportSkillsNeededId)
# Copy skills needed
self.Bind(wx.EVT_MENU, self.copySkillsNeeded, id=menuBar.copySkillsNeededId)
# Import character # Import character
self.Bind(wx.EVT_MENU, self.importCharacter, id=menuBar.importCharacterId) self.Bind(wx.EVT_MENU, self.importCharacter, id=menuBar.importCharacterId)
# Export HTML # Export HTML
@@ -832,6 +839,60 @@ class MainFrame(wx.Frame):
self.waitDialog = wx.BusyInfo(_t("Exporting skills needed..."), parent=self) self.waitDialog = wx.BusyInfo(_t("Exporting skills needed..."), parent=self)
sCharacter.backupSkills(filePath, saveFmt, self.getActiveFit(), self.closeWaitDialog) sCharacter.backupSkills(filePath, saveFmt, self.getActiveFit(), self.closeWaitDialog)
def copySkillsNeeded(self, event):
""" Copies skills used by the fit that the character has to clipboard """
activeFitID = self.getActiveFit()
if activeFitID is None:
return
sFit = Fit.getInstance()
fit = sFit.getFit(activeFitID)
if fit is None:
return
if not fit.calculated:
fit.calculate()
char = fit.character
skillsMap = {}
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo):
self._collectAffectingSkills(thing, char, skillsMap)
skillsList = ""
for skillName in sorted(skillsMap):
charLevel = skillsMap[skillName]
for level in range(1, charLevel + 1):
skillsList += "%s %d\n" % (skillName, level)
toClipboard(skillsList)
def _collectAffectingSkills(self, thing, char, skillsMap):
""" Collect skills that affect items in the fit that the character has """
for attr in ("item", "charge"):
if attr == "charge" and isinstance(thing, es_Fighter):
continue
subThing = getattr(thing, attr, None)
if subThing is None:
continue
if isinstance(thing, es_Fighter) and attr == "charge":
continue
if attr == "charge":
cont = getattr(thing, "chargeModifiedAttributes", None)
else:
cont = getattr(thing, "itemModifiedAttributes", None)
if cont is not None:
for attrName in cont.iterAfflictions():
for fit, afflictors in cont.getAfflictions(attrName).items():
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if isinstance(afflictor, Skill) and afflictor.character == char:
skillName = afflictor.item.name
if skillName not in skillsMap:
skillsMap[skillName] = afflictor.level
elif skillsMap[skillName] < afflictor.level:
skillsMap[skillName] = afflictor.level
def fileImportDialog(self, event): def fileImportDialog(self, event):
"""Handles importing single/multiple EVE XML / EFT cfg fit files""" """Handles importing single/multiple EVE XML / EFT cfg fit files"""
with wx.FileDialog( with wx.FileDialog(

View File

@@ -42,6 +42,7 @@ class MainMenuBar(wx.MenuBar):
self.graphFrameId = wx.NewId() self.graphFrameId = wx.NewId()
self.backupFitsId = wx.NewId() self.backupFitsId = wx.NewId()
self.exportSkillsNeededId = wx.NewId() self.exportSkillsNeededId = wx.NewId()
self.copySkillsNeededId = wx.NewId()
self.importCharacterId = wx.NewId() self.importCharacterId = wx.NewId()
self.exportHtmlId = wx.NewId() self.exportHtmlId = wx.NewId()
self.wikiId = wx.NewId() self.wikiId = wx.NewId()
@@ -117,6 +118,7 @@ class MainMenuBar(wx.MenuBar):
characterMenu.AppendSeparator() characterMenu.AppendSeparator()
characterMenu.Append(self.importCharacterId, _t("&Import Character File"), _t("Import characters into pyfa from file")) characterMenu.Append(self.importCharacterId, _t("&Import Character File"), _t("Import characters into pyfa from file"))
characterMenu.Append(self.exportSkillsNeededId, _t("&Export Skills Needed"), _t("Export skills needed for this fitting")) characterMenu.Append(self.exportSkillsNeededId, _t("&Export Skills Needed"), _t("Export skills needed for this fitting"))
characterMenu.Append(self.copySkillsNeededId, _t("&Copy Skills Needed"), _t("Copy skills needed for this fitting to clipboard"))
characterMenu.AppendSeparator() characterMenu.AppendSeparator()
characterMenu.Append(self.ssoLoginId, _t("&Manage ESI Characters")) characterMenu.Append(self.ssoLoginId, _t("&Manage ESI Characters"))
@@ -178,6 +180,7 @@ class MainMenuBar(wx.MenuBar):
self.Enable(wx.ID_SAVEAS, enable) self.Enable(wx.ID_SAVEAS, enable)
self.Enable(wx.ID_COPY, enable) self.Enable(wx.ID_COPY, enable)
self.Enable(self.exportSkillsNeededId, enable) self.Enable(self.exportSkillsNeededId, enable)
self.Enable(self.copySkillsNeededId, enable)
self.refreshUndo() self.refreshUndo()

View File

@@ -54,6 +54,7 @@ class MarketBrowser(wx.Panel):
self.settings = MarketPriceSettings.getInstance() self.settings = MarketPriceSettings.getInstance()
self.__mode = 'normal' self.__mode = 'normal'
self.__normalBtnMap = {} self.__normalBtnMap = {}
self.__normalSlotBtnMap = {}
self.marketView = MarketTree(self.splitter, self) self.marketView = MarketTree(self.splitter, self)
self.itemView = ItemView(self.splitter, self) self.itemView = ItemView(self.splitter, self)
@@ -64,22 +65,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 +140,21 @@ class MarketBrowser(wx.Panel):
self.itemView.filterItemStore() self.itemView.filterItemStore()
def toggleSlotButton(self, event):
"""Process clicks on slot/fits filter buttons"""
clickedBtn = event.EventObject
# All buttons (Fits, High, Med, Low, Rig) work as checkboxes (independent toggles)
clickedBtn.setUserSelection(clickedBtn.GetValue())
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)
@@ -141,6 +196,9 @@ class MarketBrowser(wx.Panel):
self.__normalBtnMap.clear() self.__normalBtnMap.clear()
for btn in self.metaButtons: for btn in self.metaButtons:
self.__normalBtnMap[btn] = btn.userSelected self.__normalBtnMap[btn] = btn.userSelected
self.__normalSlotBtnMap.clear()
for btn in self.slotButtons:
self.__normalSlotBtnMap[btn] = btn.userSelected
if newMode == 'search': if newMode == 'search':
self.marketView.UnselectAll() self.marketView.UnselectAll()
setting = self.settings.get('marketMGSearchMode') setting = self.settings.get('marketMGSearchMode')
@@ -149,12 +207,16 @@ class MarketBrowser(wx.Panel):
if newMode in ('search', 'recent', 'charges'): if newMode in ('search', 'recent', 'charges'):
for btn in self.metaButtons: for btn in self.metaButtons:
btn.setUserSelection(True) btn.setUserSelection(True)
# Clear slot button selections when searching (search can return any item type)
for btn in self.slotButtons:
btn.setUserSelection(False)
if newMode == 'normal': if newMode == 'normal':
for btn, state in self.__normalBtnMap.items(): for btn, state in self.__normalBtnMap.items():
btn.setUserSelection(state) btn.setUserSelection(state)
for btn, state in self.__normalSlotBtnMap.items():
btn.setUserSelection(state)
# We turn on all meta buttons permanently # We turn on all meta buttons permanently
if setting == 2: if setting == 2:
for btn in self.metaButtons: for btn in self.metaButtons:
btn.setUserSelection(True) btn.setUserSelection(True)
self.__mode = newMode self.__mode = newMode

View File

@@ -1,22 +1,68 @@
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import wx import wx
from logbook import Logger
logger = Logger(__name__)
def toClipboard(text): def toClipboard(text):
clip = wx.TheClipboard """
clip.Open() Copy text to clipboard. Explicitly uses CLIPBOARD selection, not PRIMARY.
data = wx.TextDataObject(text)
clip.SetData(data) On X11 systems, wxPython can confuse between PRIMARY and CLIPBOARD selections,
clip.Close() causing "already open" errors. This function ensures we always use CLIPBOARD.
See: https://discuss.wxpython.org/t/wx-theclipboard-pasting-different-content-on-every-second-paste/35361
"""
clipboard = wx.TheClipboard
try:
# Explicitly use CLIPBOARD selection, not PRIMARY selection
# This prevents X11 confusion between the two clipboard types
clipboard.UsePrimarySelection(False)
if clipboard.Open():
try:
data = wx.TextDataObject(text)
clipboard.SetData(data)
return True
finally:
clipboard.Close()
else:
logger.debug("Failed to open clipboard for writing")
return False
except Exception as e:
logger.warning("Error writing to clipboard: {}", e)
return False
def fromClipboard(): def fromClipboard():
clip = wx.TheClipboard """
clip.Open() Read text from clipboard. Explicitly uses CLIPBOARD selection, not PRIMARY.
data = wx.TextDataObject("")
if clip.GetData(data): On X11 systems, wxPython can confuse between PRIMARY and CLIPBOARD selections,
clip.Close() causing "already open" errors. This function ensures we always use CLIPBOARD.
return data.GetText()
else: See: https://discuss.wxpython.org/t/wx-theclipboard-pasting-different-content-on-every-second-paste/35361
clip.Close() """
clipboard = wx.TheClipboard
try:
# Explicitly use CLIPBOARD selection, not PRIMARY selection
# This prevents X11 confusion between the two clipboard types
clipboard.UsePrimarySelection(False)
if clipboard.Open():
try:
data = wx.TextDataObject()
if clipboard.GetData(data):
return data.GetText()
else:
logger.debug("Clipboard open but no CLIPBOARD data available")
return None
finally:
clipboard.Close()
else:
logger.debug("Failed to open clipboard for reading")
return None
except Exception as e:
logger.warning("Error reading from clipboard: {}", e)
return None return None

BIN
imgs/icons/10850@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

BIN
imgs/icons/10850@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
imgs/icons/1546@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

BIN
imgs/icons/1546@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/24566@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

BIN
imgs/icons/24566@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
imgs/icons/25235@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

BIN
imgs/icons/25235@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25236@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
imgs/icons/25236@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25237@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
imgs/icons/25237@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25238@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

BIN
imgs/icons/25238@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25240@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

BIN
imgs/icons/25240@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25241@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

BIN
imgs/icons/25241@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25242@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

BIN
imgs/icons/25242@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25243@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

BIN
imgs/icons/25243@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25245@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

BIN
imgs/icons/25245@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
imgs/icons/25246@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

BIN
imgs/icons/25246@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25247@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

BIN
imgs/icons/25247@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25248@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

BIN
imgs/icons/25248@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25250@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

BIN
imgs/icons/25250@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/25251@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

BIN
imgs/icons/25251@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/25252@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

BIN
imgs/icons/25252@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
imgs/icons/25253@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

BIN
imgs/icons/25253@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27053@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

BIN
imgs/icons/27053@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27054@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

BIN
imgs/icons/27054@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27055@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

BIN
imgs/icons/27055@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27056@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

BIN
imgs/icons/27056@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27058@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

BIN
imgs/icons/27139@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

BIN
imgs/icons/27139@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27154@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

BIN
imgs/icons/27154@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
imgs/icons/27198@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

BIN
imgs/icons/27198@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27199@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

BIN
imgs/icons/27199@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27200@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

BIN
imgs/icons/27200@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27201@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
imgs/icons/27201@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
imgs/icons/27202@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

BIN
imgs/icons/27202@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27203@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 B

BIN
imgs/icons/27203@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
imgs/icons/27204@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

BIN
imgs/icons/27204@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
imgs/icons/27205@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

BIN
imgs/icons/27205@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27206@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

BIN
imgs/icons/27206@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27207@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

BIN
imgs/icons/27207@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
imgs/icons/27208@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

BIN
imgs/icons/27208@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
imgs/icons/27209@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
imgs/icons/27209@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
imgs/icons/27210@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Some files were not shown because too many files have changed in this diff Show More