Compare commits

...

71 Commits

Author SHA1 Message Date
169b041677 Add an "all" skills button to any right click skills menu 2026-01-09 20:50:34 +01:00
3a5a9c6e09 Implement "import from clipboard" for skills 2026-01-04 16:02:45 +01:00
eadf18ec00 Add price to market search 2026-01-02 16:12:00 +01:00
b70833ea3e Add a necessary skills tab to ship skills 2026-01-01 22:42:50 +01:00
f12a0fe237 Implement shift-tab to switch between selected characters 2026-01-01 22:30:46 +01:00
de7f6a0523 Move the skills menu to the ship stats window 2026-01-01 22:15:02 +01:00
fa6dc76d10 Actually don't include ship in required skills 2026-01-01 22:02:15 +01:00
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
cryonox
edec81f4b8 Refresh tokens when pyfa start to avoid expiry 2025-09-10 12:39:34 +08:00
192 changed files with 90489 additions and 29549 deletions

View File

@@ -32,7 +32,7 @@ for:
- sh: export PYFA_VERSION="$(python3 -B scripts/dump_version.py)"
- sh: mkdir build
# 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
build_script:
- sh: mkdir -p AppDir/opt/pyfa

2
.gitattributes vendored
View File

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

3
.gitignore vendored
View File

@@ -126,4 +126,5 @@ gitversion
/locale/progress.json
# 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
if cappingKey:
cappingValue = self.original.get(cappingKey, self.__calculateValue(cappingKey))
cappingValue = self[cappingKey]
cappingValue = cappingValue.value if hasattr(cappingValue, "value") else cappingValue
else:
cappingValue = None

View File

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

View File

@@ -140,8 +140,8 @@ class Fit:
self.__remoteRepMap = {}
self.__minerYield = None
self.__droneYield = None
self.__minerWaste = None
self.__droneWaste = None
self.__minerDrain = None
self.__droneDrain = None
self.__droneDps = None
self.__droneVolley = None
self.__sustainableTank = None
@@ -378,11 +378,11 @@ class Fit:
return self.__minerYield
@property
def minerWaste(self):
if self.__minerWaste is None:
def minerDrain(self):
if self.__minerDrain is None:
self.calculatemining()
return self.__minerWaste
return self.__minerDrain
@property
def droneYield(self):
@@ -392,19 +392,19 @@ class Fit:
return self.__droneYield
@property
def droneWaste(self):
if self.__droneWaste is None:
def droneDrain(self):
if self.__droneDrain is None:
self.calculatemining()
return self.__droneWaste
return self.__droneDrain
@property
def totalYield(self):
return self.droneYield + self.minerYield
@property
def totalWaste(self):
return self.droneWaste + self.minerWaste
def totalDrain(self):
return self.droneDrain + self.minerDrain
@property
def maxTargets(self):
@@ -518,8 +518,8 @@ class Fit:
self.__remoteRepMap = {}
self.__minerYield = None
self.__droneYield = None
self.__minerWaste = None
self.__droneWaste = None
self.__minerDrain = None
self.__droneDrain = None
self.__effectiveSustainableTank = None
self.__sustainableTank = None
self.__droneDps = None
@@ -702,15 +702,12 @@ class Fit:
mod.item.requiresSkill("High Speed Maneuvering"),
"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
mod.item.requiresSkill("Ice Harvesting") or
mod.item.requiresSkill("Gas Cloud Harvesting"),
"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
self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or
mod.item.requiresSkill("Ice Harvesting") or
@@ -925,6 +922,36 @@ class Fit:
lambda mod: (mod.item.requiresSkill("Repair Systems")
or mod.item.requiresSkill("Capital Repair Systems")),
"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]
@@ -1707,21 +1734,21 @@ class Fit:
def calculatemining(self):
minerYield = 0
minerWaste = 0
minerDrain = 0
droneYield = 0
droneWaste = 0
droneDrain = 0
for mod in self.modules:
minerYield += mod.getMiningYPS()
minerWaste += mod.getMiningWPS()
minerDrain += mod.getMiningDPS()
for drone in self.drones:
droneYield += drone.getMiningYPS()
droneWaste += drone.getMiningWPS()
droneDrain += drone.getMiningDPS()
self.__minerYield = minerYield
self.__minerWaste = minerWaste
self.__minerDrain = minerDrain
self.__droneYield = droneYield
self.__droneWaste = droneWaste
self.__droneDrain = droneDrain
def calculateWeaponDmgStats(self, spoolOptions):
weaponVolley = DmgTypes.default()

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,6 +89,24 @@ class ChangeAffectingSkills(ContextMenuSingle):
self.skillIds = {}
sub = wx.Menu()
# Add "All" entry
allItem = wx.MenuItem(sub, ContextMenuSingle.nextID(), _t("All"))
grandSubAll = wx.Menu()
allItem.SetSubMenu(grandSubAll)
# For "All", only show levels 1-5 (not "Not Learned")
for i in range(1, 6):
id = ContextMenuSingle.nextID()
self.skillIds[id] = (None, i) # None indicates "All" was selected
label = _t("Level %s") % i
menuItem = wx.MenuItem(rootMenu if msw else grandSubAll, id, label, kind=wx.ITEM_RADIO)
grandSubAll.Bind(wx.EVT_MENU, self.handleSkillChange, menuItem)
grandSubAll.Append(menuItem)
sub.Append(allItem)
# Add separator
sub.AppendSeparator()
for skill in self.skills:
skillItem = wx.MenuItem(sub, ContextMenuSingle.nextID(), skill.item.name)
grandSub = wx.Menu()
@@ -110,7 +128,12 @@ class ChangeAffectingSkills(ContextMenuSingle):
def handleSkillChange(self, event):
skill, level = self.skillIds[event.Id]
self.sChar.changeLevel(self.charID, skill.item.ID, level)
if skill is None: # "All" was selected
for s in self.skills:
self.sChar.changeLevel(self.charID, s.item.ID, level)
else:
self.sChar.changeLevel(self.charID, skill.item.ID, level)
fitID = self.mainFrame.getActiveFit()
self.sFit.changeChar(fitID, self.charID)

