Greasy Fork is available in English.
Fully customizable keyboard hotkeys for fast navigation around Torn. Open settings with Alt+, or the floating ⚙️ button.
// ==UserScript==
// @name Torn Quick-Nav Hotkeys
// @namespace https://www.torn.com
// @version 1.5.3
// @description Fully customizable keyboard hotkeys for fast navigation around Torn. Open settings with Alt+, or the floating ⚙️ button.
// @author Ashbrak
// @match https://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
/* eslint-disable no-multi-spaces */
(function () {
'use strict';
// ─── Default preset pages ───────────────────────────────────────────────────
const PRESET_PAGES = [
{ label: 'Home', url: '/index.php' },
{ label: 'Gym', url: '/gym.php' },
{ label: 'Travel', url: '/page.php?sid=travel' },
{ label: 'Hospital', url: '/hospitalview.php' },
{ label: 'Jail', url: '/jailview.php' },
{ label: 'Crimes', url: '/crimes.php' },
{ label: 'Faction', url: '/factions.php?step=your' },
{ label: 'Items', url: '/item.php' },
{ label: 'City / Map', url: '/city.php' },
{ label: 'Properties', url: '/properties.php' },
{ label: 'Education', url: '/education.php' },
{ label: 'Job / Company', url: '/companies.php' },
{ label: 'Forums', url: '/forums.php' },
{ label: 'Stock Market', url: '/page.php?sid=stocks' },
{ label: 'Bank', url: '/bank.php' },
{ label: 'Casino', url: '/casino.php' },
{ label: 'View Player Profile',url: '/profiles.php?XID=__USERID__', needsUserId: true },
{ label: 'Attack Player', url: '/loader.php?sid=attack&user2ID=__USERID__', needsUserId: true },
];
const MODIFIERS = ['None (single key)', 'Alt', 'Ctrl', 'Shift', 'Alt+Shift', 'Ctrl+Shift'];
const SETTINGS_HOTKEY = { modifier: 'Alt', key: ',' };
// ─── Load / Save settings ───────────────────────────────────────────────────
function loadSettings() {
const raw = GM_getValue('quicknav_settings', null);
if (raw) return JSON.parse(raw);
return {
showButton: true,
allEnabled: true,
hotkeys: [], // [{ label, url, modifier, key, enabled }]
};
}
function saveSettings(s) {
GM_setValue('quicknav_settings', JSON.stringify(s));
}
let settings = loadSettings();
// ─── Styles ─────────────────────────────────────────────────────────────────
GM_addStyle(`
#tqn-fab {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999999;
width: 42px;
height: 42px;
border-radius: 50%;
background: #2c2c2c;
color: #fff;
font-size: 20px;
border: 2px solid #555;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
transition: background 0.2s;
user-select: none;
}
#tqn-fab:hover { background: #444; }
#tqn-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 1000000;
display: flex;
align-items: center;
justify-content: center;
}
#tqn-modal {
background: #1e1e1e;
color: #e0e0e0;
border-radius: 10px;
width: min(680px, 95vw);
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 40px rgba(0,0,0,0.7);
font-family: Arial, sans-serif;
font-size: 14px;
overflow: hidden;
}
#tqn-modal-header {
padding: 16px 20px;
background: #141414;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
#tqn-modal-header h2 {
margin: 0;
font-size: 16px;
color: #fff;
}
#tqn-modal-header span {
font-size: 12px;
color: #888;
}
#tqn-modal-body {
overflow-y: auto;
padding: 16px 20px;
flex: 1;
}
#tqn-modal-footer {
padding: 12px 20px;
background: #141414;
border-top: 1px solid #333;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
gap: 10px;
}
.tqn-section-title {
font-size: 11px;
text-transform: uppercase;
color: #888;
letter-spacing: 0.08em;
margin: 0 0 10px 0;
}
.tqn-hotkey-row {
display: grid;
grid-template-columns: 20px 1fr 130px 70px 32px;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.tqn-hotkey-row.tqn-disabled input,
.tqn-hotkey-row.tqn-disabled select {
opacity: 0.35;
}
.tqn-hotkey-row input, .tqn-hotkey-row select {
background: #2a2a2a;
border: 1px solid #444;
border-radius: 5px;
color: #e0e0e0;
padding: 5px 8px;
font-size: 13px;
width: 100%;
box-sizing: border-box;
}
.tqn-hotkey-row input:focus, .tqn-hotkey-row select:focus {
outline: none;
border-color: #e3a800;
}
.tqn-key-input {
text-align: center;
font-weight: bold;
text-transform: uppercase;
}
.tqn-btn-del {
background: #3a1a1a;
border: 1px solid #622;
border-radius: 5px;
color: #e06060;
cursor: pointer;
font-size: 16px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tqn-btn-del:hover { background: #511; }
.tqn-add-row {
display: grid;
grid-template-columns: 80px 1fr;
gap: 8px;
margin-bottom: 16px;
}
.tqn-add-row select, .tqn-add-row input {
background: #2a2a2a;
border: 1px solid #444;
border-radius: 5px;
color: #e0e0e0;
padding: 5px 8px;
font-size: 13px;
width: 100%;
box-sizing: border-box;
}
.tqn-btn {
padding: 7px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: bold;
}
.tqn-btn-primary {
background: #e3a800;
color: #111;
}
.tqn-btn-primary:hover { background: #f0b800; }
.tqn-btn-secondary {
background: #2a2a2a;
color: #ccc;
border: 1px solid #444;
}
.tqn-btn-secondary:hover { background: #333; }
.tqn-btn-add {
background: #1a3a1a;
color: #6de06d;
border: 1px solid #3a6a3a;
width: 100%;
margin-bottom: 16px;
}
.tqn-btn-add:hover { background: #243a24; }
.tqn-divider { border: none; border-top: 1px solid #333; margin: 16px 0; }
.tqn-toggle-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.tqn-toggle {
appearance: none;
width: 36px;
height: 20px;
background: #444;
border-radius: 10px;
position: relative;
cursor: pointer;
transition: background 0.2s;
flex-shrink: 0;
}
.tqn-toggle:checked { background: #e3a800; }
.tqn-toggle::after {
content: '';
position: absolute;
width: 14px; height: 14px;
background: #fff;
border-radius: 50%;
top: 3px; left: 3px;
transition: left 0.2s;
}
.tqn-toggle:checked::after { left: 19px; }
/* smaller variant used in hotkey rows */
.tqn-hk-enabled {
width: 20px !important;
height: 12px !important;
border-radius: 6px !important;
}
.tqn-hk-enabled::after {
width: 8px !important;
height: 8px !important;
top: 2px !important;
left: 2px !important;
}
.tqn-hk-enabled:checked::after {
left: 10px !important;
}
.tqn-col-headers {
display: grid;
grid-template-columns: 20px 1fr 130px 70px 32px;
gap: 8px;
font-size: 11px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 6px;
}
.tqn-toast {
position: fixed;
bottom: 80px;
right: 20px;
background: #2a2a2a;
color: #6de06d;
border: 1px solid #3a6a3a;
border-radius: 6px;
padding: 8px 16px;
font-size: 13px;
z-index: 1000001;
animation: tqn-fadein 0.2s ease;
}
@keyframes tqn-fadein { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
`);
// ─── Floating button (draggable) ─────────────────────────────────────────────
const fab = document.createElement('div');
fab.id = 'tqn-fab';
fab.textContent = '⚙️';
fab.title = 'Torn Quick-Nav Settings (Alt+,)';
// Restore saved position or default (higher up from bottom edge)
const savedPos = JSON.parse(GM_getValue('quicknav_fab_pos', 'null'));
if (savedPos) {
fab.style.left = savedPos.x + 'px';
fab.style.top = savedPos.y + 'px';
fab.style.right = 'auto';
fab.style.bottom = 'auto';
} else {
fab.style.bottom = '80px';
fab.style.right = '20px';
}
fab.style.display = settings.showButton ? 'flex' : 'none';
document.body.appendChild(fab);
// Drag logic — distinguishes click vs drag so modal still opens on click
let dragging = false, dragOffX = 0, dragOffY = 0, dragMoved = false;
fab.addEventListener('mousedown', function (e) {
dragging = true;
dragMoved = false;
const rect = fab.getBoundingClientRect();
dragOffX = e.clientX - rect.left;
dragOffY = e.clientY - rect.top;
fab.style.transition = 'none';
fab.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', function (e) {
if (!dragging) return;
dragMoved = true;
const x = Math.max(0, Math.min(window.innerWidth - fab.offsetWidth, e.clientX - dragOffX));
const y = Math.max(0, Math.min(window.innerHeight - fab.offsetHeight, e.clientY - dragOffY));
fab.style.left = x + 'px';
fab.style.top = y + 'px';
fab.style.right = 'auto';
fab.style.bottom = 'auto';
});
document.addEventListener('mouseup', function () {
if (!dragging) return;
dragging = false;
fab.style.cursor = 'pointer';
if (dragMoved) {
GM_setValue('quicknav_fab_pos', JSON.stringify({
x: parseInt(fab.style.left), y: parseInt(fab.style.top)
}));
}
});
fab.addEventListener('click', function () { if (!dragMoved) openModal(); });
// ─── Modal ───────────────────────────────────────────────────────────────────
function openModal() {
if (document.getElementById('tqn-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'tqn-overlay';
overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); });
overlay.innerHTML = `
<div id="tqn-modal">
<div id="tqn-modal-header">
<div style="display:flex; flex-direction:column; gap:3px;">
<h2 style="margin:0; font-size:16px; color:#fff;">⚡ Torn Quick-Nav Hotkeys</h2>
<a href="https://www.torn.com/profiles.php?XID=3888401" target="_blank" style="font-size:11px; color:#e3a800; text-decoration:none; opacity:0.6; transition:opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.6'">by Ashbrak</a>
</div>
<span>Alt+, to open anytime</span>
</div>
<div id="tqn-modal-body">
<div class="tqn-section-title">General</div>
<div class="tqn-toggle-row">
<input type="checkbox" class="tqn-toggle" id="tqn-show-btn" ${settings.showButton ? 'checked' : ''}>
<label for="tqn-show-btn">Show floating ⚙️ button on page</label>
</div>
<div class="tqn-toggle-row">
<input type="checkbox" class="tqn-toggle" id="tqn-all-enabled" ${settings.allEnabled !== false ? 'checked' : ''}>
<label for="tqn-all-enabled">Hotkeys active <span style="color:#666; font-weight:normal; font-size:12px;">(master switch)</span></label>
</div>
<hr class="tqn-divider">
<div class="tqn-section-title">Add a hotkey</div>
<div class="tqn-add-row">
<select id="tqn-add-type">
<option value="preset">Preset</option>
<option value="custom">Custom URL</option>
</select>
<select id="tqn-add-preset">
${PRESET_PAGES.map(p => `<option value="${p.url}">${p.label}${p.needsUserId ? ' 🔑' : ''}</option>`).join('')}
</select>
</div>
<div id="tqn-custom-url-row" style="display:none; margin-bottom:8px;">
<input type="text" id="tqn-custom-label" placeholder="Label (e.g. My Page)" style="margin-bottom:6px;">
<input type="text" id="tqn-custom-url" placeholder="Full URL or path (e.g. /crimes.php or https://www.torn.com/...)">
</div>
<button class="tqn-btn tqn-btn-add" id="tqn-add-btn">+ Add Hotkey</button>
<hr class="tqn-divider">
<div class="tqn-section-title">Your hotkeys</div>
<div class="tqn-col-headers">
<span>On</span><span>Page / URL</span><span>Modifier</span><span>Key</span><span></span>
</div>
<div id="tqn-hotkey-list"></div>
</div>
<div id="tqn-modal-footer">
<div style="font-size:12px; color:#666;">Changes auto-save</div>
<div style="display:flex; gap:8px;">
<button class="tqn-btn tqn-btn-secondary" id="tqn-close-btn">Close</button>
<button class="tqn-btn tqn-btn-primary" id="tqn-save-btn">Save & Close</button>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
renderHotkeyList();
// Type toggle
document.getElementById('tqn-add-type').addEventListener('change', function () {
const isCustom = this.value === 'custom';
document.getElementById('tqn-add-preset').style.display = isCustom ? 'none' : '';
document.getElementById('tqn-custom-url-row').style.display = isCustom ? 'grid' : 'none';
document.getElementById('tqn-custom-url-row').style.gridTemplateColumns = '1fr';
document.getElementById('tqn-custom-url-row').style.gap = '6px';
});
document.getElementById('tqn-add-btn').addEventListener('click', () => {
const type = document.getElementById('tqn-add-type').value;
let label, url;
if (type === 'preset') {
const sel = document.getElementById('tqn-add-preset');
const preset = PRESET_PAGES.find(p => p.url === sel.value);
url = preset.url;
label = preset.label;
if (preset.needsUserId) {
const uid = prompt(`Enter the Torn user ID for "${label}":`);
if (!uid || !uid.trim()) return;
url = url.replace('__USERID__', uid.trim());
label = `${label} (${uid.trim()})`;
}
} else {
label = document.getElementById('tqn-custom-label').value.trim();
url = document.getElementById('tqn-custom-url').value.trim();
if (!label || !url) { alert('Please fill in both label and URL.'); return; }
}
settings.hotkeys.push({ label, url, modifier: 'None (single key)', key: '', enabled: true });
saveSettings(settings);
renderHotkeyList();
});
document.getElementById('tqn-show-btn').addEventListener('change', function () {
settings.showButton = this.checked;
fab.style.display = this.checked ? 'flex' : 'none';
saveSettings(settings);
});
document.getElementById('tqn-all-enabled').addEventListener('change', function () {
settings.allEnabled = this.checked;
saveSettings(settings);
});
document.getElementById('tqn-close-btn').addEventListener('click', closeModal);
document.getElementById('tqn-save-btn').addEventListener('click', () => {
collectHotkeyEdits();
saveSettings(settings);
showToast('Hotkeys saved!');
closeModal();
});
}
function closeModal() {
const overlay = document.getElementById('tqn-overlay');
if (overlay) overlay.remove();
}
function renderHotkeyList() {
const list = document.getElementById('tqn-hotkey-list');
if (!list) return;
list.innerHTML = '';
if (settings.hotkeys.length === 0) {
list.innerHTML = '<div style="color:#666; font-size:13px; padding: 8px 0;">No hotkeys yet. Add one above!</div>';
return;
}
settings.hotkeys.forEach((hk, i) => {
const enabled = hk.enabled !== false;
const row = document.createElement('div');
row.className = 'tqn-hotkey-row' + (enabled ? '' : ' tqn-disabled');
row.dataset.index = i;
row.innerHTML = `
<input type="checkbox" class="tqn-toggle tqn-hk-enabled" ${enabled ? 'checked' : ''} title="Enable/disable this hotkey">
<input type="text" class="tqn-hk-label" value="${escHtml(hk.label)}" placeholder="Label or URL" title="${escHtml(hk.url)}">
<select class="tqn-hk-modifier">
${MODIFIERS.map(m => `<option value="${m}" ${hk.modifier === m ? 'selected' : ''}>${m}</option>`).join('')}
</select>
<input type="text" class="tqn-hk-key tqn-key-input" maxlength="1" value="${escHtml(hk.key)}" placeholder="key">
<button class="tqn-btn-del" data-del="${i}" title="Remove">×</button>
`;
list.appendChild(row);
});
list.querySelectorAll('.tqn-hk-enabled').forEach(cb => {
cb.addEventListener('change', function () {
const row = this.closest('.tqn-hotkey-row');
row.classList.toggle('tqn-disabled', !this.checked);
collectHotkeyEdits();
saveSettings(settings);
});
});
list.querySelectorAll('.tqn-btn-del').forEach(btn => {
btn.addEventListener('click', () => {
collectHotkeyEdits();
const idx = parseInt(btn.dataset.del);
settings.hotkeys.splice(idx, 1);
saveSettings(settings);
renderHotkeyList();
});
});
// Capture single keypress for key field
list.querySelectorAll('.tqn-hk-key').forEach(input => {
input.addEventListener('keydown', function (e) {
e.preventDefault();
if (e.key.length === 1) this.value = e.key.toUpperCase();
});
});
}
function collectHotkeyEdits() {
const list = document.getElementById('tqn-hotkey-list');
if (!list) return;
list.querySelectorAll('.tqn-hotkey-row').forEach(row => {
const i = parseInt(row.dataset.index);
if (!settings.hotkeys[i]) return;
settings.hotkeys[i].label = row.querySelector('.tqn-hk-label').value.trim();
settings.hotkeys[i].modifier = row.querySelector('.tqn-hk-modifier').value;
settings.hotkeys[i].key = row.querySelector('.tqn-hk-key').value.trim().toUpperCase();
settings.hotkeys[i].enabled = row.querySelector('.tqn-hk-enabled').checked;
});
}
// ─── Keyboard listener ───────────────────────────────────────────────────────
function isTyping() {
const el = document.activeElement;
if (!el) return false;
const tag = el.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
if (el.isContentEditable) return true;
if (tag === 'iframe') return true;
// Check if focus is inside a contenteditable (e.g. rich text editor)
if (el.closest && el.closest('[contenteditable="true"]')) return true;
return false;
}
document.addEventListener('keydown', function (e) {
if (isTyping()) return;
// Settings hotkey: Alt+,
if (e.altKey && e.key === ',') {
e.preventDefault();
if (document.getElementById('tqn-overlay')) closeModal();
else openModal();
return;
}
for (const hk of settings.hotkeys) {
if (!hk.key) continue;
if (hk.enabled === false) continue;
if (settings.allEnabled === false) continue;
const mod = hk.modifier || 'Alt';
const keyMatch = e.key.toUpperCase() === hk.key.toUpperCase();
const modMatch = checkModifier(e, mod);
if (keyMatch && modMatch) {
e.preventDefault();
window.location.href = resolveUrl(hk.url);
return;
}
}
});
function checkModifier(e, mod) {
switch (mod) {
case 'None (single key)': return !e.altKey && !e.ctrlKey && !e.shiftKey;
case 'Alt': return e.altKey && !e.ctrlKey && !e.shiftKey;
case 'Ctrl': return e.ctrlKey && !e.altKey && !e.shiftKey;
case 'Shift': return e.shiftKey && !e.altKey && !e.ctrlKey;
case 'Alt+Shift': return e.altKey && e.shiftKey && !e.ctrlKey;
case 'Ctrl+Shift': return e.ctrlKey && e.shiftKey && !e.altKey;
default: return false;
}
}
// ─── URL normalizer — accepts full URL or path, always navigates correctly ───
function resolveUrl(raw) {
raw = raw.trim();
if (raw.startsWith('https://www.torn.com')) return raw; // full URL — use as-is
if (raw.startsWith('http')) return raw; // external URL — use as-is
// path only — prepend origin
if (!raw.startsWith('/')) raw = '/' + raw;
return 'https://www.torn.com' + raw;
}
function showToast(msg) {
const t = document.createElement('div');
t.className = 'tqn-toast';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2000);
}
function escHtml(s) {
return String(s ?? '').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
}
})();