Vault Sharing - Mobile Enhanced

Automatically keeps track of your vault transactions when vault sharing

// ==UserScript==
// @name         Vault Sharing - Mobile Enhanced
// @namespace    vault.sharing.mobile
// @version      2.1
// @description  Automatically keeps track of your vault transactions when vault sharing 
// @author       ANITABURN
// @match        https://www.torn.com/properties.php*
// @match        https://www.torn.com/properties.php
// @grant        none
// @run-at       document-end
// @license      MIT 
// ==/UserScript==

(function() {
'use strict';

const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent) ||
window.innerWidth <= 768 ||
('ontouchstart' in window) ||
navigator.maxTouchPoints > 0;

let localStorageKey = "vault_sharing_mobile:settings";
let playerName = "";

window.addEventListener("hashchange", (event) => {
if (event.newURL.indexOf("vault") > -1) {
setTimeout(startVaultSharing, 1000);
}
}, false);

if (window.location.href.indexOf("vault") > -1) {
setTimeout(startVaultSharing, 1000);
}

function startVaultSharing() {
console.log('Starting vault sharing on mobile:', isMobile);

try {
let wsData = document.querySelector("#websocketConnectionData");
if (wsData) {
playerName = JSON.parse(wsData.innerText).playername;
} else {
let userLinks = document.querySelectorAll('a[href*="/profiles.php?XID="]');
if (userLinks.length > 0) {
playerName = userLinks[0].textContent.trim();
}
}
} catch (e) {
console.log('Could not get player name:', e);
playerName = "You";
}

console.log('Player name:', playerName);

let { startTime, ownStartingBalance, spouseStartingBalance, playerDisplayName, spouseDisplayName } = JSON.parse(localStorage.getItem(localStorageKey)) || {
startTime: new Date().toISOString().slice(0, 16),
ownStartingBalance: 0,
spouseStartingBalance: 0,
playerDisplayName: playerName || "You",
spouseDisplayName: "Spouse"
};

let isDarkMode = true;

function parseTransaction(transactionItem) {
try {
let dateEl = transactionItem.querySelector(".transaction-date") || transactionItem.querySelector('[class*="date"]');
let timeEl = transactionItem.querySelector(".transaction-time") || transactionItem.querySelector('[class*="time"]');
let userEl = transactionItem.querySelector(".user.name") || transactionItem.querySelector('a[href*="XID="]');
let typeEl = transactionItem.querySelector(".type") || transactionItem.querySelector('[class*="type"]');
let amountEl = transactionItem.querySelector(".amount") || transactionItem.querySelector('[class*="amount"]');
let balanceEl = transactionItem.querySelector(".balance") || transactionItem.querySelector('[class*="balance"]');

if (!dateEl || !timeEl || !userEl || !typeEl || !amountEl) {
return null;
}

let date = dateEl.innerText.trim().split("/");
let time = timeEl.innerText.trim();
let datetime = new Date(Date.parse(`20${date[2]}-${date[1]}-${date[0]}T${time}Z`));

let userName = userEl.title ? userEl.title.split(" ")[0]: userEl.textContent.trim();
let type = typeEl.innerText.replace(/[^A-z]/g, "");
let amount = parseInt(amountEl.innerText.replace(/[^0-9]/g, ""));
let balance = balanceEl ? parseInt(balanceEl.innerText.replace(/[^0-9]/g, "")): 0;

return {
datetime: datetime,
name: userName,
type: type,
amount: amount,
originalBalance: balance
};
} catch (e) {
console.log('Error parsing transaction:', e);
return null;
}
}

function readTransactionData() {
let transactionData = {};

let selectors = [
".vault-trans-wrap ul li[transaction_id]",
".vault-trans-wrap li",
'[class*="vault"] li',
'[class*="transaction"]'
];

let transactionListItems = [];
for (let selector of selectors) {
transactionListItems = document.querySelectorAll(selector);
if (transactionListItems.length > 0) break;
}

console.log(`Found ${transactionListItems.length} transaction items`);

for (let item of transactionListItems) {
let transactionId = item.getAttribute("transaction_id") || item.getAttribute("data-id") || Date.now() + Math.random();
let parsedTransaction = parseTransaction(item);
if (parsedTransaction) {
transactionData[transactionId] = parsedTransaction;
}
}

return transactionData;
}

function formatBalance(balance) {
return (balance < 0 ? "-": "") + "$" + Math.abs(balance).toLocaleString();
}

function showBalances(ownBalance, spouseBalance) {
let ownEl = document.getElementById("vault-sharing-own-balance");
let spouseEl = document.getElementById("vault-sharing-spouse-balance");
let totalEl = document.getElementById("vault-sharing-total-balance");

if (ownEl) ownEl.innerText = formatBalance(ownBalance);
if (spouseEl) spouseEl.innerText = formatBalance(spouseBalance);
if (totalEl) totalEl.innerText = formatBalance(ownBalance + spouseBalance);
}

function calculateBalances() {
let transactionData = readTransactionData();
let startTimeAsDate = new Date(Date.parse(startTime + "Z"));
let ownBalance = ownStartingBalance;
let spouseBalance = spouseStartingBalance;

console.log('Calculating balances from:', startTimeAsDate);
console.log('Starting balances - Own:', ownBalance, 'Spouse:', spouseBalance);

let relevantTransactions = Object.entries(transactionData)
.filter(e => e[1].datetime > startTimeAsDate)
.sort((a, b) => a[1].datetime - b[1].datetime);

console.log(`Processing ${relevantTransactions.length} relevant transactions`);

for (let [id, transaction] of relevantTransactions) {
let amount = parseInt(transaction.type === "Deposit" ? transaction.amount: -transaction.amount);

if (transaction.name === playerName || transaction.name.toLowerCase().includes(playerName.toLowerCase())) {
ownBalance += amount;
console.log(`${transaction.type} by ${transaction.name}: ${amount}, new balance: ${ownBalance}`);
} else {
spouseBalance += amount;
console.log(`${transaction.type} by ${transaction.name}: ${amount}, spouse balance: ${spouseBalance}`);
}
}

return { ownBalance, spouseBalance };
}

function calculateAndShowBalances() {
let result = calculateBalances();
showBalances(result.ownBalance, result.spouseBalance);
}

function saveSettings() {
let ownBalanceInput = document.getElementById("vault-sharing-own-start-balance");
let spouseBalanceInput = document.getElementById("vault-sharing-spouse-start-balance");
let startTimeInput = document.getElementById("vault-sharing-start-time");
let playerNameInput = document.getElementById("vault-sharing-player-name");
let spouseNameInput = document.getElementById("vault-sharing-spouse-name");

if (!ownBalanceInput || !spouseBalanceInput || !startTimeInput) return;

let ownBalanceSetting = Number(ownBalanceInput.value.replace(/[^0-9]/g, ""));
let spouseBalanceSetting = Number(spouseBalanceInput.value.replace(/[^0-9]/g, ""));
let startTimeSetting = startTimeInput.value;
let playerNameSetting = playerNameInput ? playerNameInput.value.trim(): playerDisplayName;
let spouseNameSetting = spouseNameInput ? spouseNameInput.value.trim(): spouseDisplayName;

localStorage.setItem(localStorageKey, JSON.stringify({
startTime: startTimeSetting,
ownStartingBalance: ownBalanceSetting,
spouseStartingBalance: spouseBalanceSetting,
playerDisplayName: playerNameSetting,
spouseDisplayName: spouseNameSetting
}));

startTime = startTimeSetting;
ownStartingBalance = ownBalanceSetting;
spouseStartingBalance = spouseBalanceSetting;
playerDisplayName = playerNameSetting;
spouseDisplayName = spouseNameSetting;

toggleEditMode(false);

showStatus('Settings saved!', false);
}

function handleSave() {
saveSettings();
calculateAndShowBalances();
}

function handleSetCurrent() {
let result = calculateBalances();
let ownBalanceInput = document.getElementById("vault-sharing-own-start-balance");
let spouseBalanceInput = document.getElementById("vault-sharing-spouse-start-balance");
let startTimeInput = document.getElementById("vault-sharing-start-time");

if (ownBalanceInput) ownBalanceInput.value = result.ownBalance.toLocaleString();
if (spouseBalanceInput) spouseBalanceInput.value = result.spouseBalance.toLocaleString();
if (startTimeInput) startTimeInput.value = new Date().toISOString().slice(0, 16);

showStatus('Updated to current values!', false);
}

function toggleEditMode(forceState = null) {
let editSection = document.getElementById("vault-sharing-edit-section");
let editButton = document.getElementById("vault-sharing-edit-btn");
let playerNameEl = document.getElementById("vault-sharing-player-display-name");
let spouseNameEl = document.getElementById("vault-sharing-spouse-display-name");

let currentlyEditing = editSection && editSection.style.display !== 'none';
let shouldEdit = forceState !== null ? forceState: !currentlyEditing;

if (editSection) {
editSection.style.display = shouldEdit ? 'block': 'none';
}
if (editButton) {
editButton.textContent = shouldEdit ? 'Cancel': 'Edit';
editButton.style.background = shouldEdit ? '#68acff': '#68ebff';
}

if (playerNameEl) playerNameEl.textContent = playerDisplayName;
if (spouseNameEl) spouseNameEl.textContent = spouseDisplayName;
}

function showStatus(message, isError = false) {
let statusEl = document.getElementById("vault-sharing-status");
if (statusEl) {
statusEl.textContent = message;
statusEl.style.display = 'block';
statusEl.style.color = isError ? '#68acff': '#e168ff';
statusEl.style.background = isError ? 'rgba(255,155,200,0.1)': 'rgba(155,183,255,0.1)';
statusEl.style.padding = '8px';
statusEl.style.borderRadius = '4px';
statusEl.style.marginTop = '10px';

setTimeout(() => {
statusEl.style.display = 'none';
}, isMobile ? 5000: 3000);
}
}

function formatMoneyInput(event) {
let value = event.target.value.replace(/[^\d]/g, '');
if (value) {
event.target.value = parseInt(value).toLocaleString();
}
}

function addUI() {
let existing = document.getElementById("vault-sharing-container");
if (existing) existing.remove();

let mobileStyles = isMobile ? `
padding: 10px;
margin: 5px;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
`: `
padding: 8px;
margin: 3px 0;
`;

let html = `
<div id="vault-sharing-container" style="
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 6px;
${mobileStyles}
position: relative;
color: #ddd;
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h4 style="margin: 0; color: #ddd; font-size: ${isMobile ? '16px': '14px'};">Vault Sharing</h4>
<div style="display: flex; gap: 4px;">
<button id="vault-sharing-edit-btn" style="
background: #68ebff;
color: white;
border: none;
padding: ${isMobile ? '6px 10px': '4px 8px'};
border-radius: 3px;
cursor: pointer;
font-size: ${isMobile ? '12px': '11px'};
min-height: ${isMobile ? '32px': '24px'};
">Edit</button>
</div>
</div>

<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<div style="flex: 1; background: #4a4a4a; padding: ${isMobile ? '8px': '6px'}; border-radius: 4px; text-align: center; border-left: 3px solid #e168ff;">
<div style="font-size: ${isMobile ? '11px': '10px'}; color: #aaa; margin-bottom: 2px;">
<span id="vault-sharing-player-display-name">${playerDisplayName}</span>
</div>
<div id="vault-sharing-own-balance" style="font-size: ${isMobile ? '14px': '12px'}; color: #e168ff; font-weight: bold;">Calculating...</div>
</div>

<div style="flex: 1; background: #4a4a4a; padding: ${isMobile ? '8px': '6px'}; border-radius: 4px; text-align: center; border-left: 3px solid #68acff;">
<div style="font-size: ${isMobile ? '11px': '10px'}; color: #aaa; margin-bottom: 2px;">
<span id="vault-sharing-spouse-display-name">${spouseDisplayName}</span>
</div>
<div id="vault-sharing-spouse-balance" style="font-size: ${isMobile ? '14px': '12px'}; color: #68acff; font-weight: bold;">Calculating...</div>
</div>
</div>

<div style="text-align: center; padding: ${isMobile ? '6px': '4px'}; background: #4a4a4a; border-radius: 4px; border-left: 3px solid #68ebff;">
<span style="font-size: ${isMobile ? '11px': '10px'}; color: #aaa;">Total: </span>
<span id="vault-sharing-total-balance" style="font-size: ${isMobile ? '14px': '12px'}; color: #68ebff; font-weight: bold;">$0</span>
</div>

<div id="vault-sharing-edit-section" style="display: none; margin-top: 10px; padding: 10px; background: #4a4a4a; border-radius: 4px; border: 1px solid #555;">

<div style="display: ${isMobile ? 'block': 'flex'}; gap: 8px; margin-bottom: 10px;">
<div style="flex: 1; margin-bottom: ${isMobile ? '8px': '0'};">
<label style="display: block; margin-bottom: 2px; font-size: ${isMobile ? '12px': '11px'}; color: #ddd;">Your Name:</label>
<input id="vault-sharing-player-name" type="text" value="${playerDisplayName}" style="
width: 100%;
padding: ${isMobile ? '6px': '4px'};
border: 1px solid #666;
border-radius: 3px;
font-size: ${isMobile ? '14px': '12px'};
box-sizing: border-box;
background: #333;
color: #ddd;
">
</div>
<div style="flex: 1;">
<label style="display: block; margin-bottom: 2px; font-size: ${isMobile ? '12px': '11px'}; color: #ddd;">Spouse Name:</label>
<input id="vault-sharing-spouse-name" type="text" value="${spouseDisplayName}" style="
width: 100%;
padding: ${isMobile ? '6px': '4px'};
border: 1px solid #666;
border-radius: 3px;
font-size: ${isMobile ? '14px': '12px'};
box-sizing: border-box;
background: #333;
color: #ddd;
">
</div>
</div>

<div style="display: ${isMobile ? 'block': 'flex'}; gap: 8px; margin-bottom: 10px;">
<div style="flex: 1; margin-bottom: ${isMobile ? '8px': '0'};">
<label style="display: block; margin-bottom: 2px; font-size: ${isMobile ? '12px': '11px'}; color: #ddd;">Your Starting Balance:</label>
<input id="vault-sharing-own-start-balance" type="text" value="${ownStartingBalance.toLocaleString()}" style="
width: 100%;
padding: ${isMobile ? '6px': '4px'};
border: 1px solid #666;
border-radius: 3px;
font-size: ${isMobile ? '14px': '12px'};
box-sizing: border-box;
background: #333;
color: #ddd;
">
</div>
<div style="flex: 1;">
<label style="display: block; margin-bottom: 2px; font-size: ${isMobile ? '12px': '11px'}; color: #ddd;">Spouse Starting Balance:</label>
<input id="vault-sharing-spouse-start-balance" type="text" value="${spouseStartingBalance.toLocaleString()}" style="
width: 100%;
padding: ${isMobile ? '6px': '4px'};
border: 1px solid #666;
border-radius: 3px;
font-size: ${isMobile ? '14px': '12px'};
box-sizing: border-box;
background: #333;
color: #ddd;
">
</div>
</div>

<div style="margin-bottom: 10px;">
<label style="display: block; margin-bottom: 2px; font-size: ${isMobile ? '12px': '11px'}; color: #ddd;">Start Date & Time:</label>
<input id="vault-sharing-start-time" type="datetime-local" value="${startTime}" style="
width: 100%;
padding: ${isMobile ? '6px': '4px'};
border: 1px solid #666;
border-radius: 3px;
font-size: ${isMobile ? '14px': '12px'};
box-sizing: border-box;
background: #333;
color: #ddd;
">
</div>

<div style="display: ${isMobile ? 'block': 'flex'}; gap: 6px;">
<button id="vault-sharing-save" style="
background: #e168ff;
color: white;
border: none;
padding: ${isMobile ? '8px 12px': '6px 10px'};
border-radius: 3px;
cursor: pointer;
font-size: ${isMobile ? '12px': '11px'};
flex: 1;
margin-bottom: ${isMobile ? '6px': '0'};
min-height: ${isMobile ? '36px': '28px'};
">Save</button>

<button id="vault-sharing-update" style="
background: #68acff;
color: white;
border: none;
padding: ${isMobile ? '8px 12px': '6px 10px'};
border-radius: 3px;
cursor: pointer;
font-size: ${isMobile ? '12px': '11px'};
flex: 1;
min-height: ${isMobile ? '36px': '28px'};
">Set to Current</button>
</div>

<div id="vault-sharing-status" style="display: none; margin-top: 8px; font-size: ${isMobile ? '12px': '11px'};"></div>
</div>
</div>
`;

let insertTarget = document.querySelector(".vault-trans-wrap") ||
document.querySelector('[class*="vault-trans"]') ||
document.querySelector('[class*="property-option"]') ||
document.querySelector('#properties-page-wrap');
if (insertTarget) {
insertTarget.insertAdjacentHTML("beforebegin", html);

document.getElementById("vault-sharing-save")?.addEventListener("click", handleSave);
document.getElementById("vault-sharing-update")?.addEventListener("click", handleSetCurrent);
document.getElementById("vault-sharing-edit-btn")?.addEventListener("click", () => toggleEditMode());

['vault-sharing-own-start-balance', 'vault-sharing-spouse-start-balance'].forEach(id => {
let input = document.getElementById(id);
if (input) {
input.addEventListener('input', formatMoneyInput);

if (isMobile) {
input.addEventListener('focus', () => {
document.querySelector('meta[name="viewport"]')?.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
});

input.addEventListener('blur', () => {
document.querySelector('meta[name="viewport"]')?.setAttribute('content', 'width=device-width, initial-scale=1.0');
});
}
}
});

toggleEditMode(false);

console.log('UI added successfully');
return true;
} else {
console.log('Could not find insertion target');
return false;
}
}

function setupObservers() {
let mutationConfig = { attributes: false, childList: true, subtree: true };

let transactionCallback = (mutationList, observer) => {
calculateAndShowBalances();
};

let initialCallback = (mutationList, observer) => {
for (let mutation of mutationList) {
if (mutation.type === "childList") {
for (let node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList?.contains("property-option") ||
node.querySelector?.('.vault-trans-wrap') ||
node.classList?.contains("vault-trans-wrap")) {

if (addUI()) {
calculateAndShowBalances();

let transactionList = document.querySelector(".vault-trans-wrap ul") ||
document.querySelector('[class*="vault-trans"] ul');

if (transactionList) {
let transactionObserver = new MutationObserver(transactionCallback);
transactionObserver.observe(transactionList, mutationConfig);
}
}
}
}
}
}
}
};

let pageWrap = document.getElementById("properties-page-wrap");
if (pageWrap) {
let pageObserver = new MutationObserver(initialCallback);
pageObserver.observe(pageWrap, mutationConfig);
}

let vaultTransWrap = document.querySelector(".vault-trans-wrap");
if (vaultTransWrap) {
if (addUI()) {
calculateAndShowBalances();

let transactionList = vaultTransWrap.querySelector("ul");
if (transactionList) {
let transactionObserver = new MutationObserver(transactionCallback);
transactionObserver.observe(transactionList, mutationConfig);
}
}
}
}

setupObservers();
}

if (isMobile) {
let style = document.createElement('style');
style.textContent = `
#vault-sharing-container input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}

#vault-sharing-container button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}

#vault-sharing-container button:active {
transform: scale(0.98);
}

#vault-sharing-container input:focus {
border-color: #e94560 !important;
}
`;
document.head.appendChild(style);
}

})();