View File

@@ -0,0 +1,220 @@
# noinspection PyPackageRequirements
import wx
import itertools
import gui.mainFrame
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 eos.saveddata.ship import Ship
from gui.utils.clipboard import toClipboard
from gui.utils.numberFormatter import formatAmount
from service.fit import Fit
_t = wx.GetTranslation
class ItemSkills(wx.Panel):
def __init__(self, parent, stuff, item):
wx.Panel.__init__(self, parent)
self.stuff = stuff
self.item = item
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
leftPanel = wx.Panel(self)
leftSizer = wx.BoxSizer(wx.VERTICAL)
leftPanel.SetSizer(leftSizer)
header = wx.StaticText(leftPanel, wx.ID_ANY, _t("Components"))
font = header.GetFont()
font.SetWeight(wx.FONTWEIGHT_BOLD)
header.SetFont(font)
leftSizer.Add(header, 0, wx.ALL, 5)
self.checkboxes = {}
components = [
("Ship", "ship"),
("Modules", "modules"),
("Drones", "drones"),
("Fighters", "fighters"),
("Cargo", "cargo"),
("Implants", "appliedImplants"),
("Boosters", "boosters"),
("Necessary", "necessary"),
]
for label, key in components:
cb = wx.CheckBox(leftPanel, wx.ID_ANY, label)
cb.SetValue(True)
cb.Bind(wx.EVT_CHECKBOX, self.onCheckboxChange)
self.checkboxes[key] = cb
leftSizer.Add(cb, 0, wx.ALL, 2)
leftSizer.AddStretchSpacer()
mainSizer.Add(leftPanel, 0, wx.EXPAND | wx.ALL, 5)
rightPanel = wx.Panel(self)
rightSizer = wx.BoxSizer(wx.VERTICAL)
rightPanel.SetSizer(rightSizer)
headerRight = wx.StaticText(rightPanel, wx.ID_ANY, _t("Skills"))
fontRight = headerRight.GetFont()
fontRight.SetWeight(wx.FONTWEIGHT_BOLD)
headerRight.SetFont(fontRight)
rightSizer.Add(headerRight, 0, wx.ALL, 5)
self.skillsText = wx.TextCtrl(rightPanel, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP)
font = wx.Font(9, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
self.skillsText.SetFont(font)
rightSizer.Add(self.skillsText, 1, wx.EXPAND | wx.ALL, 5)
mainSizer.Add(rightPanel, 1, wx.EXPAND | wx.ALL, 5)
self.SetSizer(mainSizer)
self.nbContainer = parent if isinstance(parent, wx.Notebook) else None
if self.nbContainer:
self.nbContainer.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onTabChanged)
self.updateSkills()
def onCheckboxChange(self, event):
self.updateSkills()
self._copyToClipboard()
def updateSkills(self):
fitID = gui.mainFrame.MainFrame.getInstance().getActiveFit()
if fitID is None:
self.skillsText.SetValue("")
self._updateCheckboxStates(None)
return
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
self.skillsText.SetValue("")
self._updateCheckboxStates(None)
return
if not fit.calculated:
fit.calculate()
self._updateCheckboxStates(fit)
char = fit.character
skillsMap = {}
items = []
if self.checkboxes["ship"].GetValue():
items.append(fit.ship)
if self.checkboxes["modules"].GetValue():
items.extend(fit.modules)
if self.checkboxes["drones"].GetValue():
items.extend(fit.drones)
if self.checkboxes["fighters"].GetValue():
items.extend(fit.fighters)
if self.checkboxes["cargo"].GetValue():
items.extend(fit.cargo)
if self.checkboxes["appliedImplants"].GetValue():
items.extend(fit.appliedImplants)
if self.checkboxes["boosters"].GetValue():
items.extend(fit.boosters)
for thing in items:
self._collectAffectingSkills(thing, char, skillsMap)
if self.checkboxes["necessary"].GetValue():
self._collectRequiredSkills(items, char, skillsMap)
skillsList = ""
for skillName in sorted(skillsMap):
charLevel = skillsMap[skillName]
for level in range(1, charLevel + 1):
skillsList += "%s %d\n" % (skillName, level)
self.skillsText.SetValue(skillsList)
self._copyToClipboard()
def _copyToClipboard(self):
skillsText = self.skillsText.GetValue()
if skillsText:
toClipboard(skillsText)
def onTabChanged(self, event):
if self.nbContainer:
pageIndex = self.nbContainer.FindPage(self)
if pageIndex != -1 and event.GetSelection() == pageIndex:
self.updateSkills()
event.Skip()
def _updateCheckboxStates(self, fit):
if fit is None:
for cb in self.checkboxes.values():
cb.Enable(False)
return
self.checkboxes["ship"].Enable(True)
self.checkboxes["modules"].Enable(len(fit.modules) > 0)
self.checkboxes["drones"].Enable(len(fit.drones) > 0)
self.checkboxes["fighters"].Enable(len(fit.fighters) > 0)
self.checkboxes["cargo"].Enable(len(fit.cargo) > 0)
self.checkboxes["appliedImplants"].Enable(len(fit.appliedImplants) > 0)
self.checkboxes["boosters"].Enable(len(fit.boosters) > 0)
self.checkboxes["necessary"].Enable(True)
def _collectAffectingSkills(self, thing, char, skillsMap):
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 _collectRequiredSkills(self, items, char, skillsMap):
"""Collect required skills from items (necessary to use them)"""
for thing in items:
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 hasattr(subThing, "requiredSkills"):
for reqSkill, level in subThing.requiredSkills.items():
skillName = reqSkill.name
charSkill = char.getSkill(reqSkill) if char else None
charLevel = charSkill.level if charSkill else 0
if charLevel > 0:
if skillName not in skillsMap:
skillsMap[skillName] = charLevel
elif skillsMap[skillName] < charLevel:
skillsMap[skillName] = charLevel
else:
if skillName not in skillsMap:
skillsMap[skillName] = level
elif skillsMap[skillName] < level:
skillsMap[skillName] = level

