Mimics hosts file behavior with user-managed redirections via local storage.
// ==UserScript==
// @name Advanced Browser Hosts File (User Managed)
// @description Mimics hosts file behavior with user-managed redirections via local storage.
// @namespace http://tampermonkey.net/
// @version 1.1
// @author s3b
// @license MIT
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const STORAGE_KEY = 'browserHostsRules';
const MANAGEMENT_TRIGGER_KEY = 'Alt+Shift+H'; // Keyboard shortcut to open management UI
const MANAGEMENT_URL_PATH = '/_tmredirect'; // A unique path to trigger the management UI
// --- END CONFIGURATION ---
let hostsRules = {}; // Will be loaded from localStorage
// --- UI Styles ---
GM_addStyle(`
#tmHostsManagerOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 99999;
font-family: sans-serif;
color: #333;
}
#tmHostsManagerPanel {
background-color: #fff;
padding: 25px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 90%;
max-height: 90%;
overflow-y: auto;
position: relative;
}
#tmHostsManagerPanel h2 {
margin-top: 0;
color: #0056b3;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
margin-bottom: 20px;
}
#tmHostsManagerPanel label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
#tmHostsManagerPanel input[type="text"] {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
#tmHostsManagerPanel button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
transition: background-color 0.2s ease;
}
#tmHostsManagerPanel button:hover {
background-color: #0056b3;
}
#tmHostsManagerPanel button.delete {
background-color: #dc3545;
}
#tmHostsManagerPanel button.delete:hover {
background-color: #c82333;
}
#tmHostsManagerPanel button.close {
background-color: #6c757d;
}
#tmHostsManagerPanel button.close:hover {
background-color: #5a6268;
}
#tmHostsManagerPanel #rulesList {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 20px;
}
#tmHostsManagerPanel .rule-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px dashed #eee;
}
#tmHostsManagerPanel .rule-item:last-child {
border-bottom: none;
}
#tmHostsManagerPanel .rule-item span {
flex-grow: 1;
word-break: break-all;
}
#tmHostsManagerPanel .rule-item .actions {
margin-left: 15px;
display: flex;
gap: 5px;
}
#tmHostsManagerPanel .rule-item .actions button {
padding: 5px 10px;
font-size: 14px;
margin-right: 0;
}
#tmHostsManagerPanel .info-text {
font-size: 0.9em;
color: #666;
margin-bottom: 15px;
background-color: #f9f9f9;
border-left: 4px solid #007bff;
padding: 10px;
border-radius: 4px;
}
#tmHostsManagerPanel .close-button-top {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
font-weight: bold;
color: #aaa;
cursor: pointer;
line-height: 1;
padding: 0 5px;
}
#tmHostsManagerPanel .close-button-top:hover {
color: #666;
}
`);
// --- Core Functions ---
async function loadRules() {
try {
const storedRules = await GM_getValue(STORAGE_KEY, '{}');
hostsRules = JSON.parse(storedRules);
} catch (e) {
console.error('Tampermonkey Hosts: Failed to load rules from localStorage', e);
hostsRules = {}; // Reset if parsing fails
}
}
async function saveRules() {
try {
await GM_setValue(STORAGE_KEY, JSON.stringify(hostsRules));
console.log('Tampermonkey Hosts: Rules saved.');
} catch (e) {
console.error('Tampermonkey Hosts: Failed to save rules to localStorage', e);
}
}
function addRule(source, target) {
source = source.trim().toLowerCase();
target = target.trim();
if (source && target) {
hostsRules[source] = target;
saveRules();
renderRules(); // Re-render UI if it's open
return true;
}
return false;
}
function deleteRule(source) {
if (confirm(`Are you sure you want to delete the rule for "${source}"?`)) {
delete hostsRules[source];
saveRules();
renderRules(); // Re-render UI if it's open
}
}
function clearRuleInputs() {
document.getElementById('tmSourceInput').value = '';
document.getElementById('tmTargetInput').value = '';
}
// --- UI Management ---
function createManagementUI() {
if (document.getElementById('tmHostsManagerOverlay')) {
// UI already exists
return;
}
const overlay = document.createElement('div');
overlay.id = 'tmHostsManagerOverlay';
overlay.innerHTML = `
<div id="tmHostsManagerPanel">
<span class="close-button-top" id="tmCloseManagerBtnTop">×</span>
<h2>Browser Hosts Manager</h2>
<div class="info-text">
Add rules to redirect websites. Use 'BLOCK' as the target URL to redirect to <code>about:blank</code> (effectively blocking).
<br>Rules are matched against the hostname (e.g., <code>www.google.com</code>) or its subdomains.
</div>
<div>
<label for="tmSourceInput">Source Hostname (e.g., youtube.com, www.facebook.com):</label>
<input type="text" id="tmSourceInput" placeholder="Enter source domain (e.g., badsite.com)" required>
</div>
<div>
<label for="tmTargetInput">Target URL (e.g., https://newsite.com, or BLOCK):</label>
<input type="text" id="tmTargetInput" placeholder="Enter target URL or 'BLOCK'" required>
</div>
<button id="tmAddRuleBtn">Add/Update Rule</button>
<button id="tmCloseManagerBtnBottom" class="close">Close</button>
<div id="rulesList">
<h3>Current Rules:</h3>
<div id="tmRulesContainer">
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
// Event Listeners
document.getElementById('tmAddRuleBtn').addEventListener('click', () => {
const source = document.getElementById('tmSourceInput').value;
const target = document.getElementById('tmTargetInput').value;
if (addRule(source, target)) {
clearRuleInputs();
} else {
alert('Please provide both a source hostname and a target URL.');
}
});
document.getElementById('tmCloseManagerBtnTop').addEventListener('click', closeManagementUI);
document.getElementById('tmCloseManagerBtnBottom').addEventListener('click', closeManagementUI);
// Allow clicking outside the panel to close it
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
closeManagementUI();
}
});
renderRules();
}
function renderRules() {
const rulesContainer = document.getElementById('tmRulesContainer');
if (!rulesContainer) return; // UI not open
rulesContainer.innerHTML = ''; // Clear previous rules
if (Object.keys(hostsRules).length === 0) {
rulesContainer.innerHTML = '<p>No rules added yet. Add your first rule above!</p>';
return;
}
for (const source in hostsRules) {
if (hostsRules.hasOwnProperty(source)) {
const target = hostsRules[source];
const ruleItem = document.createElement('div');
ruleItem.classList.add('rule-item');
ruleItem.innerHTML = `
<span><b>${source}</b> → ${target === 'BLOCK' ? '<span style="color:red; font-weight:bold;">BLOCK</span>' : target}</span>
<div class="actions">
<button class="delete" data-source="${source}">Delete</button>
</div>
`;
rulesContainer.appendChild(ruleItem);
ruleItem.querySelector('.delete').addEventListener('click', (event) => {
deleteRule(event.target.dataset.source);
});
}
}
}
function openManagementUI() {
createManagementUI();
document.getElementById('tmHostsManagerOverlay').style.display = 'flex';
renderRules(); // Ensure rules are up-to-date when opening
}
function closeManagementUI() {
const overlay = document.getElementById('tmHostsManagerOverlay');
if (overlay) {
overlay.style.display = 'none';
}
}
// --- Main Script Logic ---
async function initializeAndRun() {
await loadRules(); // Load rules asynchronously at script start
const currentHostname = window.location.hostname;
const currentPath = window.location.pathname;
// Check for management UI trigger URL
if (currentPath === MANAGEMENT_URL_PATH) {
// Prevent the original page from loading
document.documentElement.innerHTML = ''; // Clear existing content
document.head.innerHTML = ''; // Clear head content
document.title = 'Browser Hosts Manager'; // Set a temporary title
openManagementUI();
// Stop further script execution for the main page logic
return;
}
// Check for redirection rules
for (const domain in hostsRules) {
if (hostsRules.hasOwnProperty(domain)) {
// More robust check: exact match or currentHostname ends with '.' + domain
// This covers www.example.com for example.com, but not example.com for an-example.com
if (currentHostname === domain || currentHostname.endsWith('.' + domain)) {
const redirectTarget = hostsRules[domain];
if (redirectTarget === 'BLOCK') {
// Redirect to about:blank to "block" the site
window.location.replace('about:blank');
// Prevent the original page from loading further
throw new Error('Site blocked by Tampermonkey script.');
} else {
// Redirect to a specific URL
window.location.replace(redirectTarget);
// Prevent the original page from loading further
throw new Error('Site redirected by Tampermonkey script.');
}
}
}
}
}
// Run the initialization and main logic
initializeAndRun();
// Add keyboard shortcut listener for convenience
document.addEventListener('keydown', (e) => {
const [modifier, key] = MANAGEMENT_TRIGGER_KEY.split('+');
if (e.altKey === (modifier === 'Alt') &&
e.shiftKey === (modifier === 'Shift') &&
e.ctrlKey === (modifier === 'Control') &&
e.key.toUpperCase() === key.toUpperCase()) {
e.preventDefault(); // Prevent default browser action
openManagementUI();
}
});
// Option to open management UI from Tampermonkey's menu
// This part is more for a context menu or similar, Tampermonkey doesn't have a direct GM_registerMenuCommand
// However, the keyboard shortcut or the specific URL will serve the purpose.
})();