// ==UserScript==
// @name Steam Inventory Enhancer
// @namespace https://kurotaku.de
// @version 1.1.4
// @description Adds mass stacking/unstacking tools, a customizable sidebar with favorites, advanced inventory filtering/sorting, and ASF IPC integration for seamless 2FA confirmations.
// @description:de Fügt Tools zum Massen-Stapeln/Entstapeln, eine anpassbare Seitenleiste mit Favoriten, erweiterte Filter- und Sortierfunktionen für Inventare sowie eine ASF-IPC-Integration für 2FA-Bestätigungen hinzu.
// @author Kurotaku
// @license CC BY-NC-SA 4.0
// @match https://steamcommunity.com/profiles/*/inventory*
// @match https://steamcommunity.com/id/*/inventory*
// @icon https://steamcommunity.com/favicon.ico
// @updateURL https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/userscripts/Steam_Inventory_Enhancer/script.user.js
// @downloadURL https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/userscripts/Steam_Inventory_Enhancer/script.user.js
// @require https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/libraries/kuros_library.js
// @require https://cdn.jsdelivr.net/npm/sweetalert2
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @run-at document-body
// ==/UserScript==
// ==========================================================
// Main
// ==========================================================
(async function() {
await init_gm_config();
if (GM_config.get("inv_sidebar_enabled"))
Sidebar.init();
if (GM_config.get("item_stacker_enabled")) {
(async () => {
await wait_for_element(".new_trade_offer_btn");
Stacker.init();
})();
}
if (GM_config.get("inv_favorites_enabled"))
Favorites.init();
if (GM_config.get("filter_search_enabled"))
Filter.init();
if (GM_config.get("asf_enabled"))
ASF.init();
})();
async function init_gm_config() {
const config_id = "configuration_sie";
GM_registerMenuCommand("Settings", () => GM_config.open());
GM_config.init({
id: config_id,
title: 'Steam Inventory Enhancer',
fields: {
inv_sidebar_enabled: {
section: ['Sidebar Settings'],
type: 'checkbox',
default: true,
label: 'Enable Inventories Sidebar',
},
inv_sidebar_hide_active_headline: {
type: 'checkbox',
default: false,
label: 'Hide active inventory headline',
},
inv_sidebar_rows: {
label: 'Sidebar Columns
Higher values require more screen space',
type: 'select',
options: ['1', '2', '3', '4'],
default: '2',
},
item_stacker_enabled: {
section: ['Item Stacker Settings'],
type: 'checkbox',
default: true,
label: 'Enable Stacker Panel',
},
item_stacker_max_size_enabled: {
type: 'checkbox',
default: true,
label: 'Enable Max Stack Size Input
(if disabled, stacks will be infinite)',
},
item_stacker_single_row: {
type: 'checkbox',
default: true,
label: 'Compact Stacker Layout
Toggle between single-row and multi-row display',
},
inv_favorites_enabled: {
section: ['Favorites'],
type: 'checkbox',
default: true,
label: 'Enable Favorites',
},
inv_favorites_order: {
label: 'Order of the Favorites in the Favorites section',
type: 'select',
options: ['Name', 'Amount', 'Added to Favorites', 'Added to Favorites Reversed'],
default: 'Amount',
},
filter_search_enabled: {
section: ['Filter & Search'],
type: 'checkbox',
default: true,
label: 'Enable Filter & Search'
},
filter_search_by_appid: {
type: 'checkbox',
default: false,
label: 'Search by AppID'
},
filter_show_only_missing: {
type: 'checkbox',
default: true,
label: 'Enable "Hide Owned" Filter Button'
},
filter_search_ctrl_f_hotkey: {
type: 'checkbox',
default: true,
label: 'Override CTRL + F to focus searchbox'
},
asf_enabled: {
section: ['ArchiSteamFarm (ASF) Integration'],
type: 'checkbox',
default: false,
label: 'Enable ASF Integration
' +
'' +
'WARNING: This feature connects to your ASF IPC interface.
' +
'Ensure your IPC is secured with a password and you use SSL.
' +
'The author is not responsible for accidental confirmations or account issues.
' +
'Use at your own risk!',
},
asf_ip: {
label: 'IP/Hostname',
type: 'input',
default: '127.0.0.1',
},
asf_port: {
label: 'Port',
type: 'number',
default: 1242,
},
asf_password: {
label: 'IPC Password (if set)',
type: 'input',
default: '',
},
asf_botname: {
label: 'Bot Name (Use ASF for all Bots)',
type: 'text',
default: 'ASF',
},
install_steam_economy_enhancer: {
section: ['Third-Party Recommendations'],
label: "Install Steam Economy Enhancer",
type: "button",
size: 100,
click: () => window.open("https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js", "_blank"),
},
install_augmented_steam: {
label: "Install Augmented Steam",
type: "button",
size: 100,
click: () => window.open("https://augmentedsteam.com/", "_blank"),
},
},
events: {
save: () => { location.reload() },
},
frame: create_configuration_container(),
});
}
// ==========================================================
// Sidebar
// ==========================================================
class Sidebar {
static init() {
this.inject_styles();
}
static inject_styles() {
// Get rows and check if headline must be hidden (only own inventory)
const rows = GM_config.get("inv_sidebar_rows") || '2';
const hide_headline = GM_config.get("inv_sidebar_hide_active_headline");
GM_addStyle(`
${hide_headline ? `
.games_list_separator:not(.actionable) {
display: none;
}
` : ''}
@media (min-width: 1450px) {
#BG_bottom, #mainContents {
width: max-content !important;
max-width: unset !important;
}
#mainContents {
display: grid !important;
grid-template-columns: auto 1fr;
}
#mainContents > * {
grid-column: 1 / span 2;
}
#mainContents > .tabitems_ctn {
grid-column: 1;
}
#mainContents > #tabcontent_inventory {
grid-column: 2;
margin-top: unset;
}
#tabcontent_inventory {
margin-top: unset;
}
.games_list_tabs {
display: grid !important;
grid-template-columns: repeat(${rows}, 1fr);
overflow-y: scroll;
overflow-x: hidden;
max-height: min(100vh, 600px);
}
#game_list_favorites {
margin-top: unset;
}
}
`);
}
}
// ==========================================================
// Stacker
// ==========================================================
class Stacker {
// Global identifiers for the active Steam inventory session
static g_app_id = 0;
static g_context_id = 2;
static g_token = "";
static is_cancelled = false;
// Delay to prevent rate limiting (429) from Steam API
static DELAY_MS = 300;
static INV_AMOUNT = 1000;
// Inventories where stacking is not supported or handled differently by Steam
// 440 TF2 | 570 Dota 2 | 730 Steam | 753 CS2 | 238460 BattleBlock Theater | 252490 Rust | 304930 Unturned | 1269260 Artifact Foundry
static ITEM_STACKER_IGNORED_INVENTORIES = [440, 570, 730, 753, 238460, 252490, 304930, 1269260];
static init() {
// Attempt to retrieve the required WebAPI token from the page source
if (!this.fetch_api_token())
console.error("[SIE Stacker] Failed to load Token.");
// Locate the inventory action area to inject the UI
const btn_area = document.querySelector("div.inventory_links");
if (!btn_area) return;
// Load user preferences for UI display
const show_max_size = GM_config.get("item_stacker_max_size_enabled");
const is_single_row = GM_config.get("item_stacker_single_row");
// Build the HTML panel for Stack/Unstack controls
const panel_html = `
`;
btn_area.insertAdjacentHTML('afterbegin', panel_html);
const input_max = document.getElementById("sie_item_stacker_ipt_max");
input_max.value = GM_getValue("sie_item_stacker_limit", "0");
// Input validation: Allow navigation keys and numbers, block everything else
restrict_input_to_numbers(input_max);
document.getElementById("sie_item_stacker_btn_stack").onclick = () => this.execute_action("stack");
document.getElementById("sie_item_stacker_btn_unstack").onclick = () => this.execute_action("unstack");
}
static fetch_api_token() {
// Steam stores the loyalty/webapi token in a JSON blob within this element
let el = document.querySelector("#application_config");
if (el) {
this.g_token = el.getAttribute("data-loyalty_webapi_token")?.replace(/"/g, "");
return !!this.g_token;
}
return false;
}
static get_game_name() {
// Extracts the display name of the currently selected game from the inventory tabs
const active_tab = document.querySelector(".games_list_tab.active .games_list_tab_name");
return active_tab ? active_tab.innerText.trim() : "Game";
}
static update_inventory_context() {
// Syncs global variables with Steam's internal g_ActiveInventory object
if (typeof g_ActiveInventory !== 'undefined' && g_ActiveInventory.m_appid) {
this.g_app_id = g_ActiveInventory.m_appid;
this.g_context_id = g_ActiveInventory.m_contextid;
return true;
}
return false;
}
static async execute_action(mode) {
this.is_cancelled = false;
if (!this.update_inventory_context()) {
Swal.fire({
title: "Error",
text: "Could not detect active inventory. Please select a game first.",
icon: "error",
theme: "dark"
});
return;
}
// Prevent execution on inventories known to cause issues or errors
if (this.ITEM_STACKER_IGNORED_INVENTORIES.includes(parseInt(this.g_app_id))) {
Swal.fire({
title: "Not Supported",
text: `This inventory is not (un)stackable (AppID: ${this.g_app_id}).`,
icon: "warning",
theme: "dark"
});
return;
}
const input_el = document.getElementById("sie_item_stacker_ipt_max");
let stack_max = GM_config.get("item_stacker_max_size_enabled") ? parseInt(input_el.value) : 0;
if (isNaN(stack_max))
stack_max = 0;
const game_name = this.get_game_name();
if (GM_config.get("item_stacker_max_size_enabled"))
GM_setValue("sie_item_stacker_limit", stack_max.toString());
Swal.fire({
title: "Fetching inventory...",
allowOutsideClick: false,
showConfirmButton: false,
theme: "dark",
didOpen: () => {
Swal.showLoading();
}
});
try {
// Fetch the full list of assets via the inventory Web API
const inv = await this.load_inventory_with_retry(this.g_app_id, this.g_context_id, this.INV_AMOUNT);
if (!inv || !inv.assets)
throw new Error("Failed to load inventory (Steam API error).");
const assets = inv.assets;
let success = false;
// Delegate task to specific handler based on user choice
if (mode === "stack")
success = await this.handle_stacking(assets, game_name, stack_max);
else
success = await this.handle_unstacking(assets, game_name);
// Refresh page on success to show updated inventory layout
if (success)
location.reload();
else
Swal.close();
} catch (err) {
if (!this.is_cancelled)
Swal.fire({
title: "Error",
text: err.message || err,
icon: "error",
theme: "dark"
});
else
Swal.close();
}
}
static async load_inventory_with_retry(app_id, context_id, count) {
let all_assets = [];
let last_assetid = null;
let has_more = true;
let retry_count = 0;
// Loop through paginated inventory results
while (has_more && !this.is_cancelled) {
let url = `https://steamcommunity.com/inventory/${g_steamID}/${app_id}/${context_id}?l=english&count=${count}`;
if (last_assetid)
url += `&start_assetid=${last_assetid}`;
try {
const res = await fetch(url);
if (!res.ok)
throw new Error(`Steam API error: ${res.status}`);
const data = await res.json();
if (data && data.assets) {
all_assets = all_assets.concat(data.assets);
// Handle pagination if Steam indicates more items are available
if (data.more_items && data.assets.length === count) {
last_assetid = data.last_assetid;
await new Promise(r => setTimeout(r, 1000));
} else
has_more = false;
} else
throw new Error("No assets in response");
} catch (e) {
// Simple retry logic for network instability
if (retry_count < 2) {
retry_count++;
await new Promise(r => setTimeout(r, 2000));
} else
has_more = false;
}
}
return { assets: all_assets };
}
static async handle_stacking(assets, game_name, stack_max) {
const item_group = {};
const descriptions = g_ActiveInventory.m_rgDescriptions || {};
// Group items by classid and instanceid to identify identical items
assets.forEach(item => {
const { classid, instanceid } = item;
const key = `${classid}_${instanceid}`;
if (!item_group[key])
item_group[key] = [];
item.amount = parseInt(item.amount);
// Attach display name for progress tracking UI
const desc = descriptions[key] || descriptions[classid] || { name: "Unknown Item" };
item.item_display_name = desc.name;
item_group[key].push(item);
});
let total_req = 0;
const todo_list = [];
// Logic to determine which items need to be merged
for (let key in item_group) {
const items = item_group[key];
// Case A: No limit - merge all identical items into the first one
if (stack_max === 0) {
if (items.length > 1) {
todo_list.push(items);
total_req += items.length - 1;
}
}
// Case B: Max stack size limit active - requires bin-packing logic
else {
const stacks = [];
const temp_items = [...items];
while (temp_items.length > 0) {
const item = temp_items.pop();
if (item.amount > stack_max) continue; // Skip if item is already larger than limit
let added = false;
// Try to fit the item into an existing created stack
for (let stack of stacks) {
if (stack.amount + item.amount <= stack_max) {
stack.list.push(item);
stack.amount += item.amount;
added = true;
break;
}
}
// If it doesn't fit, start a new stack
if (!added)
stacks.push({ list: [item], amount: item.amount });
}
// Filter groups that actually need merging (more than 1 item)
for (let stack of stacks) {
if (stack.list.length > 1) {
todo_list.push(stack.list);
total_req += stack.list.length - 1;
}
}
}
}
if (total_req === 0) return false;
this.show_dual_progress_swal(`Stacking: ${game_name}`);
let actions_done = 0;
const total_types = todo_list.length;
// Execute the CombineItemStacks API calls sequentially
for (let j = 0; j < todo_list.length; j++) {
if (this.is_cancelled) break;
const items = todo_list[j];
const current_item_name = items[0].item_display_name;
for (let i = 1; i < items.length; i++) {
if (this.is_cancelled) break;
actions_done++;
const overall_percent = Math.round((actions_done / total_req) * 100);
this.update_dual_progress_swal(
current_item_name,
(j + 1),
total_types,
overall_percent,
actions_done,
total_req
);
// Merges fromitemid into destitemid
await this.run_steam_api("CombineItemStacks", `fromitemid=${items[i].assetid}&destitemid=${items[0].assetid}&quantity=${items[i].amount}`);
await new Promise(r => setTimeout(r, this.DELAY_MS));
}
}
return true;
}
static async handle_unstacking(assets, game_name) {
const descriptions = g_ActiveInventory.m_rgDescriptions || {};
// Identify all items with an amount greater than 1
const unstack_jobs = assets.filter(item => parseInt(item.amount) > 1);
let total_actions = 0;
unstack_jobs.forEach(item => {
const { classid, instanceid } = item;
const key = `${classid}_${instanceid}`;
const desc = descriptions[key] || descriptions[classid] || { name: "Unknown Item" };
item.item_display_name = desc.name;
// Number of splits needed is amount - 1
total_actions += (parseInt(item.amount) - 1);
});
if (total_actions === 0) return false;
this.show_dual_progress_swal(`Unstacking: ${game_name}`);
let actions_done = 0;
const total_items = unstack_jobs.length;
// Execute SplitItemStack API calls sequentially
for (let j = 0; j < unstack_jobs.length; j++) {
if (this.is_cancelled) break;
const item = unstack_jobs[j];
const amt = parseInt(item.amount);
// Repeatedly split off 1 unit until the stack is empty
for (let i = 1; i < amt; i++) {
if (this.is_cancelled) break;
actions_done++;
const overall_percent = Math.round((actions_done / total_actions) * 100);
this.update_dual_progress_swal(
item.item_display_name,
(j + 1),
total_items,
overall_percent,
actions_done,
total_actions
);
await this.run_steam_api("SplitItemStack", `itemid=${item.assetid}&quantity=1`);
await new Promise(r => setTimeout(r, this.DELAY_MS));
}
}
return true;
}
static show_dual_progress_swal(title) {
// UI modal with two progress indicators (current item and overall total)
Swal.fire({
title: title,
html: `
Initializing...
Item 0 / 0
Overall Progress: 0%
0 / 0
`,
showCancelButton: true,
cancelButtonText: "Cancel",
allowOutsideClick: false,
showConfirmButton: false,
theme: "dark",
didOpen: () => {
const btn_cancel = Swal.getCancelButton();
btn_cancel.onclick = () => {
this.is_cancelled = true;
Swal.close();
};
}
});
}
static update_dual_progress_swal(item_name, item_curr, item_total, overall_percent, total_done, total_all) {
// Dynamically update the modal text and progress bar values
if (Swal.isVisible() && !this.is_cancelled) {
const el_name = document.getElementById('sie_item_name');
const el_item_count = document.getElementById('sie_item_count');
const el_overall_text = document.getElementById('sie_item_stacker_overall_text');
const el_prog = document.getElementById('sie_item_stacker_prog');
const el_prog_val = document.getElementById('sie_item_stacker_val');
if (el_name)
el_name.innerText = item_name;
if (el_item_count)
el_item_count.innerText = `Item ${item_curr} / ${item_total}`;
if (el_overall_text)
el_overall_text.innerText = `Overall Progress: ${overall_percent}%`;
if (el_prog) {
el_prog.max = total_all;
el_prog.value = total_done;
}
if (el_prog_val) el_prog_val.innerText = `${total_done} / ${total_all}`;
}
}
static async run_steam_api(method, params) {
// General wrapper for IInventoryService POST requests
const url = `https://api.steampowered.com/IInventoryService/${method}/v1/`;
const body = `access_token=${this.g_token}&appid=${this.g_app_id}&steamid=${g_steamID}&${params}`;
const res = await fetch(url, {
method: "POST",
body: body,
headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }
});
return res.json();
}
}
GM_addStyle(`
.sie_item_stacker_container {
gap: 5px;
display: inline-flex;
}
.sie_item_stacker_container.sie_item_stacker_multi_row {
flex-direction: column;
align-items: flex-start;
}
.sie_item_stacker_container.sie_item_stacker_single_row {
flex-direction: row;
align-items: center;
}
.sie_item_stacker_row {
display: flex;
align-items: center;
gap: 5px;
}
.sie_item_stacker_inputbox {
width: 50px;
color: #fff;
padding: 2px 4px;
}
`);
// ==========================================================
// Favorites
// ==========================================================
class Favorites {
// Entry point: initializes styles, creates the container, and adds stars to the game list
static init() {
this.inject_styles();
this.add_favorites_container();
this.add_stars_to_games();
}
// Injects custom CSS for the star icon, its animations, and the favorite list layout
static inject_styles() {
GM_addStyle(`
a.games_list_tab {
position: relative;
}
.sie_fav_star {
position: absolute;
top: 2px;
right: 2px;
width: 14px;
height: 14px;
color: #555;
cursor: pointer;
transition: color 0.2s, transform 0.1s;
line-height: 0;
z-index: 5;
}
.sie_fav_star:hover {
transform: scale(1.3);
}
.sie_fav_star.active {
color: #ffde24;
filter: drop-shadow(0 0 2px rgba(255, 222, 36, 0.5));
}
#sie_favorites_list .games_list_tab.active {
background: rgba(255, 255, 255, 0.05);
box-shadow: none;
}
#sie_favorites_list .sie_hidden_by_search {
display: flex !important;
}
`);
}
// Waits for the Steam inventory sidebar and injects the "Favorites" section header and list container
static async add_favorites_container() {
const tab_container = await wait_for_element(".tabitems_ctn");
const favorites_html = `
Favorites
`;
tab_container.insertAdjacentHTML('afterbegin', favorites_html);
}
// Scrapes the existing game list to map AppIDs to their names, item counts, and DOM elements
static build_app_inventory_map() {
const app_map = new Map();
document.querySelectorAll("a.games_list_tab").forEach(tab => {
const href = tab.getAttribute("href");
if(!href) return;
// Extract AppID from href (e.g., "#753" -> "753")
const app_id = href.replace('#', '');
if(!app_id) return;
const name_el = tab.querySelector(".games_list_tab_name");
const app_name = name_el ? name_el.innerText.trim() : "";
// Parse item count from the tab, removing non-numeric characters (like brackets)
const count_el = tab.querySelector(".games_list_tab_number");
const text_amount = count_el ? count_el.innerText : "0";
const item_count = parseInt(text_amount.replace(/\D/g, '')) || 0;
app_map.set(app_id, {
app_name: app_name,
item_count: item_count,
tab_element: tab
});
});
return app_map;
}
// Injects the SVG star into every game tab in the original list
static async add_stars_to_games() {
await wait_for_element("a.games_list_tab");
const fav_ids = GM_getValue("sie_fav_ids", []);
const tabs = document.querySelectorAll("a.games_list_tab");
tabs.forEach(tab => {
// Prevent duplicate stars if the script runs multiple times
if(tab.querySelector(".sie_fav_star")) return;
const app_id = tab.getAttribute("href")?.replace('#', '');
if(!app_id) return;
const star = document.createElement("div");
star.className = `sie_fav_star ${fav_ids.includes(app_id) ? 'active' : ''}`;
star.innerHTML = `
`;
// Prevent the tab click from triggering when clicking the star itself
star.onclick = e => {
e.preventDefault();
e.stopPropagation();
this.toggle_favorite(app_id, star);
};
tab.appendChild(star);
});
this.update_favorites_list();
}
// Handles adding/removing AppIDs from the local storage and updates UI state
static toggle_favorite(app_id, star_el) {
let fav_ids = GM_getValue("sie_fav_ids", []);
if(fav_ids.includes(app_id))
fav_ids = fav_ids.filter(id => id !== app_id);
else
fav_ids.push(app_id);
if(star_el.classList.contains("active"))
star_el.classList.remove("active");
else
star_el.classList.add("active");
GM_setValue("sie_fav_ids", fav_ids);
this.update_favorites_list();
}
// Rebuilds the favorites section based on saved IDs and the chosen sorting method
static update_favorites_list() {
const fav_ids = GM_getValue("sie_fav_ids", []);
const order_type = GM_config.get("inv_favorites_order") || 'Amount';
const list_container = document.querySelector("#sie_favorites_list");
const header = document.getElementById("game_list_favorites");
const list_wrapper = list_container?.parentElement;
if(!list_container || !header || !list_wrapper) return;
// Clear existing items before rebuilding
list_container.innerHTML = "";
// Hide the section entirely if no favorites are selected
if(!fav_ids || fav_ids.length === 0) {
header.style.display = "none";
list_wrapper.style.display = "none";
return;
}
const app_map = this.build_app_inventory_map();
let sorted_ids = [...fav_ids];
// Apply sorting based on user configuration
if(order_type === 'Name')
sorted_ids.sort((a, b) => `${app_map.get(a)?.app_name || ''}`.toLowerCase().localeCompare(`${app_map.get(b)?.app_name || ''}`.toLowerCase()));
else if(order_type === 'Amount')
sorted_ids.sort((a, b) => (app_map.get(b)?.item_count || 0) - (app_map.get(a)?.item_count || 0));
else if(order_type === 'Added to Favorites')
{} // Default order is the order in which they were added to the array
else if(order_type === 'Added to Favorites Reversed')
sorted_ids.reverse();
sorted_ids.forEach(id => {
const original_tab = app_map.get(id)?.tab_element;
if(!original_tab) return;
// Deep clone the original tab to maintain Steam's native styling and icons
const clone = original_tab.cloneNode(true);
clone.removeAttribute("id");
const clone_star = clone.querySelector(".sie_fav_star");
if(clone_star) {
clone_star.classList.add("active");
clone_star.onclick = e => {
e.preventDefault();
e.stopPropagation();
// Clicking clone star affects the original star and re-triggers the logic
this.toggle_favorite(id, original_tab.querySelector(".sie_fav_star"));
};
}
// Clicking the favorite clone triggers a click on the original tab to switch inventory view
clone.onclick = e => {
e.preventDefault();
original_tab.click();
document.querySelectorAll('.games_list_tab').forEach(t => t.classList.remove('active'));
clone.classList.add('active');
};
list_container.appendChild(clone);
});
// Ensure proper layout visibility depending on item presence
if(list_container.children.length === 0) {
header.style.display = "none";
list_wrapper.style.display = "none";
} else {
header.style.display = "block";
list_wrapper.style.display = "grid";
}
}
}
// ==========================================================
// Filter
// ==========================================================
class Filter {
static app_data_cache = [];
static owned_app_ids = null; // Stores the user's owned app ids after fetch
static hide_owned_active = false; // Toggle state for the "Hide Owned" filter
static async init() {
const container = await wait_for_element("#games_list_public");
// initial cache before UI injection to get the total count for the placeholder
this.cache_elements();
this.inject_controls(container);
// Load initial sort state or default to amount_desc
const current_sort = GM_getValue("sie_filter_sort", "amount_desc");
this.sort_list(current_sort);
if (GM_config.get("filter_search_ctrl_f_hotkey"))
this.override_hotkey();
}
static inject_controls(container) {
const active_sort = GM_getValue("sie_filter_sort", "amount_desc");
const total_count = this.app_data_cache.length;
// Define placeholder text based on config setting
const search_by_appid = GM_config.get("filter_search_by_appid");
const placeholder_text = search_by_appid
? `Search ${total_count} inventories (Name or ID)...`
: `Search ${total_count} inventories...`;
// Check if the "Hide Owned" feature is enabled in config
const show_missing_enabled = GM_config.get("filter_show_only_missing");
// Check if the viewed inventory belongs to the logged-in user
// Using UserYou.strSteamId to compare the owner of the page with the own g_steamID
const is_own_profile = (typeof UserYou !== 'undefined' && UserYou.strSteamId === g_steamID);
const filter_html = `
${(show_missing_enabled && !is_own_profile) ? `
` : ''}
✕
`;
container.insertAdjacentHTML('afterbegin', filter_html);
// Sorting Event Listeners
document.getElementById("sie_sort_amount_desc").onclick = () => this.sort_list("amount_desc");
document.getElementById("sie_sort_amount_asc").onclick = () => this.sort_list("amount_asc");
document.getElementById("sie_sort_name_asc").onclick = () => this.sort_list("name_asc");
document.getElementById("sie_sort_name_desc").onclick = () => this.sort_list("name_desc");
// Toggle Owned Filter Event Listener
const btn_owned = document.getElementById("sie_filter_hide_owned");
if (btn_owned) {
btn_owned.onclick = () => this.toggle_hide_owned();
}
// Combined Input Event Listeners
const inputs = ["sie_filter_search", "sie_filter_min", "sie_filter_max"];
inputs.forEach(id => document.getElementById(id).oninput = () => this.apply_filters());
// Apply number restriction helper
restrict_input_to_numbers(document.getElementById("sie_filter_min"));
restrict_input_to_numbers(document.getElementById("sie_filter_max"));
// Individual Clear Buttons (X)
document.querySelectorAll(".sie_clear_input").forEach(btn => {
btn.onclick = () => {
const target = document.getElementById(btn.dataset.target);
target.value = "";
this.apply_filters();
};
});
// Global Reset Button
document.getElementById("sie_filter_reset_all").onclick = () => {
inputs.forEach(id => document.getElementById(id).value = "");
this.hide_owned_active = false;
if (btn_owned) {
btn_owned.classList.remove("active");
btn_owned.querySelector("span").innerText = "Hide Owned";
}
this.apply_filters();
};
}
static cache_elements() {
this.app_data_cache = [];
// Selector expanded to include public, private and failed inventory sections
const selectors = [
"#games_list_public a.games_list_tab",
"#games_list_private a.games_list_tab",
"#games_list_failed a.games_list_tab"
];
const tabs = document.querySelectorAll(selectors.join(", "));
tabs.forEach(tab => {
const name_el = tab.querySelector(".games_list_tab_name");
const count_el = tab.querySelector(".games_list_tab_number");
// Extract AppID from the href attribute
const href_attr = tab.getAttribute("href") || "";
const app_id = href_attr.replace('#', '');
const name = name_el ? name_el.innerText.trim() : "";
const text_amount = count_el ? count_el.innerText : "0";
// Extract numbers only for accurate integer sorting
const amount = parseInt(text_amount.replace(/\D/g, '')) || 0;
this.app_data_cache.push({
node: tab,
name: name,
app_id: app_id,
amount: amount
});
});
}
static sort_list(type) {
GM_setValue("sie_filter_sort", type);
// Update active UI states
document.querySelectorAll(".sie_filter_buttons button").forEach(btn => btn.classList.remove("active"));
if (type === "amount_desc") document.getElementById("sie_sort_amount_desc").classList.add("active");
if (type === "amount_asc") document.getElementById("sie_sort_amount_asc").classList.add("active");
if (type === "name_asc") document.getElementById("sie_sort_name_asc").classList.add("active");
if (type === "name_desc") document.getElementById("sie_sort_name_desc").classList.add("active");
const container = document.querySelector("#games_list_public .games_list_tabs");
if (!container) return;
const sorted = [...this.app_data_cache].sort((a, b) => {
if (type === "name_asc") return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
if (type === "name_desc") return b.name.toLowerCase().localeCompare(a.name.toLowerCase());
if (type === "amount_asc") return a.amount - b.amount;
return b.amount - a.amount; // default: amount_desc
});
// Append nodes in new order (DOM automatically moves existing elements)
// Note: sorting remains primarily visual for the public container as per legacy logic
sorted.forEach(item => {
if (item.node.parentElement) {
item.node.parentElement.appendChild(item.node);
}
});
}
static async toggle_hide_owned() {
// Fetch user's own inventory app ids if not already cached
if (this.owned_app_ids === null) {
Swal.fire({
title: "Detecting owned inventories...",
allowOutsideClick: false,
showConfirmButton: false,
theme: "dark",
didOpen: () => {
Swal.showLoading();
}
});
try {
// Fetch the current user's own profile inventory using the own g_steamID
const response = await fetch(`https://steamcommunity.com/profiles/${g_steamID}/inventory/`);
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Extract all appids from the sidebar tabs of the user's own inventory
const own_tabs = doc.querySelectorAll(".games_list_tab");
this.owned_app_ids = Array.from(own_tabs).map(tab => {
const href = tab.getAttribute("href") || "";
return href.replace('#', '');
}).filter(id => id !== "");
Swal.close();
} catch (err) {
Swal.fire({
title: "Error",
text: "Failed to load your owned inventory list.",
icon: "error",
theme: "dark"
});
return;
}
}
// Toggle state and update UI
this.hide_owned_active = !this.hide_owned_active;
const btn = document.getElementById("sie_filter_hide_owned");
if (this.hide_owned_active) {
btn.classList.add("active");
btn.querySelector("span").innerText = "Show Owned";
} else {
btn.classList.remove("active");
btn.querySelector("span").innerText = "Hide Owned";
}
this.apply_filters();
}
static apply_filters() {
const query = document.getElementById("sie_filter_search").value.toLowerCase();
const min = parseInt(document.getElementById("sie_filter_min").value) || 0;
const max = parseInt(document.getElementById("sie_filter_max").value) || Infinity;
// Retrieve config setting for appid search
const search_by_appid = GM_config.get("filter_search_by_appid");
this.app_data_cache.forEach(item => {
// Check if search matches name
const matches_name = item.name.toLowerCase().includes(query);
// Check if search matches app_id only if enabled in config
const matches_id = search_by_appid ? item.app_id.includes(query) : false;
const matches_text = matches_name || matches_id;
const matches_range = item.amount >= min && item.amount <= max;
// Check if the item should be hidden based on "Hide Owned" toggle
let matches_owned = true;
if (this.hide_owned_active && this.owned_app_ids) {
// Hide if the app_id is in our owned list
matches_owned = !this.owned_app_ids.includes(item.app_id);
}
// Hide element if it doesn't match all criteria
if (matches_text && matches_range && matches_owned) return item.node.classList.remove("sie_hidden_by_search");
item.node.classList.add("sie_hidden_by_search");
});
}
static override_hotkey() {
window.addEventListener('keydown', (e) => {
// Check for CTRL+F (Windows/Linux) or CMD+F (Mac)
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
const search_input = document.getElementById("sie_filter_search");
if (search_input) {
e.preventDefault(); // Prevent Browser Search
search_input.focus();
// Select Text
search_input.select();
}
}
});
}
}
GM_addStyle(`
#sie_filter_controls {
padding: 10px;
background: rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
gap: 5px;
border-bottom: 1px solid #2c3235;
margin-bottom: 5px;
}
.sie_filter_top_row {
display: flex;
gap: 5px;
}
.sie_filter_buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5px;
flex: 1;
}
.sie_filter_buttons .btn_small {
justify-content: center;
padding: 4px 0;
}
.sie_filter_buttons .btn_small.active {
background: #4e7297 !important;
color: white !important;
}
#sie_filter_reset_all {
width: 30px;
justify-content: center;
color: #ff5c5c;
}
.sie_filter_range {
display: flex;
gap: 5px;
}
.sie_input_wrapper {
position: relative;
flex: 1;
display: flex;
align-items: center;
}
#sie_filter_search,
#sie_filter_min,
#sie_filter_max {
width: 100%;
background: #101214;
border: 1px solid #2c3235;
color: #fff;
padding: 4px 22px 4px 8px;
border-radius: 2px;
font-size: 12px;
}
/* Remove arrows from number inputs */
#sie_filter_min::-webkit-inner-spin-button,
#sie_filter_max::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.sie_clear_input {
position: absolute;
right: 6px;
cursor: pointer;
color: #555;
font-size: 10px;
user-select: none;
}
.sie_clear_input:hover { color: #fff; }
.sie_hidden_by_search {
display: none !important;
}
`);
// ==========================================================
// ASF Integration
// ==========================================================
class ASF {
// Initializes the ASF integration by injecting menu items into the Steam inventory "More" dropdown
static async init() {
// Wait for the Steam inventory's popup menu to exist in the DOM before modification
const dropdown_menu = await wait_for_element("#inventory_more_dropdown .popup_body.popup_menu");
// Retrieve the configured bot name from script settings (defaults to 'ASF')
const bot = GM_config.get("asf_botname") || 'ASF';
// HTML template for the 2FA confirmation controls
const asf_html = `
`;
// Insert the new menu items at the end of the existing dropdown list
dropdown_menu.insertAdjacentHTML('beforeend', asf_html);
// Click handler for accepting all pending 2FA mobile confirmations
document.getElementById("sie_asf_2faok").onclick = (e) => {
e.preventDefault();
this.send_command(`2faok ${bot}`, "Confirmations accepted!");
};
// Click handler for denying/cancelling all pending 2FA mobile confirmations
document.getElementById("sie_asf_2fano").onclick = (e) => {
e.preventDefault();
this.send_command(`2fano ${bot}`, "Confirmations denied.", "warning");
};
}
// Communicates with the ASF IPC (Inter-Process Communication) interface via HTTP POST
static async send_command(cmd, success_msg = "Command send", icon = "success") {
// Retrieve connection details from the script configuration
const ip = GM_config.get("asf_ip");
const port = GM_config.get("asf_port");
const password = GM_config.get("asf_password");
const url = `http://${ip}:${port}/Api/Command`;
// Show a loading overlay while waiting for the network request
Swal.fire({
title: "ASF sending command...",
theme: "dark",
didOpen: () => Swal.showLoading()
});
// Use GM_xmlhttpRequest to bypass Cross-Origin (CORS) restrictions when talking to local/remote IPC
GM_xmlhttpRequest({
method: "POST",
url: url,
headers: {
"Content-Type": "application/json",
// ASF IPC requires the password in the Authentication header
"Authentication": password
},
data: JSON.stringify({ Command: cmd }),
onload: (res) => {
// Success: ASF received and processed the command
if (res.status === 200) {
const data = JSON.parse(res.responseText);
Swal.fire({
title: "ASF Result",
text: data.Result || success_msg, // Display the actual response text from ASF
icon: icon,
toast: true,
position: 'top-end',
timer: 5000,
showConfirmButton: false,
theme: "dark"
});
}
// Error 401: IPC password in settings does not match the ASF configuration
else if (res.status === 401) {
Swal.fire({
title: "Error: Invalid IPC Password",
icon: "error",
theme: "dark"
});
}
// Generic error handler for other HTTP status codes
else {
Swal.fire({
title: `Error: ${res.status}`,
icon: "error",
theme: "dark"
});
}
},
// Triggered if the request fails completely (e.g. ASF not running or wrong IP/Port)
onerror: () => {
Swal.fire({
title: "Error: ASF is not reachable",
html: `Make sure:
- IPC is enabled
- Check if IP/Hostname and port are correct
- ASF is accessible from this machine`,
icon: "error",
theme: "dark"
});
}
});
}
}
// ==========================================================
// General Styling
// ==========================================================
GM_addStyle(`
.games_list_tabs {
display: grid !important;
grid-template-columns: repeat(4, 1fr);
}
.games_list_tab_separator,
.games_list_tab_row_separator {
display: none;
}
.games_list_separator {
border-bottom: unset !important;
}
.games_list_separator,
.games_list_tabs_ctn{
border: 1px solid #2c3235;
}
.games_list_tabs_ctn {
padding: 0;
}
.games_list_tabs > div {
display: none;
}
a.games_list_tab {
width: 100%;
display: flex;
gap: 10px;
align-items: center;
background: unset;
border: unset !important;
}
.games_list_tab > span {
padding: 0;
line-height: 2;
}
.games_list_tab_icon {
margin: 8px 0;
}
.games_list_tab_number {
margin-left: auto;
}
.games_list_tab.active {
box-shadow: unset;
border-radius: 10px;
background: #2c4056 !important;
position: sticky;
top: 0;
z-index: 10;
}
.games_list_tab:hover,
a.games_list_tab:hover {
background: #4e7297;
border-radius: 10px;
}
.games_list_tab:hover .games_list_tab_name {
text-decoration: none;
}
`);