View File

@@ -5,6 +5,7 @@ import gui.builtinMarketBrowser.pfSearchBox as SBox
import gui.globalEvents as GE
from config import slotColourMap, slotColourMapDark
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.contextMenu import ContextMenu
from gui.display import Display
@@ -22,6 +23,7 @@ class ItemView(Display):
DEFAULT_COLS = ["Base Icon",
"Base Name",
"Price",
"attr:power,,,True",
"attr:cpu,,,True"]
@@ -153,19 +155,22 @@ class ItemView(Display):
# skip the event so the other handlers also get called
event.Skip()
if self.marketBrowser.mode != 'charges':
return
activeFitID = self.mainFrame.getActiveFit()
# 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:
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
self.updateItemStore(items)
self.filterItemStore()
# If "Fits" filter is active, re-filter the current view
if self.marketBrowser.getFitsFilter():
self.filterItemStore()
def updateItemStore(self, items):
self.unfilteredStore = items
@@ -197,13 +202,115 @@ class ItemView(Display):
if btn.userSelected:
selectedMetas.update(sMkt.META_MAP[btn.metaName])
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
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):
metaIDs = set()
slotIDs = set()
sMkt = self.sMkt
for item in self.unfilteredStore:
metaIDs.add(sMkt.getMetaGroupIdByItem(item))
slot = Module.calculateSlot(item)
if slot is not None:
slotIDs.add(slot)
for btn in self.marketBrowser.metaButtons:
btn.reset()
@@ -212,6 +319,23 @@ class ItemView(Display):
btn.setMetaAvailable(True)
else:
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):
self.searchTimer.Stop() # Cancel any pending timers

View File

@@ -130,9 +130,9 @@ class MiningYieldViewFull(StatsView):
def refreshPanel(self, fit):
# 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),
("labelFullminingyieldDrone", lambda: fit.droneYield, lambda: fit.droneWaste, 3, 0, 0, "{}{} m\u00B3/s", None),
("labelFullminingyieldTotal", lambda: fit.totalYield, lambda: fit.totalWaste, 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.droneDrain, 3, 0, 0, "{} m\u00B3/s", None),
("labelFullminingyieldTotal", lambda: fit.totalYield, lambda: fit.totalDrain, 3, 0, 0, "{} m\u00B3/s", None))
def processValue(value):
value = value() if fit is not None else 0
@@ -140,23 +140,26 @@ class MiningYieldViewFull(StatsView):
return value
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)
yieldValue = processValue(yieldValue)
wasteValue = processValue(wasteValue)
if self._cachedValues[counter] != (yieldValue, wasteValue):
drainValue = processValue(drainValue)
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)
yph = formatAmount(yieldValue * 3600, prec, lowest, highest)
wps = formatAmount(wasteValue, prec, lowest, highest)
wph = formatAmount(wasteValue * 3600, prec, lowest, highest)
wasteSuffix = '\u02b7' if wasteValue > 0 else ''
label.SetLabel(valueFormat.format(yps, wasteSuffix))
dps = formatAmount(drainValue, prec, lowest, highest)
dph = formatAmount(drainValue * 3600, prec, lowest, highest)
label.SetLabel(valueFormat.format(yps))
tipLines = []
tipLines.append("{} m\u00B3 mining yield per second ({} m\u00B3 per hour)".format(yps, yph))
if wasteValue > 0:
tipLines.append("{} m\u00B3 mining waste per second ({} m\u00B3 per hour)".format(wps, wph))
tipLines.append("{} m\u00B3 yield per second ({} m\u00B3 per hour)".format(yps, yph))
tipLines.append("{} m\u00B3 drain per second ({} m\u00B3 per hour)".format(dps, dph))
tipLines.append(f'{efficiency} efficiency')
label.SetToolTip(wx.ToolTip('\n'.join(tipLines)))
self._cachedValues[counter] = (yieldValue, wasteValue)
self._cachedValues[counter] = (yieldValue, drainValue)
counter += 1
self.panel.Layout()
self.headerPanel.Layout()

View File

