sellSkinDialogue.py (21060B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\cosmetics\ship\pages\collection\sellSkinDialogue.py 2 import eveicon 3 import math 4 import uthread2 5 from carbonui import Align, AxisAlignment, ButtonStyle, ButtonVariant, Density, uiconst 6 from carbonui import TextColor 7 from carbonui.text.styles import TextHeader, TextBody, TextDetail 8 from carbonui.button.group import ButtonGroup, ButtonSizeMode 9 from carbonui.control.button import Button 10 from carbonui.control.combo import Combo 11 from carbonui.control.comboEntryData import ComboEntryData 12 from carbonui.control.singlelineedits.singleLineEditInteger import SingleLineEditInteger 13 from carbonui.control.window import Window 14 from carbonui.decorative.divider_line import DividerLine 15 from carbonui.primitives.container import Container 16 from carbonui.primitives.containerAutoSize import ContainerAutoSize 17 from carbonui.primitives.sprite import Sprite 18 from cosmetics.client.ships.skins.errors import get_listing_error_text 19 from cosmetics.client.shipSkinTradingSvc import get_ship_skin_trading_svc 20 from cosmetics.common.ships.skins.util import Currency 21 from eve.client.script.ui import eveColor 22 from player_messaging.client.ui_message import message_player 23 from eve.client.script.ui.cosmetics.ship import tradingUtil 24 from eve.client.script.ui.cosmetics.ship.controls.buyerFee import SellSKINBuyerFee 25 from eve.client.script.ui.cosmetics.ship.tradingUtil import SellAvailability 26 from eve.client.script.ui.cosmetics.ship.pages.store.tooltipInfoIcons import HubTaxRateSkillsInfoIcon 27 from eve.client.script.ui.shared.market.singleLineEditMarketPrice import SingleLineEditMarketPrice 28 from eve.common.script.util.eveFormat import FmtAmt, FmtISK 29 from eveui.autocomplete.player.field import CharOrCorpOrAllianceField 30 from localization import GetByLabel 31 32 class SellSkinDialogue(Window): 33 __notifyevents__ = ['OnSessionChanged'] 34 default_windowID = 'SellSkinDialogue' 35 default_iconNum = 'res:/UI/Texture/WindowIcons/paint_tool.png' 36 default_caption = GetByLabel('UI/Personalization/ShipSkins/SKINR/SellSkin') 37 default_fixedWidth = 515 38 default_fixedHeight = 480 39 default_isMinimizable = False 40 default_isStackable = False 41 default_isCollapseable = False 42 43 def __init__(self, skin_license, *args, **kwargs): 44 super(SellSkinDialogue, self).__init__(*args, **kwargs) 45 self.skin_license = skin_license 46 self.is_layout_done = False 47 self.update_thread = None 48 self.sell_skin_thread = None 49 self._tax_rate = None 50 self._broker_fee = None 51 self._buyer_fee_amount = None 52 self.construct_layout() 53 sm.RegisterNotify(self) 54 55 def Close(self, setClosed = False, *args, **kwargs): 56 try: 57 self.kill_threads() 58 sm.UnregisterNotify(self) 59 finally: 60 super(SellSkinDialogue, self).Close(setClosed, *args, **kwargs) 61 62 def kill_threads(self): 63 self.kill_update_thread() 64 self.kill_sell_skin_thread() 65 66 def kill_update_thread(self): 67 if self.update_thread is not None: 68 self.update_thread.kill() 69 self.update_thread = None 70 71 def kill_sell_skin_thread(self): 72 if self.sell_skin_thread is not None: 73 self.sell_skin_thread.kill() 74 self.sell_skin_thread = None 75 76 def construct_layout(self): 77 self.content.padding = (32, 32, 32, 32) 78 self.construct_duration() 79 self.construct_availability() 80 self.construct_search() 81 self.construct_listing() 82 DividerLine(parent=self.content, align=Align.TOTOP, padTop=32) 83 self.construct_buyer_fee() 84 self.construct_service_fee() 85 self.construct_listing_cost() 86 self.construct_profit() 87 DividerLine(parent=self.content, align=Align.TOTOP, padTop=16) 88 self.construct_buttons() 89 self.is_layout_done = True 90 self.update_async() 91 92 def construct_duration(self): 93 duration_container = Container(name='duration_container', parent=self.content, align=Align.TOTOP, height=32) 94 TextHeader(name='duration_label', parent=duration_container, align=Align.CENTERLEFT, text=GetByLabel('UI/Personalization/ShipSkins/SKINR/Collection/SellListingDuration'), bold=True) 95 options = tradingUtil.get_sell_duration_options() 96 self.duration_combo = Combo(name='duration_combo', parent=duration_container, align=Align.TORIGHT, options=options, select=options[-1], callback=self.on_input_change, maxVisibleEntries=len(options), width=160) 97 98 def construct_listing(self): 99 self.listing_container = Container(name='listing_container', parent=self.content, align=Align.TOTOP, height=32, padTop=16) 100 self.construct_copies() 101 self.construct_amount() 102 self.construct_currency() 103 104 def construct_availability(self): 105 availability_container = Container(name='availability_container', parent=self.content, align=Align.TOTOP, height=32, padTop=16) 106 TextHeader(name='availability_label', parent=availability_container, align=Align.CENTERLEFT, text=GetByLabel('UI/Personalization/ShipSkins/SKINR/ListingAvailability'), bold=True) 107 options = tradingUtil.get_sell_availability_options() 108 self.availability_combo = Combo(name='availability_combo', parent=availability_container, align=Align.TORIGHT, options=options, select=options[-1], callback=self.on_availability_change, maxVisibleEntries=len(options), width=160) 109 110 def construct_search(self): 111 self.search_container = Container(name='search_container', parent=self.content, align=Align.TOTOP, height=52, padTop=8, display=False) 112 header_container = Container(name='header_container', parent=self.search_container, align=Align.TOTOP, height=16) 113 Sprite(name='icon', parent=header_container, align=Align.TOLEFT, texturePath=eveicon.pilot_or_organization, color=TextColor.SECONDARY, pos=(0, 0, 16, 16)) 114 TextDetail(name='label', parent=header_container, align=Align.TOLEFT, text=GetByLabel('UI/Personalization/ShipSkins/SKINR/Collection/SellTargetSearchHeader'), color=TextColor.SECONDARY, padLeft=4) 115 self.search_field = CharOrCorpOrAllianceField(name='search_field', parent=self.search_container, align=Align.TOTOP, placeholder=GetByLabel('UI/Personalization/ShipSkins/SKINR/Collection/SellTargetSearchPlaceholder'), show_suggestions_on_focus=True, padTop=4) 116 self.search_field.autocomplete_controller.on_fetch_start.connect(self.on_search_fetch_start) 117 self.search_field.autocomplete_controller.on_completed.connect(self.on_search_completed) 118 119 def on_search_fetch_start(self, *args): 120 self.update_async() 121 122 def on_search_completed(self, *args): 123 self.update_async() 124 125 def construct_copies(self): 126 copies_container = ContainerAutoSize(name='copies_container', parent=self.listing_container, align=Align.TOLEFT, height=32) 127 TextHeader(name='copies_label', parent=copies_container, align=Align.TOLEFT, text=GetByLabel('UI/Industry/Copies'), bold=True, padTop=4) 128 self.copies_edit = SingleLineEditInteger(name='copies_edit', parent=copies_container, align=Align.TOLEFT, density=Density.EXPANDED, width=56, minValue=1, maxValue=self.skin_license.nb_unactivated, setvalue=1, OnChange=self.on_input_change, padLeft=10) 129 130 def construct_amount(self): 131 self.amount_container = ContainerAutoSize(name='amount_container', parent=self.listing_container, align=Align.TORIGHT, height=32, padLeft=10) 132 self.amount_edit = SingleLineEditMarketPrice(name='amount_edit', parent=self.amount_container, align=Align.TOLEFT, density=Density.EXPANDED, minValue=1, setvalue=10, decimalPlaces=0, OnChange=self.on_input_change, width=160, isTickActivated=False) 133 134 def construct_currency(self): 135 currency_container = ContainerAutoSize(name='currency_container', parent=self.listing_container, align=Align.TORIGHT, height=32) 136 TextHeader(name='currency_label', parent=currency_container, align=Align.TOLEFT, text=GetByLabel('UI/Market/Marketbase/Price'), bold=True, padTop=4) 137 options = [ComboEntryData(label=GetByLabel('UI/Common/ISK'), returnValue=Currency.ISK, icon=eveicon.isk), ComboEntryData(label=GetByLabel('UI/Common/PLEX'), returnValue=Currency.PLEX, icon=eveicon.plex)] 138 self.currency_combo = Combo(name='currency_combo', parent=currency_container, align=Align.TOLEFT, options=options, select=Currency.PLEX, callback=self.on_currency_change, maxVisibleEntries=len(options), width=56, padLeft=10) 139 140 def construct_buyer_fee(self): 141 self.buyer_fee = SellSKINBuyerFee(name='buyer_fee', parent=self.content, align=Align.TOTOP, padTop=24, display=False) 142 143 def construct_service_fee(self): 144 self.service_fee_container = Container(name='service_fee_container', parent=self.content, align=Align.TOTOP, height=18, padTop=20) 145 label_container = ContainerAutoSize(name='label_container', parent=self.service_fee_container, align=Align.TOLEFT, state=uiconst.UI_NORMAL, hint=GetByLabel('UI/Personalization/ShipSkins/SKINR/ServiceFeeHint')) 146 TextBody(name='service_fee_label', parent=label_container, align=Align.TOLEFT, text=GetByLabel('UI/Personalization/ShipSkins/SKINR/ServiceFee'), color=TextColor.SECONDARY) 147 HubTaxRateSkillsInfoIcon(parent=ContainerAutoSize(parent=label_container, align=Align.TOLEFT, padLeft=4), align=Align.CENTERLEFT) 148 amount_container = ContainerAutoSize(name='amount_container', parent=self.service_fee_container, align=Align.TORIGHT, state=uiconst.UI_NORMAL, hint=GetByLabel('UI/Personalization/ShipSkins/SKINR/ServiceFeeHint')) 149 self.service_fee_amount_label = TextBody(name='service_fee_amount_label', parent=amount_container, align=Align.TOLEFT, color=TextColor.SECONDARY) 150 151 def construct_listing_cost(self): 152 listing_cost_container = Container(name='listing_cost_container', parent=self.content, align=Align.TOTOP, height=18, padTop=8) 153 label_container = ContainerAutoSize(name='label_container', parent=listing_cost_container, align=Align.TOLEFT, state=uiconst.UI_NORMAL, hint=GetByLabel('UI/Personalization/ShipSkins/SKINR/ListingCostHint')) 154 TextBody(name='listing_cost_label', parent=label_container, align=Align.TOLEFT, text=GetByLabel('UI/Personalization/ShipSkins/SKINR/ListingCost'), color=TextColor.SECONDARY) 155 amount_container = ContainerAutoSize(name='amount_container', parent=listing_cost_container, align=Align.TORIGHT, state=uiconst.UI_NORMAL, hint=GetByLabel('UI/Personalization/ShipSkins/SKINR/ListingCostHint')) 156 self.listing_cost_amount_label = TextBody(name='listing_cost_amount_label', parent=amount_container, align=Align.TOLEFT, color=TextColor.SECONDARY, padLeft=4) 157 158 def construct_profit(self): 159 profit_container = Container(name='profit_container', parent=self.content, align=Align.TOTOP, height=28, padTop=16) 160 TextHeader(name='profit_label', parent=profit_container, align=Align.TOLEFT, text=GetByLabel('UI/Personalization/ShipSkins/SKINR/TotalProfit'), bold=True) 161 self.profit_amount_label = TextHeader(name='profit_amount_label', parent=profit_container, align=Align.TORIGHT, bold=True, padLeft=4) 162 currency_icon_container = Container(name='currency_icon_container', parent=profit_container, align=Align.TORIGHT, width=20) 163 self.currency_icon = Sprite(name='currency_icon', parent=currency_icon_container, align=Align.CENTER, pos=(0, 0, 20, 20), padBottom=4) 164 165 def construct_buttons(self): 166 button_group = ButtonGroup(name='button_group', parent=self.content, align=Align.TOBOTTOM, button_alignment=AxisAlignment.CENTER, density=Density.EXPANDED, button_size_mode=ButtonSizeMode.STRETCH) 167 self.sell_button = Button(name='sell_button', label=GetByLabel('UI/Personalization/ShipSkins/SKINR/Collection/Sell'), variant=ButtonVariant.PRIMARY, func=self.on_sell_button) 168 button_group.add_button(self.sell_button) 169 self.cancel_button = Button(name='cancel_button', label=GetByLabel('UI/Common/Buttons/Cancel'), variant=ButtonVariant.GHOST, func=self.on_cancel_button) 170 button_group.add_button(self.cancel_button) 171 172 def on_input_change(self, *args): 173 self.update_async() 174 175 def on_availability_change(self, combobox, key, value): 176 self.search_container.display = value == SellAvailability.SPECIFIC 177 self.update_async() 178 179 def on_currency_change(self, *args): 180 self.update_amount_edit() 181 self.update_async() 182 183 @uthread2.debounce(wait=0.2) 184 def update_async(self): 185 if not self.is_layout_done: 186 return 187 self.kill_update_thread() 188 self.update_thread = uthread2.start_tasklet(self.update) 189 190 def update(self): 191 self.update_fee_and_tax_values() 192 self.update_buyer_fee() 193 self.update_service_fee() 194 self.update_listing_cost() 195 self.update_profit() 196 self.update_currency() 197 self.update_buttons() 198 self.update_size() 199 200 def update_fee_and_tax_values(self): 201 self._tax_rate = self.get_tax_rate() 202 self._broker_fee = self.get_broker_fee() 203 self._buyer_fee_amount = self.get_buyer_fee_amount() 204 205 def update_buyer_fee(self): 206 self.buyer_fee.amount = self.buyer_fee_amount 207 208 def update_service_fee(self): 209 if self.tax_rate is None: 210 self.service_fee_amount_label.text = None 211 return 212 self.service_fee_amount_label.text = '{tax}%'.format(tax=self.tax_rate) 213 self.service_fee_amount_label.color = eveColor.SUCCESS_GREEN if self.tax_rate == 0 else TextColor.SECONDARY 214 215 def update_listing_cost(self): 216 if self.broker_fee is None: 217 self.listing_cost_amount_label.text = None 218 return 219 self.listing_cost_amount_label.text = FmtISK(-self.broker_fee) 220 self.listing_cost_amount_label.color = eveColor.SUCCESS_GREEN if self.broker_fee == 0 else TextColor.SECONDARY 221 222 def update_profit(self): 223 if self.tax_rate is None or self.broker_fee is None: 224 self.profit_amount_label.text = None 225 return 226 tax_rate_percent = self.tax_rate / 100.0 227 if self.currency == Currency.PLEX: 228 profit = math.floor(self.price - self.price * tax_rate_percent) 229 self.profit_amount_label.text = FmtAmt(profit) 230 else: 231 profit = self.price - self.price * tax_rate_percent - self.broker_fee 232 self.profit_amount_label.text = FmtISK(profit) 233 self.profit_amount_label.color = self.get_profit_text_color(profit) 234 235 def get_profit_text_color(self, profit): 236 if profit > 0: 237 return TextColor.SUCCESS 238 elif profit < 0: 239 return TextColor.DANGER 240 else: 241 return TextColor.NORMAL 242 243 def update_currency(self): 244 if self.currency == Currency.ISK: 245 self.currency_icon.texturePath = eveicon.isk 246 self.currency_icon.color = eveColor.WHITE 247 elif self.currency == Currency.PLEX: 248 self.currency_icon.texturePath = eveicon.plex 249 self.currency_icon.color = eveColor.PLEX_YELLOW 250 251 def update_availabilities(self): 252 select = self.availability_combo.selectedValue 253 options = tradingUtil.get_sell_availability_options() 254 self.availability_combo.LoadOptions(options, select) 255 self.search_container.display = self.availability_combo.selectedValue == SellAvailability.SPECIFIC 256 257 def update_amount_edit(self): 258 value = self.amount 259 if self.currency == Currency.ISK: 260 self.amount_edit.decimalPlaces = 2 261 self.amount_edit.ActivateTickAlignment() 262 elif self.currency == Currency.PLEX: 263 self.amount_edit.decimalPlaces = 0 264 self.amount_edit.DeactivateTickAlignment() 265 self.amount_edit.SetValue(value, docallback=False) 266 267 def update_buttons(self): 268 if self.is_valid: 269 self.sell_button.style = ButtonStyle.NORMAL 270 self.sell_button.hint = None 271 return 272 self.sell_button.style = ButtonStyle.DANGER 273 if not self.has_sufficient_funds: 274 self.sell_button.hint = GetByLabel('UI/Personalization/ShipSkins/SKINR/Studio/ListingErrors/InsufficientISKFunds') 275 elif not self.has_valid_target: 276 self.sell_button.hint = GetByLabel('UI/Personalization/ShipSkins/SKINR/Studio/ListingErrors/InvalidListingTarget') 277 278 def update_size(self): 279 if self.buyer_fee_amount: 280 if self.sell_availability == SellAvailability.SPECIFIC: 281 self.SetFixedHeight(610) 282 else: 283 self.SetFixedHeight(550) 284 elif self.sell_availability == SellAvailability.SPECIFIC: 285 self.SetFixedHeight(540) 286 else: 287 self.SetFixedHeight(self.default_fixedHeight) 288 289 def on_sell_button(self, *args): 290 if self.is_valid: 291 self.sell_skin_async() 292 293 def sell_skin_async(self): 294 self.kill_sell_skin_thread() 295 self.sell_skin_thread = uthread2.StartTasklet(self.sell_skin) 296 297 def sell_skin(self): 298 self.sell_button.busy = True 299 self.sell_button.disabled = True 300 try: 301 svc = get_ship_skin_trading_svc() 302 error = svc.create_listing(currency=self.currency, price=int(self.amount), skin_hex=self.skin_license.skin_hex, duration_in_seconds=int(self.duration), quantity=int(self.copies), target_id=self.sell_target_type_id) 303 if error: 304 message_player(GetByLabel(get_listing_error_text(error))) 305 else: 306 self.SetModalResult(uiconst.ID_OK) 307 except Exception as e: 308 raise RuntimeError('Unsupported error occurred when creating skin listing: %s' % e) 309 finally: 310 self.sell_skin_thread = None 311 self.sell_button.busy = False 312 self.sell_button.disabled = False 313 314 def get_tax_rate(self): 315 tax_rate, error = get_ship_skin_trading_svc().get_tax_rate(target_id=self.sell_target_type_id) 316 if error: 317 self.show_error_message(error) 318 return None 319 else: 320 return tax_rate 321 322 def get_broker_fee(self): 323 broker_fees, error = get_ship_skin_trading_svc().get_broker_fees_per_duration(target_id=self.sell_target_type_id) 324 if error: 325 self.show_error_message(error) 326 return None 327 else: 328 return broker_fees[self.duration] 329 330 def get_buyer_fee_amount(self): 331 target_id = self.sell_target_type_id 332 if target_id is None: 333 return 334 else: 335 buyer_fee, error = get_ship_skin_trading_svc().get_predicted_buyer_fee(skin_hex=self.skin_license.skin_hex, target_id=target_id) 336 if error: 337 self.show_error_message(error) 338 return 339 return buyer_fee.lp_amount 340 341 def on_cancel_button(self, *args): 342 self.SetModalResult(uiconst.ID_CANCEL) 343 344 def OnSessionChanged(self, isRemote, session, change): 345 if 'corprole' in change or 'corpid' in change or 'allianceid' in change: 346 self.Close() 347 348 @uthread2.debounce(wait=0.2) 349 def show_error_message(self, error): 350 message_player(GetByLabel(get_listing_error_text(error))) 351 352 @property 353 def has_sufficient_funds(self): 354 if self.broker_fee is None: 355 return False 356 return sm.GetService('wallet').GetWealth() >= self.broker_fee 357 358 @property 359 def has_valid_target(self): 360 if self.sell_availability == SellAvailability.SPECIFIC and self.sell_target_type_id is None: 361 return False 362 return True 363 364 @property 365 def tax_rate(self): 366 return self._tax_rate 367 368 @property 369 def broker_fee(self): 370 return self._broker_fee 371 372 @property 373 def buyer_fee_amount(self): 374 return self._buyer_fee_amount 375 376 @property 377 def is_valid(self): 378 return self.has_sufficient_funds and self.has_valid_target 379 380 @property 381 def currency(self): 382 return self.currency_combo.GetValue() 383 384 @property 385 def price(self): 386 return self.amount * self.copies 387 388 @property 389 def amount(self): 390 return self.amount_edit.GetValue() 391 392 @property 393 def copies(self): 394 return self.copies_edit.GetValue() 395 396 @property 397 def duration(self): 398 return self.duration_combo.GetValue() 399 400 @property 401 def sell_availability(self): 402 return self.availability_combo.GetValue() 403 404 @property 405 def sell_target(self): 406 return self.search_field.completed_suggestion 407 408 @property 409 def sell_target_type_id(self): 410 availability = self.sell_availability 411 if availability == SellAvailability.MY_CORPORATION: 412 return session.corpid 413 if availability == SellAvailability.MY_ALLIANCE: 414 return session.allianceid 415 if availability == SellAvailability.SPECIFIC: 416 sell_target = self.sell_target 417 if sell_target: 418 return sell_target.owner_id 419 return None