// ==UserScript== // @name Advanced Search for X (Twitter) 🔍 // @name:ja Advanced Search for X(Twitter)🔍 // @name:en Advanced Search for X (Twitter) 🔍 // @name:zh-CN Advanced Search for X(Twitter)🔍 // @name:zh-TW Advanced Search for X(Twitter)🔍 // @name:ko Advanced Search for X (Twitter) 🔍 // @name:fr Advanced Search for X (Twitter) 🔍 // @name:es Advanced Search for X (Twitter) 🔍 // @name:de Advanced Search for X (Twitter) 🔍 // @name:pt-BR Advanced Search for X (Twitter) 🔍 // @name:ru Advanced Search for X (Twitter) 🔍 // @version 6.0.0 // @description Adds a floating modal for advanced search on X.com (Twitter). Syncs with search box and remembers position/display state. The top-right search icon is now draggable and its position persists. // @description:ja X.com(Twitter)に高度な検索機能を呼び出せるフローティング・モーダルを追加します。検索ボックスと双方向で同期し、位置や表示状態も記憶します。右上の検索アイコンはドラッグで移動でき、位置は保存されます。 // @description:en Adds a floating modal for advanced search on X.com (formerly Twitter). Syncs with search box and remembers position/display state. The top-right search icon is draggable with persistent position. // @description:zh-CN 为X.com(Twitter)添加高级搜索浮动模态框,支持与搜索框双向同步并记住位置与显示状态。右上角的搜索图标可拖动,并会记住位置。 // @description:zh-TW 為 X.com(Twitter)增加高級搜尋模態框,支援與搜尋框雙向同步並記住位置與顯示狀態。右上角搜尋圖示可拖曳,位置會被保存。 // @description:ko X.com(Twitter)에 고급 검색 모달을 추가합니다. 검색창과 양방향 동기화하며 위치와 표시 상태를 기억합니다. 우상단 검색 아이콘은 드래그 이동 및 위치 저장이 가능합니다. // @description:fr Ajoute une fenêtre modale de recherche avancée à X.com (Twitter), synchronisée avec la barre de recherche et mémorise de l’état d’affichage. L’icône de recherche en haut à droite est déplaçable. // @description:es Agrega un modal flotante de búsqueda avanzada en X.com (Twitter), sincronizado con la caja de búsqueda y con estado persistente. // @description:de Fügt X.com (Twitter) ein modales Fenster für erweiterte Suche hinzu, synchronisiert mit der Suchleiste und speichert Position/Zustand. Das Suchsymbol oben rechts ist per Drag & Drop verschiebbar und bleibt gespeichert. // @description:pt-BR Adiciona um modal de busca avançada flutuante no X.com (Twitter), sincronizado com a caixa de busca e com estado salvo. O ícone de busca no canto superior direito é arrastável com posição persistente. // @description:ru Добавляет модальное окно расширенного поиска на X.com (Twitter). Синхронизируется с поисковой строкой и запоминает состояние. Кнопку поиска в правом верхнем углу можно перетаскивать; её положение сохраняется. // @namespace https://github.com/koyasi777/advanced-search-for-x-twitter // @author koyasi777 // @match https://x.com/* // @match https://twitter.com/* // @exclude https://x.com/i/tweetdeck* // @exclude https://twitter.com/i/tweetdeck* // @icon https://raw.githubusercontent.com/koyasi777/advanced-search-for-x-twitter/refs/heads/main/extension/icons/icon-128.png // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @run-at document-idle // @license MIT // @homepageURL https://github.com/koyasi777/advanced-search-for-x-twitter // @supportURL https://github.com/koyasi777/advanced-search-for-x-twitter/issues // @downloadURL none // ==/UserScript== (function() { 'use strict'; if (window.__X_ADV_SEARCH_INITED__) return; window.__X_ADV_SEARCH_INITED__ = true; const i18n = { translations: { 'en': { modalTitle: "Advanced Search", tooltipClose: "Close", labelAllWords: "All of these words", placeholderAllWords: "e.g., AI news", labelExactPhrase: "This exact phrase", placeholderExactPhrase: 'e.g., "ChatGPT 4o"', labelAnyWords: "Any of these words (OR)", placeholderAnyWords: "e.g., iPhone Android", labelNotWords: "None of these words (-)", placeholderNotWords: "e.g., -sale -ads", labelHashtag: "Hashtags (#)", placeholderHashtag: "e.g., #TechEvent", labelLang: "Language (lang:)", optLangDefault: "Any language", optLangJa: "Japanese (ja)", optLangEn: "English (en)", optLangId: "Indonesian (id)", optLangHi: "Hindi (hi)", optLangDe: "German (de)", optLangTr: "Turkish (tr)", optLangEs: "Spanish (es)", optLangPt: "Portuguese (pt)", optLangAr: "Arabic (ar)", optLangFr: "French (fr)", optLangKo: "Korean (ko)", optLangRu: "Russian (ru)", optLangZhHans: "Chinese Simplified (zh-cn)", optLangZhHant: "Chinese Traditional (zh-tw)", hrSeparator: " ", labelFilters: "Filters", labelVerified: "Verified accounts", labelLinks: "Links", labelImages: "Images", labelVideos: "Videos", labelReposts: "Reposts", labelTimelineHashtags: "Hashtags (#)", checkInclude: "Include", checkExclude: "Exclude", labelReplies: "Replies", optRepliesDefault: "Default (Show all)", optRepliesInclude: "Include replies", optRepliesOnly: "Replies only", optRepliesExclude: "Exclude replies", labelEngagement: "Engagement", placeholderMinReplies: "Min replies", placeholderMinLikes: "Min likes", placeholderMinRetweets: "Min retweets", labelDateRange: "Date range", tooltipSince: "From this date", tooltipUntil: "Until this date", labelFromUser: "From these accounts (from:)", placeholderFromUser: "e.g., @X", labelToUser: "To these accounts (to:)", placeholderToUser: "e.g., @google", labelMentioning: "Mentioning these accounts (@)", placeholderMentioning: "e.g., @OpenAI", buttonClear: "Clear", buttonApply: "Search", tooltipTrigger: "Open Advanced Search", buttonOpen: "Open", tabSearch: "Search", tabHistory: "History", tabSaved: "Saved", buttonSave: "Save", buttonSaved: "Saved", secretMode: "Secret", secretOn: "Secret mode ON (No history)", secretOff: "Secret mode OFF", toastSaved: "Saved.", toastDeleted: "Deleted.", toastReordered: "Order updated.", emptyHistory: "No history yet.", emptySaved: "No saved searches.", run: "Run", delete: "Delete", updated: "Updated.", tooltipSecret: "Toggle Secret Mode (no history will be recorded)", historyClearAll: "Clear All", confirmClearHistory: "Clear all history?", labelAccountScope: "Accounts", optAccountAll: "All accounts", optAccountFollowing: "Accounts you follow", labelLocationScope: "Location", optLocationAll: "All locations", optLocationNearby: "Near you", chipFollowing: "Following", chipNearby: "Nearby", labelSearchTarget: "Search target", labelHitName: "Exclude hits only in display name", labelHitHandle: "Exclude hits only in username (@handle)", hintSearchTarget: "Hide posts that only match in name or handle (not in body).", hintName: "If a keyword appears only in the display name, hide it.", hintHandle: "If a keyword appears only in @username, hide it. Exception: when the query explicitly uses from:/to:/@ with the same word.", tabMute: "Mute", labelMuteWord: "Add mute word", placeholderMuteWord: "e.g., spoiler", labelCaseSensitive: "Case sensitive", labelEnabled: "Enabled", labelEnableAll: "Enable all", buttonAdd: "Add", emptyMuted: "No muted words.", mutedListTitle: "Muted words", mutedListHeading: "Muted items", muteHit: "Mute hits in body", buttonImport: "Import", buttonExport: "Export", /* Accounts tab */ tabAccounts: "Accounts", emptyAccounts: "No accounts yet. Open a profile and click the Add button to save it.", buttonAddAccount: "Add account", toastAccountAdded: "Account added.", toastAccountExists: "Already added.", buttonConfirm: "Confirm", /* Lists tab */ tabLists: "Lists", emptyLists: "No lists yet. Open a List and click the + button in the top-right to add it.", buttonAddList: "Add list", toastListAdded: "List added.", toastListExists: "Already added.", /* History tab */ placeholderSearchHistory: "Search history (query)", labelSortBy: "Sort by:", placeholderSearchSaved: "Search saved (query)", sortNewest: "Newest first", sortOldest: "Oldest first", sortNameAsc: "Query (A-Z)", sortNameDesc: "Query (Z-A)", /* Folder/List/Account tabs */ placeholderFilterAccounts: "Filter accounts (@, name)", placeholderFilterLists: "Filter lists (name, url)", buttonAddFolder: "+Folder", folderFilterAll: "ALL", folderFilterUnassigned: "Unassigned", folderRename: "Rename", folderRenameTitle: "Rename folder", folderDelete: "Delete", folderDeleteTitle: "Delete folder", promptNewFolder: "New folder name", confirmDeleteFolder: "Delete this folder and all items inside it? This cannot be undone.", optListsAll: "Lists", defaultSavedFolders: "Saved Searches", /* Favorites */ tabFavorites: "Favorites", emptyFavorites: "No favorite tweets yet. Click the ★ icon on tweets to save them.", optFavoritesAll: "All Favorites", toastFavorited: "Added to favorites.", toastUnfavorited: "Removed from favorites.", /* Settings */ settingsTitle: "Settings", settingsTitleGeneral: "General", settingsTitleFeatures: "Tab Visibility", settingsTitleData: "Data", buttonClose: "Close", labelUILang: "Interface language", optUILangAuto: "Auto", labelImportExport: "Import / Export", placeholderSettingsJSON: "Paste backup JSON here...", tooltipSettings: "Open settings", toastImported: "Imported.", toastExported: "Exported to file.", buttonReset: "Reset all data", confirmResetAll: "Reset all data? This cannot be undone.", toastReset: "All data has been reset.", buttonImportSuccess: "Imported successfully 👍️", /* Favorites Sort */ sortSavedNewest: "Saved date (Newest)", sortSavedOldest: "Saved date (Oldest)", sortPostedNewest: "Posted date (Newest)", sortPostedOldest: "Posted date (Oldest)", /* --- Favorite Tags --- */ FT_UNCATEGORIZED: 'Uncategorized', FT_DROPDOWN_TITLE: 'Favorite Tags', FT_DROPDOWN_SETTINGS_TITLE: 'Favorite Tag Settings', FT_DROPDOWN_NEW_TAG: 'New tag', FT_DROPDOWN_NEW_TAG_PLACEHOLDER: 'Tag name', FT_DROPDOWN_NEW_TAG_ADD: 'Add', FT_FILTER_ALL: 'All', FT_SETTINGS_TITLE: 'Favorite Tag Settings', FT_SETTINGS_EMPTY_TAG_LIST: 'No tags yet. You can add one from "New tag".', FT_SETTINGS_UNCATEGORIZED_NAME: 'Uncategorized', FT_SETTINGS_UNCATEGORIZED_NAME_TOOLTIP: 'The name of "Uncategorized" cannot be changed.', FT_SETTINGS_UNCATEGORIZED_DELETE_TOOLTIP: '"Uncategorized" cannot be deleted.', FT_SETTINGS_CLOSE: 'Close', FT_SETTINGS_DELETE_BUTTON: 'Delete', FT_SETTINGS_UP: '▲', FT_SETTINGS_DOWN: '▼', FT_SETTINGS_DISPLAY_SECTION_TITLE: 'Display', FT_SETTINGS_DISPLAY_MODE_LABEL: 'Tag label format', FT_SETTINGS_DISPLAY_MODE_LEAF: 'Leaf only', FT_SETTINGS_DISPLAY_MODE_FULL: 'Full path', FT_CONFIRM_DELETE_TAG_MSG: 'Delete tag "{tagName}"?\nFavorites with this tag will become "Uncategorized".', FT_SETTINGS_BUTTON_TITLE: 'Favorite Tag Settings', }, 'ja': { modalTitle: "高度な検索", tooltipClose: "閉じる", labelAllWords: "すべての語句を含む", placeholderAllWords: "例: AI ニュース", labelExactPhrase: "この語句を完全に含む", placeholderExactPhrase: '例: "ChatGPT 4o"', labelAnyWords: "いずれかの語句を含む (OR)", placeholderAnyWords: "例: iPhone Android", labelNotWords: "含まない語句 (-)", placeholderNotWords: "例: -セール -広告", labelHashtag: "ハッシュタグ (#)", placeholderHashtag: "例: #技術書典", labelLang: "言語 (lang:)", optLangDefault: "指定しない", optLangJa: "日本語 (ja)", optLangEn: "英語 (en)", optLangId: "インドネシア語 (id)", optLangHi: "ヒンディー語 (hi)", optLangDe: "ドイツ語 (de)", optLangTr: "トルコ語 (tr)", optLangEs: "スペイン語 (es)", optLangPt: "ポルトガル語 (pt)", optLangAr: "アラビア語 (ar)", optLangFr: "フランス語 (fr)", optLangKo: "韓国語 (ko)", optLangRu: "ロシア語 (ru)", optLangZhHans: "中国語(簡体字)(zh-cn)", optLangZhHant: "中国語(繁体字)(zh-tw)", hrSeparator: " ", labelFilters: "フィルター", labelVerified: "認証済みアカウント", labelLinks: "リンク", labelImages: "画像", labelVideos: "動画", labelReposts: "リポスト", labelTimelineHashtags: "ハッシュタグ (#)", checkInclude: "含む", checkExclude: "含まない", labelReplies: "返信", optRepliesDefault: "指定しない", optRepliesInclude: "返信を含める", optRepliesOnly: "返信のみ", optRepliesExclude: "返信を除外", labelEngagement: "エンゲージメント", placeholderMinReplies: "最小返信数", placeholderMinLikes: "最小いいね数", placeholderMinRetweets: "最小リポスト数", labelDateRange: "期間指定", tooltipSince: "この日以降", tooltipUntil: "この日以前", labelFromUser: "このアカウントから (from:)", placeholderFromUser: "例: @X", labelToUser: "このアカウントへ (to:)", placeholderToUser: "例: @google", labelMentioning: "このアカウントへのメンション (@)", placeholderMentioning: "例: @OpenAI", buttonClear: "クリア", buttonApply: "検索実行", tooltipTrigger: "高度な検索を開く", buttonOpen: "開く", tabSearch: "検索", tabHistory: "履歴", tabSaved: "保存", buttonSave: "保存", buttonSaved: "保存済み", secretMode: "シークレット", secretOn: "シークレットモード ON(履歴は記録しません)", secretOff: "シークレットモード OFF", toastSaved: "保存しました。", toastDeleted: "削除しました。", toastReordered: "並び順を更新しました。", emptyHistory: "履歴はまだありません。", emptySaved: "保存済みの検索はありません。", run: "実行", delete: "削除", updated: "更新しました。", tooltipSecret: "シークレットモードを切り替え(履歴を記録しません)", historyClearAll: "すべて削除", confirmClearHistory: "履歴をすべて削除しますか?", labelAccountScope: "アカウント", optAccountAll: "すべてのアカウント", optAccountFollowing: "フォローしているアカウント", labelLocationScope: "場所", optLocationAll: "すべての場所", optLocationNearby: "近くの場所", chipFollowing: "フォロー中", chipNearby: "近く", labelSearchTarget: "検索対象", labelHitName: "表示名(名前)のみのヒットは除外", labelHitHandle: "ユーザー名(@)のみのヒットは除外", hintSearchTarget: "本文ではなく、名前/ユーザー名のみに一致した投稿を非表示にします。", hintName: "キーワードが表示名のみに含まれる場合は非表示にします。", hintHandle: "キーワードが @ユーザー名のみに含まれる場合は非表示にします。例外: 同じ語を from:/to:/@ で明示しているときは表示します。", tabMute: "ミュート", labelMuteWord: "ミュート語句の追加", placeholderMuteWord: "例: ネタバレ", labelCaseSensitive: "大文字小文字を区別", labelEnabled: "有効", labelEnableAll: "すべて有効", buttonAdd: "追加", emptyMuted: "ミュート語句はまだありません。", mutedListTitle: "ミュート語句", mutedListHeading: "ミュート一覧", muteHit: "本文でのヒットをミュート", buttonImport: "インポート", buttonExport: "エクスポート", /* Accounts tab */ tabAccounts: "アカウント", emptyAccounts: "アカウントはまだありません。アカウントページの追加ボタンから追加してください。", buttonAddAccount: "アカウントを追加", toastAccountAdded: "アカウントを追加しました。", toastAccountExists: "すでに追加済みです。", buttonConfirm: "確認", /* Lists tab */ tabLists: "リスト", emptyLists: "リストはまだありません。リストを開き右上の+ボタンから追加してください。", buttonAddList: "リストを追加", toastListAdded: "リストを追加しました。", toastListExists: "すでに追加済みです。", /* History tab */ placeholderSearchHistory: "履歴を検索(クエリ)", labelSortBy: "並び順:", placeholderSearchSaved: "保存済みを検索(クエリ)", sortNewest: "新しい順", sortOldest: "古い順", sortNameAsc: "クエリ (昇順)", sortNameDesc: "クエリ (降順)", /* Folder/List/Account tabs */ placeholderFilterAccounts: "アカウントを検索 (@, 名前)", placeholderFilterLists: "リストを検索 (名前, URL)", buttonAddFolder: "+フォルダー", folderFilterAll: "すべて", folderFilterUnassigned: "未分類", folderRename: "名前変更", folderRenameTitle: "フォルダー名を変更", folderDelete: "削除", folderDeleteTitle: "フォルダーを削除", promptNewFolder: "新しいフォルダー名", confirmDeleteFolder: "このフォルダーと中のすべてのアイテムを完全に削除しますか?この操作は元に戻せません。", optListsAll: "リスト", defaultSavedFolders: "保存済み検索", /* Favorites */ tabFavorites: "お気に入り", emptyFavorites: "お気に入りはまだありません。ツイートの★ボタンをクリックして保存できます。", optFavoritesAll: "すべてのお気に入り", toastFavorited: "お気に入りに追加しました。", toastUnfavorited: "お気に入りから削除しました。", /* Settings */ settingsTitle: "設定", settingsTitleGeneral: "一般設定", settingsTitleFeatures: "タブ表示設定", settingsTitleData: "データ管理", buttonClose: "閉じる", labelUILang: "UI 言語", optUILangAuto: "自動判定", labelImportExport: "インポート / エクスポート", placeholderSettingsJSON: "ここにバックアップ JSON を貼り付けてください...", tooltipSettings: "設定を開く", toastImported: "インポートしました。", toastExported: "ファイルにエクスポートしました。", buttonReset: "すべて初期化", confirmResetAll: "すべてのデータを初期化しますか?この操作は元に戻せません。", toastReset: "すべてのデータを初期化しました。", buttonImportSuccess: "インポートに成功しました👍️", /* Favorites Sort */ sortSavedNewest: "追加日 (新しい順)", sortSavedOldest: "追加日 (古い順)", sortPostedNewest: "投稿日 (新しい順)", sortPostedOldest: "投稿日 (古い順)", /* --- Favorite Tags --- */ FT_UNCATEGORIZED: '未分類', FT_DROPDOWN_TITLE: 'お気に入りタグ', FT_DROPDOWN_SETTINGS_TITLE: 'お気に入りタグ設定', FT_DROPDOWN_NEW_TAG: '新しいタグ', FT_DROPDOWN_NEW_TAG_PLACEHOLDER: 'タグ名', FT_DROPDOWN_NEW_TAG_ADD: '追加', FT_FILTER_ALL: 'すべて', FT_SETTINGS_TITLE: 'お気に入りタグ設定', FT_SETTINGS_EMPTY_TAG_LIST: 'タグはまだありません。「新しいタグ」から追加できます。', FT_SETTINGS_UNCATEGORIZED_NAME: '未分類', FT_SETTINGS_UNCATEGORIZED_NAME_TOOLTIP: '未分類の名前は変更できません', FT_SETTINGS_UNCATEGORIZED_DELETE_TOOLTIP: '未分類は削除できません', FT_SETTINGS_CLOSE: '閉じる', FT_SETTINGS_DELETE_BUTTON: '削除', FT_SETTINGS_UP: '▲', FT_SETTINGS_DOWN: '▼', FT_SETTINGS_DISPLAY_SECTION_TITLE: '表示設定', FT_SETTINGS_DISPLAY_MODE_LABEL: 'タグの表示形式', FT_SETTINGS_DISPLAY_MODE_LEAF: '末尾のみ (leaf)', FT_SETTINGS_DISPLAY_MODE_FULL: 'フルパス (full)', FT_CONFIRM_DELETE_TAG_MSG: 'タグ「{tagName}」を削除しますか?\nこのタグが付いていたお気に入りは未分類になります。', FT_SETTINGS_BUTTON_TITLE: 'お気に入りタグ設定', }, 'zh-CN': {}, 'ko': {}, 'fr': {}, 'es': {}, 'de': {}, 'pt-BR': {}, 'ru': {} }, lang: 'en', init: function() { const supportedLangs = Object.keys(this.translations); let detectedLang = document.documentElement.lang || navigator.language || 'en'; if (supportedLangs.includes(detectedLang)) { this.lang = detectedLang; return; } const baseLang = detectedLang.split('-')[0]; if (supportedLangs.includes(baseLang)) { this.lang = baseLang; return; } this.lang = 'en'; }, t: function(key) { return this.translations[this.lang]?.[key] || this.translations['en'][key] || `[${key}]`; }, apply: function(container) { container.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = this.t(el.dataset.i18n); }); container.querySelectorAll('[data-i18n-placeholder]').forEach(el => { el.placeholder = this.t(el.dataset.i18nPlaceholder); }); container.querySelectorAll('[data-i18n-title]').forEach(el => { el.title = this.t(el.dataset.i18nTitle); }); } }; const SEARCH_SVG = ` `; const SETTINGS_SVG = ` `; const FOLDER_TOGGLE_OPEN_SVG = ` `; const FOLDER_TOGGLE_CLOSED_SVG = ` `; // トグルボタンの小ユーティリティ function renderFolderToggleButton(collapsed) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'adv-folder-toggle-btn'; btn.setAttribute('aria-label', collapsed ? 'Expand' : 'Collapse'); btn.setAttribute('title', collapsed ? 'Expand' : 'Collapse'); btn.setAttribute('aria-expanded', (!collapsed).toString()); btn.style.cssText = ` appearance:none;border:none;background:transparent;cursor:pointer; width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center; margin-right:8px;color:inherit;flex:0 0 auto; `; btn.innerHTML = collapsed ? FOLDER_TOGGLE_CLOSED_SVG : FOLDER_TOGGLE_OPEN_SVG; return btn; } function updateFolderToggleButton(btn, collapsed) { if (!btn) return; btn.innerHTML = collapsed ? FOLDER_TOGGLE_CLOSED_SVG : FOLDER_TOGGLE_OPEN_SVG; btn.setAttribute('aria-label', collapsed ? 'Expand' : 'Collapse'); btn.setAttribute('title', collapsed ? 'Expand' : 'Collapse'); btn.setAttribute('aria-expanded', (!collapsed).toString()); } const themeManager = { colors: { light: { '--modal-bg': '#ffffff', '--modal-text-primary': '#0f1419', '--modal-text-secondary': '#536471', '--modal-border': '#d9e1e8', '--modal-input-bg': '#eff3f4', '--modal-input-border': '#cfd9de', '--modal-button-hover-bg': 'rgba(15, 20, 25, 0.1)', '--modal-scrollbar-thumb': '#aab8c2', '--modal-scrollbar-track': '#eff3f4', '--modal-close-color': '#0f1419', '--modal-close-hover-bg': 'rgba(15, 20, 25, 0.1)', '--hr-color': '#eff3f4', }, dim: { '--modal-bg': '#15202b', '--modal-text-primary': '#f7f9f9', '--modal-text-secondary': '#8899a6', '--modal-border': '#38444d', '--modal-input-bg': '#192734', '--modal-input-border': '#38444d', '--modal-button-hover-bg': 'rgba(247, 249, 249, 0.1)', '--modal-scrollbar-thumb': '#536471', '--modal-scrollbar-track': '#192734', '--modal-close-color': '#f7f9f9', '--modal-close-hover-bg': 'rgba(247, 249, 249, 0.1)', '--hr-color': '#38444d', }, dark: { '--modal-bg': '#000000', '--modal-text-primary': '#e7e9ea', '--modal-text-secondary': '#71767b', '--modal-border': '#2f3336', '--modal-input-bg': '#16181c', '--modal-input-border': '#54595d', '--modal-button-hover-bg': 'rgba(231, 233, 234, 0.1)', '--modal-scrollbar-thumb': '#536471', '--modal-scrollbar-track': '#16181c', '--modal-close-color': '#e7e9ea', '--modal-close-hover-bg': 'rgba(231, 233, 234, 0.1)', '--hr-color': '#2f3336', } }, applyTheme: function(modalElement, triggerEl) { if (!modalElement) return; const bodyBg = getComputedStyle(document.body).backgroundColor; let theme = 'dark'; if (bodyBg === 'rgb(21, 32, 43)') theme = 'dim'; else if (bodyBg === 'rgb(255, 255, 255)') theme = 'light'; // ▼ ブックマークUIのテーマ切替用にクラスを付与 try { document.documentElement.classList.remove('x-theme-light', 'x-theme-dim', 'x-theme-dark'); if (theme === 'light') { document.documentElement.classList.add('x-theme-light'); } else if (theme === 'dim') { document.documentElement.classList.add('x-theme-dim'); } else { document.documentElement.classList.add('x-theme-dark'); } } catch (e) {} const themeColors = this.colors[theme] || this.colors.dark; const targets = [modalElement, document.documentElement]; if (triggerEl) targets.push(triggerEl); for (const t of targets) { for (const [key, value] of Object.entries(themeColors)) { t.style.setProperty(key, value); } } }, observeChanges: function(modalElement, triggerEl) { const observer = new MutationObserver(() => this.applyTheme(modalElement, triggerEl)); observer.observe(document.body, { attributes: true, attributeFilter: ['style'] }); this.applyTheme(modalElement, triggerEl); } }; function decodeURIComponentSafe(s) { try { return decodeURIComponent(s); } catch { return s; } } // “ ” 『』などのスマート引用を ASCII の " に寄せる function normalizeQuotes(s) { return String(s).replace(/[\u201C\u201D\u300C\u300D\uFF02]/g, '"'); } // 解析前に軽く正規化(URL から来る %22..., 連続空白など) function normalizeForParse(s) { if (!s) return ''; let out = String(s); // URL っぽいエンコードだけ軽く剥がす(%22 等) if (/%[0-9A-Fa-f]{2}/.test(out)) out = decodeURIComponentSafe(out); out = normalizeQuotes(out); // 制御文字を潰し、空白を整形 out = out.replace(/\s+/g, ' ').trim(); return out; } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // ── OR/引用のための簡易トークナイザ function tokenizeQuotedWords(s) { const out = []; let cur = ''; let inQ = false; for (let i = 0; i < s.length; i++) { const c = s[i]; if (c === '"') { inQ = !inQ; cur += c; continue; } if (!inQ && /\s/.test(c)) { if (cur) { out.push(cur); cur=''; } } else { cur += c; } } if (cur) out.push(cur); return out.filter(Boolean); } // トップレベルの OR で文字列を分割(引用/括弧を考慮) function splitTopLevelOR(str) { const parts = []; let cur = ''; let inQ = false, depth = 0; for (let i = 0; i < str.length; ) { const c = str[i]; if (c === '"') { inQ = !inQ; cur += c; i++; continue; } if (!inQ && (c === '(' || c === ')')) { depth += (c === '(' ? 1 : -1); cur += c; i++; continue; } if (!inQ && depth === 0) { // 単語境界の "or" / "OR" if ((str.slice(i, i+2).toLowerCase() === 'or') && (i === 0 || /\s|\(/.test(str[i-1] || '')) && (i+2 >= str.length || /\s|\)/.test(str[i+2] || ''))) { parts.push(cur.trim()); cur = ''; i += 2; continue; } } cur += c; i++; } if (cur.trim()) parts.push(cur.trim()); return parts.length > 1 ? parts : null; } // OR 専用判定(演算子/否定/括弧が無い素の OR 群なら true) function isPureORQuery(q) { const hasOps = /(?:^|\s)(?:from:|to:|lang:|filter:|is:|min_replies:|min_faves:|min_retweets:|since:|until:)\b/i.test(q); const hasNeg = /(^|\s)-\S/.test(q); const hasPar = /[()]/.test(q); return !hasOps && !hasNeg && !hasPar; } function waitForElement(selector, timeout = 10000, checkProperty = null) { return new Promise((resolve) => { const checkInterval = 100; let elapsedTime = 0; const intervalId = setInterval(() => { const element = document.querySelector(selector); if (element) { if (checkProperty) { if (element[checkProperty]) { clearInterval(intervalId); resolve(element); return; } } else { clearInterval(intervalId); resolve(element); return; } } elapsedTime += checkInterval; if (elapsedTime >= timeout) { clearInterval(intervalId); resolve(null); } }, checkInterval); }); } function hideUIImmediately(modal, trigger) { if (modal) modal.style.display = 'none'; if (trigger) trigger.style.display = 'none'; } // ▼ ルート適用を軽く検証(URL一致 + プロフィール系DOMが現れたか) function waitForRouteApply(path, timeoutMs = 2000) { const goal = new URL(path, location.origin).pathname; // ルート毎の判定を用意(必要に応じて拡張) const perRouteProbes = [ // 検索ページ:検索結果タイムライン or 検索ボックス or 何かしらのツイート { test: p => p.startsWith('/search'), sels: [ '[aria-label*="Search results"], [aria-label*="検索結果"]', 'div[data-testid="primaryColumn"] input[data-testid="SearchBox_Search_Input"]', 'div[data-testid="primaryColumn"] article[data-testid="tweet"]' ] }, // プロフィール { test: p => /^\/[A-Za-z0-9_]{1,50}\/?$/.test(p), sels: [ '[data-testid="UserName"]', 'div[data-testid="UserProfileHeader_Items"]', 'div[data-testid="UserDescription"]' ] }, // デフォルト(保険):主要カラムに何かレンダされたらOK { test: _ => true, sels: [ 'div[data-testid="primaryColumn"]', 'main[role="main"]' ] } ]; const probes = (perRouteProbes.find(x => x.test(goal)) || perRouteProbes.at(-1)).sels; return new Promise(resolve => { const t0 = performance.now(); (function tick() { const elapsed = performance.now() - t0; const urlOk = location.pathname === goal; const domOk = probes.some(sel => document.querySelector(sel)); if (urlOk && domOk) return resolve(true); if (elapsed >= timeoutMs) return resolve(false); // 立ち上がりは速く、以後はやや疎にポーリング setTimeout(tick, elapsed < 300 ? 60 : elapsed < 700 ? 120 : 180); })(); }); } // ▼ SPA 遷移の核。pushState → 合成 popstate → DOM適用待ち → 失敗ならフォールバック async function spaNavigate(path, { ctrlMeta = false, timeoutMs = 1200 } = {}) { try { const to = new URL(path, location.origin); if (to.origin !== location.origin) throw new Error('cross-origin'); history.pushState(history.state, '', to.pathname + to.search + to.hash); // X のルーターは popstate を購読している想定 window.dispatchEvent(new PopStateEvent('popstate', { state: history.state })); const ok = await waitForRouteApply(to.pathname, timeoutMs); if (ok) return; // 成功 } catch (e) { // fall through to fallback } // フォールバック:修飾キーありなら新規タブ、なければ通常遷移 if (ctrlMeta) window.open(path, '_blank', 'noopener'); else location.assign(path); } const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36); let isUpdating = false; let manualOverrideOpen = false; const lastHistory = { q: null, pf: null, lf: null, ts: 0 }; // ▼ パース結果をキャッシュ(スクロール時の再パース防止) let __cachedSearchTokens = null; let __cachedSearchQuery = null; // このクエリ文字列で __cachedSearchTokens が生成された // ▼ 入力中ガード(IME合成を含めてカバー) let __typingGuardUntil = 0; const TYPING_GRACE_MS = 600; // 入力終了からこのmsはスキャン停止 const markTyping = () => { __typingGuardUntil = Date.now() + TYPING_GRACE_MS; }; const isTyping = () => Date.now() < __typingGuardUntil; const isMediaViewPath = (pathname) => /\/status\/\d+\/(?:photo|video|media|analytics)(?:\/\d+)?\/?$/.test(pathname); const isComposePath = (pathname) => /^\/compose\/post(?:\/|$)/.test(pathname); const isProfileMediaPath = (pathname) => /^\/[A-Za-z0-9_]{1,50}\/(?:photo|header_photo)\/?$/.test(pathname); const isBroadcastPath = (pathname) => /^\/i\/broadcasts\//.test(pathname); const isBlockedPath = (pathname) => isMediaViewPath(pathname) || isComposePath(pathname) || isProfileMediaPath(pathname) || isBroadcastPath(pathname); GM_addStyle(` :root { --modal-primary-color:#1d9bf0; --modal-primary-color-hover:#1a8cd8; --modal-primary-text-color:#fff; } #advanced-search-trigger { position:fixed; top:18px; right:20px; z-index:9999; background-color:var(--modal-primary-color); color:var(--modal-primary-text-color); border:none; border-radius:50%; width:50px; height:50px; font-size:24px; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,0.15); display:flex; align-items:center; justify-content:center; transition:transform .2s, background-color .2s; } #advanced-search-trigger:hover { transform:scale(1.1); background-color:var(--modal-primary-color-hover); } #advanced-search-modal { position:fixed; z-index:10000; width:450px; display:none; flex-direction:column; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background-color:var(--modal-bg, #000); color:var(--modal-text-primary, #e7e9ea); border:1px solid var(--modal-border, #333); border-radius:16px; box-shadow:0 8px 24px rgba(29,155,240,.2); transition:background-color .2s,color .2s,border-color .2s; } .adv-modal-header{padding:12px 16px;border-bottom:1px solid var(--modal-border,#333);cursor:move;display:flex;justify-content:space-between;align-items:center} .adv-modal-title-left{display:flex;align-items:center;gap:8px;} .adv-modal-header h2{margin:0;font-size:18px;font-weight:700} .adv-settings-btn{ margin-left:6px; width:26px;height:26px; border-radius:9999px; border:1px solid var(--modal-input-border,#38444d); background:var(--modal-input-bg,#202327); display:inline-flex; align-items:center; justify-content:center; cursor:pointer; padding:0; } .adv-settings-btn:hover{ background-color:var(--modal-button-hover-bg,rgba(231,233,234,.1)); } .adv-settings-btn svg{ width:14px; height:14px; } .adv-modal-close{background:0 0;border:none;color:var(--modal-close-color,#e7e9ea);font-size:24px;cursor:pointer;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color .2s} .adv-modal-close:hover{background-color:var(--modal-close-hover-bg,rgba(231,233,234,.1))} .adv-modal-body{flex:1;overflow-y:auto;padding:0} .adv-form-group{margin-bottom:16px} .adv-form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:700;color:var(--modal-text-secondary,#8b98a5)} .adv-form-group input[type=text],.adv-form-group input[type=number],.adv-form-group input[type=date],.adv-form-group select{width:100%;background-color:var(--modal-input-bg,#202327);border:1px solid var(--modal-input-border,#38444d);border-radius:4px;padding:8px 12px;color:var(--modal-text-primary,#e7e9ea);font-size:15px;box-sizing:border-box} .adv-form-group input:focus,.adv-form-group select:focus{outline:0;border-color:var(--modal-primary-color)} .adv-form-group input::placeholder{color:var(--modal-text-secondary,#536471)} .adv-form-group-date-container{display:flex;gap:10px} .adv-filter-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} .adv-checkbox-group{background-color:var(--modal-input-bg,#202327);border:1px solid var(--modal-input-border,#38444d);border-radius:8px;padding:10px;display:flex;flex-direction:column;gap:8px} .adv-checkbox-group span{font-weight:700;font-size:14px;color:var(--modal-text-primary,#e7e9ea)} .adv-checkbox-item{display:flex;align-items:center} .adv-checkbox-item input{margin-right:8px; accent-color:var(--modal-primary-color);} .adv-checkbox-item label{color:var(--modal-text-secondary,#8b98a5);margin-bottom:0} .adv-checkbox-item input[type="checkbox"]:disabled { opacity: 0.5; cursor: not-allowed; } .adv-checkbox-item input[type="checkbox"]:disabled + label { opacity: 0.5; cursor: not-allowed; text-decoration: line-through; } .adv-modal-footer{padding:12px 16px;border-top:1px solid var(--modal-border,#333);display:flex;justify-content:flex-end;gap:12px} .adv-modal-button{padding:5px 16px;border-radius:9999px;border:1px solid var(--modal-text-secondary,#536471);background-color:transparent;color:var(--modal-text-primary,#e7e9ea);font-weight:700;cursor:pointer;transition:background-color .2s} .adv-modal-button:hover{background-color:var(--modal-button-hover-bg,rgba(231,233,234,.1))} .adv-modal-button.primary, .adv-chip.primary { background-color:var(--modal-primary-color); border-color:var(--modal-primary-color); color:var(--modal-primary-text-color); } .adv-modal-button.primary:hover{background-color:var(--modal-primary-color-hover)} .adv-modal-button[disabled]{opacity:.5; cursor:not-allowed;} #adv-settings-import.adv-modal-button[disabled]{opacity:1;} .adv-modal-body::-webkit-scrollbar{width:8px} .adv-modal-body::-webkit-scrollbar-track{background:var(--modal-scrollbar-track,#202327)} .adv-modal-body::-webkit-scrollbar-thumb{background:var(--modal-scrollbar-thumb,#536471);border-radius:4px} body.adv-dragging{user-select:none} .adv-account-label-group{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px} .adv-exclude-toggle{display:flex;align-items:center} .adv-exclude-toggle input{margin-right:4px} .adv-exclude-toggle label{font-size:13px;font-weight:normal;color:var(--modal-text-secondary,#8b98a5);cursor:pointer} hr.adv-separator{border:none;height:1px;background-color:var(--hr-color,#333);margin:20px 0;transition:background-color .2s} /* ★全タブ共通のズーム対象に拡張(検索タブの既存idにも適用維持) */ .adv-zoom-root, #adv-zoom-root{ transform-origin: top left; will-change: transform; padding:12px 11.6px 10px 11px; } #adv-zoom-root { padding-top: 16px; /* 検索タブの上余白だけを 16px に上書き */ padding-left:16px; padding-right:20px; } .adv-modal-body{ overflow:auto; } .adv-form-row.two-cols { display:grid; grid-template-columns:1fr 1fr; gap:10px; } @media (max-width: 480px) { .adv-form-row.two-cols { grid-template-columns:1fr; } } .adv-tabs { display: flex; border-bottom: 1px solid var(--modal-border, #333); padding: 0 8px 0 6px; gap: 4px; align-items: stretch; /* 幅不足の時は隠さず、2行にする */ flex-wrap: wrap; /* 幅検知の基準にする */ container-type: inline-size; } .adv-tab-btn { appearance: none; border: none; background: transparent; color: var(--modal-text-secondary, #8b98a5); padding: 10px 8px; cursor: pointer; font-weight: 700; border-radius: 8px 8px 0 0; font-size: 0.78rem; /* ボタン内のテキストは折り返さない */ white-space: nowrap; /* 余ったスペースを全員で分け合う(均等配置・最大化) */ flex: 1 1 auto; text-align: center; /* なめらかな変化 */ transition: font-size 0.1s, padding 0.1s, background-color 0.2s; } .adv-tab-btn.active { color: var(--modal-text-primary, #e7e9ea); background-color: var(--modal-input-bg, #202327); border: 1px solid var(--modal-input-border, #38444d); border-bottom: none; /* アクティブタブは少し強調 */ z-index: 1; } /* ▼▼▼ コンテナクエリ: 幅に応じて最適化 ▼▼▼ */ /* 幅 480px 以下: フォントを少し小さくし、1行収まりを狙う */ @container (max-width: 480px) { .adv-tab-btn { font-size: 12px; padding: 8px 4px; } } /* 幅 380px 以下: さらにフォントを詰め、もし2行になっても違和感ないサイズに */ @container (max-width: 380px) { .adv-tab-btn { font-size: 11px; padding: 6px 2px; border-radius: 6px; /* 角丸も少し小さく */ } /* 2行になった際に上下の列がくっつきすぎないようにする */ .adv-tabs { row-gap: 2px; } /* 2行目のボーダー処理(見た目を整える) */ .adv-tab-btn.active { border-bottom: 1px solid var(--modal-input-bg, #202327); margin-bottom: -1px; } } .adv-tab-content { display:none; } .adv-tab-content.active { display:block; } .adv-secret-wrap { display:flex; align-items:center; gap:8px; } .adv-secret-btn { cursor:pointer; border:1px solid var(--modal-input-border,#38444d); background:var(--modal-input-bg,#202327); color:var(--modal-text-primary,#e7e9ea); padding:4px 8px; border-radius:9999px; font-weight:700; user-select:none; display:flex; align-items:center; gap:6px; font-size:12px; } .adv-secret-btn .dot { width:7px; height:7px; border-radius:50%; background:#777; box-shadow:0 0 0px #0000; transition:all .2s; } .adv-secret-btn.off { opacity:0.9; } .adv-secret-btn.on { background-color:var(--modal-primary-color); border-color:var(--modal-primary-color); color:var(--modal-primary-text-color); } .adv-secret-btn.on .dot { background:#fff; box-shadow:0 0 8px rgba(255,255,255,.9); } .adv-list { display:flex; flex-direction:column; gap:8px; } .adv-item { position: relative; border:1px solid var(--modal-input-border,#38444d); background:var(--modal-input-bg,#202327); border-radius:8px; padding:8px; display:flex; gap:8px; align-items:flex-start; } .adv-item.dragging { opacity:.6; } .adv-item-handle { cursor:grab; user-select:none; padding:4px 6px; border-radius:6px; border:1px dashed var(--modal-border,#333); } .adv-item-avatar { width:36px; height:36px; border-radius:9999px; object-fit:cover; flex:0 0 auto; background:var(--modal-border,#333); } a.adv-link { color: inherit; text-decoration: none; } a.adv-link:hover { text-decoration: underline; cursor: pointer; } .adv-item-avatar-link { display:inline-block; border-radius:9999px; } .adv-item-main { flex:1; min-width:0; } .adv-item-title { font-size:14px; font-weight:700; color:var(--modal-text-primary,#e7e9ea); word-break:break-word; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; } .adv-item-sub { font-size:12px; color:var(--modal-text-secondary,#8b98a5); margin-top:2px; display:flex; gap:6px; flex-wrap:wrap; align-items:center; } .adv-item-actions { display:flex; gap:6px; align-items:center; align-self:center; } .adv-chip { border:1px solid var(--modal-input-border,#38444d); background:transparent; color:var(--modal-text-primary,#e7e9ea); padding:4px 8px; border-radius:9999px; font-size:12px; cursor:pointer; } .adv-fav-btn-pos { position: absolute; right: 8px; } .adv-fav-btn-top { top: 8px; } .adv-fav-btn-bottom { bottom: 8px; } .adv-chip.danger { border-color:#8b0000; color:#ffb3b3; } .adv-modal-button.danger { border-color:#8b0000; color:#ffb3b3; } .adv-modal-button.danger:hover{ background-color:rgba(139,0,0,0.2); } .adv-chip.scope { padding:2px 6px; font-size:11px; line-height:1.2; opacity:0.95; } .adv-toast { position:fixed; z-index:10001; left:50%; transform:translateX(-50%); bottom:24px; background:#111a; color:#fff; backdrop-filter: blur(6px); border:1px solid #fff3; padding:8px 12px; border-radius:8px; font-weight:700; opacity:0; pointer-events:none; transition:opacity .2s, transform .2s; } .adv-toast.show { opacity:1; transform:translateX(-50%) translateY(-6px); } .adv-modal-footer { justify-content:flex-end; } .adv-modal-footer .adv-modal-button#adv-save-button { margin-right:auto; } .adv-tab-toolbar { display:flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom:12px; padding: 0 2px; } /* ツールバーの左側(検索・ソート) */ .adv-tab-toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 150px; } /* ツールバーの右側(すべて削除ボタン) */ .adv-tab-toolbar-right { display: flex; flex: 0 0 auto; } /* ツールバー入力欄の共通スタイル */ .adv-select, .adv-input { background-color:var(--modal-input-bg,#202327); border:1px solid var(--modal-input-border,#38444d); border-radius:8px; padding:6px 10px; color:var(--modal-text-primary,#e7e9ea); } /* 検索ボックスとセレクトボックスのスタイル(.adv-folder-toolbar内と共通化) */ /* 共通スタイルは .adv-input, .adv-select が担当 */ .adv-tab-toolbar .adv-input { flex: 1; min-width: 80px; } .adv-tab-toolbar .adv-select { flex: 0 1 auto; } [data-testid="cellInnerDiv"][data-adv-hidden], article[data-adv-hidden] { display:none !important; content-visibility: hidden; contain: strict; } #advanced-search-modal { max-height:none; } .adv-resizer { position:absolute; z-index:10002; background:transparent; } .adv-resizer.e, .adv-resizer.w { top:-3px; bottom:-3px; width:8px; } .adv-resizer.e { right:-3px; cursor: ew-resize; } .adv-resizer.w { left:-3px; cursor: ew-resize; } .adv-resizer.n, .adv-resizer.s { left:-3px; right:-3px; height:8px; } .adv-resizer.n { top:-3px; cursor: ns-resize; } .adv-resizer.s { bottom:-3px; cursor: ns-resize; } .adv-resizer.se, .adv-resizer.ne, .adv-resizer.sw, .adv-resizer.nw { width:12px; height:12px; } .adv-resizer.se { right:-4px; bottom:-4px; cursor:nwse-resize; } .adv-resizer.ne { right:-4px; top:-4px; cursor:nesw-resize; } .adv-resizer.sw { left:-4px; bottom:-4px; cursor:nesw-resize; } .adv-resizer.nw { left:-4px; top:-4px; cursor:nwse-resize; } /* ▶ Mute タブ */ .adv-mute-add { display:flex; gap:8px; align-items:center; margin-bottom:10px; } .adv-mute-add input[type=text]{ flex:1; border-radius:8px; } .adv-mute-list { display:flex; flex-direction:column; gap:8px; } /* ▼ グローバル無効(マスターOFF)のとき:リスト全体を淡く */ .adv-mute-list.disabled { opacity: .6; filter: grayscale(35%); } /* ▼ 個別無効(enabled=false)の行だけ淡く+打ち消し等の視覚 */ .adv-mute-item { border:1px solid var(--modal-input-border,#38444d); background:var(--modal-input-bg,#202327); border-radius:8px; padding:8px; display:flex; flex-wrap: wrap; gap:8px; align-items:flex-start; transition: opacity .15s ease, filter .15s ease, border-color .15s ease; } .adv-mute-item.disabled { opacity: .55; filter: grayscale(25%); border-color: color-mix(in oklab, var(--modal-input-border,#38444d), transparent 20%); } .adv-mute-item.disabled .adv-mute-word { color: var(--modal-text-secondary,#8b98a5); text-decoration: line-through; } .adv-mute-word { font-weight:700; color:var(--modal-text-primary,#e7e9ea); word-break:break-word; } .adv-mute-actions { display:flex; gap:6px; align-items:center; flex: 0 0 auto; white-space: nowrap; margin-left: auto; } @media (max-width: 480px) { .adv-mute-actions { margin-top: 4px; } } .adv-toggle { display: inline-flex; gap: 6px; align-items: center; color: var(--modal-text-secondary,#8b98a5); line-height: 1; margin-bottom:0!important; } .adv-toggle input[type="checkbox"] { width: 14px; height: 14px; margin: 0; flex: 0 0 auto; vertical-align: middle; } .adv-toggle span { font-size: 11px; line-height: 1; } .adv-mute-header { display:flex; justify-content:space-between; align-items:center; margin:12px 0 6px; } .adv-mute-title { font-weight:700; color: var(--modal-text-primary,#e7e9ea); } /* マスター切替の一瞬だけ付けるガードクラス */ .adv-no-anim, .adv-no-anim * { transition: none !important; } #adv-accounts-empty:not(:empty), #adv-lists-empty:not(:empty) { padding: 0 12px 12px 12px; } /* ▼ マスターOFF中は、個別無効の“さらに薄く”を抑制(親の薄さのみ適用) */ .adv-mute-list.disabled .adv-mute-item.disabled { opacity: 1; /* 子の追加の薄さを無効化(親のopacityのみが効く) */ filter: none; /* 子の追加グレースケールも無効化(親側のfilterのみ適用) */ /* ボーダーだけ通常色に戻す */ /* border-color: var(--modal-input-border,#38444d); */ } /* === Trigger: モーダルと同質の見た目に合わせる === */ #advanced-search-trigger.adv-trigger-search { width: 49px; height: 49px; border-radius: 9999px; background-color: var(--modal-bg, #000); color: var(--modal-text-primary, #e7e9ea); border: 2px solid var(--modal-border, #2f3336); /* ← モーダルと同じ枠色 */ box-shadow: 0 8px 24px rgba(29,155,240,.2); /* ← モーダルと同じshadow */ display:flex; align-items:center; justify-content:center; transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; } #advanced-search-trigger.adv-trigger-search:hover { /* 背景は変えず、浮かせる表現だけ強化 */ transform: translateZ(0) scale(1.04); box-shadow: 0 12px 36px rgba(29,155,240,.28); border-color: var(--modal-border, #2f3336); } #advanced-search-trigger.adv-trigger-search:active { transform: translateZ(0) scale(0.98); box-shadow: 0 6px 18px rgba(29,155,240,.22); } #advanced-search-trigger.adv-trigger-search:focus-visible { outline: none; box-shadow: 0 8px 24px rgba(29,155,240,.2), 0 0 0 3px color-mix(in oklab, var(--modal-primary-color, #1d9bf0) 45%, transparent); } #advanced-search-trigger.adv-trigger-search svg { width: 22px; height: 22px; display:block; /* 検索アイコンは stroke="currentColor" を使っているので配色は自動追従 */ } /* === Folders === */ .adv-folder { border:1px solid var(--modal-input-border,#38444d); border-radius:10px; margin-bottom:10px; } .adv-folder-header { display:flex; justify-content:space-between; align-items:center; padding:8px 10px; background:var(--modal-input-bg,#202327); border-bottom:1px solid var(--modal-input-border,#38444d); } .adv-folder[data-drop="1"] { outline:2px dashed var(--modal-primary-color); outline-offset:-2px; } .adv-folder-title { display:flex; gap:8px; align-items:baseline; } .adv-folder-actions { display:flex; gap:6px; } .adv-folder-toolbar { display:flex; gap:8px; align-items:center; margin:0 0 12px; padding:0 2px; } .adv-folder-toolbar input[type="text"] { flex:1; min-width:80px; } .adv-folder-collapsed .adv-list { display:none; } /* ▶ Folder headers: show grab cursor except on action buttons */ .adv-folder-header { cursor: grab; } .adv-folder-header:active { cursor: grabbing; } /* ボタン上では通常のポインタ(=ドラッグ開始させない見た目) */ .adv-folder-header .adv-folder-actions, .adv-folder-header .adv-folder-actions * { cursor: pointer; } /* ▼ トグルボタン(左端) */ .adv-folder-toggle { appearance: none; border: none; background: transparent; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 6px; cursor: pointer; margin-right: 6px; } .adv-folder-toggle:focus-visible { outline: none; box-shadow: 0 0 0 2px color-mix(in oklab, var(--modal-primary-color, #1d9bf0) 60%, transparent); } /* ▼ アイコン(chevron) */ .adv-folder-toggle svg { width: 16px; height: 16px; transition: transform .15s ease; } /* ▼ 開閉で向きを変える(右▶ → 下▼) */ .adv-folder:not(.adv-folder-collapsed) .adv-folder-toggle svg { transform: rotate(90deg); } /* ▼ 開いているヘッダーはわずかに背景強調 */ .adv-folder:not(.adv-folder-collapsed) .adv-folder-header { background: color-mix(in oklab, var(--modal-input-bg,#202327) 92%, var(--modal-primary-color,#1d9bf0)); } /* ▼ ドラッグハンドルは“掴める”見た目を強調 */ .adv-folder-drag-handle { cursor: grab; user-select: none; padding: 4px 6px; border-radius: 6px; border: 1px dashed var(--modal-border,#38444d); } .adv-folder-drag-handle:active { cursor: grabbing; } /* ▼ Unassigned セクション(見出しなし・枠なし) */ .adv-unassigned { margin-bottom: 10px; min-height: 30px; /* ★ 空の時でもドロップできるように最小高さを確保 */ } .adv-unassigned .adv-list { display: flex; flex-direction: column; gap: 8px; } /* フォルダー並び替え用のドラッグ時の視覚(Unassigned も対象) */ .adv-unassigned.dragging-folder { opacity: .6; } /* タブ背景およびリストコンテナ背景へのドロップハイライト */ #adv-tab-accounts.adv-bg-drop-active, #adv-tab-lists.adv-bg-drop-active, #adv-tab-saved.adv-bg-drop-active, #adv-accounts-list.adv-bg-drop-active, #adv-lists-list.adv-bg-drop-active, #adv-saved-list.adv-bg-drop-active { outline: 2px dashed var(--modal-primary-color, #1d9bf0); /* リストコンテナ側はパディングが無いためオフセットを小さく */ outline-offset: -4px; } /* タブパネル(上部余白)側は既存のオフセットを維持 */ #adv-tab-accounts.adv-bg-drop-active, #adv-tab-lists.adv-bg-drop-active, #adv-tab-saved.adv-bg-drop-active { outline-offset: -8px; } /* 背景(Unassigned 宛て)をドロップ中は、フォルダー内の“薄い残像”を消す */ #adv-tab-accounts.adv-bg-drop-active .adv-list .adv-item.dragging, #adv-accounts-list.adv-bg-drop-active .adv-list .adv-item.dragging, #adv-tab-lists.adv-bg-drop-active .adv-list .adv-item.dragging, #adv-lists-list.adv-bg-drop-active .adv-list .adv-item.dragging, #adv-tab-saved.adv-bg-drop-active .adv-list .adv-item.dragging, #adv-saved-list.adv-bg-drop-active .adv-list .adv-item.dragging { display: none !important; } /* === Settings modal === */ #adv-settings-modal.adv-settings-modal{ position:fixed; inset:0; z-index:10001; display:none; align-items:center; justify-content:center; background:rgba(0,0,0,.5); } .adv-settings-dialog{ width:420px; max-width:90vw; max-height:80vh; background-color:var(--modal-bg,#000); color:var(--modal-text-primary,#e7e9ea); border-radius:16px; border:1px solid var(--modal-border,#333); box-shadow:0 8px 24px rgba(0,0,0,.3); display:flex; flex-direction:column; overflow:hidden; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; } .adv-settings-header{ padding:12px 16px; border-bottom:1px solid var(--modal-border,#333); display:flex; align-items:center; justify-content:space-between; } .adv-settings-title{ margin:0; font-size:16px; font-weight:700; } .adv-settings-close{ border:none; background:transparent; color:var(--modal-close-color,#e7e9ea); font-size:20px; width:32px; height:32px; border-radius:50%; display:flex; align-items:center; justify-content:center; cursor:pointer; } .adv-settings-close:hover{ background-color:var(--modal-close-hover-bg,rgba(231,233,234,.1)); } .adv-settings-body{ padding:12px 16px 23px 16px; overflow-y:auto; display:flex; flex-direction:column; gap:16px; } .adv-settings-group label{ display:block; margin-bottom:4px; font-size:14px; font-weight:700; color:var(--modal-text-secondary,#8b98a5); } .adv-settings-group select, .adv-settings-group textarea{ width:100%; background-color:var(--modal-input-bg,#202327); border:1px solid var(--modal-input-border,#38444d); border-radius:8px; padding:8px 10px; color:var(--modal-text-primary,#e7e9ea); font-size:14px; box-sizing:border-box; } .adv-settings-group textarea{ resize:vertical; min-height:80px; } .adv-settings-section-header { margin: 12px 0 2px 0; padding-bottom: 4px; border-bottom: 1px solid var(--modal-border,#333); font-size: 13px; font-weight: 700; color: var(--modal-text-primary,#e7e9ea); } .adv-settings-toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; } .adv-settings-toggle-row .adv-toggle { font-size: 14px; color: var(--modal-text-primary,#e7e9ea); user-select: none; cursor: pointer; } .adv-settings-toggle-row .adv-toggle span { font-size: 14px; } /* Simple toggle switch CSS */ .adv-switch { position: relative; display: inline-block; width: 40px; height: 22px; } .adv-switch input { opacity: 0; width: 0; height: 0; } .adv-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--modal-input-border,#38444d); transition: .2s; border-radius: 22px; } .adv-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: var(--modal-bg, #000); transition: .2s; border-radius: 50%; } .adv-switch input:checked + .adv-slider { background-color: var(--modal-primary-color); } .adv-switch input:checked + .adv-slider:before { transform: translateX(18px); } .adv-settings-actions-inline{ display:flex; gap:8px; margin-top:6px; flex-wrap:wrap; } .adv-settings-footer{ padding:10px 16px; border-top:1px solid var(--modal-border,#333); display:flex; justify-content:flex-end; gap:8px; } /* === Tab Drag & Drop === */ .adv-tab-btn { user-select: none; } .adv-tab-btn:active { cursor: grabbing; } .adv-tab-btn.dragging { opacity: .5; } /* --- Favorite Tags CSS --- */ /* ▼ ブックマークUI専用の配色変数を定義 */ :root { /* デフォルト (Dim / Dark) は静的なダークテーマ */ --ft-bg: rgb(21, 24, 28); --ft-border-light: rgba(239, 243, 244, 0.24); --ft-border-dim: rgba(239, 243, 244, 0.15); --ft-border-strong: rgba(239, 243, 244, 0.3); --ft-border-accent: rgba(239, 243, 244, 0.8); --ft-text-primary: rgb(239, 243, 244); --ft-text-secondary: rgba(239, 243, 244, 0.7); --ft-input-bg: rgba(0,0,0,0.2); --ft-input-border: rgba(239,243,244,0.2); --ft-hover-bg: rgba(255, 255, 255, 0.06); --ft-hover-bg-strong: rgba(255, 255, 255, 0.08); --ft-accent-color: #1d9bf0; } :root.x-theme-light { /* Lightテーマの時だけ、X本体の動的変数を参照する */ --ft-bg: var(--modal-bg); --ft-border-light: var(--modal-border); --ft-border-dim: var(--modal-border); --ft-border-strong: var(--modal-text-secondary); --ft-border-accent: var(--modal-text-primary); --ft-text-primary: var(--modal-text-primary); --ft-text-secondary: var(--modal-text-secondary); --ft-input-bg: var(--modal-input-bg); --ft-input-border: var(--modal-input-border); --ft-hover-bg: var(--modal-button-hover-bg); --ft-hover-bg-strong: var(--modal-button-hover-bg); --ft-accent-color: var(--modal-primary-color); } /* Tag chip on tweet header */ .ft-tag-chip { display: inline-flex; align-items: center; margin-left: 4px; /* JS (ft_attachTagChipToArticle) 側の gap: 4px と連動 */ padding: 1px 8px; border-radius: 9999px; border: 1px solid currentColor; font-size: 11px; line-height: 1.4; cursor: pointer; user-select: none; white-space: nowrap; background: rgba(255, 255, 255, 0.03); /* これは静的なまま (ほぼ透明なので) */ flex: 0 0 auto; } .ft-tag-chip-label { max-width: 150px; overflow: hidden; text-overflow: ellipsis; } .ft-tag-chip-uncategorized { opacity: 0.7; } /* Dropdown for selecting tag / filter */ .ft-tag-dropdown { position: fixed; z-index: 2147482000; min-width: 220px; max-width: 260px; max-height: 60vh; overflow-y: auto; padding: 8px; border-radius: 12px; border: 1px solid var(--ft-border-light); background: var(--ft-bg); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.7); font-size: 13px; color: var(--ft-text-primary); } .ft-tag-dropdown-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-weight: 600; } .ft-tag-dropdown-close { border: none; background: transparent; color: inherit; cursor: pointer; padding: 2px 4px; } .ft-tag-dropdown-tags { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; } .ft-tag-dropdown-tag-item { display: flex; align-items: center; padding: 4px 6px; border-radius: 6px; cursor: pointer; } .ft-tag-dropdown-tag-item:hover { background: var(--ft-hover-bg); } .ft-tag-dropdown-tag-color { width: 10px; height: 10px; border-radius: 9999px; margin-right: 6px; } .ft-tag-dropdown-tag-label { flex: 1; } .ft-tag-dropdown-tag-selected::after { content: '✓'; margin-left: 6px; font-size: 11px; } /* New tag row in dropdown */ .ft-tag-dropdown-new { border-top: 1px solid var(--ft-border-dim); padding-top: 6px; display: flex; flex-direction: column; gap: 4px; } .ft-tag-dropdown-new-row { display: flex; gap: 4px; } .ft-tag-dropdown-new-input { flex: 1; background: var(--ft-input-bg); border: 1px solid var(--ft-input-border); border-radius: 6px; padding: 3px 6px; color: inherit; } .ft-tag-dropdown-new-color { width: 36px; padding: 0; border-radius: 6px; border: 1px solid var(--ft-input-border); background: transparent; } .ft-tag-dropdown-new-button { border-radius: 6px; border: 1px solid var(--ft-border-strong); background: transparent; color: inherit; padding: 2px 6px; font-size: 12px; cursor: pointer; } .ft-tag-dropdown-new-button:hover { background: var(--ft-hover-bg); } /* Bookmark header controls (テーマ変数適用) */ .ft-filter-button { border-radius: 8px; border: 1px solid var(--modal-border, rgba(239,243,244,0.3)); background: var(--modal-input-bg, rgba(0,0,0,0.2)); color: var(--modal-text-primary, rgb(239,243,244)); font-size: 14px; padding: 4px 10px; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; } .ft-filter-button-label { max-width: 140px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .ft-filter-button-caret { font-size: 10px; opacity: 0.8; } .ft-filter-button[disabled] { opacity: 0.4; cursor: default; } .ft-filter-button:not([disabled]):hover { background: var(--modal-button-hover-bg, rgba(255,255,255,0.06)); border-color: var(--modal-text-secondary, rgba(239,243,244,0.6)); } .ft-settings-button { border-radius: 9999px; width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--modal-border, rgba(239,243,244,0.3)); background: var(--modal-input-bg, rgba(0,0,0,0.2)); color: var(--modal-text-primary, rgb(239,243,244)); cursor: pointer; } .ft-settings-button:hover { background: var(--modal-button-hover-bg, rgba(255,255,255,0.06)); } /* Settings modal */ .ft-modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 2147483000; display: flex; align-items: center; justify-content: center; } .ft-modal { width: min(380px, 100vw - 32px); max-height: 80vh; border-radius: 16px; background: var(--ft-bg); border: 1px solid var(--ft-border-light); box-shadow: 0 20px 40px rgba(0,0,0,0.75); display: flex; flex-direction: column; color: var(--ft-text-primary); } .ft-modal-header { padding: 10px 14px; border-bottom: 1px solid var(--ft-border-dim); display: flex; align-items: center; justify-content: space-between; gap: 8px; } .ft-modal-title { font-size: 14px; font-weight: 600; } .ft-modal-toggle { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; } .ft-modal-toggle input[type="checkbox"] { accent-color: var(--ft-accent-color); } .ft-modal-body { padding: 10px 14px 12px; overflow-y: auto; font-size: 13px; } .ft-modal-footer { padding: 8px 14px 10px; border-top: 1px solid var(--ft-border-dim); display: flex; justify-content: flex-end; gap: 8px; } .ft-modal-button { border-radius: 9999px; border: 1px solid var(--ft-border-strong); background: transparent; color: inherit; font-size: 12px; padding: 4px 10px; cursor: pointer; } .ft-modal-button:hover { background: var(--ft-hover-bg); } /* Display settings section */ .ft-modal-display-settings { margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--ft-border-dim); font-size: 12px; } .ft-modal-display-settings-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 4px; } .ft-modal-display-radio { display: inline-flex; align-items: center; gap: 4px; } /* Tag list in modal */ .ft-modal-tag-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; } .ft-modal-tag-item { position: relative; display: grid; /* [mainCell] [dragHandle] [orderButtons] [deleteBtn] */ grid-template-columns: minmax(0, 1fr) auto auto auto; align-items: center; gap: 6px; /* cursor: grab; を削除 (ハンドルが担当) */ } .ft-modal-tag-main { display: flex; align-items: center; gap: 6px; } .ft-modal-tag-item-dragging { opacity: 0.6; } .ft-modal-tag-item-drop-before::before { content: ''; position: absolute; left: 0; right: 0; top: -4px; border-top: 2px solid var(--ft-border-accent); } .ft-modal-tag-item-drop-after::after { content: ''; position: absolute; left: 0; right: 0; bottom: -4px; border-bottom: 2px solid var(--ft-border-accent); } .ft-modal-tag-item-drop-child { background: var(--ft-hover-bg-strong); } .ft-modal-tag-name { background: var(--ft-input-bg); border-radius: 6px; border: 1px solid var(--ft-input-border); padding: 3px 6px; color: inherit; font-size: 12px; } .ft-modal-tag-color { width: 40px; padding: 0; border-radius: 6px; border: 1px solid var(--ft-input-border); background: transparent; } .ft-modal-tag-order, .ft-modal-tag-delete { border-radius: 6px; border: 1px solid var(--ft-border-strong); background: transparent; color: inherit; padding: 2px 4px; cursor: pointer; font-size: 11px; } .ft-modal-tag-order:hover, .ft-modal-tag-delete:hover { background: var(--ft-hover-bg-strong); } /* --- Drag handle for tag settings --- */ .ft-modal-tag-drag-handle { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; cursor: grab; color: var(--ft-text-secondary); user-select: none; } .ft-modal-tag-drag-handle:hover { background: var(--ft-hover-bg-strong); color: var(--ft-text-primary); } /* Uncategorized: disable drag */ .ft-modal-tag-item[data-kind="uncat"] .ft-modal-tag-drag-handle { cursor: not-allowed; opacity: 0.5; } /* New tag row */ .ft-modal-new-tag { border-top: 1px solid var(--ft-border-dim); padding-top: 8px; display: flex; flex-direction: column; gap: 6px; } .ft-modal-new-tag-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 6px; } /* 未分類は名前変更不可&削除不可の視覚表現 */ .ft-modal-tag-name[readonly] { cursor: not-allowed; opacity: 0.8; } .ft-modal-tag-delete:disabled { cursor: not-allowed; opacity: 0.4; } /* Hidden helper */ .ft-hidden { display: none !important; content-visibility: hidden; contain: strict; } /* --- End Favorite Tags CSS --- */ /* --- Favorites Feature --- */ .adv-fav-btn { display: inline-flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; color: rgb(83, 100, 113); /* Default grey */ padding: 0; margin: 0; width: 34.75px; height: 34.75px; /* X standard icon size touch target */ border-radius: 50%; transition: background-color 0.2s, color 0.2s; } /* ネイティブのクラスを借用した時は固定サイズを無効化する */ .adv-fav-btn.adv-native-style { width: auto; height: auto; min-width: 34.75px; /* 最低限の大きさは確保 */ min-height: 34.75px; } .adv-fav-btn:hover { background-color: rgba(29, 155, 240, 0.1); color: rgb(29, 155, 240); } .adv-fav-btn.active { color: rgb(249, 24, 128); /* Pink/Red like Like, or Gold? Let's use Gold for Star */ color: rgb(255, 215, 0); } .adv-fav-btn.active:hover { background-color: rgba(255, 215, 0, 0.1); } .adv-fav-btn svg { width: 20px; height: 20px; fill: currentColor; } .adv-item-body-text { font-size: 13px; color: var(--modal-text-primary); margin-top: 4px; white-space: pre-wrap; /* 改行を維持 */ word-break: break-word; /* 長い単語を折り返し */ } /* Favorites Media */ .adv-item-media-row { display: flex; gap: 4px; margin-top: 6px; overflow-x: auto; padding-bottom: 2px; } .adv-item-media-row::-webkit-scrollbar { height: 4px; } .adv-item-media-row::-webkit-scrollbar-thumb { background: var(--modal-border); border-radius: 2px; } .adv-media-thumb { height: 60px; min-width: 60px; border-radius: 6px; border: 1px solid var(--modal-border); object-fit: cover; cursor: pointer; } /* Favorites Quote */ .adv-quote-box { margin-top: 8px; border: 1px solid var(--modal-border); border-radius: 12px; padding: 8px 12px; background-color: rgba(0, 0, 0, 0.03); } .adv-quote-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; font-size: 12px; } .adv-quote-avatar { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; } .adv-quote-name { font-weight: 700; color: var(--modal-text-primary); } .adv-quote-handle { color: var(--modal-text-secondary); } .adv-quote-text { font-size: 13px; color: var(--modal-text-primary); white-space: pre-wrap; word-break: break-word; } /* Content Link */ .adv-content-link { color: var(--modal-primary-color); text-decoration: none; } .adv-content-link:hover { text-decoration: underline; } /* Media Play Icon */ .adv-media-wrap { position: relative; display: inline-flex; } .adv-media-play-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; background-color: rgba(0, 0, 0, 0.6); color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; pointer-events: none; /* クリックを下の画像(リンク)に透過させる */ backdrop-filter: blur(1px); z-index: 1; } .adv-media-play-icon svg { width: 14px; height: 14px; fill: currentColor; display: block; margin-left: 2px; } /* Favorites Item Tag Container */ .adv-fav-tag-container { margin-top:0.7px; margin-left: 2px; display: inline-flex; align-items: center; } /* タグチップのサイズ微調整 */ .adv-item-sub .ft-tag-chip { margin-left: 8px; font-size: 10px; padding: 0 6px; height: 18px; } `); const modalHTML = `