@@ -130,6 +130,7 @@ class TargetingMiscViewMinimal(StatsView):
("specialPlanetaryCommoditiesHoldCapacity", _t("Planetary goods hold")),
("specialQuafeHoldCapacity", _t("Quafe hold")),
("specialMobileDepotHoldCapacity", _t("Mobile depot hold")),
("specialExpeditionHoldCapacity", _t("Expedition hold")),
))
cargoValues = {
@@ -154,6 +155,7 @@ class TargetingMiscViewMinimal(StatsView):
"specialPlanetaryCommoditiesHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialPlanetaryCommoditiesHoldCapacity"),
"specialQuafeHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialQuafeHoldCapacity"),
"specialMobileDepotHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialMobileDepotHoldCapacity"),
"specialExpeditionHoldCapacity": lambda: fit.ship.getModifiedItemAttr("specialExpeditionHoldCapacity"),
}
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.utils.numberFormatter import formatAmount
from gui.utils.listFormatter import formatList
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import SpoolType, SpoolOptions
import eos.config
@@ -195,7 +196,7 @@ class Miscellanea(ViewColumn):
tooltip = "Warp core strength modification"
return text, tooltip
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)
):
speedFactor = stuff.getModifiedItemAttr("speedFactor")
@@ -547,18 +548,24 @@ class Miscellanea(ViewColumn):
if not yps:
return "", None
yph = yps * 3600
wps = stuff.getMiningWPS(ignoreState=True)
wph = wps * 3600
dps = stuff.getMiningDPS(ignoreState=True)
dph = dps * 3600
try:
efficiency = yps / dps
except ZeroDivisionError:
efficiency = 0
textParts = []
textParts.append(formatAmount(yps, 3, 0, 3))
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(
formatAmount(yps, 3, 0, 3), formatAmount(yph, 3, 0, 3)))
if wps > 0:
textParts.append(formatAmount(wps, 3, 0, 3))
tipLines.append("{} m\u00B3 mining waste per second ({} m\u00B3 per hour)".format(
formatAmount(wps, 3, 0, 3), formatAmount(wph, 3, 0, 3)))
text = '{} m\u00B3/s'.format('+'.join(textParts))
tipLines.append("{} m\u00B3 mining drain per second ({} m\u00B3 per hour)".format(
formatAmount(dps, 3, 0, 3), formatAmount(dph, 3, 0, 3)))
if floatUnerr(efficiency) != 1:
eff_text = '{}%'.format(formatAmount(efficiency * 100, 4, 0, 0))
textParts.append(eff_text)
tipLines.append(f"{eff_text} mining efficiency")
text = '{}'.format(' | '.join(textParts))
tooltip = '\n'.join(tipLines)
return text, tooltip
elif itemGroup == "Logistic Drone":
@@ -701,7 +708,7 @@ class Miscellanea(ViewColumn):
formatAmount(itemArmorResistanceShiftHardenerExp, 3, 0, 3),
)
return text, tooltip
elif itemGroup in ("Cargo Scanner", "Ship Scanner", "Survey Scanner"):
elif itemGroup in ("Cargo Scanner", "Ship Scanner"):
duration = stuff.getModifiedItemAttr("duration")
if not duration:
return "", None
@@ -766,15 +773,36 @@ class Miscellanea(ViewColumn):
elif buffId == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
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)}%")
tooltipSections.append("mining/survey module range")
tooltipSections.append("mining module range")
elif buffId == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
tooltipSections.append("mining module duration & capacitor use")
elif buffId == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility
textSections.append(f"{formatAmount(buffValue, 3, 0, 3, forceSign=True)}%")
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:
return '', None
text = ' | '.join(textSections)

View File

@@ -20,6 +20,7 @@
# noinspection PyPackageRequirements
import wx
from eos.gamedata import Item
from eos.saveddata.cargo import Cargo
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
@@ -53,7 +54,14 @@ class Price(ViewColumn):
self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "gui")
def getText(self, stuff):
if stuff.item is None or stuff.item.group.name == "Ship Modifiers":
if isinstance(stuff, Item):
item = stuff
else:
if not hasattr(stuff, "item") or stuff.item is None:
return ""
item = stuff.item
if item.group.name == "Ship Modifiers":
return ""
if hasattr(stuff, "isEmpty"):
@@ -63,7 +71,7 @@ class Price(ViewColumn):
if isinstance(stuff, Module) and stuff.isMutated:
return ""
priceObj = stuff.item.price
priceObj = item.price
if not priceObj.isValid():
return False
@@ -79,7 +87,11 @@ class Price(ViewColumn):
display.SetItem(colItem)
sPrice.getPrices([mod.item], callback, waitforthread=True)
if isinstance(mod, Item):
item = mod
else:
item = mod.item
sPrice.getPrices([item], callback, waitforthread=True)
def getImageId(self, mod):
return -1

View File

