// ==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. 最も確実な方法: