diff --git a/src/lib/components/common/item_holder_metadata.ts b/src/lib/components/common/item_holder_metadata.ts
index d75f79d7..0a572ea1 100644
--- a/src/lib/components/common/item_holder_metadata.ts
+++ b/src/lib/components/common/item_holder_metadata.ts
@@ -13,17 +13,19 @@ import {
isCharm,
isHighlightCharm,
} from '../../utils/skin';
-import {isSkin, floor} from '../../utils/skin';
+import {isSkin} from '../../utils/skin';
import {getRankColour} from '../../utils/ranks';
import {Observe} from '../../utils/observers';
import {FetchBluegem, FetchBluegemResponse} from '../../bridge/handlers/fetch_bluegem';
import {ClientSend} from '../../bridge/client';
+import {renderBluegemPercentage, renderFadePercentage, patternDetailStyles} from './pattern_details';
// Generic annotator of item holder metadata (float, seed, etc...)
// Must be extended to use as a component
export abstract class ItemHolderMetadata extends FloatElement {
static styles = [
...FloatElement.styles,
+ patternDetailStyles,
css`
.float {
position: absolute;
@@ -40,32 +42,10 @@ export abstract class ItemHolderMetadata extends FloatElement {
font-size: 12px;
}
- .fade-base {
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- }
-
- .fade {
- background-image: -webkit-linear-gradient(0deg, #d9bba5 0%, #e5903b 33%, #db5977 66%, #6775e1 100%);
- }
-
- .amber-fade {
- background-image: -webkit-linear-gradient(0deg, #627d66 0%, #896944 50%, #7e4201 100%);
- }
-
- .acid-fade {
- background-image: -webkit-linear-gradient(0deg, #2b441b 0%, #3e6b2f 11%, #82a64a 66%, #c1a16c 100%);
- }
-
.csfloat-shine-fade-text {
font-weight: 1000;
-webkit-text-stroke: 1px black;
}
-
- .bluegem {
- color: deepskyblue;
- }
`,
];
@@ -108,7 +88,7 @@ export abstract class ItemHolderMetadata extends FloatElement {
if (!this.itemInfo || !this.asset) return html``;
if (isSkin(this.asset)) {
- const fadeDetails = this.asset && getFadePercentage(this.asset, this.itemInfo);
+ const fadeDetails = this.asset && getFadePercentage(this.asset.market_hash_name, this.itemInfo);
if (fadeDetails?.percentage === 100) {
$J(this).parent().addClass('full-fade-border');
@@ -121,17 +101,10 @@ export abstract class ItemHolderMetadata extends FloatElement {
${formatFloatWithRank(this.itemInfo, 6)}
${formatSeed(this.itemInfo)}
- ${fadeDetails !== undefined
- ? html`(${floor(fadeDetails.percentage, 1)}%)`
- : nothing}
- ${this.bluegemData
- ? html`(${floor(this.bluegemData.playside_blue, 1)}%)`
+ ${fadeDetails
+ ? renderFadePercentage(fadeDetails, 1, rank && rank <= 5 ? 'csfloat-shine-fade-text' : '')
: nothing}
+ ${this.bluegemData ? renderBluegemPercentage(this.bluegemData) : nothing}
`;
diff --git a/src/lib/components/common/pattern_details.ts b/src/lib/components/common/pattern_details.ts
new file mode 100644
index 00000000..fab6ade4
--- /dev/null
+++ b/src/lib/components/common/pattern_details.ts
@@ -0,0 +1,57 @@
+import {css, html, TemplateResult} from 'lit';
+
+import {floor} from '../../utils/skin';
+import {FetchBluegemResponse} from '../../bridge/handlers/fetch_bluegem';
+
+export const patternDetailStyles = css`
+ .fade-base {
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+
+ .fade {
+ background-image: -webkit-linear-gradient(0deg, #d9bba5 0%, #e5903b 33%, #db5977 66%, #6775e1 100%);
+ }
+
+ .amber-fade {
+ background-image: -webkit-linear-gradient(0deg, #627d66 0%, #896944 50%, #7e4201 100%);
+ }
+
+ .acid-fade {
+ background-image: -webkit-linear-gradient(0deg, #2b441b 0%, #3e6b2f 11%, #82a64a 66%, #c1a16c 100%);
+ }
+
+ .bluegem {
+ color: deepskyblue;
+ }
+`;
+
+/**
+ * Renders HTML for the fade percentage.
+ * Requires the component to include {@link patternDetailStyles} in its static styles.
+ */
+export function renderFadePercentage(
+ fade: {percentage: number; className: string},
+ precision = 1,
+ extraClasses = ''
+): TemplateResult<1> {
+ return html``;
+}
+
+/**
+ * Renders HTML for the blue gem percentage.
+ * Requires the component to include {@link patternDetailStyles} in its static styles.
+ */
+export function renderBluegemPercentage(bluegemData: FetchBluegemResponse, showBackside = false): TemplateResult<1> {
+ // Some skins got only one blue value
+ if (showBackside && bluegemData.backside_blue !== undefined) {
+ return html`(${floor(bluegemData.playside_blue, 1)}% / ${floor(bluegemData.backside_blue, 1)}%)`;
+ }
+
+ return html`(${floor(bluegemData.playside_blue, 1)}%)`;
+}
diff --git a/src/lib/components/inventory/selected_item_info.ts b/src/lib/components/inventory/selected_item_info.ts
index a76bf0c6..cbb6a3e3 100644
--- a/src/lib/components/inventory/selected_item_info.ts
+++ b/src/lib/components/inventory/selected_item_info.ts
@@ -131,7 +131,10 @@ export class SelectedItemInfo extends FloatElement {
containerChildren.push(html`
Paint Seed: ${formatSeed(this.itemInfo)}
`);
// Fade skins
- const fadePercentage = getFadePercentage(this.asset.description, this.itemInfo)?.percentage;
+ const fadePercentage = getFadePercentage(
+ this.asset.description.market_hash_name,
+ this.itemInfo
+ )?.percentage;
if (fadePercentage !== undefined) {
containerChildren.push(html`Fade: ${floor(fadePercentage, 5)}%
`);
}
diff --git a/src/lib/components/market/item_row_wrapper.ts b/src/lib/components/market/item_row_wrapper.ts
index c78a87aa..39234d36 100644
--- a/src/lib/components/market/item_row_wrapper.ts
+++ b/src/lib/components/market/item_row_wrapper.ts
@@ -247,7 +247,8 @@ export class ItemRowWrapper extends FloatElement {
}
if (this.itemInfo && isSkin(this.asset)) {
- const fadePercentage = this.asset && getFadePercentage(this.asset, this.itemInfo)?.percentage;
+ const fadePercentage =
+ this.asset && getFadePercentage(this.asset.market_hash_name, this.itemInfo)?.percentage;
return html`
diff --git a/src/lib/components/market/react/placement.ts b/src/lib/components/market/react/placement.ts
index 8978828f..d07f148d 100644
--- a/src/lib/components/market/react/placement.ts
+++ b/src/lib/components/market/react/placement.ts
@@ -1,7 +1,16 @@
import type {ScopedInjectionArgs} from '../../injectors';
-import {parseRank} from '../../../utils/skin';
+import {getFadePercentage, isBlueSkin, parseRank} from '../../../utils/skin';
+import {hasDopplerPhase} from '../../../utils/dopplers';
import type {ReactListingContext} from './listing';
+function hasSeedDetail(context: ReactListingContext): boolean {
+ return (
+ getFadePercentage(context.listing.description.market_hash_name, context.itemInfo) !== undefined ||
+ isBlueSkin(context.itemInfo) ||
+ hasDopplerPhase(context.itemInfo.paintindex)
+ );
+}
+
export function findWearSpan({
scope,
context,
@@ -19,3 +28,21 @@ export function findWearSpan({
return undefined;
}
+
+export function findSeedSpan({
+ scope,
+ context,
+}: ScopedInjectionArgs): HTMLSpanElement | null | undefined {
+ if (!hasSeedDetail(context)) return null;
+
+ const spans = scope.querySelectorAll('span[style*="pre-wrap"]');
+ for (const span of spans) {
+ const text = span.textContent?.trim();
+ if (!text) continue;
+
+ const value = parseInt(text, 10);
+ if (!Number.isNaN(value) && String(value) === text && value === context.itemInfo.paintseed) return span;
+ }
+
+ return undefined;
+}
diff --git a/src/lib/components/market/react/seed_info.ts b/src/lib/components/market/react/seed_info.ts
new file mode 100644
index 00000000..75989ab8
--- /dev/null
+++ b/src/lib/components/market/react/seed_info.ts
@@ -0,0 +1,89 @@
+import {css, html, nothing} from 'lit';
+import {property, state} from 'lit/decorators.js';
+
+import {CustomElement, InjectIntoScope, InjectionPosition} from '../../injectors';
+import {FloatElement} from '../../custom';
+import {getFadePercentage, isBlueSkin} from '../../../utils/skin';
+import {getDopplerPhase, hasDopplerPhase} from '../../../utils/dopplers';
+import {ClientSend} from '../../../bridge/client';
+import {FetchBluegem, FetchBluegemResponse} from '../../../bridge/handlers/fetch_bluegem';
+import {renderBluegemPercentage, renderFadePercentage, patternDetailStyles} from '../../common/pattern_details';
+import {ReactMarketListingScope, type ReactListingContext} from './listing';
+import {findSeedSpan} from './placement';
+import type {ItemInfo} from '../../../bridge/handlers/fetch_inspect_info';
+
+/**
+ * Renders the fade percentage and blue-gem percentage next to the paint seed in the Steam Market beta,
+ * mirroring {@link ReactListingRank}. Self-contained: fetches its own blue-gem data.
+ */
+@CustomElement()
+@InjectIntoScope(ReactMarketListingScope, {
+ anchor: findSeedSpan,
+ position: InjectionPosition.After,
+})
+export class ReactListingSeedInfo extends FloatElement {
+ @property({attribute: false}) injectionContext?: ReactListingContext;
+
+ @state() private bluegemData: FetchBluegemResponse | undefined;
+
+ static styles = [
+ patternDetailStyles,
+ css`
+ :host {
+ margin-left: 4px;
+ }
+ `,
+ ];
+
+ async connectedCallback(): Promise {
+ super.connectedCallback();
+
+ const itemInfo = this.injectionContext?.itemInfo;
+ if (itemInfo) {
+ this.bluegemData = await this.fetchBluegem(itemInfo);
+ }
+ }
+
+ private async fetchBluegem(itemInfo: ItemInfo): Promise {
+ if (!isBlueSkin(itemInfo)) return undefined;
+
+ try {
+ return await ClientSend(FetchBluegem, {iteminfo: itemInfo});
+ } catch (e) {
+ return undefined;
+ }
+ }
+
+ private get fadeDetails(): {percentage: number; className: string} | undefined {
+ const context = this.injectionContext;
+ if (!context) return undefined;
+
+ return getFadePercentage(context.listing.description.market_hash_name, context.itemInfo);
+ }
+
+ private get dopplerPhase(): string | undefined {
+ const itemInfo = this.injectionContext?.itemInfo;
+ if (!itemInfo || !hasDopplerPhase(itemInfo.paintindex)) return undefined;
+
+ return getDopplerPhase(itemInfo.paintindex);
+ }
+
+ protected render() {
+ if (!this.injectionContext) return nothing;
+
+ if (this.fadeDetails) {
+ return renderFadePercentage(this.fadeDetails, 2);
+ }
+
+ if (this.bluegemData) {
+ return renderBluegemPercentage(this.bluegemData, true);
+ }
+
+ const phase = this.dopplerPhase;
+ if (phase) {
+ return html`(${phase})`;
+ }
+
+ return nothing;
+ }
+}
diff --git a/src/lib/components/market/sort_listings.ts b/src/lib/components/market/sort_listings.ts
index dce0a0b2..8e43ac70 100644
--- a/src/lib/components/market/sort_listings.ts
+++ b/src/lib/components/market/sort_listings.ts
@@ -59,7 +59,7 @@ export class SortListings extends FloatElement {
const asset = g_rgAssets[AppId.CSGO][ContextId.PRIMARY][listingInfo.asset.id];
- return getFadeParams(asset) !== undefined;
+ return getFadeParams(asset.market_hash_name) !== undefined;
}
connectedCallback() {
@@ -130,7 +130,7 @@ export class SortListings extends FloatElement {
info,
listingId: listingId!,
converted_price: listingInfo?.converted_price || 0,
- fadePercentage: (asset && getFadePercentage(asset, info)?.percentage) || 0,
+ fadePercentage: (asset && getFadePercentage(asset.market_hash_name, info)?.percentage) || 0,
};
} catch (error) {
console.error(`CSFloat: Failed to fetch float for listing ${listingId}:`, error);
diff --git a/src/lib/page_scripts/market_listing.ts b/src/lib/page_scripts/market_listing.ts
index e8149f06..fedf15ec 100644
--- a/src/lib/page_scripts/market_listing.ts
+++ b/src/lib/page_scripts/market_listing.ts
@@ -2,6 +2,7 @@ import {init} from './utils';
import '../components/market/item_row_wrapper';
import '../components/market/utility_belt';
import '../components/market/react/rank';
+import '../components/market/react/seed_info';
init('src/lib/page_scripts/market_listing.js', main);
diff --git a/src/lib/utils/skin.ts b/src/lib/utils/skin.ts
index c6408969..538281c0 100644
--- a/src/lib/utils/skin.ts
+++ b/src/lib/utils/skin.ts
@@ -183,7 +183,7 @@ export function isBlueSkin(itemInfo: ItemInfo): boolean {
);
}
-export function getFadeParams(asset: rgAsset):
+export function getFadeParams(marketHashName: string):
| {
calculator: typeof FadeCalculator | typeof AcidFadeCalculator | typeof AmberFadeCalculator;
weaponName: string;
@@ -198,7 +198,7 @@ export function getFadeParams(asset: rgAsset):
for (const [fadeType, calculator] of Object.entries(FADE_TYPE_TO_CALCULATOR)) {
for (const supportedWeapon of calculator.getSupportedWeapons()) {
- if (asset.market_hash_name.includes(`${supportedWeapon} | ${fadeType}`)) {
+ if (marketHashName.includes(`${supportedWeapon} | ${fadeType}`)) {
return {
calculator,
weaponName: supportedWeapon.toString(),
@@ -210,10 +210,10 @@ export function getFadeParams(asset: rgAsset):
}
export function getFadePercentage(
- asset: rgAsset,
+ marketHashName: string,
itemInfo: ItemInfo
): {percentage: number; className: string} | undefined {
- const fadeInfo = getFadeParams(asset);
+ const fadeInfo = getFadeParams(marketHashName);
if (fadeInfo !== undefined) {
const {calculator, weaponName, className} = fadeInfo;