Google Search Custom Sidebar

Customizable Google Search sidebar: quick filters (lang, time, filetype, country, date), site search, Verbatim & Personalization tools.

  1. // ==UserScript==
  2. // @name Google Search Custom Sidebar
  3. // @name:zh-TW Google 搜尋自訂側邊欄
  4. // @name:ja Google検索カスタムサイドバー
  5. // @namespace https://gf.qytechs.cn/en/users/1467948-stonedkhajiit
  6. // @version 0.2.0
  7. // @description Customizable Google Search sidebar: quick filters (lang, time, filetype, country, date), site search, Verbatim & Personalization tools.
  8. // @description:zh-TW Google 搜尋自訂側邊欄:快速篩選(語言、時間、檔案類型、國家、日期)、站內搜尋、一字不差與個人化工具。
  9. // @description:ja Google検索カスタムサイドバー:高速フィルター(言語,期間,ファイル形式,国,日付)、サイト検索、完全一致検索とパーソナライズツール。
  10. // @match https://www.google.com/search*
  11. // @include /^https:\/\/(?:ipv4|ipv6|www)\.google\.(?:[a-z\.]+)\/search\?(?:.+&)?q=[^&]+(?:&.+)?$/
  12. // @exclude /^https:\/\/(?:ipv4|ipv6|www)\.google\.(?:[a-z\.]+)\/search\?(?:.+&)?(?:tbm=(?:isch|shop|bks|flm|fin|lcl)|udm=(?:2|28))(?:&.+)?$/
  13. // @icon https://www.google.com/favicon.ico
  14. // @grant GM_addStyle
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_deleteValue
  19. // @run-at document-idle
  20. // @author StonedKhajiit
  21. // @license MIT
  22. // @require https://update.gf.qytechs.cn/scripts/535624/1607059/Google%20Search%20Custom%20Sidebar%20-%20i18n.js
  23. // @require https://update.gf.qytechs.cn/scripts/535625/1607060/Google%20Search%20Custom%20Sidebar%20-%20Styles.js
  24. // ==/UserScript==
  25.  
  26. (function() {
  27. 'use strict';
  28.  
  29. // --- Constants and Configuration ---
  30. const SCRIPT_INTERNAL_NAME = 'GoogleSearchCustomSidebar';
  31. const SCRIPT_VERSION = '0.2.0';
  32. const LOG_PREFIX = `[${SCRIPT_INTERNAL_NAME} v${SCRIPT_VERSION}]`;
  33.  
  34. const DEFAULT_SECTION_ORDER = [
  35. 'sidebar-section-language', 'sidebar-section-time', 'sidebar-section-filetype',
  36. 'sidebar-section-occurrence',
  37. 'sidebar-section-country', 'sidebar-section-date-range', 'sidebar-section-site-search', 'sidebar-section-tools'
  38. ];
  39.  
  40. const defaultSettings = {
  41. sidebarPosition: { left: 0, top: 80 },
  42. sectionStates: {},
  43. theme: 'system',
  44. hoverMode: false,
  45. idleOpacity: 0.8,
  46. sidebarWidth: 135,
  47. fontSize: 12.5,
  48. headerIconSize: 16,
  49. verticalSpacingMultiplier: 0.5,
  50. interfaceLanguage: 'auto',
  51. visibleSections: {
  52. 'sidebar-section-language': true, 'sidebar-section-time': true, 'sidebar-section-filetype': true,
  53. 'sidebar-section-occurrence': true,
  54. 'sidebar-section-country': true, 'sidebar-section-date-range': true,
  55. 'sidebar-section-site-search': true, 'sidebar-section-tools': true
  56. },
  57. sectionDisplayMode: 'remember',
  58. accordionMode: false,
  59. resetButtonLocation: 'topBlock',
  60. verbatimButtonLocation: 'header',
  61. advancedSearchLinkLocation: 'header',
  62. personalizationButtonLocation: 'tools',
  63. googleScholarShortcutLocation: 'tools',
  64. googleTrendsShortcutLocation: 'tools',
  65. googleDatasetSearchShortcutLocation: 'tools',
  66. countryDisplayMode: 'iconAndText',
  67. customLanguages: [],
  68. customTimeRanges: [],
  69. customFiletypes: [
  70. { text: "📄Documents", value: "pdf OR docx OR doc OR odt OR rtf OR txt" },
  71. { text: "💹Spreadsheets", value: "xlsx OR xls OR ods OR csv" },
  72. { text: "📊Presentations", value: "pptx OR ppt OR odp OR key" },
  73. ],
  74. customCountries: [],
  75. displayLanguages: [],
  76. displayCountries: [],
  77. favoriteSites: [
  78. // == SINGLE SITES: GENERAL KNOWLEDGE & REFERENCE ==
  79. { text: 'Wikipedia (EN)', url: 'en.wikipedia.org' },
  80. { text: 'Wiktionary', url: 'wiktionary.org' },
  81. { text: 'Internet Archive', url: 'archive.org' },
  82.  
  83. // == SINGLE SITES: DEVELOPER & TECH ==
  84. { text: 'GitHub', url: 'github.com' },
  85. { text: 'GitLab', url: 'gitlab.com' },
  86. { text: 'Stack Overflow', url: 'stackoverflow.com' },
  87. { text: 'Hacker News', url: 'news.ycombinator.com' },
  88. { text: 'Greasy Fork镜像', url: 'gf.qytechs.cn' },
  89.  
  90. // == SINGLE SITES: SOCIAL, FORUMS & COMMUNITIES ==
  91. { text: 'Reddit', url: 'reddit.com' },
  92. { text: 'X', url: 'x.com' },
  93. { text: 'Mastodon', url: 'mastodon.social' },
  94. { text: 'Bluesky', url: 'bsky.app' },
  95. { text: 'Lemmy', url: 'lemmy.world' },
  96.  
  97. // == SINGLE SITES: ENTERTAINMENT, ARTS & HOBBIES ==
  98. { text: 'IMDb', url: 'imdb.com' },
  99. { text: 'TMDb', url: 'themoviedb.org' },
  100. { text: 'Letterboxd', url: 'letterboxd.com' },
  101. { text: 'Metacritic', url: 'metacritic.com' },
  102. { text: 'OpenCritic', url: 'opencritic.com' },
  103. { text: 'Steam', url: 'store.steampowered.com' },
  104. { text: 'Bandcamp', url: 'bandcamp.com' },
  105. { text: 'Last.fm', url: 'last.fm' },
  106.  
  107. // == COMBINED SITE GROUPS ==
  108. {
  109. text: '💬Social',
  110. url: 'x.com OR facebook.com OR instagram.com OR threads.net OR bluesky.social OR mastodon.social OR reddit.com OR tumblr.com OR linkedin.com OR lemmy.world'
  111. },
  112. {
  113. text: '📦Repositories',
  114. url: 'github.com OR gitlab.com OR bitbucket.org OR codeberg.org OR sourceforge.net'
  115. },
  116. {
  117. text: '🎓Academics',
  118. url: 'scholar.google.com OR arxiv.org OR researchgate.net OR jstor.org OR academia.edu OR pubmed.ncbi.nlm.nih.gov OR semanticscholar.org OR core.ac.uk'
  119. },
  120. {
  121. text: '📰News',
  122. url: 'bbc.com/news OR reuters.com OR apnews.com OR nytimes.com OR theguardian.com OR cnn.com OR wsj.com'
  123. },
  124. {
  125. text: '🎨Creative',
  126. url: 'behance.net OR dribbble.com OR artstation.com OR deviantart.com'
  127. }
  128. ],
  129. enableSiteSearchCheckboxMode: true,
  130. showFaviconsForSiteSearch: true,
  131. enableFiletypeCheckboxMode: true,
  132. sidebarCollapsed: false,
  133. draggableHandleEnabled: true,
  134. enabledPredefinedOptions: {
  135. language: ['lang_en'],
  136. country: ['countryUS'],
  137. time: ['d', 'w', 'm', 'y', 'h'],
  138. filetype: ['pdf', 'docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls', 'txt']
  139. },
  140. sidebarSectionOrder: [...DEFAULT_SECTION_ORDER],
  141. hideGoogleLogoWhenExpanded: false,
  142. };
  143.  
  144. let sidebar = null, systemThemeMediaQuery = null;
  145. const MIN_SIDEBAR_TOP_POSITION = 5;
  146. let debouncedSaveSettings;
  147. let globalMessageTimeout = null;
  148.  
  149. const IDS = {
  150. SIDEBAR: 'customizable-search-sidebar', SETTINGS_OVERLAY: 'settings-overlay', SETTINGS_WINDOW: 'settings-window',
  151. COLLAPSE_BUTTON: 'sidebar-collapse-button', SETTINGS_BUTTON: 'open-settings-button',
  152. TOOL_RESET_BUTTON: 'tool-reset-button', TOOL_VERBATIM: 'tool-verbatim', TOOL_PERSONALIZE: 'tool-personalize-search',
  153. TOOL_GOOGLE_SCHOLAR: 'tool-google-scholar',
  154. TOOL_GOOGLE_TRENDS: 'tool-google-trends',
  155. TOOL_GOOGLE_DATASET_SEARCH: 'tool-google-dataset-search',
  156. APPLY_SELECTED_SITES_BUTTON: 'apply-selected-sites-button',
  157. APPLY_SELECTED_FILETYPES_BUTTON: 'apply-selected-filetypes-button',
  158. FIXED_TOP_BUTTONS: 'sidebar-fixed-top-buttons',
  159. SETTINGS_MESSAGE_BAR: 'gscs-settings-message-bar',
  160. SETTING_WIDTH: 'setting-sidebar-width', SETTING_FONT_SIZE: 'setting-font-size', SETTING_HEADER_ICON_SIZE: 'setting-header-icon-size',
  161. SETTING_VERTICAL_SPACING: 'setting-vertical-spacing', SETTING_INTERFACE_LANGUAGE: 'setting-interface-language',
  162. SETTING_SECTION_MODE: 'setting-section-display-mode', SETTING_ACCORDION: 'setting-accordion-mode',
  163. SETTING_DRAGGABLE: 'setting-draggable-handle', SETTING_RESET_LOCATION: 'setting-reset-button-location',
  164. SETTING_VERBATIM_LOCATION: 'setting-verbatim-button-location', SETTING_ADV_SEARCH_LOCATION: 'setting-adv-search-link-location',
  165. SETTING_PERSONALIZE_LOCATION: 'setting-personalize-button-location',
  166. SETTING_SCHOLAR_LOCATION: 'setting-scholar-shortcut-location',
  167. SETTING_TRENDS_LOCATION: 'setting-trends-shortcut-location',
  168. SETTING_DATASET_SEARCH_LOCATION: 'setting-dataset-search-shortcut-location',
  169. SETTING_SITE_SEARCH_CHECKBOX_MODE: 'setting-site-search-checkbox-mode',
  170. SETTING_SHOW_FAVICONS: 'setting-show-favicons',
  171. SETTING_FILETYPE_SEARCH_CHECKBOX_MODE: 'setting-filetype-search-checkbox-mode',
  172. SETTING_COUNTRY_DISPLAY_MODE: 'setting-country-display-mode', SETTING_THEME: 'setting-theme',
  173. SETTING_HOVER: 'setting-hover-mode', SETTING_OPACITY: 'setting-idle-opacity',
  174. SETTING_HIDE_GOOGLE_LOGO: 'setting-hide-google-logo',
  175. TAB_PANE_GENERAL: 'tab-pane-general', TAB_PANE_APPEARANCE: 'tab-pane-appearance', TAB_PANE_FEATURES: 'tab-pane-features', TAB_PANE_CUSTOM: 'tab-pane-custom',
  176. SITES_LIST: 'custom-sites-list', LANG_LIST: 'custom-languages-list', TIME_LIST: 'custom-time-ranges-list',
  177. FT_LIST: 'custom-filetypes-list', COUNTRIES_LIST: 'custom-countries-list',
  178. NEW_SITE_NAME: 'new-site-name', NEW_SITE_URL: 'new-site-url', ADD_SITE_BTN: 'add-site-button',
  179. NEW_LANG_TEXT: 'new-lang-text', NEW_LANG_VALUE: 'new-lang-value', ADD_LANG_BTN: 'add-lang-button',
  180. NEW_TIME_TEXT: 'new-timerange-text', NEW_TIME_VALUE: 'new-timerange-value', ADD_TIME_BTN: 'add-timerange-button',
  181. NEW_FT_TEXT: 'new-ft-text', NEW_FT_VALUE: 'new-ft-value', ADD_FT_BTN: 'add-ft-button',
  182. NEW_COUNTRY_TEXT: 'new-country-text', NEW_COUNTRY_VALUE: 'new-country-value', ADD_COUNTRY_BTN: 'add-country-button',
  183. DATE_MIN: 'date-min', DATE_MAX: 'date-max', DATE_RANGE_ERROR_MSG: 'date-range-error-msg',
  184. SIDEBAR_SECTION_ORDER_LIST: 'sidebar-section-order-list',
  185. NOTIFICATION_CONTAINER: 'gscs-notification-container',
  186. MODAL_ADD_NEW_OPTION_BTN: 'gscs-modal-add-new-option-btn',
  187. MODAL_PREDEFINED_CHOOSER_CONTAINER: 'gscs-modal-predefined-chooser-container',
  188. MODAL_PREDEFINED_CHOOSER_LIST: 'gscs-modal-predefined-chooser-list',
  189. MODAL_PREDEFINED_CHOOSER_ADD_BTN: 'gscs-modal-predefined-chooser-add-btn',
  190. MODAL_PREDEFINED_CHOOSER_CANCEL_BTN: 'gscs-modal-predefined-chooser-cancel-btn',
  191. CLEAR_SITE_SEARCH_OPTION: 'clear-site-search-option',
  192. CLEAR_FILETYPE_SEARCH_OPTION: 'clear-filetype-search-option'
  193. };
  194. const CSS = {
  195. SIDEBAR_COLLAPSED: 'sidebar-collapsed', SIDEBAR_HEADER: 'sidebar-header', SIDEBAR_CONTENT_WRAPPER: 'sidebar-content-wrapper',
  196. DRAG_HANDLE: 'sidebar-drag-handle', SETTINGS_BUTTON: 'sidebar-settings-button', HEADER_BUTTON: 'sidebar-header-button',
  197. SIDEBAR_SECTION: 'sidebar-section', FIXED_TOP_BUTTON_ITEM: 'fixed-top-button-item', SECTION_TITLE: 'section-title',
  198. SECTION_CONTENT: 'section-content', COLLAPSED: 'collapsed', FILTER_OPTION: 'filter-option', SELECTED: 'selected',
  199. SITE_SEARCH_ITEM_CHECKBOX: 'site-search-item-checkbox',
  200. FILETYPE_SEARCH_ITEM_CHECKBOX: 'filetype-search-item-checkbox',
  201. APPLY_SITES_BUTTON: 'apply-sites-button',
  202. APPLY_FILETYPES_BUTTON: 'apply-filetypes-button',
  203. DATE_INPUT_LABEL: 'date-input-label', DATE_INPUT: 'date-input', TOOL_BUTTON: 'tool-button', ACTIVE: 'active',
  204. CUSTOM_LIST: 'custom-list', ITEM_CONTROLS: 'item-controls', EDIT_CUSTOM_ITEM: 'edit-custom-item',
  205. DELETE_CUSTOM_ITEM: 'delete-custom-item', CUSTOM_LIST_INPUT_GROUP: 'custom-list-input-group',
  206. ADD_CUSTOM_BUTTON: 'add-custom-button', SETTINGS_HEADER: 'settings-header', SETTINGS_CLOSE_BTN: 'settings-close-button',
  207. SETTINGS_TABS: 'settings-tabs', TAB_BUTTON: 'tab-button', SETTINGS_TAB_CONTENT: 'settings-tab-content',
  208. TAB_PANE: 'tab-pane', SETTING_ITEM: 'setting-item', INLINE_LABEL: 'inline', SETTINGS_FOOTER: 'settings-footer',
  209. SAVE_BUTTON: 'save-button', CANCEL_BUTTON: 'cancel-button', RESET_BUTTON: 'reset-button',
  210. LIGHT_THEME: 'light-theme', DARK_THEME: 'dark-theme',
  211. SIMPLE_ITEM: 'simple', RANGE_VALUE: 'range-value', RANGE_HINT: 'setting-range-hint', SECTION_ORDER_LIST: 'section-order-list',
  212. INPUT_ERROR_MESSAGE: 'input-error-message', ERROR_VISIBLE: 'error-visible', INPUT_HAS_ERROR: 'input-has-error',
  213. DATE_RANGE_ERROR_MSG: 'date-range-error-message',
  214. MESSAGE_BAR: 'gscs-message-bar', MSG_INFO: 'gscs-msg-info', MSG_SUCCESS: 'gscs-msg-success',
  215. MSG_WARNING: 'gscs-msg-warning', MSG_ERROR: 'gscs-msg-error', MANAGE_CUSTOM_BUTTON: 'manage-custom-button',
  216. NOTIFICATION: 'gscs-notification',
  217. NTF_INFO: 'gscs-ntf-info', NTF_SUCCESS: 'gscs-ntf-success',
  218. NTF_WARNING: 'gscs-ntf-warning', NTF_ERROR: 'gscs-ntf-error',
  219. DRAGGING_ITEM: 'gscs-dragging-item',
  220. DRAG_OVER_HIGHLIGHT: 'gscs-drag-over-highlight',
  221. DRAG_ICON: 'gscs-drag-icon',
  222. FAVICON: 'gscs-favicon',
  223. REMOVE_FROM_LIST_BTN: 'gscs-remove-from-list-btn',
  224. MODAL_ADD_NEW_OPTION_BTN_CLASS: 'gscs-modal-add-new-option-button-class',
  225. MODAL_PREDEFINED_CHOOSER_CLASS: 'gscs-modal-predefined-chooser',
  226. MODAL_PREDEFINED_CHOOSER_ITEM: 'gscs-modal-predefined-chooser-item',
  227. SETTING_VALUE_HINT: 'setting-value-hint'
  228. };
  229. const DATA_ATTR = {
  230. FILTER_TYPE: 'filterType', FILTER_VALUE: 'filterValue', SITE_URL: 'siteUrl', SECTION_ID: 'sectionId',
  231. FILETYPE_VALUE: 'filetypeValue',
  232. LIST_ID: 'listId', INDEX: 'index', LISTENER_ATTACHED: 'listenerAttached', TAB: 'tab', MANAGE_TYPE: 'managetype',
  233. ITEM_TYPE: 'itemType', ITEM_ID: 'itemId'
  234. };
  235. const STORAGE_KEY = 'googleSearchCustomSidebarSettings_v1';
  236. const SVG_ICONS = {
  237. chevronLeft: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`,
  238. chevronRight: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`,
  239. settings: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06-.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`,
  240. reset: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
  241. verbatim: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><g transform="translate(-4.3875 -3.2375) scale(1.15)"><path d="M6 17.5c0 1.5 1.5 2.5 3 2.5h1.5c1.5 0 3-1 3-2.5V9c0-1.5-1.5-2.5-3-2.5H9C7.5 6.5 6 7.5 6 9v8.5z"/><path d="M15 17.5c0 1.5 1.5 2.5 3 2.5h1.5c1.5 0 3-1 3-2.5V9c0-1.5-1.5-2.5-3-2.5H18c-1.5 0-3 1-3 2.5v8.5z"/></g></svg>`,
  242. magnifyingGlass: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
  243. close: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
  244. edit: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
  245. delete: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>`,
  246. add: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
  247. update: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
  248. personalization: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
  249. dragGrip: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>`,
  250. removeFromList: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
  251. googleScholar: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M12 3L1 9l11 6 9-4.91V17h2V9L12 3zm0 11.24L3.62 9 12 5.11 20.38 9 12 14.24zM5 13.18V17.5a1.5 1.5 0 001.5 1.5h11A1.5 1.5 0 0019 17.5v-4.32l-7 3.82-7-3.82z"/></svg>`,
  252. googleTrends: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6h-6z"/></svg>`,
  253. googleDatasetSearch: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M3 18v-2h18v2H3Zm0-5v-2h18v2H3Zm0-5V6h18v2H3Z"/></svg>`
  254. };
  255. const PREDEFINED_OPTIONS = {
  256. language: [ { textKey: 'predefined_lang_en', value: 'lang_en' }, { textKey: 'predefined_lang_ja', value: 'lang_ja' }, { textKey: 'predefined_lang_ko', value: 'lang_ko' }, { textKey: 'predefined_lang_fr', value: 'lang_fr' }, { textKey: 'predefined_lang_de', value: 'lang_de' }, { textKey: 'predefined_lang_es', value: 'lang_es' }, { textKey: 'predefined_lang_it', value: 'lang_it' }, { textKey: 'predefined_lang_pt', value: 'lang_pt' }, { textKey: 'predefined_lang_ru', value: 'lang_ru' }, { textKey: 'predefined_lang_ar', value: 'lang_ar' }, { textKey: 'predefined_lang_hi', value: 'lang_hi' }, { textKey: 'predefined_lang_nl', value: 'lang_nl' }, { textKey: 'predefined_lang_tr', value: 'lang_tr' }, { textKey: 'predefined_lang_vi', value: 'lang_vi' }, { textKey: 'predefined_lang_th', value: 'lang_th' }, { textKey: 'predefined_lang_id', value: 'lang_id' }, { textKey: 'predefined_lang_zh_tw', value: 'lang_zh-TW' }, { textKey: 'predefined_lang_zh_cn', value: 'lang_zh-CN' }, { textKey: 'predefined_lang_zh_all', value: 'lang_zh-TW|lang_zh-CN' }, ],
  257. country: [ { textKey: 'predefined_country_us', value: 'countryUS' }, { textKey: 'predefined_country_gb', value: 'countryGB' }, { textKey: 'predefined_country_ca', value: 'countryCA' }, { textKey: 'predefined_country_au', value: 'countryAU' }, { textKey: 'predefined_country_de', value: 'countryDE' }, { textKey: 'predefined_country_fr', value: 'countryFR' }, { textKey: 'predefined_country_jp', value: 'countryJP' }, { textKey: 'predefined_country_kr', value: 'countryKR' }, { textKey: 'predefined_country_cn', value: 'countryCN' }, { textKey: 'predefined_country_in', value: 'countryIN' }, { textKey: 'predefined_country_br', value: 'countryBR' }, { textKey: 'predefined_country_mx', value: 'countryMX' }, { textKey: 'predefined_country_es', value: 'countryES' }, { textKey: 'predefined_country_it', value: 'countryIT' }, { textKey: 'predefined_country_ru', value: 'countryRU' }, { textKey: 'predefined_country_nl', value: 'countryNL' }, { textKey: 'predefined_country_sg', value: 'countrySG' }, { textKey: 'predefined_country_hk', value: 'countryHK' }, { textKey: 'predefined_country_tw', value: 'countryTW' }, { textKey: 'predefined_country_my', value: 'countryMY' }, { textKey: 'predefined_country_vn', value: 'countryVN' }, { textKey: 'predefined_country_ph', value: 'countryPH' }, { textKey: 'predefined_country_th', value: 'countryTH' }, { textKey: 'predefined_country_za', value: 'countryZA' }, { textKey: 'predefined_country_tr', value: 'countryTR' }, ],
  258. time: [ { textKey: 'predefined_time_h', value: 'h' }, { textKey: 'predefined_time_h2', value: 'h2' }, { textKey: 'predefined_time_h6', value: 'h6' }, { textKey: 'predefined_time_h12', value: 'h12' }, { textKey: 'predefined_time_d', value: 'd' }, { textKey: 'predefined_time_d2', value: 'd2' }, { textKey: 'predefined_time_d3', value: 'd3' }, { textKey: 'predefined_time_w', value: 'w' }, { textKey: 'predefined_time_m', value: 'm' }, { textKey: 'predefined_time_y', value: 'y' }, ],
  259. filetype: [ { textKey: 'predefined_filetype_pdf', value: 'pdf' }, { textKey: 'predefined_filetype_docx', value: 'docx' }, { textKey: 'predefined_filetype_doc', value: 'doc' }, { textKey: 'predefined_filetype_xlsx', value: 'xlsx' }, { textKey: 'predefined_filetype_xls', value: 'xls' }, { textKey: 'predefined_filetype_pptx', value: 'pptx' }, { textKey: 'predefined_filetype_ppt', value: 'ppt' }, { textKey: 'predefined_filetype_txt', value: 'txt' }, { textKey: 'predefined_filetype_rtf', value: 'rtf' }, { textKey: 'predefined_filetype_html', value: 'html' }, { textKey: 'predefined_filetype_htm', value: 'htm' }, { textKey: 'predefined_filetype_xml', value: 'xml' }, { textKey: 'predefined_filetype_jpg', value: 'jpg' }, { textKey: 'predefined_filetype_png', value: 'png' }, { textKey: 'predefined_filetype_gif', value: 'gif' }, { textKey: 'predefined_filetype_svg', value: 'svg' }, { textKey: 'predefined_filetype_bmp', value: 'bmp' }, { textKey: 'predefined_filetype_js', value: 'js' }, { textKey: 'predefined_filetype_css', value: 'css' }, { textKey: 'predefined_filetype_py', value: 'py' }, { textKey: 'predefined_filetype_java', value: 'java' }, { textKey: 'predefined_filetype_cpp', value: 'cpp' }, { textKey: 'predefined_filetype_cs', value: 'cs' }, { textKey: 'predefined_filetype_kml', value: 'kml'}, { textKey: 'predefined_filetype_kmz', value: 'kmz'}, ]
  260. };
  261. const ALL_SECTION_DEFINITIONS = [
  262. { id: 'sidebar-section-language', type: 'filter', titleKey: 'section_language', scriptDefined: [{textKey:'filter_any_language',v:''}], param: 'lr', predefinedOptionsKey: 'language', customItemsKey: 'customLanguages', displayItemsKey: 'displayLanguages' },
  263. { id: 'sidebar-section-time', type: 'filter', titleKey: 'section_time', scriptDefined: [{textKey:'filter_any_time',v:''}], param: 'qdr', predefinedOptionsKey: 'time', customItemsKey: 'customTimeRanges' },
  264. { id: 'sidebar-section-filetype', type: 'filetype', titleKey: 'section_filetype', scriptDefined: [{ textKey: 'filter_any_format', v: '' }], param: 'as_filetype', predefinedOptionsKey: 'filetype', customItemsKey: 'customFiletypes' },
  265. {
  266. id: 'sidebar-section-occurrence',
  267. type: 'filter',
  268. titleKey: 'section_occurrence',
  269. scriptDefined: [
  270. { textKey: 'filter_occurrence_any', v: 'any' },
  271. { textKey: 'filter_occurrence_title', v: 'title' },
  272. { textKey: 'filter_occurrence_text', v: 'body' },
  273. { textKey: 'filter_occurrence_url', v: 'url' },
  274. { textKey: 'filter_occurrence_links', v: 'links' }
  275. ],
  276. param: 'as_occt'
  277. },
  278. { id: 'sidebar-section-country', type: 'filter', titleKey: 'section_country', scriptDefined: [{textKey:'filter_any_country',v:''}], param: 'cr', predefinedOptionsKey: 'country', customItemsKey: 'customCountries', displayItemsKey: 'displayCountries' },
  279. { id: 'sidebar-section-date-range', type: 'date', titleKey: 'section_date_range' },
  280. { id: 'sidebar-section-site-search', type: 'site', titleKey: 'section_site_search', scriptDefined: [{ textKey: 'filter_any_site', v:''}] },
  281. { id: 'sidebar-section-tools', type: 'tools', titleKey: 'section_tools' }
  282. ];
  283.  
  284. const LocalizationService = (function() {
  285. const builtInTranslations = {
  286. 'en': {
  287. scriptName: 'Google Search Custom Sidebar', settingsTitle: 'Google Search Custom Sidebar Settings', manageOptionsTitle: 'Manage Options', manageSitesTitle: 'Manage Favorite Sites', manageLanguagesTitle: 'Manage Language Options', manageCountriesTitle: 'Manage Country/Region Options', manageTimeRangesTitle: 'Manage Time Ranges', manageFileTypesTitle: 'Manage File Types', section_language: 'Language', section_time: 'Time', section_filetype: 'File Type', section_country: 'Country/Region', section_date_range: 'Date Range', section_site_search: 'Site Search', section_tools: 'Tools',
  288. section_occurrence: 'Keyword Location',
  289. filter_any_language: 'Any Language', filter_any_time: 'Any Time', filter_any_format: 'Any Format', filter_any_country: 'Any Country/Region', filter_any_site: 'Any Site',
  290. filter_occurrence_any: 'Anywhere in the page', filter_occurrence_title: 'In the title of the page', filter_occurrence_text: 'In the text of the page', filter_occurrence_url: 'In the URL of the page',
  291. filter_occurrence_links: 'In links to the page',
  292. filter_clear_site_search: 'Clear Site Search', filter_clear_tooltip_suffix: '(Clear)', predefined_lang_zh_tw: 'Traditional Chinese', predefined_lang_zh_cn: 'Simplified Chinese', predefined_lang_zh_all: 'All Chinese', predefined_lang_en: 'English', predefined_lang_ja: 'Japanese', predefined_lang_ko: 'Korean', predefined_lang_fr: 'French', predefined_lang_de: 'German', predefined_lang_es: 'Spanish', predefined_lang_it: 'Italian', predefined_lang_pt: 'Portuguese', predefined_lang_ru: 'Russian', predefined_lang_ar: 'Arabic', predefined_lang_hi: 'Hindi', predefined_lang_nl: 'Dutch', predefined_lang_tr: 'Turkish', predefined_lang_vi: 'Vietnamese', predefined_lang_th: 'Thai', predefined_lang_id: 'Indonesian', predefined_country_tw: '🇹🇼 Taiwan', predefined_country_jp: '🇯🇵 Japan', predefined_country_kr: '🇰🇷 South Korea', predefined_country_cn: '🇨🇳 China', predefined_country_hk: '🇭🇰 Hong Kong', predefined_country_sg: '🇸🇬 Singapore', predefined_country_my: '🇲🇾 Malaysia', predefined_country_vn: '🇻🇳 Vietnam', predefined_country_ph: '🇵🇭 Philippines', predefined_country_th: '🇹🇭 Thailand', predefined_country_us: '🇺🇸 United States', predefined_country_ca: '🇨🇦 Canada', predefined_country_br: '🇧🇷 Brazil', predefined_country_mx: '🇲🇽 Mexico', predefined_country_gb: '🇬🇧 United Kingdom', predefined_country_de: '🇩🇪 Germany', predefined_country_fr: '🇫🇷 France', predefined_country_it: '🇮🇹 Italy', predefined_country_es: '🇪🇸 Spain', predefined_country_ru: '🇷🇺 Russia', predefined_country_nl: '🇳🇱 Netherlands', predefined_country_au: '🇦🇺 Australia', predefined_country_in: '🇮🇳 India', predefined_country_za: '🇿🇦 South Africa', predefined_country_tr: '🇹🇷 Turkey', predefined_time_h: 'Past hour', predefined_time_h2: 'Past 2 hours', predefined_time_h6: 'Past 6 hours', predefined_time_h12: 'Past 12 hours', predefined_time_d: 'Past 24 hours', predefined_time_d2: 'Past 2 days', predefined_time_d3: 'Past 3 days', predefined_time_w: 'Past week', predefined_time_m: 'Past month', predefined_time_y: 'Past year', predefined_filetype_pdf: 'PDF', predefined_filetype_docx: 'Word (docx)', predefined_filetype_doc: 'Word (doc)', predefined_filetype_xlsx: 'Excel (xlsx)', predefined_filetype_xls: 'Excel (xls)', predefined_filetype_pptx: 'PowerPoint (pptx)', predefined_filetype_ppt: 'PowerPoint (ppt)', predefined_filetype_txt: 'Plain Text', predefined_filetype_rtf: 'Rich Text Format', predefined_filetype_html: 'Web Page (html)', predefined_filetype_htm: 'Web Page (htm)', predefined_filetype_xml: 'XML', predefined_filetype_jpg: 'JPEG Image', predefined_filetype_png: 'PNG Image', predefined_filetype_gif: 'GIF Image', predefined_filetype_svg: 'SVG Image', predefined_filetype_bmp: 'BMP Image', predefined_filetype_js: 'JavaScript', predefined_filetype_css: 'CSS', predefined_filetype_py: 'Python', predefined_filetype_java: 'Java', predefined_filetype_cpp: 'C++', predefined_filetype_cs: 'C#', predefined_filetype_kml: 'Google Earth (kml)', predefined_filetype_kmz: 'Google Earth (kmz)',
  293. tool_reset_filters: 'Reset Filters', tool_verbatim_search: 'Verbatim Search', tool_advanced_search: 'Advanced Search', tool_apply_date: 'Apply Dates',
  294. tool_personalization_toggle: 'Personalization', tool_apply_selected_sites: 'Apply Selected',
  295. tool_apply_selected_filetypes: 'Apply Selected',
  296. tool_google_scholar: 'Scholar',
  297. tooltip_google_scholar_search: 'Search keywords on Google Scholar',
  298. service_name_google_scholar: 'Google Scholar',
  299. tool_google_trends: 'Trends',
  300. tooltip_google_trends_search: 'Explore keywords on Google Trends',
  301. service_name_google_trends: 'Google Trends',
  302. tool_google_dataset_search: 'Dataset Search',
  303. tooltip_google_dataset_search: 'Search keywords on Google Dataset Search',
  304. service_name_google_dataset_search: 'Google Dataset Search',
  305. link_advanced_search_title: 'Open Google Advanced Search page', tooltip_site_search: 'Search within {siteUrl}', tooltip_clear_site_search: 'Remove site: restriction', tooltip_toggle_personalization_on: 'Click to turn Personalization ON (Results tailored to you)', tooltip_toggle_personalization_off: 'Click to turn Personalization OFF (More generic results)', settings_tab_general: 'General', settings_tab_appearance: 'Appearance', settings_tab_features: 'Features', settings_tab_custom: 'Custom', settings_close_button_title: 'Close', settings_interface_language: 'Interface Language:', settings_language_auto: 'Auto (Browser Default)', settings_section_mode: 'Section Collapse Mode:', settings_section_mode_remember: 'Remember State', settings_section_mode_expand: 'Expand All', settings_section_mode_collapse: 'Collapse All',
  306. settings_accordion_mode: 'Accordion Mode (only when "Remember State" is active)',
  307. settings_accordion_mode_hint_desc: 'When enabled, expanding one section will automatically collapse other open sections.',
  308. settings_enable_drag: 'Enable Dragging', settings_reset_button_location: 'Reset Button Location:', settings_verbatim_button_location: 'Verbatim Button Location:', settings_adv_search_location: '"Advanced Search" Link Location:', settings_personalize_button_location: 'Personalization Button Location:',
  309. settings_scholar_location: 'Google Scholar Shortcut Location:',
  310. settings_trends_location: 'Google Trends Shortcut Location:',
  311. settings_dataset_search_location: 'Dataset Search Shortcut Location:',
  312. settings_enable_site_search_checkbox_mode: 'Enable Checkbox Mode for Site Search',
  313. settings_enable_site_search_checkbox_mode_hint: 'Allows selecting multiple favorite sites for a combined (OR) search.',
  314. settings_show_favicons: 'Show Favicons for Site Search',
  315. settings_show_favicons_hint: 'Displays a website icon next to single-site entries for better identification.',
  316. settings_enable_filetype_search_checkbox_mode: 'Enable Checkbox Mode for Filetype Search',
  317. settings_enable_filetype_search_checkbox_mode_hint: 'Allows selecting multiple filetypes for a combined (OR) search.',
  318. settings_location_tools: 'Tools Section', settings_location_top: 'Top Block', settings_location_header: 'Sidebar Header', settings_location_hide: 'Hide', settings_sidebar_width: 'Sidebar Width (px)', settings_width_range_hint: '(Range: 90-270, Step: 5)', settings_font_size: 'Base Font Size (px)', settings_font_size_range_hint: '(Range: 8-24, Step: 0.5)', settings_header_icon_size: 'Header Icon Size (px)', settings_header_icon_size_range_hint: '(Range: 8-32, Step: 0.5)', settings_vertical_spacing: 'Vertical Spacing', settings_vertical_spacing_range_hint: '(Multiplier Range: 0.05-1.5, Step: 0.05)', settings_theme: 'Theme:', settings_theme_system: 'Follow System', settings_theme_light: 'Light', settings_theme_dark: 'Dark', settings_theme_minimal_light: 'Minimal (Light)', settings_theme_minimal_dark: 'Minimal (Dark)', settings_hover_mode: 'Hover Mode', settings_idle_opacity: 'Idle Opacity:', settings_opacity_range_hint: '(Range: 0.1-1.0, Step: 0.05)', settings_country_display: 'Country/Region Display:', settings_country_display_icontext: 'Icon & Text', settings_country_display_text: 'Text Only', settings_country_display_icon: 'Icon Only', settings_visible_sections: 'Visible Sections:', settings_section_order: 'Adjust Sidebar Section Order (Drag & Drop):',
  319. settings_section_order_hint: '(Drag items to reorder. Only affects checked sections)',
  320. settings_no_orderable_sections: 'No visible sections to order.',
  321. settings_move_up_title: 'Move Up',
  322. settings_move_down_title: 'Move Down',
  323. settings_hide_google_logo: 'Hide Google Logo when sidebar is expanded',
  324. settings_hide_google_logo_hint: 'Useful if the sidebar is placed in the top-left corner with a minimal theme.',
  325. settings_custom_intro: 'Manage filter options for each section:',
  326. settings_manage_sites_button: 'Manage Favorite Sites...', settings_manage_languages_button: 'Manage Language Options...', settings_manage_countries_button: 'Manage Country/Region Options...', settings_manage_time_ranges_button: 'Manage Time Ranges...', settings_manage_file_types_button: 'Manage File Types...', settings_save_button: 'Save Settings', settings_cancel_button: 'Cancel', settings_reset_all_button: 'Reset All',
  327. modal_label_enable_predefined: 'Enable Predefined {type}:',
  328. modal_label_my_custom: 'My Custom {type}:',
  329. modal_label_display_options_for: 'Display Options for {type} (Drag to Sort):',
  330. modal_button_add_new_option: 'Add New Option...',
  331. modal_button_add_predefined_option: 'Add Predefined...',
  332. modal_button_add_custom_option: 'Add Custom...',
  333. modal_placeholder_name: 'Name', modal_placeholder_domain: 'Domain (e.g., site.com OR example.net/path)',
  334. modal_placeholder_text: 'Text', modal_placeholder_value: 'Value (e.g., pdf OR docx)',
  335. modal_hint_domain: 'Format: domain/path (e.g., `wikipedia.org/wiki/Page` or `site.com`). Use `OR` (case-insensitive, space separated) for multiple.',
  336. modal_hint_language: 'Format: starts with `lang_`, e.g., `lang_ja`, `lang_zh-TW`. Use `|` for multiple.', modal_hint_country: 'Format: `country` + 2-letter uppercase code, e.g., `countryDE`', modal_hint_time: 'Format: `h`, `d`, `w`, `m`, `y`, optionally followed by numbers, e.g., `h1`, `d7`, `w`',
  337. modal_hint_filetype: 'Format: extension (e.g., `pdf`). Use `OR` (case-insensitive, space separated) for multiple (e.g., `docx OR xls`).',
  338. modal_tooltip_domain: 'Enter domain(s) with optional path(s). Use OR for multiple, e.g., site.com/path OR example.org',
  339. modal_tooltip_language: 'Format: lang_xx or lang_xx-XX, separate multiple with |', modal_tooltip_country: 'Format: countryXX (XX = uppercase country code)', modal_tooltip_time: 'Format: h, d, w, m, y, optionally followed by numbers',
  340. modal_tooltip_filetype: 'File extension(s). Use OR for multiple, e.g., pdf OR docx',
  341. modal_button_add_title: 'Add', modal_button_update_title: 'Update Item', modal_button_cancel_edit_title: 'Cancel Edit', modal_button_edit_title: 'Edit', modal_button_delete_title: 'Delete', modal_button_remove_from_list_title: 'Remove from list', modal_button_complete: 'Done', value_empty: '(empty)', date_range_from: 'From:', date_range_to: 'To:', sidebar_collapse_title: 'Collapse', sidebar_expand_title: 'Expand', sidebar_drag_title: 'Drag', sidebar_settings_title: 'Settings',
  342. alert_invalid_start_date: 'Invalid start date', alert_invalid_end_date: 'Invalid end date', alert_end_before_start: 'End date cannot be earlier than start date', alert_start_in_future: 'Start date cannot be in the future', alert_end_in_future: 'End date cannot be in the future', alert_select_date: 'Please select a date', alert_error_applying_date: 'Error applying date range', alert_error_applying_filter: 'Error applying filter {type}={value}', alert_error_applying_site_search: 'Error applying site search for {site}', alert_error_clearing_site_search: 'Error clearing site search', alert_error_resetting_filters: 'Error resetting filters', alert_error_toggling_verbatim: 'Error toggling Verbatim search', alert_error_toggling_personalization: 'Error toggling Personalization search', alert_enter_display_name: 'Please enter the display name for {type}.', alert_enter_value: 'Please enter the corresponding value for {type}.', alert_invalid_value_format: 'The value format for {type} is incorrect. {hint}', alert_duplicate_name: 'Custom item display name "{name}" already exists. Please use a different name.', alert_update_failed_invalid_index: 'Update failed: Invalid item index.', alert_edit_failed_missing_fields: 'Cannot edit: Input or button fields not found.',
  343. alert_no_more_predefined_to_add: 'No more predefined {type} options available to add.',
  344. alert_no_keywords_for_shortcut: 'No keywords found in current search to use for {service_name}.',
  345. alert_error_opening_link: 'Error opening link for {service_name}.',
  346. alert_generic_error: 'An unexpected error occurred. Please check the console or try again. Context: {context}',
  347. confirm_delete_item: 'Are you sure you want to delete the custom item "{name}"?', confirm_remove_item_from_list: 'Are you sure you want to remove "{name}" from this display list?', confirm_reset_settings: 'Are you sure you want to reset all settings to their default values?', alert_settings_reset_success: 'Settings have been reset to default. You can continue editing or click "Save Settings" to confirm.', confirm_reset_all_menu: 'Are you sure you want to reset all settings to their default values?\nThis cannot be undone and requires a page refresh to take effect.', alert_reset_all_menu_success: 'All settings have been reset to defaults.\nPlease refresh the page to apply the changes.', alert_reset_all_menu_fail: 'Failed to reset settings via menu command! Please check the console.', alert_init_fail: '{scriptName} initialization failed. Some features may not work. Please check the console for technical details.\nTechnical Error: {error}', menu_open_settings: '⚙️ Open Settings', menu_reset_all_settings: '🚨 Reset All Settings',
  348. },
  349. };
  350. let effectiveTranslations = JSON.parse(JSON.stringify(builtInTranslations));
  351. let _currentLocale = 'en';
  352.  
  353. function _mergeExternalTranslations() {
  354. if (typeof window.GSCS_Namespace !== 'undefined' && typeof window.GSCS_Namespace.i18nPack === 'object' && typeof window.GSCS_Namespace.i18nPack.translations === 'object') {
  355. const externalTranslations = window.GSCS_Namespace.i18nPack.translations;
  356. for (const langCode in externalTranslations) {
  357. if (Object.prototype.hasOwnProperty.call(externalTranslations, langCode)) {
  358. if (!effectiveTranslations[langCode]) {
  359. effectiveTranslations[langCode] = {};
  360. }
  361. for (const key in externalTranslations[langCode]) {
  362. if (Object.prototype.hasOwnProperty.call(externalTranslations[langCode], key)) {
  363. effectiveTranslations[langCode][key] = externalTranslations[langCode][key];
  364. }
  365. }
  366. }
  367. }
  368. // After merging, ensure 'en' from builtInTranslations acts as a fallback for all known languages
  369. const englishDefaults = builtInTranslations.en;
  370. for (const langCode in effectiveTranslations) {
  371. if (langCode !== 'en' && Object.prototype.hasOwnProperty.call(effectiveTranslations, langCode)) {
  372. for (const key in englishDefaults) {
  373. if (Object.prototype.hasOwnProperty.call(englishDefaults, key) && typeof effectiveTranslations[langCode][key] === 'undefined') {
  374. effectiveTranslations[langCode][key] = englishDefaults[key];
  375. }
  376. }
  377. }
  378. }
  379.  
  380. } else {
  381. console.warn(`${LOG_PREFIX} [i18n] External i18n pack (window.GSCS_Namespace.i18nPack) not found or invalid. Using built-in translations only.`);
  382. }
  383. // Ensure all keys from builtInTranslations.en exist in 'en' to prevent errors
  384. // if i18n.js is older or missing keys.
  385. const ensureKeys = (lang, defaults) => {
  386. if (!effectiveTranslations[lang]) effectiveTranslations[lang] = {};
  387. for (const key in defaults) {
  388. if (!effectiveTranslations[lang][key]) {
  389. effectiveTranslations[lang][key] = defaults[key]; // Fallback to built-in English if key is missing in target lang
  390. }
  391. }
  392. };
  393. ensureKeys('en', builtInTranslations.en); // Ensure English is complete based on built-in
  394. }
  395.  
  396. function _detectBrowserLocale() {
  397. let locale = 'en'; // Default
  398. try {
  399. if (navigator.languages && navigator.languages.length) {
  400. locale = navigator.languages[0];
  401. } else if (navigator.language) {
  402. locale = navigator.language;
  403. }
  404. } catch (e) {
  405. console.warn(`${LOG_PREFIX} [i18n] Error accessing navigator.language(s):`, e);
  406. }
  407.  
  408. // Try to match full locale (e.g., "zh-TW")
  409. if (effectiveTranslations[locale]) return locale;
  410.  
  411. // Try to match generic part (e.g., "zh" from "zh-TW")
  412. if (locale.includes('-')) {
  413. const parts = locale.split('-');
  414. if (parts.length > 0 && effectiveTranslations[parts[0]]) return parts[0];
  415. // Try "language-Script" (e.g., "zh-Hant") if applicable, though less common for userscripts
  416. if (parts.length > 2 && effectiveTranslations[`${parts[0]}-${parts[1]}`]) return `${parts[0]}-${parts[1]}`;
  417. }
  418. return 'en'; // Fallback to English
  419. }
  420.  
  421. function _updateActiveLocale(settingsToUse) {
  422. let newLocale = 'en'; // Default
  423. const langSettingSource = (settingsToUse && Object.keys(settingsToUse).length > 0 && typeof settingsToUse.interfaceLanguage === 'string')
  424. ? settingsToUse
  425. : defaultSettings; // Fallback to defaultSettings if settingsToUse is empty/invalid
  426.  
  427. const userSelectedLang = langSettingSource.interfaceLanguage;
  428.  
  429. if (userSelectedLang && userSelectedLang !== 'auto') {
  430. if (effectiveTranslations[userSelectedLang]) {
  431. newLocale = userSelectedLang;
  432. } else if (userSelectedLang.includes('-')) {
  433. const genericLang = userSelectedLang.split('-')[0];
  434. if (effectiveTranslations[genericLang]) {
  435. newLocale = genericLang;
  436. } else {
  437. newLocale = _detectBrowserLocale(); // Fallback to browser if specific parts aren't found
  438. }
  439. } else {
  440. newLocale = _detectBrowserLocale(); // Fallback if selected lang doesn't exist
  441. }
  442. } else { // 'auto' or undefined
  443. newLocale = _detectBrowserLocale();
  444. }
  445.  
  446. if (_currentLocale !== newLocale) {
  447. _currentLocale = newLocale;
  448. // console.log(`${LOG_PREFIX} [i18n] Locale updated to: ${_currentLocale}`);
  449. }
  450. // Warn if the chosen language isn't exactly what was set (e.g. "fr-CA" setting becomes "fr" due to availability)
  451. if (userSelectedLang && userSelectedLang !== 'auto' && _currentLocale !== userSelectedLang && !userSelectedLang.startsWith(_currentLocale.split('-')[0])) {
  452. console.warn(`${LOG_PREFIX} [i18n] User selected language "${userSelectedLang}" was not fully available or matched. Using best match: "${_currentLocale}".`);
  453. }
  454. }
  455.  
  456. _mergeExternalTranslations(); // Merge external translations once at service creation
  457.  
  458. function getString(key, replacements = {}) {
  459. let str = `[ERR: ${key} @ ${_currentLocale}]`; // Default error string
  460. let found = false;
  461.  
  462. // 1. Try current locale
  463. if (effectiveTranslations[_currentLocale] && typeof effectiveTranslations[_currentLocale][key] !== 'undefined') {
  464. str = effectiveTranslations[_currentLocale][key];
  465. found = true;
  466. }
  467. // 2. If current locale has a generic part (e.g., "zh" from "zh-TW"), try that
  468. else if (_currentLocale.includes('-')) {
  469. const genericLang = _currentLocale.split('-')[0];
  470. if (effectiveTranslations[genericLang] && typeof effectiveTranslations[genericLang][key] !== 'undefined') {
  471. str = effectiveTranslations[genericLang][key];
  472. found = true;
  473. }
  474. }
  475.  
  476. // 3. If not found and current locale is not English, fallback to English
  477. if (!found && _currentLocale !== 'en') {
  478. if (effectiveTranslations['en'] && typeof effectiveTranslations['en'][key] !== 'undefined') {
  479. str = effectiveTranslations['en'][key];
  480. found = true;
  481. // console.warn(`${LOG_PREFIX} [i18n] Missing translation for key: "${key}" in locale: "${_currentLocale}". Fell back to "en".`);
  482. }
  483. }
  484.  
  485. // 4. If still not found (even in English), it's a critical miss
  486. if (!found) {
  487. if (!(effectiveTranslations['en'] && typeof effectiveTranslations['en'][key] !== 'undefined')) {
  488. console.error(`${LOG_PREFIX} [i18n] CRITICAL: Missing translation for key: "${key}" in BOTH locale: "${_currentLocale}" AND default locale "en".`);
  489. } else {
  490. // This case should ideally not be hit if English is complete in builtInTranslations
  491. str = effectiveTranslations['en'][key]; // Should have been caught by step 3 if _currentLocale wasn't 'en'
  492. found = true;
  493. }
  494. if(!found) str = `[ERR_NF: ${key}]`; // Final error if truly not found anywhere
  495. }
  496.  
  497. // Replace placeholders
  498. if (typeof str === 'string') {
  499. for (const placeholder in replacements) {
  500. if (Object.prototype.hasOwnProperty.call(replacements, placeholder)) {
  501. str = str.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), replacements[placeholder]);
  502. }
  503. }
  504. } else {
  505. console.error(`${LOG_PREFIX} [i18n] CRITICAL: Translation for key "${key}" is not a string:`, str);
  506. return `[INVALID_TYPE_FOR_KEY: ${key}]`;
  507. }
  508. return str;
  509. }
  510.  
  511. return {
  512. getString: getString,
  513. getCurrentLocale: function() { return _currentLocale; },
  514. getTranslationsForLocale: function(locale = 'en') { return effectiveTranslations[locale] || effectiveTranslations['en']; },
  515. initializeBaseLocale: function() { _updateActiveLocale(defaultSettings); }, // Initialize with defaults
  516. updateActiveLocale: function(activeSettings) { _updateActiveLocale(activeSettings); },
  517. getAvailableLocales: function() {
  518. const locales = new Set(['auto', 'en']); // 'auto' and 'en' are always options
  519. Object.keys(effectiveTranslations).forEach(lang => {
  520. // Only add if it's a valid language pack (not just an empty object)
  521. if (Object.keys(effectiveTranslations[lang]).length > 0) {
  522. locales.add(lang);
  523. }
  524. });
  525. return Array.from(locales).sort((a, b) => {
  526. if (a === 'auto') return -1;
  527. if (b === 'auto') return 1;
  528. if (a === 'en' && b !== 'auto') return -1;
  529. if (b === 'en' && a !== 'auto') return 1;
  530.  
  531. let nameA = a, nameB = b;
  532. try { nameA = new Intl.DisplayNames([a],{type:'language'}).of(a); } catch(e){}
  533. try { nameB = new Intl.DisplayNames([b],{type:'language'}).of(b); } catch(e){}
  534. return nameA.localeCompare(nameB);
  535. });
  536. }
  537. };
  538. })();
  539. const _ = LocalizationService.getString; // Shortcut
  540.  
  541. const Utils = {
  542. debounce: function(func, wait) {
  543. let timeout;
  544. return function executedFunction(...args) {
  545. const context = this;
  546. const later = () => {
  547. timeout = null;
  548. func.apply(context, args);
  549. };
  550. clearTimeout(timeout);
  551. timeout = setTimeout(later, wait);
  552. };
  553. },
  554. mergeDeep: function(target, source) {
  555. if (!source) return target; // If source is undefined or null, return target as is.
  556. target = target || {}; // Ensure target is an object if it's initially null/undefined.
  557.  
  558. for (const key in source) {
  559. if (Object.prototype.hasOwnProperty.call(source, key)) {
  560. const targetValue = target[key];
  561. const sourceValue = source[key];
  562.  
  563. if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
  564. // Recurse for nested objects
  565. target[key] = Utils.mergeDeep(targetValue, sourceValue);
  566. } else if (typeof sourceValue !== 'undefined') {
  567. // Assign if sourceValue is a primitive, array, or explicitly undefined
  568. target[key] = sourceValue;
  569. }
  570. // If sourceValue is undefined, target[key] remains unchanged (implicit else)
  571. }
  572. }
  573. return target;
  574. },
  575. clamp: function(num, min, max) {
  576. return Math.min(Math.max(num, min), max);
  577. },
  578. parseIconAndText: function(fullText) {
  579. // Regex to match one or more non-letter, non-number characters at the beginning, followed by optional whitespace
  580. // \P{L} matches any character that is not a letter.
  581. // \P{N} matches any character that is not a number.
  582. // Using [^\p{L}\p{N}\s] might be too restrictive if icons can be complex symbols.
  583. // This regex tries to capture common emoji/symbol patterns for icons.
  584. const match = fullText.match(/^(\P{L}\P{N}\s*)+/u);
  585. let icon = '';
  586. let text = fullText;
  587.  
  588. if (match && match[0].trim() !== '') {
  589. icon = match[0].trim(); // Trim to remove trailing spaces from the icon part
  590. text = fullText.substring(icon.length).trim(); // Trim to remove leading spaces from the text part
  591. }
  592. return { icon, text };
  593. },
  594. getCurrentURL: function() {
  595. try {
  596. return new URL(window.location.href);
  597. } catch (e) {
  598. console.error(`${LOG_PREFIX} Error creating URL object:`, e);
  599. return null;
  600. }
  601. },
  602. parseCombinedValue: function(valueString) {
  603. if (typeof valueString !== 'string' || !valueString.trim()) {
  604. return [];
  605. }
  606. // Split by " OR " (case-insensitive, with spaces around OR)
  607. return valueString.split(/\s+OR\s+/i).map(s => s.trim()).filter(s => s.length > 0);
  608. }
  609. };
  610. const NotificationManager = (function() {
  611. let container = null;
  612.  
  613. function init() {
  614. if (document.getElementById(IDS.NOTIFICATION_CONTAINER)) {
  615. container = document.getElementById(IDS.NOTIFICATION_CONTAINER);
  616. return;
  617. }
  618. container = document.createElement('div');
  619. container.id = IDS.NOTIFICATION_CONTAINER;
  620. if (document.body) {
  621. document.body.appendChild(container);
  622. } else {
  623. // This case should be rare as script runs at document-idle
  624. console.error(LOG_PREFIX + " NotificationManager.init(): document.body is not available!");
  625. container = null; // Ensure container is null if append fails
  626. }
  627. }
  628.  
  629. function show(messageKey, messageArgs = {}, type = 'info', duration = 3000) {
  630. if (!container) {
  631. // Fallback to alert if container isn't initialized
  632. const alertMsg = (typeof _ === 'function' && _(messageKey, messageArgs) && !(_(messageKey, messageArgs).startsWith('[ERR:')))
  633. ? _(messageKey, messageArgs)
  634. : `${messageKey} (args: ${JSON.stringify(messageArgs)})`; // Basic fallback if _ is not ready
  635. alert(alertMsg);
  636. return null;
  637. }
  638.  
  639. const notificationElement = document.createElement('div');
  640. notificationElement.classList.add(CSS.NOTIFICATION);
  641.  
  642. const typeClass = CSS[`NTF_${type.toUpperCase()}`] || CSS.NTF_INFO; // Fallback to info type
  643. notificationElement.classList.add(typeClass);
  644.  
  645. notificationElement.textContent = _(messageKey, messageArgs);
  646.  
  647. if (duration <= 0) { // Persistent notification, add a close button
  648. const closeButton = document.createElement('span');
  649. closeButton.innerHTML = '×'; // Simple 'x'
  650. closeButton.style.cursor = 'pointer';
  651. closeButton.style.marginLeft = '10px';
  652. closeButton.style.float = 'right'; // Position to the right
  653. closeButton.onclick = () => notificationElement.remove();
  654. notificationElement.appendChild(closeButton);
  655. }
  656.  
  657. container.appendChild(notificationElement);
  658.  
  659. if (duration > 0) {
  660. setTimeout(() => {
  661. notificationElement.style.opacity = '0'; // Start fade out
  662. setTimeout(() => notificationElement.remove(), 500); // Remove after fade out
  663. }, duration);
  664. }
  665. return notificationElement; // Return the element for potential further manipulation
  666. }
  667.  
  668. return { init: init, show: show };
  669. })();
  670.  
  671. // --- UI Element Creation and Management ---
  672. // (createGenericListItem, populateListInModal, getListMapping etc. are complex and related to ModalManager now)
  673. // Simplified here as they are mostly within ModalManager's scope or specific to section building
  674.  
  675. function createGenericListItem(index, item, listId, mapping) {
  676. // ... (implementation as provided, assuming it's largely correct for modal context)
  677. const listItem = document.createElement('li');
  678. listItem.dataset[DATA_ATTR.INDEX] = index;
  679. listItem.dataset[DATA_ATTR.LIST_ID] = listId;
  680. listItem.dataset[DATA_ATTR.ITEM_ID] = item.id || item.value || item.url; // Unique ID for the item itself
  681. listItem.draggable = true; // All modal list items are draggable
  682.  
  683. const dragIconSpan = document.createElement('span');
  684. dragIconSpan.classList.add(CSS.DRAG_ICON);
  685. dragIconSpan.innerHTML = SVG_ICONS.dragGrip;
  686. listItem.appendChild(dragIconSpan);
  687. const textSpan = document.createElement('span');
  688.  
  689. // Favicon logic for Site Search list in the modal
  690. const currentSettings = SettingsManager.getCurrentSettings();
  691. if (listId === IDS.SITES_LIST && currentSettings.showFaviconsForSiteSearch && item.url && !item.url.includes(' OR ')) {
  692. const favicon = document.createElement('img');
  693. favicon.src = `https://www.google.com/s2/favicons?sz=32&domain_url=${item.url}`;
  694. favicon.classList.add(CSS.FAVICON);
  695. favicon.loading = 'lazy';
  696. textSpan.prepend(favicon);
  697. }
  698.  
  699. let displayText = item.text;
  700. let paramName = ''; // To show "param=value"
  701.  
  702. if (item.type === 'predefined' && item.originalKey) {
  703. displayText = _(item.originalKey);
  704. if (listId === IDS.COUNTRIES_LIST) { // Special handling for country icon+text
  705. const parsed = Utils.parseIconAndText(displayText);
  706. displayText = `${parsed.icon} ${parsed.text}`.trim();
  707. }
  708. }
  709.  
  710. // Determine param name for display
  711. if (mapping) { // Mapping comes from getListMapping
  712. if (listId === IDS.LANG_LIST) paramName = ALL_SECTION_DEFINITIONS.find(s=>s.id === 'sidebar-section-language').param;
  713. else if (listId === IDS.COUNTRIES_LIST) paramName = ALL_SECTION_DEFINITIONS.find(s=>s.id === 'sidebar-section-country').param;
  714. else if (listId === IDS.SITES_LIST) paramName = 'site'; // Site search uses `site:` in query
  715. else if (listId === IDS.TIME_LIST) paramName = ALL_SECTION_DEFINITIONS.find(s=>s.id === 'sidebar-section-time').param;
  716. else if (listId === IDS.FT_LIST) {
  717. const ftSection = ALL_SECTION_DEFINITIONS.find(s => s.id === 'sidebar-section-filetype');
  718. if (ftSection) paramName = ftSection.param;
  719. }
  720. }
  721.  
  722. const valueForDisplay = item.value || item.url || _('value_empty');
  723. const fullTextContent = `${displayText} (${paramName}=${valueForDisplay})`;
  724. textSpan.appendChild(document.createTextNode(fullTextContent));
  725. textSpan.title = fullTextContent;
  726. listItem.appendChild(textSpan);
  727.  
  728. const controlsSpan = document.createElement('span');
  729. controlsSpan.classList.add(CSS.ITEM_CONTROLS);
  730.  
  731. // Determine if item is "custom" or "predefined" for button display
  732. if (item.type === 'custom' || listId === IDS.SITES_LIST || listId === IDS.TIME_LIST || listId === IDS.FT_LIST) {
  733. // Sites, Time, Filetype lists are always treated as "custom" in terms of editability
  734. controlsSpan.innerHTML =
  735. `<button class="${CSS.EDIT_CUSTOM_ITEM}" title="${_('modal_button_edit_title')}">${SVG_ICONS.edit}</button> ` +
  736. `<button class="${CSS.DELETE_CUSTOM_ITEM}" title="${_('modal_button_delete_title')}">${SVG_ICONS.delete}</button>`;
  737. listItem.dataset[DATA_ATTR.ITEM_TYPE] = 'custom';
  738. } else if (item.type === 'predefined') {
  739. // Languages, Countries in mixed mode can have predefined items that can be removed (not deleted from source)
  740. controlsSpan.innerHTML =
  741. `<button class="${CSS.REMOVE_FROM_LIST_BTN}" title="${_('modal_button_remove_from_list_title')}">${SVG_ICONS.removeFromList}</button>`;
  742. listItem.dataset[DATA_ATTR.ITEM_TYPE] = 'predefined';
  743. }
  744. listItem.appendChild(controlsSpan);
  745. return listItem;
  746. }
  747.  
  748. function populateListInModal(listId, items, contextElement = document) {
  749. const listElement = contextElement.querySelector(`#${listId}`);
  750. if (!listElement) {
  751. console.warn(`${LOG_PREFIX} List element not found: #${listId} in context`, contextElement);
  752. return;
  753. }
  754. listElement.innerHTML = ''; // Clear existing items
  755. const fragment = document.createDocumentFragment();
  756. const mapping = getListMapping(listId); // Get mapping for param name display
  757.  
  758. if (!Array.isArray(items)) items = []; // Ensure items is an array
  759.  
  760. items.forEach((item, index) => {
  761. fragment.appendChild(createGenericListItem(index, item, listId, mapping));
  762. });
  763. listElement.appendChild(fragment);
  764. }
  765.  
  766. function getListMapping(listId) {
  767. // Centralized configuration for custom lists in modals
  768. const listMappings = {
  769. [IDS.SITES_LIST]: { itemsArrayKey: 'favoriteSites', customItemsMasterKey: null, valueKey: 'url', populateFn: populateListInModal, textInput: `#${IDS.NEW_SITE_NAME}`, valueInput: `#${IDS.NEW_SITE_URL}`, addButton: `#${IDS.ADD_SITE_BTN}`, nameKey: 'section_site_search', isSortableMixed: false, predefinedSourceKey: null },
  770. [IDS.LANG_LIST]: { itemsArrayKey: 'displayLanguages', customItemsMasterKey: 'customLanguages', valueKey: 'value', populateFn: populateListInModal, textInput: `#${IDS.NEW_LANG_TEXT}`, valueInput: `#${IDS.NEW_LANG_VALUE}`, addButton: `#${IDS.ADD_LANG_BTN}`, nameKey: 'section_language', isSortableMixed: true, predefinedSourceKey: 'language' },
  771. [IDS.COUNTRIES_LIST]: { itemsArrayKey: 'displayCountries', customItemsMasterKey: 'customCountries', valueKey: 'value', populateFn: populateListInModal, textInput: `#${IDS.NEW_COUNTRY_TEXT}`, valueInput: `#${IDS.NEW_COUNTRY_VALUE}`, addButton: `#${IDS.ADD_COUNTRY_BTN}`, nameKey: 'section_country', isSortableMixed: true, predefinedSourceKey: 'country' },
  772. [IDS.TIME_LIST]: { itemsArrayKey: 'customTimeRanges', customItemsMasterKey: null, valueKey: 'value', populateFn: populateListInModal, textInput: `#${IDS.NEW_TIME_TEXT}`, valueInput: `#${IDS.NEW_TIME_VALUE}`, addButton: `#${IDS.ADD_TIME_BTN}`, nameKey: 'section_time', isSortableMixed: false, predefinedSourceKey: 'time' }, // predefinedSourceKey for enabling checkbox list
  773. [IDS.FT_LIST]: { itemsArrayKey: 'customFiletypes', customItemsMasterKey: null, valueKey: 'value', populateFn: populateListInModal, textInput: `#${IDS.NEW_FT_TEXT}`, valueInput: `#${IDS.NEW_FT_VALUE}`, addButton: `#${IDS.ADD_FT_BTN}`, nameKey: 'section_filetype', isSortableMixed: false, predefinedSourceKey: 'filetype' },// predefinedSourceKey for enabling checkbox list
  774. };
  775. return listMappings[listId] || null;
  776. }
  777.  
  778. function validateCustomInput(inputElement) {
  779. if (!inputElement) return false; // Should not happen if called correctly
  780. const value = inputElement.value.trim();
  781. const id = inputElement.id;
  782. let isValid = false;
  783. let isEmpty = value === '';
  784.  
  785. // Basic validation: name/text fields cannot be empty
  786. if (id === IDS.NEW_SITE_NAME || id === IDS.NEW_LANG_TEXT || id === IDS.NEW_TIME_TEXT || id === IDS.NEW_FT_TEXT || id === IDS.NEW_COUNTRY_TEXT) {
  787. isValid = !isEmpty;
  788. } else if (id === IDS.NEW_SITE_URL) {
  789. // Allow domains with paths, or TLD/SLD
  790. // Updated regex to support paths:
  791. // - Domain part: (?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}
  792. // - Optional path part: (/[a-zA-Z0-9_.\-~%!$&'()*+,;=:@/']*)? (Note: removed ' from allowed chars in path based on common practice)
  793. // - TLD/SLD: (?:^\.(?:[a-zA-Z0-9-]{1,63}\.)*[a-zA-Z]{2,63}$)
  794. const singleSiteRegex = /^(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,})(?:\/[a-zA-Z0-9_.\-~%!$&()*+,;=:@/]+)*\/?)$|(?:^\.(?:[a-zA-Z0-9-]{1,63}\.)*[a-zA-Z]{2,63}$)/;
  795. const parts = Utils.parseCombinedValue(value); // Handles " OR " separation
  796. if (isEmpty) isValid = true;
  797. else if (parts.length > 0) isValid = parts.every(part => singleSiteRegex.test(part));
  798. else isValid = false;
  799. } else if (id === IDS.NEW_LANG_VALUE) {
  800. // Language code format: lang_xx or lang_xx-XX, multiple with |
  801. isValid = isEmpty || /^lang_[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,4})?(?:\|lang_[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,4})?)*$/.test(value);
  802. } else if (id === IDS.NEW_TIME_VALUE) {
  803. // Time value format: h, d, w, m, y, optionally followed by numbers
  804. isValid = isEmpty || /^[hdwmy]\d*$/.test(value);
  805. } else if (id === IDS.NEW_FT_VALUE) {
  806. // Filetype format: extension, multiple with " OR "
  807. const singleFiletypeRegex = /^[a-zA-Z0-9]+$/;
  808. const parts = Utils.parseCombinedValue(value);
  809. if (isEmpty) isValid = true;
  810. else if (parts.length > 0) isValid = parts.every(part => singleFiletypeRegex.test(part));
  811. else isValid = false;
  812. } else if (id === IDS.NEW_COUNTRY_VALUE) {
  813. // Country code format: countryXX (XX = uppercase country code)
  814. isValid = isEmpty || /^country[A-Z]{2}$/.test(value);
  815. }
  816.  
  817. // Visual feedback
  818. inputElement.classList.remove('input-valid', 'input-invalid', CSS.INPUT_HAS_ERROR); // Clear previous states
  819. _clearInputError(inputElement); // Clear any existing error message for this input
  820.  
  821. if (!isEmpty) { // Only add validation classes if not empty
  822. inputElement.classList.add(isValid ? 'input-valid' : 'input-invalid');
  823. if (!isValid) inputElement.classList.add(CSS.INPUT_HAS_ERROR); // Red border for error
  824. }
  825.  
  826. return isValid || isEmpty; // Return true if format is valid OR if it's empty (emptiness check is separate)
  827. }
  828.  
  829. function _getInputErrorElement(inputElement) {
  830. if (!inputElement || !inputElement.id) return null;
  831. // Try to find the specific error message span for this input
  832. let errorEl = inputElement.nextElementSibling;
  833. if (errorEl && errorEl.classList.contains(CSS.INPUT_ERROR_MESSAGE) && errorEl.id === `${inputElement.id}-error-msg`) {
  834. return errorEl;
  835. }
  836. // Fallback: search within parent div if structured that way
  837. const parentDiv = inputElement.parentElement;
  838. if (parentDiv) {
  839. return parentDiv.querySelector(`#${inputElement.id}-error-msg`);
  840. }
  841. return null;
  842. }
  843. function _showInputError(inputElement, messageKey, messageArgs = {}) {
  844. if (!inputElement) return;
  845. const errorElement = _getInputErrorElement(inputElement);
  846. if (errorElement) {
  847. errorElement.textContent = _(messageKey, messageArgs);
  848. errorElement.classList.add(CSS.ERROR_VISIBLE);
  849. }
  850. inputElement.classList.add(CSS.INPUT_HAS_ERROR);
  851. inputElement.classList.remove('input-valid'); // Remove valid class if error
  852. }
  853. function _clearInputError(inputElement) {
  854. if (!inputElement) return;
  855. const errorElement = _getInputErrorElement(inputElement);
  856. if (errorElement) {
  857. errorElement.textContent = '';
  858. errorElement.classList.remove(CSS.ERROR_VISIBLE);
  859. }
  860. inputElement.classList.remove(CSS.INPUT_HAS_ERROR, 'input-invalid');
  861. }
  862. function _clearAllInputErrorsInGroup(inputGroupElement) {
  863. if (!inputGroupElement) return;
  864. inputGroupElement.querySelectorAll(`input[type="text"]`).forEach(input => {
  865. _clearInputError(input);
  866. input.classList.remove('input-valid', 'input-invalid'); // Also clear validation classes
  867. });
  868. }
  869. function _showGlobalMessage(messageKey, messageArgs = {}, type = 'info', duration = 3000, targetElementId = IDS.SETTINGS_MESSAGE_BAR) {
  870. const messageBar = document.getElementById(targetElementId);
  871. if (!messageBar) {
  872. // If specific target (like modal message bar) not found, try general notification or alert
  873. if (targetElementId !== IDS.SETTINGS_MESSAGE_BAR && NotificationManager && typeof NotificationManager.show === 'function') {
  874. NotificationManager.show(messageKey, messageArgs, type, duration > 0 ? duration : 5000); // Longer for notifications if persistent
  875. } else {
  876. const alertMsg = (typeof _ === 'function' && _(messageKey, messageArgs) && !(_(messageKey, messageArgs).startsWith('[ERR:')))
  877. ? _(messageKey, messageArgs)
  878. : `${messageKey} (args: ${JSON.stringify(messageArgs)})`;
  879. alert(alertMsg);
  880. }
  881. return;
  882. }
  883.  
  884. if (globalMessageTimeout && targetElementId === IDS.SETTINGS_MESSAGE_BAR) { // Clear previous timeout for main settings bar
  885. clearTimeout(globalMessageTimeout);
  886. globalMessageTimeout = null;
  887. }
  888.  
  889. messageBar.textContent = _(messageKey, messageArgs);
  890. messageBar.className = `${CSS.MESSAGE_BAR}`; // Reset classes
  891. messageBar.classList.add(CSS[`MSG_${type.toUpperCase()}`] || CSS.MSG_INFO); // Add type-specific class
  892. messageBar.style.display = 'block';
  893.  
  894. if (duration > 0 && targetElementId === IDS.SETTINGS_MESSAGE_BAR) {
  895. globalMessageTimeout = setTimeout(() => {
  896. messageBar.style.display = 'none';
  897. messageBar.textContent = '';
  898. messageBar.className = CSS.MESSAGE_BAR; // Reset classes
  899. }, duration);
  900. }
  901. }
  902. // Validates and prepares custom item data from input fields
  903. function _validateAndPrepareCustomItemData(textInput, valueInput, itemTypeName, listId) {
  904. if (!textInput || !valueInput) {
  905. _showGlobalMessage('alert_edit_failed_missing_fields', {}, 'error', 0); // Persistent error
  906. return { isValid: false };
  907. }
  908. _clearInputError(textInput);
  909. _clearInputError(valueInput);
  910.  
  911. const text = textInput.value.trim();
  912. const value = valueInput.value.trim();
  913. let hint = '';
  914.  
  915. if (text === '') {
  916. _showInputError(textInput, 'alert_enter_display_name', { type: itemTypeName });
  917. textInput.focus();
  918. return { isValid: false, errorField: textInput };
  919. } else {
  920. // If text is not empty, ensure no lingering error style
  921. textInput.classList.remove(CSS.INPUT_HAS_ERROR);
  922. // validateCustomInput(textInput); // No need to re-validate text if non-empty for basic types
  923. }
  924.  
  925.  
  926. if (value === '') {
  927. _showInputError(valueInput, 'alert_enter_value', { type: itemTypeName });
  928. valueInput.focus();
  929. return { isValid: false, errorField: valueInput };
  930. } else {
  931. const isValueFormatValid = validateCustomInput(valueInput); // This also handles visual feedback
  932. if (!isValueFormatValid) { // validateCustomInput already showed error if !isValid and not empty
  933. if (valueInput.classList.contains('input-invalid')) { // Error was shown by validateCustomInput
  934. valueInput.focus();
  935. return { isValid: false, errorField: valueInput };
  936. }
  937. // If not marked invalid by validateCustomInput but still failed (e.g. more complex logic not in regex)
  938. if (listId === IDS.COUNTRIES_LIST) hint = _('modal_tooltip_country');
  939. else if (listId === IDS.LANG_LIST) hint = _('modal_tooltip_language');
  940. else if (listId === IDS.TIME_LIST) hint = _('modal_tooltip_time');
  941. else if (listId === IDS.FT_LIST) hint = _('modal_tooltip_filetype');
  942. else if (listId === IDS.SITES_LIST) hint = _('modal_tooltip_domain');
  943.  
  944. _showInputError(valueInput, 'alert_invalid_value_format', { type: itemTypeName, hint: hint });
  945. valueInput.focus();
  946. return { isValid: false, errorField: valueInput };
  947. }
  948. }
  949. return { isValid: true, text: text, value: value };
  950. }
  951. function _isDuplicateCustomItem(text, itemsToCheck, listId, editingIndex, editingItemInfoRef) {
  952. const lowerText = text.toLowerCase();
  953. return itemsToCheck.some((item, idx) => {
  954. // Only check custom-type items for name duplication, or all items in SITES/TIME/FT lists
  955. const itemIsCustom = item.type === 'custom' ||
  956. listId === IDS.SITES_LIST ||
  957. listId === IDS.TIME_LIST ||
  958. listId === IDS.FT_LIST;
  959. if (!itemIsCustom) return false; // Don't check predefined display names for duplication
  960.  
  961. // If editing, allow saving if the name hasn't changed from its original
  962. if (editingItemInfoRef && editingItemInfoRef.listId === listId && editingIndex === idx) {
  963. if (editingItemInfoRef.originalText?.toLowerCase() === lowerText) {
  964. return false; // Not a duplicate if it's the same item and name hasn't changed
  965. }
  966. }
  967. return item.text.toLowerCase() === lowerText;
  968. });
  969. }
  970. function applyThemeToElement(element, themeSetting) {
  971. if (!element) return;
  972. // Remove all potential theme classes first
  973. element.classList.remove(
  974. CSS.LIGHT_THEME, CSS.DARK_THEME,
  975. 'minimal-theme', 'minimal-light', 'minimal-dark' // From styles.js
  976. );
  977.  
  978. let effectiveTheme = themeSetting;
  979. // Settings window/modals should not use minimal theme directly, but fall back to light/dark
  980. const isSettingsOrModal = element.id === IDS.SETTINGS_WINDOW ||
  981. element.id === IDS.SETTINGS_OVERLAY ||
  982. element.classList.contains('settings-modal-content') || // Modal content itself
  983. element.classList.contains('settings-modal-overlay'); // Modal overlay
  984.  
  985. if (isSettingsOrModal) {
  986. if (themeSetting === 'minimal-light') effectiveTheme = 'light';
  987. else if (themeSetting === 'minimal-dark') effectiveTheme = 'dark';
  988. }
  989.  
  990. switch (effectiveTheme) {
  991. case 'dark':
  992. element.classList.add(CSS.DARK_THEME);
  993. break;
  994. case 'minimal-light':
  995. element.classList.add('minimal-theme', 'minimal-light');
  996. break;
  997. case 'minimal-dark':
  998. element.classList.add('minimal-theme', 'minimal-dark');
  999. break;
  1000. case 'system':
  1001. const systemIsDark = systemThemeMediaQuery && systemThemeMediaQuery.matches;
  1002. element.classList.add(systemIsDark ? CSS.DARK_THEME : CSS.LIGHT_THEME);
  1003. break;
  1004. case 'light':
  1005. default:
  1006. element.classList.add(CSS.LIGHT_THEME);
  1007. break;
  1008. }
  1009. }
  1010.  
  1011. const PredefinedOptionChooser = (function() {
  1012. let _chooserContainer = null;
  1013. let _currentListId = null;
  1014. let _currentPredefinedSourceKey = null;
  1015. let _currentDisplayItemsArrayRef = null; // Reference to the array like settings.displayLanguages
  1016. let _currentModalContentContext = null; // The modal body where this chooser is shown
  1017. let _onAddCallback = null;
  1018.  
  1019. function _buildChooserHTML(listId, predefinedSourceKey, displayItemsArrayRef) {
  1020. const allPredefinedSystemOptions = PREDEFINED_OPTIONS[predefinedSourceKey] || [];
  1021.  
  1022. // Get values of predefined items already in the display list
  1023. const currentDisplayedValues = new Set(
  1024. displayItemsArrayRef.filter(item => item.type === 'predefined').map(item => item.value)
  1025. );
  1026.  
  1027. const availablePredefinedToAdd = allPredefinedSystemOptions.filter(
  1028. opt => !currentDisplayedValues.has(opt.value)
  1029. );
  1030.  
  1031. if (availablePredefinedToAdd.length === 0) {
  1032. const itemTypeName = getListMapping(listId)?.nameKey ? _(getListMapping(listId).nameKey) : predefinedSourceKey;
  1033. _showGlobalMessage('alert_no_more_predefined_to_add', { type: itemTypeName }, 'info', 3000, IDS.SETTINGS_MESSAGE_BAR); // Show in main settings message bar
  1034. return null; // No HTML if nothing to add
  1035. }
  1036.  
  1037. let listHTML = `<ul id="${IDS.MODAL_PREDEFINED_CHOOSER_LIST}">`;
  1038. availablePredefinedToAdd.forEach(opt => {
  1039. let displayText = _(opt.textKey);
  1040. if (listId === IDS.COUNTRIES_LIST) { // Special handling for country display
  1041. const parsed = Utils.parseIconAndText(displayText);
  1042. displayText = `${parsed.icon} ${parsed.text}`.trim();
  1043. }
  1044. const sanitizedValueForId = opt.value.replace(/[^a-zA-Z0-9-_]/g, ''); // Make value safe for ID
  1045. listHTML += `<li class="${CSS.MODAL_PREDEFINED_CHOOSER_ITEM}"><input type="checkbox" value="${opt.value}" id="chooser-${sanitizedValueForId}"><label for="chooser-${sanitizedValueForId}">${displayText}</label></li>`;
  1046. });
  1047. listHTML += `</ul>`;
  1048.  
  1049. const buttonsHTML = `
  1050. <div class="chooser-buttons" style="text-align: right; margin-top: 10px;">
  1051. <button id="${IDS.MODAL_PREDEFINED_CHOOSER_ADD_BTN}" class="${CSS.TOOL_BUTTON}" style="margin-right: 5px;">${_('modal_button_add_title')}</button>
  1052. <button id="${IDS.MODAL_PREDEFINED_CHOOSER_CANCEL_BTN}" class="${CSS.TOOL_BUTTON}">${_('settings_cancel_button')}</button>
  1053. </div>`; // Use TOOL_BUTTON class for consistency
  1054.  
  1055. return listHTML + buttonsHTML;
  1056. }
  1057.  
  1058. function _handleAdd() {
  1059. if (!_chooserContainer) return;
  1060. const selectedValues = [];
  1061. _chooserContainer.querySelectorAll(`#${IDS.MODAL_PREDEFINED_CHOOSER_LIST} input[type="checkbox"]:checked`).forEach(cb => {
  1062. selectedValues.push(cb.value);
  1063. });
  1064.  
  1065. if (selectedValues.length > 0 && typeof _onAddCallback === 'function') {
  1066. _onAddCallback(selectedValues, _currentPredefinedSourceKey, _currentDisplayItemsArrayRef, _currentListId, _currentModalContentContext);
  1067. }
  1068. hide(); // Close the chooser after adding
  1069. }
  1070.  
  1071. function show(manageType, listId, predefinedSourceKey, displayItemsArrayRef, contextElement, onAddCb) {
  1072. hide(); // Ensure any previous chooser is closed
  1073.  
  1074. _currentListId = listId;
  1075. _currentPredefinedSourceKey = predefinedSourceKey;
  1076. _currentDisplayItemsArrayRef = displayItemsArrayRef;
  1077. _currentModalContentContext = contextElement; // The modal's body
  1078. _onAddCallback = onAddCb;
  1079.  
  1080. const chooserHTML = _buildChooserHTML(listId, predefinedSourceKey, displayItemsArrayRef);
  1081. if (!chooserHTML) return; // Nothing to show
  1082.  
  1083. _chooserContainer = document.createElement('div');
  1084. _chooserContainer.id = IDS.MODAL_PREDEFINED_CHOOSER_CONTAINER;
  1085. _chooserContainer.classList.add(CSS.MODAL_PREDEFINED_CHOOSER_CLASS); // Apply specific styling
  1086. _chooserContainer.innerHTML = chooserHTML;
  1087.  
  1088. // Insert the chooser UI into the modal
  1089. // Typically after the "Add New Option..." button or before the main custom list
  1090. const addNewBtn = contextElement.querySelector(`#${IDS.MODAL_ADD_NEW_OPTION_BTN}`);
  1091. if (addNewBtn && addNewBtn.parentNode) {
  1092. addNewBtn.insertAdjacentElement('afterend', _chooserContainer);
  1093. } else {
  1094. // Fallback: insert before the main list if the "Add New" button isn't there (e.g., if it was hidden)
  1095. const mainListElement = contextElement.querySelector(`#${listId}`);
  1096. mainListElement?.insertAdjacentElement('beforebegin', _chooserContainer);
  1097. }
  1098. _chooserContainer.style.display = 'block'; // Make it visible
  1099.  
  1100. // Add event listeners for the chooser's buttons
  1101. _chooserContainer.querySelector(`#${IDS.MODAL_PREDEFINED_CHOOSER_ADD_BTN}`).addEventListener('click', _handleAdd);
  1102. _chooserContainer.querySelector(`#${IDS.MODAL_PREDEFINED_CHOOSER_CANCEL_BTN}`).addEventListener('click', hide);
  1103. }
  1104.  
  1105. function hide() {
  1106. if (_chooserContainer) {
  1107. _chooserContainer.remove();
  1108. _chooserContainer = null;
  1109. }
  1110. // Clear state
  1111. _currentListId = null;
  1112. _currentPredefinedSourceKey = null;
  1113. _currentDisplayItemsArrayRef = null;
  1114. _currentModalContentContext = null;
  1115. _onAddCallback = null;
  1116. }
  1117.  
  1118. return {
  1119. show: show,
  1120. hide: hide,
  1121. isOpen: function() { return !!_chooserContainer; }
  1122. };
  1123. })();
  1124.  
  1125.  
  1126. const ModalManager = (function() {
  1127. let _currentModal = null;
  1128. let _currentModalContent = null; // Reference to the modal's content div for event binding
  1129. let _editingItemInfo = null; // { listId, index, originalValue, originalText, addButton, cancelButton, arrayKey }
  1130. let _draggedListItem = null; // For drag-and-drop within modal lists
  1131.  
  1132. // Configuration for different types of custom options modals
  1133. const modalConfigsData = {
  1134. 'site': { modalTitleKey: 'manageSitesTitle', listId: IDS.SITES_LIST, itemsArrayKey: 'favoriteSites', customItemsMasterKey: null, textPKey: 'modal_placeholder_name', valPKey: 'modal_placeholder_domain', hintKey: 'modal_hint_domain', fmtKey: 'modal_tooltip_domain', isSortableMixed: false, predefinedSourceKey: null, hasPredefinedToggles: false, manageType: 'site' },
  1135. 'language': { modalTitleKey: 'manageLanguagesTitle', listId: IDS.LANG_LIST, itemsArrayKey: 'displayLanguages', customItemsMasterKey: 'customLanguages', textPKey: 'modal_placeholder_text', valPKey: 'modal_placeholder_value', hintKey: 'modal_hint_language', fmtKey: 'modal_tooltip_language', predefinedSourceKey: 'language', isSortableMixed: true, hasPredefinedToggles: false, manageType: 'language' }, // displayLanguages is sortable, can mix predefined/custom
  1136. 'country': { modalTitleKey: 'manageCountriesTitle', listId: IDS.COUNTRIES_LIST, itemsArrayKey: 'displayCountries', customItemsMasterKey: 'customCountries', textPKey: 'modal_placeholder_text', valPKey: 'modal_placeholder_value', hintKey: 'modal_hint_country', fmtKey: 'modal_tooltip_country', predefinedSourceKey: 'country', isSortableMixed: true, hasPredefinedToggles: false, manageType: 'country' }, // displayCountries is sortable
  1137. 'time': { modalTitleKey: 'manageTimeRangesTitle',listId: IDS.TIME_LIST, itemsArrayKey: 'customTimeRanges', customItemsMasterKey: null, textPKey: 'modal_placeholder_text', valPKey: 'modal_placeholder_value', hintKey: 'modal_hint_time', fmtKey: 'modal_tooltip_time', predefinedSourceKey: 'time', isSortableMixed: false, hasPredefinedToggles: true, manageType: 'time' }, // customTimeRanges only, but has predefined toggles
  1138. 'filetype': { modalTitleKey: 'manageFileTypesTitle', listId: IDS.FT_LIST, itemsArrayKey: 'customFiletypes', customItemsMasterKey: null, textPKey: 'modal_placeholder_text', valPKey: 'modal_placeholder_value', hintKey: 'modal_hint_filetype', fmtKey: 'modal_tooltip_filetype', predefinedSourceKey: 'filetype', isSortableMixed: false, hasPredefinedToggles: true, manageType: 'filetype' }, // customFiletypes only, but has predefined toggles
  1139. };
  1140.  
  1141.  
  1142. function _createPredefinedOptionsSectionHTML(currentOptionType, typeNameKey, predefinedOptionsSource, enabledPredefinedValues) {
  1143. const label = _(typeNameKey); // e.g., "Time", "File Type"
  1144. const optionsHTML = (predefinedOptionsSource[currentOptionType] || []).map(option => {
  1145. const checkboxId = `predefined-${currentOptionType}-${option.value.replace(/[^a-zA-Z0-9-_]/g, '')}`; // Sanitize value for ID
  1146. const translatedOptionText = _(option.textKey);
  1147. const isChecked = enabledPredefinedValues.has(option.value);
  1148. return `<li><input type="checkbox" id="${checkboxId}" value="${option.value}" data-option-type="${currentOptionType}" ${isChecked ? 'checked' : ''}><label for="${checkboxId}">${translatedOptionText}</label></li>`;
  1149. }).join('');
  1150.  
  1151. return `<label style="font-weight: bold;">${_('modal_label_enable_predefined', { type: label })}</label><ul class="predefined-options-list" data-option-type="${currentOptionType}">${optionsHTML}</ul>`;
  1152. }
  1153.  
  1154.  
  1155. function _createModalListAndInputHTML(currentListId, textPlaceholderKey, valuePlaceholderKey, hintKey, formatTooltipKey, itemTypeName, isSortableMixed = false) {
  1156. const mapping = getListMapping(currentListId); // To get input IDs
  1157. const typeNameToDisplay = itemTypeName || (mapping ? _(mapping.nameKey) : 'Items'); // Fallback if itemTypeName isn't directly passed
  1158.  
  1159. let headerHTML = '';
  1160. let addNewOptionButtonHTML = '';
  1161.  
  1162. if (isSortableMixed) {
  1163. // For Language/Country, where predefined and custom are mixed and sorted
  1164. headerHTML = `<label style="font-weight: bold; margin-top: 0.5em; display: block;">${_('modal_label_display_options_for', {type: typeNameToDisplay})}</label>`;
  1165. // Button to open the PredefinedOptionChooser or focus custom input
  1166. addNewOptionButtonHTML = `<div style="margin-bottom: 0.5em;"><button id="${IDS.MODAL_ADD_NEW_OPTION_BTN}" class="${CSS.MODAL_ADD_NEW_OPTION_BTN_CLASS} ${CSS.TOOL_BUTTON}">${_('modal_button_add_new_option')}</button></div>`;
  1167. } else {
  1168. // For Sites, Time, Filetype (custom items only in the sortable list part)
  1169. headerHTML = `<label style="font-weight: bold; margin-top: 0.5em; display: block;">${_('modal_label_my_custom', { type: typeNameToDisplay })}</label>`;
  1170. }
  1171.  
  1172. // Get input IDs from mapping
  1173. const textInputId = mapping ? mapping.textInput.substring(1) : `new-custom-${currentListId}-text`; // remove #
  1174. const valueInputId = mapping ? mapping.valueInput.substring(1) : `new-custom-${currentListId}-value`;
  1175. const addButtonId = mapping ? mapping.addButton.substring(1) : `add-custom-${currentListId}-button`;
  1176.  
  1177. const inputGroupHTML = `
  1178. <div class="${CSS.CUSTOM_LIST_INPUT_GROUP}">
  1179. <div><input type="text" id="${textInputId}" placeholder="${_(textPlaceholderKey)}"><span id="${textInputId}-error-msg" class="${CSS.INPUT_ERROR_MESSAGE}"></span></div>
  1180. <div><input type="text" id="${valueInputId}" placeholder="${_(valuePlaceholderKey)}" title="${_(formatTooltipKey)}"><span id="${valueInputId}-error-msg" class="${CSS.INPUT_ERROR_MESSAGE}"></span></div>
  1181. <button id="${addButtonId}" class="${CSS.ADD_CUSTOM_BUTTON} custom-list-action-button" data-list-id="${currentListId}" title="${_('modal_button_add_title')}">${SVG_ICONS.add}</button>
  1182. <button class="cancel-edit-button" style="display: none;" title="${_('modal_button_cancel_edit_title')}">${SVG_ICONS.close}</button>
  1183. </div>`;
  1184. const hintHTML = `<span class="setting-value-hint">${_(hintKey)}</span>`;
  1185.  
  1186. return `${addNewOptionButtonHTML}${headerHTML}<ul id="${currentListId}" class="${CSS.CUSTOM_LIST}"></ul>${inputGroupHTML}${hintHTML}`;
  1187. }
  1188.  
  1189.  
  1190. // Resets the input fields and button states when an edit is cancelled or completed
  1191. function _resetEditStateInternal(contextElement = _currentModalContent) {
  1192. if (_editingItemInfo) {
  1193. const mapping = getListMapping(_editingItemInfo.listId);
  1194. if (_editingItemInfo.addButton) {
  1195. _editingItemInfo.addButton.innerHTML = SVG_ICONS.add;
  1196. _editingItemInfo.addButton.title = _('modal_button_add_title');
  1197. _editingItemInfo.addButton.classList.remove('update-mode'); // Visual cue for update mode
  1198. }
  1199. if (_editingItemInfo.cancelButton) {
  1200. _editingItemInfo.cancelButton.style.display = 'none';
  1201. }
  1202.  
  1203. // Clear input fields if mapping and context are available
  1204. if (mapping && contextElement) {
  1205. const textInput = contextElement.querySelector(mapping.textInput);
  1206. const valueInput = contextElement.querySelector(mapping.valueInput);
  1207. const inputGroup = textInput?.closest(`.${CSS.CUSTOM_LIST_INPUT_GROUP}`);
  1208. if(inputGroup) _clearAllInputErrorsInGroup(inputGroup);
  1209.  
  1210. if (textInput) { textInput.value = ''; textInput.classList.remove('input-valid', 'input-invalid', CSS.INPUT_HAS_ERROR); _clearInputError(textInput); }
  1211. if (valueInput) { valueInput.value = ''; valueInput.classList.remove('input-valid', 'input-invalid', CSS.INPUT_HAS_ERROR); _clearInputError(valueInput); }
  1212. }
  1213. }
  1214. _editingItemInfo = null;
  1215. }
  1216.  
  1217. function _prepareEditItemActionInternal(item, index, listId, mapping, contextElement) {
  1218. const textInput = contextElement.querySelector(mapping.textInput);
  1219. const valueInput = contextElement.querySelector(mapping.valueInput);
  1220. const addButton = contextElement.querySelector(mapping.addButton); // Should be the "Add/Update" button
  1221. const cancelButton = addButton?.parentElement?.querySelector('.cancel-edit-button');
  1222.  
  1223. if (textInput && valueInput && addButton && cancelButton) {
  1224. // If already editing another item, reset that state first
  1225. if (_editingItemInfo && (_editingItemInfo.listId !== listId || _editingItemInfo.index !== index)) {
  1226. _resetEditStateInternal(contextElement);
  1227. }
  1228.  
  1229. textInput.value = item.text;
  1230. valueInput.value = item[mapping.valueKey] || item.value; // Use specific valueKey if defined (e.g., 'url' for sites)
  1231.  
  1232. _editingItemInfo = {
  1233. listId,
  1234. index,
  1235. originalValue: item[mapping.valueKey] || item.value, // Store original for comparison on save
  1236. originalText: item.text,
  1237. addButton,
  1238. cancelButton,
  1239. arrayKey: mapping.itemsArrayKey || mapping.displayArrayKey // Which array in settings this list maps to
  1240. };
  1241.  
  1242. addButton.innerHTML = SVG_ICONS.update; // Change icon to "update"
  1243. addButton.title = _('modal_button_update_title');
  1244. addButton.classList.add('update-mode');
  1245. cancelButton.style.display = 'inline-block'; // Show cancel button
  1246.  
  1247. // Validate initially loaded values (might be invalid if manually edited in storage)
  1248. validateCustomInput(valueInput);
  1249. validateCustomInput(textInput); // Although text is usually just non-empty
  1250. textInput.focus();
  1251. } else {
  1252. // This indicates a problem with the modal's HTML structure or selectors
  1253. const errorSourceInput = textInput || valueInput || addButton?.closest(`.${CSS.CUSTOM_LIST_INPUT_GROUP}`)?.querySelector('input[type="text"]');
  1254. if (errorSourceInput) _showInputError(errorSourceInput, 'alert_edit_failed_missing_fields');
  1255. else _showGlobalMessage('alert_edit_failed_missing_fields', {}, 'error', 0, _currentModalContent?.querySelector(`#${IDS.SETTINGS_MESSAGE_BAR}`) ? IDS.SETTINGS_MESSAGE_BAR : null);
  1256.  
  1257. }
  1258. }
  1259.  
  1260. function _handleCustomListActionsInternal(event, contextElement, itemsArrayRef) {
  1261. const button = event.target.closest(`button.${CSS.EDIT_CUSTOM_ITEM}, button.${CSS.DELETE_CUSTOM_ITEM}, button.${CSS.REMOVE_FROM_LIST_BTN}`);
  1262. if (!button) return;
  1263.  
  1264. const listItem = button.closest(`li[data-${DATA_ATTR.INDEX}][data-list-id]`);
  1265. if (!listItem) return;
  1266.  
  1267. const index = parseInt(listItem.dataset[DATA_ATTR.INDEX], 10);
  1268. const listId = listItem.getAttribute('data-list-id'); // e.g., IDS.SITES_LIST
  1269.  
  1270. if (isNaN(index) || !listId || index < 0 || index >= itemsArrayRef.length) return;
  1271.  
  1272. const mapping = getListMapping(listId);
  1273. if (!mapping) return;
  1274.  
  1275. const item = itemsArrayRef[index];
  1276. if (!item) return; // Should not happen
  1277.  
  1278. const itemIsTrulyCustom = item.type === 'custom' ||
  1279. (!item.type && (listId === IDS.SITES_LIST || listId === IDS.TIME_LIST || listId === IDS.FT_LIST));
  1280.  
  1281.  
  1282. if (button.classList.contains(CSS.DELETE_CUSTOM_ITEM) && itemIsTrulyCustom) {
  1283. if (confirm(_('confirm_delete_item', { name: item.text }))) {
  1284. // If deleting the item currently being edited, reset edit state
  1285. if (_editingItemInfo && _editingItemInfo.listId === listId && _editingItemInfo.index === index) {
  1286. _resetEditStateInternal(contextElement);
  1287. }
  1288. itemsArrayRef.splice(index, 1);
  1289. mapping.populateFn(listId, itemsArrayRef, contextElement); // Re-render list
  1290. }
  1291. } else if (button.classList.contains(CSS.REMOVE_FROM_LIST_BTN) && item.type === 'predefined') {
  1292. // For sortable mixed lists (Lang, Country), predefined items can be removed from display
  1293. if (confirm(_('confirm_remove_item_from_list', { name: (item.originalKey ? _(item.originalKey) : item.text) }))) {
  1294. itemsArrayRef.splice(index, 1);
  1295. mapping.populateFn(listId, itemsArrayRef, contextElement);
  1296. }
  1297. } else if (button.classList.contains(CSS.EDIT_CUSTOM_ITEM) && itemIsTrulyCustom) {
  1298. _prepareEditItemActionInternal(item, index, listId, mapping, contextElement);
  1299. }
  1300. }
  1301.  
  1302. function _handleCustomItemSubmitInternal(listId, contextElement, itemsArrayRef) {
  1303. const mapping = getListMapping(listId);
  1304. if (!mapping) return;
  1305.  
  1306. const itemTypeName = _(mapping.nameKey); // For error messages
  1307. const textInput = contextElement.querySelector(mapping.textInput);
  1308. const valueInput = contextElement.querySelector(mapping.valueInput);
  1309.  
  1310. // Clear previous errors for this group before re-validating
  1311. const inputGroup = textInput?.closest(`.${CSS.CUSTOM_LIST_INPUT_GROUP}`);
  1312. if (inputGroup) _clearAllInputErrorsInGroup(inputGroup);
  1313.  
  1314.  
  1315. const validationResult = _validateAndPrepareCustomItemData(textInput, valueInput, itemTypeName, listId);
  1316. if (!validationResult.isValid) {
  1317. if (validationResult.errorField) validationResult.errorField.focus(); // Focus the first problematic field
  1318. return;
  1319. }
  1320.  
  1321. const { text, value } = validationResult;
  1322. const editingIdx = (_editingItemInfo && _editingItemInfo.listId === listId) ? _editingItemInfo.index : -1;
  1323.  
  1324. // Check for duplicate display names (only for custom items or all in non-mixed lists)
  1325. let isDuplicate;
  1326. if (mapping.isSortableMixed) { // Lang, Country - check against other custom items in the display list
  1327. isDuplicate = _isDuplicateCustomItem(text, itemsArrayRef, listId, editingIdx, _editingItemInfo);
  1328. } else { // Sites, Time, Filetypes - all items are effectively "custom" in editability
  1329. isDuplicate = itemsArrayRef.some((item, idx) => {
  1330. if (editingIdx === idx && (_editingItemInfo?.originalText?.toLowerCase() === text.toLowerCase())) return false; // Allow saving if name unchanged
  1331. return item.text.toLowerCase() === text.toLowerCase();
  1332. });
  1333. }
  1334.  
  1335. if (isDuplicate) {
  1336. if (textInput) _showInputError(textInput, 'alert_duplicate_name', { name: text });
  1337. textInput?.focus();
  1338. return;
  1339. }
  1340.  
  1341. // Prepare new item data
  1342. let newItemData;
  1343. if (listId === IDS.SITES_LIST) {
  1344. newItemData = { text: text, url: value }; // Sites use 'url'
  1345. } else if (mapping.isSortableMixed) { // Lang, Country
  1346. newItemData = { id: value, text: text, value: value, type: 'custom' }; // Ensure type is 'custom'
  1347. } else { // Time, Filetype
  1348. newItemData = { text: text, value: value };
  1349. }
  1350.  
  1351. // Determine if this item being edited is actually of a type that *can* be edited
  1352. // (e.g., a predefined item in a mixed list should not be "updated" this way)
  1353. const itemBeingEdited = (editingIdx > -1) ? itemsArrayRef[editingIdx] : null;
  1354. const itemBeingEditedIsCustom = itemBeingEdited &&
  1355. (itemBeingEdited.type === 'custom' || // Explicitly custom
  1356. listId === IDS.SITES_LIST || // Or one of these list types
  1357. listId === IDS.TIME_LIST ||
  1358. listId === IDS.FT_LIST);
  1359.  
  1360. if (editingIdx > -1 && itemBeingEditedIsCustom) { // Update existing custom item
  1361. // Merge new data into existing, preserving other properties if any
  1362. itemsArrayRef[editingIdx] = {...itemsArrayRef[editingIdx], ...newItemData };
  1363. _resetEditStateInternal(contextElement); // Reset inputs and button
  1364. } else { // Add as new custom item
  1365. itemsArrayRef.push(newItemData);
  1366. }
  1367.  
  1368. mapping.populateFn(listId, itemsArrayRef, contextElement); // Re-render the list
  1369.  
  1370. // Clear inputs if not in edit mode (or if edit was just completed for a different list)
  1371. if (!_editingItemInfo || _editingItemInfo.listId !== listId) {
  1372. if (textInput) { textInput.value = ''; _clearInputError(textInput); textInput.focus(); }
  1373. if (valueInput) { valueInput.value = ''; _clearInputError(valueInput); }
  1374. }
  1375. }
  1376.  
  1377.  
  1378. // --- Drag and Drop for Modal Lists ---
  1379. function _getDragAfterModalListItem(container, y) {
  1380. const draggableElements = [...container.querySelectorAll(`li[draggable="true"]:not(.${CSS.DRAGGING_ITEM})`)];
  1381. return draggableElements.reduce((closest, child) => {
  1382. const box = child.getBoundingClientRect();
  1383. const offset = y - box.top - box.height / 2;
  1384. if (offset < 0 && offset > closest.offset) {
  1385. return { offset: offset, element: child };
  1386. } else {
  1387. return closest;
  1388. }
  1389. }, { offset: Number.NEGATIVE_INFINITY }).element;
  1390. }
  1391.  
  1392. function _handleModalListDragStart(event) {
  1393. if (!event.target.matches('li[draggable="true"]')) return;
  1394. _draggedListItem = event.target;
  1395. event.dataTransfer.effectAllowed = 'move';
  1396. event.dataTransfer.setData('text/plain', _draggedListItem.dataset.index); // Store original index
  1397. _draggedListItem.classList.add(CSS.DRAGGING_ITEM);
  1398.  
  1399. // Disable pointer events on other items to prevent interference during dragover
  1400. const list = _draggedListItem.closest('ul');
  1401. if (list) { list.querySelectorAll('li:not(.gscs-dragging-item)').forEach(li => li.style.pointerEvents = 'none'); }
  1402. }
  1403.  
  1404. function _handleModalListDragOver(event) {
  1405. event.preventDefault(); // Necessary to allow drop
  1406. const listElement = event.currentTarget; // The UL element
  1407. // Clear previous highlights
  1408. listElement.querySelectorAll(`li.${CSS.DRAG_OVER_HIGHLIGHT}`).forEach(li => li.classList.remove(CSS.DRAG_OVER_HIGHLIGHT));
  1409.  
  1410. const targetItem = event.target.closest('li[draggable="true"]');
  1411. if (targetItem && targetItem !== _draggedListItem) {
  1412. targetItem.classList.add(CSS.DRAG_OVER_HIGHLIGHT); // Highlight item being dragged over
  1413. } else {
  1414. // If not directly over an item, find where it would be inserted
  1415. const afterElement = _getDragAfterModalListItem(listElement, event.clientY);
  1416. if (afterElement) {
  1417. afterElement.classList.add(CSS.DRAG_OVER_HIGHLIGHT); // Highlight element it would go before
  1418. }
  1419. }
  1420. }
  1421. function _handleModalListDragLeave(event) {
  1422. const listElement = event.currentTarget;
  1423. // Only remove highlight if leaving the list container itself, not just moving between items
  1424. if (event.relatedTarget && listElement.contains(event.relatedTarget)) return;
  1425. listElement.querySelectorAll(`li.${CSS.DRAG_OVER_HIGHLIGHT}`).forEach(li => li.classList.remove(CSS.DRAG_OVER_HIGHLIGHT));
  1426. }
  1427.  
  1428.  
  1429. function _handleModalListDrop(event, listId, itemsArrayRef) {
  1430. event.preventDefault();
  1431. if (!_draggedListItem) return;
  1432.  
  1433. const draggedIndexOriginal = parseInt(event.dataTransfer.getData('text/plain'), 10);
  1434. if (isNaN(draggedIndexOriginal) || draggedIndexOriginal < 0 || draggedIndexOriginal >= itemsArrayRef.length) {
  1435. _handleModalListDragEnd(event.currentTarget); // Pass the list element
  1436. return;
  1437. }
  1438.  
  1439. const listElement = event.currentTarget; // The UL
  1440. const mapping = getListMapping(listId);
  1441. if (!mapping) { _handleModalListDragEnd(listElement); return; }
  1442.  
  1443.  
  1444. const draggedItemData = itemsArrayRef[draggedIndexOriginal]; // Get the actual data object
  1445. if (!draggedItemData) { _handleModalListDragEnd(listElement); return; }
  1446.  
  1447.  
  1448. // Create a new array without the dragged item to calculate new position correctly
  1449. const itemsWithoutDragged = itemsArrayRef.filter((item, index) => index !== draggedIndexOriginal);
  1450.  
  1451. const afterElement = _getDragAfterModalListItem(listElement, event.clientY);
  1452. let newIndexInSplicedArray;
  1453.  
  1454. if (afterElement) {
  1455. const originalIndexOfAfterElement = parseInt(afterElement.dataset.index, 10);
  1456. // Find the index of 'afterElement' in the 'itemsWithoutDragged' array
  1457. let countSkipped = 0; newIndexInSplicedArray = -1;
  1458. for(let i=0; i < itemsArrayRef.length; i++) {
  1459. if (i === draggedIndexOriginal) continue; // Skip the one we are dragging
  1460. if (i === originalIndexOfAfterElement) {
  1461. newIndexInSplicedArray = countSkipped;
  1462. break;
  1463. }
  1464. countSkipped++;
  1465. }
  1466. // If afterElement was the last visible item and dragged item was before it
  1467. if (newIndexInSplicedArray === -1 && originalIndexOfAfterElement === itemsArrayRef.length -1 && draggedIndexOriginal < originalIndexOfAfterElement) {
  1468. newIndexInSplicedArray = itemsWithoutDragged.length;
  1469. } else if (newIndexInSplicedArray === -1) { // Fallback: if not found (e.g. dragging to end)
  1470. newIndexInSplicedArray = itemsWithoutDragged.length;
  1471. }
  1472.  
  1473. } else {
  1474. // Dropped at the end of the list
  1475. newIndexInSplicedArray = itemsWithoutDragged.length;
  1476. }
  1477.  
  1478. // Insert the dragged item data into the new position in the temporary array
  1479. itemsWithoutDragged.splice(newIndexInSplicedArray, 0, draggedItemData);
  1480.  
  1481. // Update the original itemsArrayRef with the new order
  1482. itemsArrayRef.length = 0; // Clear original array
  1483. itemsWithoutDragged.forEach(item => itemsArrayRef.push(item)); // Push items in new order
  1484.  
  1485. _handleModalListDragEnd(listElement); // Cleanup drag styles
  1486. mapping.populateFn(listId, itemsArrayRef, _currentModalContent); // Re-render the list
  1487.  
  1488. // Update dataset.index on all li elements after re-rendering
  1489. const newLiElements = listElement.querySelectorAll('li');
  1490. newLiElements.forEach((li, idx) => {
  1491. li.dataset.index = idx;
  1492. });
  1493. }
  1494.  
  1495.  
  1496. function _handleModalListDragEnd(listElement) { // listElement is the UL
  1497. if (_draggedListItem) {
  1498. _draggedListItem.classList.remove(CSS.DRAGGING_ITEM);
  1499. }
  1500. _draggedListItem = null;
  1501. (listElement || _currentModalContent)?.querySelectorAll(`li.${CSS.DRAG_OVER_HIGHLIGHT}`).forEach(li => li.classList.remove(CSS.DRAG_OVER_HIGHLIGHT));
  1502. // Re-enable pointer events on list items
  1503. _currentModalContent?.querySelectorAll('ul.custom-list li[draggable="true"]').forEach(li => li.style.pointerEvents = '');
  1504. }
  1505.  
  1506.  
  1507. function _addPredefinedItemsToModalList(selectedValues, predefinedSourceKey, displayItemsArrayRef, listIdToUpdate, modalContentContext) {
  1508. const mapping = getListMapping(listIdToUpdate);
  1509. if (!mapping) return;
  1510.  
  1511. selectedValues.forEach(value => {
  1512. const predefinedOpt = PREDEFINED_OPTIONS[predefinedSourceKey]?.find(p => p.value === value);
  1513. if (predefinedOpt && !displayItemsArrayRef.some(item => item.value === value && item.type === 'predefined')) {
  1514. displayItemsArrayRef.push({
  1515. id: predefinedOpt.value, // Use value as ID for predefined
  1516. text: _(predefinedOpt.textKey), // Store original text key for re-translation if lang changes
  1517. value: predefinedOpt.value,
  1518. type: 'predefined',
  1519. originalKey: predefinedOpt.textKey // Store the key
  1520. });
  1521. }
  1522. });
  1523. // No sort here, rely on user to sort if needed, or initial sort if it's a mixed list.
  1524. // Or, sort them by their translated text if that's desired for newly added ones.
  1525. // For now, just append.
  1526. mapping.populateFn(listIdToUpdate, displayItemsArrayRef, modalContentContext);
  1527. }
  1528.  
  1529. // Binds events to the content within the modal
  1530. function _bindModalContentEventsInternal(modalContent, itemsArrayRef, listIdForDragDrop = null) {
  1531. if (!modalContent) return;
  1532.  
  1533. // Prevent re-binding if already bound for this specific list setup
  1534. if (modalContent.dataset.modalEventsBound === 'true' && listIdForDragDrop === modalContent.dataset.boundListId) return;
  1535.  
  1536.  
  1537. // General click listener for the modal body (delegated)
  1538. modalContent.addEventListener('click', (event) => {
  1539. const target = event.target;
  1540. // Determine which list this action pertains to (can be complex if multiple lists in one modal, but not current design)
  1541. let listIdForAction = listIdForDragDrop || target.closest('[data-list-id]')?.dataset.listId || target.closest(`.${CSS.ADD_CUSTOM_BUTTON}`)?.dataset.listId;
  1542.  
  1543. const addNewOptionButton = target.closest(`#${IDS.MODAL_ADD_NEW_OPTION_BTN}`);
  1544. if (addNewOptionButton && listIdForAction) {
  1545. // This button is for mixed lists (Lang/Country) to add predefined or custom items
  1546. const mapping = getListMapping(listIdForAction);
  1547. const configForModal = modalConfigsData[Object.keys(modalConfigsData).find(key => modalConfigsData[key].listId === listIdForAction)];
  1548.  
  1549. if (mapping && configForModal && configForModal.predefinedSourceKey && configForModal.isSortableMixed) {
  1550. PredefinedOptionChooser.show(
  1551. configForModal.manageType,
  1552. listIdForAction,
  1553. configForModal.predefinedSourceKey,
  1554. itemsArrayRef, // Pass the live array
  1555. modalContent,
  1556. _addPredefinedItemsToModalList
  1557. );
  1558. } else if (mapping) { // If not a mixed list with predefined chooser, just focus input
  1559. const textInput = modalContent.querySelector(mapping.textInput);
  1560. textInput?.focus();
  1561. }
  1562. return; // Handled
  1563. }
  1564.  
  1565.  
  1566. const addButton = target.closest(`.${CSS.ADD_CUSTOM_BUTTON}.custom-list-action-button`);
  1567. const itemControlButton = target.closest(`button.${CSS.EDIT_CUSTOM_ITEM}, button.${CSS.DELETE_CUSTOM_ITEM}, button.${CSS.REMOVE_FROM_LIST_BTN}`);
  1568. const cancelEditButton = target.closest('.cancel-edit-button');
  1569.  
  1570. if (itemControlButton && listIdForAction) { _handleCustomListActionsInternal(event, modalContent, itemsArrayRef); return; }
  1571. if (addButton && listIdForAction) { _handleCustomItemSubmitInternal(listIdForAction, modalContent, itemsArrayRef); return; }
  1572. if (cancelEditButton) { _resetEditStateInternal(modalContent); return; }
  1573. });
  1574. modalContent.dataset.modalEventsBound = 'true';
  1575. modalContent.dataset.boundListId = listIdForDragDrop; // Store which list drag events are bound to
  1576.  
  1577.  
  1578. // Input validation listener
  1579. modalContent.addEventListener('input', (event) => {
  1580. const target = event.target;
  1581. // Validate relevant input fields on input
  1582. if (target.matches(`#${IDS.NEW_SITE_NAME}, #${IDS.NEW_SITE_URL}, #${IDS.NEW_LANG_TEXT}, #${IDS.NEW_LANG_VALUE}, #${IDS.NEW_TIME_TEXT}, #${IDS.NEW_TIME_VALUE}, #${IDS.NEW_FT_TEXT}, #${IDS.NEW_FT_VALUE}, #${IDS.NEW_COUNTRY_TEXT}, #${IDS.NEW_COUNTRY_VALUE}`)) {
  1583. _clearInputError(target); // Clear previous error first
  1584. validateCustomInput(target);
  1585. }
  1586. });
  1587.  
  1588. // Bind drag-and-drop listeners if a specific listId is provided for it
  1589. if (listIdForDragDrop) {
  1590. const draggableListElement = modalContent.querySelector(`#${listIdForDragDrop}`);
  1591. if (draggableListElement) {
  1592. if (draggableListElement.dataset.dragEventsBound !== 'true') { // Prevent multiple bindings
  1593. draggableListElement.dataset.dragEventsBound = 'true';
  1594. draggableListElement.addEventListener('dragstart', _handleModalListDragStart);
  1595. draggableListElement.addEventListener('dragover', _handleModalListDragOver);
  1596. draggableListElement.addEventListener('dragleave', _handleModalListDragLeave);
  1597. draggableListElement.addEventListener('drop', (event) => _handleModalListDrop(event, listIdForDragDrop, itemsArrayRef));
  1598. draggableListElement.addEventListener('dragend', (event) => _handleModalListDragEnd(draggableListElement));
  1599. }
  1600. }
  1601. }
  1602. }
  1603.  
  1604. return {
  1605. show: function(titleKey, contentHTML, onCompleteCallback, currentTheme) {
  1606. this.hide(); // Hide any existing modal
  1607.  
  1608. _currentModal = document.createElement('div');
  1609. _currentModal.className = 'settings-modal-overlay';
  1610. applyThemeToElement(_currentModal, currentTheme); // Apply theme to overlay
  1611.  
  1612. _currentModalContent = document.createElement('div');
  1613. _currentModalContent.className = 'settings-modal-content';
  1614. applyThemeToElement(_currentModalContent, currentTheme); // Apply theme to content
  1615.  
  1616. const header = document.createElement('div');
  1617. header.className = 'settings-modal-header';
  1618. header.innerHTML = `<h4>${_(titleKey)}</h4><button class="settings-modal-close-btn" title="${_('settings_close_button_title')}">${SVG_ICONS.close}</button>`;
  1619.  
  1620. const body = document.createElement('div');
  1621. body.className = 'settings-modal-body';
  1622. body.innerHTML = contentHTML;
  1623.  
  1624. const footer = document.createElement('div');
  1625. footer.className = 'settings-modal-footer';
  1626. footer.innerHTML = `<button class="modal-complete-btn">${_('modal_button_complete')}</button>`;
  1627.  
  1628. _currentModalContent.appendChild(header);
  1629. _currentModalContent.appendChild(body);
  1630. _currentModalContent.appendChild(footer);
  1631. _currentModal.appendChild(_currentModalContent);
  1632.  
  1633. // Event listeners for closing
  1634. let closeModalHandlerInstance = null; // To store the event handler for removal
  1635. let completeModalHandlerInstance = null;
  1636. const self = this; // For context in handlers
  1637.  
  1638. closeModalHandlerInstance = (event) => {
  1639. // Close if clicking overlay directly or the close button
  1640. if (event.target === _currentModal || event.target.closest('.settings-modal-close-btn')) {
  1641. self.hide(true); // true for cancel
  1642. // Clean up listeners to prevent memory leaks
  1643. _currentModal?.removeEventListener('click', closeModalHandlerInstance, true); // Use capture for overlay click
  1644. header.querySelector('.settings-modal-close-btn')?.removeEventListener('click', closeModalHandlerInstance);
  1645. footer.querySelector('.modal-complete-btn')?.removeEventListener('click', completeModalHandlerInstance);
  1646. }
  1647. };
  1648. completeModalHandlerInstance = () => {
  1649. if (onCompleteCallback && typeof onCompleteCallback === 'function') {
  1650. onCompleteCallback(_currentModalContent); // Pass content for data extraction
  1651. }
  1652. // Clean up listeners
  1653. _currentModal?.removeEventListener('click', closeModalHandlerInstance, true);
  1654. header.querySelector('.settings-modal-close-btn')?.removeEventListener('click', closeModalHandlerInstance);
  1655. footer.querySelector('.modal-complete-btn')?.removeEventListener('click', completeModalHandlerInstance);
  1656. self.hide(false); // false for complete
  1657. };
  1658.  
  1659. _currentModal.addEventListener('click', closeModalHandlerInstance, true); // Overlay click (capture phase)
  1660. header.querySelector('.settings-modal-close-btn').addEventListener('click', closeModalHandlerInstance);
  1661. footer.querySelector('.modal-complete-btn').addEventListener('click', completeModalHandlerInstance);
  1662.  
  1663. // Prevent clicks inside the modal content from closing it via overlay click
  1664. _currentModalContent.addEventListener('click', (event) => event.stopPropagation());
  1665.  
  1666. document.body.appendChild(_currentModal);
  1667. return _currentModalContent; // Return the content div for further population/binding
  1668. },
  1669. hide: function(isCancel = false) {
  1670. PredefinedOptionChooser.hide(); // Ensure chooser is also hidden
  1671. if (_currentModal) {
  1672. // Clear any specific states tied to the modal content before removing
  1673. const inputGroup = _currentModalContent?.querySelector(`.${CSS.CUSTOM_LIST_INPUT_GROUP}`);
  1674. if (inputGroup) _clearAllInputErrorsInGroup(inputGroup);
  1675. _resetEditStateInternal(); // Clear editing state
  1676.  
  1677. _currentModal.remove(); // Remove from DOM
  1678. }
  1679. _currentModal = null;
  1680. _currentModalContent = null;
  1681. _handleModalListDragEnd(); // Clean up general drag state if any
  1682. },
  1683. openManageCustomOptions: function(manageType, currentSettingsRef, PREDEFINED_OPTIONS_REF, onModalCompleteCallback) {
  1684. const config = modalConfigsData[manageType];
  1685. if (!config) { console.error("Error: Could not get config for manageType:", manageType); return; }
  1686.  
  1687. const mapping = getListMapping(config.listId); // For input IDs etc.
  1688. if (!mapping) { console.error("Error: Could not get mapping for listId:", config.listId); return; }
  1689.  
  1690. // Create a deep copy of the items to be managed in the modal, so direct modifications
  1691. // don't affect the main settings until "Done" is clicked.
  1692. const tempItems = JSON.parse(JSON.stringify(currentSettingsRef[config.itemsArrayKey] || []));
  1693. let contentHTML = '';
  1694. const itemTypeNameForDisplay = _(mapping.nameKey); // e.g., "Languages", "Favorite Sites"
  1695.  
  1696. if (config.isSortableMixed) { // For Language, Country
  1697. contentHTML += _createModalListAndInputHTML(config.listId, config.textPKey, config.valPKey, config.hintKey, config.fmtKey, itemTypeNameForDisplay, true);
  1698. } else if (config.hasPredefinedToggles && config.predefinedSourceKey && PREDEFINED_OPTIONS_REF[config.predefinedSourceKey]) {
  1699. // For Time, Filetype - which have a list of predefined checkboxes AND a custom list
  1700. const enabledValues = new Set(currentSettingsRef.enabledPredefinedOptions[config.predefinedSourceKey] || []);
  1701. contentHTML += _createPredefinedOptionsSectionHTML(config.predefinedSourceKey, mapping.nameKey, PREDEFINED_OPTIONS_REF, enabledValues);
  1702. contentHTML += '<hr style="margin: 1em 0;">'; // Separator
  1703. contentHTML += _createModalListAndInputHTML(config.listId, config.textPKey, config.valPKey, config.hintKey, config.fmtKey, itemTypeNameForDisplay, false);
  1704. } else { // For Sites (no predefined toggles, only custom list)
  1705. contentHTML += _createModalListAndInputHTML(config.listId, config.textPKey, config.valPKey, config.hintKey, config.fmtKey, itemTypeNameForDisplay, false);
  1706. }
  1707.  
  1708. const modalContentElement = this.show(
  1709. config.modalTitleKey,
  1710. contentHTML,
  1711. (modalContent) => { // onCompleteCallback
  1712. let newEnabledPredefs = null;
  1713. if (config.hasPredefinedToggles && config.predefinedSourceKey) {
  1714. newEnabledPredefs = [];
  1715. modalContent.querySelectorAll(`.predefined-options-list input[data-option-type="${config.predefinedSourceKey}"]:checked`).forEach(cb => newEnabledPredefs.push(cb.value));
  1716. }
  1717. onModalCompleteCallback(tempItems, newEnabledPredefs, config.itemsArrayKey, config.predefinedSourceKey, config.customItemsMasterKey, config.isSortableMixed, manageType);
  1718. },
  1719. currentSettingsRef.theme // Pass current theme for modal styling
  1720. );
  1721.  
  1722. // Populate the list within the modal and bind events
  1723. if (modalContentElement) {
  1724. if (mapping && mapping.populateFn) {
  1725. mapping.populateFn(config.listId, tempItems, modalContentElement);
  1726. }
  1727. _bindModalContentEventsInternal(modalContentElement, tempItems, config.listId); // Pass tempItems for direct modification
  1728. }
  1729. },
  1730. resetEditStateGlobally: function() { _resetEditStateInternal(_currentModalContent || document); },
  1731. isModalOpen: function() { return !!_currentModal; }
  1732. };
  1733. })();
  1734.  
  1735. const SettingsUIPaneGenerator = (function() {
  1736. function createGeneralPaneHTML() {
  1737. let langOpts = LocalizationService.getAvailableLocales().map(lc => {
  1738. let dn;
  1739. if (lc === 'auto') dn = _('settings_language_auto');
  1740. else { try { dn = new Intl.DisplayNames([lc],{type:'language'}).of(lc); dn = dn.charAt(0).toUpperCase() + dn.slice(1); } catch(e){ dn = lc; } dn = `${dn} (${lc})`; }
  1741. return `<option value="${lc}">${dn}</option>`;
  1742. }).join('');
  1743. const accordionHintHTML = `<div class="${CSS.SETTING_VALUE_HINT}" style="margin-top:0.3em; margin-left:1.7em; font-weight:normal;">${_('settings_accordion_mode_hint_desc')}</div>`;
  1744. const locationOptionsHTML = `
  1745. <option value="tools">${_('settings_location_tools')}</option>
  1746. <option value="topBlock">${_('settings_location_top')}</option>
  1747. <option value="header">${_('settings_location_header')}</option>
  1748. <option value="none">${_('settings_location_hide')}</option>`;
  1749.  
  1750. return `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_INTERFACE_LANGUAGE}">${_('settings_interface_language')}</label><select id="${IDS.SETTING_INTERFACE_LANGUAGE}">${langOpts}</select></div>` +
  1751. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_SECTION_MODE}">${_('settings_section_mode')}</label><select id="${IDS.SETTING_SECTION_MODE}"><option value="remember">${_('settings_section_mode_remember')}</option><option value="expandAll">${_('settings_section_mode_expand')}</option><option value="collapseAll">${_('settings_section_mode_collapse')}</option></select><div style="margin-top:0.6em;"><input type="checkbox" id="${IDS.SETTING_ACCORDION}"><label for="${IDS.SETTING_ACCORDION}" class="${CSS.INLINE_LABEL}">${_('settings_accordion_mode')}</label>${accordionHintHTML}</div></div>` +
  1752. `<div class="${CSS.SETTING_ITEM}"><input type="checkbox" id="${IDS.SETTING_DRAGGABLE}"><label for="${IDS.SETTING_DRAGGABLE}" class="${CSS.INLINE_LABEL}">${_('settings_enable_drag')}</label></div>` +
  1753. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_RESET_LOCATION}">${_('settings_reset_button_location')}</label><select id="${IDS.SETTING_RESET_LOCATION}">${locationOptionsHTML}</select></div>` +
  1754. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_VERBATIM_LOCATION}">${_('settings_verbatim_button_location')}</label><select id="${IDS.SETTING_VERBATIM_LOCATION}">${locationOptionsHTML}</select></div>` +
  1755. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_ADV_SEARCH_LOCATION}">${_('settings_adv_search_location')}</label><select id="${IDS.SETTING_ADV_SEARCH_LOCATION}">${locationOptionsHTML}</select></div>`+
  1756. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_PERSONALIZE_LOCATION}">${_('settings_personalize_button_location')}</label><select id="${IDS.SETTING_PERSONALIZE_LOCATION}">${locationOptionsHTML}</select></div>` +
  1757. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_SCHOLAR_LOCATION}">${_('settings_scholar_location')}</label><select id="${IDS.SETTING_SCHOLAR_LOCATION}">${locationOptionsHTML}</select></div>` +
  1758. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_TRENDS_LOCATION}">${_('settings_trends_location')}</label><select id="${IDS.SETTING_TRENDS_LOCATION}">${locationOptionsHTML}</select></div>` +
  1759. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_DATASET_SEARCH_LOCATION}">${_('settings_dataset_search_location')}</label><select id="${IDS.SETTING_DATASET_SEARCH_LOCATION}">${locationOptionsHTML}</select></div>`;
  1760. }
  1761. function createAppearancePaneHTML() {
  1762. // New HTML for hideGoogleLogo setting
  1763. const hideLogoSettingHTML =
  1764. `<div class="${CSS.SETTING_ITEM}">` +
  1765. `<input type="checkbox" id="${IDS.SETTING_HIDE_GOOGLE_LOGO}"><label for="${IDS.SETTING_HIDE_GOOGLE_LOGO}" class="${CSS.INLINE_LABEL}">${_('settings_hide_google_logo')}</label>` +
  1766. `<div class="${CSS.SETTING_VALUE_HINT}" style="margin-top:0.3em; margin-left:1.7em; font-weight:normal;">${_('settings_hide_google_logo_hint')}</div>` +
  1767. `</div>`;
  1768.  
  1769. return `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_WIDTH}">${_('settings_sidebar_width')}</label><span class="${CSS.RANGE_HINT}">${_('settings_width_range_hint')}</span><input type="range" id="${IDS.SETTING_WIDTH}" min="90" max="270" step="5"><span class="${CSS.RANGE_VALUE}"></span></div>` +
  1770. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_FONT_SIZE}">${_('settings_font_size')}</label><span class="${CSS.RANGE_HINT}">${_('settings_font_size_range_hint')}</span><input type="range" id="${IDS.SETTING_FONT_SIZE}" min="8" max="24" step="0.5"><span class="${CSS.RANGE_VALUE}"></span></div>` +
  1771. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_HEADER_ICON_SIZE}">${_('settings_header_icon_size')}</label><span class="${CSS.RANGE_HINT}">${_('settings_header_icon_size_range_hint')}</span><input type="range" id="${IDS.SETTING_HEADER_ICON_SIZE}" min="8" max="32" step="0.5"><span class="${CSS.RANGE_VALUE}"></span></div>` +
  1772. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_VERTICAL_SPACING}">${_('settings_vertical_spacing')}</label><span class="${CSS.RANGE_HINT}">${_('settings_vertical_spacing_range_hint')}</span><input type="range" id="${IDS.SETTING_VERTICAL_SPACING}" min="0.05" max="1.5" step="0.05"><span class="${CSS.RANGE_VALUE}"></span></div>` +
  1773. `<div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_THEME}">${_('settings_theme')}</label><select id="${IDS.SETTING_THEME}"><option value="system">${_('settings_theme_system')}</option><option value="light">${_('settings_theme_light')}</option><option value="dark">${_('settings_theme_dark')}</option><option value="minimal-light">${_('settings_theme_minimal_light')}</option><option value="minimal-dark">${_('settings_theme_minimal_dark')}</option></select></div><div class="${CSS.SETTING_ITEM}"><input type="checkbox" id="${IDS.SETTING_HOVER}"><label for="${IDS.SETTING_HOVER}" class="${CSS.INLINE_LABEL}">${_('settings_hover_mode')}</label><div style="margin-top:0.8em;padding-left:1.5em;"><label for="${IDS.SETTING_OPACITY}" style="display:block;margin-bottom:0.4em;font-weight:normal;">${_('settings_idle_opacity')}</label><span class="${CSS.RANGE_HINT}" style="width:auto;display:inline-block;margin-right:1em;">${_('settings_opacity_range_hint')}</span><input type="range" id="${IDS.SETTING_OPACITY}" min="0.1" max="1.0" step="0.05" style="width:calc(100% - 18em);vertical-align:middle;display:inline-block;"><span class="${CSS.RANGE_VALUE}" style="display:inline-block;min-width:3em;text-align:right;vertical-align:middle;"></span></div></div><div class="${CSS.SETTING_ITEM}"><label for="${IDS.SETTING_COUNTRY_DISPLAY_MODE}">${_('settings_country_display')}</label><select id="${IDS.SETTING_COUNTRY_DISPLAY_MODE}"><option value="iconAndText">${_('settings_country_display_icontext')}</option><option value="textOnly">${_('settings_country_display_text')}</option><option value="iconOnly">${_('settings_country_display_icon')}</option></select></div>` +
  1774. hideLogoSettingHTML;
  1775. }
  1776. function createFeaturesPaneHTML() {
  1777. const visItemsHTML = ALL_SECTION_DEFINITIONS.map(def=>{ const dn=_(def.titleKey)||def.id; return `<div class="${CSS.SETTING_ITEM} ${CSS.SIMPLE_ITEM}"><input type="checkbox" id="setting-visible-${def.id}" data-${DATA_ATTR.SECTION_ID}="${def.id}"><label for="setting-visible-${def.id}" class="${CSS.INLINE_LABEL}">${dn}</label></div>`; }).join('');
  1778. const siteSearchCheckboxModeHTML =
  1779. `<div class="${CSS.SETTING_ITEM}">` +
  1780. `<input type="checkbox" id="${IDS.SETTING_SITE_SEARCH_CHECKBOX_MODE}"><label for="${IDS.SETTING_SITE_SEARCH_CHECKBOX_MODE}" class="${CSS.INLINE_LABEL}">${_('settings_enable_site_search_checkbox_mode')}</label>` +
  1781. `<div class="${CSS.SETTING_VALUE_HINT}" style="margin-top:0.3em; margin-left:1.7em; font-weight:normal;">${_('settings_enable_site_search_checkbox_mode_hint')}</div>` +
  1782. `</div>`;
  1783. const showFaviconsHTML =
  1784. `<div class="${CSS.SETTING_ITEM}">` + // Indent this setting
  1785. `<input type="checkbox" id="${IDS.SETTING_SHOW_FAVICONS}"><label for="${IDS.SETTING_SHOW_FAVICONS}" class="${CSS.INLINE_LABEL}">${_('settings_show_favicons')}</label>` +
  1786. `<div class="${CSS.SETTING_VALUE_HINT}" style="margin-top:0.3em; margin-left:1.7em; font-weight:normal;">${_('settings_show_favicons_hint')}</div>` +
  1787. `</div>`;
  1788. const filetypeSearchCheckboxModeHTML =
  1789. `<div class="${CSS.SETTING_ITEM}">` +
  1790. `<input type="checkbox" id="${IDS.SETTING_FILETYPE_SEARCH_CHECKBOX_MODE}"><label for="${IDS.SETTING_FILETYPE_SEARCH_CHECKBOX_MODE}" class="${CSS.INLINE_LABEL}">${_('settings_enable_filetype_search_checkbox_mode')}</label>` +
  1791. `<div class="${CSS.SETTING_VALUE_HINT}" style="margin-top:0.3em; margin-left:1.7em; font-weight:normal;">${_('settings_enable_filetype_search_checkbox_mode_hint')}</div>` +
  1792. `</div>`;
  1793. return `<p>${_('settings_visible_sections')}</p>${visItemsHTML}` +
  1794. `${siteSearchCheckboxModeHTML}${showFaviconsHTML}${filetypeSearchCheckboxModeHTML}<hr style="margin:1.2em 0;">` +
  1795. `<p style="font-weight:bold;margin-bottom:0.5em;">${_('settings_section_order')}</p><p class="${CSS.SETTING_VALUE_HINT}" style="font-size:0.9em;margin-top:-0.3em;margin-bottom:0.7em;">${_('settings_section_order_hint')}</p><ul id="${IDS.SIDEBAR_SECTION_ORDER_LIST}" class="${CSS.SECTION_ORDER_LIST}"></ul>`;
  1796. }
  1797. function createCustomPaneHTML() {
  1798. return `<div class="${CSS.SETTING_ITEM}"><p>${_('settings_custom_intro')}</p><button class="${CSS.MANAGE_CUSTOM_BUTTON}" data-${DATA_ATTR.MANAGE_TYPE}="site">${_('settings_manage_sites_button')}</button></div><div class="${CSS.SETTING_ITEM}"><button class="${CSS.MANAGE_CUSTOM_BUTTON}" data-${DATA_ATTR.MANAGE_TYPE}="language">${_('settings_manage_languages_button')}</button></div><div class="${CSS.SETTING_ITEM}"><button class="${CSS.MANAGE_CUSTOM_BUTTON}" data-${DATA_ATTR.MANAGE_TYPE}="country">${_('settings_manage_countries_button')}</button></div><div class="${CSS.SETTING_ITEM}"><button class="${CSS.MANAGE_CUSTOM_BUTTON}" data-${DATA_ATTR.MANAGE_TYPE}="time">${_('settings_manage_time_ranges_button')}</button></div><div class="${CSS.SETTING_ITEM}"><button class="${CSS.MANAGE_CUSTOM_BUTTON}" data-${DATA_ATTR.MANAGE_TYPE}="filetype">${_('settings_manage_file_types_button')}</button></div>`;
  1799. }
  1800. return { createGeneralPaneHTML, createAppearancePaneHTML, createFeaturesPaneHTML, createCustomPaneHTML };
  1801. })();
  1802. const SectionOrderDragHandler = (function() {
  1803. let _draggedItem = null; let _listElement = null; let _settingsRef = null; let _onOrderUpdateCallback = null;
  1804. function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll(`li[draggable="true"]:not(.${CSS.DRAGGING_ITEM})`)]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; }
  1805. function handleDragStart(event) { _draggedItem = event.target; event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', _draggedItem.dataset.sectionId); _draggedItem.classList.add(CSS.DRAGGING_ITEM); if (_listElement) { _listElement.querySelectorAll('li:not(.gscs-dragging-item)').forEach(li => li.style.pointerEvents = 'none'); } }
  1806. function handleDragOver(event) { event.preventDefault(); if (!_listElement) return; _listElement.querySelectorAll(`li.${CSS.DRAG_OVER_HIGHLIGHT}`).forEach(li => { li.classList.remove(CSS.DRAG_OVER_HIGHLIGHT); }); const targetItem = event.target.closest('li[draggable="true"]'); if (targetItem && targetItem !== _draggedItem) { targetItem.classList.add(CSS.DRAG_OVER_HIGHLIGHT); } else if (!targetItem && _listElement.contains(event.target)) { const afterElement = getDragAfterElement(_listElement, event.clientY); if (afterElement) { afterElement.classList.add(CSS.DRAG_OVER_HIGHLIGHT); } } }
  1807. function handleDragLeave(event) { const relatedTarget = event.relatedTarget; if (_listElement && (!relatedTarget || !_listElement.contains(relatedTarget))) { _listElement.querySelectorAll(`li.${CSS.DRAG_OVER_HIGHLIGHT}`).forEach(li => { li.classList.remove(CSS.DRAG_OVER_HIGHLIGHT); }); } }
  1808. function handleDrop(event) {
  1809. event.preventDefault(); if (!_draggedItem || !_listElement || !_settingsRef || !_onOrderUpdateCallback) return;
  1810. const draggedSectionId = event.dataTransfer.getData('text/plain');
  1811. let currentVisibleOrder = _settingsRef.sidebarSectionOrder.filter(id => _settingsRef.visibleSections[id]);
  1812. const oldIndexInVisible = currentVisibleOrder.indexOf(draggedSectionId);
  1813. if (oldIndexInVisible > -1) { currentVisibleOrder.splice(oldIndexInVisible, 1); } else { handleDragEnd(); return; } // Should not happen if drag started correctly
  1814. const afterElement = getDragAfterElement(_listElement, event.clientY);
  1815. if (afterElement) { const targetId = afterElement.dataset.sectionId; const newIndexInVisible = currentVisibleOrder.indexOf(targetId); if (newIndexInVisible > -1) { currentVisibleOrder.splice(newIndexInVisible, 0, draggedSectionId); } else { currentVisibleOrder.push(draggedSectionId); } // Fallback if targetId not in currentVisibleOrder (should be rare)
  1816. } else { currentVisibleOrder.push(draggedSectionId); } // Dropped at the end
  1817. const hiddenSectionOrder = _settingsRef.sidebarSectionOrder.filter(id => !_settingsRef.visibleSections[id]); // Keep hidden sections at the end
  1818. _settingsRef.sidebarSectionOrder = [...currentVisibleOrder, ...hiddenSectionOrder];
  1819. handleDragEnd(); _onOrderUpdateCallback();
  1820. }
  1821. function handleDragEnd() { if (_draggedItem) { _draggedItem.classList.remove(CSS.DRAGGING_ITEM); } _draggedItem = null; if (_listElement) { _listElement.querySelectorAll('li').forEach(li => { li.classList.remove(CSS.DRAG_OVER_HIGHLIGHT); li.style.pointerEvents = ''; }); } }
  1822. function initialize(listEl, currentSettings, orderUpdateCallback) { _listElement = listEl; _settingsRef = currentSettings; _onOrderUpdateCallback = orderUpdateCallback; if (_listElement && _listElement.dataset.sectionOrderDragBound !== 'true') { _listElement.addEventListener('dragstart', handleDragStart); _listElement.addEventListener('dragover', handleDragOver); _listElement.addEventListener('dragleave', handleDragLeave); _listElement.addEventListener('drop', handleDrop); _listElement.addEventListener('dragend', handleDragEnd); _listElement.dataset.sectionOrderDragBound = 'true'; } }
  1823. function destroy() { if (_listElement && _listElement.dataset.sectionOrderDragBound === 'true') { _listElement.removeEventListener('dragstart', handleDragStart); _listElement.removeEventListener('dragover', handleDragOver); _listElement.removeEventListener('dragleave', handleDragLeave); _listElement.removeEventListener('drop', handleDrop); _listElement.removeEventListener('dragend', handleDragEnd); delete _listElement.dataset.sectionOrderDragBound; } _listElement = null; _settingsRef = null; _onOrderUpdateCallback = null; }
  1824. return { initialize, destroy };
  1825. })();
  1826.  
  1827. // SettingsManager - Manages loading, saving, applying settings, and settings UI
  1828. const SettingsManager = (function() {
  1829. let _settingsWindow = null; let _settingsOverlay = null; let _currentSettings = {};
  1830. let _settingsBackup = {}; let _defaultSettingsRef = null; let _isInitialized = false;
  1831. let _applySettingsToSidebar_cb = ()=>{}; let _buildSidebarUI_cb = ()=>{};
  1832. let _applySectionCollapseStates_cb = ()=>{}; let _initMenuCommands_cb = ()=>{};
  1833. let _renderSectionOrderList_ext_cb = ()=>{}; // External callback for rendering order list
  1834.  
  1835. // Internal helper to populate a slider and its value display
  1836. function _populateSliderSetting_internal(win,id,value,formatFn=(val)=>val){const i=win.querySelector(`#${id}`);if(i){i.value=value;let vs=i.parentNode.querySelector(`.${CSS.RANGE_VALUE}`);if(vs&&vs.classList.contains(CSS.RANGE_VALUE)){vs.textContent=formatFn(value);}}}
  1837. // Internal helper to populate general settings tab
  1838. function _populateGeneralSettings_internal(win,s){ const lS=win.querySelector(`#${IDS.SETTING_INTERFACE_LANGUAGE}`);if(lS)lS.value=s.interfaceLanguage;const sMS=win.querySelector(`#${IDS.SETTING_SECTION_MODE}`),acC=win.querySelector(`#${IDS.SETTING_ACCORDION}`);if(sMS&&acC){sMS.value=s.sectionDisplayMode;const iRM=s.sectionDisplayMode==='remember';acC.disabled=!iRM;acC.checked=iRM?s.accordionMode:false; const accordionHint = acC.parentElement.querySelector(`.${CSS.SETTING_VALUE_HINT}`); if(accordionHint) accordionHint.style.color = iRM ? '' : 'grey';} const dC=win.querySelector(`#${IDS.SETTING_DRAGGABLE}`);if(dC)dC.checked=s.draggableHandleEnabled;const rLS=win.querySelector(`#${IDS.SETTING_RESET_LOCATION}`);if(rLS)rLS.value=s.resetButtonLocation;const vLS=win.querySelector(`#${IDS.SETTING_VERBATIM_LOCATION}`);if(vLS)vLS.value=s.verbatimButtonLocation;const aSLS=win.querySelector(`#${IDS.SETTING_ADV_SEARCH_LOCATION}`);if(aSLS)aSLS.value=s.advancedSearchLinkLocation; const pznLS = win.querySelector(`#${IDS.SETTING_PERSONALIZE_LOCATION}`); if (pznLS) pznLS.value = s.personalizationButtonLocation; const schLS = win.querySelector(`#${IDS.SETTING_SCHOLAR_LOCATION}`); if (schLS) schLS.value = s.googleScholarShortcutLocation; const trnLS = win.querySelector(`#${IDS.SETTING_TRENDS_LOCATION}`); if (trnLS) trnLS.value = s.googleTrendsShortcutLocation; const dsLS = win.querySelector(`#${IDS.SETTING_DATASET_SEARCH_LOCATION}`); if (dsLS) dsLS.value = s.googleDatasetSearchShortcutLocation;}
  1839. // Internal helper to populate appearance settings tab
  1840. function _populateAppearanceSettings_internal(win,s){_populateSliderSetting_internal(win,IDS.SETTING_WIDTH,s.sidebarWidth);_populateSliderSetting_internal(win,IDS.SETTING_FONT_SIZE,s.fontSize,v=>parseFloat(v).toFixed(1));_populateSliderSetting_internal(win,IDS.SETTING_HEADER_ICON_SIZE,s.headerIconSize,v=>parseFloat(v).toFixed(1));_populateSliderSetting_internal(win,IDS.SETTING_VERTICAL_SPACING,s.verticalSpacingMultiplier,v=>`x ${parseFloat(v).toFixed(2)}`);_populateSliderSetting_internal(win,IDS.SETTING_OPACITY,s.idleOpacity,v=>parseFloat(v).toFixed(2));const tS=win.querySelector(`#${IDS.SETTING_THEME}`);if(tS)tS.value=s.theme;const cDS=win.querySelector(`#${IDS.SETTING_COUNTRY_DISPLAY_MODE}`);if(cDS)cDS.value=s.countryDisplayMode;const hC=win.querySelector(`#${IDS.SETTING_HOVER}`),oI=win.querySelector(`#${IDS.SETTING_OPACITY}`);if(hC&&oI){hC.checked=s.hoverMode;const iHE=s.hoverMode;oI.disabled=!iHE;const oC=oI.closest('div');if(oC){oC.style.opacity=iHE?'1':'0.6';oC.style.pointerEvents=iHE?'auto':'none';}} const hideLogoCb = win.querySelector(`#${IDS.SETTING_HIDE_GOOGLE_LOGO}`); if (hideLogoCb) hideLogoCb.checked = s.hideGoogleLogoWhenExpanded; }
  1841. // Internal helper to populate features settings tab
  1842. function _populateFeatureSettings_internal(win,s,renderFn){win.querySelectorAll(`#${IDS.TAB_PANE_FEATURES} input[type="checkbox"][data-${DATA_ATTR.SECTION_ID}]`)?.forEach(cb=>{const sId=cb.getAttribute(`data-${DATA_ATTR.SECTION_ID}`);if(sId&&s.visibleSections.hasOwnProperty(sId)){cb.checked=s.visibleSections[sId];}else if(sId&&_defaultSettingsRef.visibleSections.hasOwnProperty(sId)){cb.checked=_defaultSettingsRef.visibleSections[sId]??false;}}); const siteSearchCheckboxModeEl = win.querySelector(`#${IDS.SETTING_SITE_SEARCH_CHECKBOX_MODE}`); if(siteSearchCheckboxModeEl) siteSearchCheckboxModeEl.checked = s.enableSiteSearchCheckboxMode; const showFaviconsEl = win.querySelector(`#${IDS.SETTING_SHOW_FAVICONS}`); if(showFaviconsEl) showFaviconsEl.checked = s.showFaviconsForSiteSearch; const filetypeSearchCheckboxModeEl = win.querySelector(`#${IDS.SETTING_FILETYPE_SEARCH_CHECKBOX_MODE}`); if(filetypeSearchCheckboxModeEl) filetypeSearchCheckboxModeEl.checked = s.enableFiletypeCheckboxMode; renderFn(s);}
  1843. // Internal helper to set the active settings tab
  1844. function _initializeActiveSettingsTab_internal(){if(!_settingsWindow)return;const tC=_settingsWindow.querySelector(`.${CSS.SETTINGS_TABS}`),cC=_settingsWindow.querySelector(`.${CSS.SETTINGS_TAB_CONTENT}`);if(!tC||!cC)return;const aTB=tC.querySelector(`.${CSS.TAB_BUTTON}.${CSS.ACTIVE}`);const tT=(aTB&&aTB.dataset[DATA_ATTR.TAB])?aTB.dataset[DATA_ATTR.TAB]:'general';tC.querySelectorAll(`.${CSS.TAB_BUTTON}`).forEach(b=>b.classList.toggle(CSS.ACTIVE,b.dataset[DATA_ATTR.TAB]===tT));cC.querySelectorAll(`.${CSS.TAB_PANE}`).forEach(p=>p.classList.toggle(CSS.ACTIVE,p.dataset[DATA_ATTR.TAB]===tT));}
  1845. // Load settings from GM_storage
  1846. function _loadFromStorage(){try{const s=GM_getValue(STORAGE_KEY,'{}');return JSON.parse(s||'{}');}catch(e){console.error(`${LOG_PREFIX} Error loading/parsing settings:`,e);return{};}}
  1847. // Migrate settings from older versions if necessary (specific to displayLanguages/Countries)
  1848. function _migrateToDisplayArraysIfNecessary(settings) {
  1849. const displayTypes = [
  1850. { displayKey: 'displayLanguages', predefinedKey: 'language', customKey: 'customLanguages', defaultEnabled: defaultSettings.enabledPredefinedOptions.language },
  1851. { displayKey: 'displayCountries', predefinedKey: 'country', customKey: 'customCountries', defaultEnabled: defaultSettings.enabledPredefinedOptions.country }
  1852. ];
  1853. let migrationPerformed = false;
  1854. displayTypes.forEach(typeInfo => {
  1855. // Check if migration is needed: displayKey array is missing/empty AND there are old-style predefined/custom options
  1856. if ((!settings[typeInfo.displayKey] || settings[typeInfo.displayKey].length === 0) &&
  1857. (
  1858. (settings.enabledPredefinedOptions && settings.enabledPredefinedOptions[typeInfo.predefinedKey]?.length > 0) ||
  1859. (settings[typeInfo.customKey] && settings[typeInfo.customKey]?.length > 0)
  1860. )
  1861. ) {
  1862. console.log(`${LOG_PREFIX} Migrating settings for ${typeInfo.displayKey}`);
  1863. migrationPerformed = true;
  1864. const newDisplayArray = [];
  1865. const addedValues = new Set(); // To avoid duplicates if a value exists in both predefined and custom
  1866.  
  1867. // 1. Add enabled predefined options first
  1868. const enabledPredefined = settings.enabledPredefinedOptions?.[typeInfo.predefinedKey] || typeInfo.defaultEnabled || [];
  1869. enabledPredefined.forEach(val => {
  1870. const predefinedOpt = PREDEFINED_OPTIONS[typeInfo.predefinedKey]?.find(p => p.value === val);
  1871. if (predefinedOpt && !addedValues.has(predefinedOpt.value)) {
  1872. newDisplayArray.push({
  1873. id: predefinedOpt.value, // Use value as a unique ID for predefined
  1874. text: _(predefinedOpt.textKey), // Store original text key
  1875. value: predefinedOpt.value,
  1876. type: 'predefined',
  1877. originalKey: predefinedOpt.textKey
  1878. });
  1879. addedValues.add(predefinedOpt.value);
  1880. }
  1881. });
  1882. // Sort predefined items by their translated text (current locale)
  1883. newDisplayArray.sort((a,b) => {
  1884. const textA = a.originalKey ? _(a.originalKey) : a.text;
  1885. const textB = b.originalKey ? _(b.originalKey) : b.text;
  1886. return textA.localeCompare(textB, LocalizationService.getCurrentLocale(), {sensitivity: 'base'})
  1887. });
  1888.  
  1889.  
  1890. // 2. Add custom options
  1891. const customItems = settings[typeInfo.customKey] || [];
  1892. customItems.forEach(customOpt => {
  1893. if (customOpt.value && !addedValues.has(customOpt.value)) { // Ensure custom value wasn't already added as predefined
  1894. newDisplayArray.push({
  1895. id: customOpt.value, // Use value as ID for custom, assume unique among customs
  1896. text: customOpt.text,
  1897. value: customOpt.value,
  1898. type: 'custom'
  1899. // No originalKey for custom items
  1900. });
  1901. addedValues.add(customOpt.value);
  1902. }
  1903. });
  1904. settings[typeInfo.displayKey] = newDisplayArray;
  1905. // Clear old enabledPredefinedOptions for this type as they are now in displayKey
  1906. if (settings.enabledPredefinedOptions) {
  1907. settings.enabledPredefinedOptions[typeInfo.predefinedKey] = [];
  1908. }
  1909. } else if (!settings[typeInfo.displayKey]) { // Ensure array exists if no migration needed
  1910. settings[typeInfo.displayKey] = JSON.parse(JSON.stringify(defaultSettings[typeInfo.displayKey] || []));
  1911. }
  1912. });
  1913. if (migrationPerformed) console.log(`${LOG_PREFIX} Migration to display arrays complete.`);
  1914. }
  1915. // Validate and merge saved settings with defaults
  1916. function _validateAndMergeSettings(saved){
  1917. let newSettings = JSON.parse(JSON.stringify(_defaultSettingsRef)); // Start with a deep copy of defaults
  1918. newSettings = Utils.mergeDeep(newSettings, saved); // Merge saved settings into the new copy
  1919.  
  1920. _validateAndMergeCoreSettings_internal(newSettings, saved, _defaultSettingsRef);
  1921. _validateAndMergeAppearanceSettings_internal(newSettings, saved, _defaultSettingsRef);
  1922. _validateAndMergeFeatureSettings_internal(newSettings, saved, _defaultSettingsRef);
  1923. _validateAndMergeCustomLists_internal(newSettings, saved, _defaultSettingsRef);
  1924.  
  1925. _migrateToDisplayArraysIfNecessary(newSettings); // IMPORTANT: Migrate before validating display arrays
  1926.  
  1927. // Validate displayLanguages and displayCountries (after potential migration)
  1928. ['displayLanguages', 'displayCountries'].forEach(displayKey => {
  1929. if (!Array.isArray(newSettings[displayKey])) {
  1930. newSettings[displayKey] = JSON.parse(JSON.stringify(_defaultSettingsRef[displayKey])) || [];
  1931. }
  1932. // Filter out invalid items
  1933. newSettings[displayKey] = newSettings[displayKey].filter(item =>
  1934. item && typeof item.id === 'string' &&
  1935. (item.type === 'predefined' ? (typeof item.text === 'string' && typeof item.originalKey === 'string') : typeof item.text === 'string') && // Text is always string, originalKey for predefined
  1936. typeof item.value === 'string' &&
  1937. (item.type === 'predefined' || item.type === 'custom') // Valid type
  1938. );
  1939. });
  1940.  
  1941.  
  1942. _validateAndMergePredefinedOptions_internal(newSettings, saved, _defaultSettingsRef); // Validate other predefined options
  1943. _finalizeSectionOrder_internal(newSettings, saved, _defaultSettingsRef); // Finalize section order
  1944.  
  1945. return newSettings;
  1946. }
  1947. // Sub-validation functions
  1948. function _validateAndMergeCoreSettings_internal(target,source,defaults){if(typeof target.sidebarPosition!=='object'||target.sidebarPosition===null||Array.isArray(target.sidebarPosition)){target.sidebarPosition=JSON.parse(JSON.stringify(defaults.sidebarPosition));}target.sidebarPosition.left=parseInt(target.sidebarPosition.left,10)||defaults.sidebarPosition.left;target.sidebarPosition.top=parseInt(target.sidebarPosition.top,10)||defaults.sidebarPosition.top;if(typeof target.sectionStates!=='object'||target.sectionStates===null||Array.isArray(target.sectionStates)){target.sectionStates={};}target.sidebarCollapsed=!!target.sidebarCollapsed;target.draggableHandleEnabled=typeof target.draggableHandleEnabled==='boolean'?target.draggableHandleEnabled:defaults.draggableHandleEnabled;target.interfaceLanguage=typeof source.interfaceLanguage==='string'?source.interfaceLanguage:defaults.interfaceLanguage;}
  1949. function _validateAndMergeAppearanceSettings_internal(target,source,defaults){target.sidebarWidth=Utils.clamp(parseInt(target.sidebarWidth,10)||defaults.sidebarWidth,90,270);target.fontSize=Utils.clamp(parseFloat(target.fontSize)||defaults.fontSize,8,24);target.headerIconSize=Utils.clamp(parseFloat(target.headerIconSize)||defaults.headerIconSize,8,32);target.verticalSpacingMultiplier=Utils.clamp(parseFloat(target.verticalSpacingMultiplier)||defaults.verticalSpacingMultiplier,0.05,1.5);target.idleOpacity=Utils.clamp(parseFloat(target.idleOpacity)||defaults.idleOpacity,0.1,1.0);target.hoverMode=!!target.hoverMode;const validThemes=['system','light','dark','minimal-light','minimal-dark'];if(target.theme==='minimal')target.theme='minimal-light';else if(!validThemes.includes(target.theme))target.theme=defaults.theme; target.hideGoogleLogoWhenExpanded = typeof source.hideGoogleLogoWhenExpanded === 'boolean' ? source.hideGoogleLogoWhenExpanded : defaults.hideGoogleLogoWhenExpanded;}
  1950. function _validateAndMergeFeatureSettings_internal(target,source,defaults){if(typeof target.visibleSections!=='object'||target.visibleSections===null||Array.isArray(target.visibleSections)){target.visibleSections=JSON.parse(JSON.stringify(defaults.visibleSections));}const validSectionIDs=new Set(ALL_SECTION_DEFINITIONS.map(def=>def.id));Object.keys(defaults.visibleSections).forEach(id=>{if(!validSectionIDs.has(id)){console.warn(`${LOG_PREFIX} Invalid section ID in defaultSettings.visibleSections: ${id}`);}else if(typeof target.visibleSections[id]!=='boolean'){target.visibleSections[id]=defaults.visibleSections[id]??true;}});const validSectionModes=['remember','expandAll','collapseAll'];if(!validSectionModes.includes(target.sectionDisplayMode))target.sectionDisplayMode=defaults.sectionDisplayMode;target.accordionMode=!!target.accordionMode; target.enableSiteSearchCheckboxMode = typeof target.enableSiteSearchCheckboxMode === 'boolean' ? target.enableSiteSearchCheckboxMode : defaults.enableSiteSearchCheckboxMode; target.showFaviconsForSiteSearch = typeof target.showFaviconsForSiteSearch === 'boolean' ? target.showFaviconsForSiteSearch : defaults.showFaviconsForSiteSearch; target.enableFiletypeCheckboxMode = typeof target.enableFiletypeCheckboxMode === 'boolean' ? target.enableFiletypeCheckboxMode : defaults.enableFiletypeCheckboxMode; const validButtonLocations=['header','topBlock','tools','none'];if(!validButtonLocations.includes(target.resetButtonLocation))target.resetButtonLocation=defaults.resetButtonLocation;if(!validButtonLocations.includes(target.verbatimButtonLocation))target.verbatimButtonLocation=defaults.verbatimButtonLocation;if(!validButtonLocations.includes(target.advancedSearchLinkLocation))target.advancedSearchLinkLocation=defaults.advancedSearchLinkLocation; if (!validButtonLocations.includes(target.personalizationButtonLocation)) { target.personalizationButtonLocation = defaults.personalizationButtonLocation; } if (!validButtonLocations.includes(target.googleScholarShortcutLocation)) { target.googleScholarShortcutLocation = defaults.googleScholarShortcutLocation; } if (!validButtonLocations.includes(target.googleTrendsShortcutLocation)) { target.googleTrendsShortcutLocation = defaults.googleTrendsShortcutLocation; } if (!validButtonLocations.includes(target.googleDatasetSearchShortcutLocation)) { target.googleDatasetSearchShortcutLocation = defaults.googleDatasetSearchShortcutLocation; } const validCountryDisplayModes=['iconAndText','textOnly','iconOnly'];if(!validCountryDisplayModes.includes(target.countryDisplayMode))target.countryDisplayMode=defaults.countryDisplayMode;}
  1951. function _validateAndMergeCustomLists_internal(target,source,defaults){const listKeys=['favoriteSites','customLanguages','customTimeRanges','customFiletypes','customCountries'];listKeys.forEach(key=>{target[key]=Array.isArray(target[key])?target[key].filter(item=>item&&typeof item.text==='string'&&typeof item[key==='favoriteSites'?'url':'value']==='string'&&item.text.trim()!==''&&item[key==='favoriteSites'?'url':'value'].trim()!==''):JSON.parse(JSON.stringify(defaults[key]));});}
  1952. function _validateAndMergePredefinedOptions_internal(target,source,defaults){
  1953. // This function now primarily handles 'time' and 'filetype' enabledPredefinedOptions,
  1954. // as 'language' and 'country' are managed via their displayArrays.
  1955. target.enabledPredefinedOptions = target.enabledPredefinedOptions || {};
  1956. ['time', 'filetype'].forEach(type => {
  1957. if (!target.enabledPredefinedOptions[type] || !Array.isArray(target.enabledPredefinedOptions[type])) {
  1958. target.enabledPredefinedOptions[type] = JSON.parse(JSON.stringify(defaults.enabledPredefinedOptions[type] || []));
  1959. }
  1960. const savedTypeOptions = source.enabledPredefinedOptions?.[type];
  1961. if (PREDEFINED_OPTIONS[type] && Array.isArray(savedTypeOptions)) {
  1962. const validValues = new Set(PREDEFINED_OPTIONS[type].map(opt => opt.value));
  1963. target.enabledPredefinedOptions[type] = savedTypeOptions.filter(val => typeof val === 'string' && validValues.has(val));
  1964. } else if (!PREDEFINED_OPTIONS[type]) { // If this type has no predefined options in the script
  1965. target.enabledPredefinedOptions[type] = [];
  1966. }
  1967. // If savedTypeOptions is undefined or not an array, it keeps the default or already merged value.
  1968. });
  1969. // Ensure language and country are empty in enabledPredefinedOptions as they use display arrays
  1970. if (target.displayLanguages && target.enabledPredefinedOptions) target.enabledPredefinedOptions.language = [];
  1971. if (target.displayCountries && target.enabledPredefinedOptions) target.enabledPredefinedOptions.country = [];
  1972. }
  1973. function _finalizeSectionOrder_internal(target,source,defaults){const finalOrder=[];const currentVisibleOrderSet=new Set();const validSectionIDs=new Set(ALL_SECTION_DEFINITIONS.map(def=>def.id));const orderSource=(Array.isArray(source.sidebarSectionOrder)&&source.sidebarSectionOrder.length>0)?source.sidebarSectionOrder:defaults.sidebarSectionOrder;orderSource.forEach(id=>{if(typeof id==='string'&&validSectionIDs.has(id)&&target.visibleSections[id]===true&&!currentVisibleOrderSet.has(id)){finalOrder.push(id);currentVisibleOrderSet.add(id);}});defaults.sidebarSectionOrder.forEach(id=>{if(typeof id==='string'&&validSectionIDs.has(id)&&target.visibleSections[id]===true&&!currentVisibleOrderSet.has(id)){finalOrder.push(id);}});target.sidebarSectionOrder=finalOrder;}
  1974. // Event handlers for live updates in settings UI (put in an object for organization)
  1975. const _sEH_internal = {
  1976. // Sliders
  1977. [IDS.SETTING_WIDTH]:(t,vS)=>_hSLI(t,'sidebarWidth',vS,90,270,5),
  1978. [IDS.SETTING_FONT_SIZE]:(t,vS)=>_hSLI(t,'fontSize',vS,8,24,0.5,v=>parseFloat(v).toFixed(1)),
  1979. [IDS.SETTING_HEADER_ICON_SIZE]:(t,vS)=>_hSLI(t,'headerIconSize',vS,8,32,0.5,v=>parseFloat(v).toFixed(1)),
  1980. [IDS.SETTING_VERTICAL_SPACING]:(t,vS)=>_hSLI(t,'verticalSpacingMultiplier',vS,0.05,1.5,0.05,v=>`x ${parseFloat(v).toFixed(2)}`),
  1981. [IDS.SETTING_OPACITY]:(t,vS)=>_hSLI(t,'idleOpacity',vS,0.1,1.0,0.05,v=>parseFloat(v).toFixed(2)),
  1982. // Selects & Checkboxes
  1983. [IDS.SETTING_INTERFACE_LANGUAGE]:(t)=>{const nL=t.value;if(_currentSettings.interfaceLanguage!==nL){_currentSettings.interfaceLanguage=nL;LocalizationService.updateActiveLocale(_currentSettings);_initMenuCommands_cb();publicApi.populateWindow();_buildSidebarUI_cb();}}, // Re-populate and re-build for language change
  1984. [IDS.SETTING_THEME]:(t)=>{_currentSettings.theme=t.value;_applySettingsToSidebar_cb(_currentSettings);}, // Apply theme change
  1985. [IDS.SETTING_HOVER]:(t)=>{_currentSettings.hoverMode=t.checked;const oI=_settingsWindow.querySelector(`#${IDS.SETTING_OPACITY}`);if(oI){const iHE=_currentSettings.hoverMode;oI.disabled=!iHE;const oC=oI.closest('div');if(oC){oC.style.opacity=iHE?'1':'0.6';oC.style.pointerEvents=iHE?'auto':'none';}}_applySettingsToSidebar_cb(_currentSettings);},
  1986. [IDS.SETTING_DRAGGABLE]:(t)=>{_currentSettings.draggableHandleEnabled=t.checked;_applySettingsToSidebar_cb(_currentSettings);DragManager.setDraggable(t.checked, sidebar, sidebar?.querySelector(`.${CSS.DRAG_HANDLE}`), _currentSettings, debouncedSaveSettings);},
  1987. [IDS.SETTING_ACCORDION]:(t)=>{const sMS=_settingsWindow.querySelector(`#${IDS.SETTING_SECTION_MODE}`);if(sMS?.value==='remember')_currentSettings.accordionMode=t.checked;else{t.checked=false;_currentSettings.accordionMode=false;}_applySettingsToSidebar_cb(_currentSettings);_applySectionCollapseStates_cb();},
  1988. [IDS.SETTING_SECTION_MODE]:(t)=>{_currentSettings.sectionDisplayMode=t.value;const aC=_settingsWindow.querySelector(`#${IDS.SETTING_ACCORDION}`);if(aC){const iRM=t.value==='remember';aC.disabled=!iRM; if(aC.parentElement.querySelector(`.${CSS.SETTING_VALUE_HINT}`)) aC.parentElement.querySelector(`.${CSS.SETTING_VALUE_HINT}`).style.color = iRM ? '' : 'grey'; if(!iRM){aC.checked=false;_currentSettings.accordionMode=false;}else{/* Restore previous accordion state if switching back to remember */ aC.checked=_settingsBackup?.accordionMode??_currentSettings.accordionMode??_defaultSettingsRef.accordionMode;_currentSettings.accordionMode=aC.checked;}}_applySettingsToSidebar_cb(_currentSettings);_applySectionCollapseStates_cb();},
  1989. [IDS.SETTING_RESET_LOCATION]:(t)=>{_currentSettings.resetButtonLocation=t.value;_buildSidebarUI_cb();},
  1990. [IDS.SETTING_VERBATIM_LOCATION]:(t)=>{_currentSettings.verbatimButtonLocation=t.value;_buildSidebarUI_cb();},
  1991. [IDS.SETTING_ADV_SEARCH_LOCATION]:(t)=>{_currentSettings.advancedSearchLinkLocation=t.value;_buildSidebarUI_cb();},
  1992. [IDS.SETTING_PERSONALIZE_LOCATION]: (target) => { _currentSettings.personalizationButtonLocation = target.value; _buildSidebarUI_cb(); },
  1993. [IDS.SETTING_SCHOLAR_LOCATION]: (target) => { _currentSettings.googleScholarShortcutLocation = target.value; _buildSidebarUI_cb(); },
  1994. [IDS.SETTING_TRENDS_LOCATION]: (target) => { _currentSettings.googleTrendsShortcutLocation = target.value; _buildSidebarUI_cb(); },
  1995. [IDS.SETTING_DATASET_SEARCH_LOCATION]: (target) => { _currentSettings.googleDatasetSearchShortcutLocation = target.value; _buildSidebarUI_cb(); },
  1996. [IDS.SETTING_SITE_SEARCH_CHECKBOX_MODE]: (target) => { _currentSettings.enableSiteSearchCheckboxMode = target.checked; _buildSidebarUI_cb(); },
  1997. [IDS.SETTING_SHOW_FAVICONS]: (target) => { _currentSettings.showFaviconsForSiteSearch = target.checked; _buildSidebarUI_cb(); },
  1998. [IDS.SETTING_FILETYPE_SEARCH_CHECKBOX_MODE]: (target) => { _currentSettings.enableFiletypeCheckboxMode = target.checked; _buildSidebarUI_cb(); },
  1999. [IDS.SETTING_COUNTRY_DISPLAY_MODE]:(t)=>{_currentSettings.countryDisplayMode=t.value;_buildSidebarUI_cb();},
  2000. [IDS.SETTING_HIDE_GOOGLE_LOGO]:(t)=>{_currentSettings.hideGoogleLogoWhenExpanded=t.checked;_applySettingsToSidebar_cb(_currentSettings);},
  2001. };
  2002. // Helper for handling slider input event
  2003. function _hSLI(t,sK,vS,min,max,step,fFn=v=>v){const v=Utils.clamp((step===1||step===5)?parseInt(t.value,10):parseFloat(t.value),min,max);if(isNaN(v))_currentSettings[sK]=_defaultSettingsRef[sK];else _currentSettings[sK]=v;if(vS)vS.textContent=fFn(_currentSettings[sK]);_applySettingsToSidebar_cb(_currentSettings);}
  2004. // Unified live update handler
  2005. function _lUH_internal(e){const t=e.target;if(!t)return;const sI=t.id;const vS=(t.type==='range')?t.parentNode.querySelector(`.${CSS.RANGE_VALUE}`):null;if(_sEH_internal[sI]){if(t.type==='range')_sEH_internal[sI](t,vS);else _sEH_internal[sI](t);}}
  2006.  
  2007. const publicApi = {
  2008. initialize: function(defaultSettingsObj, applyCb, buildCb, collapseCb, menuCb, renderOrderCb) { if(_isInitialized) return; _defaultSettingsRef = defaultSettingsObj; _applySettingsToSidebar_cb = applyCb; _buildSidebarUI_cb = buildCb; _applySectionCollapseStates_cb = collapseCb; _initMenuCommands_cb = menuCb; _renderSectionOrderList_ext_cb = renderOrderCb; this.load(); this.buildSkeleton(); _isInitialized = true; },
  2009. load: function(){ const s=_loadFromStorage(); _currentSettings=_validateAndMergeSettings(s); LocalizationService.updateActiveLocale(_currentSettings);},
  2010. save: function(logContext='SaveBtn'){
  2011. try {
  2012. // Consolidate custom items from displayLanguages/Countries back to customLanguages/Countries master lists
  2013. ['displayLanguages', 'displayCountries'].forEach(displayKey => {
  2014. const mapping = getListMapping(displayKey === 'displayLanguages' ? IDS.LANG_LIST : IDS.COUNTRIES_LIST);
  2015. if (mapping && mapping.customItemsMasterKey && _currentSettings[displayKey] && Array.isArray(_currentSettings[mapping.customItemsMasterKey])) {
  2016. const displayItems = _currentSettings[displayKey];
  2017. const currentDisplayCustomItems = displayItems.filter(item => item.type === 'custom');
  2018. const currentDisplayCustomItemValues = new Set(currentDisplayCustomItems.map(item => item.value));
  2019.  
  2020. // Filter master list: keep only those custom items that are still in the display list
  2021. const newMasterList = (_currentSettings[mapping.customItemsMasterKey] || [])
  2022. .filter(masterItem => currentDisplayCustomItemValues.has(masterItem.value))
  2023. .map(oldMasterItem => { // Update text from display item if it changed
  2024. const correspondingDisplayItem = currentDisplayCustomItems.find(d => d.value === oldMasterItem.value);
  2025. return correspondingDisplayItem ? { text: correspondingDisplayItem.text, value: oldMasterItem.value } : oldMasterItem;
  2026. });
  2027.  
  2028. // Add new custom items from display list to master if not already there
  2029. currentDisplayCustomItems.forEach(dispItem => {
  2030. if (!newMasterList.find(mi => mi.value === dispItem.value)) {
  2031. newMasterList.push({ text: dispItem.text, value: dispItem.value });
  2032. }
  2033. });
  2034. _currentSettings[mapping.customItemsMasterKey] = newMasterList;
  2035. }
  2036. });
  2037.  
  2038. GM_setValue(STORAGE_KEY, JSON.stringify(_currentSettings));
  2039. console.log(`${LOG_PREFIX} Settings saved by SM${logContext ? ` (${logContext})` : ''}.`);
  2040. _settingsBackup = JSON.parse(JSON.stringify(_currentSettings)); // Update backup after successful save
  2041. } catch (e) {
  2042. console.error(`${LOG_PREFIX} SM save error:`, e);
  2043. NotificationManager.show('alert_generic_error', { context: 'saving settings' }, 'error', 5000);
  2044. }
  2045. },
  2046. reset: function(){ if(confirm(_('confirm_reset_settings'))){ _currentSettings = JSON.parse(JSON.stringify(_defaultSettingsRef)); _migrateToDisplayArraysIfNecessary(_currentSettings); /* Ensure section order is reset if empty */ if(!_currentSettings.sidebarSectionOrder||_currentSettings.sidebarSectionOrder.length===0){ _currentSettings.sidebarSectionOrder = [..._defaultSettingsRef.sidebarSectionOrder]; } LocalizationService.updateActiveLocale(_currentSettings); this.populateWindow(); _applySettingsToSidebar_cb(_currentSettings); _buildSidebarUI_cb(); _initMenuCommands_cb(); _showGlobalMessage('alert_settings_reset_success',{},'success',4000);}},
  2047. resetAllFromMenu: function(){ if(confirm(_('confirm_reset_all_menu'))){ try{ GM_setValue(STORAGE_KEY,JSON.stringify(_defaultSettingsRef)); alert(_('alert_reset_all_menu_success')); }catch(e){ _showGlobalMessage('alert_reset_all_menu_fail',{},'error',0); /* Use alert as NotificationManager might not be ready */ }}},
  2048. getCurrentSettings: function(){ return _currentSettings;},
  2049. buildSkeleton: function(){ if(_settingsWindow)return; _settingsOverlay=document.createElement('div');_settingsOverlay.id=IDS.SETTINGS_OVERLAY;_settingsWindow=document.createElement('div');_settingsWindow.id=IDS.SETTINGS_WINDOW;const h=document.createElement('div');h.classList.add(CSS.SETTINGS_HEADER);h.innerHTML=`<h3>${_('settingsTitle')}</h3><button class="${CSS.SETTINGS_CLOSE_BTN}" title="${_('settings_close_button_title')}">${SVG_ICONS.close}</button>`;const mB=document.createElement('div');mB.id=IDS.SETTINGS_MESSAGE_BAR;mB.classList.add(CSS.MESSAGE_BAR);mB.style.display='none';const ts=document.createElement('div');ts.classList.add(CSS.SETTINGS_TABS);ts.innerHTML=`<button class="${CSS.TAB_BUTTON} ${CSS.ACTIVE}" data-${DATA_ATTR.TAB}="general">${_('settings_tab_general')}</button> <button class="${CSS.TAB_BUTTON}" data-${DATA_ATTR.TAB}="appearance">${_('settings_tab_appearance')}</button> <button class="${CSS.TAB_BUTTON}" data-${DATA_ATTR.TAB}="features">${_('settings_tab_features')}</button> <button class="${CSS.TAB_BUTTON}" data-${DATA_ATTR.TAB}="custom">${_('settings_tab_custom')}</button>`;const c=document.createElement('div');c.classList.add(CSS.SETTINGS_TAB_CONTENT);c.innerHTML=`<div class="${CSS.TAB_PANE} ${CSS.ACTIVE}" data-${DATA_ATTR.TAB}="general" id="${IDS.TAB_PANE_GENERAL}"></div><div class="${CSS.TAB_PANE}" data-${DATA_ATTR.TAB}="appearance" id="${IDS.TAB_PANE_APPEARANCE}"></div><div class="${CSS.TAB_PANE}" data-${DATA_ATTR.TAB}="features" id="${IDS.TAB_PANE_FEATURES}"></div><div class="${CSS.TAB_PANE}" data-${DATA_ATTR.TAB}="custom" id="${IDS.TAB_PANE_CUSTOM}"></div>`;const f=document.createElement('div');f.classList.add(CSS.SETTINGS_FOOTER);f.innerHTML=`<button class="${CSS.RESET_BUTTON}">${_('settings_reset_all_button')}</button><button class="${CSS.CANCEL_BUTTON}">${_('settings_cancel_button')}</button><button class="${CSS.SAVE_BUTTON}">${_('settings_save_button')}</button>`;_settingsWindow.appendChild(h);_settingsWindow.appendChild(mB);_settingsWindow.appendChild(ts);_settingsWindow.appendChild(c);_settingsWindow.appendChild(f);_settingsOverlay.appendChild(_settingsWindow);document.body.appendChild(_settingsOverlay);this.bindEvents();},
  2050. populateWindow: function(){
  2051. if(!_settingsWindow)return;
  2052. try {
  2053. // Update translatable texts first
  2054. _settingsWindow.querySelector(`.${CSS.SETTINGS_HEADER} h3`).textContent=_( 'settingsTitle');
  2055. _settingsWindow.querySelector(`.${CSS.SETTINGS_CLOSE_BTN}`).title=_( 'settings_close_button_title');
  2056. _settingsWindow.querySelector(`button[data-${DATA_ATTR.TAB}="general"]`).textContent=_( 'settings_tab_general');
  2057. _settingsWindow.querySelector(`button[data-${DATA_ATTR.TAB}="appearance"]`).textContent=_( 'settings_tab_appearance');
  2058. _settingsWindow.querySelector(`button[data-${DATA_ATTR.TAB}="features"]`).textContent=_( 'settings_tab_features');
  2059. _settingsWindow.querySelector(`button[data-${DATA_ATTR.TAB}="custom"]`).textContent=_( 'settings_tab_custom');
  2060. _settingsWindow.querySelector(`.${CSS.RESET_BUTTON}`).textContent=_( 'settings_reset_all_button');
  2061. _settingsWindow.querySelector(`.${CSS.CANCEL_BUTTON}`).textContent=_( 'settings_cancel_button');
  2062. _settingsWindow.querySelector(`.${CSS.SAVE_BUTTON}`).textContent=_( 'settings_save_button');
  2063.  
  2064. // Re-generate tab content HTML to reflect any language changes in labels
  2065. const paneGeneral = _settingsWindow.querySelector(`#${IDS.TAB_PANE_GENERAL}`); if(paneGeneral) paneGeneral.innerHTML = SettingsUIPaneGenerator.createGeneralPaneHTML();
  2066. const paneAppearance = _settingsWindow.querySelector(`#${IDS.TAB_PANE_APPEARANCE}`); if(paneAppearance) paneAppearance.innerHTML = SettingsUIPaneGenerator.createAppearancePaneHTML();
  2067. const paneFeatures = _settingsWindow.querySelector(`#${IDS.TAB_PANE_FEATURES}`); if(paneFeatures) paneFeatures.innerHTML = SettingsUIPaneGenerator.createFeaturesPaneHTML();
  2068. const paneCustom = _settingsWindow.querySelector(`#${IDS.TAB_PANE_CUSTOM}`); if(paneCustom) paneCustom.innerHTML = SettingsUIPaneGenerator.createCustomPaneHTML();
  2069.  
  2070. // Populate values into the newly generated/updated UI
  2071. _populateGeneralSettings_internal(_settingsWindow, _currentSettings);
  2072. _populateAppearanceSettings_internal(_settingsWindow, _currentSettings);
  2073. _populateFeatureSettings_internal(_settingsWindow, _currentSettings, _renderSectionOrderList_ext_cb); // Pass callback
  2074. ModalManager.resetEditStateGlobally(); // Reset modal edit state if any was active
  2075. _initializeActiveSettingsTab_internal(); // Ensure correct tab is active
  2076. this.bindLiveUpdateEvents(); // Re-bind for dynamic elements if panes were rebuilt
  2077. this.bindFeaturesTabEvents(); // Specifically for features tab dynamic elements
  2078. }catch(e){ _showGlobalMessage('alert_init_fail',{scriptName:SCRIPT_INTERNAL_NAME,error:"Settings UI pop err"},'error',0); }
  2079. },
  2080. show: function(){ if(!_settingsOverlay||!_settingsWindow)return;_settingsBackup = JSON.parse(JSON.stringify(_currentSettings));LocalizationService.updateActiveLocale(_currentSettings);this.populateWindow();applyThemeToElement(_settingsWindow, _currentSettings.theme);applyThemeToElement(_settingsOverlay, _currentSettings.theme);_settingsOverlay.style.display = 'flex';},
  2081. hide: function(isCancel = false){ if(!_settingsOverlay)return;ModalManager.resetEditStateGlobally();if(ModalManager.isModalOpen()) ModalManager.hide(true);_settingsOverlay.style.display = 'none';const messageBar = document.getElementById(IDS.SETTINGS_MESSAGE_BAR);if(messageBar) messageBar.style.display = 'none';if(isCancel && _settingsBackup && Object.keys(_settingsBackup).length > 0){ _currentSettings = JSON.parse(JSON.stringify(_settingsBackup));LocalizationService.updateActiveLocale(_currentSettings);this.populateWindow();_applySettingsToSidebar_cb(_currentSettings);_buildSidebarUI_cb();_initMenuCommands_cb();} else if(isCancel) { console.warn(`${LOG_PREFIX} SM: Cancelled, no backup to restore or backup was identical.`); }},
  2082. bindEvents: function(){
  2083. if(!_settingsWindow || _settingsWindow.dataset.eventsBound === 'true') return; // Prevent multiple bindings
  2084. _settingsWindow.querySelector(`.${CSS.SETTINGS_CLOSE_BTN}`)?.addEventListener('click', () => this.hide(true));
  2085. _settingsWindow.querySelector(`.${CSS.CANCEL_BUTTON}`)?.addEventListener('click', () => this.hide(true));
  2086. _settingsWindow.querySelector(`.${CSS.SAVE_BUTTON}`)?.addEventListener('click', () => { this.save(); LocalizationService.updateActiveLocale(_currentSettings); _initMenuCommands_cb(); _buildSidebarUI_cb(); this.hide(false); });
  2087. _settingsWindow.querySelector(`.${CSS.RESET_BUTTON}`)?.addEventListener('click', () => this.reset());
  2088. const tabsContainer = _settingsWindow.querySelector(`.${CSS.SETTINGS_TABS}`);
  2089. if(tabsContainer){ tabsContainer.addEventListener('click', e => { const targetButton = e.target.closest(`.${CSS.TAB_BUTTON}`); if(targetButton && !targetButton.classList.contains(CSS.ACTIVE)){ ModalManager.resetEditStateGlobally(); const tabToActivate = targetButton.dataset[DATA_ATTR.TAB]; if(!tabToActivate) return; tabsContainer.querySelectorAll(`.${CSS.TAB_BUTTON}`).forEach(b => b.classList.remove(CSS.ACTIVE)); targetButton.classList.add(CSS.ACTIVE); _settingsWindow.querySelector(`.${CSS.SETTINGS_TAB_CONTENT}`)?.querySelectorAll(`.${CSS.TAB_PANE}`)?.forEach(p => p.classList.remove(CSS.ACTIVE)); _settingsWindow.querySelector(`.${CSS.SETTINGS_TAB_CONTENT} .${CSS.TAB_PANE}[data-${DATA_ATTR.TAB}="${tabToActivate}"]`)?.classList.add(CSS.ACTIVE); } }); }
  2090. _settingsWindow.dataset.eventsBound = 'true';
  2091. // Bind "Manage..." buttons in Custom tab
  2092. const customTabPane = _settingsWindow.querySelector(`#${IDS.TAB_PANE_CUSTOM}`);
  2093. if(customTabPane){ customTabPane.addEventListener('click', (e) => { const manageButton = e.target.closest(`button.${CSS.MANAGE_CUSTOM_BUTTON}`); if(manageButton){ const manageType = manageButton.dataset[DATA_ATTR.MANAGE_TYPE]; if(manageType){ ModalManager.openManageCustomOptions( manageType, _currentSettings, PREDEFINED_OPTIONS, (updatedItemsArray, newEnabledPredefs, itemsArrayKey, predefinedOptKey, customItemsMasterKey, isSortableMixed, manageTypeFromCallback) => { if (itemsArrayKey) { _currentSettings[itemsArrayKey] = updatedItemsArray; } if (predefinedOptKey && newEnabledPredefs) { if (!_currentSettings.enabledPredefinedOptions) _currentSettings.enabledPredefinedOptions = {}; _currentSettings.enabledPredefinedOptions[predefinedOptKey] = newEnabledPredefs; } _buildSidebarUI_cb(); } ); } } }); }
  2094. },
  2095. bindLiveUpdateEvents: function(){ if(!_settingsWindow)return; _settingsWindow.querySelectorAll('input[type="range"]').forEach(el=>{ el.removeEventListener('input',_lUH_internal); el.addEventListener('input',_lUH_internal); }); _settingsWindow.querySelectorAll('select, input[type="checkbox"]:not([data-section-id])').forEach(el=>{ if(_sEH_internal[el.id]){ el.removeEventListener('change',_lUH_internal); el.addEventListener('change',_lUH_internal); } }); },
  2096. bindFeaturesTabEvents: function() { // For elements specific to the features tab that might be rebuilt
  2097. const featuresPane = _settingsWindow?.querySelector(`#${IDS.TAB_PANE_FEATURES}`); if (!featuresPane) return;
  2098. // Section visibility checkboxes
  2099. featuresPane.querySelectorAll(`input[type="checkbox"][data-${DATA_ATTR.SECTION_ID}]`).forEach(checkbox => { checkbox.removeEventListener('change', this._handleVisibleSectionChange); checkbox.addEventListener('change', this._handleVisibleSectionChange.bind(this)); });
  2100. // Site search checkbox mode
  2101. const siteSearchCheckboxModeEl = featuresPane.querySelector(`#${IDS.SETTING_SITE_SEARCH_CHECKBOX_MODE}`);
  2102. if (siteSearchCheckboxModeEl && _sEH_internal[IDS.SETTING_SITE_SEARCH_CHECKBOX_MODE]) {
  2103. siteSearchCheckboxModeEl.removeEventListener('change', _lUH_internal);
  2104. siteSearchCheckboxModeEl.addEventListener('change', _lUH_internal);
  2105. }
  2106. const showFaviconsEl = featuresPane.querySelector(`#${IDS.SETTING_SHOW_FAVICONS}`);
  2107. if (showFaviconsEl && _sEH_internal[IDS.SETTING_SHOW_FAVICONS]) {
  2108. showFaviconsEl.removeEventListener('change', _lUH_internal);
  2109. showFaviconsEl.addEventListener('change', _lUH_internal);
  2110. }
  2111. // Filetype search checkbox mode
  2112. const filetypeSearchCheckboxModeEl = featuresPane.querySelector(`#${IDS.SETTING_FILETYPE_SEARCH_CHECKBOX_MODE}`);
  2113. if (filetypeSearchCheckboxModeEl && _sEH_internal[IDS.SETTING_FILETYPE_SEARCH_CHECKBOX_MODE]) {
  2114. filetypeSearchCheckboxModeEl.removeEventListener('change', _lUH_internal);
  2115. filetypeSearchCheckboxModeEl.addEventListener('change', _lUH_internal);
  2116. }
  2117.  
  2118. // Section order list drag handler
  2119. const orderListElement = featuresPane.querySelector(`#${IDS.SIDEBAR_SECTION_ORDER_LIST}`);
  2120. if (orderListElement) { SectionOrderDragHandler.initialize(orderListElement, _currentSettings, () => { _renderSectionOrderList_ext_cb(_currentSettings); _buildSidebarUI_cb(); }); }
  2121. },
  2122. _handleVisibleSectionChange: function(e){ const target = e.target; const sectionId = target.getAttribute(`data-${DATA_ATTR.SECTION_ID}`); if (sectionId && _currentSettings.visibleSections.hasOwnProperty(sectionId)) { _currentSettings.visibleSections[sectionId] = target.checked; _finalizeSectionOrder_internal(_currentSettings, _currentSettings, _defaultSettingsRef); _renderSectionOrderList_ext_cb(_currentSettings); _buildSidebarUI_cb(); } },
  2123. };
  2124. return publicApi;
  2125. })();
  2126.  
  2127. // --- DragManager for sidebar positioning ---
  2128. const DragManager = (function() {
  2129. let _isDragging = false; let _dragStartX, _dragStartY, _sidebarStartX, _sidebarStartY;
  2130. let _sidebarElement, _handleElement; let _settingsManagerRef, _saveCallbackRef;
  2131.  
  2132. function _getEventCoordinates(e) { return (e.touches && e.touches.length > 0) ? { x: e.touches[0].clientX, y: e.touches[0].clientY } : { x: e.clientX, y: e.clientY }; }
  2133. function _startDrag(e) {
  2134. const currentSettings = _settingsManagerRef.getCurrentSettings();
  2135. if (!currentSettings.draggableHandleEnabled || currentSettings.sidebarCollapsed || (e.type === 'mousedown' && e.button !== 0)) { // Only main mouse button
  2136. return;
  2137. }
  2138. e.preventDefault(); // Prevent text selection, etc.
  2139. _isDragging = true;
  2140. const coords = _getEventCoordinates(e);
  2141. _dragStartX = coords.x;
  2142. _dragStartY = coords.y;
  2143. _sidebarStartX = _sidebarElement.offsetLeft;
  2144. _sidebarStartY = _sidebarElement.offsetTop;
  2145. _sidebarElement.style.cursor = 'grabbing';
  2146. _sidebarElement.style.userSelect = 'none'; // Prevent text selection during drag
  2147. document.body.style.cursor = 'grabbing'; // Change cursor for the whole body
  2148. }
  2149. function _drag(e) {
  2150. if (!_isDragging) return;
  2151. e.preventDefault();
  2152. const coords = _getEventCoordinates(e);
  2153. const dx = coords.x - _dragStartX;
  2154. const dy = coords.y - _dragStartY;
  2155. let newLeft = _sidebarStartX + dx;
  2156. let newTop = _sidebarStartY + dy;
  2157.  
  2158. // Constrain within viewport
  2159. const maxLeft = window.innerWidth - (_sidebarElement?.offsetWidth ?? 0); // Use offsetWidth if available
  2160. const maxTop = window.innerHeight - (_sidebarElement?.offsetHeight ?? 0);
  2161. newLeft = Utils.clamp(newLeft, 0, maxLeft);
  2162. newTop = Utils.clamp(newTop, MIN_SIDEBAR_TOP_POSITION, maxTop); // MIN_SIDEBAR_TOP_POSITION from global
  2163.  
  2164. if (_sidebarElement) {
  2165. _sidebarElement.style.left = `${newLeft}px`;
  2166. _sidebarElement.style.top = `${newTop}px`;
  2167. }
  2168. }
  2169. function _stopDrag() {
  2170. if (_isDragging) {
  2171. _isDragging = false;
  2172. if (_sidebarElement) {
  2173. _sidebarElement.style.cursor = 'default'; // Reset cursor on sidebar
  2174. _sidebarElement.style.userSelect = ''; // Re-enable text selection
  2175. }
  2176. document.body.style.cursor = ''; // Reset body cursor
  2177.  
  2178. const currentSettings = _settingsManagerRef.getCurrentSettings();
  2179. if (!currentSettings.sidebarPosition) currentSettings.sidebarPosition = {};
  2180. currentSettings.sidebarPosition.left = _sidebarElement.offsetLeft;
  2181. currentSettings.sidebarPosition.top = _sidebarElement.offsetTop;
  2182.  
  2183. if (typeof _saveCallbackRef === 'function') {
  2184. _saveCallbackRef('Drag Stop'); // Save settings
  2185. }
  2186. }
  2187. }
  2188. return {
  2189. init: function(sidebarEl, handleEl, settingsMgr, saveCb) {
  2190. _sidebarElement = sidebarEl;
  2191. _handleElement = handleEl;
  2192. _settingsManagerRef = settingsMgr;
  2193. _saveCallbackRef = saveCb;
  2194.  
  2195. if (_handleElement) {
  2196. _handleElement.addEventListener('mousedown', _startDrag);
  2197. _handleElement.addEventListener('touchstart', _startDrag, { passive: false }); // Use passive: false for preventDefault
  2198. }
  2199. document.addEventListener('mousemove', _drag);
  2200. document.addEventListener('touchmove', _drag, { passive: false });
  2201. document.addEventListener('mouseup', _stopDrag);
  2202. document.addEventListener('touchend', _stopDrag);
  2203. document.addEventListener('touchcancel', _stopDrag); // Handle cancellation (e.g. system interruption)
  2204. },
  2205. setDraggable: function(isEnabled, sidebarEl, handleEl) { // Update references if sidebar/handle changes
  2206. _sidebarElement = sidebarEl;
  2207. _handleElement = handleEl;
  2208. if (_handleElement) {
  2209. _handleElement.style.display = isEnabled ? 'block' : 'none'; // Show/hide handle based on setting
  2210. }
  2211. }
  2212. };
  2213. })();
  2214.  
  2215. // --- URLActionManager for applying filters via URL manipulation ---
  2216. const URLActionManager = (function() {
  2217. function _getURLObject() { try { return new URL(window.location.href); } catch (e) { console.error(`${LOG_PREFIX} Error creating URL object: `, e); return null; }}
  2218. function _navigateTo(url) { const urlString = url.toString(); window.location.href = urlString; }
  2219. function _setSearchParam(urlObj, paramName, value) { urlObj.searchParams.set(paramName, value); }
  2220. function _deleteSearchParam(urlObj, paramName) { urlObj.searchParams.delete(paramName); }
  2221. function _getTbsParts(urlObj) { const tbs = urlObj.searchParams.get('tbs'); return tbs ? tbs.split(',').filter(p => p.trim() !== '') : []; }
  2222. function _setTbsParam(urlObj, tbsPartsArray) { const newTbsValue = tbsPartsArray.join(','); if (newTbsValue) { _setSearchParam(urlObj, 'tbs', newTbsValue); } else { _deleteSearchParam(urlObj, 'tbs'); }}
  2223.  
  2224. return {
  2225. triggerResetFilters: function() {
  2226. try {
  2227. const u = _getURLObject(); if (!u) return;
  2228. const q = u.searchParams.get('q') || '';
  2229. const nP = new URLSearchParams(); // Start with fresh params
  2230.  
  2231. // Clean query: remove existing site: and filetype: operators
  2232. let cQ = q.replace(/\s*(?:\(\s*)?(?:(?:site|filetype):[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' ');
  2233. cQ = cQ.replace(/\s*(?:site|filetype):[\w.:\/~%?#=&+-]+\s*/gi, ' ');
  2234. cQ = cQ.replace(/\s\s+/g, ' ').trim();
  2235.  
  2236. if (cQ) { nP.set('q', cQ); }
  2237. u.search = nP.toString();
  2238.  
  2239. _deleteSearchParam(u, 'tbs'); _deleteSearchParam(u, 'lr'); _deleteSearchParam(u, 'cr');
  2240. _deleteSearchParam(u, 'as_filetype'); _deleteSearchParam(u, 'as_occt');
  2241. _navigateTo(u);
  2242. } catch (e) {
  2243. NotificationManager.show('alert_error_resetting_filters', {}, 'error', 5000);
  2244. }
  2245. },
  2246. triggerToggleVerbatim: function() { try { const u = _getURLObject(); if (!u) return; let tP = _getTbsParts(u); const vP = 'li:1'; const iCA = tP.includes(vP); tP = tP.filter(p => p !== vP); if (!iCA) { tP.push(vP); } _setTbsParam(u, tP); _navigateTo(u); } catch (e) { NotificationManager.show('alert_error_toggling_verbatim', {}, 'error', 5000); }},
  2247. isPersonalizationActive: function() { try { const currentUrl = _getURLObject(); if (!currentUrl) { return true; } return currentUrl.searchParams.get('pws') !== '0'; } catch (e) { console.warn(`${LOG_PREFIX} [URLActionManager.isPersonalizationActive] Error checking personalization status:`, e); return true; /* Assume active on error */ } },
  2248. triggerTogglePersonalization: function() { try { const u = _getURLObject(); if (!u) { return; } const personalizationCurrentlyActive = URLActionManager.isPersonalizationActive(); if (personalizationCurrentlyActive) { _setSearchParam(u, 'pws', '0'); } else { _deleteSearchParam(u, 'pws'); } _navigateTo(u); } catch (e) { NotificationManager.show('alert_error_toggling_personalization', {}, 'error', 5000); console.error(`${LOG_PREFIX} [URLActionManager.triggerTogglePersonalization] Error:`, e); } },
  2249. applyFilter: function(type, value) {
  2250. try {
  2251. const u = _getURLObject(); if (!u) return;
  2252. let tbsParts = _getTbsParts(u);
  2253. let processedTbsParts;
  2254.  
  2255. const isTimeFilter = type === 'qdr';
  2256. const isStandaloneParam = ['lr', 'cr', 'as_occt'].includes(type);
  2257.  
  2258. if (isTimeFilter) {
  2259. processedTbsParts = tbsParts.filter(p => !p.startsWith(`qdr:`) && !p.startsWith('cdr:') && !p.startsWith('cd_min:') && !p.startsWith('cd_max:'));
  2260. if (value !== '') processedTbsParts.push(`qdr:${value}`);
  2261. _setTbsParam(u, processedTbsParts);
  2262. } else if (isStandaloneParam) {
  2263. _deleteSearchParam(u, type);
  2264. if (value !== '' && !(type === 'as_occt' && value === 'any')) {
  2265. _setSearchParam(u, type, value);
  2266. }
  2267. } else if (type === 'as_filetype') {
  2268. const filetypesToApply = Utils.parseCombinedValue(value);
  2269. if (filetypesToApply.length > 1) {
  2270. this.applyCombinedFiletypeSearch(filetypesToApply);
  2271. return;
  2272. }
  2273. let currentQuery = u.searchParams.get('q') || '';
  2274. currentQuery = currentQuery.replace(/\s*(?:\(\s*)?(?:filetype:[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' ');
  2275. currentQuery = currentQuery.replace(/\s*filetype:[\w.:\/~%?#=&+-]+\s*/gi, ' ');
  2276. currentQuery = currentQuery.replace(/\s\s+/g, ' ').trim();
  2277.  
  2278. if (value !== '') {
  2279. _setSearchParam(u, 'q', (currentQuery + ` filetype:${filetypesToApply[0]}`).trim());
  2280. } else {
  2281. if(currentQuery) _setSearchParam(u, 'q', currentQuery);
  2282. else _deleteSearchParam(u, 'q');
  2283. }
  2284. _deleteSearchParam(u, 'as_filetype');
  2285. }
  2286. _navigateTo(u);
  2287. } catch (e) {
  2288. NotificationManager.show('alert_error_applying_filter', { type: type, value: value }, 'error', 5000);
  2289. }
  2290. },
  2291.  
  2292. applySiteSearch: function(siteCriteria) {
  2293. const sitesToSearch = Array.isArray(siteCriteria)
  2294. ? siteCriteria.flatMap(sc => Utils.parseCombinedValue(sc))
  2295. : Utils.parseCombinedValue(siteCriteria);
  2296. const uniqueSites = [...new Set(sitesToSearch.map(s => s.toLowerCase()))];
  2297.  
  2298. if (uniqueSites.length === 0) {
  2299. this.clearSiteSearch(); return;
  2300. }
  2301. try {
  2302. const u = _getURLObject(); if (!u) return;
  2303. let q = u.searchParams.get('q') || '';
  2304.  
  2305. q = q.replace(/\s*(?:\(\s*)?(?:(?:site|filetype):[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' ');
  2306. q = q.replace(/\s*(?:site|filetype):[\w.:\/~%?#=&+-]+\s*/gi, ' ');
  2307. q = q.replace(/\s\s+/g, ' ').trim();
  2308.  
  2309. let siteQueryPart = uniqueSites.map(s => `site:${s}`).join(' OR '); // Corrected to join with ' OR '
  2310.  
  2311. const nQ = `${q} ${siteQueryPart}`.trim();
  2312. _setSearchParam(u, 'q', nQ);
  2313. _deleteSearchParam(u, 'tbs'); _deleteSearchParam(u, 'lr'); _deleteSearchParam(u, 'cr');
  2314. _deleteSearchParam(u, 'as_filetype'); _deleteSearchParam(u, 'as_occt');
  2315. _navigateTo(u);
  2316. } catch (e) {
  2317. const siteForError = uniqueSites.join(', ');
  2318. NotificationManager.show('alert_error_applying_site_search', { site: siteForError }, 'error', 5000);
  2319. }
  2320. },
  2321. applyCombinedFiletypeSearch: function(filetypeCriteria) {
  2322. const filetypesToSearch = Array.isArray(filetypeCriteria)
  2323. ? filetypeCriteria.flatMap(fc => Utils.parseCombinedValue(fc))
  2324. : Utils.parseCombinedValue(filetypeCriteria);
  2325. const uniqueFiletypes = [...new Set(filetypesToSearch.map(f => f.toLowerCase()))];
  2326.  
  2327. if (uniqueFiletypes.length === 0) {
  2328. this.clearFiletypeSearch();
  2329. return;
  2330. }
  2331. try {
  2332. const u = _getURLObject(); if (!u) return;
  2333. let q = u.searchParams.get('q') || '';
  2334.  
  2335. q = q.replace(/\s*(?:\(\s*)?(?:(?:filetype|site):[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' ');
  2336. q = q.replace(/\s*(?:filetype|site):[\w.:\/~%?#=&+-]+\s*/gi, ' ');
  2337. q = q.replace(/\s\s+/g, ' ').trim();
  2338.  
  2339. let filetypeQueryPart = uniqueFiletypes.map(ft => `filetype:${ft}`).join(' OR '); // Corrected to join with ' OR '
  2340.  
  2341. const nQ = `${q} ${filetypeQueryPart}`.trim();
  2342. _setSearchParam(u, 'q', nQ);
  2343. _deleteSearchParam(u, 'as_filetype');
  2344. _navigateTo(u);
  2345. } catch (e) {
  2346. const ftForError = uniqueFiletypes.join(', ');
  2347. NotificationManager.show('alert_error_applying_filter', { type: 'filetype (combined)', value: ftForError }, 'error', 5000);
  2348. }
  2349. },
  2350. clearSiteSearch: function() {
  2351. try {
  2352. const u = _getURLObject(); if (!u) return;
  2353. const q = u.searchParams.get('q') || '';
  2354. let nQ = q.replace(/\s*(?:\(\s*)?(?:site:[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' ');
  2355. nQ = nQ.replace(/\s*site:[\w.:\/~%?#=&+-]+\s*/gi, ' ');
  2356. nQ = nQ.replace(/\s\s+/g, ' ').trim();
  2357. if (nQ) { _setSearchParam(u, 'q', nQ); } else { _deleteSearchParam(u, 'q'); }
  2358. _navigateTo(u);
  2359. } catch (e) {
  2360. NotificationManager.show('alert_error_clearing_site_search', {}, 'error', 5000);
  2361. }
  2362. },
  2363. clearFiletypeSearch: function() {
  2364. try {
  2365. const u = _getURLObject(); if (!u) return;
  2366. let q = u.searchParams.get('q') || '';
  2367. q = q.replace(/\s*(?:\(\s*)?(?:filetype:[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' ');
  2368. q = q.replace(/\s*filetype:[\w.:\/~%?#=&+-]+\s*/gi, ' ');
  2369. q = q.replace(/\s\s+/g, ' ').trim();
  2370. if (q) { _setSearchParam(u, 'q', q); } else { _deleteSearchParam(u, 'q'); }
  2371. _deleteSearchParam(u, 'as_filetype');
  2372. _navigateTo(u);
  2373. } catch (e) {
  2374. NotificationManager.show('alert_error_applying_filter', { type: 'filetype', value: '(clear)' }, 'error', 5000);
  2375. }
  2376. },
  2377. isVerbatimActive: function() { try { const currentUrl = _getURLObject(); if (!currentUrl) return false; return /li:1/.test(currentUrl.searchParams.get('tbs') || ''); } catch (e) { console.warn(`${LOG_PREFIX} Error checking verbatim status:`, e); return false; }},
  2378. applyDateRange: function(dateMinStr, dateMaxStr) { try { const url = _getURLObject(); if (!url) return; let dateTbsPart = 'cdr:1'; if (dateMinStr) { const [y, m, d] = dateMinStr.split('-'); dateTbsPart += `,cd_min:${m}/${d}/${y}`; } if (dateMaxStr) { const [y, m, d] = dateMaxStr.split('-'); dateTbsPart += `,cd_max:${m}/${d}/${y}`; } let tbsParts = _getTbsParts(url); let preservedTbsParts = tbsParts.filter(p => !p.startsWith('qdr:') && !p.startsWith('cdr:') && !p.startsWith('cd_min:') && !p.startsWith('cd_max:')); let newTbsParts = [...preservedTbsParts, dateTbsPart]; _setTbsParam(url, newTbsParts); _navigateTo(url); } catch (e) { NotificationManager.show('alert_error_applying_date', {}, 'error', 5000); }}
  2379. };
  2380. })();
  2381.  
  2382. // --- Global Style Injection ---
  2383. function addGlobalStyles() { if (typeof window.GSCS_Namespace !== 'undefined' && typeof window.GSCS_Namespace.stylesText === 'string') { const cleanedCSS = window.GSCS_Namespace.stylesText.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1').replace(/\n\s*\n/g, '\n'); GM_addStyle(cleanedCSS); } else { console.error(`${LOG_PREFIX} CRITICAL: CSS styles provider not found.`); if (typeof IDS !== 'undefined' && IDS.SIDEBAR) { GM_addStyle(`#${IDS.SIDEBAR} { border: 3px dashed red !important; padding: 15px !important; background: white !important; color: red !important; } #${IDS.SIDEBAR}::before { content: "Error: CSS Missing!"; }`);} } }
  2384. // --- System Theme Listener ---
  2385. function setupSystemThemeListener() { if (systemThemeMediaQuery && systemThemeMediaQuery._sidebarThemeListener) { try { systemThemeMediaQuery.removeEventListener('change', systemThemeMediaQuery._sidebarThemeListener); } catch (e) {} systemThemeMediaQuery._sidebarThemeListener = null; } if (window.matchMedia) { systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const listener = () => { const cs = SettingsManager.getCurrentSettings(); if (sidebar && cs.theme === 'system') { applyThemeToElement(sidebar, 'system'); } }; systemThemeMediaQuery.addEventListener('change', listener); systemThemeMediaQuery._sidebarThemeListener = listener; } }
  2386. // --- Sidebar Skeleton and UI Building ---
  2387. function buildSidebarSkeleton() { sidebar = document.createElement('div'); sidebar.id = IDS.SIDEBAR; const header = document.createElement('div'); header.classList.add(CSS.SIDEBAR_HEADER); const collapseBtn = document.createElement('button'); collapseBtn.id = IDS.COLLAPSE_BUTTON; collapseBtn.innerHTML = SVG_ICONS.chevronLeft; collapseBtn.title = _('sidebar_collapse_title'); const dragHandle = document.createElement('div'); dragHandle.classList.add(CSS.DRAG_HANDLE); dragHandle.title = _('sidebar_drag_title'); const settingsBtn = document.createElement('button'); settingsBtn.id = IDS.SETTINGS_BUTTON; settingsBtn.classList.add(CSS.SETTINGS_BUTTON); settingsBtn.innerHTML = SVG_ICONS.settings; settingsBtn.title = _('sidebar_settings_title'); header.appendChild(collapseBtn); header.appendChild(dragHandle); header.appendChild(settingsBtn); sidebar.appendChild(header); document.body.appendChild(sidebar); }
  2388. function applySettings(settingsToApply) {
  2389. if (!sidebar) return;
  2390. const currentSettings = settingsToApply || SettingsManager.getCurrentSettings();
  2391. let targetTop = currentSettings.sidebarPosition.top;
  2392. targetTop = Math.max(MIN_SIDEBAR_TOP_POSITION, targetTop);
  2393. sidebar.style.left = `${currentSettings.sidebarPosition.left}px`;
  2394. sidebar.style.top = `${targetTop}px`;
  2395. sidebar.style.setProperty('--sidebar-font-base-size', `${currentSettings.fontSize}px`);
  2396. sidebar.style.setProperty('--sidebar-header-icon-base-size', `${currentSettings.headerIconSize}px`);
  2397. sidebar.style.setProperty('--sidebar-spacing-multiplier', currentSettings.verticalSpacingMultiplier);
  2398. if (!currentSettings.sidebarCollapsed) { sidebar.style.width = `${currentSettings.sidebarWidth}px`; }
  2399. else { sidebar.style.width = '40px';}
  2400. applyThemeToElement(sidebar, currentSettings.theme);
  2401. if (sidebar._hoverListeners) { sidebar.removeEventListener('mouseenter', sidebar._hoverListeners.enter); sidebar.removeEventListener('mouseleave', sidebar._hoverListeners.leave); sidebar._hoverListeners = null; sidebar.style.opacity = '1';}
  2402. if (currentSettings.hoverMode && !currentSettings.sidebarCollapsed) { const idleOpacityValue = currentSettings.idleOpacity; sidebar.style.opacity = idleOpacityValue.toString(); const enterL = () => { if (!currentSettings.sidebarCollapsed) sidebar.style.opacity = '1'; }; const leaveL = () => { if (!currentSettings.sidebarCollapsed) sidebar.style.opacity = idleOpacityValue.toString(); }; sidebar.addEventListener('mouseenter', enterL); sidebar.addEventListener('mouseleave', leaveL); sidebar._hoverListeners = { enter: enterL, leave: leaveL }; }
  2403. else { sidebar.style.opacity = '1'; }
  2404. applySidebarCollapseVisuals(currentSettings.sidebarCollapsed);
  2405.  
  2406. // Apply Google Logo hiding logic
  2407. const googleLogo = document.querySelector('#logo'); // Updated selector
  2408. if (googleLogo) {
  2409. if (currentSettings.hideGoogleLogoWhenExpanded && !currentSettings.sidebarCollapsed) {
  2410. googleLogo.style.visibility = 'hidden';
  2411. } else {
  2412. googleLogo.style.visibility = 'visible';
  2413. }
  2414. }
  2415. }
  2416. function _parseTimeValueToMinutes(timeValue) { if (!timeValue || typeof timeValue !== 'string') return Infinity; const match = timeValue.match(/^([hdwmy])(\d*)$/i); if (!match) return Infinity; const unit = match[1].toLowerCase(); const number = parseInt(match[2] || '1', 10); if (isNaN(number)) return Infinity; switch (unit) { case 'h': return number * 60; case 'd': return number * 24 * 60; case 'w': return number * 7 * 24 * 60; case 'm': return number * 30 * 24 * 60; case 'y': return number * 365 * 24 * 60; default: return Infinity; } }
  2417.  
  2418. function _prepareFilterOptions(sectionId, scriptDefinedOptions, currentSettings, predefinedOptionsSource) {
  2419. const finalOptions = [];
  2420. const tempAddedValues = new Set(); // To track values already added to finalOptions
  2421. const sectionDef = ALL_SECTION_DEFINITIONS.find(s => s.id === sectionId);
  2422. if (!sectionDef) return [];
  2423.  
  2424. const isSortableMixedType = sectionDef.displayItemsKey && Array.isArray(currentSettings[sectionDef.displayItemsKey]);
  2425. const isFiletypeCheckboxModeActive = sectionId === 'sidebar-section-filetype' && currentSettings.enableFiletypeCheckboxMode;
  2426. const isSiteCheckboxModeActive = sectionId === 'sidebar-section-site-search' && currentSettings.enableSiteSearchCheckboxMode;
  2427. const isOccurrenceSection = sectionId === 'sidebar-section-occurrence';
  2428.  
  2429. // 1. Add "Any" type script-defined options or all script-defined for occurrence
  2430. if (scriptDefinedOptions) {
  2431. scriptDefinedOptions.forEach(opt => {
  2432. if (opt && typeof opt.textKey === 'string' && typeof opt.v === 'string') {
  2433. if (isOccurrenceSection) { // For occurrence, add all scriptDefined options in their defined order
  2434. const translatedText = _(opt.textKey);
  2435. finalOptions.push({ text: translatedText, value: opt.v, originalText: translatedText, isCustom: false, isAnyOption: (opt.v === '') });
  2436. tempAddedValues.add(opt.v);
  2437. } else if (opt.v === '') { // For other sections, only add "Any" option here
  2438. if (!((isFiletypeCheckboxModeActive && sectionId === 'sidebar-section-filetype') ||
  2439. (isSiteCheckboxModeActive && sectionId === 'sidebar-section-site-search'))) {
  2440. const translatedText = (sectionId === 'sidebar-section-site-search') ? _('filter_any_site') : _(opt.textKey);
  2441. finalOptions.push({ text: translatedText, value: opt.v, originalText: translatedText, isCustom: false, isAnyOption: true });
  2442. tempAddedValues.add(opt.v);
  2443. }
  2444. }
  2445. }
  2446. });
  2447. }
  2448. if (isOccurrenceSection) return finalOptions; // Occurrence section is fully prepared now
  2449.  
  2450.  
  2451. if (isSortableMixedType) { // For Language, Country (options come from displayLanguages/displayCountries)
  2452. const displayItems = currentSettings[sectionDef.displayItemsKey] || [];
  2453. displayItems.forEach(item => {
  2454. if (!tempAddedValues.has(item.value)) {
  2455. let displayText = item.text;
  2456. if (item.type === 'predefined' && item.originalKey) {
  2457. displayText = _(item.originalKey);
  2458. if (sectionId === 'sidebar-section-country') {
  2459. const parsed = Utils.parseIconAndText(displayText);
  2460. displayText = `${parsed.icon} ${parsed.text}`.trim();
  2461. }
  2462. }
  2463. finalOptions.push({ text: displayText, value: item.value, originalText: displayText, isCustom: item.type === 'custom' });
  2464. tempAddedValues.add(item.value);
  2465. }
  2466. });
  2467. } else { // For Time, Filetype (if not checkbox mode)
  2468. const predefinedKey = sectionDef.predefinedOptionsKey;
  2469. const customKey = sectionDef.customItemsKey;
  2470. const predefinedOptsFromSource = predefinedOptionsSource && predefinedKey ? (predefinedOptionsSource[predefinedKey] || []) : [];
  2471. const customOptsFromSettings = customKey ? (currentSettings[customKey] || []) : [];
  2472. let enabledPredefinedSystemVals;
  2473.  
  2474. if (isFiletypeCheckboxModeActive && sectionId === 'sidebar-section-filetype') {
  2475. enabledPredefinedSystemVals = currentSettings.enabledPredefinedOptions[predefinedKey] || [];
  2476. } else {
  2477. enabledPredefinedSystemVals = predefinedKey ? (currentSettings.enabledPredefinedOptions[predefinedKey] || []) : [];
  2478. }
  2479.  
  2480. const itemsForThisSection = [];
  2481. const enabledSet = new Set(enabledPredefinedSystemVals);
  2482.  
  2483. if (Array.isArray(predefinedOptsFromSource)) {
  2484. predefinedOptsFromSource.forEach(opt => {
  2485. if (opt && typeof opt.textKey === 'string' && typeof opt.value === 'string' && enabledSet.has(opt.value) && !tempAddedValues.has(opt.value)) {
  2486. const translatedText = _(opt.textKey);
  2487. itemsForThisSection.push({ text: translatedText, value: opt.value, originalText: translatedText, isCustom: false });
  2488. }
  2489. });
  2490. }
  2491.  
  2492. const validCustomOptions = Array.isArray(customOptsFromSettings) ? customOptsFromSettings.filter(cOpt => cOpt && typeof cOpt.text === 'string' && typeof cOpt.value === 'string') : [];
  2493. validCustomOptions.forEach(opt => {
  2494. if (!tempAddedValues.has(opt.value)){
  2495. itemsForThisSection.push({ text: opt.text, value: opt.value, originalText: opt.text, isCustom: true });
  2496. }
  2497. });
  2498.  
  2499. itemsForThisSection.forEach(opt => {
  2500. if (!tempAddedValues.has(opt.value)){
  2501. finalOptions.push(opt);
  2502. tempAddedValues.add(opt.value);
  2503. }
  2504. });
  2505. }
  2506.  
  2507. if (scriptDefinedOptions && !isOccurrenceSection) { // Add other non-empty script-defined if not occurrence
  2508. scriptDefinedOptions.forEach(opt => {
  2509. if (opt && typeof opt.textKey === 'string' && typeof opt.v === 'string' && opt.v !== '' && !tempAddedValues.has(opt.v)) {
  2510. const translatedText = _(opt.textKey);
  2511. finalOptions.push({ text: translatedText, value: opt.v, originalText: translatedText, isCustom: false });
  2512. tempAddedValues.add(opt.v);
  2513. }
  2514. });
  2515. }
  2516.  
  2517. // Sorting logic, skip for occurrence (already ordered) and mixed types
  2518. if (!isSortableMixedType && !isOccurrenceSection) {
  2519. let anyOptionToSortSeparately = null;
  2520. const anyOptionIdx = finalOptions.findIndex(opt => opt.isAnyOption === true);
  2521.  
  2522. if (anyOptionIdx !== -1) {
  2523. anyOptionToSortSeparately = finalOptions.splice(anyOptionIdx, 1)[0];
  2524. }
  2525.  
  2526. finalOptions.sort((a, b) => {
  2527. const isTimeSection = (sectionId === 'sidebar-section-time');
  2528. if (isTimeSection) {
  2529. const timeA = _parseTimeValueToMinutes(a.value);
  2530. const timeB = _parseTimeValueToMinutes(b.value);
  2531. if (timeA !== Infinity || timeB !== Infinity) {
  2532. if (timeA !== timeB) return timeA - timeB;
  2533. }
  2534. }
  2535. const sTA = a.originalText || a.text;
  2536. const sTB = b.originalText || b.text;
  2537. const sL = LocalizationService.getCurrentLocale() === 'en' ? undefined : LocalizationService.getCurrentLocale();
  2538. return sTA.localeCompare(sTB, sL, { numeric: true, sensitivity: 'base' });
  2539. });
  2540.  
  2541. if (anyOptionToSortSeparately) {
  2542. finalOptions.unshift(anyOptionToSortSeparately);
  2543. }
  2544. }
  2545. return finalOptions;
  2546. }
  2547. function _createFilterOptionElement(optionData, filterParam, isCountrySection, countryDisplayMode) { const optionElement = document.createElement('div'); optionElement.classList.add(CSS.FILTER_OPTION); const displayText = optionData.text; if (isCountrySection) { const { icon, text: countryTextOnly } = Utils.parseIconAndText(displayText); switch (countryDisplayMode) { case 'textOnly': optionElement.textContent = countryTextOnly || displayText; break; case 'iconOnly': if (icon) { optionElement.innerHTML = `<span class="country-icon-container">${icon}</span>`; } else { optionElement.textContent = countryTextOnly || displayText; } break; case 'iconAndText': default: if (icon) { const textPart = countryTextOnly || displayText.substring(icon.length).trim(); optionElement.innerHTML = `<span class="country-icon-container">${icon}</span>${textPart}`; } else { optionElement.textContent = displayText; } break; } } else { optionElement.textContent = displayText; } optionElement.title = `${displayText} (${filterParam}=${optionData.value || _('filter_clear_tooltip_suffix')})`; optionElement.dataset[DATA_ATTR.FILTER_TYPE] = filterParam; optionElement.dataset[DATA_ATTR.FILTER_VALUE] = optionData.value; return optionElement; }
  2548. function buildSidebarUI() { if (!sidebar) { console.error("Sidebar element not ready for buildSidebarUI"); return; } const currentSettings = SettingsManager.getCurrentSettings(); const header = sidebar.querySelector(`.${CSS.SIDEBAR_HEADER}`); if (!header) { console.error("Sidebar header not found in buildSidebarUI"); return; } sidebar.querySelectorAll(`#${IDS.FIXED_TOP_BUTTONS}, .${CSS.SIDEBAR_CONTENT_WRAPPER}`).forEach(el => el.remove()); header.querySelectorAll(`.${CSS.HEADER_BUTTON}:not(#${IDS.SETTINGS_BUTTON}):not(#${IDS.COLLAPSE_BUTTON}), a.${CSS.HEADER_BUTTON}`).forEach(el => el.remove()); const rBL = currentSettings.resetButtonLocation; const vBL = currentSettings.verbatimButtonLocation; const aSL = currentSettings.advancedSearchLinkLocation; const pznBL = currentSettings.personalizationButtonLocation; const schL = currentSettings.googleScholarShortcutLocation; const trnL = currentSettings.googleTrendsShortcutLocation; const dsL = currentSettings.googleDatasetSearchShortcutLocation; const settingsButtonRef = header.querySelector(`#${IDS.SETTINGS_BUTTON}`); _buildSidebarHeaderControls(header, settingsButtonRef, rBL, vBL, aSL, pznBL, schL, trnL, dsL, _createAdvancedSearchElementHTML, _createPersonalizationButtonHTML, _createScholarShortcutHTML, _createTrendsShortcutHTML, _createDatasetSearchShortcutHTML, currentSettings); const fixedTopControlsContainer = _buildSidebarFixedTopControls(rBL, vBL, aSL, pznBL, schL, trnL, dsL, _createAdvancedSearchElementHTML, _createPersonalizationButtonHTML, _createScholarShortcutHTML, _createTrendsShortcutHTML, _createDatasetSearchShortcutHTML, currentSettings); if (fixedTopControlsContainer) { header.after(fixedTopControlsContainer); } const contentWrapper = document.createElement('div'); contentWrapper.classList.add(CSS.SIDEBAR_CONTENT_WRAPPER); const sectionDefinitionsMap = new Map(ALL_SECTION_DEFINITIONS.map(def => [def.id, def])); const sectionsFragment = _buildSidebarSections(sectionDefinitionsMap, rBL, vBL, aSL, pznBL, schL, trnL, dsL, _createAdvancedSearchElementHTML, _createPersonalizationButtonHTML, _createScholarShortcutHTML, _createTrendsShortcutHTML, _createDatasetSearchShortcutHTML, currentSettings, PREDEFINED_OPTIONS); contentWrapper.appendChild(sectionsFragment); sidebar.appendChild(contentWrapper); _initializeSidebarEventListenersAndStates(); }
  2549. function _buildSidebarSections(sectionDefinitionMap, rBL, vBL, aSL, pznBL, schL, trnL, dsL, advSearchFn, personalizeBtnFn, scholarFn, trendsFn, datasetSearchFn, currentSettings, PREDEFINED_OPTIONS_REF) { const contentFragment = document.createDocumentFragment(); currentSettings.sidebarSectionOrder.forEach(sectionId => { if (!currentSettings.visibleSections[sectionId]) return; const sectionData = sectionDefinitionMap.get(sectionId); if (!sectionData) { console.warn(`${LOG_PREFIX} No definition for section ID: ${sectionId}`); return; } let sectionElement = null; const sectionTitleKey = sectionData.titleKey; const sectionIdForDisplay = sectionData.id; switch (sectionData.type) { case 'filter': sectionElement = createFilterSection(sectionIdForDisplay, sectionTitleKey, sectionData.scriptDefined, sectionData.param, currentSettings, PREDEFINED_OPTIONS_REF, currentSettings.countryDisplayMode); break; case 'filetype': sectionElement = _createFiletypeSectionElement(sectionIdForDisplay, sectionTitleKey, sectionData.scriptDefined, sectionData.param, currentSettings, PREDEFINED_OPTIONS_REF); break; case 'date': sectionElement = _createDateSectionElement(sectionIdForDisplay, sectionTitleKey); break; case 'site': sectionElement = _createSiteSearchSectionElement(sectionIdForDisplay, sectionTitleKey, currentSettings); break; case 'tools': sectionElement = _createToolsSectionElement( sectionIdForDisplay, sectionTitleKey, rBL, vBL, aSL, pznBL, schL, trnL, dsL, advSearchFn, personalizeBtnFn, scholarFn, trendsFn, datasetSearchFn ); break; default: console.warn(`${LOG_PREFIX} Unknown section type: ${sectionData.type} for ID: ${sectionIdForDisplay}`); break; } if (sectionElement) contentFragment.appendChild(sectionElement); }); return contentFragment; }
  2550. function createFilterSection(id, titleKey, scriptDefinedOptions, filterParam, currentSettings, predefinedOptionsSource, countryDisplayMode) { if (!sidebar) return null; const { section, sectionContent, sectionTitle } = _createSectionShell(id, titleKey); sectionTitle.textContent = _(titleKey); const fragment = document.createDocumentFragment(); const isCountrySection = (id === 'sidebar-section-country'); const combinedOptions = _prepareFilterOptions(id, scriptDefinedOptions, currentSettings, predefinedOptionsSource); combinedOptions.forEach(option => { fragment.appendChild(_createFilterOptionElement(option, filterParam, isCountrySection, countryDisplayMode)); }); sectionContent.innerHTML = ''; sectionContent.appendChild(fragment); if (!sectionContent.dataset.filterClickListenerAttached) { sectionContent.addEventListener('click', function(event) { const target = event.target.closest(`.${CSS.FILTER_OPTION}`); if (target && target.classList.contains(CSS.FILTER_OPTION)) { event.preventDefault(); const clickedFilterType = target.dataset[DATA_ATTR.FILTER_TYPE]; const clickedFilterValue = target.dataset[DATA_ATTR.FILTER_VALUE]; if (typeof clickedFilterType !== 'undefined' && typeof clickedFilterValue !== 'undefined') { this.querySelectorAll(`.${CSS.FILTER_OPTION}`).forEach(opt => opt.classList.remove(CSS.SELECTED)); target.classList.add(CSS.SELECTED); if (clickedFilterValue === '' || (clickedFilterType === 'as_occt' && clickedFilterValue === 'any') ) { const defaultVal = (clickedFilterType === 'as_occt') ? 'any' : ''; const anyOpt = this.querySelector(`.${CSS.FILTER_OPTION}[data-${DATA_ATTR.FILTER_VALUE}="${defaultVal}"]`); if (anyOpt) anyOpt.classList.add(CSS.SELECTED); } URLActionManager.applyFilter(clickedFilterType, clickedFilterValue); } } }); sectionContent.dataset.filterClickListenerAttached = 'true'; } return section; }
  2551.  
  2552. function _createSiteSearchSectionElement(sectionId, titleKey, currentSettings) {
  2553. const { section, sectionContent, sectionTitle } = _createSectionShell(sectionId, titleKey);
  2554. sectionTitle.textContent = _(titleKey);
  2555. populateSiteSearchList(sectionContent, currentSettings.favoriteSites, currentSettings.enableSiteSearchCheckboxMode, currentSettings.showFaviconsForSiteSearch);
  2556. return section;
  2557. }
  2558.  
  2559. function populateSiteSearchList(sectionContentElement, favoriteSitesArray, checkboxModeEnabled, showFaviconsEnabled) {
  2560. if (!sectionContentElement) { console.error("Site search section content element missing"); return; }
  2561. sectionContentElement.innerHTML = ''; // Clear previous content
  2562.  
  2563. const sites = Array.isArray(favoriteSitesArray) ? favoriteSitesArray : [];
  2564. const listFragment = document.createDocumentFragment(); // For LIs
  2565.  
  2566. // "Any Site" option (always a div, not part of UL)
  2567. const clearOptDiv = document.createElement('div');
  2568. clearOptDiv.classList.add(CSS.FILTER_OPTION);
  2569. clearOptDiv.id = IDS.CLEAR_SITE_SEARCH_OPTION;
  2570. clearOptDiv.title = _('tooltip_clear_site_search');
  2571. clearOptDiv.textContent = _('filter_any_site');
  2572. clearOptDiv.dataset[DATA_ATTR.FILTER_TYPE] = 'site_clear'; // Special type for click handler
  2573. sectionContentElement.appendChild(clearOptDiv);
  2574.  
  2575. const listElement = document.createElement('ul');
  2576. listElement.classList.add(CSS.CUSTOM_LIST);
  2577. if (checkboxModeEnabled) {
  2578. listElement.classList.add('checkbox-mode-enabled');
  2579. }
  2580.  
  2581. sites.forEach((site, index) => {
  2582. if (site?.text && site?.url) {
  2583. const li = document.createElement('li');
  2584. const siteValue = site.url; // Can be "site.com" or "site.com OR example.org"
  2585.  
  2586. const isGroup = siteValue.includes(' OR ');
  2587.  
  2588. if (checkboxModeEnabled) {
  2589. const checkbox = document.createElement('input');
  2590. const uniqueId = `site-cb-${index}-${Date.now()}`;
  2591. checkbox.id = uniqueId;
  2592. checkbox.type = 'checkbox';
  2593. checkbox.value = siteValue;
  2594. checkbox.classList.add(CSS.SITE_SEARCH_ITEM_CHECKBOX);
  2595. checkbox.dataset[DATA_ATTR.SITE_URL] = siteValue;
  2596. li.appendChild(checkbox);
  2597.  
  2598. const label = document.createElement('label');
  2599. label.htmlFor = uniqueId;
  2600. if (showFaviconsEnabled && !isGroup) {
  2601. const favicon = document.createElement('img');
  2602. favicon.src = `https://www.google.com/s2/favicons?sz=32&domain_url=${siteValue}`;
  2603. favicon.classList.add(CSS.FAVICON);
  2604. favicon.loading = 'lazy';
  2605. label.appendChild(favicon);
  2606. }
  2607. label.appendChild(document.createTextNode(site.text));
  2608. label.dataset[DATA_ATTR.SITE_URL] = siteValue;
  2609. label.title = _('tooltip_site_search', { siteUrl: siteValue.replace(/\s+OR\s+/gi, ', ') });
  2610. li.appendChild(label);
  2611. } else {
  2612. const divOpt = document.createElement('div');
  2613. divOpt.classList.add(CSS.FILTER_OPTION);
  2614. if (showFaviconsEnabled && !isGroup) {
  2615. const favicon = document.createElement('img');
  2616. favicon.src = `https://www.google.com/s2/favicons?sz=32&domain_url=${siteValue}`;
  2617. favicon.classList.add(CSS.FAVICON);
  2618. favicon.loading = 'lazy';
  2619. divOpt.appendChild(favicon);
  2620. }
  2621. divOpt.appendChild(document.createTextNode(site.text));
  2622. divOpt.dataset[DATA_ATTR.SITE_URL] = siteValue;
  2623. divOpt.title = _('tooltip_site_search', { siteUrl: siteValue.replace(/\s+OR\s+/gi, ', ') });
  2624. li.appendChild(divOpt);
  2625. }
  2626.  
  2627. listFragment.appendChild(li);
  2628. }
  2629. });
  2630. listElement.appendChild(listFragment);
  2631. sectionContentElement.appendChild(listElement);
  2632.  
  2633. if (checkboxModeEnabled) {
  2634. let applyButton = sectionContentElement.querySelector(`#${IDS.APPLY_SELECTED_SITES_BUTTON}`);
  2635. if (!applyButton) {
  2636. applyButton = document.createElement('button');
  2637. applyButton.id = IDS.APPLY_SELECTED_SITES_BUTTON;
  2638. applyButton.classList.add(CSS.TOOL_BUTTON, CSS.APPLY_SITES_BUTTON);
  2639. applyButton.textContent = _('tool_apply_selected_sites');
  2640. sectionContentElement.appendChild(applyButton);
  2641. }
  2642. applyButton.disabled = true; // Initially disabled
  2643. applyButton.style.display = 'none'; // Initially hidden
  2644. }
  2645. // Attach event listener if not already attached
  2646. if (!sectionContentElement.dataset.siteSearchClickListenerAttached) {
  2647. sectionContentElement.dataset.siteSearchClickListenerAttached = 'true';
  2648. sectionContentElement.addEventListener('click', (event) => {
  2649. const target = event.target;
  2650. const currentSettings = SettingsManager.getCurrentSettings(); // Get current settings
  2651. const isCheckboxMode = currentSettings.enableSiteSearchCheckboxMode;
  2652. const clearSiteOpt = target.closest(`#${IDS.CLEAR_SITE_SEARCH_OPTION}`);
  2653.  
  2654. if (clearSiteOpt) {
  2655. URLActionManager.clearSiteSearch();
  2656. // Update UI: deselect all, select "Any Site"
  2657. sectionContentElement.querySelectorAll(`.${CSS.FILTER_OPTION}.${CSS.SELECTED}, label.${CSS.SELECTED}`).forEach(o => o.classList.remove(CSS.SELECTED));
  2658. clearSiteOpt.classList.add(CSS.SELECTED);
  2659. if (isCheckboxMode) {
  2660. sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}`).forEach(cb => cb.checked = false);
  2661. _updateApplySitesButtonState(sectionContentElement);
  2662. }
  2663. } else if (isCheckboxMode) {
  2664. const labelElement = target.closest('label');
  2665. if (labelElement && labelElement.dataset[DATA_ATTR.SITE_URL]) {
  2666. event.preventDefault(); // Prevent default label behavior which might toggle checkbox twice
  2667. const siteUrlOrCombined = labelElement.dataset[DATA_ATTR.SITE_URL];
  2668. // Single-select behavior when clicking a label in checkbox mode
  2669. sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}`).forEach(cb => {
  2670. const correspondingLabel = sectionContentElement.querySelector(`label[for="${cb.id}"]`);
  2671. cb.checked = (cb.value === siteUrlOrCombined); // Check only the clicked one
  2672. if(correspondingLabel) correspondingLabel.classList.toggle(CSS.SELECTED, cb.checked);
  2673. });
  2674. URLActionManager.applySiteSearch(siteUrlOrCombined);
  2675. _updateApplySitesButtonState(sectionContentElement); // Update button based on single selection
  2676. sectionContentElement.querySelector(`#${IDS.CLEAR_SITE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2677. }
  2678. } else { // Not checkbox mode, or click was not on a label in checkbox mode
  2679. const siteOptionDiv = target.closest(`div.${CSS.FILTER_OPTION}:not(#${IDS.CLEAR_SITE_SEARCH_OPTION})`);
  2680. if (siteOptionDiv && siteOptionDiv.dataset[DATA_ATTR.SITE_URL]) {
  2681. const siteUrlOrCombined = siteOptionDiv.dataset[DATA_ATTR.SITE_URL];
  2682. sectionContentElement.querySelectorAll(`.${CSS.FILTER_OPTION}.${CSS.SELECTED}`).forEach(o => o.classList.remove(CSS.SELECTED));
  2683. URLActionManager.applySiteSearch(siteUrlOrCombined);
  2684. siteOptionDiv.classList.add(CSS.SELECTED);
  2685. sectionContentElement.querySelector(`#${IDS.CLEAR_SITE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2686. }
  2687. }
  2688. });
  2689.  
  2690. // Change listener for checkboxes in checkbox mode
  2691. if (checkboxModeEnabled) {
  2692. sectionContentElement.addEventListener('change', (event) => {
  2693. if (event.target.matches(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}`)) {
  2694. _updateApplySitesButtonState(sectionContentElement); // Update button visibility/state
  2695. const label = sectionContentElement.querySelector(`label[for="${event.target.id}"]`);
  2696. if (label) label.classList.toggle(CSS.SELECTED, event.target.checked);
  2697. if (event.target.checked) { // If any checkbox is checked, "Any Site" is no longer selected
  2698. sectionContentElement.querySelector(`#${IDS.CLEAR_SITE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2699. }
  2700. }
  2701. });
  2702. // Listener for the "Apply Selected Sites" button
  2703. const applyBtn = sectionContentElement.querySelector(`#${IDS.APPLY_SELECTED_SITES_BUTTON}`);
  2704. if(applyBtn && !applyBtn.dataset[DATA_ATTR.LISTENER_ATTACHED]){
  2705. applyBtn.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true';
  2706. applyBtn.addEventListener('click', () => {
  2707. const selectedValuesFromCheckboxes = [];
  2708. sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}:checked`).forEach(cb => {
  2709. selectedValuesFromCheckboxes.push(cb.value); // cb.value contains the site URL or combined "OR" string
  2710. });
  2711. if (selectedValuesFromCheckboxes.length > 0) {
  2712. URLActionManager.applySiteSearch(selectedValuesFromCheckboxes);
  2713. sectionContentElement.querySelector(`#${IDS.CLEAR_SITE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2714. }
  2715. });
  2716. }
  2717. }
  2718. }
  2719. }
  2720.  
  2721. function _createFiletypeSectionElement(sectionId, titleKey, scriptDefinedOptions, filterParam, currentSettings, predefinedOptionsSource) {
  2722. const { section, sectionContent, sectionTitle } = _createSectionShell(sectionId, titleKey);
  2723. sectionTitle.textContent = _(titleKey);
  2724. populateFiletypeList(sectionContent, scriptDefinedOptions, currentSettings, predefinedOptionsSource, filterParam);
  2725. return section;
  2726. }
  2727.  
  2728. function populateFiletypeList(sectionContentElement, scriptDefinedOpts, currentSettings, predefinedOptsSource, filterParam) {
  2729. if (!sectionContentElement) { console.error("Filetype section content element missing"); return; }
  2730. sectionContentElement.innerHTML = ''; // Clear previous content
  2731.  
  2732. const checkboxModeEnabled = currentSettings.enableFiletypeCheckboxMode;
  2733. // Get all options (script-defined "Any", enabled predefined, custom)
  2734. const combinedOptions = _prepareFilterOptions('sidebar-section-filetype', scriptDefinedOpts, currentSettings, predefinedOptsSource);
  2735. const listFragment = document.createDocumentFragment(); // For LIs
  2736.  
  2737. // "Any Format" option (always a div, not part of UL)
  2738. const clearOptDiv = document.createElement('div');
  2739. clearOptDiv.classList.add(CSS.FILTER_OPTION);
  2740. clearOptDiv.id = IDS.CLEAR_FILETYPE_SEARCH_OPTION;
  2741. clearOptDiv.title = _('filter_clear_tooltip_suffix'); // Generic clear tooltip
  2742. clearOptDiv.textContent = _('filter_any_format');
  2743. clearOptDiv.dataset[DATA_ATTR.FILTER_TYPE] = 'filetype_clear'; // Special type for click handler
  2744. sectionContentElement.appendChild(clearOptDiv);
  2745.  
  2746. const listElement = document.createElement('ul');
  2747. listElement.classList.add(CSS.CUSTOM_LIST);
  2748. if (checkboxModeEnabled) {
  2749. listElement.classList.add('checkbox-mode-enabled');
  2750. }
  2751.  
  2752. combinedOptions.forEach((option, index) => {
  2753. if (option.isAnyOption) return; // "Any Format" is handled by the div above
  2754.  
  2755. const li = document.createElement('li');
  2756. const filetypeValue = option.value; // Can be single "pdf" or combined "pdf OR docx"
  2757.  
  2758. if (checkboxModeEnabled) {
  2759. const checkbox = document.createElement('input');
  2760. checkbox.type = 'checkbox';
  2761. checkbox.id = `ft-cb-${index}-${Date.now()}`; // Unique ID
  2762. checkbox.value = filetypeValue;
  2763. checkbox.classList.add(CSS.FILETYPE_SEARCH_ITEM_CHECKBOX);
  2764. checkbox.dataset[DATA_ATTR.FILETYPE_VALUE] = filetypeValue;
  2765. li.appendChild(checkbox);
  2766.  
  2767. const label = document.createElement('label');
  2768. label.htmlFor = checkbox.id; // Match checkbox ID
  2769. label.dataset[DATA_ATTR.FILETYPE_VALUE] = filetypeValue;
  2770. label.title = `${option.text} (${filterParam}=${filetypeValue.replace(/\s+OR\s+/gi, ', ')})`;
  2771. label.textContent = option.text;
  2772. li.appendChild(label);
  2773. } else { // Radio-button like behavior with divs
  2774. const divOpt = document.createElement('div');
  2775. divOpt.classList.add(CSS.FILTER_OPTION);
  2776. divOpt.dataset[DATA_ATTR.FILTER_TYPE] = filterParam; // e.g., "as_filetype"
  2777. divOpt.dataset[DATA_ATTR.FILTER_VALUE] = filetypeValue;
  2778. divOpt.title = `${option.text} (${filterParam}=${filetypeValue.replace(/\s+OR\s+/gi, ', ')})`;
  2779. divOpt.textContent = option.text;
  2780. li.appendChild(divOpt);
  2781. }
  2782. listFragment.appendChild(li);
  2783. });
  2784. listElement.appendChild(listFragment);
  2785. sectionContentElement.appendChild(listElement);
  2786.  
  2787. if (checkboxModeEnabled) {
  2788. let applyButton = sectionContentElement.querySelector(`#${IDS.APPLY_SELECTED_FILETYPES_BUTTON}`);
  2789. if(!applyButton) { // Create if doesn't exist
  2790. applyButton = document.createElement('button');
  2791. applyButton.id = IDS.APPLY_SELECTED_FILETYPES_BUTTON;
  2792. applyButton.classList.add(CSS.TOOL_BUTTON, CSS.APPLY_FILETYPES_BUTTON);
  2793. applyButton.textContent = _('tool_apply_selected_filetypes');
  2794. sectionContentElement.appendChild(applyButton);
  2795. }
  2796. applyButton.disabled = true; // Initially disabled
  2797. applyButton.style.display = 'none'; // Initially hidden
  2798. }
  2799.  
  2800. // Attach event listener if not already attached
  2801. if (!sectionContentElement.dataset.filetypeClickListenerAttached) {
  2802. sectionContentElement.dataset.filetypeClickListenerAttached = 'true';
  2803. sectionContentElement.addEventListener('click', (event) => {
  2804. const target = event.target;
  2805. const isCheckboxMode = SettingsManager.getCurrentSettings().enableFiletypeCheckboxMode;
  2806. const clearFiletypeOpt = target.closest(`#${IDS.CLEAR_FILETYPE_SEARCH_OPTION}`);
  2807.  
  2808. if (clearFiletypeOpt) {
  2809. URLActionManager.clearFiletypeSearch();
  2810. sectionContentElement.querySelectorAll(`.${CSS.FILTER_OPTION}.${CSS.SELECTED}, label.${CSS.SELECTED}`).forEach(o => o.classList.remove(CSS.SELECTED));
  2811. clearFiletypeOpt.classList.add(CSS.SELECTED);
  2812. if (isCheckboxMode) {
  2813. sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}`).forEach(cb => cb.checked = false);
  2814. _updateApplyFiletypesButtonState(sectionContentElement);
  2815. }
  2816. } else if (isCheckboxMode) {
  2817. const labelElement = target.closest('label');
  2818. if (labelElement && labelElement.dataset[DATA_ATTR.FILETYPE_VALUE]) {
  2819. event.preventDefault();
  2820. const filetypeValueOrCombined = labelElement.dataset[DATA_ATTR.FILETYPE_VALUE];
  2821. // Single-select behavior when clicking a label in checkbox mode
  2822. sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}`).forEach(cb => {
  2823. const correspondingLabel = sectionContentElement.querySelector(`label[for="${cb.id}"]`);
  2824. cb.checked = (cb.value === filetypeValueOrCombined);
  2825. if(correspondingLabel) correspondingLabel.classList.toggle(CSS.SELECTED, cb.checked);
  2826. });
  2827. URLActionManager.applyCombinedFiletypeSearch(filetypeValueOrCombined);
  2828. _updateApplyFiletypesButtonState(sectionContentElement);
  2829. sectionContentElement.querySelector(`#${IDS.CLEAR_FILETYPE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2830. }
  2831. } else { // Not checkbox mode
  2832. const optionDiv = target.closest(`div.${CSS.FILTER_OPTION}:not(#${IDS.CLEAR_FILETYPE_SEARCH_OPTION})`);
  2833. if (optionDiv && optionDiv.dataset[DATA_ATTR.FILTER_VALUE]) {
  2834. const clickedFilterType = optionDiv.dataset[DATA_ATTR.FILTER_TYPE]; // Should be "as_filetype"
  2835. const clickedFilterValueOrCombined = optionDiv.dataset[DATA_ATTR.FILTER_VALUE];
  2836. sectionContentElement.querySelectorAll(`.${CSS.FILTER_OPTION}.${CSS.SELECTED}`).forEach(o => o.classList.remove(CSS.SELECTED));
  2837. optionDiv.classList.add(CSS.SELECTED);
  2838. URLActionManager.applyFilter(clickedFilterType, clickedFilterValueOrCombined); // applyFilter handles single or combined
  2839. sectionContentElement.querySelector(`#${IDS.CLEAR_FILETYPE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2840. }
  2841. }
  2842. });
  2843.  
  2844. // Change listener for checkboxes in checkbox mode
  2845. if (checkboxModeEnabled) {
  2846. sectionContentElement.addEventListener('change', (event) => {
  2847. if (event.target.matches(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}`)) {
  2848. _updateApplyFiletypesButtonState(sectionContentElement);
  2849. const label = sectionContentElement.querySelector(`label[for="${event.target.id}"]`);
  2850. if (label) label.classList.toggle(CSS.SELECTED, event.target.checked);
  2851. if (event.target.checked) {
  2852. sectionContentElement.querySelector(`#${IDS.CLEAR_FILETYPE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2853. }
  2854. }
  2855. });
  2856. // Listener for the "Apply Selected" button
  2857. const applyBtn = sectionContentElement.querySelector(`#${IDS.APPLY_SELECTED_FILETYPES_BUTTON}`);
  2858. if (applyBtn && !applyBtn.dataset[DATA_ATTR.LISTENER_ATTACHED]) {
  2859. applyBtn.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true';
  2860. applyBtn.addEventListener('click', () => {
  2861. const selectedValuesFromCheckboxes = [];
  2862. sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}:checked`).forEach(cb => {
  2863. selectedValuesFromCheckboxes.push(cb.value); // cb.value contains the filetype or combined "OR" string
  2864. });
  2865. if (selectedValuesFromCheckboxes.length > 0) {
  2866. URLActionManager.applyCombinedFiletypeSearch(selectedValuesFromCheckboxes);
  2867. sectionContentElement.querySelector(`#${IDS.CLEAR_FILETYPE_SEARCH_OPTION}`)?.classList.remove(CSS.SELECTED);
  2868. }
  2869. });
  2870. }
  2871. }
  2872. }
  2873. }
  2874.  
  2875. function _updateApplySitesButtonState(sectionContentElement) {
  2876. if (!sectionContentElement) return;
  2877. const applyButton = sectionContentElement.querySelector(`#${IDS.APPLY_SELECTED_SITES_BUTTON}`);
  2878. if (!applyButton) return;
  2879. const checkedCount = sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}:checked`).length;
  2880. applyButton.disabled = checkedCount === 0;
  2881. applyButton.style.display = checkedCount > 0 ? 'inline-flex' : 'none'; // Use inline-flex if that's how tool-buttons are displayed
  2882. }
  2883.  
  2884. function _updateApplyFiletypesButtonState(sectionContentElement) {
  2885. if (!sectionContentElement) return;
  2886. const applyButton = sectionContentElement.querySelector(`#${IDS.APPLY_SELECTED_FILETYPES_BUTTON}`);
  2887. if (!applyButton) return;
  2888. const checkedCount = sectionContentElement.querySelectorAll(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}:checked`).length;
  2889. applyButton.disabled = checkedCount === 0;
  2890. applyButton.style.display = checkedCount > 0 ? 'inline-flex' : 'none';
  2891. }
  2892.  
  2893. // Render the sortable list of sections in the settings UI
  2894. function renderSectionOrderList(settingsRef) { const settingsWindowEl = document.getElementById(IDS.SETTINGS_WINDOW); const orderListElement = settingsWindowEl?.querySelector(`#${IDS.SIDEBAR_SECTION_ORDER_LIST}`); if (!orderListElement) return; orderListElement.innerHTML = ''; const currentSettings = settingsRef || SettingsManager.getCurrentSettings(); const visibleOrderedSections = currentSettings.sidebarSectionOrder.filter(id => currentSettings.visibleSections[id]); if (visibleOrderedSections.length === 0) { orderListElement.innerHTML = `<li><span style="font-style:italic;color:var(--settings-tab-color);">${_('settings_no_orderable_sections')}</span></li>`; return; } const fragment = document.createDocumentFragment(); visibleOrderedSections.forEach((sectionId) => { const definition = ALL_SECTION_DEFINITIONS.find(def => def.id === sectionId); const displayName = definition ? _(definition.titleKey) : sectionId; const listItem = document.createElement('li'); listItem.dataset.sectionId = sectionId; listItem.draggable = true; const dragIconSpan = document.createElement('span'); dragIconSpan.classList.add(CSS.DRAG_ICON); dragIconSpan.innerHTML = SVG_ICONS.dragGrip; listItem.appendChild(dragIconSpan); const nameSpan = document.createElement('span'); nameSpan.textContent = displayName; listItem.appendChild(nameSpan); fragment.appendChild(listItem); }); orderListElement.appendChild(fragment); }
  2895. function _initMenuCommands() { if (typeof GM_registerMenuCommand === 'function') { const openSettingsText = _('menu_open_settings'); const resetAllText = _('menu_reset_all_settings'); if (typeof GM_unregisterMenuCommand === 'function') { try { GM_unregisterMenuCommand(openSettingsText); } catch (e) {} try { GM_unregisterMenuCommand(resetAllText); } catch (e) {} } GM_registerMenuCommand(openSettingsText, SettingsManager.show.bind(SettingsManager)); GM_registerMenuCommand(resetAllText, SettingsManager.resetAllFromMenu.bind(SettingsManager)); } }
  2896. function _createSectionShell(id, titleKey) { const section = document.createElement('div'); section.id = id; section.classList.add(CSS.SIDEBAR_SECTION); const sectionTitle = document.createElement('div'); sectionTitle.classList.add(CSS.SECTION_TITLE); sectionTitle.textContent = _(titleKey); section.appendChild(sectionTitle); const sectionContent = document.createElement('div'); sectionContent.classList.add(CSS.SECTION_CONTENT); section.appendChild(sectionContent); return { section, sectionContent, sectionTitle }; }
  2897. function _createDateSectionElement(sectionId, titleKey) { const { section, sectionContent, sectionTitle } = _createSectionShell(sectionId, titleKey); sectionTitle.textContent = _(titleKey); const today = new Date(); const yyyy = today.getFullYear(); const mm = String(today.getMonth() + 1).padStart(2, '0'); const dd = String(today.getDate()).padStart(2, '0'); const todayString = `${yyyy}-${mm}-${dd}`; sectionContent.innerHTML = `<label class="${CSS.DATE_INPUT_LABEL}" for="${IDS.DATE_MIN}">${_('date_range_from')}</label>` + `<input type="date" class="${CSS.DATE_INPUT}" id="${IDS.DATE_MIN}" max="${todayString}">` + `<label class="${CSS.DATE_INPUT_LABEL}" for="${IDS.DATE_MAX}">${_('date_range_to')}</label>` + `<input type="date" class="${CSS.DATE_INPUT}" id="${IDS.DATE_MAX}" max="${todayString}">` + `<span id="${IDS.DATE_RANGE_ERROR_MSG}" class="${CSS.DATE_RANGE_ERROR_MSG} ${CSS.INPUT_ERROR_MESSAGE}"></span>` + `<button class="${CSS.TOOL_BUTTON} apply-date-range">${_('tool_apply_date')}</button>`; return section; }
  2898. function _createStandardButton({ id = null, className, svgIcon, textContent = null, title, clickHandler, isActive = false }) { const button = document.createElement('button'); if (id) button.id = id; button.classList.add(className); if (isActive) button.classList.add(CSS.ACTIVE); button.title = title; let content = svgIcon || ''; if (textContent) { content = svgIcon ? `${svgIcon} ${textContent}` : textContent; } button.innerHTML = content.trim(); if (clickHandler) { if (!button.dataset[DATA_ATTR.LISTENER_ATTACHED]) { button.addEventListener('click', clickHandler); button.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true'; } } return button; }
  2899. function _createPersonalizationButtonHTML(forLocation = 'tools') { const personalizationActive = URLActionManager.isPersonalizationActive(); const isIconOnlyLocation = (forLocation === 'header'); const svgIcon = SVG_ICONS.personalization || ''; const displayText = !isIconOnlyLocation ? _('tool_personalization_toggle') : ''; const titleKey = personalizationActive ? 'tooltip_toggle_personalization_off' : 'tooltip_toggle_personalization_on'; return _createStandardButton({ id: IDS.TOOL_PERSONALIZE, className: (forLocation === 'header') ? CSS.HEADER_BUTTON : CSS.TOOL_BUTTON, svgIcon: svgIcon, textContent: displayText, title: _(titleKey), clickHandler: () => URLActionManager.triggerTogglePersonalization(), isActive: personalizationActive }); }
  2900. function _createAdvancedSearchElementHTML(isButtonLike = false) { const el = document.createElement('a'); let iconHTML = SVG_ICONS.magnifyingGlass || ''; if (isButtonLike) { el.classList.add(CSS.TOOL_BUTTON); el.innerHTML = `${iconHTML} ${_('tool_advanced_search')}`; } else { el.classList.add(CSS.HEADER_BUTTON); el.innerHTML = iconHTML; } const baseUrl = "https://www.google.com/advanced_search"; let finalUrl = baseUrl; try { const currentFullUrl = Utils.getCurrentURL(); if (currentFullUrl) { const currentQuery = currentFullUrl.searchParams.get('q'); if (currentQuery) { let queryWithoutSite = currentQuery.replace(/\s*(?:\(\s*)?(?:site:[\w.:\/~%?#=&+-]+(?:\s+OR\s+|$))+[^)]*\)?\s*/gi, ' '); queryWithoutSite = queryWithoutSite.replace(/\s*site:[\w.:\/~%?#=&+-]+\s*/gi, ' '); queryWithoutSite = queryWithoutSite.replace(/\s\s+/g, ' ').trim(); if (queryWithoutSite) { finalUrl = `${baseUrl}?as_q=${encodeURIComponent(queryWithoutSite)}`; } } } } catch (e) { console.warn(`${LOG_PREFIX} Error constructing advanced search URL with query:`, e); } el.href = finalUrl; el.target = "_blank"; el.rel = "noopener noreferrer"; el.title = _('link_advanced_search_title'); return el; }
  2901. function _createScholarShortcutHTML(forLocation = 'tools') { const isIconOnlyLocation = (forLocation === 'header'); const svgIcon = SVG_ICONS.googleScholar || ''; const displayText = !isIconOnlyLocation ? _('tool_google_scholar') : ''; return _createStandardButton({ id: IDS.TOOL_GOOGLE_SCHOLAR, className: (forLocation === 'header') ? CSS.HEADER_BUTTON : CSS.TOOL_BUTTON, svgIcon: svgIcon, textContent: displayText, title: _('tooltip_google_scholar_search'), clickHandler: () => { try { const currentUrl = Utils.getCurrentURL(); if (currentUrl) { const query = currentUrl.searchParams.get('q'); if (query) { const scholarUrl = `https://scholar.google.com/scholar?q=${encodeURIComponent(query)}`; window.open(scholarUrl, '_blank'); } else { window.open('https://scholar.google.com/', '_blank'); NotificationManager.show('alert_no_keywords_for_shortcut', { service_name: _('service_name_google_scholar') }, 'info'); } } } catch (e) { console.error(`${LOG_PREFIX} Error opening Google Scholar:`, e); NotificationManager.show('alert_error_opening_link', { service_name: _('service_name_google_scholar') }, 'error'); } } }); }
  2902. function _createTrendsShortcutHTML(forLocation = 'tools') { const isIconOnlyLocation = (forLocation === 'header'); const svgIcon = SVG_ICONS.googleTrends || ''; const displayText = !isIconOnlyLocation ? _('tool_google_trends') : ''; return _createStandardButton({ id: IDS.TOOL_GOOGLE_TRENDS, className: (forLocation === 'header') ? CSS.HEADER_BUTTON : CSS.TOOL_BUTTON, svgIcon: svgIcon, textContent: displayText, title: _('tooltip_google_trends_search'), clickHandler: () => { try { const currentUrl = Utils.getCurrentURL(); if (currentUrl) { const query = currentUrl.searchParams.get('q'); if (query) { const trendsUrl = `https://trends.google.com/trends/explore?q=${encodeURIComponent(query)}`; window.open(trendsUrl, '_blank'); } else { window.open('https://trends.google.com/trends/', '_blank'); NotificationManager.show('alert_no_keywords_for_shortcut', { service_name: _('service_name_google_trends') }, 'info'); } } } catch (e) { console.error(`${LOG_PREFIX} Error opening Google Trends:`, e); NotificationManager.show('alert_error_opening_link', { service_name: _('service_name_google_trends') }, 'error'); } } }); }
  2903. function _createDatasetSearchShortcutHTML(forLocation = 'tools') { const isIconOnlyLocation = (forLocation === 'header'); const svgIcon = SVG_ICONS.googleDatasetSearch || ''; const displayText = !isIconOnlyLocation ? _('tool_google_dataset_search') : ''; return _createStandardButton({ id: IDS.TOOL_GOOGLE_DATASET_SEARCH, className: (forLocation === 'header') ? CSS.HEADER_BUTTON : CSS.TOOL_BUTTON, svgIcon: svgIcon, textContent: displayText, title: _('tooltip_google_dataset_search'), clickHandler: () => { try { const currentUrl = Utils.getCurrentURL(); if (currentUrl) { const query = currentUrl.searchParams.get('q'); if (query) { const datasetSearchUrl = `https://datasetsearch.research.google.com/search?query=${encodeURIComponent(query)}`; window.open(datasetSearchUrl, '_blank'); } else { window.open('https://datasetsearch.research.google.com/', '_blank'); NotificationManager.show('alert_no_keywords_for_shortcut', { service_name: _('service_name_google_dataset_search') }, 'info'); } } } catch (e) { console.error(`${LOG_PREFIX} Error opening Google Dataset Search:`, e); NotificationManager.show('alert_error_opening_link', { service_name: _('service_name_google_dataset_search') }, 'error'); } } }); }
  2904. function _buildSidebarHeaderControls(headerEl, settingsBtnRef, rBL, vBL, aSL, pznBL, schL, trnL, dsL, advSearchFn, personalizeBtnFn, scholarFn, trendsFn, datasetSearchFn, settings) { const verbatimActive = URLActionManager.isVerbatimActive(); const buttonsInOrder = []; if (aSL === 'header' && advSearchFn && settings.advancedSearchLinkLocation !== 'none') { buttonsInOrder.push(advSearchFn(false)); } if (schL === 'header' && scholarFn && settings.googleScholarShortcutLocation !== 'none') { buttonsInOrder.push(scholarFn('header')); } if (trnL === 'header' && trendsFn && settings.googleTrendsShortcutLocation !== 'none') { buttonsInOrder.push(trendsFn('header')); } if (dsL === 'header' && datasetSearchFn && settings.googleDatasetSearchShortcutLocation !== 'none') { buttonsInOrder.push(datasetSearchFn('header')); } if (vBL === 'header' && settings.verbatimButtonLocation !== 'none') { buttonsInOrder.push(_createStandardButton({ id: IDS.TOOL_VERBATIM, className: CSS.HEADER_BUTTON, svgIcon: SVG_ICONS.verbatim, title: _('tool_verbatim_search'), clickHandler: URLActionManager.triggerToggleVerbatim, isActive: verbatimActive })); } if (pznBL === 'header' && personalizeBtnFn && settings.personalizationButtonLocation !== 'none') { buttonsInOrder.push(personalizeBtnFn('header')); } if (rBL === 'header' && settings.resetButtonLocation !== 'none') { buttonsInOrder.push(_createStandardButton({ id: IDS.TOOL_RESET_BUTTON, className: CSS.HEADER_BUTTON, svgIcon: SVG_ICONS.reset, title: _('tool_reset_filters'), clickHandler: URLActionManager.triggerResetFilters })); } buttonsInOrder.forEach(btn => { if (settingsBtnRef) { headerEl.insertBefore(btn, settingsBtnRef); } else { headerEl.appendChild(btn); } }); }
  2905. function _buildSidebarFixedTopControls(rBL, vBL, aSL, pznBL, schL, trnL, dsL, advSearchFn, personalizeBtnFn, scholarFn, trendsFn, datasetSearchFn, settings) { const fTBC = document.createElement('div'); fTBC.id = IDS.FIXED_TOP_BUTTONS; const fTF = document.createDocumentFragment(); const verbatimActive = URLActionManager.isVerbatimActive(); if (rBL === 'topBlock' && settings.resetButtonLocation !== 'none') { const btn = _createStandardButton({ id: IDS.TOOL_RESET_BUTTON, className: CSS.TOOL_BUTTON, svgIcon: SVG_ICONS.reset, textContent: _('tool_reset_filters'), title: _('tool_reset_filters'), clickHandler: URLActionManager.triggerResetFilters }); const bD = document.createElement('div'); bD.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bD.appendChild(btn); fTF.appendChild(bD); } if (pznBL === 'topBlock' && personalizeBtnFn && settings.personalizationButtonLocation !== 'none') { const btnPzn = personalizeBtnFn('topBlock'); const bDPzn = document.createElement('div'); bDPzn.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bDPzn.appendChild(btnPzn); fTF.appendChild(bDPzn); } if (vBL === 'topBlock' && settings.verbatimButtonLocation !== 'none') { const btnVerbatim = _createStandardButton({ id: IDS.TOOL_VERBATIM, className: CSS.TOOL_BUTTON, svgIcon: SVG_ICONS.verbatim, textContent: _('tool_verbatim_search'), title: _('tool_verbatim_search'), clickHandler: URLActionManager.triggerToggleVerbatim, isActive: verbatimActive }); const bDVerbatim = document.createElement('div'); bDVerbatim.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bDVerbatim.appendChild(btnVerbatim); fTF.appendChild(bDVerbatim); } if (aSL === 'topBlock' && advSearchFn && settings.advancedSearchLinkLocation !== 'none') { const linkEl = advSearchFn(true); const bDAdv = document.createElement('div'); bDAdv.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bDAdv.appendChild(linkEl); fTF.appendChild(bDAdv); } if (schL === 'topBlock' && scholarFn && settings.googleScholarShortcutLocation !== 'none') { const btnSch = scholarFn('topBlock'); const bDSch = document.createElement('div'); bDSch.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bDSch.appendChild(btnSch); fTF.appendChild(bDSch); } if (trnL === 'topBlock' && trendsFn && settings.googleTrendsShortcutLocation !== 'none') { const btnTrn = trendsFn('topBlock'); const bDTrn = document.createElement('div'); bDTrn.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bDTrn.appendChild(btnTrn); fTF.appendChild(bDTrn); } if (dsL === 'topBlock' && datasetSearchFn && settings.googleDatasetSearchShortcutLocation !== 'none') { const btnDs = datasetSearchFn('topBlock'); const bDDs = document.createElement('div'); bDDs.classList.add(CSS.FIXED_TOP_BUTTON_ITEM); bDDs.appendChild(btnDs); fTF.appendChild(bDDs); } if (fTF.childElementCount > 0) { fTBC.appendChild(fTF); return fTBC; } return null; }
  2906. function _createToolsSectionElement(sectionId, titleKey, rBL, vBL, aSL, pznBL, schL, trnL, dsL, advSearchFn, personalizeBtnFn, scholarFn, trendsFn, datasetSearchFn) { const { section, sectionContent, sectionTitle } = _createSectionShell(sectionId, titleKey); sectionTitle.textContent = _(titleKey); const frag = document.createDocumentFragment(); const verbatimActive = URLActionManager.isVerbatimActive(); const currentSettings = SettingsManager.getCurrentSettings(); if (rBL === 'tools' && currentSettings.resetButtonLocation !== 'none') { const btn = _createStandardButton({ id: IDS.TOOL_RESET_BUTTON, className: CSS.TOOL_BUTTON, svgIcon: SVG_ICONS.reset, textContent: _('tool_reset_filters'), title: _('tool_reset_filters'), clickHandler: URLActionManager.triggerResetFilters }); frag.appendChild(btn); } if (pznBL === 'tools' && personalizeBtnFn && currentSettings.personalizationButtonLocation !== 'none') { const btnPzn = personalizeBtnFn('tools'); frag.appendChild(btnPzn); } if (vBL === 'tools' && currentSettings.verbatimButtonLocation !== 'none') { const btnVerbatim = _createStandardButton({ id: IDS.TOOL_VERBATIM, className: CSS.TOOL_BUTTON, svgIcon: SVG_ICONS.verbatim, textContent: _('tool_verbatim_search'), title: _('tool_verbatim_search'), clickHandler: URLActionManager.triggerToggleVerbatim, isActive: verbatimActive }); frag.appendChild(btnVerbatim); } if (aSL === 'tools' && advSearchFn && currentSettings.advancedSearchLinkLocation !== 'none') { frag.appendChild(advSearchFn(true)); } if (schL === 'tools' && scholarFn && currentSettings.googleScholarShortcutLocation !== 'none') { frag.appendChild(scholarFn('tools')); } if (trnL === 'tools' && trendsFn && currentSettings.googleTrendsShortcutLocation !== 'none') { frag.appendChild(trendsFn('tools')); } if (dsL === 'tools' && datasetSearchFn && currentSettings.googleDatasetSearchShortcutLocation !== 'none') { frag.appendChild(datasetSearchFn('tools')); } if (frag.childElementCount > 0) { sectionContent.appendChild(frag); return section; } return null; }
  2907. function _validateDateInputs(minInput, maxInput, errorMsgElement) { _clearElementMessage(errorMsgElement, CSS.ERROR_VISIBLE); minInput.classList.remove(CSS.INPUT_HAS_ERROR); maxInput.classList.remove(CSS.INPUT_HAS_ERROR); let isValid = true; const today = new Date(); today.setHours(0, 0, 0, 0); const startDateStr = minInput.value; const endDateStr = maxInput.value; let startDate = null; let endDate = null; if (startDateStr) { startDate = new Date(startDateStr); startDate.setHours(0,0,0,0); if (startDate > today) { _showElementMessage(errorMsgElement, 'alert_start_in_future', {}, CSS.ERROR_VISIBLE); minInput.classList.add(CSS.INPUT_HAS_ERROR); isValid = false; } } if (endDateStr) { endDate = new Date(endDateStr); endDate.setHours(0,0,0,0); if (endDate > today && !maxInput.getAttribute('max')) { /* Check only if max is not today (which it is by default) */ if (isValid) _showElementMessage(errorMsgElement, 'alert_end_in_future', {}, CSS.ERROR_VISIBLE); else errorMsgElement.textContent += " " + _('alert_end_in_future'); maxInput.classList.add(CSS.INPUT_HAS_ERROR); isValid = false; } } if (startDate && endDate && startDate > endDate) { if (isValid) _showElementMessage(errorMsgElement, 'alert_end_before_start', {}, CSS.ERROR_VISIBLE); else errorMsgElement.textContent += " " + _('alert_end_before_start'); minInput.classList.add(CSS.INPUT_HAS_ERROR); maxInput.classList.add(CSS.INPUT_HAS_ERROR); isValid = false; } return isValid; }
  2908. function addDateRangeListener() { const dateRangeSection = sidebar?.querySelector('#sidebar-section-date-range'); if (!dateRangeSection) return; const applyButton = dateRangeSection.querySelector('.apply-date-range'); const errorMsgElement = dateRangeSection.querySelector(`#${IDS.DATE_RANGE_ERROR_MSG}`); const dateMinInput = dateRangeSection.querySelector(`#${IDS.DATE_MIN}`); const dateMaxInput = dateRangeSection.querySelector(`#${IDS.DATE_MAX}`); if (!applyButton || !errorMsgElement || !dateMinInput || !dateMaxInput) { console.warn(`${LOG_PREFIX} Date range elements not found for listener setup.`); return; } const handleDateValidation = () => { const isValid = _validateDateInputs(dateMinInput, dateMaxInput, errorMsgElement); applyButton.disabled = !isValid; }; if (!dateMinInput.dataset[DATA_ATTR.LISTENER_ATTACHED]) { dateMinInput.addEventListener('input', handleDateValidation); dateMinInput.addEventListener('change', handleDateValidation); dateMinInput.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true'; } if (!dateMaxInput.dataset[DATA_ATTR.LISTENER_ATTACHED]) { dateMaxInput.addEventListener('input', handleDateValidation); dateMaxInput.addEventListener('change', handleDateValidation); dateMaxInput.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true'; } if (!applyButton.dataset[DATA_ATTR.LISTENER_ATTACHED]) { applyButton.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true'; applyButton.addEventListener('click', () => { if (!_validateDateInputs(dateMinInput, dateMaxInput, errorMsgElement)) return; URLActionManager.applyDateRange(dateMinInput.value, dateMaxInput.value); }); } handleDateValidation(); /* Initial validation */ }
  2909. function _initializeSidebarEventListenersAndStates() { addDateRangeListener(); addToolButtonListeners(); initializeSelectedFilters(); applySectionCollapseStates(); }
  2910. function _clearElementMessage(element, visibleClass = CSS.ERROR_VISIBLE) { if(!element)return; element.textContent=''; element.classList.remove(visibleClass);}
  2911. function _showElementMessage(element, messageKey, messageArgs = {}, visibleClass = CSS.ERROR_VISIBLE) { if(!element)return; element.textContent=_(messageKey,messageArgs); element.classList.add(visibleClass);}
  2912. // --- Tool Button Listeners (for buttons not handled elsewhere like date apply) ---
  2913. function addToolButtonListeners() { const queryAreas = [ sidebar?.querySelector(`.${CSS.SIDEBAR_HEADER}`), sidebar?.querySelector(`#${IDS.FIXED_TOP_BUTTONS}`), sidebar?.querySelector(`#sidebar-section-tools .${CSS.SECTION_CONTENT}`) ].filter(Boolean); queryAreas.forEach(area => { area.querySelectorAll(`#${IDS.TOOL_VERBATIM}:not([data-${DATA_ATTR.LISTENER_ATTACHED}])`).forEach(b => { b.addEventListener('click', URLActionManager.triggerToggleVerbatim); b.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true'; }); area.querySelectorAll(`#${IDS.TOOL_RESET_BUTTON}:not([data-${DATA_ATTR.LISTENER_ATTACHED}])`).forEach(b => { b.addEventListener('click', URLActionManager.triggerResetFilters); b.dataset[DATA_ATTR.LISTENER_ATTACHED] = 'true'; }); }); }
  2914. // --- Sidebar Collapse/Expand and Section State Application ---
  2915. function applySidebarCollapseVisuals(isCollapsed) { if(!sidebar)return; const collapseButton = sidebar.querySelector(`#${IDS.COLLAPSE_BUTTON}`); if(isCollapsed){ sidebar.classList.add(CSS.SIDEBAR_COLLAPSED); if(collapseButton){ collapseButton.innerHTML = SVG_ICONS.chevronRight; collapseButton.title = _('sidebar_expand_title');}} else{ sidebar.classList.remove(CSS.SIDEBAR_COLLAPSED); if(collapseButton){ collapseButton.innerHTML = SVG_ICONS.chevronLeft; collapseButton.title = _('sidebar_collapse_title');}} }
  2916. function applySectionCollapseStates() { if(!sidebar)return; const currentSettings = SettingsManager.getCurrentSettings(); const sections = sidebar.querySelectorAll(`.${CSS.SIDEBAR_CONTENT_WRAPPER} .${CSS.SIDEBAR_SECTION}`); sections.forEach(section => { const content = section.querySelector(`.${CSS.SECTION_CONTENT}`); const title = section.querySelector(`.${CSS.SECTION_TITLE}`); const sectionId = section.id; if (content && title && sectionId) { let shouldBeCollapsed = false; if (currentSettings.sectionDisplayMode === 'collapseAll') { shouldBeCollapsed = true; } else if (currentSettings.sectionDisplayMode === 'expandAll') { shouldBeCollapsed = false; } else { /* remember state */ shouldBeCollapsed = currentSettings.sectionStates?.[sectionId] === true; } content.classList.toggle(CSS.COLLAPSED, shouldBeCollapsed); title.classList.toggle(CSS.COLLAPSED, shouldBeCollapsed); if (currentSettings.sectionDisplayMode === 'remember') { if (!currentSettings.sectionStates) currentSettings.sectionStates = {}; currentSettings.sectionStates[sectionId] = shouldBeCollapsed; } } }); }
  2917.  
  2918. // --- Initialize Selected Filters based on URL ---
  2919. function initializeSelectedFilters() {
  2920. if (!sidebar) return;
  2921. try {
  2922. const currentUrl = URLActionManager._getURLObject ? URLActionManager._getURLObject() : Utils.getCurrentURL(); // Use URLActionManager's internal helper if possible
  2923. if (!currentUrl) return; // Should not happen if script is running on a valid page
  2924. const params = currentUrl.searchParams;
  2925. const currentTbs = params.get('tbs') || '';
  2926. const currentQuery = params.get('q') || '';
  2927.  
  2928. // Handle standalone params like lr, cr, as_occt
  2929. ALL_SECTION_DEFINITIONS.forEach(sectionDef => {
  2930. if (sectionDef.type === 'filter' && sectionDef.param && sectionDef.id !== 'sidebar-section-filetype' && sectionDef.id !== 'sidebar-section-site-search') { // Exclude filetype/site as they use 'q'
  2931. _initializeStandaloneFilterState(params, sectionDef.id, sectionDef.param);
  2932. }
  2933. });
  2934. _initializeTimeFilterState(currentTbs);
  2935. _initializeVerbatimState();
  2936. _initializePersonalizationState();
  2937. _initializeDateRangeInputs(currentTbs);
  2938. _initializeSiteSearchState(currentQuery); // Pass only query for site search
  2939. _initializeFiletypeSearchState(currentQuery, params.get('as_filetype')); // Pass query and as_filetype
  2940. } catch (e) {
  2941. console.error(`${LOG_PREFIX} Error initializing filter highlights:`, e);
  2942. }
  2943. }
  2944.  
  2945. function _initializeStandaloneFilterState(params, sectionId, paramToGetFromURL) {
  2946. const sectionElement = sidebar?.querySelector(`#${sectionId}`);
  2947. if (!sectionElement) return;
  2948. const urlValue = params.get(paramToGetFromURL);
  2949. const options = sectionElement.querySelectorAll(`.${CSS.FILTER_OPTION}`);
  2950. let anOptionWasSelectedBasedOnUrl = false;
  2951.  
  2952. options.forEach(opt => {
  2953. const optionValue = opt.dataset[DATA_ATTR.FILTER_VALUE];
  2954. const isSelected = (urlValue !== null && urlValue === optionValue); // Strict check for value presence
  2955. opt.classList.toggle(CSS.SELECTED, isSelected);
  2956. if (isSelected) anOptionWasSelectedBasedOnUrl = true;
  2957. });
  2958.  
  2959. // If no option was selected based on URL (e.g., param not present or value doesn't match), select the default
  2960. if (!anOptionWasSelectedBasedOnUrl) {
  2961. // For as_occt, default is 'any'. For others (lr, cr), default is empty string value.
  2962. const defaultOptionQuery = (paramToGetFromURL === 'as_occt')
  2963. ? `.${CSS.FILTER_OPTION}[data-${DATA_ATTR.FILTER_VALUE}="any"]`
  2964. : `.${CSS.FILTER_OPTION}[data-${DATA_ATTR.FILTER_VALUE}=""]`;
  2965. const defaultOpt = sectionElement.querySelector(defaultOptionQuery);
  2966. if (defaultOpt) {
  2967. defaultOpt.classList.add(CSS.SELECTED);
  2968. }
  2969. }
  2970. }
  2971.  
  2972. function _initializeTimeFilterState(currentTbs){ const timeSection = sidebar?.querySelector('#sidebar-section-time'); if(!timeSection) return; const qdrMatch = currentTbs.match(/qdr:([^,]+)/); const activeQdrValue = qdrMatch ? qdrMatch[1] : null; const hasDateRange = /cdr:1/.test(currentTbs); const timeOptions = timeSection.querySelectorAll(`.${CSS.FILTER_OPTION}`); timeOptions.forEach(opt => { const optionValue = opt.dataset[DATA_ATTR.FILTER_VALUE]; let shouldBeSelected = false; if(hasDateRange){ shouldBeSelected = (optionValue === '');/* If custom date range, select "Any Time" */ } else if(activeQdrValue){ shouldBeSelected = (optionValue === activeQdrValue); } else { shouldBeSelected = (optionValue === ''); /* Default to "Any Time" */ } opt.classList.toggle(CSS.SELECTED, shouldBeSelected); }); }
  2973. function _initializeVerbatimState(){ const isVerbatimActiveNow = URLActionManager.isVerbatimActive(); sidebar?.querySelectorAll(`#${IDS.TOOL_VERBATIM}`).forEach(b=>b.classList.toggle(CSS.ACTIVE, isVerbatimActiveNow)); }
  2974. function _initializePersonalizationState() { const isActive = URLActionManager.isPersonalizationActive(); sidebar?.querySelectorAll(`#${IDS.TOOL_PERSONALIZE}`).forEach(button => { button.classList.toggle(CSS.ACTIVE, isActive); const titleKey = isActive ? 'tooltip_toggle_personalization_off' : 'tooltip_toggle_personalization_on'; button.title = _(titleKey); /* Update icon/text if needed based on state - already done by _createPersonalizationButtonHTML */ const svgIcon = SVG_ICONS.personalization || ''; const isIconOnly = button.classList.contains(CSS.HEADER_BUTTON) && !button.classList.contains(CSS.TOOL_BUTTON); const currentText = !isIconOnly ? _('tool_personalization_toggle') : ''; let newHTML = ''; if(svgIcon) newHTML += svgIcon; if(currentText) newHTML += (svgIcon && currentText ? ' ' : '') + currentText; button.innerHTML = newHTML.trim(); }); }
  2975. function _initializeDateRangeInputs(currentTbs){ const dateSection = sidebar?.querySelector('#sidebar-section-date-range'); if (!dateSection) return; const dateMinInput = dateSection.querySelector(`#${IDS.DATE_MIN}`); const dateMaxInput = dateSection.querySelector(`#${IDS.DATE_MAX}`); const errorMsgElement = dateSection.querySelector(`#${IDS.DATE_RANGE_ERROR_MSG}`); const applyButton = dateSection.querySelector('.apply-date-range'); if (errorMsgElement) _clearElementMessage(errorMsgElement, CSS.ERROR_VISIBLE); if (/cdr:1/.test(currentTbs)) { const minMatch = currentTbs.match(/cd_min:(\d{1,2})\/(\d{1,2})\/(\d{4})/); const maxMatch = currentTbs.match(/cd_max:(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (dateMinInput) dateMinInput.value = minMatch ? `${minMatch[3]}-${minMatch[1].padStart(2, '0')}-${minMatch[2].padStart(2, '0')}` : ''; if (dateMaxInput) dateMaxInput.value = maxMatch ? `${maxMatch[3]}-${maxMatch[1].padStart(2, '0')}-${maxMatch[2].padStart(2, '0')}` : ''; } else { if (dateMinInput) dateMinInput.value = ''; if (dateMaxInput) dateMaxInput.value = ''; } if (dateMinInput && dateMaxInput && errorMsgElement && applyButton) { const isValid = _validateDateInputs(dateMinInput, dateMaxInput, errorMsgElement); applyButton.disabled = !isValid; } }
  2976.  
  2977. function _initializeSiteSearchState(currentQuery){
  2978. const siteSearchSectionContent = sidebar?.querySelector('#sidebar-section-site-search .'+CSS.SECTION_CONTENT);
  2979. if (!siteSearchSectionContent) return;
  2980.  
  2981. const clearSiteOptDiv = siteSearchSectionContent.querySelector(`#${IDS.CLEAR_SITE_SEARCH_OPTION}`);
  2982. const listElement = siteSearchSectionContent.querySelector('ul.' + CSS.CUSTOM_LIST);
  2983. const currentSettings = SettingsManager.getCurrentSettings();
  2984. const checkboxModeEnabled = currentSettings.enableSiteSearchCheckboxMode;
  2985.  
  2986. // Reset all selections first
  2987. siteSearchSectionContent.querySelectorAll(`.${CSS.FILTER_OPTION}.${CSS.SELECTED}, label.${CSS.SELECTED}`).forEach(opt => opt.classList.remove(CSS.SELECTED));
  2988. if (checkboxModeEnabled && listElement) {
  2989. listElement.querySelectorAll(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}`).forEach(cb => cb.checked = false);
  2990. }
  2991.  
  2992. // Extract all site: operators from the query
  2993. const siteMatches = [...currentQuery.matchAll(/(?<!\S)site:([\w.:\/~%?#=&+-]+)(?!\S)/gi)];
  2994. let activeSiteUrlsFromQuery = siteMatches.map(match => match[1].toLowerCase());
  2995. activeSiteUrlsFromQuery.sort();
  2996.  
  2997. if (activeSiteUrlsFromQuery.length > 0) {
  2998. if(clearSiteOptDiv) clearSiteOptDiv.classList.remove(CSS.SELECTED);
  2999. let customOptionFullyMatched = false;
  3000.  
  3001. if (listElement) {
  3002. const customSiteOptions = Array.from(listElement.querySelectorAll(checkboxModeEnabled ? 'label' : `div.${CSS.FILTER_OPTION}`));
  3003. for (const optElement of customSiteOptions) {
  3004. const customSiteValue = optElement.dataset[DATA_ATTR.SITE_URL];
  3005. if (!customSiteValue) continue;
  3006.  
  3007. const definedCustomSites = Utils.parseCombinedValue(customSiteValue).map(s => s.toLowerCase()).sort();
  3008. if (definedCustomSites.length > 0 && definedCustomSites.length === activeSiteUrlsFromQuery.length && definedCustomSites.every((val, index) => val === activeSiteUrlsFromQuery[index])) {
  3009. if (checkboxModeEnabled) {
  3010. const checkbox = listElement.querySelector(`input[type="checkbox"][value="${customSiteValue}"]`);
  3011. if (checkbox) checkbox.checked = true;
  3012. }
  3013. optElement.classList.add(CSS.SELECTED);
  3014. customOptionFullyMatched = true;
  3015. break;
  3016. }
  3017. }
  3018. }
  3019.  
  3020. if (!customOptionFullyMatched && listElement && checkboxModeEnabled) { // Only check individual checkboxes if not a full match and in checkbox mode
  3021. activeSiteUrlsFromQuery.forEach(url => {
  3022. const checkbox = listElement.querySelector(`input[type="checkbox"].${CSS.SITE_SEARCH_ITEM_CHECKBOX}[value="${url}"]`);
  3023. if (checkbox) {
  3024. checkbox.checked = true;
  3025. const label = listElement.querySelector(`label[for="${checkbox.id}"]`);
  3026. if(label) label.classList.add(CSS.SELECTED);
  3027. }
  3028. });
  3029. } else if (!customOptionFullyMatched && listElement && !checkboxModeEnabled && activeSiteUrlsFromQuery.length === 1) {
  3030. // If not checkbox mode, and only a single site is in query, try to select its div option
  3031. const singleSiteInQuery = activeSiteUrlsFromQuery[0];
  3032. const optionDiv = listElement.querySelector(`div.${CSS.FILTER_OPTION}[data-${DATA_ATTR.SITE_URL}="${singleSiteInQuery}"]`);
  3033. if (optionDiv) optionDiv.classList.add(CSS.SELECTED);
  3034. }
  3035.  
  3036.  
  3037. } else {
  3038. if (clearSiteOptDiv) clearSiteOptDiv.classList.add(CSS.SELECTED);
  3039. }
  3040.  
  3041. if (checkboxModeEnabled) {
  3042. _updateApplySitesButtonState(siteSearchSectionContent);
  3043. }
  3044. }
  3045.  
  3046. function _initializeFiletypeSearchState(currentQuery, asFiletypeParam) {
  3047. const filetypeSectionContent = sidebar?.querySelector('#sidebar-section-filetype .'+CSS.SECTION_CONTENT);
  3048. if (!filetypeSectionContent) return;
  3049.  
  3050. const clearFiletypeOptDiv = filetypeSectionContent.querySelector(`#${IDS.CLEAR_FILETYPE_SEARCH_OPTION}`);
  3051. const listElement = filetypeSectionContent.querySelector('ul.' + CSS.CUSTOM_LIST);
  3052. const currentSettings = SettingsManager.getCurrentSettings();
  3053. const checkboxModeEnabled = currentSettings.enableFiletypeCheckboxMode;
  3054.  
  3055. // Reset all selections first
  3056. filetypeSectionContent.querySelectorAll(`.${CSS.FILTER_OPTION}.${CSS.SELECTED}, label.${CSS.SELECTED}`).forEach(opt => opt.classList.remove(CSS.SELECTED));
  3057. if (checkboxModeEnabled && listElement) {
  3058. listElement.querySelectorAll(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}`).forEach(cb => cb.checked = false);
  3059. }
  3060.  
  3061. let activeFiletypesFromQuery = [];
  3062. const filetypeMatches = [...currentQuery.matchAll(/(?<!\S)filetype:([\w.:\/~%?#=&+-]+)(?!\S)/gi)];
  3063.  
  3064. if (filetypeMatches.length > 0) {
  3065. activeFiletypesFromQuery = filetypeMatches.map(match => match[1].toLowerCase());
  3066. } else if (asFiletypeParam && !currentQuery.includes('filetype:')) {
  3067. activeFiletypesFromQuery = Utils.parseCombinedValue(asFiletypeParam).map(ft => ft.toLowerCase());
  3068. }
  3069. activeFiletypesFromQuery.sort();
  3070.  
  3071. if (activeFiletypesFromQuery.length > 0) {
  3072. if(clearFiletypeOptDiv) clearFiletypeOptDiv.classList.remove(CSS.SELECTED);
  3073. let customOptionFullyMatched = false;
  3074.  
  3075. if (listElement) {
  3076. const customFiletypeOptions = Array.from(listElement.querySelectorAll(checkboxModeEnabled ? 'label' : `div.${CSS.FILTER_OPTION}`));
  3077. for (const optElement of customFiletypeOptions) {
  3078. const customFtValueAttr = checkboxModeEnabled ? optElement.dataset[DATA_ATTR.FILETYPE_VALUE] : optElement.dataset[DATA_ATTR.FILTER_VALUE];
  3079. if (!customFtValueAttr) continue;
  3080.  
  3081. const definedCustomFiletypes = Utils.parseCombinedValue(customFtValueAttr).map(s => s.toLowerCase()).sort();
  3082. if (definedCustomFiletypes.length > 0 && definedCustomFiletypes.length === activeFiletypesFromQuery.length && definedCustomFiletypes.every((val, index) => val === activeFiletypesFromQuery[index])) {
  3083. if (checkboxModeEnabled) {
  3084. const checkbox = listElement.querySelector(`input[type="checkbox"][value="${customFtValueAttr}"]`);
  3085. if (checkbox) checkbox.checked = true;
  3086. }
  3087. optElement.classList.add(CSS.SELECTED);
  3088. customOptionFullyMatched = true;
  3089. break;
  3090. }
  3091. }
  3092. }
  3093.  
  3094. if (!customOptionFullyMatched && listElement && checkboxModeEnabled) {
  3095. activeFiletypesFromQuery.forEach(ft => {
  3096. const checkbox = listElement.querySelector(`input[type="checkbox"].${CSS.FILETYPE_SEARCH_ITEM_CHECKBOX}[value="${ft}"]`);
  3097. if (checkbox) {
  3098. checkbox.checked = true;
  3099. const label = listElement.querySelector(`label[for="${checkbox.id}"]`);
  3100. if(label) label.classList.add(CSS.SELECTED);
  3101. }
  3102. });
  3103. } else if (!customOptionFullyMatched && listElement && !checkboxModeEnabled && activeFiletypesFromQuery.length === 1) {
  3104. const singleFtInQuery = activeFiletypesFromQuery[0];
  3105. const optionDiv = listElement.querySelector(`div.${CSS.FILTER_OPTION}[data-${DATA_ATTR.FILTER_VALUE}="${singleFtInQuery}"]`);
  3106. if (optionDiv) optionDiv.classList.add(CSS.SELECTED);
  3107. }
  3108. } else {
  3109. if (clearFiletypeOptDiv) clearFiletypeOptDiv.classList.add(CSS.SELECTED);
  3110. }
  3111. if (checkboxModeEnabled) {
  3112. _updateApplyFiletypesButtonState(filetypeSectionContent);
  3113. }
  3114. }
  3115.  
  3116. // --- Event Binding for Sidebar ---
  3117. function bindSidebarEvents() { if (!sidebar) return; const collapseButton = sidebar.querySelector(`#${IDS.COLLAPSE_BUTTON}`); const settingsButton = sidebar.querySelector(`#${IDS.SETTINGS_BUTTON}`); if (collapseButton) collapseButton.title = _('sidebar_collapse_title'); if (settingsButton) settingsButton.title = _('sidebar_settings_title'); sidebar.addEventListener('click', (e) => { const settingsBtnTarget = e.target.closest(`#${IDS.SETTINGS_BUTTON}`); if (settingsBtnTarget) { SettingsManager.show(); return; } const collapseBtnTarget = e.target.closest(`#${IDS.COLLAPSE_BUTTON}`); if (collapseBtnTarget) { toggleSidebarCollapse(); return; } const sectionTitleTarget = e.target.closest(`.${CSS.SIDEBAR_CONTENT_WRAPPER} .${CSS.SECTION_TITLE}`); if (sectionTitleTarget && !sidebar.classList.contains(CSS.SIDEBAR_COLLAPSED)) { handleSectionCollapse(e); return; } }); }
  3118. // --- Sidebar and Section Collapse Logic ---
  3119. function toggleSidebarCollapse() { const cs = SettingsManager.getCurrentSettings(); cs.sidebarCollapsed = !cs.sidebarCollapsed; applySettings(cs); SettingsManager.save('Sidebar Collapse');}
  3120. function handleSectionCollapse(event) { const title = event.target.closest(`.${CSS.SECTION_TITLE}`); if (!title || sidebar?.classList.contains(CSS.SIDEBAR_COLLAPSED) || title.closest(`#${IDS.FIXED_TOP_BUTTONS}`)) return; const section = title.closest(`.${CSS.SIDEBAR_SECTION}`); if (!section) return; const content = section.querySelector(`.${CSS.SECTION_CONTENT}`); const sectionId = section.id; if (!content || !sectionId) return; const currentSettings = SettingsManager.getCurrentSettings(); const isCurrentlyCollapsed = content.classList.contains(CSS.COLLAPSED); const shouldBeCollapsedAfterClick = !isCurrentlyCollapsed; let overallStateChanged = false; if (currentSettings.accordionMode && !shouldBeCollapsedAfterClick) { const sectionsContainer = section.parentElement; if (_applyAccordionEffectToSections(sectionId, sectionsContainer, currentSettings)) overallStateChanged = true; } if (_toggleSectionVisualState(section, title, content, sectionId, shouldBeCollapsedAfterClick, currentSettings)) overallStateChanged = true; if (overallStateChanged && currentSettings.sectionDisplayMode === 'remember') { debouncedSaveSettings('Section Collapse/Accordion'); } }
  3121. function _applyAccordionEffectToSections(clickedSectionId, allSectionsContainer, currentSettings) { let stateChangedForAccordion = false; allSectionsContainer?.querySelectorAll(`.${CSS.SIDEBAR_SECTION}`)?.forEach(otherSection => { if (otherSection.id !== clickedSectionId) { const otherContent = otherSection.querySelector(`.${CSS.SECTION_CONTENT}`); const otherTitle = otherSection.querySelector(`.${CSS.SECTION_TITLE}`); if (otherContent && !otherContent.classList.contains(CSS.COLLAPSED)) { otherContent.classList.add(CSS.COLLAPSED); otherTitle?.classList.add(CSS.COLLAPSED); if (currentSettings.sectionDisplayMode === 'remember') { if (!currentSettings.sectionStates) currentSettings.sectionStates = {}; if (currentSettings.sectionStates[otherSection.id] !== true) { currentSettings.sectionStates[otherSection.id] = true; stateChangedForAccordion = true; } } } } }); return stateChangedForAccordion; }
  3122. function _toggleSectionVisualState(sectionEl, titleEl, contentEl, sectionId, newCollapsedState, currentSettings) { let sectionStateActuallyChanged = false; const isCurrentlyCollapsed = contentEl.classList.contains(CSS.COLLAPSED); if (isCurrentlyCollapsed !== newCollapsedState) { contentEl.classList.toggle(CSS.COLLAPSED, newCollapsedState); titleEl.classList.toggle(CSS.COLLAPSED, newCollapsedState); sectionStateActuallyChanged = true; } if (currentSettings.sectionDisplayMode === 'remember') { if (!currentSettings.sectionStates) currentSettings.sectionStates = {}; if (currentSettings.sectionStates[sectionId] !== newCollapsedState) { currentSettings.sectionStates[sectionId] = newCollapsedState; if (!sectionStateActuallyChanged) sectionStateActuallyChanged = true; /* Mark changed even if visual was already matching if state obj differs */ } } return sectionStateActuallyChanged; }
  3123.  
  3124. // --- Initialization ---
  3125. function initializeScript() {
  3126. console.log(LOG_PREFIX + " Initializing script...");
  3127. debouncedSaveSettings = Utils.debounce(() => SettingsManager.save('Debounced Save'), 800);
  3128. try {
  3129. addGlobalStyles(); NotificationManager.init(); LocalizationService.initializeBaseLocale();
  3130. SettingsManager.initialize( defaultSettings, applySettings, buildSidebarUI, applySectionCollapseStates, _initMenuCommands, renderSectionOrderList );
  3131. setupSystemThemeListener(); buildSidebarSkeleton();
  3132. DragManager.init( sidebar, sidebar.querySelector(`.${CSS.DRAG_HANDLE}`), SettingsManager, debouncedSaveSettings );
  3133. const initialSettings = SettingsManager.getCurrentSettings();
  3134. DragManager.setDraggable(initialSettings.draggableHandleEnabled, sidebar, sidebar.querySelector(`.${CSS.DRAG_HANDLE}`));
  3135. applySettings(initialSettings); buildSidebarUI(); bindSidebarEvents(); _initMenuCommands();
  3136. console.log(`${LOG_PREFIX} Script initialization complete. Final effective locale: ${LocalizationService.getCurrentLocale()}`);
  3137. } catch (error) {
  3138. console.error(`${LOG_PREFIX} [initializeScript] CRITICAL ERROR DURING INITIALIZATION:`, error, error.stack);
  3139. const scriptNameForAlert = (typeof _ === 'function' && _('scriptName') && !(_('scriptName').startsWith('[ERR:'))) ? _('scriptName') : SCRIPT_INTERNAL_NAME;
  3140. // Try to use NotificationManager if available, otherwise alert
  3141. if (typeof NotificationManager !== 'undefined' && NotificationManager.show) { NotificationManager.show('alert_init_fail', { scriptName: scriptNameForAlert, error: error.message }, 'error', 0); }
  3142. else { _showGlobalMessage('alert_init_fail', { scriptName: scriptNameForAlert, error: error.message }, 'error', 0); } // Fallback
  3143. // Attempt to clean up UI elements if init fails badly
  3144. if(sidebar && sidebar.remove) sidebar.remove(); const settingsOverlayEl = document.getElementById(IDS.SETTINGS_OVERLAY); if(settingsOverlayEl) settingsOverlayEl.remove(); ModalManager.hide();
  3145. }
  3146. }
  3147.  
  3148. // --- Script Entry Point (Dependency Loading Logic) ---
  3149. if (document.getElementById(IDS.SIDEBAR)) { console.warn(`${LOG_PREFIX} Sidebar with ID "${IDS.SIDEBAR}" already exists. Skipping initialization.`); return; }
  3150. const dependenciesReady = { styles: false, i18n: false }; let initializationAttempted = false; let timeoutFallback;
  3151. function checkDependenciesAndInitialize() { if (initializationAttempted) return; if (dependenciesReady.styles && dependenciesReady.i18n) { console.log(`${LOG_PREFIX} All dependencies ready. Initializing script.`); clearTimeout(timeoutFallback); initializationAttempted = true; if (document.readyState === 'complete' || document.readyState === 'interactive' || document.readyState === 'loaded') { initializeScript(); } else { window.addEventListener('DOMContentLoaded', initializeScript, { once: true }); } } }
  3152. document.addEventListener('gscsStylesLoaded', function stylesLoadedHandler() { console.log(`${LOG_PREFIX} Event "gscsStylesLoaded" received.`); dependenciesReady.styles = true; checkDependenciesAndInitialize(); }, { once: true });
  3153. document.addEventListener('gscsi18nLoaded', function i18nLoadedHandler() { console.log(`${LOG_PREFIX} Event "gscsi18nLoaded" received.`); dependenciesReady.i18n = true; checkDependenciesAndInitialize(); }, { once: true });
  3154. timeoutFallback = setTimeout(() => { if (initializationAttempted) return; console.log(`${LOG_PREFIX} Fallback: Checking dependencies after timeout.`); if (typeof window.GSCS_Namespace !== 'undefined') { if (typeof window.GSCS_Namespace.stylesText === 'string' && window.GSCS_Namespace.stylesText.trim() !== '' && !dependenciesReady.styles) { console.log(`${LOG_PREFIX} Fallback: Styles found via namespace.`); dependenciesReady.styles = true; } if (typeof window.GSCS_Namespace.i18nPack === 'object' && Object.keys(window.GSCS_Namespace.i18nPack.translations || {}).length > 0 && !dependenciesReady.i18n) { console.log(`${LOG_PREFIX} Fallback: i18n pack found via namespace.`); dependenciesReady.i18n = true; } } if (dependenciesReady.styles && dependenciesReady.i18n) { checkDependenciesAndInitialize(); } else { console.error(`${LOG_PREFIX} Fallback: Dependencies still not fully loaded after timeout. Styles: ${dependenciesReady.styles}, i18n: ${dependenciesReady.i18n}.`); if (!initializationAttempted) { console.warn(`${LOG_PREFIX} Attempting to initialize with potentially incomplete dependencies due to fallback timeout.`); if (!dependenciesReady.styles) { console.warn(`${LOG_PREFIX} Styles dependency forced true in fallback.`); dependenciesReady.styles = true; } if (!dependenciesReady.i18n) { console.warn(`${LOG_PREFIX} i18n dependency forced true in fallback.`); dependenciesReady.i18n = true; } checkDependenciesAndInitialize(); } } }, 2000);
  3155. // Additional check for cases where scripts load very fast and events might be missed if this script's DOMContentLoaded is later
  3156. if (document.readyState === 'complete' || document.readyState === 'interactive' || document.readyState === 'loaded') {
  3157. if (typeof window.GSCS_Namespace !== 'undefined') {
  3158. if (typeof window.GSCS_Namespace.stylesText === 'string' && window.GSCS_Namespace.stylesText.trim() !== '' && !dependenciesReady.styles) {
  3159. // console.log(`${LOG_PREFIX} Post-load check: Styles found via namespace.`);
  3160. dependenciesReady.styles = true;
  3161. }
  3162. if (typeof window.GSCS_Namespace.i18nPack === 'object' && Object.keys(window.GSCS_Namespace.i18nPack.translations || {}).length > 0 && !dependenciesReady.i18n) {
  3163. // console.log(`${LOG_PREFIX} Post-load check: i18n pack found via namespace.`);
  3164. dependenciesReady.i18n = true;
  3165. }
  3166. }
  3167. if (dependenciesReady.styles && dependenciesReady.i18n && !initializationAttempted) {
  3168. // console.log(`${LOG_PREFIX} Post-load check: All dependencies ready. Initializing script.`);
  3169. checkDependenciesAndInitialize();
  3170. }
  3171. }
  3172. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址