@@ -29,7 +29,7 @@ import config
import gui.globalEvents as GE
import gui.mainFrame
from gui.bitmap_loader import BitmapLoader
from gui.utils.clipboard import toClipboard
from gui.utils.clipboard import toClipboard, fromClipboard
from service.character import Character
from service.fit import Fit
@@ -49,6 +49,10 @@ class CharacterSelection(wx.Panel):
# cache current selection to fall back in case we choose to open char editor
self.charCache = None
# history for Shift-Tab navigation
self.charHistory = []
self._updatingFromHistory = False
self.charChoice = wx.Choice(self)
mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3)
@@ -92,7 +96,7 @@ class CharacterSelection(wx.Panel):
sFit = Fit.getInstance()
fit = sFit.getFit(self.mainFrame.getActiveFit())
if not fit or not self.needsSkills:
if not fit:
return
pos = wx.GetMousePosition()
@@ -100,14 +104,23 @@ class CharacterSelection(wx.Panel):
menu = wx.Menu()
grantItem = menu.Append(wx.ID_ANY, _t("Grant Missing Skills"))
self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem)
if self.needsSkills:
grantItem = menu.Append(wx.ID_ANY, _t("Grant Missing Skills"))
self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)"))
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, 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)"))
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem)
menu.AppendSeparator()
importItem = menu.Append(wx.ID_ANY, _t("Import Skills from Clipboard"))
self.Bind(wx.EVT_MENU, self.importSkillsFromClipboard, importItem)
self.PopupMenu(menu, pos)
@@ -186,6 +199,13 @@ class CharacterSelection(wx.Panel):
sFit = Fit.getInstance()
sFit.changeChar(fitID, charID)
self.charCache = self.charChoice.GetCurrentSelection()
if not self._updatingFromHistory and charID is not None:
currentChar = self.getActiveCharacter()
if currentChar is not None:
if not self.charHistory or self.charHistory[-1] != currentChar:
self.charHistory.append(currentChar)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
def toggleRefreshButton(self):
@@ -207,6 +227,29 @@ class CharacterSelection(wx.Panel):
return True
return False
def selectPreviousChar(self):
currentChar = self.getActiveCharacter()
if currentChar is None:
return
if not self.charHistory:
return
if self.charHistory and self.charHistory[-1] == currentChar:
self.charHistory.pop()
if not self.charHistory:
return
prevChar = self.charHistory.pop()
if currentChar != prevChar:
self.charHistory.append(currentChar)
self._updatingFromHistory = True
if self.selectChar(prevChar):
self.charChanged(None)
self._updatingFromHistory = False
def fitChanged(self, event):
"""
@@ -219,7 +262,7 @@ class CharacterSelection(wx.Panel):
self.charChoice.Enable(activeFitID is not None)
choice = self.charChoice
sFit = Fit.getInstance()
currCharID = choice.GetClientData(choice.GetCurrentSelection())
currCharID = choice.GetClientData(choice.GetCurrentSelection()) if choice.GetCurrentSelection() != -1 else None
fit = sFit.getFit(activeFitID)
newCharID = fit.character.ID if fit is not None else None
@@ -253,6 +296,9 @@ class CharacterSelection(wx.Panel):
self.selectChar(sChar.all5ID())
elif currCharID != newCharID:
if currCharID is not None and not self._updatingFromHistory:
if not self.charHistory or self.charHistory[-1] != currCharID:
self.charHistory.append(currCharID)
self.selectChar(newCharID)
if not fit.calculated:
self.charChanged(None)
@@ -268,6 +314,15 @@ class CharacterSelection(wx.Panel):
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):
skillsMap = self._buildSkillsTooltipCondensed(self.reqs, skillsMap={})
@@ -277,6 +332,83 @@ class CharacterSelection(wx.Panel):
toClipboard(list)
def importSkillsFromClipboard(self, evt):
charID = self.getActiveCharacter()
if charID is None:
return
sChar = Character.getInstance()
char = sChar.getCharacter(charID)
text = fromClipboard()
if not text:
with wx.MessageDialog(self, _t("Clipboard is empty"), _t("Error"), wx.OK | wx.ICON_ERROR) as dlg:
dlg.ShowModal()
return
try:
lines = text.strip().splitlines()
imported = 0
errors = []
for line in lines:
line = line.strip()
if not line:
continue
try:
parts = line.rsplit(None, 1)
if len(parts) != 2:
errors.append(_t("Invalid format: {}").format(line))
continue
skillName = parts[0]
levelStr = parts[1]
try:
level = int(levelStr)
except ValueError:
try:
level = roman.fromRoman(levelStr.upper())
except (roman.InvalidRomanNumeralError, ValueError):
errors.append(_t("Invalid level format: {}").format(line))
continue
if level < 0 or level > 5:
errors.append(_t("Level must be between 0 and 5: {}").format(line))
continue
skill = char.getSkill(skillName)
sChar.changeLevel(charID, skill.item.ID, level)
imported += 1
except KeyError as e:
errors.append(_t("Skill not found: {}").format(skillName))
pyfalog.error("Skill not found: '{}'", skillName)
except Exception as e:
errors.append(_t("Error processing line '{}': {}").format(line, str(e)))
pyfalog.error("Error importing skill from line '{}': {}", line, e)
if imported > 0:
self.refreshCharacterList()
wx.PostEvent(self.mainFrame, GE.CharListUpdated())
fitID = self.mainFrame.getActiveFit()
if fitID is not None:
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
if errors:
errorMsg = _t("Imported {} skill(s). Errors:\n{}").format(imported, "\n".join(errors))
with wx.MessageDialog(self, errorMsg, _t("Import Skills"), wx.OK | wx.ICON_WARNING) as dlg:
dlg.ShowModal()
elif imported > 0:
with wx.MessageDialog(self, _t("Successfully imported {} skill(s)").format(imported), _t("Import Skills"), wx.OK) as dlg:
dlg.ShowModal()
except Exception as e:
pyfalog.error("Error importing skills from clipboard: {}", e)
with wx.MessageDialog(self, _t("Error importing skills. Please check the log file."), _t("Error"), wx.OK | wx.ICON_ERROR) as dlg:
dlg.ShowModal()
def _buildSkillsTooltip(self, reqs, currItem="", tabulationLevel=0):
tip = ""
sCharacter = Character.getInstance()

View File

@@ -24,6 +24,7 @@ import config
import gui.mainFrame
from eos.saveddata.drone import Drone
from eos.saveddata.module import Module
from eos.saveddata.ship import Ship
from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy
@@ -35,6 +36,7 @@ from gui.builtinItemStatsViews.itemEffects import ItemEffects
from gui.builtinItemStatsViews.itemMutator import ItemMutatorPanel
from gui.builtinItemStatsViews.itemProperties import ItemProperties
from gui.builtinItemStatsViews.itemRequirements import ItemRequirements
from gui.builtinItemStatsViews.itemSkills import ItemSkills
from gui.builtinItemStatsViews.itemTraits import ItemTraits
from service.market import Market
@@ -156,6 +158,8 @@ class ItemStatsContainer(wx.Panel):
def __init__(self, parent, stuff, item, context=None):
wx.Panel.__init__(self, parent)
sMkt = Market.getInstance()
self.stuff = stuff
self.context = context
mainSizer = wx.BoxSizer(wx.VERTICAL)
@@ -196,6 +200,10 @@ class ItemStatsContainer(wx.Panel):
self.affectedby = ItemAffectedBy(self.nbContainer, stuff, item)
self.nbContainer.AddPage(self.affectedby, _t("Affected by"))
if stuff is not None and isinstance(stuff, Ship):
self.skills = ItemSkills(self.nbContainer, stuff, item)
self.nbContainer.AddPage(self.skills, _t("Skills"))
if config.debug:
self.properties = ItemProperties(self.nbContainer, stuff, item, context)
self.nbContainer.AddPage(self.properties, _t("Properties"))

View File

@@ -18,6 +18,7 @@
# =============================================================================
import datetime
import itertools
import os.path
import threading
import time
@@ -60,8 +61,12 @@ from gui.shipBrowser import ShipBrowser
from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditor
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 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.esi import Esi
from service.fit import Fit
@@ -522,6 +527,8 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.backupToXml, id=menuBar.backupFitsId)
# Export skills needed
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
self.Bind(wx.EVT_MENU, self.importCharacter, id=menuBar.importCharacterId)
# Export HTML
@@ -571,6 +578,7 @@ class MainFrame(wx.Frame):
toggleShipMarketId = wx.NewId()
ctabnext = wx.NewId()
ctabprev = wx.NewId()
charPrevId = wx.NewId()
# Close Page
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
@@ -580,6 +588,7 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.toggleShipMarket, id=toggleShipMarketId)
self.Bind(wx.EVT_MENU, self.CTabNext, id=ctabnext)
self.Bind(wx.EVT_MENU, self.CTabPrev, id=ctabprev)
self.Bind(wx.EVT_MENU, self.selectPreviousCharacter, id=charPrevId)
actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId),
(wx.ACCEL_CMD, ord('T'), self.addPageId),
@@ -613,7 +622,10 @@ class MainFrame(wx.Frame):
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO)
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO),
# Shift+Tab for previous character
(wx.ACCEL_SHIFT, wx.WXK_TAB, charPrevId)
]
# Ctrl/Cmd+# for addition pane selection
@@ -739,6 +751,9 @@ class MainFrame(wx.Frame):
def CTabPrev(self, event):
self.fitMultiSwitch.PrevPage()
def selectPreviousCharacter(self, event):
self.charSelection.selectPreviousChar()
def HAddPage(self, event):
self.fitMultiSwitch.AddPage()
@@ -832,6 +847,61 @@ class MainFrame(wx.Frame):
self.waitDialog = wx.BusyInfo(_t("Exporting skills needed..."), parent=self)
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):
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, 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):
"""Handles importing single/multiple EVE XML / EFT cfg fit files"""
with wx.FileDialog(

View File

@@ -42,6 +42,7 @@ class MainMenuBar(wx.MenuBar):
self.graphFrameId = wx.NewId()
self.backupFitsId = wx.NewId()
self.exportSkillsNeededId = wx.NewId()
self.copySkillsNeededId = wx.NewId()
self.importCharacterId = wx.NewId()
self.exportHtmlId = wx.NewId()
self.wikiId = wx.NewId()
@@ -117,6 +118,7 @@ class MainMenuBar(wx.MenuBar):
characterMenu.AppendSeparator()
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.copySkillsNeededId, _t("&Copy Skills Needed"), _t("Copy skills needed for this fitting to clipboard"))
characterMenu.AppendSeparator()
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_COPY, enable)
self.Enable(self.exportSkillsNeededId, enable)
self.Enable(self.copySkillsNeededId, enable)
self.refreshUndo()

View File

@@ -54,6 +54,7 @@ class MarketBrowser(wx.Panel):
self.settings = MarketPriceSettings.getInstance()
self.__mode = 'normal'
self.__normalBtnMap = {}
self.__normalSlotBtnMap = {}
self.marketView = MarketTree(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,
# need some pixels of extra space or everything clips and is ugly
p = wx.Panel(self)
box = wx.BoxSizer(wx.HORIZONTAL)
p.SetSizer(box)
vbox_panel = wx.BoxSizer(wx.VERTICAL)
p.SetSizer(vbox_panel)
vbox.Add(p, 0, wx.EXPAND)
# First row: meta buttons
metaBox = wx.BoxSizer(wx.HORIZONTAL)
vbox_panel.Add(metaBox, 0, wx.EXPAND)
self.metaButtons = []
btn = None
for name in list(self.sMkt.META_MAP.keys()):
btn = MetaButton(p, wx.ID_ANY, name.capitalize(), style=wx.BU_EXACTFIT)
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.metaName = name
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
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):
"""Process clicks on toggle buttons"""
@@ -100,6 +140,21 @@ class MarketBrowser(wx.Panel):
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):
self.mode = 'normal'
self.marketView.jump(item)
@@ -141,6 +196,9 @@ class MarketBrowser(wx.Panel):
self.__normalBtnMap.clear()
for btn in self.metaButtons:
self.__normalBtnMap[btn] = btn.userSelected
self.__normalSlotBtnMap.clear()
for btn in self.slotButtons:
self.__normalSlotBtnMap[btn] = btn.userSelected
if newMode == 'search':
self.marketView.UnselectAll()
setting = self.settings.get('marketMGSearchMode')
@@ -149,12 +207,16 @@ class MarketBrowser(wx.Panel):
if newMode in ('search', 'recent', 'charges'):
for btn in self.metaButtons:
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':
for btn, state in self.__normalBtnMap.items():
btn.setUserSelection(state)
for btn, state in self.__normalSlotBtnMap.items():
btn.setUserSelection(state)
# We turn on all meta buttons permanently
if setting == 2:
for btn in self.metaButtons:
btn.setUserSelection(True)
self.__mode = newMode

View File

@@ -1,22 +1,68 @@
# noinspection PyPackageRequirements
import wx
from logbook import Logger
logger = Logger(__name__)
def toClipboard(text):
clip = wx.TheClipboard
clip.Open()
data = wx.TextDataObject(text)
clip.SetData(data)
clip.Close()
"""
Copy text to clipboard. Explicitly uses CLIPBOARD selection, not PRIMARY.
On X11 systems, wxPython can confuse between PRIMARY and CLIPBOARD selections,
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():
clip = wx.TheClipboard
clip.Open()
data = wx.TextDataObject("")
if clip.GetData(data):
clip.Close()
return data.GetText()
else:
clip.Close()
"""
Read text from clipboard. Explicitly uses CLIPBOARD selection, not PRIMARY.
On X11 systems, wxPython can confuse between PRIMARY and CLIPBOARD selections,
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()
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

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/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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

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