TokyoMotion Enhancer

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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