您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
右クリックメニューやショートカットでブラウザ履歴の最初に戻る。
// ==UserScript== // @name 履歴の最初に戻る // @namespace https://bsky.app/profile/neon-ai.art // @homepage https://bsky.app/profile/neon-ai.art // @icon data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛄️</text></svg> // @version 3.2 // @description 右クリックメニューやショートカットでブラウザ履歴の最初に戻る。 // @author ねおん // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @license CC BY-NC 4.0 // ==/UserScript== // Ctrl+Shift+Aなど一部の予約済みのショートカットキーは設定できません (function() { 'use strict'; const SCRIPT_VERSION = '3.2'; const STORE_KEY = 'historyGoTop__shortcut'; let userShortcut = GM_getValue(STORE_KEY, 'Ctrl+Shift+H'); // デフォルトショートカット let menuId = null; let settingsId = null; // ========= 履歴の最初に戻る ========= function goBackToHistoryStart() { const initialUrl = window.location.href; setTimeout(() => { history.go(1 - history.length); setTimeout(() => { const isSuccessful = window.location.href !== initialUrl; const message = isSuccessful ? '履歴の最初に戻ったよ!' : '履歴の最初に戻れなかったよ!'; showToast(message, isSuccessful); }, 500); }, 0); } // ========= ショートカット処理 ========= function normalizeShortcutString(s) { if (!s) return 'Ctrl+Shift+H'; s = String(s).trim().replace(/\s+/g, ''); const parts = s.split('+').map(p => p.toLowerCase()); const mods = new Set(); let main = ''; for (const p of parts) { if (['ctrl','control'].includes(p)) mods.add('Ctrl'); else if (['alt','option'].includes(p)) mods.add('Alt'); else if (['shift'].includes(p)) mods.add('Shift'); else if (['meta','cmd','command','⌘'].includes(p)) mods.add('Meta'); else main = p; } if (!main) main = 'H'; if (/^key[a-z]$/i.test(main)) main = main.slice(3); if (/^digit[0-9]$/i.test(main)) main = main.slice(5); if (/^[a-z]$/.test(main)) main = main.toUpperCase(); if (/^f([1-9]|1[0-2])$/i.test(main)) main = main.toUpperCase(); const order = ['Ctrl','Alt','Shift','Meta']; const modStr = order.filter(m => mods.has(m)).join('+'); return (modStr ? modStr+'+' : '') + main; } function eventMatchesShortcut(e, shortcut) { const norm = normalizeShortcutString(shortcut); const parts = norm.split('+'); const mods = new Set(parts.slice(0,-1)); const keyPart = parts[parts.length-1]; const need = { Ctrl: mods.has('Ctrl'), Alt: mods.has('Alt'), Shift: mods.has('Shift'), Meta: mods.has('Meta') }; if (need.Ctrl !== e.ctrlKey) return false; if (need.Alt !== e.altKey) return false; if (need.Shift !== e.shiftKey) return false; if (need.Meta !== e.metaKey) return false; let pressed = ''; if (e.code.startsWith('Key')) pressed = e.code.slice(3).toUpperCase(); else if (e.code.startsWith('Digit')) pressed = e.code.slice(5); else pressed = e.key.length===1?e.key.toUpperCase():e.key; return pressed === keyPart; } function handleKeyDown(event) { const tag = (event.target && event.target.tagName) || ''; if (/(INPUT|TEXTAREA|SELECT)/.test(tag)) return; const overlay = document.querySelector('.hgt-overlay'); if (overlay) return; if (eventMatchesShortcut(event, userShortcut)) { event.preventDefault(); goBackToHistoryStart(); } } // ========= 設定UI ========= function ensureStyle() { if (document.getElementById('hgt-style')) return; const style = document.createElement('style'); style.id = 'hgt-style'; style.textContent = ` .hgt-overlay {position: fixed; top:0;left:0;width:100%;height:100%;background: rgba(0,0,0,0.7); z-index:100000; display:flex;justify-content:center;align-items:center;} .hgt-panel {background:#212529;color:#f0f0f0;width:90%;max-width:400px;border-radius:12px;box-shadow:0 8px 16px rgba(0,0,0,0.5);border:1px solid #333; font-family:'Inter',sans-serif; overflow:hidden;} .hgt-title {padding:15px 20px; display:flex;justify-content:space-between; align-items:center; font-size:1.25rem; font-weight:600; border-bottom:1px solid #333;} .hgt-close {background:none;border:none;cursor:pointer;font-size:24px;color:#f0f0f0;opacity:0.7;padding:0;} .hgt-close:hover {opacity:1;} .hgt-section {padding:20px;} .hgt-label {font-size:1rem;font-weight:500;color:#e0e0e0;display:block;margin-bottom:8px;} .hgt-input {width:100%;padding:8px 12px;background:#343a40;color:#f0f0f0;border:1px solid #333;border-radius:6px;cursor:text;box-sizing:border-box;} .hgt-input:focus {border-color:#007bff; box-shadow:0 0 4px #007bff;} .hgt-bottom {padding:15px 20px;border-top:1px solid #333;display:flex;justify-content:space-between;align-items:center;} .hgt-version {font-size:0.8rem;color:#aaa;} .hgt-button {padding:10px 20px;border:none;border-radius:6px;font-weight:bold;cursor:pointer;transition:all 0.2s ease;background:#007bff;color:white;} .hgt-button:hover {background:#0056b3;} `; document.head.appendChild(style); } // ========= トーストメッセージを表示する関数だよ ========= // @param {string} msg - 表示するメッセージ // @param {boolean} isSuccess - 成功したかどうかのフラグ function showToast(msg, isSuccess) { console.log(`[TOAST] ${msg}`); const toast = document.createElement('div'); toast.textContent = msg; const bgColor = isSuccess ? '#007bff' : '#dc3545'; // トーストの初期スタイルを設定するよ! // ここでは、最初からopacityを0(透明)にしておくのがポイントだよ! toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: ${bgColor}; color: white; padding: 10px 20px; border-radius: 6px; z-index: 100000; font-size: 14px; transition: opacity 1.0s ease, transform 1.0s ease; /* opacityとtransformにtransitionを適用! */ opacity: 0; /* 最初は透明 */ `; document.body.appendChild(toast); // 画面に要素が追加された後、少しだけ待ってから、透明度を1にするよ! // このわずかな時間差が、transitionを発動させる鍵だよ! setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translate(-50%, -20px)'; // ちょっと上に動かすアニメーションを追加してみたよ! }, 10); // わずかな時間差(10ms)を設けるのがコツだよ! // 3秒後に消える処理 setTimeout(() => { toast.style.opacity = '0'; // 消すときも滑らかに消えてもらうために、opacityを0にするよ! toast.style.transform = 'translate(-50%, 0)'; setTimeout(() => { if (document.body.contains(toast)) { toast.remove(); } }, 1000); // transitionの時間と同じだけ待つのがベストだよ! }, 3000); } function openSettings() { ensureStyle(); const overlay = document.createElement('div'); overlay.className='hgt-overlay'; // ========= ESCキーで閉じる ========= const onEsc = (e) => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onEsc); } }; document.addEventListener('keydown', onEsc); const panel = document.createElement('div'); panel.className='hgt-panel'; const closeBtn = document.createElement('span'); closeBtn.className='hgt-close'; closeBtn.textContent='×'; closeBtn.title='閉じる'; closeBtn.addEventListener('click',()=>document.body.removeChild(overlay)); const title = document.createElement('div'); title.className='hgt-title'; title.textContent='設定'; title.appendChild(closeBtn); const section = document.createElement('div'); section.className='hgt-section'; const label = document.createElement('div'); label.className='hgt-label'; label.textContent='ショートカットキー'; const input = document.createElement('input'); input.type='text'; input.className='hgt-input'; input.placeholder='例: Ctrl+Shift+H'; input.readOnly=true; input.value = normalizeShortcutString(userShortcut); input.addEventListener('keydown',e=>{ e.preventDefault(); const mods=[]; if(e.ctrlKey) mods.push('Ctrl'); if(e.altKey) mods.push('Alt'); if(e.shiftKey) mods.push('Shift'); if(e.metaKey) mods.push('Meta'); let main=''; if(e.code.startsWith('Key')) main=e.code.slice(3).toUpperCase(); else if(e.code.startsWith('Digit')) main=e.code.slice(5); else if(/^F([1-9]|1[0-2])$/.test(e.key)) main=e.key.toUpperCase(); else if(e.key && e.key.length===1) main=e.key.toUpperCase(); input.value = (mods.length ? mods.join('+')+'+' : '') + main; }); section.appendChild(label); section.appendChild(input); const bottom=document.createElement('div'); bottom.className='hgt-bottom'; const version=document.createElement('div'); version.className='hgt-version'; version.textContent='('+SCRIPT_VERSION+')'; const saveBtn=document.createElement('button'); saveBtn.className='hgt-button'; saveBtn.textContent='保存'; saveBtn.addEventListener('click',()=>{ const norm = normalizeShortcutString(input.value); userShortcut = norm; GM_setValue(STORE_KEY,userShortcut); addContextMenu(); document.body.removeChild(overlay); showToast('設定を保存したよ!'); }); bottom.appendChild(version); bottom.appendChild(saveBtn); overlay.addEventListener('click',e=>{if(e.target===overlay) overlay.remove();}); panel.appendChild(title); panel.appendChild(section); panel.appendChild(bottom); overlay.appendChild(panel); document.body.appendChild(overlay); input.focus(); } // ========= 右クリックメニュー ========= function addContextMenu() { if(menuId) GM_unregisterMenuCommand(menuId); menuId = GM_registerMenuCommand('最初に戻る ['+userShortcut+']', goBackToHistoryStart); if(settingsId) GM_unregisterMenuCommand(settingsId); settingsId = GM_registerMenuCommand('設定', openSettings); } // ========= 初期化 ========= function init() { document.removeEventListener('keydown',handleKeyDown); document.addEventListener('keydown',handleKeyDown); addContextMenu(); } init(); })();