TokyoMotion Enhancer

TokyoMotionをより便利にするスクリプト - 高評価保存、視聴履歴、プレイリスト、購読・友達動画の簡易閲覧、簡易自動ログイン (English/日本語対応)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         TokyoMotion Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  TokyoMotionをより便利にするスクリプト - 高評価保存、視聴履歴、プレイリスト、購読・友達動画の簡易閲覧、簡易自動ログイン (English/日本語対応)
// @author       meranoa
// @license MIT
// @match        https://www.tokyomotion.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tokyomotion.net
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_openInTab
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    console.log('[TM Enhancer] Userscript started (Version 1.0)');

    // ========================================
    // 言語・翻訳管理 (Localization)
    // ========================================
    const TranslationManager = {
        getLanguage() {
            const config = GM_getValue('appLanguage', 'auto');
            if (config === 'auto') {
                const navLang = (navigator.language || navigator.userLanguage || 'ja').toLowerCase();
                return navLang.startsWith('ja') ? 'ja' : 'en';
            }
            return config;
        },
        setLanguage(lang) {
            GM_setValue('appLanguage', lang);
        },
        getText(key, replacements = {}) {
            const lang = this.getLanguage();
            let text = (TRANSLATIONS[lang] && TRANSLATIONS[lang][key]) || TRANSLATIONS['en'][key] || key;
            Object.keys(replacements).forEach(k => {
                text = text.replace(`{${k}}`, replacements[k]);
            });
            return text;
        }
    };

    const t = (key, repl) => TranslationManager.getText(key, repl);

    const TRANSLATIONS = {
        ja: {
            'tab_liked': '❤️ 高評価',
            'tab_history': '📺 履歴',
            'tab_playlists': '📁 リスト',
            'tab_feed': '📡 購読',
            'tab_friends': '🤝 友達',
            'tab_settings': '⚙️ 設定',
            'btn_close': '閉じる',
            'btn_update': '更新',
            'btn_create': '作成',
            'btn_delete': '削除',
            'btn_remove': '削除',
            'btn_export': '📥 エクスポート',
            'btn_import': '📤 インポート',
            'btn_clear_all': '🗑️ データ全削除',
            'btn_back': '← 戻る',
            'btn_rename': '名前を変更',
            'btn_sort_new': '▼ 新しい順',
            'btn_sort_old': '▲ 古い順',
            'label_private': 'PRIVATE',
            'msg_empty_liked': 'まだ高評価した動画がありません',
            'msg_empty_history': 'まだ視聴履歴がありません',
            'msg_empty_playlist': 'プレイリストがありません',
            'msg_empty_videos': '動画がありません',
            'msg_empty_feed': 'なし',
            'msg_saved_liked': '高評価に保存しました',
            'msg_added_to': '「{name}」に追加',
            'msg_removed_from': '「{name}」から削除',
            'msg_created_added': '「{name}」を作成して追加',
            'msg_fetching': '取得中...',
            'msg_fetching_users': '{count}人の動画を取得中...',
            'msg_fetching_progress': '取得中... ({current}/{total}人)',
            'msg_complete': '完了 ({count}件)',
            'msg_error': 'エラー: {msg}',
            'msg_no_users': 'ユーザーなし',
            'msg_auto_login': '自動ログイン中...',
            'msg_import_done': 'インポート完了。ページをリロードします。',
            'msg_import_error': '読み込みエラー: {msg}',
            'msg_data_cleared': '全データを削除しました。',
            'msg_scroll_reset': '長時間経過のためスクロールをリセットしました',
            'confirm_delete_liked': '高評価した動画消しますか?',
            'confirm_delete_playlist': 'プレイリスト「{name}」を削除しますか?',
            'confirm_settings_hidden': '設定タブを非表示にすると、後から再表示するのが難しくなります。本当によろしいですか?',
            'confirm_overwrite': '現在のデータを上書きします。よろしいですか?',
            'confirm_clear_all': '全てのデータ(高評価、履歴、プレイリスト)を削除しますか?\nこの操作は取り消せません。',
            'prompt_playlist_name': '新しいプレイリスト名を入力してください',
            'alert_exists': '既に存在します',
            'alert_name_used': 'その名前は既に使用されています。',
            'stg_language': '言語 / Language',
            'stg_startup_tab': '起動時に表示するタブ',
            'stg_tab_last_open': '🔄 最後に開いた項目',
            'stg_auto_login': '🔐 自動ログイン',
            'stg_auto_login_desc': 'ログインページを開いた際に自動でログインボタンを押します。',
            'stg_tab_visibility': '📑 タブ表示設定',
            'stg_grid_cols': '動画一覧の列数',
            'stg_feed_pages': '購読フィード取得ページ数',
            'stg_friend_pages': '友達フィード取得ページ数',
            'stg_page_unit': 'ページ',
            'stg_unlimited': '無制限 (全て取得)',
            'stg_col_unit': '列',
            'stg_scroll_reset': '非アクティブ時のスクロールリセット時間',
            'unit_sec': '秒',
            'unit_min': '分',
            'unit_hour': '時間',
            'time_just_now': 'たった今',
            'time_min_ago': '分前',
            'time_hour_ago': '時間前',
            'time_day_ago': '日前',
            'time_long_ago': 'かなり前',
            'time_videos_count': '{count} 動画',
            'modal_title': 'Myリスト',
            'placeholder_new_playlist': '新規リスト名',
        },
        en: {
            'tab_liked': '❤️ Liked',
            'tab_history': '📺 History',
            'tab_playlists': '📁 Playlists',
            'tab_feed': '📡 Feed',
            'tab_friends': '🤝 Friends',
            'tab_settings': '⚙️ Settings',
            'btn_close': 'Close',
            'btn_update': 'Update',
            'btn_create': 'Create',
            'btn_delete': 'Delete',
            'btn_remove': 'Remove',
            'btn_export': '📥 Export',
            'btn_import': '📤 Import',
            'btn_clear_all': '🗑️ Clear All Data',
            'btn_back': '← Back',
            'btn_rename': 'Rename',
            'btn_sort_new': '▼ Newest',
            'btn_sort_old': '▲ Oldest',
            'label_private': 'PRIVATE',
            'msg_empty_liked': 'No liked videos yet.',
            'msg_empty_history': 'No watch history yet.',
            'msg_empty_playlist': 'No playlists created.',
            'msg_empty_videos': 'No videos found.',
            'msg_empty_feed': 'Empty',
            'msg_saved_liked': 'Saved to Liked Videos',
            'msg_added_to': 'Added to "{name}"',
            'msg_removed_from': 'Removed from "{name}"',
            'msg_created_added': 'Created "{name}" and added video',
            'msg_fetching': 'Fetching...',
            'msg_fetching_users': 'Fetching videos from {count} users...',
            'msg_fetching_progress': 'Fetching... ({current}/{total} users)',
            'msg_complete': 'Done ({count} videos)',
            'msg_error': 'Error: {msg}',
            'msg_no_users': 'No users found',
            'msg_auto_login': 'Auto logging in...',
            'msg_import_done': 'Import complete. Reloading page.',
            'msg_import_error': 'Import Error: {msg}',
            'msg_data_cleared': 'All data cleared.',
            'msg_scroll_reset': 'Inactive for too long. Scroll reset.',
            'confirm_delete_liked': 'Remove this video from Liked?',
            'confirm_delete_playlist': 'Delete playlist "{name}"?',
            'confirm_settings_hidden': 'If you hide the Settings tab, it will be difficult to show it again. Are you sure?',
            'confirm_overwrite': 'This will overwrite current data. Are you sure?',
            'confirm_clear_all': 'Delete ALL data (Liked, History, Playlists)?\nThis cannot be undone.',
            'prompt_playlist_name': 'Enter new playlist name',
            'alert_exists': 'Already exists',
            'alert_name_used': 'That name is already taken.',
            'stg_language': 'Language / 言語',
            'stg_startup_tab': 'Startup Tab',
            'stg_tab_last_open': '🔄 Last Opened',
            'stg_auto_login': '🔐 Auto Login',
            'stg_auto_login_desc': 'Automatically clicks the login button when opening the login modal.',
            'stg_tab_visibility': '📑 Tab Visibility',
            'stg_grid_cols': 'Video Grid Columns',
            'stg_feed_pages': 'Feed Fetch Pages',
            'stg_friend_pages': 'Friends Fetch Pages',
            'stg_page_unit': ' pages',
            'stg_unlimited': 'Unlimited',
            'stg_col_unit': ' cols',
            'stg_scroll_reset': 'Scroll Reset Time (Inactive)',
            'unit_sec': 'Seconds',
            'unit_min': 'Minutes',
            'unit_hour': 'Hours',
            'time_just_now': 'Just now',
            'time_min_ago': 'm ago',
            'time_hour_ago': 'h ago',
            'time_day_ago': 'd ago',
            'time_long_ago': 'Long ago',
            'time_videos_count': '{count} videos',
            'modal_title': 'My Playlists',
            'placeholder_new_playlist': 'New Playlist Name',
        }
    };

    // ========================================
    // 定数・初期設定
    // ========================================
    const DEFAULT_TAB_ORDER = ['liked', 'history', 'playlists', 'feed', 'friends', 'settings'];
    const SCROLLBAR_MARGIN = 25;

    // ========================================
    // ストレージマネージャー
    // ========================================
    const StorageManager = {
        async getLikedVideos() { return GM_getValue('likedVideos', []); },
        async addLikedVideo(videoData) {
            const videos = await this.getLikedVideos();
            if (!videos.some(v => v.id === videoData.id)) {
                videos.unshift(videoData);
                GM_setValue('likedVideos', videos);
            }
        },
        async removeLikedVideo(videoId) {
            let videos = await this.getLikedVideos();
            videos = videos.filter(v => v.id !== videoId);
            GM_setValue('likedVideos', videos);
        },
        async getHistory() { return GM_getValue('history', []); },
        async addToHistory(videoData) {
            let history = await this.getHistory();
            history.unshift({ ...videoData, watchedAt: Date.now() });
            GM_setValue('history', history);
        },
        async clearHistory() { GM_setValue('history', []); },
        async getPlaylists() { return GM_getValue('playlists', {}); },
        async createPlaylist(name) {
            const playlists = await this.getPlaylists();
            if (playlists[name]) return false;
            playlists[name] = [];
            GM_setValue('playlists', playlists);
            return true;
        },
        async deletePlaylist(name) {
            const playlists = await this.getPlaylists();
            delete playlists[name];
            GM_setValue('playlists', playlists);
            if (this.getActivePlaylist() === name) {
                this.setActivePlaylist(null);
            }
        },
        async renamePlaylist(oldName, newName) {
            if (oldName === newName) return true;
            const playlists = await this.getPlaylists();
            if (playlists[newName]) return false;
            playlists[newName] = playlists[oldName];
            delete playlists[oldName];
            GM_setValue('playlists', playlists);
            const order = this.getPlaylistOrder();
            const idx = order.indexOf(oldName);
            if (idx !== -1) {
                order[idx] = newName;
                GM_setValue('playlistOrder', order);
            }
            if (this.getActivePlaylist() === oldName) {
                this.setActivePlaylist(newName);
            }
            return true;
        },
        async addToPlaylist(name, videoData) {
            const playlists = await this.getPlaylists();
            if (!playlists[name]) return;
            if (!playlists[name].some(v => v.id === videoData.id)) {
                playlists[name].unshift(videoData);
                GM_setValue('playlists', playlists);
            }
        },
        async removeFromPlaylist(name, videoId) {
            const playlists = await this.getPlaylists();
            if (!playlists[name]) return;
            playlists[name] = playlists[name].filter(v => v.id !== videoId);
            GM_setValue('playlists', playlists);
        },
        getPrivateCache() { return GM_getValue('privateVideoCache', {}); },
        addPrivateToCache(ids) {
            const cache = this.getPrivateCache();
            let changed = false;
            ids.forEach(id => {
                if (!cache[id]) { cache[id] = 1; changed = true; }
            });
            if (changed) GM_setValue('privateVideoCache', cache);
        },
        isPrivateCached(id) {
            const cache = this.getPrivateCache();
            return !!cache[id];
        },
        getDefaultTab() { return GM_getValue('defaultTab', 'liked'); },
        setDefaultTab(tab) { GM_setValue('defaultTab', tab); },
        getLastActiveTab() { return GM_getValue('lastActiveTab', 'liked'); },
        setLastActiveTab(tab) { GM_setValue('lastActiveTab', tab); },
        isAutoLoginEnabled() { return GM_getValue('autoLoginEnabled', false); },
        setAutoLoginEnabled(enabled) { GM_setValue('autoLoginEnabled', enabled); },
        getPlaylistOrder() { return GM_getValue('playlistOrder', []); },
        setPlaylistOrder(order) { GM_setValue('playlistOrder', order); },
        async getOrderedPlaylistNames() {
            const playlists = await this.getPlaylists();
            const savedOrder = this.getPlaylistOrder();
            const allNames = Object.keys(playlists);
            const ordered = savedOrder.filter(name => allNames.includes(name));
            const remaining = allNames.filter(name => !ordered.includes(name));
            return [...ordered, ...remaining];
        },
        getFeedData() { return GM_getValue('feedData', []); },
        setFeedData(data) { GM_setValue('feedData', data); },
        getFeedLastUpdated() { return GM_getValue('feedLastUpdated', 0); },
        setFeedLastUpdated(time) { GM_setValue('feedLastUpdated', time); },
        getFriendsFeedData() { return GM_getValue('friendsFeedData', []); },
        setFriendsFeedData(data) { GM_setValue('friendsFeedData', data); },
        getFriendsLastUpdated() { return GM_getValue('friendsLastUpdated', 0); },
        setFriendsLastUpdated(time) { GM_setValue('friendsLastUpdated', time); },
        getFeedMaxPages() { return GM_getValue('feedMaxPages', 1); },
        setFeedMaxPages(pages) { GM_setValue('feedMaxPages', pages); },
        getFriendsMaxPages() { return GM_getValue('friendsMaxPages', 1); },
        setFriendsMaxPages(pages) { GM_setValue('friendsMaxPages', pages); },
        getModalCols() { return GM_getValue('modalCols', 3); },
        setModalCols(cols) { GM_setValue('modalCols', cols); },
        getPlaylistGridCols() { return GM_getValue('playlistGridCols', 2); },
        setPlaylistGridCols(cols) { GM_setValue('playlistGridCols', cols); },
        getVideoGridCols() { return GM_getValue('videoGridCols', 2); },
        setVideoGridCols(cols) { GM_setValue('videoGridCols', cols); },
        getTabOrder() { return GM_getValue('tabOrder', DEFAULT_TAB_ORDER); },
        setTabOrder(order) { GM_setValue('tabOrder', order); },
        getTabVisibility() {
            const defaults = {};
            DEFAULT_TAB_ORDER.forEach(k => defaults[k] = true);
            return GM_getValue('tabVisibility', defaults);
        },
        setTabVisibility(vis) { GM_setValue('tabVisibility', vis); },
        getPanelState() { return GM_getValue('panelState', null); },
        setPanelState(state) { GM_setValue('panelState', state); },
        getBtnPosition() { return GM_getValue('btnPosition', null); },
        setBtnPosition(pos) { GM_setValue('btnPosition', pos); },
        getTabScroll(tab) {
            const scrolls = GM_getValue('tabScrolls', {});
            return scrolls[tab] || 0;
        },
        setTabScroll(tab, val) {
            const scrolls = GM_getValue('tabScrolls', {});
            scrolls[tab] = val;
            GM_setValue('tabScrolls', scrolls);
        },
        getActivePlaylist() { return GM_getValue('activePlaylist', null); },
        setActivePlaylist(name) { GM_setValue('activePlaylist', name); },

        // スクロールリセット設定用
        getScrollResetValue() { return GM_getValue('scrollResetValue', 5); }, // デフォルト5
        setScrollResetValue(val) { GM_setValue('scrollResetValue', val); },
        getScrollResetUnit() { return GM_getValue('scrollResetUnit', 'minutes'); }, // デフォルトminutes
        setScrollResetUnit(unit) { GM_setValue('scrollResetUnit', unit); },
        getLastClosedTime() { return GM_getValue('lastClosedTime', 0); },
        setLastClosedTime(time) { GM_setValue('lastClosedTime', time); },
    };

    // リセット時間を計算するヘルパー
    function getScrollResetMs() {
        const val = StorageManager.getScrollResetValue();
        const unit = StorageManager.getScrollResetUnit();
        let multiplier = 1000; // seconds
        if (unit === 'minutes') multiplier = 60 * 1000;
        if (unit === 'hours') multiplier = 60 * 60 * 1000;
        console.log(`[TokyoMotion Enhancer] Reset time: ${val} ${unit} = ${val * multiplier}ms`); // Debug log
        return val * multiplier;
    }

    // ========================================
    // Privateスキャナー
    // ========================================
    const PrivateScanner = {
        scan() {
            const privateIds = [];
            const cards = document.querySelectorAll('.col-sm-4, .video-card, .thumb-block');
            cards.forEach(card => {
                const isPrivate = card.querySelector('.label-private') ||
                    card.querySelector('.img-private') ||
                    (card.textContent && card.textContent.includes('PRIVATE'));
                if (isPrivate) {
                    const link = card.querySelector('a[href*="/video/"]');
                    if (link) {
                        const match = link.getAttribute('href').match(/\/video\/(\d+)/);
                        if (match && match[1]) privateIds.push(match[1]);
                    }
                }
            });
            if (privateIds.length > 0) StorageManager.addPrivateToCache(privateIds);
        },
        startObserver() {
            this.scan();
            new MutationObserver(() => this.scan()).observe(document.body, { childList: true, subtree: true });
        }
    };

    // ========================================
    // 購読(フォロー)マネージャー
    // ========================================
    const SubscriptionManager = {
        async getMyProfileUrl() {
            const profileLink = document.querySelector('a[href^="/user/"]:not([href*="logout"]):not([href*="login"])');
            if (profileLink) return profileLink.href;
            const userLink = document.querySelector('.username a');
            if (userLink) return userLink.href;
            const avatarLink = document.querySelector('.avatar-container a, .header-avatar a');
            if (avatarLink) return avatarLink.href;
            return null;
        },
        async getSubscriptionsBaseUrl() {
            const profileUrl = await this.getMyProfileUrl();
            if (!profileUrl) return null;
            return profileUrl.split('/').slice(0, 5).join('/') + '/subscriptions';
        },
        async getFriendsBaseUrl() {
            const profileUrl = await this.getMyProfileUrl();
            if (!profileUrl) return null;
            return profileUrl.split('/').slice(0, 5).join('/') + '/friends';
        },
        async fetchDocument(url) {
            try {
                const response = await fetch(url);
                return new DOMParser().parseFromString(await response.text(), 'text/html');
            } catch (e) { return null; }
        },
        async getFollowedUsers(statusCallback) {
            const baseUrl = await this.getSubscriptionsBaseUrl();
            if (!baseUrl) throw new Error('ログインしていないか、プロフィールが見つかりません');
            return this._getUsersFromPages(baseUrl, statusCallback);
        },
        async getFriends(statusCallback) {
            const baseUrl = await this.getFriendsBaseUrl();
            if (!baseUrl) throw new Error('ログインしていないか、プロフィールが見つかりません');
            return this._getUsersFromPages(baseUrl, statusCallback);
        },
        async _getUsersFromPages(baseUrl, statusCallback) {
            const usersMap = new Map();
            let page = 1;
            let hasNextPage = true;
            const myUsernameMatch = baseUrl.match(/\/user\/([^\/]+)/);
            const myUsername = myUsernameMatch ? myUsernameMatch[1] : null;
            while (hasNextPage) {
                const url = page === 1 ? baseUrl : `${baseUrl}?page=${page}`;
                if (statusCallback) statusCallback(`${t('msg_fetching')} (${page} p)`);
                const doc = await this.fetchDocument(url);
                if (!doc) break;
                const userCards = Array.from(doc.querySelectorAll('.thumb-block, .user-card, .col-sm-6, .col-sm-4, .col-xs-6'));
                userCards.forEach(card => {
                    const userLink = card.querySelector('a[href*="/user/"]');
                    if (!userLink) return;
                    const href = userLink.getAttribute('href');
                    if (href.includes('/video/')) return;
                    const userMatch = href.match(/\/user\/([^\/\?#]+)/);
                    if (!userMatch || !userMatch[1]) return;
                    const username = userMatch[1];
                    if (myUsername && username.toLowerCase() === myUsername.toLowerCase()) return;
                    const invalidUsernames = ['edit', 'avatar', 'logout', 'login', 'register', 'settings', 'upload', 'search', 'help', 'contact', 'about', 'terms', 'privacy', 'dmca'];
                    if (invalidUsernames.includes(username.toLowerCase())) return;
                    const normalizedUrl = `https://www.tokyomotion.net/user/${username}`;
                    if (usersMap.has(normalizedUrl)) return;
                    let iconSrc = '';
                    const img = card.querySelector('img');
                    if (img) iconSrc = img.src || img.dataset.src || '';
                    if (!iconSrc) iconSrc = 'https://www.tokyomotion.net/img/user-avatar.png';
                    usersMap.set(normalizedUrl, { url: normalizedUrl, icon: iconSrc });
                });
                const userLinks = Array.from(doc.querySelectorAll('a[href*="/user/"]'));
                userLinks.forEach(a => {
                    const href = a.getAttribute('href');
                    if (!href || href.includes('/video/') || href.includes('/subscriptions') || href.includes('/friends') || href.includes('/favorites')) return;
                    const userMatch = href.match(/\/user\/([^\/\?#]+)/);
                    if (!userMatch || !userMatch[1]) return;
                    const username = userMatch[1];
                    if (myUsername && username.toLowerCase() === myUsername.toLowerCase()) return;
                    const invalidUsernames = ['edit', 'avatar', 'logout', 'login', 'register', 'settings', 'upload', 'search', 'help', 'contact', 'about', 'terms', 'privacy', 'dmca'];
                    if (invalidUsernames.includes(username.toLowerCase())) return;
                    const normalizedUrl = `https://www.tokyomotion.net/user/${username}`;
                    if (usersMap.has(normalizedUrl)) return;
                    let iconSrc = '';
                    const img = a.querySelector('img') || (a.parentElement ? a.parentElement.querySelector('img') : null);
                    if (img) iconSrc = img.src || img.dataset.src || '';
                    if (!iconSrc) iconSrc = 'https://www.tokyomotion.net/img/user-avatar.png';
                    usersMap.set(normalizedUrl, { url: normalizedUrl, icon: iconSrc });
                });
                const paginationLinks = Array.from(doc.querySelectorAll('.pagination a'));
                const hasNext = paginationLinks.some(a => a.href.includes(`page=${page + 1}`));
                if (hasNext) page++; else hasNextPage = false;
                await new Promise(r => setTimeout(r, 500));
            }
            return Array.from(usersMap.values());
        },
        async getUserVideos(userData, maxPages = 5) {
            const userUrl = userData.url;
            const userIcon = userData.icon;
            const videosBaseUrl = userUrl.replace(/\/$/, '') + '/videos';
            const allVideos = [];
            let page = 1;
            let hasNextPage = true;
            while (hasNextPage && page <= maxPages) {
                const targetUrl = page === 1 ? videosBaseUrl : `${videosBaseUrl}?page=${page}`;
                try {
                    const doc = await this.fetchDocument(targetUrl);
                    if (!doc) break;
                    const videoLinks = Array.from(doc.querySelectorAll('a[href*="/video/"]'));
                    let foundInPage = 0;
                    videoLinks.forEach(link => {
                        try {
                            const img = link.querySelector('img') || (link.parentElement ? link.parentElement.querySelector('img') : null);
                            if (!img) return;
                            const container = link.closest('.col-sm-4') || link.closest('.col-xs-6') || link.closest('.video-card') || link.closest('.thumb-block') || link.parentElement;
                            let title = '', duration = '', dateStr = '';
                            let isPrivate = false;
                            if (container) {
                                const titleEl = container.querySelector('.video-card-title, .title, .video-title, h4, h5');
                                if (titleEl) title = titleEl.innerText.trim();
                                const durationEl = container.querySelector('.duration');
                                if (durationEl) duration = durationEl.innerText.trim();
                                const dateEl = container.querySelector('.video-added');
                                if (dateEl) dateStr = dateEl.innerText.trim();
                                if (container.querySelector('.label-private') || container.querySelector('.img-private')) isPrivate = true;
                                const overlay = container.querySelector('.thumb-overlay');
                                if (!isPrivate && overlay && overlay.textContent.toUpperCase().includes('PRIVATE')) isPrivate = true;
                            }
                            if (!title && img.alt) title = img.alt.trim();
                            if (!duration) {
                                const insideDuration = link.querySelector('.duration');
                                if (insideDuration) duration = insideDuration.innerText.trim();
                            }
                            if (!title) title = 'Untitled';
                            const href = link.getAttribute('href');
                            const fullUrl = href.startsWith('http') ? href : (new URL(href, userUrl).href);
                            const idMatch = fullUrl.match(/\/video\/(\d+)/);
                            if (!idMatch) return;
                            if (allVideos.some(v => v.id === idMatch[1])) return;
                            if (isPrivate) StorageManager.addPrivateToCache([idMatch[1]]);
                            allVideos.push({
                                id: idMatch[1],
                                title: title,
                                thumbnail: img.src || img.dataset.src,
                                url: fullUrl,
                                author: userUrl.split('/').pop(),
                                authorIcon: userIcon,
                                duration: duration,
                                date: dateStr,
                                isPrivate: isPrivate,
                                timestamp: Date.now()
                            });
                            foundInPage++;
                        } catch (e) { }
                    });
                    const paginationLinks = Array.from(doc.querySelectorAll('.pagination a'));
                    const hasNext = paginationLinks.some(a => a.href.includes(`page=${page + 1}`));
                    if (hasNext && foundInPage > 0) page++; else hasNextPage = false;
                    await new Promise(r => setTimeout(r, 500));
                } catch (err) { break; }
            }
            return allVideos;
        }
    };

    function formatRelativeTime(rawTime) {
        if (!rawTime) return '';
        let cleaned = rawTime.replace(/\s+/g, '').trim();
        if (cleaned.match(/^\d+時前$/)) cleaned = cleaned.replace('時前', '時間前');
        cleaned = cleaned.replace(/時\s*前/g, '時間前').replace(/分\s*前/g, '分前').replace(/日\s*前/g, '日前').replace(/週\s*前/g, '週間前').replace(/月\s*前/g, 'ヶ月前').replace(/年\s*前/g, '年前');
        return cleaned;
    }

    function calcTimeAgo(timestamp) {
        if (!timestamp) return '-';
        const diff = Date.now() - timestamp;
        const minute = 60 * 1000;
        const hour = 60 * minute;
        const day = 24 * hour;
        if (diff < minute) return t('time_just_now');
        if (diff < hour) return Math.floor(diff / minute) + t('time_min_ago');
        if (diff < day) return Math.floor(diff / hour) + t('time_hour_ago');
        if (diff < day * 30) return Math.floor(diff / day) + t('time_day_ago');
        return t('time_long_ago');
    }

    // ========================================
    // スタイル注入
    // ========================================
    GM_addStyle(`
        .tm-panel {
            position: fixed; width: 500px; height: 600px; min-width: 300px; min-height: 250px;
            background: #1a1a1a; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
            z-index: 999999; display: none; flex-direction: column; font-family: 'Segoe UI', sans-serif;
            bottom: 90px; right: 20px;
        }
        .tm-panel.active { display: flex; }
        .tm-panel-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px;
            font-weight: 600; display: flex; justify-content: space-between; align-items: center;
            cursor: move; user-select: none; border-radius: 12px 12px 0 0;
        }
        .tm-panel-close { background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; }
        .tm-toggle-btn {
            position: fixed; width: 56px; height: 56px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border: none; border-radius: 50%; color: white; font-size: 24px; cursor: pointer;
            box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); z-index: 999998; user-select: none; touch-action: none;
            bottom: 20px; right: 20px;
        }
        .tm-toggle-btn:hover { transform: scale(1.1); }
        .tm-toggle-btn:active { transform: scale(0.95); }
        .tm-tabs { display: flex; background: #252525; border-bottom: 2px solid #333; overflow-x: auto; }
        .tm-tab {
            flex: 1; padding: 12px 8px; background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 12px;
            border-bottom: 3px solid transparent; min-width: 60px; text-align: center; white-space: nowrap; user-select: none;
        }
        .tm-tab.active { background: #1a1a1a; border-bottom-color: #667eea; color: #fff; font-weight: 600; }
        .tm-tab:hover { background: #333; }
        .tm-tab.dragging { opacity: 0.5; background: #444; }
        .tm-content { flex: 1; overflow-y: auto; padding: 10px; background: #111; min-height: 0; }
        .tm-tab-content { display: none; height: 100%; }
        .tm-tab-content.active { display: block; }
        .tm-feed-header { display: flex; justify-content: center; align-items: center; gap: 15px; padding: 10px; margin-bottom: 5px; }
        .tm-time-label { font-size: 11px; color: #888; width: 90px; text-align: center; white-space: nowrap; }
        .tm-grid-view { display: grid; grid-template-columns: repeat(var(--tm-video-cols, 2), 1fr); gap: 15px; padding: 5px; }
        .tm-card { display: flex; flex-direction: column; background: transparent; cursor: pointer; border: none; position: relative; transition: opacity 0.2s; }
        .tm-card:hover { opacity: 0.9; }
        .tm-card-thumb-box { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
        .tm-card-thumb { width: 100%; height: 100%; object-fit: cover; }
        .tm-card-duration { position: absolute; bottom: 5px; right: 5px; background: rgba(0,0,0,0.7); color: #fff; padding: 2px 5px; font-size: 11px; border-radius: 3px; line-height: 1; }
        .tm-card-private { position: absolute; bottom: 5px; left: 5px; background: #cc0000; color: #fff; padding: 2px 5px; font-size: 10px; border-radius: 3px; line-height: 1; font-weight: bold; }
        .tm-card-title {
            color: #ff5e5e; font-size: 13px; font-weight: 500; line-height: 1.3; max-height: 2.6em; overflow: hidden; margin-bottom: 5px;
            display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
        }
        .tm-card:hover .tm-card-title { text-decoration: underline; }
        .tm-card-meta { font-size: 11px; color: #888; line-height: 1.3; }
        .tm-user-row { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; flex-wrap: wrap; }
        .tm-user-icon { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; background: #333; flex-shrink: 0; }
        .tm-user-link { color: #aaa; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
        .tm-user-link:hover { color: #fff; text-decoration: underline; }
        .tm-relative-time { color: #888; font-size: 10px; margin-left: 6px; white-space: nowrap; }
        .tm-input { width: 100%; padding: 10px; background: #2d2d2d; border: 2px solid #444; border-radius: 6px; color: #e0e0e0; font-size: 13px; margin-bottom: 10px; }
        .tm-btn-row { display: flex; gap: 8px; }
        .tm-btn-primary, .tm-btn-secondary { flex: 1; padding: 10px; border: none; border-radius: 6px; cursor: pointer; color: white; }
        .tm-btn-primary { background: #667eea; }
        .tm-btn-secondary { background: #555; }
        .tm-btn-danger { width: 100%; padding: 10px; background: #e74c3c; color: white; border: none; border-radius: 6px; cursor: pointer; margin-top: 10px; }
        .tm-empty { text-align: center; color: #666; padding: 30px; }
        .tm-toast {
            position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%) translateY(20px);
            background: #333; color: #fff; padding: 12px 24px; border-radius: 8px;
            opacity: 0; transition: all 0.3s; z-index: 9999999;
        }
        .tm-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
        .tm-card-remove {
            position: absolute; top: 5px; right: 5px; background: rgba(231, 76, 60, 0.8); color: white;
            border: none; width: 20px; height: 20px; border-radius: 50%; cursor: pointer; display: none; z-index: 10; font-size: 12px;
            align-items: center; justify-content: center;
        }
        .tm-card:hover .tm-card-remove { display: flex; }
        .tm-playlist-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
        .tm-playlist-card { padding: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px; cursor: pointer; position: relative; transition: all 0.3s; }
        .tm-playlist-name { font-weight: bold; margin-bottom: 5px; }
        .tm-playlist-count { font-size: 12px; opacity: 0.9; }
        .tm-playlist-delete { position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.3); border: none; color: white; width: 22px; height: 22px; border-radius: 50%; cursor: pointer; }
        .tm-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 9999990; display: flex; align-items: center; justify-content: center; }
        .tm-modal-content { background: #1a1a1a; border-radius: 12px; width: auto; min-width: 320px; max-width: 90vw; transition: width 0.3s; }
        .tm-modal-header { background: #667eea; color: white; padding: 15px; display: flex; justify-content: space-between; align-items: center; gap: 10px; border-radius: 12px 12px 0 0; }
        .tm-playlist-list { padding: 10px; display: grid; gap: 8px; max-height: 350px; overflow-y: auto; }
        .tm-playlist-item { display: flex; gap: 5px; padding: 8px; background: #2d2d2d; border-radius: 4px; color: #fff; cursor: pointer; }
        .tm-modal-footer { padding: 15px; }
        .tm-new-playlist-form { display: flex; gap: 5px; }
        .tm-playlist-btn-inline { margin-left: 5px !important; cursor: pointer !important; }
        .tm-toggle-switch { width: 44px; height: 24px; background: #555; border-radius: 12px; cursor: pointer; position: relative; }
        .tm-toggle-switch.active { background: #667eea; }
        .tm-toggle-switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: 0.3s; }
        .tm-toggle-switch.active::after { transform: translateX(20px); }
        .tm-col-select { background: rgba(0,0,0,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 2px 5px; font-size: 12px; cursor: pointer; outline: none; }
        .tm-col-select option { background: #333; color: white; }
        .tm-tab-visibility-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #333; }
        .tm-tab-visibility-item:last-child { border-bottom: none; }
        .tm-resizer { position: absolute; z-index: 100; touch-action: none; }
        .tm-resizer.n { top: -5px; left: 0; right: 0; height: 10px; cursor: ns-resize; }
        .tm-resizer.s { bottom: -5px; left: 0; right: 0; height: 10px; cursor: ns-resize; }
        .tm-resizer.e { right: -5px; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
        .tm-resizer.w { left: -5px; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
        .tm-resizer.ne { top: -5px; right: -5px; width: 15px; height: 15px; cursor: nesw-resize; }
        .tm-resizer.nw { top: -5px; left: -5px; width: 15px; height: 15px; cursor: nwse-resize; }
        .tm-resizer.se { bottom: -5px; right: -5px; width: 15px; height: 15px; cursor: nwse-resize; }
        .tm-resizer.sw { bottom: -5px; left: -5px; width: 15px; height: 15px; cursor: nesw-resize; }
        body.tm-dragging { user-select: none; }
        .tm-btn-icon { background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 14px; margin-left: 8px; padding: 2px; transition: color 0.2s; }
        .tm-btn-icon:hover { color: #fff; }
        .tm-added-time { color: #888; font-size: 10px; margin-top: 3px; }
    `);

    // ========================================
    // ユーティリティ
    // ========================================
    function formatDate(timestamp) {
        if (!timestamp) return '';
        const d = new Date(timestamp);
        const pad = (n) => String(n).padStart(2, '0');
        return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    }

    function formatSeconds(seconds) {
        if (!seconds || isNaN(seconds)) return null;
        seconds = Math.floor(seconds);
        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;
        const pad = n => n.toString().padStart(2, '0');
        if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
        return `${m}:${pad(s)}`;
    }

    function showToast(message) {
        const existing = document.querySelector('.tm-toast');
        if (existing) existing.remove();
        const toast = document.createElement('div');
        toast.className = 'tm-toast';
        toast.textContent = message;
        document.body.appendChild(toast);
        requestAnimationFrame(() => toast.classList.add('show'));
        setTimeout(() => {
            toast.classList.remove('show');
            setTimeout(() => toast.remove(), 3000); // 通知時間
        }, 3000);
    }

    function extractVideoId() {
        const match = window.location.pathname.match(/\/video\/(\d+)/);
        return match ? match[1] : null;
    }

    function extractVideoData(forceDuration = null) {
        const videoId = extractVideoId();
        if (!videoId) return null;
        let title = 'Untitled';
        const titleEl = document.querySelector('h3.big-title-truncate, h4.big-title-truncate');
        if (titleEl) title = titleEl.textContent.trim();
        else if (document.title) title = document.title.replace(' - TokyoMotion', '').trim();
        let duration = '';
        const videoEl = document.querySelector('video');
        if (forceDuration) {
            duration = forceDuration;
        } else if (videoEl && videoEl.duration && !isNaN(videoEl.duration) && videoEl.duration !== Infinity && videoEl.duration > 0) {
            duration = formatSeconds(videoEl.duration) || '';
        }
        if (!duration) {
            const durEl = document.querySelector('.vjs-duration-display') || document.querySelector('.duration');
            if (durEl) {
                const text = durEl.innerText.replace(/[^\d:]/g, '');
                if (text && text !== '0:00' && text !== '00:00') duration = text;
            }
        }
        let author = 'Unknown';
        let authorIcon = '';
        const userContainer = document.querySelector('.user-container');
        if (userContainer) {
            const link = userContainer.querySelector('a[href^="/user/"]');
            if (link) {
                const span = link.querySelector('span');
                author = span ? span.textContent.trim() : link.textContent.trim();
                const img = link.querySelector('img');
                if (img) authorIcon = img.src;
            }
        } else {
            const userLink = document.querySelector('.video-info a[href^="/user/"], .user-info a[href^="/user/"], a.username');
            if (userLink) author = userLink.innerText.trim();
            const avatarImg = document.querySelector('.avatar-container img, .video-info img.avatar, .user-avatar img');
            if (avatarImg) authorIcon = avatarImg.src;
        }
        if (!authorIcon) authorIcon = 'https://www.tokyomotion.net/img/user-avatar.png';
        let isPrivate = false;
        if (StorageManager.isPrivateCached(videoId)) isPrivate = true;
        if (!isPrivate) {
            try {
                if (document.querySelector('.label-private') || document.querySelector('.img-private')) isPrivate = true;
            } catch (e) { }
        }
        return {
            id: videoId,
            title: title,
            thumbnail: document.querySelector('video[poster]')?.getAttribute('poster') || '',
            url: window.location.href,
            duration: duration,
            author: author,
            authorIcon: authorIcon,
            isPrivate: isPrivate,
            timestamp: Date.now()
        };
    }

    function getDurationFromPlayer() {
        const videoEl = document.querySelector('video');
        if (videoEl && videoEl.duration && !isNaN(videoEl.duration) && videoEl.duration !== Infinity && videoEl.duration > 0) {
            const formatted = formatSeconds(videoEl.duration);
            if (formatted) return formatted;
        }
        const durEl = document.querySelector('.vjs-duration-display');
        if (durEl) {
            const text = durEl.innerText.replace(/[^\d:]/g, '');
            if (text && text !== '0:00' && text !== '00:00') return text;
        }
        return null;
    }

    // ========================================
    // パネル・ボタン移動 & リサイズ
    // ========================================
    function setupDraggableButton(btn) {
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;
        const DRAG_THRESHOLD = 5;
        let hasMoved = false;

        btn.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return;
            isDragging = true;
            hasMoved = false;
            const rect = btn.getBoundingClientRect();
            btn.style.bottom = 'auto'; btn.style.right = 'auto';
            btn.style.left = rect.left + 'px'; btn.style.top = rect.top + 'px';
            startX = e.clientX; startY = e.clientY;
            initialLeft = rect.left; initialTop = rect.top;
            document.body.classList.add('tm-dragging');
            e.preventDefault();
        });
        window.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) hasMoved = true;
            const winW = window.innerWidth; const winH = window.innerHeight;
            const btnW = btn.offsetWidth; const btnH = btn.offsetHeight;
            let newLeft = initialLeft + dx; let newTop = initialTop + dy;
            newLeft = Math.max(0, Math.min(newLeft, winW - btnW));
            newTop = Math.max(0, Math.min(newTop, winH - btnH));
            btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px';
        });
        window.addEventListener('mouseup', (e) => {
            if (isDragging) {
                isDragging = false; document.body.classList.remove('tm-dragging');
                if (hasMoved) StorageManager.setBtnPosition({ left: btn.style.left, top: btn.style.top });
            }
        });
        btn.addEventListener('click', (e) => {
            if (hasMoved) { e.stopImmediatePropagation(); e.preventDefault(); }
        }, true);
    }

    function setupDraggablePanel(panel) {
        const header = panel.querySelector('.tm-panel-header');
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;
        header.addEventListener('mousedown', (e) => {
            if (e.target.closest('button') || e.target.closest('.tm-resizer')) return;
            isDragging = true;
            const rect = panel.getBoundingClientRect();
            panel.style.bottom = 'auto'; panel.style.right = 'auto';
            panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px';
            startX = e.clientX; startY = e.clientY;
            initialLeft = rect.left; initialTop = rect.top;
            document.body.classList.add('tm-dragging');
            e.preventDefault();
        });
        window.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            let newLeft = initialLeft + dx; let newTop = initialTop + dy;
            const winW = window.innerWidth; const winH = window.innerHeight;
            const panelW = panel.offsetWidth; const panelH = panel.offsetHeight;
            newLeft = Math.max(0, Math.min(newLeft, winW - panelW - SCROLLBAR_MARGIN));
            newTop = Math.max(0, Math.min(newTop, winH - panelH));
            panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px';
        });
        window.addEventListener('mouseup', () => {
            if (isDragging) { isDragging = false; document.body.classList.remove('tm-dragging'); savePanelState(panel); }
        });
    }

    function setupResizablePanel(panel) {
        const directions = ['n', 'e', 's', 'w', 'ne', 'nw', 'se', 'sw'];
        directions.forEach(dir => {
            const resizer = document.createElement('div');
            resizer.className = `tm-resizer ${dir}`;
            panel.appendChild(resizer);
            resizer.addEventListener('mousedown', (e) => initResize(e, dir));
        });
        let isResizing = false;
        let currentDir = '';
        let startX, startY, startW, startH, startLeft, startTop;
        function initResize(e, dir) {
            e.preventDefault(); e.stopPropagation();
            isResizing = true; currentDir = dir;
            const rect = panel.getBoundingClientRect();
            startX = e.clientX; startY = e.clientY;
            startW = rect.width; startH = rect.height;
            startLeft = rect.left; startTop = rect.top;
            panel.style.left = startLeft + 'px'; panel.style.top = startTop + 'px';
            panel.style.right = 'auto'; panel.style.bottom = 'auto';
            panel.style.width = startW + 'px'; panel.style.height = startH + 'px';
            document.body.classList.add('tm-dragging');
            document.body.style.cursor = window.getComputedStyle(e.target).cursor;
        }
        window.addEventListener('mousemove', (e) => {
            if (!isResizing) return;
            const dx = e.clientX - startX; const dy = e.clientY - startY;
            const winW = window.innerWidth; const winH = window.innerHeight;
            let newW = startW; let newH = startH; let newLeft = startLeft; let newTop = startTop;
            if (currentDir.includes('e')) newW = Math.min(startW + dx, winW - startLeft - SCROLLBAR_MARGIN);
            if (currentDir.includes('w')) { const maxDelta = startLeft; const actualDx = Math.max(dx, -maxDelta); newW = startW - actualDx; newLeft = startLeft + actualDx; }
            if (currentDir.includes('s')) newH = Math.min(startH + dy, winH - startTop);
            if (currentDir.includes('n')) { const maxDelta = startTop; const actualDy = Math.max(dy, -maxDelta); newH = startH - actualDy; newTop = startTop + actualDy; }
            const minW = 300; const minH = 200;
            if (newW < minW) { if (currentDir.includes('w')) newLeft = startLeft + (startW - minW); newW = minW; }
            if (newH < minH) { if (currentDir.includes('n')) newTop = startTop + (startH - minH); newH = minH; }
            panel.style.width = newW + 'px'; panel.style.height = newH + 'px';
            panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px';
        });
        window.addEventListener('mouseup', () => {
            if (isResizing) { isResizing = false; document.body.classList.remove('tm-dragging'); document.body.style.cursor = ''; savePanelState(panel); }
        });
    }

    function savePanelState(panel) {
        const style = window.getComputedStyle(panel);
        StorageManager.setPanelState({ width: style.width, height: style.height, left: style.left, top: style.top });
    }

    function restoreUIState(panel, btn) {
        const btnPos = StorageManager.getBtnPosition();
        if (btnPos) { btn.style.bottom = 'auto'; btn.style.right = 'auto'; btn.style.left = btnPos.left; btn.style.top = btnPos.top; }
        const panelState = StorageManager.getPanelState();
        if (panelState) {
            panel.style.bottom = 'auto'; panel.style.right = 'auto';
            const winW = window.innerWidth; const winH = window.innerHeight;
            let w = parseFloat(panelState.width); let h = parseFloat(panelState.height);
            let l = parseFloat(panelState.left); let t = parseFloat(panelState.top);
            w = Math.min(w, winW - SCROLLBAR_MARGIN); h = Math.min(h, winH);
            l = Math.max(0, Math.min(l, winW - w - SCROLLBAR_MARGIN));
            t = Math.max(0, Math.min(t, winH - h));
            panel.style.left = l + 'px'; panel.style.top = t + 'px';
            panel.style.width = w + 'px'; panel.style.height = h + 'px';
        }
    }

    function setupScrollPersistence(panel) {
        const content = panel.querySelector('.tm-content');
        if (!content) return;
        content.addEventListener('scroll', () => {
            const currentTab = StorageManager.getLastActiveTab();
            if (currentTab) {
                if (content.scrollTimeout) clearTimeout(content.scrollTimeout);
                content.scrollTimeout = setTimeout(() => {
                    StorageManager.setTabScroll(currentTab, content.scrollTop);
                }, 100);
            }
        });
    }

    function restoreScrollPosition(panel, tabName) {
        const content = panel.querySelector('.tm-content');
        if (content) {
            const savedScroll = StorageManager.getTabScroll(tabName);
            setTimeout(() => { content.scrollTop = savedScroll; }, 50);
        }
    }

    // ========================================
    // メインUI
    // ========================================
    function createMainUI() {
        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'tm-toggle-btn';
        toggleBtn.innerHTML = '🎬';
        toggleBtn.title = 'TokyoMotion Enhancer';
        document.body.appendChild(toggleBtn);

        const panel = document.createElement('div');
        panel.className = 'tm-panel';

        const isVideoPage = location.pathname.startsWith('/video/');
        if (isVideoPage) {
            panel.classList.add('active');
        } else {
            panel.classList.remove('active');
        }

        panel.innerHTML = `
            <div class="tm-panel-header">
                <span>🎬 TokyoMotion Enhancer</span>
                <button class="tm-panel-close">×</button>
            </div>
            <div class="tm-tabs" id="tm-tabs-container"></div>
            <div class="tm-content">
                <div class="tm-tab-content" id="tm-liked"></div>
                <div class="tm-tab-content" id="tm-history"></div>
                <div class="tm-tab-content" id="tm-playlists"></div>

                <div class="tm-tab-content" id="tm-feed">
                    <div class="tm-feed-header">
                        <span id="tm-feed-time-ago" class="tm-time-label"></span>
                        <button class="tm-btn-primary" id="tm-feed-update">${t('tab_feed')} ${t('btn_update')}</button>
                        <span id="tm-feed-time-absolute" class="tm-time-label"></span>
                    </div>
                    <div id="tm-feed-status" style="margin:0 0 10px 0; text-align:center; font-size:11px; color:#888;"></div>
                    <div id="tm-feed-list"></div>
                </div>

                <div class="tm-tab-content" id="tm-friends">
                    <div class="tm-feed-header">
                        <span id="tm-friends-time-ago" class="tm-time-label"></span>
                        <button class="tm-btn-primary" id="tm-friends-update">${t('tab_friends')} ${t('btn_update')}</button>
                        <span id="tm-friends-time-absolute" class="tm-time-label"></span>
                    </div>
                    <div id="tm-friends-status" style="margin:0 0 10px 0; text-align:center; font-size:11px; color:#888;"></div>
                    <div id="tm-friends-list"></div>
                </div>

                <div class="tm-tab-content" id="tm-settings">
                    <!-- Settings will be rendered by JS -->
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        // 初期描画
        renderTabs(panel);
        renderSettingsTab(panel);
        restoreUIState(panel, toggleBtn);
        setupDraggableButton(toggleBtn);
        setupDraggablePanel(panel);
        setupResizablePanel(panel);
        setupScrollPersistence(panel);
        applyVideoGridCols();

        // ----------------------------------------
        // ★自動スクロールリセット機能
        // ----------------------------------------
        let leaveTime = 0;
        document.addEventListener('visibilitychange', () => {
            if (document.hidden) {
                // サイトを離れた(タブを隠した)時間を記録
                leaveTime = Date.now();
            } else {
                // サイトに戻ってきた時
                if (leaveTime > 0) {
                    const diff = Date.now() - leaveTime;
                    const threshold = getScrollResetMs();
                    // 設定時間を経過していたらリセット
                    if (diff > threshold) {
                        const content = panel.querySelector('.tm-content');
                        if (content) {
                            content.scrollTop = 0;
                        }

                        // 全タブのスクロール位置をリセット
                        const tabsToReset = ['liked', 'history', 'playlists', 'feed', 'friends'];
                        tabsToReset.forEach(tab => {
                            StorageManager.setTabScroll(tab, 0);
                        });

                        // プレイリストタブは詳細画面から一覧に戻す
                        StorageManager.setActivePlaylist(null);

                        // 現在のタブを再読み込みしてリセット反映
                        const currentTab = StorageManager.getLastActiveTab();
                        if (currentTab === 'liked') {
                            loadLikedVideos();
                        } else if (currentTab === 'history') {
                            loadHistory();
                        } else if (currentTab === 'playlists') {
                            loadPlaylists();
                        }

                        showToast(t('msg_scroll_reset')); // 通知
                    }
                    leaveTime = 0;
                }
            }
        });

        panel.addEventListener('click', (e) => e.stopPropagation());

        toggleBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            panel.classList.toggle('active');
            if (panel.classList.contains('active')) {
                let defaultTab = StorageManager.getDefaultTab();
                if (defaultTab === 'last_open') defaultTab = StorageManager.getLastActiveTab();
                const visibility = StorageManager.getTabVisibility();
                if (!visibility[defaultTab]) {
                    const order = StorageManager.getTabOrder();
                    defaultTab = order.find(t => visibility[t]) || 'settings';
                }
                switchToTab(panel, defaultTab);
            }
        });

        panel.querySelector('.tm-panel-close').addEventListener('click', () => {
            panel.classList.remove('active');
        });

        document.addEventListener('click', (e) => {
            if (panel.classList.contains('active')) {
                if (document.querySelector('.tm-modal-overlay')) return;
                if (!isVideoPage) {
                    if (!panel.contains(e.target) && !toggleBtn.contains(e.target)) {
                        panel.classList.remove('active');
                    }
                }
            }
        });

        if (panel.classList.contains('active')) {
            let defaultTab = StorageManager.getDefaultTab();
            if (defaultTab === 'last_open') defaultTab = StorageManager.getLastActiveTab();
            switchToTab(panel, defaultTab);
        }

        document.addEventListener('fullscreenchange', () => {
            if (document.fullscreenElement) {
                panel.style.display = 'none';
                toggleBtn.style.display = 'none';
            } else {
                panel.style.display = '';
                toggleBtn.style.display = '';
            }
        });
    }

    // ========================================
    // 設定タブの動的描画
    // ========================================
    function renderSettingsTab(panel = document.querySelector('.tm-panel')) {
        const container = panel.querySelector('#tm-settings');
        if (!container) return;

        // オプション生成ヘルパー
        const generatePageOptions = () => {
            let opts = '';
            for (let i = 1; i <= 10; i++) {
                opts += `<option value="${i}">${i}${t('stg_page_unit')}</option>`;
            }
            opts += `<option value="99999">${t('stg_unlimited')}</option>`;
            return opts;
        };

        const currentScrollVal = StorageManager.getScrollResetValue();
        const currentScrollUnit = StorageManager.getScrollResetUnit();

        container.innerHTML = `
            <div style="margin-bottom:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_language')}</label>
                <select id="tm-language-selector" class="tm-input">
                    <option value="auto">Auto (自動)</option>
                    <option value="ja">日本語</option>
                    <option value="en">English</option>
                </select>
            </div>
            <div style="border-top:1px solid #444; padding-top:15px; margin-bottom:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_startup_tab')}</label>
                <select id="tm-default-tab" class="tm-input">
                    <option value="last_open">${t('stg_tab_last_open')}</option>
                    <option value="liked">${t('tab_liked')}</option>
                    <option value="history">${t('tab_history')}</option>
                    <option value="playlists">${t('tab_playlists')}</option>
                    <option value="feed">${t('tab_feed')}</option>
                    <option value="friends">${t('tab_friends')}</option>
                    <option value="settings">${t('tab_settings')}</option>
                </select>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_scroll_reset')}</label>
                <div style="display:flex; gap:10px;">
                    <input type="number" id="tm-scroll-val" class="tm-input" style="flex:1;" min="1" value="${currentScrollVal}">
                    <select id="tm-scroll-unit" class="tm-input" style="flex:1;">
                        <option value="seconds">${t('unit_sec')}</option>
                        <option value="minutes">${t('unit_min')}</option>
                        <option value="hours">${t('unit_hour')}</option>
                    </select>
                </div>
            </div>

            <div class="tm-login-section" style="border-top:1px solid #444; padding-top:15px;">
                <div style="color:#e0e0e0;font-size:14px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px;">
                    ${t('stg_auto_login')} <div class="tm-toggle-switch" id="tm-auto-login-toggle"></div>
                </div>
                <div style="font-size:12px;color:#888;">${t('stg_auto_login_desc')}</div>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <div style="color:#e0e0e0;font-size:14px;font-weight:600;margin-bottom:12px;">${t('stg_tab_visibility')}</div>
                <div id="tm-tab-visibility-settings"></div>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_grid_cols')}</label>
                <select id="tm-video-grid-cols" class="tm-input">
                    <option value="1">1${t('stg_col_unit')}</option>
                    <option value="2">2${t('stg_col_unit')}</option>
                    <option value="3">3${t('stg_col_unit')}</option>
                    <option value="4">4${t('stg_col_unit')}</option>
                    <option value="5">5${t('stg_col_unit')}</option>
                </select>
            </div>

            <div style="margin-top:15px; border-top:1px solid #444; padding-top:15px;">
                <div style="display:flex; gap:10px;">
                    <div style="flex:1;">
                        <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_feed_pages')}</label>
                        <select id="tm-feed-max-pages" class="tm-input">
                           ${generatePageOptions()}
                        </select>
                    </div>
                    <div style="flex:1;">
                         <label style="color:#e0e0e0;font-size:13px;display:block;margin-bottom:8px;">${t('stg_friend_pages')}</label>
                         <select id="tm-friends-max-pages" class="tm-input">
                            ${generatePageOptions()}
                         </select>
                    </div>
                </div>
            </div>
            <div class="tm-btn-row" style="margin-top:20px;">
                <button class="tm-btn-secondary" id="tm-export">${t('btn_export')}</button>
                <button class="tm-btn-secondary" id="tm-import">${t('btn_import')}</button>
            </div>
            <button class="tm-btn-danger" id="tm-clear-all">${t('btn_clear_all')}</button>
        `;

        // イベントバインドの再実行
        renderTabVisibilitySettings();
        initLoginSettings();

        document.getElementById('tm-export').addEventListener('click', exportData);
        document.getElementById('tm-import').addEventListener('click', importData);
        document.getElementById('tm-clear-all').addEventListener('click', clearAllData);

        const defaultTabSelect = document.getElementById('tm-default-tab');
        defaultTabSelect.value = StorageManager.getDefaultTab();
        defaultTabSelect.addEventListener('change', (e) => StorageManager.setDefaultTab(e.target.value));

        const scrollValInput = document.getElementById('tm-scroll-val');
        const saveScrollVal = (e) => {
            const val = parseInt(e.target.value);
            if (val > 0) {
                StorageManager.setScrollResetValue(val);
                console.log(`[TokyoMotion Enhancer] Saved scroll val: ${val}`);
            }
        };
        // 'input'だと急な変更で保存が追いつかない場合があるので'change'も併用
        scrollValInput.addEventListener('input', saveScrollVal);
        scrollValInput.addEventListener('change', saveScrollVal);

        const scrollUnitSelect = document.getElementById('tm-scroll-unit');
        scrollUnitSelect.value = currentScrollUnit;
        scrollUnitSelect.addEventListener('change', (e) => {
            StorageManager.setScrollResetUnit(e.target.value);
            console.log(`[TokyoMotion Enhancer] Saved scroll unit: ${e.target.value}`);
        });

        const videoGridColsSelect = document.getElementById('tm-video-grid-cols');
        videoGridColsSelect.value = StorageManager.getVideoGridCols();
        videoGridColsSelect.addEventListener('change', (e) => {
            const val = parseInt(e.target.value);
            StorageManager.setVideoGridCols(val);
            applyVideoGridCols();
        });

        const feedMaxSelect = document.getElementById('tm-feed-max-pages');
        feedMaxSelect.value = StorageManager.getFeedMaxPages();
        feedMaxSelect.addEventListener('change', (e) => StorageManager.setFeedMaxPages(parseInt(e.target.value)));

        const friendsMaxSelect = document.getElementById('tm-friends-max-pages');
        friendsMaxSelect.value = StorageManager.getFriendsMaxPages();
        friendsMaxSelect.addEventListener('change', (e) => StorageManager.setFriendsMaxPages(parseInt(e.target.value)));

        const langSelect = document.getElementById('tm-language-selector');
        langSelect.value = GM_getValue('appLanguage', 'auto');
        langSelect.addEventListener('change', (e) => {
            TranslationManager.setLanguage(e.target.value);
            // リロードではなく、描画関数を呼び出して即時反映
            renderTabs(panel);
            renderSettingsTab(panel);

            // 現在のタブが設定以外(既に開いていたタブ)の場合、その内容も更新する必要がある
            // ただし設定タブにいるので、次にフィードタブを開いたときに更新されればよい
            // _setupFeedLogicがタブ切り替え時に呼ばれ、そこで言語更新を行うように修正済み
        });
    }

    // ========================================
    // タブ関連ロジック
    // ========================================
    function renderTabs(panel) {
        const container = panel.querySelector('#tm-tabs-container');
        container.innerHTML = '';
        const order = StorageManager.getTabOrder();
        DEFAULT_TAB_ORDER.forEach(def => { if (!order.includes(def)) order.push(def); });
        const visibility = StorageManager.getTabVisibility();
        order.forEach(tabKey => {
            if (!visibility[tabKey]) return;
            const btn = document.createElement('button');
            btn.className = 'tm-tab';
            btn.dataset.tab = tabKey;
            btn.textContent = t(`tab_${tabKey}`);
            btn.draggable = true;
            if (tabKey === StorageManager.getLastActiveTab()) btn.classList.add('active'); // アクティブ状態維持
            btn.addEventListener('click', () => switchToTab(panel, tabKey));
            btn.addEventListener('dragstart', (e) => {
                btn.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', tabKey);
            });
            btn.addEventListener('dragend', () => { btn.classList.remove('dragging'); });
            container.appendChild(btn);
        });
        container.addEventListener('dragover', (e) => {
            e.preventDefault();
            const afterElement = getDragAfterElementHorizontal(container, e.clientX);
            const dragging = document.querySelector('.tm-tab.dragging');
            if (afterElement == null) container.appendChild(dragging); else container.insertBefore(dragging, afterElement);
        });
        container.addEventListener('drop', (e) => {
            e.preventDefault();
            const newOrder = [...container.querySelectorAll('.tm-tab')].map(el => el.dataset.tab);
            const currentFullOrder = StorageManager.getTabOrder();
            const hiddenTabs = currentFullOrder.filter(t => !newOrder.includes(t));
            const finalOrder = [...newOrder, ...hiddenTabs];
            StorageManager.setTabOrder(finalOrder);
        });
    }

    function getDragAfterElementHorizontal(container, x) {
        const draggableElements = [...container.querySelectorAll('.tm-tab:not(.dragging)')];
        return draggableElements.reduce((closest, child) => {
            const box = child.getBoundingClientRect();
            const offset = x - box.left - box.width / 2;
            if (offset < 0 && offset > closest.offset) return { offset: offset, element: child }; else return closest;
        }, { offset: Number.NEGATIVE_INFINITY }).element;
    }

    function renderTabVisibilitySettings() {
        const container = document.getElementById('tm-tab-visibility-settings');
        if (!container) return;
        container.innerHTML = '';
        const visibility = StorageManager.getTabVisibility();
        const order = StorageManager.getTabOrder();
        DEFAULT_TAB_ORDER.forEach(def => { if (!order.includes(def)) order.push(def); });
        order.forEach(tabKey => {
            const row = document.createElement('div');
            row.className = 'tm-tab-visibility-item';
            const label = document.createElement('span');
            label.textContent = t(`tab_${tabKey}`);
            label.style.color = '#e0e0e0';
            label.style.fontSize = '13px';
            const toggle = document.createElement('div');
            toggle.className = 'tm-toggle-switch';
            if (visibility[tabKey]) toggle.classList.add('active');
            toggle.addEventListener('click', () => {
                const newVis = !toggle.classList.contains('active');
                if (tabKey === 'settings' && !newVis) {
                    if (!confirm(t('confirm_settings_hidden'))) return;
                }
                if (newVis) toggle.classList.add('active'); else toggle.classList.remove('active');
                const currentVis = StorageManager.getTabVisibility();
                currentVis[tabKey] = newVis;
                StorageManager.setTabVisibility(currentVis);
                const panel = document.querySelector('.tm-panel');
                renderTabs(panel);
            });
            row.appendChild(label); row.appendChild(toggle); container.appendChild(row);
        });
    }

    function initLoginSettings() {
        const toggle = document.getElementById('tm-auto-login-toggle');
        if (!toggle) return;
        if (StorageManager.isAutoLoginEnabled()) toggle.classList.add('active');
        toggle.addEventListener('click', () => {
            const newState = !StorageManager.isAutoLoginEnabled();
            StorageManager.setAutoLoginEnabled(newState);
            toggle.classList.toggle('active', newState);
        });
    }

    function switchToTab(panel, tabName) {
        panel.querySelectorAll('.tm-tab').forEach(t => t.classList.remove('active'));
        panel.querySelectorAll('.tm-tab-content').forEach(c => c.classList.remove('active'));
        const targetBtn = panel.querySelector(`.tm-tab[data-tab="${tabName}"]`);
        if (targetBtn) targetBtn.classList.add('active');
        const targetContent = document.getElementById(`tm-${tabName}`);
        if (targetContent) targetContent.classList.add('active');
        StorageManager.setLastActiveTab(tabName);
        const restore = () => restoreScrollPosition(panel, tabName);
        if (tabName === 'liked') loadLikedVideos().then(restore);
        else if (tabName === 'history') loadHistory().then(restore);
        else if (tabName === 'playlists') loadPlaylists().then(restore);
        else if (tabName === 'feed') { setupFeedTab(); restore(); } // restoreを追加
        else if (tabName === 'friends') { setupFriendsTab(); restore(); } // restoreを追加
        else restore();
    }

    // ========================================
    // HTML生成 & イベント (既存機能)
    // ========================================
    function generateVideoCard(v, extraButton = '', addedTime = null) {
        const iconSrc = v.authorIcon || 'https://www.tokyomotion.net/img/user-avatar.png';
        const authorLink = v.author ? `/user/${v.author}/videos` : '#';
        const relativeTime = formatRelativeTime(v.date);
        const privateLabel = v.isPrivate ? `<div class="tm-card-private">${t('label_private')}</div>` : '';
        const addedTimeDisplay = addedTime ? `<div class="tm-added-time">${formatDate(addedTime)}</div>` : '';
        return `
            <div class="tm-card" data-url="${v.url}">
                ${extraButton}
                <div class="tm-card-thumb-box">
                    <img src="${v.thumbnail || ''}" class="tm-card-thumb" onerror="this.style.display='none'">
                    ${v.duration ? `<div class="tm-card-duration">${v.duration}</div>` : ''}
                    ${privateLabel}
                </div>
                <div class="tm-card-title">${v.title}</div>
                <div class="tm-card-meta">
                    ${v.author ? `
                    <div class="tm-user-row">
                        <img src="${iconSrc}" class="tm-user-icon" onerror="this.onerror=null;this.src='https://www.tokyomotion.net/img/user-avatar.png'">
                        <a href="${authorLink}" class="tm-user-link" target="_blank">${v.author}</a>
                        ${relativeTime ? `<span class="tm-relative-time">${relativeTime}</span>` : ''}
                    </div>
                    ` : ''}
                    ${addedTimeDisplay}
                </div>
            </div>
        `;
    }

    function attachCardEvents(container) {
        container.querySelectorAll('.tm-card').forEach(card => {
            card.addEventListener('click', (e) => {
                if (e.target.closest('a') || e.target.closest('button')) return;
                window.location.href = card.dataset.url;
            });
        });
        container.querySelectorAll('.tm-user-link').forEach(link => { link.addEventListener('click', (e) => e.stopPropagation()); });
        container.querySelectorAll('.tm-card-thumb').forEach(img => {
            if (!img.src || !img.src.includes('/media/videos/')) return;
            const baseUrl = img.src.substring(0, img.src.lastIndexOf('/') + 1);
            img.dataset.baseurl = baseUrl;
            img.addEventListener('mousemove', function (e) {
                if (!this.dataset.preloaded) {
                    this.dataset.preloaded = 'true';
                    for (let i = 1; i <= 20; i++) (new Image()).src = `${this.dataset.baseurl}${i}.jpg`;
                }
                const rect = this.getBoundingClientRect(); const x = e.clientX - rect.left;
                let percent = (x / rect.width) * 100; let num = Math.ceil(percent / 5); num = Math.max(1, Math.min(20, num));
                const targetSrc = `${this.dataset.baseurl}${num}.jpg`;
                if (this.src !== targetSrc) this.src = targetSrc;
            });
        });
    }

    async function loadLikedVideos(sortOrder = 'desc') {
        const container = document.getElementById('tm-liked');
        let videos = await StorageManager.getLikedVideos();
        if (videos.length === 0) { container.innerHTML = `<div class="tm-empty">${t('msg_empty_liked')}</div>`; return; }
        videos.sort((a, b) => sortOrder === 'desc' ? b.timestamp - a.timestamp : a.timestamp - b.timestamp);
        container.innerHTML = `
            <div style="margin-bottom:10px; text-align:right;">
                <button class="tm-btn-secondary" id="tm-sort-liked" style="width:auto; padding:4px 8px; font-size:11px;">${sortOrder === 'desc' ? t('btn_sort_new') : t('btn_sort_old')}</button>
            </div>
            <div class="tm-grid-view">
                ${videos.map(v => generateVideoCard(v, `<button class="tm-card-remove" data-remove-liked="${v.id}" title="${t('btn_remove')}">×</button>`, v.timestamp)).join('')}
            </div>
        `;
        document.getElementById('tm-sort-liked').addEventListener('click', (e) => { e.stopPropagation(); loadLikedVideos(sortOrder === 'desc' ? 'asc' : 'desc'); });
        container.querySelectorAll('[data-remove-liked]').forEach(btn => {
            btn.addEventListener('click', async (e) => {
                e.stopPropagation();
                if (confirm(t('confirm_delete_liked'))) { await StorageManager.removeLikedVideo(btn.dataset.removeLiked); loadLikedVideos(sortOrder); }
            });
        });
        attachCardEvents(container);
    }

    async function loadHistory(sortOrder = 'desc') {
        const container = document.getElementById('tm-history');
        let videos = await StorageManager.getHistory();
        if (videos.length === 0) { container.innerHTML = `<div class="tm-empty">${t('msg_empty_history')}</div>`; return; }
        videos.sort((a, b) => sortOrder === 'desc' ? b.watchedAt - a.watchedAt : a.watchedAt - b.watchedAt);
        container.innerHTML = `
            <div style="margin-bottom:10px; text-align:right;">
                <button class="tm-btn-secondary" id="tm-sort-history" style="width:auto; padding:4px 8px; font-size:11px;">${sortOrder === 'desc' ? t('btn_sort_new') : t('btn_sort_old')}</button>
            </div>
            <div class="tm-grid-view">
                ${videos.map(v => generateVideoCard(v, '', v.watchedAt)).join('')}
            </div>
        `;
        document.getElementById('tm-sort-history').addEventListener('click', (e) => { e.stopPropagation(); loadHistory(sortOrder === 'desc' ? 'asc' : 'desc'); });
        attachCardEvents(container);
    }

    async function loadPlaylists() {
        const activeName = StorageManager.getActivePlaylist();
        const playlists = await StorageManager.getPlaylists();
        if (activeName && playlists[activeName]) { showPlaylistDetail(activeName); return; }
        const container = document.getElementById('tm-playlists');
        const names = await StorageManager.getOrderedPlaylistNames();
        const currentCols = StorageManager.getPlaylistGridCols();
        container.innerHTML = `
            <div style="margin-bottom:15px; display:flex; gap:5px; align-items:center;">
                <input type="text" class="tm-input" id="tm-new-playlist-name" placeholder="${t('placeholder_new_playlist')}" style="margin:0; flex:1;">
                <button class="tm-btn-primary" id="tm-create-playlist" style="flex:0 0 60px;">${t('btn_create')}</button>
                <select id="tm-playlist-col-selector" class="tm-col-select" style="margin-left:auto; background:#333;">
                    <option value="1">1${t('stg_col_unit')}</option>
                    <option value="2">2${t('stg_col_unit')}</option>
                    <option value="3">3${t('stg_col_unit')}</option>
                    <option value="4">4${t('stg_col_unit')}</option>
                    <option value="5">5${t('stg_col_unit')}</option>
                </select>
            </div>
            ${names.length === 0 ? `<div class="tm-empty">${t('msg_empty_playlist')}</div>` : `
                <div class="tm-playlist-grid" id="tm-playlist-grid" style="grid-template-columns: repeat(${currentCols}, 1fr);">
                    ${names.map((name) => `
                        <div class="tm-playlist-card" data-playlist="${name}" draggable="true">
                            <button class="tm-playlist-delete" data-delete="${name}">×</button>
                            <div class="tm-playlist-name">${name}</div>
                            <div class="tm-playlist-count">${t('time_videos_count', { count: playlists[name].length })}</div>
                        </div>
                    `).join('')}
                </div>
            `}
        `;
        const colSelector = document.getElementById('tm-playlist-col-selector');
        if (colSelector) {
            colSelector.value = currentCols;
            colSelector.addEventListener('change', (e) => {
                const val = parseInt(e.target.value); StorageManager.setPlaylistGridCols(val); loadPlaylists();
            });
        }
        document.getElementById('tm-create-playlist').addEventListener('click', async (e) => {
            e.stopPropagation();
            const input = document.getElementById('tm-new-playlist-name'); const name = input.value.trim();
            if (!name) return;
            if (await StorageManager.createPlaylist(name)) {
                const order = StorageManager.getPlaylistOrder(); order.push(name); StorageManager.setPlaylistOrder(order); loadPlaylists();
            } else alert(t('alert_exists'));
        });
        document.getElementById('tm-new-playlist-name').addEventListener('click', e => e.stopPropagation());
        container.querySelectorAll('[data-playlist]').forEach(card => {
            card.addEventListener('click', (e) => {
                e.stopPropagation(); if (!e.target.dataset.delete) showPlaylistDetail(card.dataset.playlist);
            });
            setupDragAndDrop(card, container);
        });
        container.querySelectorAll('[data-delete]').forEach(btn => {
            btn.addEventListener('click', async (e) => {
                e.stopPropagation();
                if (confirm(t('confirm_delete_playlist', { name: btn.dataset.delete }))) {
                    await StorageManager.deletePlaylist(btn.dataset.delete);
                    const order = StorageManager.getPlaylistOrder().filter(n => n !== btn.dataset.delete);
                    StorageManager.setPlaylistOrder(order); loadPlaylists();
                }
            });
        });
    }

    function setupDragAndDrop(card, container) {
        card.addEventListener('dragstart', (e) => { e.dataTransfer.effectAllowed = 'move'; container.draggedEl = card; card.style.opacity = '0.5'; });
        card.addEventListener('dragend', () => { card.style.opacity = '1'; container.draggedEl = null; });
        card.addEventListener('dragover', (e) => e.preventDefault());
        card.addEventListener('drop', (e) => {
            e.preventDefault(); e.stopPropagation();
            if (container.draggedEl && container.draggedEl !== card) {
                const dragged = container.draggedEl; const target = card; const parent = target.parentNode;
                const temp = document.createTextNode('');
                parent.insertBefore(temp, target); parent.insertBefore(target, dragged); parent.insertBefore(dragged, temp); temp.remove();
                const newOrder = [...container.querySelectorAll('.tm-playlist-card')].map(c => c.dataset.playlist);
                StorageManager.setPlaylistOrder(newOrder);
            }
        });
    }

    async function showPlaylistDetail(name) {
        StorageManager.setActivePlaylist(name);
        const container = document.getElementById('tm-playlists');
        const playlists = await StorageManager.getPlaylists();
        const videos = playlists[name] || [];
        container.innerHTML = `
            <button class="tm-btn-secondary" id="tm-back-to-playlists" style="width:100%;margin-bottom:10px;">${t('btn_back')}</button>
            <div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
                <h3 style="color:#e0e0e0; margin:0;">${name}</h3>
                <button class="tm-btn-icon" id="tm-rename-playlist" title="${t('btn_rename')}">✏️</button>
            </div>
            ${videos.length === 0 ? `<div class="tm-empty">${t('msg_empty_videos')}</div>` : `
                <div class="tm-grid-view">
                    ${videos.map(v => generateVideoCard(v, `<button class="tm-card-remove" data-remove-from="${v.id}" title="${t('btn_remove')}">×</button>`, v.timestamp)).join('')}
                </div>
            `}
        `;
        document.getElementById('tm-back-to-playlists').addEventListener('click', (e) => { e.stopPropagation(); StorageManager.setActivePlaylist(null); loadPlaylists(); });
        document.getElementById('tm-rename-playlist').addEventListener('click', async (e) => {
            e.stopPropagation();
            const newName = prompt(t('prompt_playlist_name'), name);
            if (newName && newName.trim() && newName !== name) {
                const success = await StorageManager.renamePlaylist(name, newName.trim());
                if (success) showPlaylistDetail(newName.trim()); else alert(t('alert_name_used'));
            }
        });
        container.querySelectorAll('[data-remove-from]').forEach(btn => {
            btn.addEventListener('click', async (e) => { e.stopPropagation(); await StorageManager.removeFromPlaylist(name, btn.dataset.removeFrom); showPlaylistDetail(name); });
        });
        attachCardEvents(container);
    }

    function setupFeedTab() {
        _setupFeedLogic('feed', document.getElementById('tm-feed-update'), document.getElementById('tm-feed-status'), document.getElementById('tm-feed-list'), document.getElementById('tm-feed-time-ago'), document.getElementById('tm-feed-time-absolute'));
    }

    function setupFriendsTab() {
        _setupFeedLogic('friends', document.getElementById('tm-friends-update'), document.getElementById('tm-friends-status'), document.getElementById('tm-friends-list'), document.getElementById('tm-friends-time-ago'), document.getElementById('tm-friends-time-absolute'));
    }

    function _setupFeedLogic(type, btn, status, list, timeAgoEl, timeAbsEl) {
        if (!btn) return;

        // 言語設定に合わせてボタンのテキストを更新
        const labelKey = type === 'feed' ? 'tab_feed' : 'tab_friends';
        btn.textContent = `${t(labelKey)} ${t('btn_update')}`;

        const saved = type === 'feed' ? StorageManager.getFeedData() : StorageManager.getFriendsFeedData();
        const lastUpdated = type === 'feed' ? StorageManager.getFeedLastUpdated() : StorageManager.getFriendsLastUpdated();
        const updateTimeDisplay = (timestamp) => {
            if (timestamp > 0) { timeAgoEl.innerText = calcTimeAgo(timestamp); timeAbsEl.innerText = formatDate(timestamp); }
            else { timeAgoEl.innerText = '-'; timeAbsEl.innerText = '-'; }
        };
        updateTimeDisplay(lastUpdated);
        if (saved.length) {
            const privateIds = saved.filter(v => v.isPrivate).map(v => v.id);
            if (privateIds.length > 0) StorageManager.addPrivateToCache(privateIds);
            renderFeed(saved, list);
        }
        const newBtn = btn.cloneNode(true);
        btn.parentNode.replaceChild(newBtn, btn);
        newBtn.addEventListener('click', async (e) => {
            e.stopPropagation(); newBtn.disabled = true; status.innerText = t('msg_fetching');
            try {
                const users = type === 'feed' ? await SubscriptionManager.getFollowedUsers(msg => status.innerText = msg) : await SubscriptionManager.getFriends(msg => status.innerText = msg);
                if (users.length === 0) { status.innerText = t('msg_no_users'); return; }
                status.innerText = t('msg_fetching_users', { count: users.length });
                let allVideos = [], completed = 0;
                const maxPages = type === 'feed' ? StorageManager.getFeedMaxPages() : StorageManager.getFriendsMaxPages();
                for (let i = 0; i < users.length; i += 5) {
                    const chunk = users.slice(i, i + 5);
                    await Promise.all(chunk.map(async u => { try { allVideos.push(...await SubscriptionManager.getUserVideos(u, maxPages)); } catch (e) { } }));
                    completed += chunk.length;
                    status.innerText = t('msg_fetching_progress', { current: Math.min(completed, users.length), total: users.length });
                }
                status.innerText = t('msg_complete', { count: allVideos.length });
                const now = Date.now();
                if (type === 'feed') { StorageManager.setFeedData(allVideos); StorageManager.setFeedLastUpdated(now); }
                else { StorageManager.setFriendsFeedData(allVideos); StorageManager.setFriendsLastUpdated(now); }
                updateTimeDisplay(now); renderFeed(allVideos, list);
            } catch (e) { status.innerText = t('msg_error', { msg: e.message }); } finally { newBtn.disabled = false; }
        });
    }

    function renderFeed(videos, container) {
        if (!videos.length) { container.innerHTML = `<div class="tm-empty">${t('msg_empty_feed')}</div>`; return; }
        const unique = []; const seen = new Set();
        videos.forEach(v => { if (!seen.has(v.id)) { seen.add(v.id); unique.push(v); } });
        unique.sort((a, b) => parseInt(b.id) - parseInt(a.id));
        container.innerHTML = `<div class="tm-grid-view">${unique.map(v => generateVideoCard(v)).join('')}</div>`;
        attachCardEvents(container);
    }

    function setupVideoPage() {
        const videoId = extractVideoId();
        if (!videoId) return;
        let historyRecorded = false;
        const handleVideoElement = (videoEl) => {
            if (videoEl.dataset.tmEnhanced) return;
            videoEl.dataset.tmEnhanced = 'true';
            const recordHistory = () => {
                if (historyRecorded) return;
                if (!videoEl.duration || isNaN(videoEl.duration) || videoEl.duration === Infinity) {
                    const waitForMeta = () => {
                        if (historyRecorded) return;
                        if (videoEl.duration && !isNaN(videoEl.duration) && videoEl.duration !== Infinity) {
                            save(); videoEl.removeEventListener('loadedmetadata', waitForMeta); videoEl.removeEventListener('durationchange', waitForMeta);
                        }
                    };
                    videoEl.addEventListener('loadedmetadata', waitForMeta); videoEl.addEventListener('durationchange', waitForMeta);
                    return;
                }
                save();
            };
            const save = () => {
                let duration = formatSeconds(videoEl.duration); if (!duration) duration = getDurationFromPlayer();
                const videoData = extractVideoData(duration);
                if (videoData) { StorageManager.addToHistory(videoData); historyRecorded = true; }
            };
            videoEl.addEventListener('play', recordHistory);
            videoEl.addEventListener('timeupdate', () => { if (!historyRecorded && videoEl.currentTime > 0.5) recordHistory(); });
        };
        const v = document.querySelector('video'); if (v) handleVideoElement(v);
        new MutationObserver((mutations) => {
            for (const m of mutations) for (const n of m.addedNodes) {
                if (n.tagName === 'VIDEO') handleVideoElement(n);
                if (n.querySelector) { const v = n.querySelector('video'); if (v) handleVideoElement(v); }
            }
        }).observe(document.body, { childList: true, subtree: true });
        setupLikeObserver(videoId);
        injectPlaylistButton(videoId);
    }

    function setupLikeObserver(videoId) {
        const observer = new MutationObserver(() => {
            const likeCountEl = document.querySelector('#video_likes');
            if (likeCountEl && !likeCountEl.dataset.observed) {
                likeCountEl.dataset.observed = 'true';
                let lastCount = parseInt(likeCountEl.innerText) || 0;
                new MutationObserver(() => {
                    const current = parseInt(likeCountEl.innerText);
                    if (current > lastCount) {
                        const data = extractVideoData();
                        if (data) { StorageManager.addLikedVideo(data); showToast(t('msg_saved_liked')); }
                    }
                    lastCount = current;
                }).observe(likeCountEl, { childList: true, characterData: true, subtree: true });
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function injectPlaylistButton(videoId) {
        const findTarget = () => {
            const selectors = [`#favorite_video_${videoId}`, `#vote_like_${videoId}`, '.fa-heart', '.fa-thumbs-up', '.fa-share-alt'];
            for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el.closest('a, button') || el; }
            return document.querySelector('.video-actions') || document.querySelector('.video-info .pull-right');
        };
        const attemptInject = () => {
            if (document.querySelector('.tm-playlist-btn-inline')) return true;
            const targetBtn = findTarget();
            if (targetBtn && targetBtn.parentNode) {
                const btn = document.createElement('a');
                const btnClass = targetBtn.tagName === 'BUTTON' ? 'btn btn-default' : (targetBtn.className || 'btn btn-default');
                btn.className = btnClass + ' tm-playlist-btn-inline';
                btn.href = 'javascript:void(0);';
                btn.innerHTML = `<i class="fa fa-folder-open"></i><span style="margin-left:4px;">${t('modal_title')}</span>`;
                btn.title = 'TokyoMotion Enhancer List';
                btn.style.marginLeft = '5px';
                btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openPlaylistModal(videoId); });
                targetBtn.parentNode.insertBefore(btn, targetBtn.nextSibling);
                return true;
            }
            return false;
        };
        if (!attemptInject()) {
            const observer = new MutationObserver((mutations, obs) => { if (attemptInject()) obs.disconnect(); });
            observer.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => observer.disconnect(), 10000);
        }
    }

    async function openPlaylistModal(videoId) {
        const existing = document.querySelector('.tm-modal-overlay'); if (existing) existing.remove();
        const currentDuration = getDurationFromPlayer();
        const videoData = extractVideoData(currentDuration);
        const playlists = await StorageManager.getPlaylists();
        const names = Object.keys(playlists);
        const currentCols = StorageManager.getModalCols();
        const overlay = document.createElement('div');
        overlay.className = 'tm-modal-overlay';
        overlay.innerHTML = `
            <div class="tm-modal-content">
                <div class="tm-modal-header">
                    <div style="display:flex; align-items:center; gap:10px;">
                        <span>${t('modal_title')}</span>
                        <select id="tm-col-selector" class="tm-col-select" title="Cols">
                            <option value="1">1${t('stg_col_unit')}</option>
                            <option value="2">2${t('stg_col_unit')}</option>
                            <option value="3">3${t('stg_col_unit')}</option>
                            <option value="4">4${t('stg_col_unit')}</option>
                            <option value="5">5${t('stg_col_unit')}</option>
                        </select>
                    </div>
                    <button class="tm-panel-close">×</button>
                </div>
                <div class="tm-playlist-list" style="grid-template-columns: repeat(${currentCols}, 1fr);">
                    ${names.map(name => {
            const checked = playlists[name].some(v => v.id === videoId) ? 'checked' : '';
            return `<label class="tm-playlist-item"><input type="checkbox" data-name="${name}" ${checked}> ${name}</label>`;
        }).join('')}
                </div>
                <div class="tm-modal-footer">
                    <div class="tm-new-playlist-form">
                        <input type="text" class="tm-input" id="tm-modal-new-name" placeholder="${t('placeholder_new_playlist')}" style="margin:0;">
                        <button class="tm-btn-primary" id="tm-modal-create">${t('btn_create')}</button>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);
        const updateModalSize = (cols) => {
            const content = overlay.querySelector('.tm-modal-content');
            const list = overlay.querySelector('.tm-playlist-list');
            list.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
            let width = 320;
            if (cols === 2) width = 450; if (cols === 3) width = 600; if (cols === 4) width = 750; if (cols === 5) width = 900;
            content.style.width = `${width}px`;
        };
        updateModalSize(currentCols);
        overlay.querySelector('#tm-col-selector').value = currentCols;
        overlay.querySelector('.tm-panel-close').addEventListener('click', () => overlay.remove());
        overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
        overlay.querySelector('#tm-col-selector').addEventListener('change', (e) => {
            const val = parseInt(e.target.value); StorageManager.setModalCols(val); updateModalSize(val);
        });
        overlay.querySelectorAll('input[type="checkbox"]').forEach(cb => {
            cb.addEventListener('change', async (e) => {
                const name = e.target.dataset.name;
                if (e.target.checked) { await StorageManager.addToPlaylist(name, videoData); showToast(t('msg_added_to', { name })); }
                else { await StorageManager.removeFromPlaylist(name, videoId); showToast(t('msg_removed_from', { name })); }
            });
        });
        overlay.querySelector('#tm-modal-create').addEventListener('click', async () => {
            const name = overlay.querySelector('#tm-modal-new-name').value.trim();
            if (name && await StorageManager.createPlaylist(name)) {
                await StorageManager.addToPlaylist(name, videoData); showToast(t('msg_created_added', { name })); overlay.remove();
            }
        });
    }

    function attemptAutoLogin() {
        if (!StorageManager.isAutoLoginEnabled()) return;
        const loginLink = document.querySelector('a[href="#login-modal"]'); if (!loginLink) return;
        const modal = document.getElementById('login-modal');
        if (!(modal && (modal.style.display === 'block' || modal.classList.contains('in')))) loginLink.click();
        let attempts = 0;
        const interval = setInterval(() => {
            const user = document.getElementById('login_username'); const pass = document.getElementById('login_password'); const btn = document.getElementById('login_submit');
            if (user && pass && btn) {
                if (!user.value && document.activeElement !== user) { user.focus(); user.click(); }
                else if (user.value && !pass.value && document.activeElement !== pass) { pass.focus(); pass.click(); }
                if (user.value && pass.value) { clearInterval(interval); showToast(t('msg_auto_login')); btn.click(); }
            }
            if (++attempts > 50) clearInterval(interval);
        }, 100);
    }

    function exportData() {
        const data = {
            liked: GM_getValue('likedVideos', []),
            history: GM_getValue('history', []),
            playlists: GM_getValue('playlists', {}),
            playlistOrder: GM_getValue('playlistOrder', []),
            settings: {
                defaultTab: GM_getValue('defaultTab', 'liked'),
                autoLogin: GM_getValue('autoLoginEnabled', false),
                tabOrder: GM_getValue('tabOrder', DEFAULT_TAB_ORDER),
                tabVisibility: GM_getValue('tabVisibility', {}),
                panelState: GM_getValue('panelState', null),
                btnPosition: GM_getValue('btnPosition', null)
            }
        };
        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a'); a.href = url;
        a.download = `tokyomotion_backup_${new Date().toISOString().slice(0, 10)}.json`;
        a.click(); URL.revokeObjectURL(url);
    }

    function importData() {
        const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json';
        input.onchange = (e) => {
            const file = e.target.files[0]; if (!file) return;
            const reader = new FileReader();
            reader.onload = (ev) => {
                try {
                    const data = JSON.parse(ev.target.result);
                    if (confirm(t('confirm_overwrite'))) {
                        if (data.liked) GM_setValue('likedVideos', data.liked);
                        if (data.history) GM_setValue('history', data.history);
                        if (data.playlists) GM_setValue('playlists', data.playlists);
                        if (data.playlistOrder) GM_setValue('playlistOrder', data.playlistOrder);
                        if (data.settings) {
                            GM_setValue('defaultTab', data.settings.defaultTab);
                            GM_setValue('autoLoginEnabled', data.settings.autoLogin);
                            if (data.settings.tabOrder) GM_setValue('tabOrder', data.settings.tabOrder);
                            if (data.settings.tabVisibility) GM_setValue('tabVisibility', data.settings.tabVisibility);
                            if (data.settings.panelState) GM_setValue('panelState', data.settings.panelState);
                            if (data.settings.btnPosition) GM_setValue('btnPosition', data.settings.btnPosition);
                        }
                        alert(t('msg_import_done')); location.reload();
                    }
                } catch (err) { alert(t('msg_import_error', { msg: err })); }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    function clearAllData() {
        if (confirm(t('confirm_clear_all'))) {
            GM_setValue('likedVideos', []); GM_setValue('history', []); GM_setValue('playlists', {}); GM_setValue('feedData', []); GM_setValue('friendsFeedData', []);
            alert(t('msg_data_cleared')); location.reload();
        }
    }

    function applyVideoGridCols() {
        const cols = StorageManager.getVideoGridCols();
        document.documentElement.style.setProperty('--tm-video-cols', cols);
    }

    function init() {
        if (window.self !== window.top) return;

        // 終了時間の記録
        window.addEventListener('beforeunload', () => {
            StorageManager.setLastClosedTime(Date.now());
        });

        // 起動時のリセットチェック
        const lastClosed = StorageManager.getLastClosedTime();
        if (lastClosed > 0) {
            const diff = Date.now() - lastClosed;
            const threshold = getScrollResetMs();
            if (diff > threshold) {
                // リセット対象のデータをクリア
                const tabsToReset = ['liked', 'history', 'playlists', 'feed', 'friends'];
                tabsToReset.forEach(tab => {
                    StorageManager.setTabScroll(tab, 0);
                });
                StorageManager.setActivePlaylist(null);

                // 次回起動時にリセット通知を出すためのフラグを立てる(オプション)
                // 今回は単純にすべてのタブスクロールを0にするため、UI生成時にそれが反映されるはず
                // ただし、パネルがまだ生成されていないので、パネル生成後に適用される必要がある。
                // restoreScrollPosition はパネル生成後に呼ばれるので、ここで値を0にしておけば0が復元される。

                // 通知はDOMがないので出せないが、コンソールに出しておく
                console.log(`[TokyoMotion Enhancer] Startup scroll reset triggered. (Closed for ${diff}ms > ${threshold}ms)`);
                // DOMが準備できているはずなので通知を試みる
                if (document.body) {
                    setTimeout(() => showToast(t('msg_scroll_reset')), 500); // UI描画と被らないよう少し遅延
                } else {
                    document.addEventListener('DOMContentLoaded', () => setTimeout(() => showToast(t('msg_scroll_reset')), 500));
                }
            }
        }

        createMainUI();
        if (location.pathname.startsWith('/video/')) setupVideoPage();
        if (StorageManager.isAutoLoginEnabled()) {
            if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', attemptAutoLogin);
            else attemptAutoLogin();
        }
        PrivateScanner.startObserver();
    }

    init();
})();