Greasy Fork is available in English.
Adds buttons to withdraw/deposit preset values in property vault
// ==UserScript==
// @name Quick Access Vault
// @namespace https://torn.com/
// @version 1.20
// @description Adds buttons to withdraw/deposit preset values in property vault
// @author Scythe [2045424]
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @match *://*.torn.com/properties.php*
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'vaultPresets';
const DEFAULT_PRESETS = '1m,5m,10m,25m,50m';
const AMOUNT_UNITS = { k: 1e3, m: 1e6, b: 1e9 };
const SELECTORS = {
propertiesPage: '#properties-page-wrap',
propertyOption: '.property-option',
vaultWrap: 'div.vault-wrap',
vaultInput: '.input-money-group input[type="text"]',
moneyData: '[data-money]',
presetContainer: '.preset-container-wrap',
presetButtons: '.preset-buttons',
configTray: '.config-tray'
};
const state = {
amounts: GM_getValue(STORAGE_KEY, DEFAULT_PRESETS).split(','),
isShiftPressed: false,
configTrayOpen: false
};
function init() {
injectStyles();
setupKeyboardListeners();
observePropertyChanges();
}
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
.shift-active .preset-btn:hover {
border-color: #00ffaa !important;
box-shadow: 0 0 10px #00ffaa !important;
transition: all 0.2s ease;
}
.config-tray {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
padding: 0 10px;
}
.config-tray.open {
max-height: 200px;
padding: 10px;
}
.config-tray-content {
padding: 0 5px;
}
.config-tray input {
width: 100%;
padding: 8px;
margin: 5px 0;
background: transparent;
border: 1px solid #3d3d3d;
color: inherit;
border-radius: 3px;
font-family: inherit;
box-sizing: border-box;
}
.config-tray input:focus {
outline: none;
border-color: #555;
}
.config-tray-buttons {
display: flex;
gap: 5px;
margin-top: 10px;
}
.config-tray-buttons button {
flex: 1;
}
`;
document.head.appendChild(style);
}
function setupKeyboardListeners() {
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
}
function handleKeyDown(e) {
if (e.key === 'Shift' && !state.isShiftPressed) {
state.isShiftPressed = true;
toggleShiftClass(true);
}
if (!e.ctrlKey && !e.altKey && !e.metaKey) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
const codeMatch = e.code.match(/^Digit(\d)$/);
if (codeMatch) {
const digit = codeMatch[1];
const index = digit === '0' ? 9 : parseInt(digit) - 1;
if (index < state.amounts.length) {
e.preventDefault();
handlePresetClick(state.amounts[index], e.shiftKey);
}
}
}
}
function handleKeyUp(e) {
if (e.key === 'Shift') {
state.isShiftPressed = false;
toggleShiftClass(false);
}
}
function toggleShiftClass(add) {
const containers = document.querySelectorAll(SELECTORS.presetButtons);
containers.forEach(el => el.classList.toggle('shift-active', add));
}
function observePropertyChanges() {
const propertiesPage = document.querySelector(SELECTORS.propertiesPage);
if (!propertiesPage) return;
const observer = new MutationObserver(mutations => {
const hasNewPropertyOption = mutations.some(mutation =>
Array.from(mutation.addedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
node.classList?.contains('property-option')
)
);
if (hasNewPropertyOption) {
addButtons();
}
});
observer.observe(propertiesPage, { childList: true, subtree: true });
}
function parseAmount(amt) {
const match = amt.toLowerCase().trim().match(/^(\d+(?:\.\d+)?)([kmb])?$/);
if (!match) return 0;
const value = parseFloat(match[1]);
const multiplier = AMOUNT_UNITS[match[2]] || 1;
return Math.floor(value * multiplier);
}
function getCurrentCash() {
const moneyElem = document.querySelector(SELECTORS.moneyData);
return moneyElem ? parseInt(moneyElem.getAttribute('data-money')) || 0 : 0;
}
function setVaultValue(value) {
const vaultInputs = document.querySelectorAll(SELECTORS.vaultInput);
vaultInputs.forEach(input => {
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
});
}
function handlePresetClick(amount, isShiftClick) {
let targetValue = parseAmount(amount);
if (isShiftClick) {
const currentCash = getCurrentCash();
targetValue = Math.max(0, targetValue - currentCash);
}
setVaultValue(targetValue);
}
function toggleConfigTray(tray) {
state.configTrayOpen = !state.configTrayOpen;
tray.classList.toggle('open', state.configTrayOpen);
}
function handleConfigureSave(inputField, tray, buttonsContainer) {
const newPresets = inputField.value;
if (newPresets.trim() === '') return;
const parsedPresets = newPresets
.split(',')
.map(preset => preset.trim())
.filter(preset => preset.length > 0);
if (parsedPresets.length === 0) return;
state.amounts = parsedPresets;
GM_setValue(STORAGE_KEY, state.amounts.join(','));
renderPresetButtons(buttonsContainer, tray);
toggleConfigTray(tray);
}
function addButtons() {
if (document.querySelector(SELECTORS.presetContainer)) return;
const container = createElement('div', { class: 'preset-container-wrap' });
const title = createElement('div', {
class: 'title-black top-round m-top10',
role: 'heading',
'aria-level': '5'
}, 'Vault Presets');
const buttonsContainer = createElement('div', {
class: 'preset-buttons',
style: 'position: relative; display: flex; flex-wrap: wrap; align-items: flex-start; padding: 5px;'
});
const configTray = createElement('div', { class: 'config-tray' });
const configContent = createElement('div', { class: 'config-tray-content' });
const configInput = createElement('input', {
type: 'text',
placeholder: 'Enter comma-separated presets (e.g., 1m, 5m, 10m)'
});
configInput.value = state.amounts.join(',');
const configButtonsDiv = createElement('div', { class: 'config-tray-buttons' });
const saveButton = createElement('button', { class: 'torn-btn' }, 'Save');
const cancelButton = createElement('button', { class: 'torn-btn' }, 'Cancel');
saveButton.addEventListener('click', () => handleConfigureSave(configInput, configTray, buttonsContainer));
cancelButton.addEventListener('click', () => {
configInput.value = state.amounts.join(',');
toggleConfigTray(configTray);
});
configButtonsDiv.append(saveButton, cancelButton);
configContent.append(configInput, configButtonsDiv);
configTray.appendChild(configContent);
renderPresetButtons(buttonsContainer, configTray);
const bottomContainer = createElement('div', { class: 'cont-gray bottom-round' });
bottomContainer.appendChild(buttonsContainer);
bottomContainer.appendChild(configTray);
container.append(title, bottomContainer);
const vaultNode = document.querySelector(SELECTORS.vaultWrap);
if (vaultNode?.previousElementSibling) {
vaultNode.parentNode.insertBefore(container, vaultNode.previousElementSibling);
}
}
function renderPresetButtons(container, configTray) {
const buttons = container.querySelectorAll('.preset-btn, .config-btn-main');
buttons.forEach(btn => btn.remove());
const fragment = document.createDocumentFragment();
state.amounts.forEach((amount) => {
const button = createElement('button', {
class: 'torn-btn preset-btn',
style: 'margin: 5px;'
}, `$${amount}`);
button.addEventListener('click', (e) => {
handlePresetClick(amount, e.shiftKey);
});
fragment.appendChild(button);
});
const configButton = createElement('button', {
class: 'torn-btn config-btn-main',
style: 'margin-left: auto; margin-top: 5px;'
}, 'Configure Presets');
configButton.addEventListener('click', () => toggleConfigTray(configTray));
fragment.appendChild(configButton);
container.appendChild(fragment);
}
function createElement(type, attributes = {}, textContent = null) {
const el = document.createElement(type);
Object.entries(attributes).forEach(([key, value]) => {
el.setAttribute(key, value);
});
if (textContent) {
el.textContent = textContent;
}
return el;
}
init();
})();