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