履歴の最初に戻る

右クリックメニューやショートカットでブラウザ履歴の最初に戻る。

// ==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();

})();