`; const initialize = async () => { i18n.init(); const kv = { get(key, def) { try { return GM_getValue(key, def); } catch (_) { return def; } }, set(key, val) { try { GM_setValue(key, val); } catch (_) {} }, del(key) { try { GM_deleteValue(key); } catch (_) {} }, }; const loadJSON = (key, def) => { try { const raw = kv.get(key, JSON.stringify(def)); return JSON.parse(raw); } catch(_) { return def; } }; const saveJSON = (key, value) => { try { kv.set(key, JSON.stringify(value)); } catch(_) {} }; const DEFAULT_TABS = ['search', 'history', 'saved', 'favorites', 'mute', 'lists', 'accounts']; const DEFAULT_TABS_VISIBILITY = { search: true, history: true, saved: true, favorites: true, mute: true, lists: true, accounts: true, }; const loadTabsVisibility = () => { const stored = loadJSON(TABS_VISIBILITY_KEY, DEFAULT_TABS_VISIBILITY); const normalized = { ...DEFAULT_TABS_VISIBILITY }; for (const key of DEFAULT_TABS) { normalized[key] = stored[key] === false ? false : true; // false のみ明示的に引き継ぐ } return normalized; }; const saveTabsVisibility = (state) => { saveJSON(TABS_VISIBILITY_KEY, state); }; /* --- Favorite Tags: Code Block --- */ // ------------- 定数 & 状態 ------------- // const FT_STATE_KEY = 'ftTagState_v1'; const FT_FILTER_ALL = 'all'; const FT_FILTER_UNCATEGORIZED = 'uncategorized'; const FT_TWEET_ID_REGEX = /\/status\/(\d+)/; let ft_state = null; let ft_initialized = false; let ft_currentFilter = FT_FILTER_ALL; let ft_currentDropdown = null; let ft_settingsModalBackdrop = null; let ft_dragSrcEntry = null; // ------------- State 管理 ------------- // function ft_createDefaultState() { return { enabled: true, tags: [], tweetTags: {}, uncategorized: { color: '#8899A6', order: 0 }, display: { mode: 'leaf' }, }; } function ft_normalizeTagOrdersFor(stateObj) { if (!stateObj || !Array.isArray(stateObj.tags)) return; const groups = new Map(); for (const tag of stateObj.tags) { if (!tag || typeof tag !== 'object') continue; const pid = tag.parentId || null; if (!groups.has(pid)) groups.set(pid, []); groups.get(pid).push(tag); } for (const arr of groups.values()) { arr.sort((a, b) => (typeof a.order === 'number' ? a.order : 0) - (typeof b.order === 'number' ? b.order : 0)); arr.forEach((tag, i) => { tag.order = i; }); } } function ft_countRootTagsFor(stateObj) { if (!stateObj || !Array.isArray(stateObj.tags)) return 0; return stateObj.tags.filter((t) => !t.parentId).length; } function ft_clampUncategorizedOrderFor(stateObj) { if (!stateObj) return; if (!stateObj.uncategorized || typeof stateObj.uncategorized !== 'object') { stateObj.uncategorized = { color: '#8899A6', order: 0 }; } const rootCount = ft_countRootTagsFor(stateObj); let pos = typeof stateObj.uncategorized.order === 'number' ? stateObj.uncategorized.order : 0; if (pos < 0) pos = 0; if (pos > rootCount) pos = rootCount; stateObj.uncategorized.order = pos; } function ft_normalizeTagOrders() { if (ft_state) ft_normalizeTagOrdersFor(ft_state); } function ft_clampUncategorizedOrder() { if (ft_state) ft_clampUncategorizedOrderFor(ft_state); } function ft_loadState() { try { const parsed = loadJSON(FT_STATE_KEY, null); if (!parsed || typeof parsed !== 'object') return ft_createDefaultState(); if (!Array.isArray(parsed.tags)) parsed.tags = []; if (!parsed.tweetTags || typeof parsed.tweetTags !== 'object') parsed.tweetTags = {}; parsed.enabled = true; if (!parsed.uncategorized || typeof parsed.uncategorized !== 'object') { parsed.uncategorized = { color: '#8899A6', order: 0 }; } else { if (!parsed.uncategorized.color) parsed.uncategorized.color = '#8899A6'; if (typeof parsed.uncategorized.order !== 'number') parsed.uncategorized.order = 0; } if (!parsed.display || typeof parsed.display !== 'object') { parsed.display = { mode: 'leaf' }; } else if (parsed.display.mode !== 'leaf' && parsed.display.mode !== 'full') { parsed.display.mode = 'leaf'; } ft_normalizeTagOrdersFor(parsed); ft_clampUncategorizedOrderFor(parsed); return parsed; } catch (e) { return ft_createDefaultState(); } } function ft_saveState(newState) { if (newState) ft_state = newState; try { if (ft_state) { ft_normalizeTagOrdersFor(ft_state); ft_clampUncategorizedOrderFor(ft_state); saveJSON(FT_STATE_KEY, ft_state); } } catch (e) {} requestAnimationFrame(() => { ft_refreshAllTagChips(); // お気に入りタブが開いていれば再描画してタグ変更/絞り込みを反映 if (getActiveTabName() === 'favorites') { renderFavorites(); } }); } function ft_generateTagId() { return 'tag_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); } function ft_getTagById(tagId) { return ft_state.tags.find((t) => t.id === tagId) || null; } function ft_getAllTags() { return ft_state.tags.slice(); } function ft_getTagColor(tagId) { const tag = ft_getTagById(tagId); return tag ? tag.color || '#1d9bf0' : '#8899A6'; } function ft_getUncategorizedColor() { return ft_state?.uncategorized?.color || '#8899A6'; } function ft_createNewTag(name, color, parentId) { const pid = parentId || null; const siblingsCount = ft_state.tags.filter((t) => (t.parentId || null) === pid).length; const tag = { id: ft_generateTagId(), name, color, parentId: pid, order: siblingsCount, }; ft_state.tags.push(tag); return tag; } function ft_countRootTags() { return ft_countRootTagsFor(ft_state); } function ft_getTagAncestors(tag) { const result = []; if (!tag) return result; const seen = new Set(); let current = tag; while (current) { if (seen.has(current.id)) break; seen.add(current.id); result.unshift(current); if (!current.parentId) break; current = ft_getTagById(current.parentId); } return result; } function ft_getTagFullPath(tag) { const ancestors = ft_getTagAncestors(tag); if (!ancestors.length) return tag ? tag.name || '' : ''; return ancestors.map((t) => t.name || '').join(' / '); } function ft_getTagDisplayLabelFromTag(tag) { if (!tag) return ''; const mode = ft_state?.display?.mode; if (mode === 'full') return ft_getTagFullPath(tag); return tag.name; } function ft_getTagListWithUncategorized() { const result = []; if (!ft_state || !Array.isArray(ft_state.tags)) return result; const byParent = new Map(); for (const tag of ft_state.tags) { if (!tag || typeof tag !== 'object') continue; const pid = tag.parentId || null; if (!byParent.has(pid)) byParent.set(pid, []); byParent.get(pid).push(tag); } for (const arr of byParent.values()) { arr.sort((a, b) => (typeof a.order === 'number' ? a.order : 0) - (typeof b.order === 'number' ? b.order : 0)); } function dfs(parentId, depth) { const arr = byParent.get(parentId || null); if (!arr) return; for (const tag of arr) { result.push({ tag, depth }); dfs(tag.id, depth + 1); } } dfs(null, 0); const entries = []; const rootCount = result.filter((e) => e.depth === 0).length; let uncatPos = ft_state.uncategorized.order || 0; if (uncatPos < 0) uncatPos = 0; if (uncatPos > rootCount) uncatPos = rootCount; let rootIndex = 0; for (const item of result) { if (item.depth === 0 && rootIndex === uncatPos) { entries.push({ kind: 'uncat', depth: 0 }); } entries.push({ kind: 'tag', tag: item.tag, depth: item.depth }); if (item.depth === 0) rootIndex++; } if (rootCount === 0 || uncatPos === rootCount) { entries.push({ kind: 'uncat', depth: 0 }); } return entries; } function ft_isTagInSubtree(tagId, rootTagId) { // ft_state が存在しない場合は即座に false を返す if (!ft_state || !tagId || !rootTagId) return false; if (tagId === rootTagId) return true; let current = ft_getTagById(tagId); const visited = new Set(); while (current && current.parentId) { if (visited.has(current.id)) break; visited.add(current.id); if (current.parentId === rootTagId) return true; current = ft_getTagById(current.parentId); } return false; } function ft_wouldCreateCycle(newParentId, childId) { if (!newParentId || !childId) return false; if (newParentId === childId) return true; let current = ft_getTagById(newParentId); const visited = new Set(); while (current && current.parentId) { if (visited.has(current.id)) break; visited.add(current.id); if (current.parentId === childId) return true; current = ft_getTagById(current.parentId); } return false; } // ------------- ルート & ユーティリティ ------------- // // ツイートのDOMからIDを抽出 function ft_extractTweetId(article) { if (article.dataset.ftTweetId) return article.dataset.ftTweetId; // 引用ツイート(カード部分)の中にあるリンクを除外するための判定関数 // div[role="link"] は引用カードのコンテナに付与される属性です const isInsideQuote = (el) => { return !!el.closest('div[role="link"]'); }; // 1. 最も確実な方法: