Greasy Fork is available in English.
Мгновенная навигация с предзагрузкой страниц (НЕТ ЗАГРУЗКИ!)
// ==UserScript==
// @name FolderWaypoints Pro - Instant Navigation
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @version 8.2
// @description Мгновенная навигация с предзагрузкой страниц (НЕТ ЗАГРУЗКИ!)
// @namespace https://greasyfork.org/users/1560293
// ==/UserScript==
(function() {
'use strict';
// ========== КОНФИГУРАЦИЯ ==========
const CONFIG = {
MAX_FOLDERS: 50,
MAX_WAYPOINTS_PER_FOLDER: 200,
NOTIFICATION_DURATION: 2000,
CAPTURE_TIMEOUT: 10000,
STORAGE_KEYS: {
folders: 'folders_data_v4',
activeFolderId: 'active_folder_id',
panelVisible: 'panel_visible',
arrowPosition: 'arrow_position'
}
};
// ========== ХРАНЕНИЕ ==========
let folders = [];
let activeFolderId = null;
let panelVisible = false;
let currentView = 'folders';
let currentFolderId = null;
let isCapturingKey = false;
let currentCaptureId = null;
let captureTimeout = null;
let arrowPosition = { x: 15, y: 15 };
// Для предзагрузки
let prefetchedUrls = new Set();
let isPrefetching = false;
// ========== ФУНКЦИЯ МГНОВЕННОЙ ПРЕДЗАГРУЗКИ ==========
function instantPrefetch(url) {
if (!url || prefetchedUrls.has(url)) return;
// Способ 1: link rel="prefetch" (самый быстрый)
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
link.as = 'document';
document.head.appendChild(link);
// Способ 2: предзагрузка через fetch (для надежности)
if ('fetch' in window) {
fetch(url, {
mode: 'no-cors',
priority: 'low',
cache: 'force-cache'
}).catch(() => {});
}
prefetchedUrls.add(url);
console.log(`⚡ МГНОВЕННАЯ ПРЕДЗАГРУЗКА: ${url.substring(0, 60)}...`);
}
// Предзагружаем ВСЕ точки из папки
function prefetchAllFolderWaypoints(folderId) {
const folder = folders.find(f => f.id === folderId);
if (!folder) return;
console.log(`🚀 НАЧИНАЮ ПРЕДЗАГРУЗКУ папки: ${folder.name} (${folder.waypoints.length} точек)`);
folder.waypoints.forEach((waypoint, index) => {
// Предзагружаем с небольшой задержкой между запросами, чтобы не нагружать
setTimeout(() => {
instantPrefetch(waypoint.url);
}, index * 300);
});
}
// Когда выбрана новая активная папка - сразу предзагружаем все её точки
function setActiveFolderAndPrefetch(folderId) {
activeFolderId = folderId;
saveActiveFolder();
// МГНОВЕННО начинаем предзагрузку всех точек из этой папки
prefetchAllFolderWaypoints(activeFolderId);
renderFoldersView();
showNotification(`📌 Активна: ${folders.find(f => f.id === folderId)?.name} (предзагрузка начата)`, '#00ff88');
}
// При открытии папки - тоже предзагружаем
function openFolderAndPrefetch(folderId) {
currentFolderId = folderId;
currentView = 'waypoints';
// Предзагружаем точки из открытой папки
prefetchAllFolderWaypoints(folderId);
renderWaypointsView();
}
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
function getCurrentDomain() {
return window.location.hostname;
}
function getCurrentPageTitle() {
const h1 = document.querySelector('h1')?.textContent?.trim();
if (h1) return h1;
const title = document.querySelector('title')?.textContent?.trim();
if (title) return title.substring(0, 60);
return window.location.pathname.substring(0, 50) || 'Главная';
}
function formatHotkey(hotkey) {
if (!hotkey) return 'Не назначена';
const parts = [];
if (hotkey.ctrl) parts.push('Ctrl');
if (hotkey.alt) parts.push('Alt');
if (hotkey.shift) parts.push('Shift');
if (hotkey.meta) parts.push('Win');
if (hotkey.key) {
let key = hotkey.key;
const keyMap = {
' ': 'Space', 'Enter': 'Enter', 'Tab': 'Tab', 'Escape': 'Esc',
'ArrowUp': '↑', 'ArrowDown': '↓', 'ArrowLeft': '←', 'ArrowRight': '→',
'Delete': 'Del', 'Insert': 'Ins', 'Home': 'Home', 'End': 'End',
'PageUp': 'PgUp', 'PageDown': 'PgDn',
'F1': 'F1', 'F2': 'F2', 'F3': 'F3', 'F4': 'F4',
'F5': 'F5', 'F6': 'F6', 'F7': 'F7', 'F8': 'F8',
'F9': 'F9', 'F10': 'F10', 'F11': 'F11', 'F12': 'F12'
};
key = keyMap[key] || (key.length === 1 ? key.toUpperCase() : key);
parts.push(key);
}
return parts.join(' + ');
}
// ========== УПРАВЛЕНИЕ ДАННЫМИ ==========
function loadData() {
try {
const savedFolders = GM_getValue(CONFIG.STORAGE_KEYS.folders, '[]');
folders = JSON.parse(savedFolders);
activeFolderId = GM_getValue(CONFIG.STORAGE_KEYS.activeFolderId, null);
panelVisible = GM_getValue(CONFIG.STORAGE_KEYS.panelVisible, false);
const savedPosition = GM_getValue(CONFIG.STORAGE_KEYS.arrowPosition, null);
if (savedPosition) arrowPosition = JSON.parse(savedPosition);
if (folders.length === 0) createDefaultFolder();
if (activeFolderId && !folders.find(f => f.id === activeFolderId)) {
activeFolderId = folders[0]?.id || null;
saveActiveFolder();
}
// При загрузке сразу начинаем предзагружать активную папку
if (activeFolderId) {
setTimeout(() => prefetchAllFolderWaypoints(activeFolderId), 1000);
}
} catch (e) {
console.error('Ошибка загрузки:', e);
folders = [];
createDefaultFolder();
}
}
function createDefaultFolder() {
const defaultFolder = {
id: Date.now(),
name: 'Основная',
description: 'Основная папка',
icon: '📁',
color: '#667eea',
createdAt: new Date().toISOString(),
waypoints: [],
hotkeyMode: 'local'
};
folders.push(defaultFolder);
activeFolderId = defaultFolder.id;
saveFolders();
saveActiveFolder();
}
function saveFolders() {
GM_setValue(CONFIG.STORAGE_KEYS.folders, JSON.stringify(folders));
}
function saveActiveFolder() {
GM_setValue(CONFIG.STORAGE_KEYS.activeFolderId, activeFolderId);
}
// ========== СОЗДАНИЕ ТОЧЕК ==========
function createWaypointInFolder(folderId) {
const folder = folders.find(f => f.id === folderId);
if (!folder) return;
if (folder.waypoints.length >= CONFIG.MAX_WAYPOINTS_PER_FOLDER) {
showNotification(`❌ Лимит: ${CONFIG.MAX_WAYPOINTS_PER_FOLDER} точек`, '#ff4444');
return;
}
const domain = getCurrentDomain();
const pageTitle = getCurrentPageTitle();
const url = window.location.href;
const existing = folder.waypoints.find(w => w.url === url);
if (existing) {
showNotification('⚠️ Уже есть в папке', '#ffa500');
return;
}
const newWaypoint = {
id: Date.now(),
domain: domain,
title: pageTitle,
url: url,
hotkey: null,
createdAt: new Date().toISOString(),
lastUsed: null,
useCount: 0
};
folder.waypoints.push(newWaypoint);
saveFolders();
renderWaypointsView();
showNotification(`✅ "${pageTitle}" добавлена`, '#00ff88');
// Предзагружаем новую точку
instantPrefetch(url);
}
function deleteWaypointFromFolder(folderId, waypointId) {
const folder = folders.find(f => f.id === folderId);
if (!folder) return;
const waypoint = folder.waypoints.find(w => w.id === waypointId);
if (waypoint && confirm(`Удалить "${waypoint.title}"?`)) {
folder.waypoints = folder.waypoints.filter(w => w.id !== waypointId);
saveFolders();
renderWaypointsView();
showNotification('🗑️ Удалено', '#ff4444');
}
}
// ========== ГОРЯЧИЕ КЛАВИШИ (МГНОВЕННЫЙ ПЕРЕХОД) ==========
function startKeyCapture(folderId, waypointId) {
if (isCapturingKey) {
showNotification('⚠️ Уже идет захват', '#ffa500');
return;
}
isCapturingKey = true;
currentCaptureId = { folderId, waypointId };
const indicator = createCaptureIndicator();
document.body.appendChild(indicator);
captureTimeout = setTimeout(() => {
if (isCapturingKey) {
stopKeyCapture();
showNotification('❌ Время вышло', '#ff4444');
}
}, CONFIG.CAPTURE_TIMEOUT);
const captureHandler = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === 'Escape') {
stopKeyCapture();
showNotification('❌ Отменено', '#ff4444');
return;
}
if (e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift' || e.key === 'Meta') return;
e.preventDefault();
e.stopPropagation();
const hotkey = {
ctrl: e.ctrlKey,
alt: e.altKey,
shift: e.shiftKey,
meta: e.metaKey,
key: e.key
};
const folder = folders.find(f => f.id === folderId);
const waypoint = folder?.waypoints.find(w => w.id === waypointId);
if (waypoint) {
waypoint.hotkey = hotkey;
saveFolders();
renderWaypointsView();
showNotification(`✅ Назначено: ${formatHotkey(hotkey)}`, '#00ff88');
}
stopKeyCapture();
};
window.addEventListener('keydown', captureHandler, true);
window.__captureHandler = captureHandler;
}
function stopKeyCapture() {
if (captureTimeout) clearTimeout(captureTimeout);
if (window.__captureHandler) {
window.removeEventListener('keydown', window.__captureHandler, true);
window.__captureHandler = null;
}
const indicator = document.getElementById('key-capture-indicator');
if (indicator) indicator.remove();
isCapturingKey = false;
currentCaptureId = null;
}
// ========== МГНОВЕННАЯ ПРОВЕРКА КЛАВИШ ==========
function checkHotkey(e) {
if (isCapturingKey) return;
const target = e.target;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return;
if (e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift' || e.key === 'Meta') return;
const pressedHotkey = {
ctrl: e.ctrlKey,
alt: e.altKey,
shift: e.shiftKey,
meta: e.metaKey,
key: e.key
};
const currentDomain = getCurrentDomain();
const activeFolder = folders.find(f => f.id === activeFolderId);
if (activeFolder) {
const waypoint = activeFolder.waypoints.find(w => {
if (!w.hotkey) return false;
const hotkeyMatch = w.hotkey.ctrl === pressedHotkey.ctrl &&
w.hotkey.alt === pressedHotkey.alt &&
w.hotkey.shift === pressedHotkey.shift &&
w.hotkey.meta === pressedHotkey.meta &&
w.hotkey.key === pressedHotkey.key;
if (!hotkeyMatch) return false;
if (activeFolder.hotkeyMode === 'global') return true;
return w.domain === currentDomain;
});
if (waypoint) {
e.preventDefault();
e.stopPropagation();
waypoint.lastUsed = new Date().toISOString();
waypoint.useCount++;
saveFolders();
// МГНОВЕННЫЙ ПЕРЕХОД (страница уже предзагружена!)
showNotification(`⚡ МГНОВЕННО: ${waypoint.title}`, '#00ff88');
window.location.href = waypoint.url;
}
}
}
// ========== ЭКСПОРТ/ИМПОРТ ==========
function exportFolder(folderId) {
const folder = folders.find(f => f.id === folderId);
if (!folder) return;
const exportData = {
name: folder.name,
description: folder.description,
icon: folder.icon,
color: folder.color,
hotkeyMode: folder.hotkeyMode,
waypoints: folder.waypoints,
exportedAt: new Date().toISOString()
};
const data = JSON.stringify(exportData, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `folder_${folder.name}_${new Date().toISOString().slice(0,19)}.json`;
a.click();
URL.revokeObjectURL(url);
showNotification(`📁 "${folder.name}" экспортирована`, '#00ff88');
}
function importFolder() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const imported = JSON.parse(event.target.result);
const newFolder = {
id: Date.now(),
name: imported.name + ' (импорт)',
description: imported.description || '',
icon: imported.icon || '📁',
color: imported.color || '#667eea',
createdAt: new Date().toISOString(),
waypoints: imported.waypoints || [],
hotkeyMode: imported.hotkeyMode || 'local'
};
folders.push(newFolder);
saveFolders();
renderFoldersView();
showNotification(`📥 "${newFolder.name}" импортирована`, '#00ff88');
// Предзагружаем импортированные точки
setTimeout(() => prefetchAllFolderWaypoints(newFolder.id), 500);
} catch (err) {
showNotification('❌ Ошибка импорта', '#ff4444');
}
};
reader.readAsText(file);
};
fileInput.click();
}
// ========== ОТРИСОВКА ИНТЕРФЕЙСА ==========
function renderFoldersView() {
const container = document.getElementById('main-container');
if (!container) return;
if (folders.length === 0) {
container.innerHTML = `<div class="empty-state"><div class="empty-icon">📁</div><div>Нет папок</div><small>Нажмите "Создать папку"</small></div>`;
return;
}
container.innerHTML = `
<div class="folders-grid">
${folders.map(folder => `
<div class="folder-card ${activeFolderId === folder.id ? 'active' : ''}" style="border-color: ${folder.color}">
<div class="folder-header">
<div class="folder-icon" style="background: ${folder.color}20"><span>${folder.icon}</span></div>
<div class="folder-actions">
<button class="folder-action-btn select-folder" data-id="${folder.id}" title="Сделать активной (мгновенная предзагрузка)">
${activeFolderId === folder.id ? '✅' : '○'}
</button>
<button class="folder-action-btn export-folder" data-id="${folder.id}" title="Экспорт">💾</button>
<button class="folder-action-btn delete-folder" data-id="${folder.id}" title="Удалить">🗑️</button>
</div>
</div>
<div class="folder-body">
<h3 class="folder-name">${escapeHtml(folder.name)}</h3>
<div class="folder-stats">
<span>📌 ${folder.waypoints.length} точек</span>
<span class="mode-badge" style="background: ${folder.hotkeyMode === 'global' ? '#00ff8820' : '#ffa50020'}">
${folder.hotkeyMode === 'global' ? '🌍 Глобальный' : '📍 Локальный'}
</span>
</div>
${activeFolderId === folder.id ? '<div class="active-badge">✅ АКТИВНА (предзагружено)</div>' : ''}
</div>
<div class="folder-footer">
<button class="folder-btn open-folder" data-id="${folder.id}">📂 Открыть (предзагрузка)</button>
</div>
</div>
`).join('')}
</div>
`;
document.querySelectorAll('.open-folder').forEach(btn => {
btn.onclick = () => openFolderAndPrefetch(parseInt(btn.dataset.id));
});
document.querySelectorAll('.select-folder').forEach(btn => {
btn.onclick = () => setActiveFolderAndPrefetch(parseInt(btn.dataset.id));
});
document.querySelectorAll('.export-folder').forEach(btn => {
btn.onclick = () => exportFolder(parseInt(btn.dataset.id));
});
document.querySelectorAll('.delete-folder').forEach(btn => {
btn.onclick = () => deleteFolder(parseInt(btn.dataset.id));
});
}
function renderWaypointsView() {
const container = document.getElementById('main-container');
const folder = folders.find(f => f.id === currentFolderId);
if (!folder) {
backToFolders();
return;
}
const searchTerm = document.getElementById('search-input')?.value?.toLowerCase() || '';
let waypoints = [...folder.waypoints];
if (searchTerm) {
waypoints = waypoints.filter(w => w.title.toLowerCase().includes(searchTerm) || w.domain.toLowerCase().includes(searchTerm));
}
container.innerHTML = `
<div class="waypoints-header">
<button class="back-btn" id="back-to-folders">← Назад к папкам</button>
<div class="folder-info">
<span class="folder-icon-large">${folder.icon}</span>
<div>
<h2>${escapeHtml(folder.name)}</h2>
<div class="folder-controls">
<button class="mode-toggle-btn" id="toggle-folder-mode">
${folder.hotkeyMode === 'global' ? '🌍 Глобальный режим' : '📍 Локальный режим'}
</button>
<button class="save-page-btn" id="save-current-page">➕ Сохранить эту страницу</button>
</div>
</div>
</div>
</div>
<div class="search-bar"><input type="text" id="search-input" placeholder="🔍 Поиск..." class="search-input"></div>
<div class="prefetch-info">⚡ Все точки в этой папке предзагружаются фоном для мгновенного перехода!</div>
<div class="waypoints-list">
${waypoints.length === 0 ? `
<div class="empty-state"><div class="empty-icon">📍</div><div>Нет точек</div><small>Нажмите "Сохранить"</small></div>
` : waypoints.map(wp => `
<div class="waypoint-card">
<div class="waypoint-info">
<div class="waypoint-title">📌 ${escapeHtml(wp.title.substring(0, 50))}</div>
<div class="waypoint-meta">
${wp.hotkey ? `<span class="hotkey-badge">⌨️ ${formatHotkey(wp.hotkey)}</span>` : '<span class="no-hotkey">⚡ нет клавиши</span>'}
<span class="domain-badge">🌐 ${wp.domain}</span>
${wp.useCount ? `<span class="use-count">📊 ${wp.useCount}</span>` : ''}
</div>
</div>
<div class="waypoint-actions">
<button class="action-btn hotkey-btn" data-id="${wp.id}" title="Назначить клавишу">⌨️</button>
<button class="action-btn goto-btn" data-url="${wp.url}" title="Перейти (мгновенно)">⚡🔗</button>
<button class="action-btn delete-btn" data-id="${wp.id}" title="Удалить">✖</button>
</div>
</div>
`).join('')}
</div>
`;
document.getElementById('back-to-folders').onclick = backToFolders;
document.getElementById('save-current-page').onclick = () => createWaypointInFolder(currentFolderId);
document.getElementById('toggle-folder-mode').onclick = () => toggleFolderHotkeyMode(currentFolderId);
const searchInput = document.getElementById('search-input');
if (searchInput) searchInput.oninput = () => renderWaypointsView();
document.querySelectorAll('.goto-btn').forEach(btn => {
btn.onclick = () => { window.location.href = btn.dataset.url; };
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.onclick = () => deleteWaypointFromFolder(currentFolderId, parseInt(btn.dataset.id));
});
document.querySelectorAll('.hotkey-btn').forEach(btn => {
btn.onclick = () => startKeyCapture(currentFolderId, parseInt(btn.dataset.id));
});
}
function toggleFolderHotkeyMode(folderId) {
const folder = folders.find(f => f.id === folderId);
if (folder) {
folder.hotkeyMode = folder.hotkeyMode === 'global' ? 'local' : 'global';
saveFolders();
renderWaypointsView();
showNotification(`Режим: ${folder.hotkeyMode === 'global' ? 'Глобальный' : 'Локальный'}`, '#ffa500');
}
}
function deleteFolder(folderId) {
const folder = folders.find(f => f.id === folderId);
if (!folder) return;
if (confirm(`Удалить папку "${folder.name}" (${folder.waypoints.length} точек)?`)) {
folders = folders.filter(f => f.id !== folderId);
if (activeFolderId === folderId) {
activeFolderId = folders[0]?.id || null;
saveActiveFolder();
}
saveFolders();
if (currentView === 'waypoints' && currentFolderId === folderId) backToFolders();
renderFoldersView();
showNotification(`🗑️ Удалено`, '#ff4444');
}
}
function backToFolders() {
currentView = 'folders';
currentFolderId = null;
renderFoldersView();
}
function renderMainInterface() {
const panel = document.getElementById('hotkeys-main-panel');
if (!panel) return;
const content = panel.querySelector('.panel-content');
if (!content) return;
content.innerHTML = `
<div class="interface-header">
<div class="logo"><span class="logo-icon">⚡</span><span class="logo-text">INSTANT WAYPOINTS</span></div>
<div class="header-buttons">
<button id="create-folder-btn" class="primary-btn">➕ Создать папку</button>
<button id="import-folder-btn" class="secondary-btn">📂 Импорт</button>
</div>
</div>
<div id="main-container" class="main-container"></div>
`;
document.getElementById('create-folder-btn').onclick = createFolder;
document.getElementById('import-folder-btn').onclick = importFolder;
if (currentView === 'folders') renderFoldersView();
else renderWaypointsView();
}
function createFolder() {
if (folders.length >= CONFIG.MAX_FOLDERS) {
showNotification(`❌ Лимит: ${CONFIG.MAX_FOLDERS} папок`, '#ff4444');
return;
}
const name = prompt('Название папки:', 'Новая папка');
if (!name) return;
const newFolder = {
id: Date.now(),
name: name.substring(0, 30),
description: '',
icon: '📁',
color: '#667eea',
createdAt: new Date().toISOString(),
waypoints: [],
hotkeyMode: 'local'
};
folders.push(newFolder);
saveFolders();
renderFoldersView();
showNotification(`✅ "${name}" создана`, '#00ff88');
}
function createCaptureIndicator() {
const div = document.createElement('div');
div.id = 'key-capture-indicator';
div.innerHTML = `<div class="capture-content"><div class="capture-icon">⌨️</div><div class="capture-text">Нажмите любую комбинацию...</div><div class="capture-hint">Esc - отмена</div><div class="capture-pulse"></div></div>`;
return div;
}
// ========== УПРАВЛЕНИЕ ПАНЕЛЬЮ ==========
function togglePanel() {
panelVisible = !panelVisible;
GM_setValue(CONFIG.STORAGE_KEYS.panelVisible, panelVisible);
const panel = document.getElementById('hotkeys-main-panel');
const arrow = document.getElementById('nav-arrow-trigger');
if (panelVisible) {
panel.classList.add('panel-visible');
panel.classList.remove('panel-hidden');
if (arrow) arrow.classList.add('arrow-active');
} else {
panel.classList.remove('panel-visible');
panel.classList.add('panel-hidden');
if (arrow) arrow.classList.remove('arrow-active');
}
}
function createArrowTrigger() {
const existing = document.getElementById('nav-arrow-trigger');
if (existing) existing.remove();
const arrow = document.createElement('div');
arrow.id = 'nav-arrow-trigger';
arrow.className = 'nav-arrow';
arrow.innerHTML = `<div class="arrow-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div class="arrow-tooltip">Мгновенная навигация</div>`;
arrow.style.left = `${arrowPosition.x}px`;
arrow.style.top = `${arrowPosition.y}px`;
arrow.onclick = (e) => { e.stopPropagation(); togglePanel(); };
makeArrowDraggable(arrow);
document.body.appendChild(arrow);
return arrow;
}
function makeArrowDraggable(element) {
let isDragging = false, startX, startY, startLeft, startTop;
element.onmousedown = (e) => {
if (e.target.closest('.arrow-icon') || e.target === element) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(element.style.left);
startTop = parseInt(element.style.top);
document.body.style.userSelect = 'none';
element.style.cursor = 'grabbing';
document.onmousemove = (moveEvent) => {
if (!isDragging) return;
const newLeft = startLeft + (moveEvent.clientX - startX);
const newTop = startTop + (moveEvent.clientY - startY);
element.style.left = `${Math.max(5, Math.min(window.innerWidth - 50, newLeft))}px`;
element.style.top = `${Math.max(5, Math.min(window.innerHeight - 50, newTop))}px`;
};
document.onmouseup = () => {
isDragging = false;
document.body.style.userSelect = '';
element.style.cursor = '';
document.onmousemove = null;
document.onmouseup = null;
arrowPosition = { x: parseInt(element.style.left), y: parseInt(element.style.top) };
GM_setValue(CONFIG.STORAGE_KEYS.arrowPosition, JSON.stringify(arrowPosition));
};
e.preventDefault();
}
};
}
function createMainPanel() {
const existing = document.getElementById('hotkeys-main-panel');
if (existing) existing.remove();
const panel = document.createElement('div');
panel.id = 'hotkeys-main-panel';
panel.className = panelVisible ? 'panel-visible' : 'panel-hidden';
panel.innerHTML = `<div class="panel-header"><div class="header-title"><span class="neon-text">⚡ INSTANT NAVIGATION</span></div><div class="header-actions"><button class="icon-btn close-btn" id="close-panel-btn">✖</button></div></div><div class="panel-content"></div>`;
document.body.appendChild(panel);
document.getElementById('close-panel-btn').onclick = togglePanel;
renderMainInterface();
}
function showNotification(msg, color) {
const notif = document.createElement('div');
notif.textContent = msg;
notif.style.cssText = `position:fixed;bottom:30px;right:30px;background:linear-gradient(135deg,${color},${color}dd);color:white;padding:12px 24px;border-radius:12px;z-index:1000000;font-size:14px;font-weight:bold;box-shadow:0 4px 20px rgba(0,0,0,0.4);animation:cyberNotify 2s ease forwards;pointer-events:none`;
document.body.appendChild(notif);
setTimeout(() => notif.remove(), 2000);
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, m => ({ '&': '&', '<': '<', '>': '>' }[m]));
}
// ========== СТИЛИ ==========
GM_addStyle(`
@keyframes slideInRight { from { opacity: 0; transform: translateX(-100%); } to { opacity: 1; transform: translateX(0); } }
@keyframes slideOutLeft { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-100%); visibility: hidden; } }
@keyframes cyberNotify { 0% { opacity: 0; transform: translateX(100px); } 15% { opacity: 1; transform: translateX(0); } 85% { opacity: 1; transform: translateX(0); } 100% { opacity: 0; transform: translateX(100px); } }
@keyframes capturePulse { 0% { transform: scale(1); opacity: 0.6; } 100% { transform: scale(2); opacity: 0; } }
.nav-arrow { position: fixed; width: 48px; height: 48px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 999998; box-shadow: 0 4px 15px rgba(0,0,0,0.3); transition: all 0.3s; border: 2px solid rgba(255,255,255,0.3); }
.nav-arrow:hover { transform: scale(1.1); }
.nav-arrow.arrow-active { background: linear-gradient(135deg, #f093fb, #f5576c); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }
.arrow-icon svg { width: 28px; height: 28px; color: white; }
.arrow-tooltip { position: absolute; left: 55px; background: rgba(0,0,0,0.8); color: white; padding: 5px 10px; border-radius: 8px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
.nav-arrow:hover .arrow-tooltip { opacity: 1; }
#hotkeys-main-panel { position: fixed; top: 80px; left: 20px; width: 520px; max-width: 90vw; height: 70vh; max-height: 650px; background: linear-gradient(135deg, rgba(15,25,45,0.98), rgba(10,20,35,0.98)); backdrop-filter: blur(20px); border-radius: 20px; border: 1px solid rgba(102,126,234,0.3); z-index: 999997; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; }
#hotkeys-main-panel.panel-visible { animation: slideInRight 0.3s ease forwards; }
#hotkeys-main-panel.panel-hidden { animation: slideOutLeft 0.3s ease forwards; display: none; }
.panel-header { background: linear-gradient(135deg, rgba(102,126,234,0.2), rgba(118,75,162,0.2)); padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); }
.header-title .neon-text { font-size: 16px; font-weight: bold; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.icon-btn { background: rgba(255,255,255,0.1); border: none; padding: 6px 10px; border-radius: 10px; color: white; cursor: pointer; transition: all 0.2s; }
.icon-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.05); }
.panel-content { flex: 1; overflow-y: auto; padding: 20px; }
.panel-content::-webkit-scrollbar { width: 6px; }
.panel-content::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 10px; }
.panel-content::-webkit-scrollbar-thumb { background: #667eea; border-radius: 10px; }
.interface-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.logo { display: flex; align-items: center; gap: 10px; }
.logo-icon { font-size: 28px; }
.logo-text { font-size: 18px; font-weight: bold; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.header-buttons { display: flex; gap: 10px; }
.primary-btn, .secondary-btn { padding: 8px 16px; border: none; border-radius: 10px; cursor: pointer; font-weight: 500; transition: all 0.2s; }
.primary-btn { background: linear-gradient(135deg, #667eea, #764ba2); color: white; }
.primary-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102,126,234,0.4); }
.secondary-btn { background: rgba(255,255,255,0.1); color: white; border: 1px solid rgba(255,255,255,0.2); }
.secondary-btn:hover { background: rgba(255,255,255,0.2); }
.folders-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.folder-card { background: rgba(255,255,255,0.05); border-radius: 16px; border-left: 4px solid; padding: 16px; transition: all 0.3s; cursor: pointer; }
.folder-card:hover { transform: translateY(-2px); background: rgba(255,255,255,0.08); }
.folder-card.active { background: linear-gradient(135deg, rgba(102,126,234,0.15), rgba(118,75,162,0.15)); border-left-width: 6px; }
.folder-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.folder-icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 28px; }
.folder-actions { display: flex; gap: 6px; }
.folder-action-btn { background: rgba(255,255,255,0.1); border: none; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
.folder-action-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.05); }
.folder-name { color: white; font-size: 16px; font-weight: 600; margin: 0 0 8px 0; }
.folder-stats { display: flex; gap: 12px; font-size: 12px; color: rgba(255,255,255,0.6); }
.mode-badge { padding: 2px 8px; border-radius: 12px; font-size: 10px; }
.active-badge { margin-top: 8px; font-size: 10px; color: #00ff88; background: rgba(0,255,136,0.1); padding: 2px 8px; border-radius: 12px; display: inline-block; }
.folder-footer { margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.1); }
.folder-btn { width: 100%; padding: 8px; background: rgba(255,255,255,0.1); border: none; border-radius: 8px; color: white; cursor: pointer; transition: all 0.2s; }
.folder-btn:hover { background: rgba(255,255,255,0.2); }
.waypoints-header { margin-bottom: 20px; }
.back-btn { background: rgba(255,255,255,0.1); border: none; padding: 8px 16px; border-radius: 10px; color: white; cursor: pointer; margin-bottom: 16px; transition: all 0.2s; }
.back-btn:hover { background: rgba(255,255,255,0.2); }
.folder-info { display: flex; align-items: center; gap: 16px; }
.folder-icon-large { font-size: 48px; }
.folder-info h2 { color: white; margin: 0 0 8px 0; }
.folder-controls { display: flex; gap: 10px; }
.mode-toggle-btn, .save-page-btn { padding: 6px 12px; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
.mode-toggle-btn { background: rgba(255,165,0,0.2); color: #ffa500; }
.save-page-btn { background: linear-gradient(135deg, #00ff88, #00bfff); color: #0a0a0a; font-weight: bold; }
.search-bar { margin-bottom: 20px; }
.search-input { width: 100%; padding: 10px 14px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 12px; color: white; font-size: 14px; box-sizing: border-box; }
.search-input:focus { outline: none; border-color: #667eea; }
.prefetch-info { background: rgba(0,255,136,0.1); border: 1px solid #00ff88; border-radius: 10px; padding: 8px 12px; margin-bottom: 16px; font-size: 11px; color: #00ff88; text-align: center; }
.waypoints-list { display: flex; flex-direction: column; gap: 10px; }
.waypoint-card { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 12px; display: flex; justify-content: space-between; align-items: center; transition: all 0.2s; }
.waypoint-card:hover { background: rgba(255,255,255,0.08); transform: translateX(4px); }
.waypoint-info { flex: 1; }
.waypoint-title { color: white; font-size: 14px; font-weight: 500; margin-bottom: 6px; }
.waypoint-meta { display: flex; gap: 8px; flex-wrap: wrap; }
.hotkey-badge, .no-hotkey, .domain-badge, .use-count { padding: 2px 8px; border-radius: 12px; font-size: 10px; }
.hotkey-badge { background: rgba(0,255,136,0.15); color: #00ff88; font-family: monospace; }
.no-hotkey { background: rgba(255,100,100,0.15); color: #ff6464; }
.domain-badge { background: rgba(102,126,234,0.15); color: #667eea; }
.use-count { background: rgba(0,191,255,0.15); color: #00bfff; }
.waypoint-actions { display: flex; gap: 6px; }
.action-btn { background: rgba(255,255,255,0.1); border: none; padding: 6px 10px; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
.action-btn:hover { transform: scale(1.05); }
.hotkey-btn:hover { background: #3b82f6; color: white; }
.goto-btn:hover { background: #22c55e; color: white; }
.delete-btn:hover { background: #dc2626; color: white; }
.empty-state { text-align: center; padding: 60px 20px; color: rgba(255,255,255,0.4); }
.empty-icon { font-size: 64px; margin-bottom: 16px; }
#key-capture-indicator { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000000; }
.capture-content { background: linear-gradient(135deg, #1a1a2e, #16213e); border: 2px solid #667eea; border-radius: 24px; padding: 30px 50px; text-align: center; box-shadow: 0 0 40px rgba(102,126,234,0.5); position: relative; }
.capture-icon { font-size: 48px; margin-bottom: 15px; }
.capture-text { color: #667eea; font-size: 18px; font-weight: bold; margin-bottom: 10px; }
.capture-hint { color: #888; font-size: 12px; margin: 5px 0; }
.capture-pulse { position: absolute; top: 50%; left: 50%; width: 100%; height: 100%; border: 2px solid #667eea; border-radius: 24px; transform: translate(-50%, -50%); animation: capturePulse 1.5s infinite; pointer-events: none; }
`);
// ========== ЗАПУСК ==========
function init() {
loadData();
createArrowTrigger();
createMainPanel();
document.addEventListener('keydown', checkHotkey);
console.log('%c⚡ INSTANT WAYPOINTS PRO - МГНОВЕННАЯ НАВИГАЦИЯ ⚡', 'color: #00ff88; font-size: 16px; font-weight: bold;');
console.log('%c🚀 Предзагрузка активна! При нажатии клавиши переход будет МГНОВЕННЫМ!', 'color: #ffa500;');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();