├── .gitignore
├── LICENSE
├── README.md
├── background.js
├── content.js
├── earlyLoader.js
├── images
├── icon128.png
├── icon16.png
└── icon48.png
├── injected.js
├── manifest.json
├── popup.html
├── popup.js
├── shadowInject.js
└── styles.css
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDE 和編輯器
2 | .vscode/
3 | .idea/
4 | *.sublime-project
5 | *.sublime-workspace
6 |
7 | # 系統文件
8 | .DS_Store
9 | Thumbs.db
10 |
11 | # 暫存文件
12 | *.log
13 | *.tmp
14 | *.temp
15 |
16 | # Node.js
17 | node_modules/
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | # 打包文件
23 | *.zip
24 | *.rar
25 | *.7z
26 |
27 | # 其他
28 | *.bak
29 | *.swp
30 | *~
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 poning0224
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 搶票柴柴-售票網站篩選器
2 |
3 |
4 | 一個 Chrome 擴充功能,用於在各大售票網站上快速篩選您想要的區域票券。
5 |
6 | ## ✨ 功能特點
7 |
8 | ### 多平台支援
9 | - 拓元售票網站 (tixcraft.com)
10 | - KKTIX售票網站 (kktix.com)
11 | - ibon售票網站 (ibon.com.tw)
12 | - 遠大售票網站 (ticketplus.com.tw)
13 | - cityline售票網站 (cityline.com)
14 | - 寬宏售票網站 (kham.com.tw)
15 | - 富邦悍將售票網站 (fami.life)
16 | - JKFACE售票網站 (jkface.net)
17 |
18 | ### 篩選功能
19 | - 關鍵字篩選:輸入想要的區域名稱(如:VIP、特A區等),快速找出符合條件的票券
20 | - 黑名單過濾:輸入不想看到的區域名稱或價格,快速過濾不需要的票券
21 | - 價格篩選:輸入價格可以快速找出指定價格的票券
22 | - 已售完票券過濾:可選擇是否顯示已售完的票券
23 | - 一鍵重置:點擊「顯示全部票券」即可恢復原始顯示
24 | - 即時更新:當網頁更新時自動套用篩選條件
25 |
26 | ### 進階功能
27 | - 顯示伺服器時間:可選擇是否在頁面上顯示當前伺服器時間
28 | - 顯示篩選狀態:可選擇是否在頁面上顯示目前的篩選條件
29 | - 擴充功能開關:可快速開啟/關閉擴充功能
30 |
31 | ## 🧩 安裝方式
32 | ###
Chrome商店(版本1.3.2)
33 | 1. 前往 [Chrome商店](https://chromewebstore.google.com/detail/pofndajlpfdonhkefkppngfghocppcck?utm_source=item-share-cb)
34 | 2. 加到Chrome
35 | 3. 新增擴充功能
36 |
37 | ### 📥 本地下載(版本1.4.0)
38 | 1. [點我下載](https://github.com/coder220224/ticket-filter/releases/download/v1.4.0/ticket-filter-v1.4.0.zip)
39 | 2. 解壓縮檔案
40 | 3. 開啟 Chrome 瀏覽器,前往 chrome://extensions/
41 | 4. 開啟右上角的「開發人員模式」
42 | 5. 點擊「載入未封裝項目」
43 | 6. 選擇解壓縮後的資料夾
44 |
45 | ###
Github Releases(版本1.4.0)
46 | 1. 前往 [Releases](https://github.com/coder220224/ticket-filter/releases) 頁面
47 | 2. 下載最新版本的 ZIP 檔案
48 | 3. 解壓縮檔案
49 | 4. 開啟 Chrome 瀏覽器,前往 chrome://extensions/
50 | 5. 開啟右上角的「開發人員模式」
51 | 6. 點擊「載入未封裝項目」
52 | 7. 選擇解壓縮後的資料夾
53 |
54 | ## 📱 手機安裝方式
55 | - ios : [點此看YT教學](https://youtube.com/shorts/KQwCQwVKBBY?feature=share)
56 |
57 | ## 🔧 使用方式
58 |
59 | ### 基本操作
60 | 1. 點擊擴充功能圖示開啟篩選器
61 | 2. 在關鍵字輸入框中輸入想要篩選的區域名稱或價格
62 | 3. 在黑名單輸入框中輸入想要過濾的區域名稱或價格
63 | 4. 按 Enter 或點擊 + 按鈕新增條件
64 | 5. 可選擇是否顯示已售完的票券
65 | 6. 點擊「顯示全部票券」可重置所有條件
66 |
67 | ### 進階篩選語法
68 | - 使用逗號(,)可以篩選同時符合的票券(AND邏輯)
69 | - 例:`4800,搖滾區` - 尋找 4800 元且在搖滾區的票券
70 | - 使用加號(+)可以篩選任一條件符合的票券(OR邏輯)
71 | - 例:`4500+3200` - 尋找 4500 元或 3200 元的票券
72 | - 複合條件範例:
73 | - `4500,A區+3200,B區` - 尋找 (4500元的A區) 或 (3200元的B區) 的票券
74 |
75 | ### 智慧數字轉換
76 | - 支援中文數字和阿拉伯數字互相轉換
77 | - 輸入「特1區」可以找到「特一區」
78 | - 輸入「特一區」可以找到「特1區」
79 | - 支援價格格式轉換
80 | - 輸入「2800」可以找到「2,800」的票價
81 |
82 | ## 🔥 版本更新 (v1.4.0)
83 | ### 1. 新增平台支援
84 | - 支援寬宏售票網站 (kham.com.tw)
85 | - 支援富邦悍將售票網站 (fami.life)
86 | - 支援JKFACE售票網站 (jkface.net)
87 |
88 | ### 2. 新增快捷鍵啟用/停用功能
89 | - 可設定快捷鍵及時啟用/停用擴充功能
90 |
91 |
92 | ## ⚠️ 注意事項
93 |
94 | - 本擴充功能僅提供視覺化篩選,不影響實際購票功能
95 | - 建議搭配官方購票系統使用
96 | - 請遵守各售票網站的購票規則
97 |
98 | ## 🔒 隱私權政策
99 |
100 | - 本擴充功能不會收集任何個人資料
101 | - 所有設定均儲存在您的瀏覽器本地端
102 | - 不會向任何第三方傳送資料
103 |
104 | ## 🏷️ 版本資訊
105 |
106 | - 目前版本:1.4.0
107 | - 最後更新:2025/05/29
108 |
109 | ## 👨💻 開發者資訊
110 |
111 | 如有任何問題或建議,歡迎透過 [GitHub Issues](https://github.com/poning0224/tixcraft-filter/issues) 回報。
112 |
113 | 或著私訊我的社群帳號[摳得柴柴](https://www.threads.net/@coder22022)。
114 |
115 | ## 💝 贊助支持
116 | 如果你喜歡這個項目並希望支持它,可以考慮通過以下方式贊助:
117 |
118 |
119 |
120 | 每一分支持都對我很有幫助,謝謝!
121 |
122 |
123 |
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | // 后台脚本 - 负责拦截和修改网络请求
2 |
3 | // 全局设置存储
4 | let settings = {
5 | // 拓元
6 | tixcraftKeywords: [],
7 | tixcraftHideSoldOut: false,
8 |
9 | // KKTIX
10 | kktixKeywords: [],
11 | kktixHideSoldOut: false,
12 | kktixShowAllPrices: true,
13 |
14 | // ibon
15 | ibonKeywords: [],
16 | ibonHideSoldOut: false,
17 |
18 | // Cityline
19 | citylineKeywords: [],
20 | citylineHideSoldOut: false,
21 |
22 | // 全局设置
23 | extensionEnabled: true,
24 | showServerTime: true,
25 | showFilterStatus: true
26 | };
27 |
28 | // Service Worker激活事件
29 | chrome.runtime.onInstalled.addListener(() => {
30 | loadSettings();
31 | console.log('搶票柴柴扩展已安装/更新');
32 | });
33 |
34 | // 确保在service worker启动时也加载设置
35 | self.onload = () => {
36 | loadSettings();
37 | };
38 |
39 | // 初始化加载设置
40 | function loadSettings() {
41 | chrome.storage.local.get(null, (result) => {
42 | if (result.keywords) settings.tixcraftKeywords = result.keywords;
43 | if (result.hideSoldOut !== undefined) settings.tixcraftHideSoldOut = result.hideSoldOut;
44 |
45 | if (result.targetKeywords) settings.kktixKeywords = result.targetKeywords;
46 | if (result.showAllPrices !== undefined) settings.kktixShowAllPrices = result.showAllPrices;
47 | if (result.kktixHideSoldOut !== undefined) settings.kktixHideSoldOut = result.kktixHideSoldOut;
48 |
49 | if (result.ibonKeywords) settings.ibonKeywords = result.ibonKeywords;
50 | if (result.ibonHideSoldOut !== undefined) settings.ibonHideSoldOut = result.ibonHideSoldOut;
51 |
52 | if (result.citylineKeywords) settings.citylineKeywords = result.citylineKeywords;
53 | if (result.citylineHideSoldOut !== undefined) settings.citylineHideSoldOut = result.citylineHideSoldOut;
54 |
55 | if (result.extensionEnabled !== undefined) settings.extensionEnabled = result.extensionEnabled;
56 | if (result.showServerTime !== undefined) settings.showServerTime = result.showServerTime;
57 | if (result.showFilterStatus !== undefined) settings.showFilterStatus = result.showFilterStatus;
58 | });
59 | }
60 |
61 | // 监听设置变更
62 | chrome.storage.onChanged.addListener((changes, namespace) => {
63 | for (let key in changes) {
64 | if (key === 'keywords') settings.tixcraftKeywords = changes[key].newValue;
65 | if (key === 'hideSoldOut') settings.tixcraftHideSoldOut = changes[key].newValue;
66 |
67 | if (key === 'targetKeywords') settings.kktixKeywords = changes[key].newValue;
68 | if (key === 'showAllPrices') settings.kktixShowAllPrices = changes[key].newValue;
69 | if (key === 'kktixHideSoldOut') settings.kktixHideSoldOut = changes[key].newValue;
70 |
71 | if (key === 'ibonKeywords') settings.ibonKeywords = changes[key].newValue;
72 | if (key === 'ibonHideSoldOut') settings.ibonHideSoldOut = changes[key].newValue;
73 |
74 | if (key === 'citylineKeywords') settings.citylineKeywords = changes[key].newValue;
75 | if (key === 'citylineHideSoldOut') settings.citylineHideSoldOut = changes[key].newValue;
76 |
77 | if (key === 'extensionEnabled') settings.extensionEnabled = changes[key].newValue;
78 | if (key === 'showServerTime') settings.showServerTime = changes[key].newValue;
79 | if (key === 'showFilterStatus') settings.showFilterStatus = changes[key].newValue;
80 | }
81 | });
82 |
83 | // 数字转换函数,供匹配使用
84 | function convertNumber(input) {
85 | const chineseNums = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
86 | const arabicNums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
87 |
88 | // 移除所有空格
89 | input = input.replace(/\s+/g, '');
90 |
91 | // 生成所有可能的版本
92 | let versions = new Set([input]);
93 |
94 | // 找出所有数字和中文数字的位置
95 | let matches = [];
96 | // 匹配阿拉伯数字
97 | input.replace(/\d+/g, (match, offset) => {
98 | matches.push({
99 | type: 'arabic',
100 | value: match,
101 | offset: offset,
102 | length: match.length
103 | });
104 | return match;
105 | });
106 | // 匹配中文数字
107 | for (let i = 0; i < chineseNums.length; i++) {
108 | let pos = input.indexOf(chineseNums[i]);
109 | while (pos !== -1) {
110 | matches.push({
111 | type: 'chinese',
112 | value: chineseNums[i],
113 | offset: pos,
114 | length: 1,
115 | arabic: arabicNums[i]
116 | });
117 | pos = input.indexOf(chineseNums[i], pos + 1);
118 | }
119 | }
120 |
121 | // 按位置排序
122 | matches.sort((a, b) => a.offset - b.offset);
123 |
124 | // 生成替换版本
125 | if (matches.length > 0) {
126 | // 原始文本转换
127 | let converted = input;
128 | for (let match of matches) {
129 | if (match.type === 'arabic') {
130 | // 将阿拉伯数字转为中文数字
131 | const digits = match.value.split('');
132 | const chinese = digits.map(d => chineseNums[parseInt(d)]).join('');
133 | converted = converted.slice(0, match.offset) + chinese +
134 | converted.slice(match.offset + match.length);
135 | } else {
136 | // 将中文数字转为阿拉伯数字
137 | converted = converted.slice(0, match.offset) + match.arabic +
138 | converted.slice(match.offset + match.length);
139 | }
140 | }
141 | versions.add(converted);
142 | }
143 |
144 | return Array.from(versions);
145 | }
146 |
147 | // 检查文本是否包含关键字(考虑数字转换)
148 | function textIncludesKeyword(text, keyword) {
149 | // 移除所有空格并转换为小写再比较
150 | const cleanText = text.replace(/\s+/g, '').toLowerCase();
151 | const cleanKeyword = keyword.replace(/\s+/g, '').toLowerCase();
152 |
153 | // 获取文本的所有可能版本
154 | const textVersions = convertNumber(cleanText);
155 | const keywordVersions = convertNumber(cleanKeyword);
156 |
157 | // 交叉比对所有版本
158 | return keywordVersions.some(kw =>
159 | textVersions.some(txt => txt.includes(kw))
160 | );
161 | }
162 |
163 | // 为内容脚本通信设置
164 | // 注册消息处理程序,以便内容脚本可以直接检查元素是否应该被筛选
165 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
166 | if (!settings.extensionEnabled) {
167 | sendResponse({ shouldShow: true }); // 如果扩展被禁用,所有元素都显示
168 | return true;
169 | }
170 |
171 | // 消息类型:检查内容是否符合筛选条件
172 | if (message.type === 'CHECK_FILTER') {
173 | const { site, text, price, isSoldOut } = message.data;
174 |
175 | // 根据不同网站和条件进行判断
176 | let siteKeywords = [];
177 | let siteHideSoldOut = false;
178 |
179 | if (site === 'tixcraft') {
180 | siteKeywords = settings.tixcraftKeywords;
181 | siteHideSoldOut = settings.tixcraftHideSoldOut;
182 | } else if (site === 'kktix') {
183 | siteKeywords = settings.kktixKeywords;
184 | siteHideSoldOut = settings.kktixHideSoldOut;
185 | // 如果设置为显示所有价格,则不筛选
186 | if (settings.kktixShowAllPrices) {
187 | sendResponse({ shouldShow: !isSoldOut || !siteHideSoldOut });
188 | return true;
189 | }
190 | } else if (site === 'ibon') {
191 | siteKeywords = settings.ibonKeywords;
192 | siteHideSoldOut = settings.ibonHideSoldOut;
193 | } else if (site === 'cityline') {
194 | siteKeywords = settings.citylineKeywords;
195 | siteHideSoldOut = settings.citylineHideSoldOut;
196 | } else {
197 | sendResponse({ shouldShow: true }); // 不支持的网站直接显示
198 | return true;
199 | }
200 |
201 | // 如果设置隐藏已售完,且票券已售完,则隐藏
202 | if (siteHideSoldOut && isSoldOut) {
203 | sendResponse({ shouldShow: false });
204 | return true;
205 | }
206 |
207 | // 如果没有关键字,则显示所有非售罄票券
208 | if (!siteKeywords.length) {
209 | sendResponse({ shouldShow: true });
210 | return true;
211 | }
212 |
213 | // 将关键字组合处理为AND/OR逻辑组
214 | const keywordGroups = siteKeywords.map(keyword => {
215 | return keyword.split('+').map(k => {
216 | return k.split(',').map(item => item.trim()).filter(item => item);
217 | }).filter(group => group.length > 0);
218 | });
219 |
220 | // 根据关键字判断是否显示
221 | const searchTexts = [text, price].filter(t => t && typeof t === 'string');
222 | const shouldShow = keywordGroups.some(orGroup =>
223 | orGroup.some(andGroup => {
224 | return andGroup.every(keyword => {
225 | // 数字匹配
226 | if (!isNaN(keyword)) {
227 | const priceToFind = parseInt(keyword);
228 | return searchTexts.some(text => {
229 | const prices = (text.match(/\d+/g) || []).map(p => parseInt(p));
230 | return prices.includes(priceToFind);
231 | });
232 | } else {
233 | // 文本匹配
234 | return searchTexts.some(text => textIncludesKeyword(text, keyword));
235 | }
236 | });
237 | })
238 | );
239 |
240 | sendResponse({ shouldShow });
241 | return true;
242 | }
243 |
244 | // 消息类型:将设置应用于当前标签
245 | if (message.type === 'GET_SETTINGS') {
246 | const { site } = message.data;
247 |
248 | if (site === 'tixcraft') {
249 | sendResponse({
250 | keywords: settings.tixcraftKeywords,
251 | hideSoldOut: settings.tixcraftHideSoldOut,
252 | showServerTime: settings.showServerTime,
253 | showFilterStatus: settings.showFilterStatus,
254 | extensionEnabled: settings.extensionEnabled
255 | });
256 | } else if (site === 'kktix') {
257 | sendResponse({
258 | keywords: settings.kktixKeywords,
259 | showAllPrices: settings.kktixShowAllPrices,
260 | hideSoldOut: settings.kktixHideSoldOut,
261 | showServerTime: settings.showServerTime,
262 | showFilterStatus: settings.showFilterStatus,
263 | extensionEnabled: settings.extensionEnabled
264 | });
265 | } else if (site === 'ibon') {
266 | sendResponse({
267 | keywords: settings.ibonKeywords,
268 | hideSoldOut: settings.ibonHideSoldOut,
269 | showServerTime: settings.showServerTime,
270 | showFilterStatus: settings.showFilterStatus,
271 | extensionEnabled: settings.extensionEnabled
272 | });
273 | } else if (site === 'cityline') {
274 | sendResponse({
275 | keywords: settings.citylineKeywords,
276 | hideSoldOut: settings.citylineHideSoldOut,
277 | showServerTime: settings.showServerTime,
278 | showFilterStatus: settings.showFilterStatus,
279 | extensionEnabled: settings.extensionEnabled
280 | });
281 | } else {
282 | sendResponse({});
283 | }
284 | return true;
285 | }
286 | });
287 |
288 | // 保持service worker活跃
289 | chrome.runtime.onConnect.addListener((port) => {
290 | port.onDisconnect.addListener(() => {
291 | // 断开连接后可以记录日志或执行其他清理工作
292 | console.log('连接已断开');
293 | });
294 | });
295 |
296 | // 确保service worker不会过早终止
297 | chrome.runtime.onStartup.addListener(() => {
298 | loadSettings();
299 | console.log('浏览器启动,搶票柴柴扩展已激活');
300 | });
--------------------------------------------------------------------------------
/content.js:
--------------------------------------------------------------------------------
1 | // 等待DOM載入完成
2 | function waitForElement(selector) {
3 | return new Promise(resolve => {
4 | if (document.querySelector(selector)) {
5 | return resolve(document.querySelector(selector));
6 | }
7 |
8 | const observer = new MutationObserver(mutations => {
9 | if (document.querySelector(selector)) {
10 | observer.disconnect();
11 | resolve(document.querySelector(selector));
12 | }
13 | });
14 |
15 | observer.observe(document.body, {
16 | childList: true,
17 | subtree: true
18 | });
19 | });
20 | }
21 |
22 | // 通知早期加载脚本,content.js已加载
23 | window.addEventListener('DOMContentLoaded', () => {
24 | window.postMessage({ type: 'CONTENT_JS_LOADED' }, '*');
25 |
26 | // 尽早执行筛选
27 | setTimeout(() => {
28 | const site = getCurrentSite();
29 | if (site) {
30 | loadSettings();
31 | // 如果是 ibon 的 WEB網站入口頁面,立即執行篩選
32 | if (site === 'ibon' && window.location.href.includes('WEB網站入口')) {
33 | filterTickets();
34 | } else {
35 | filterTickets();
36 | }
37 | }
38 | }, 0);
39 |
40 | // 特別處理年代售票網站
41 | if (getCurrentSite() === 'ticket') {
42 | // 監聽頁面變化
43 | const observer = new MutationObserver((mutations) => {
44 | for (const mutation of mutations) {
45 | if (mutation.type === 'childList' || mutation.type === 'attributes') {
46 | // 檢查是否有票區列表
47 | const areaItems = document.querySelectorAll('.area-list li.main');
48 | if (areaItems.length > 0) {
49 | filterTickets();
50 | break; // 找到後立即退出循環
51 | }
52 | }
53 | }
54 | });
55 |
56 | // 監視整個文檔的變化
57 | observer.observe(document.documentElement, {
58 | childList: true,
59 | subtree: true,
60 | attributes: true
61 | });
62 |
63 | // 定期檢查並重新應用篩選(更頻繁的檢查)
64 | setInterval(() => {
65 | const areaItems = document.querySelectorAll('.area-list li.main');
66 | if (areaItems.length > 0) {
67 | filterTickets();
68 | }
69 | }, 500); // 每500ms檢查一次
70 | }
71 | });
72 |
73 | // 判斷當前網站
74 | function getCurrentSite() {
75 | const url = window.location.href;
76 | if (url.includes('tixcraft.com')) {
77 | return 'tixcraft';
78 | } else if (url.includes('kktix.com')) {
79 | return 'kktix';
80 | } else if (url.includes('ibon.com.tw')) {
81 | return 'ibon';
82 | } else if (url.includes('cityline.com')) {
83 | return 'cityline';
84 | } else if (url.includes('ticket.com.tw')) {
85 | return 'ticket';
86 | } else if (url.includes('ticketplus.com.tw')) {
87 | return 'ticketplus';
88 | }
89 | return null;
90 | }
91 |
92 | // 數字轉換函數
93 | function convertNumber(input) {
94 | const chineseNums = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
95 | const arabicNums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
96 |
97 | // 移除所有空格
98 | input = input.replace(/\s+/g, '');
99 |
100 | // 生成所有可能的版本
101 | let versions = new Set([input]);
102 |
103 | // 找出所有數字和中文數字的位置
104 | let matches = [];
105 | // 匹配阿拉伯數字
106 | input.replace(/\d+/g, (match, offset) => {
107 | matches.push({
108 | type: 'arabic',
109 | value: match,
110 | offset: offset,
111 | length: match.length
112 | });
113 | });
114 | // 匹配中文數字
115 | for (let i = 0; i < chineseNums.length; i++) {
116 | let pos = input.indexOf(chineseNums[i]);
117 | while (pos !== -1) {
118 | matches.push({
119 | type: 'chinese',
120 | value: chineseNums[i],
121 | offset: pos,
122 | length: 1,
123 | arabic: arabicNums[i]
124 | });
125 | pos = input.indexOf(chineseNums[i], pos + 1);
126 | }
127 | }
128 |
129 | // 按位置排序
130 | matches.sort((a, b) => a.offset - b.offset);
131 |
132 | // 生成替換版本
133 | if (matches.length > 0) {
134 | // 原始文本轉換
135 | let converted = input;
136 | for (let match of matches) {
137 | if (match.type === 'arabic') {
138 | // 將阿拉伯數字轉為中文數字
139 | const digits = match.value.split('');
140 | const chinese = digits.map(d => chineseNums[parseInt(d)]).join('');
141 | converted = converted.slice(0, match.offset) + chinese +
142 | converted.slice(match.offset + match.length);
143 | } else {
144 | // 將中文數字轉為阿拉伯數字
145 | converted = converted.slice(0, match.offset) + match.arabic +
146 | converted.slice(match.offset + match.length);
147 | }
148 | }
149 | versions.add(converted);
150 | }
151 |
152 | return Array.from(versions);
153 | }
154 |
155 | // 檢查文本是否包含關鍵字(考慮數字轉換)
156 | function textIncludesKeyword(text, keyword) {
157 | // 移除所有空格和逗號,並轉換為小寫再比較
158 | const cleanText = text.replace(/[\s,]+/g, '').toLowerCase();
159 |
160 | // 處理 OR 邏輯(用 + 分隔)
161 | const orParts = keyword.split('+').map(part => part.trim());
162 |
163 | // 如果任一 OR 條件符合就返回 true
164 | return orParts.some(orPart => {
165 | // 處理 AND 邏輯(用 , 分隔)
166 | const andParts = orPart.split(',').map(part => part.trim());
167 |
168 | // 所有 AND 條件都要符合才返回 true
169 | return andParts.every(andPart => {
170 | // 移除關鍵字中的逗號
171 | andPart = andPart.replace(/,/g, '');
172 |
173 | // 獲取文本的所有可能版本(包含數字轉換)
174 | const textVersions = convertNumber(cleanText);
175 | const keywordVersions = convertNumber(andPart.toLowerCase());
176 |
177 | // 交叉比對所有版本
178 | return keywordVersions.some(kw =>
179 | textVersions.some(txt => txt.includes(kw))
180 | );
181 | });
182 | });
183 | }
184 |
185 | let settings = {
186 | keywords: [],
187 | blacklist: [], // 新增黑名單設定
188 | hideSoldOut: false,
189 | isProcessing: false,
190 | showAllPrices: true
191 | };
192 |
193 | // 從storage載入設定
194 | function loadSettings() {
195 | const site = getCurrentSite();
196 |
197 | if (site === 'tixcraft') {
198 | chrome.storage.local.get(['keywords', 'blacklist', 'hideSoldOut'], (result) => {
199 | settings = {
200 | ...settings,
201 | ...result,
202 | blacklist: result.blacklist || []
203 | };
204 | if (document.readyState === 'complete') {
205 | filterTickets();
206 | showFilterStatus();
207 | } else {
208 | window.addEventListener('load', () => {
209 | filterTickets();
210 | showFilterStatus();
211 | });
212 | }
213 | });
214 | } else if (site === 'kktix') {
215 | chrome.storage.sync.get(['targetKeywords', 'blacklist', 'showAllPrices', 'hideSoldOut'], (result) => {
216 | if (result.targetKeywords) {
217 | settings.keywords = result.targetKeywords;
218 | }
219 | if (result.blacklist) {
220 | settings.blacklist = result.blacklist;
221 | }
222 | if (result.showAllPrices !== undefined) {
223 | settings.showAllPrices = result.showAllPrices;
224 | }
225 | if (result.hideSoldOut !== undefined) {
226 | settings.hideSoldOut = result.hideSoldOut;
227 | }
228 | if (document.readyState === 'complete') {
229 | filterTickets();
230 | showFilterStatus();
231 | } else {
232 | window.addEventListener('load', () => {
233 | filterTickets();
234 | showFilterStatus();
235 | });
236 | }
237 | });
238 | } else if (site === 'ibon') {
239 | chrome.storage.local.get(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], (result) => {
240 | if (result.ibonKeywords) {
241 | settings.keywords = result.ibonKeywords;
242 | }
243 | if (result.ibonBlacklist) {
244 | settings.blacklist = result.ibonBlacklist;
245 | }
246 | if (result.ibonHideSoldOut !== undefined) {
247 | settings.hideSoldOut = result.ibonHideSoldOut;
248 | }
249 | if (document.readyState === 'complete') {
250 | filterTickets();
251 | showFilterStatus();
252 | } else {
253 | window.addEventListener('load', () => {
254 | filterTickets();
255 | showFilterStatus();
256 | });
257 | }
258 | });
259 | } else if (site === 'cityline') {
260 | chrome.storage.local.get(['citylineKeywords', 'citylineBlacklist', 'citylineHideSoldOut'], (result) => {
261 | if (result.citylineKeywords) {
262 | settings.keywords = result.citylineKeywords;
263 | }
264 | if (result.citylineBlacklist) {
265 | settings.blacklist = result.citylineBlacklist;
266 | }
267 | if (result.citylineHideSoldOut !== undefined) {
268 | settings.hideSoldOut = result.citylineHideSoldOut;
269 | }
270 | if (document.readyState === 'complete') {
271 | filterTickets();
272 | showFilterStatus();
273 | } else {
274 | window.addEventListener('load', () => {
275 | filterTickets();
276 | showFilterStatus();
277 | });
278 | }
279 | });
280 | } else if (site === 'ticket') {
281 | chrome.storage.local.get(['ticketKeywords', 'ticketHideSoldOut'], (result) => {
282 | if (result.ticketKeywords) {
283 | settings.keywords = result.ticketKeywords;
284 | }
285 | if (result.ticketHideSoldOut !== undefined) {
286 | settings.hideSoldOut = result.ticketHideSoldOut;
287 | }
288 | if (document.readyState === 'complete') {
289 | filterTickets();
290 | showFilterStatus();
291 | } else {
292 | window.addEventListener('load', () => {
293 | filterTickets();
294 | showFilterStatus();
295 | });
296 | }
297 | });
298 | } else if (site === 'ticketplus') {
299 | chrome.storage.local.get(['ticketplusKeywords', 'ticketplusBlacklist', 'ticketplusHideSoldOut'], (result) => {
300 | if (result.ticketplusKeywords) {
301 | settings.keywords = result.ticketplusKeywords;
302 | }
303 | if (result.ticketplusBlacklist) {
304 | settings.blacklist = result.ticketplusBlacklist;
305 | }
306 | if (result.ticketplusHideSoldOut !== undefined) {
307 | settings.hideSoldOut = result.ticketplusHideSoldOut;
308 | }
309 | if (document.readyState === 'complete') {
310 | filterTickets();
311 | showFilterStatus();
312 | } else {
313 | window.addEventListener('load', () => {
314 | filterTickets();
315 | showFilterStatus();
316 | });
317 | }
318 | });
319 | }
320 | }
321 |
322 | // 初始化
323 | loadSettings();
324 |
325 | // 確保預設設置被正確設置
326 | chrome.storage.local.get(['showServerTime', 'showFilterStatus'], function(result) {
327 | // 如果設置尚未初始化,設置預設值
328 | const updates = {};
329 | if (result.showServerTime === undefined) {
330 | updates.showServerTime = true;
331 | }
332 | if (result.showFilterStatus === undefined) {
333 | updates.showFilterStatus = true;
334 | }
335 |
336 | // 如果有需要更新的設置,保存到storage
337 | if (Object.keys(updates).length > 0) {
338 | chrome.storage.local.set(updates);
339 | }
340 | });
341 |
342 | // 注入CSS樣式
343 | const style = document.createElement('style');
344 | style.textContent = `
345 | .ticket-filter-notification {
346 | position: fixed;
347 | right: 10px;
348 | padding: 8px 12px;
349 | background-color: #2684FF;
350 | color: #fff;
351 | border-radius: 4px;
352 | box-shadow: 0 2px 5px rgba(0,0,0,0.2);
353 | z-index: 9999;
354 | font-size: 14px;
355 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft JhengHei", sans-serif;
356 | }
357 |
358 | .server-time-display {
359 | position: fixed;
360 | top: 10px;
361 | right: 10px;
362 | padding: 8px 12px;
363 | background-color: #2684FF;
364 | color: #fff;
365 | border-radius: 4px;
366 | box-shadow: 0 2px 5px rgba(0,0,0,0.2);
367 | z-index: 10000;
368 | font-size: 14px;
369 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft JhengHei", sans-serif;
370 | }
371 |
372 | /* 遠大售票平台平滑過渡效果 */
373 | .v-expansion-panels.seats-area .v-expansion-panel {
374 | opacity: 0.1;
375 | transition: opacity 0.15s ease-out;
376 | }
377 |
378 | .v-expansion-panels.seats-area .v-expansion-panel.filter-processed {
379 | opacity: 1;
380 | }
381 | `;
382 | document.head.appendChild(style);
383 |
384 | // 顯示篩選狀態
385 | function showFilterStatus() {
386 | chrome.storage.local.get(['showServerTime', 'showFilterStatus', 'extensionEnabled'], function(result) {
387 | // 如果擴展被停用,移除所有顯示
388 | if (result.extensionEnabled === false) {
389 | const existingNotification = document.querySelector('.ticket-filter-notification');
390 | if (existingNotification) {
391 | existingNotification.remove();
392 | }
393 | const existingTimeDisplay = document.querySelector('.server-time-display');
394 | if (existingTimeDisplay) {
395 | existingTimeDisplay.remove();
396 | }
397 | return;
398 | }
399 |
400 | if (result.showFilterStatus === false) {
401 | const existingNotification = document.querySelector('.ticket-filter-notification');
402 | if (existingNotification) {
403 | existingNotification.remove();
404 | }
405 | return;
406 | }
407 |
408 | let notification = document.querySelector('.ticket-filter-notification');
409 | if (!notification) {
410 | notification = document.createElement('div');
411 | notification.className = 'ticket-filter-notification';
412 | document.body.appendChild(notification);
413 | }
414 |
415 | if (result.showServerTime) {
416 | notification.style.top = '50px'; // 在時間顯示下方
417 | } else {
418 | notification.style.top = '10px'; // 原本的位置
419 | }
420 |
421 | let statusText = [];
422 | const site = getCurrentSite();
423 |
424 | if (site === 'tixcraft') {
425 | let hasFilter = false;
426 | if (settings.keywords.length > 0) {
427 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`);
428 | hasFilter = true;
429 | }
430 | if (settings.blacklist && settings.blacklist.length > 0) {
431 | statusText.push(`黑名單:${settings.blacklist.join('、')}`);
432 | hasFilter = true;
433 | }
434 | if (!hasFilter) {
435 | statusText.push('無篩選條件');
436 | }
437 | if (settings.hideSoldOut) {
438 | statusText.push('不顯示已售完票券');
439 | }
440 | } else if (site === 'kktix') {
441 | let hasFilter = false;
442 | if (settings.keywords.length > 0) {
443 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`);
444 | hasFilter = true;
445 | }
446 | if (settings.blacklist && settings.blacklist.length > 0) {
447 | statusText.push(`黑名單:${settings.blacklist.join('、')}`);
448 | hasFilter = true;
449 | }
450 | if (!hasFilter) {
451 | statusText.push('無篩選條件');
452 | }
453 | if (settings.hideSoldOut) {
454 | statusText.push('不顯示已售完票券');
455 | }
456 | } else if (site === 'ibon') {
457 | let hasFilter = false;
458 | if (settings.keywords && settings.keywords.length > 0) {
459 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`);
460 | hasFilter = true;
461 | }
462 | if (settings.blacklist && settings.blacklist.length > 0) {
463 | statusText.push(`黑名單:${settings.blacklist.join('、')}`);
464 | hasFilter = true;
465 | }
466 | if (!hasFilter) {
467 | statusText.push('無篩選條件');
468 | }
469 | if (settings.hideSoldOut) {
470 | statusText.push('不顯示已售完票券');
471 | }
472 | } else if (site === 'cityline') {
473 | let hasFilter = false;
474 | if (settings.keywords && settings.keywords.length > 0) {
475 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`);
476 | hasFilter = true;
477 | }
478 | if (settings.blacklist && settings.blacklist.length > 0) {
479 | statusText.push(`黑名單:${settings.blacklist.join('、')}`);
480 | hasFilter = true;
481 | }
482 | if (!hasFilter) {
483 | statusText.push('無篩選條件');
484 | }
485 | if (settings.hideSoldOut) {
486 | statusText.push('不顯示已售完票券');
487 | }
488 | } else if (site === 'ticket') {
489 | let hasFilter = false;
490 | if (settings.keywords && settings.keywords.length > 0) {
491 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`);
492 | hasFilter = true;
493 | }
494 | if (settings.blacklist && settings.blacklist.length > 0) {
495 | statusText.push(`黑名單:${settings.blacklist.join('、')}`);
496 | hasFilter = true;
497 | }
498 | if (!hasFilter) {
499 | statusText.push('無篩選條件');
500 | }
501 | if (settings.hideSoldOut) {
502 | statusText.push('不顯示已售完票券');
503 | }
504 | } else if (site === 'ticketplus') {
505 | let hasFilter = false;
506 | if (settings.keywords && settings.keywords.length > 0) {
507 | statusText.push(`關鍵字篩選:${settings.keywords.join('、')}`);
508 | hasFilter = true;
509 | }
510 | if (settings.blacklist && settings.blacklist.length > 0) {
511 | statusText.push(`黑名單:${settings.blacklist.join('、')}`);
512 | hasFilter = true;
513 | }
514 | if (!hasFilter) {
515 | statusText.push('無篩選條件');
516 | }
517 | if (settings.hideSoldOut) {
518 | statusText.push('不顯示已售完票券');
519 | }
520 | }
521 |
522 | const newText = statusText.join(' | ');
523 | if (notification.textContent !== newText) {
524 | notification.textContent = newText;
525 | }
526 | });
527 | }
528 |
529 | // 顯示所有區域
530 | function showAllAreas() {
531 | const site = getCurrentSite();
532 | if (site === 'tixcraft') {
533 | document.querySelectorAll('.area-list li').forEach(item => {
534 | item.style.display = '';
535 | });
536 | document.querySelectorAll('.zone-label').forEach(label => {
537 | label.style.display = '';
538 | });
539 | } else if (site === 'kktix') {
540 | document.querySelectorAll('.ticket-unit').forEach(item => {
541 | item.style.display = '';
542 | });
543 | } else if (site === 'ibon') {
544 | function showAllTickets(element) {
545 | if (element.shadowRoot) {
546 | const rows = element.shadowRoot.querySelectorAll('tr[id^="B"]');
547 | rows.forEach(row => {
548 | // 移除所有由擴充功能添加的樣式和類別
549 | row.style.removeProperty('display');
550 | row.classList.remove('hidden-by-extension');
551 | row.classList.remove('filter-processed');
552 | Array.from(row.children).forEach(cell => {
553 | cell.style.removeProperty('display');
554 | });
555 | });
556 |
557 | // 移除 shadow root 中的過濾樣式
558 | const filterStyle = element.shadowRoot.querySelector('.extension-filter-style');
559 | if (filterStyle) {
560 | filterStyle.remove();
561 | }
562 | }
563 | Array.from(element.children || []).forEach(showAllTickets);
564 | }
565 | showAllTickets(document.documentElement);
566 |
567 | // 強制更新頁面狀態
568 | setTimeout(() => {
569 | window.dispatchEvent(new Event('resize'));
570 | }, 100);
571 | } else if (site === 'cityline') {
572 | document.querySelectorAll('.form-check').forEach(item => {
573 | item.style.display = '';
574 | });
575 | } else if (site === 'ticket') {
576 | document.querySelectorAll('.area-list li').forEach(item => {
577 | item.style.display = '';
578 | });
579 | } else if (site === 'ticketplus') {
580 | document.querySelectorAll('.v-expansion-panels.seats-area .v-expansion-panel').forEach(panel => {
581 | panel.style.display = '';
582 | panel.classList.add('filter-processed');
583 | });
584 | }
585 |
586 | // 通知早期加载脚本,已显示所有票券
587 | window.postMessage({ type: 'FILTER_APPLIED' }, '*');
588 | }
589 |
590 | // 主要篩選功能
591 | async function filterTickets() {
592 | // 檢查擴展是否被停用
593 | const result = await new Promise(resolve => {
594 | chrome.storage.local.get(['extensionEnabled'], resolve);
595 | });
596 |
597 | if (result.extensionEnabled === false) {
598 | showAllAreas();
599 | return;
600 | }
601 |
602 | if (settings.isProcessing) return;
603 | settings.isProcessing = true;
604 |
605 | try {
606 | const site = getCurrentSite();
607 |
608 | // 根據不同網站執行對應的過濾邏輯
609 | if (site === 'tixcraft' && window.location.href.includes('/ticket/area/')) {
610 | await filterTixcraftTickets();
611 | } else if (site === 'kktix' && document.querySelector('.ticket-unit')) {
612 | await filterKKTIXTickets();
613 | } else if (site === 'ibon') {
614 | await filterIbonTickets();
615 | } else if (site === 'cityline' && document.querySelector('.price-box1')) {
616 | await filterCitylineTickets();
617 | } else if (site === 'ticket') {
618 | await filterTicketComTickets();
619 | } else if (site === 'ticketplus') {
620 | await filterTicketPlusTickets();
621 | }
622 |
623 | // 通知早期加載腳本,過濾已完成
624 | window.postMessage({ type: 'FILTER_APPLIED' }, '*');
625 | } catch (error) {
626 | console.error('過濾票券時發生錯誤:', error);
627 | // 即使出錯也要通知移除隱藏樣式
628 | window.postMessage({ type: 'FILTER_APPLIED' }, '*');
629 | } finally {
630 | settings.isProcessing = false;
631 | }
632 | }
633 |
634 | // KKTIX票券篩選
635 | async function filterKKTIXTickets() {
636 | const ticketRows = document.querySelectorAll('.ticket-unit');
637 | if (!ticketRows.length) return;
638 |
639 | showAllAreas();
640 |
641 | // 如果沒有任何篩選條件,只處理是否隱藏已售完
642 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) {
643 | ticketRows.forEach(row => {
644 | const isSoldOut = row.textContent.includes('已售完');
645 | if (settings.hideSoldOut && isSoldOut) {
646 | row.style.display = 'none';
647 | } else {
648 | row.style.display = '';
649 | }
650 | });
651 | return;
652 | }
653 |
654 | // 處理每個票券
655 | ticketRows.forEach(row => {
656 | const priceElement = row.querySelector('.ticket-price');
657 | const nameElement = row.querySelector('.ticket-name');
658 |
659 | if (priceElement && nameElement) {
660 | const price = priceElement.textContent.trim();
661 | const name = nameElement.textContent.trim();
662 | const text = name + ' ' + price;
663 | const isSoldOut = row.textContent.includes('已售完');
664 |
665 | // 檢查是否在黑名單中
666 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 &&
667 | settings.blacklist.some(blacklistItem => {
668 | // 處理 OR 邏輯(用 + 分隔)
669 | const orParts = blacklistItem.split('+').map(part => part.trim());
670 | return orParts.some(orPart => {
671 | // 處理 AND 邏輯(用 , 分隔)
672 | const andParts = orPart.split(',').map(part => part.trim());
673 | return andParts.every(andPart => textIncludesKeyword(text, andPart));
674 | });
675 | });
676 |
677 | // 檢查是否符合關鍵字
678 | const matchesKeyword = settings.keywords.length === 0 ||
679 | settings.keywords.some(keyword => {
680 | // 處理 OR 邏輯(用 + 分隔)
681 | const orParts = keyword.split('+').map(part => part.trim());
682 | return orParts.some(orPart => {
683 | // 處理 AND 邏輯(用 , 分隔)
684 | const andParts = orPart.split(',').map(part => part.trim());
685 | return andParts.every(andPart => textIncludesKeyword(text, andPart));
686 | });
687 | });
688 |
689 | // 決定是否顯示
690 | let shouldShow = true;
691 |
692 | // 如果在黑名單中,不顯示
693 | if (isBlacklisted) {
694 | shouldShow = false;
695 | }
696 |
697 | // 如果有關鍵字且不符合關鍵字,不顯示
698 | if (settings.keywords.length > 0 && !matchesKeyword) {
699 | shouldShow = false;
700 | }
701 |
702 | // 如果設定隱藏已售完且已售完,不顯示
703 | if (settings.hideSoldOut && isSoldOut) {
704 | shouldShow = false;
705 | }
706 |
707 | // 立即應用顯示/隱藏狀態
708 | row.style.setProperty('display', shouldShow ? '' : 'none', 'important');
709 | }
710 | });
711 | }
712 |
713 | // 拓元票券篩選
714 | async function filterTixcraftTickets() {
715 | await waitForElement('.zone-label[data-id]');
716 |
717 | const areaGroups = document.querySelectorAll('.zone-label[data-id]');
718 | if (!areaGroups.length) return;
719 |
720 | showAllAreas();
721 |
722 | // 如果沒有任何篩選條件,只處理是否隱藏已售完
723 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) {
724 | if (settings.hideSoldOut) {
725 | document.querySelectorAll('.area-list li').forEach(item => {
726 | const remainingText = item.querySelector('font')?.textContent.trim() || '';
727 | if (remainingText.includes('已售完')) {
728 | item.style.display = 'none';
729 | } else {
730 | item.style.display = '';
731 | }
732 | });
733 | }
734 | return;
735 | }
736 |
737 | areaGroups.forEach(group => {
738 | const groupId = group.dataset.id;
739 | const areaList = document.getElementById(groupId);
740 | if (!areaList) return;
741 |
742 | const items = areaList.querySelectorAll('li');
743 | let hasVisibleItems = false;
744 |
745 | const groupTitle = group.querySelector('b')?.textContent.trim() || group.textContent.trim();
746 |
747 | items.forEach(item => {
748 | const areaText = item.textContent.trim();
749 | const remainingText = item.querySelector('font')?.textContent.trim() || '';
750 | const isSoldOut = remainingText.includes('已售完');
751 | const fullText = groupTitle + ' ' + areaText;
752 |
753 | // 檢查是否在黑名單中
754 | const isBlacklisted = isInBlacklist(fullText, settings.blacklist);
755 |
756 | // 檢查是否符合關鍵字
757 | const matchesKeyword = settings.keywords.length === 0 || settings.keywords.some(keyword =>
758 | textIncludesKeyword(fullText, keyword)
759 | );
760 |
761 | // 決定是否顯示
762 | let shouldShow = true;
763 |
764 | // 如果有黑名單項目且符合黑名單,則不顯示
765 | if (settings.blacklist && settings.blacklist.length > 0 && isBlacklisted) {
766 | shouldShow = false;
767 | }
768 |
769 | // 如果有關鍵字且不符合關鍵字,則不顯示
770 | if (settings.keywords.length > 0 && !matchesKeyword) {
771 | shouldShow = false;
772 | }
773 |
774 | // 如果設定隱藏已售完且已售完,則不顯示
775 | if (settings.hideSoldOut && isSoldOut) {
776 | shouldShow = false;
777 | }
778 |
779 | item.style.display = shouldShow ? '' : 'none';
780 | if (shouldShow) {
781 | hasVisibleItems = true;
782 | }
783 | });
784 |
785 | group.style.display = hasVisibleItems ? '' : 'none';
786 | });
787 | }
788 |
789 | // ibon票券篩選
790 | async function filterIbonTickets() {
791 | function processTicketAreas(element) {
792 | if (element.shadowRoot) {
793 | const rows = element.shadowRoot.querySelectorAll('tr[id^="B"]');
794 | if (rows.length > 0) {
795 | rows.forEach(row => {
796 | // 只保留篩選邏輯,移除 shadowRoot 處理
797 | const areaCell = row.querySelector('td[data-title="票區"]');
798 | const priceCell = row.querySelector('td[data-title="票價(NT$)"]');
799 |
800 | if (areaCell && priceCell) {
801 | const areaText = areaCell.textContent.trim();
802 | const priceText = priceCell.textContent.replace(/,/g, '').trim();
803 | const fullText = `${areaText} ${priceText}`;
804 | const isSoldOut = row.textContent.includes('售完');
805 |
806 | // 檢查是否在黑名單中
807 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 &&
808 | settings.blacklist.some(blacklistItem => {
809 | const orParts = blacklistItem.split('+').map(part => part.trim());
810 | return orParts.some(orPart => {
811 | const andParts = orPart.split(',').map(part => part.trim());
812 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart));
813 | });
814 | });
815 |
816 | // 檢查是否符合關鍵字
817 | const matchesKeyword = settings.keywords.length === 0 ||
818 | settings.keywords.some(keyword => {
819 | const orParts = keyword.split('+').map(part => part.trim());
820 | return orParts.some(orPart => {
821 | const andParts = orPart.split(',').map(part => part.trim());
822 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart));
823 | });
824 | });
825 |
826 | // 決定是否顯示
827 | let shouldShow = true;
828 |
829 | if (isBlacklisted) shouldShow = false;
830 | if (settings.keywords.length > 0 && !matchesKeyword) shouldShow = false;
831 | if (settings.hideSoldOut && isSoldOut) shouldShow = false;
832 |
833 | // 應用顯示/隱藏狀態
834 | if (!shouldShow) {
835 | row.style.setProperty('display', 'none', 'important');
836 | row.classList.add('hidden-by-extension');
837 | Array.from(row.children).forEach(cell => {
838 | cell.style.setProperty('display', 'none', 'important');
839 | });
840 | } else {
841 | row.style.removeProperty('display');
842 | row.classList.remove('hidden-by-extension');
843 | Array.from(row.children).forEach(cell => {
844 | cell.style.removeProperty('display');
845 | });
846 | }
847 | }
848 | });
849 | }
850 | }
851 |
852 | Array.from(element.children || []).forEach(processTicketAreas);
853 | }
854 |
855 | processTicketAreas(document.documentElement);
856 | }
857 |
858 | // Cityline票券篩選
859 | async function filterCitylineTickets() {
860 | const ticketItems = document.querySelectorAll('.form-check');
861 | if (!ticketItems.length) return;
862 |
863 | // 標記所有票券元素為"準備好被篩選"
864 | document.querySelectorAll('.price-box1, .form-check').forEach(el => {
865 | el.classList.add('filter-ready');
866 | });
867 |
868 | // 如果沒有任何篩選條件,只處理是否隱藏已售完
869 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) {
870 | if (settings.hideSoldOut) {
871 | ticketItems.forEach(item => {
872 | const isSoldOut = item.textContent.includes('售罄') ||
873 | item.querySelector('input[data-disabled="true"]') !== null;
874 | item.style.display = isSoldOut ? 'none' : '';
875 | if (!isSoldOut) {
876 | item.style.visibility = 'visible';
877 | item.style.opacity = '1';
878 | }
879 | });
880 | } else {
881 | ticketItems.forEach(item => {
882 | item.style.display = '';
883 | item.style.visibility = 'visible';
884 | item.style.opacity = '1';
885 | });
886 | }
887 | return;
888 | }
889 |
890 | ticketItems.forEach(item => {
891 | const priceText = item.querySelector('.price-num')?.textContent.trim() || '';
892 | const degreeText = item.querySelector('.price-degree')?.textContent.trim() || '';
893 | const isSoldOut = item.textContent.includes('售罄') ||
894 | item.querySelector('input[data-disabled="true"]') !== null;
895 |
896 | const fullText = `${degreeText} ${priceText}`;
897 |
898 | // 檢查是否在黑名單中
899 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 &&
900 | settings.blacklist.some(blacklistItem => textIncludesKeyword(fullText, blacklistItem));
901 |
902 | // 檢查是否符合關鍵字
903 | const matchesKeyword = settings.keywords.length === 0 ||
904 | settings.keywords.some(keyword => textIncludesKeyword(fullText, keyword));
905 |
906 | // 決定是否顯示
907 | let shouldShow = true;
908 |
909 | // 如果在黑名單中,不顯示
910 | if (isBlacklisted) {
911 | shouldShow = false;
912 | }
913 |
914 | // 如果有關鍵字且不符合關鍵字,不顯示
915 | if (settings.keywords.length > 0 && !matchesKeyword) {
916 | shouldShow = false;
917 | }
918 |
919 | // 如果設定隱藏已售完且已售完,不顯示
920 | if (settings.hideSoldOut && isSoldOut) {
921 | shouldShow = false;
922 | }
923 |
924 | // 立即應用顯示/隱藏狀態
925 | if (shouldShow) {
926 | item.style.display = '';
927 | item.style.visibility = 'visible';
928 | item.style.opacity = '1';
929 | } else {
930 | item.style.display = 'none';
931 | }
932 | });
933 | }
934 |
935 | // 年代售票網站篩選
936 | async function filterTicketComTickets() {
937 | // 使用輪詢機制等待票區列表加載
938 | let retryCount = 0;
939 | const maxRetries = 20; // 最多等待20次
940 | const retryInterval = 100; // 每100ms檢查一次,總共最多等待2秒
941 |
942 | async function waitForTicketList() {
943 | return new Promise((resolve) => {
944 | const checkElements = () => {
945 | const areaItems = document.querySelectorAll('.area-list li.main');
946 | if (areaItems.length > 0) {
947 | resolve(true);
948 | } else if (retryCount < maxRetries) {
949 | retryCount++;
950 | setTimeout(checkElements, retryInterval);
951 | } else {
952 | resolve(false);
953 | }
954 | };
955 | checkElements();
956 | });
957 | }
958 |
959 | // 等待票區列表加載
960 | const isLoaded = await waitForTicketList();
961 | if (!isLoaded) {
962 | console.log('票區列表加載超時');
963 | return;
964 | }
965 |
966 | const areaItems = document.querySelectorAll('.area-list li.main');
967 | if (!areaItems.length) return;
968 |
969 | // 使用 setProperty 來確保樣式被正確應用
970 | function setDisplayStyle(element, show) {
971 | if (show) {
972 | element.style.setProperty('display', '', 'important');
973 | element.style.setProperty('visibility', 'visible', 'important');
974 | } else {
975 | element.style.setProperty('display', 'none', 'important');
976 | }
977 | }
978 |
979 | // 如果沒有關鍵字,則只處理是否隱藏已售完
980 | if (settings.keywords.length === 0) {
981 | if (settings.hideSoldOut) {
982 | areaItems.forEach(item => {
983 | const soldOutText = item.querySelector('font[color="#FF0000"]')?.textContent || '';
984 | const badgeText = item.querySelector('.badge')?.textContent || '';
985 | setDisplayStyle(item, soldOutText.includes('熱賣中') && !badgeText.includes('剩位'));
986 | });
987 | }
988 | return;
989 | }
990 |
991 | // 支援AND/OR邏輯的關鍵字處理
992 | const keywordGroups = settings.keywords.map(keyword => {
993 | return keyword.split('+').map(k => {
994 | return k.split(',').map(item => item.trim()).filter(item => item);
995 | }).filter(group => group.length > 0);
996 | });
997 |
998 | areaItems.forEach(item => {
999 | const areaText = item.querySelector('font[color="#333"]')?.textContent || '';
1000 | const statusText = item.querySelector('font[color="#FF0000"]')?.textContent || '';
1001 | const badgeText = item.querySelector('.badge')?.textContent || '';
1002 | const isSoldOut = !statusText.includes('熱賣中') || badgeText.includes('剩位');
1003 |
1004 | const shouldShow = keywordGroups.some(orGroup =>
1005 | orGroup.some(andGroup => {
1006 | return andGroup.every(keyword => {
1007 | if (!isNaN(keyword)) {
1008 | // 數字比對
1009 | const priceToFind = parseInt(keyword);
1010 | const prices = (areaText.match(/\d+/g) || []).map(p => parseInt(p));
1011 | return prices.includes(priceToFind);
1012 | } else {
1013 | // 文字比對
1014 | return textIncludesKeyword(areaText, keyword);
1015 | }
1016 | });
1017 | })
1018 | );
1019 |
1020 | setDisplayStyle(item, shouldShow && (!isSoldOut || !settings.hideSoldOut));
1021 | });
1022 | }
1023 |
1024 | // 遠大售票平台票券篩選
1025 | async function filterTicketPlusTickets() {
1026 | // 等待票券元素出現
1027 | const waitForTickets = () => {
1028 | return new Promise((resolve) => {
1029 | const check = () => {
1030 | const tickets = document.querySelectorAll('.v-expansion-panels.seats-area .v-expansion-panel');
1031 | if (tickets.length > 0) {
1032 | resolve(tickets);
1033 | } else {
1034 | requestAnimationFrame(check);
1035 | }
1036 | };
1037 | check();
1038 | });
1039 | };
1040 |
1041 | const ticketPanels = await waitForTickets();
1042 |
1043 | // 如果沒有任何篩選條件,顯示所有票券
1044 | if (settings.keywords.length === 0 && (!settings.blacklist || settings.blacklist.length === 0)) {
1045 | ticketPanels.forEach(panel => {
1046 | const soldOutText = panel.querySelector('.soldout');
1047 | const remainingText = panel.querySelector('small');
1048 |
1049 | if (settings.hideSoldOut && (soldOutText || (remainingText && remainingText.textContent.includes('剩餘 0')))) {
1050 | panel.style.display = 'none';
1051 | } else {
1052 | panel.style.display = '';
1053 | panel.classList.add('filter-processed');
1054 | }
1055 | });
1056 | return;
1057 | }
1058 |
1059 | ticketPanels.forEach(panel => {
1060 | const areaText = panel.querySelector('.d-flex.align-center')?.textContent.trim() || '';
1061 | const priceText = panel.querySelector('.text-right.col.col-4')?.textContent.trim() || '';
1062 | const soldOutText = panel.querySelector('.soldout');
1063 | const remainingText = panel.querySelector('small');
1064 |
1065 | const isSoldOut = soldOutText || (remainingText && remainingText.textContent.includes('剩餘 0'));
1066 | const fullText = `${areaText} ${priceText}`;
1067 |
1068 | // 檢查是否在黑名單中
1069 | const isBlacklisted = settings.blacklist && settings.blacklist.length > 0 &&
1070 | settings.blacklist.some(blacklistItem => {
1071 | const orParts = blacklistItem.split('+').map(part => part.trim());
1072 | return orParts.some(orPart => {
1073 | const andParts = orPart.split(',').map(part => part.trim());
1074 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart));
1075 | });
1076 | });
1077 |
1078 | // 檢查是否符合關鍵字
1079 | const matchesKeyword = settings.keywords.length === 0 ||
1080 | settings.keywords.some(keyword => {
1081 | const orParts = keyword.split('+').map(part => part.trim());
1082 | return orParts.some(orPart => {
1083 | const andParts = orPart.split(',').map(part => part.trim());
1084 | return andParts.every(andPart => textIncludesKeyword(fullText, andPart));
1085 | });
1086 | });
1087 |
1088 | // 決定是否顯示
1089 | let shouldShow = true;
1090 |
1091 | if (isBlacklisted) shouldShow = false;
1092 | if (settings.keywords.length > 0 && !matchesKeyword) shouldShow = false;
1093 | if (settings.hideSoldOut && isSoldOut) shouldShow = false;
1094 |
1095 | // 應用顯示/隱藏狀態
1096 | if (shouldShow) {
1097 | panel.style.display = '';
1098 | panel.classList.add('filter-processed');
1099 | } else {
1100 | panel.style.display = 'none';
1101 | panel.classList.remove('filter-processed');
1102 | }
1103 | });
1104 | }
1105 |
1106 | // 新增:檢查文本是否在黑名單中
1107 | function isInBlacklist(text, blacklist) {
1108 | if (!blacklist || blacklist.length === 0) return false;
1109 |
1110 | // 將文本轉換為小寫並移除多餘空格,以便比對
1111 | const normalizedText = text.toLowerCase().trim();
1112 |
1113 | // 檢查每個黑名單關鍵字
1114 | return blacklist.some(blacklistItem => {
1115 | // 處理 OR 邏輯(用 + 分隔)
1116 | const orParts = blacklistItem.split('+').map(part => part.trim());
1117 |
1118 | // 如果任一 OR 條件符合就返回 true
1119 | return orParts.some(orPart => {
1120 | // 處理 AND 邏輯(用 , 分隔)
1121 | const andParts = orPart.split(',').map(part => part.trim());
1122 |
1123 | // 所有 AND 條件都要符合才返回 true
1124 | return andParts.every(andPart => {
1125 | if (!andPart) return false; // 空字串不納入比對
1126 |
1127 | // 使用 textIncludesKeyword 進行比對,支援數字轉換
1128 | return textIncludesKeyword(normalizedText, andPart);
1129 | });
1130 | });
1131 | });
1132 | }
1133 |
1134 | // 監聽來自popup的訊息
1135 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
1136 | try {
1137 | const site = getCurrentSite();
1138 |
1139 | // 處理拓元的消息
1140 | if (site === 'tixcraft') {
1141 | if (request.type === 'UPDATE_SETTINGS') {
1142 | settings = { ...settings, ...request.settings };
1143 | filterTickets();
1144 | showFilterStatus();
1145 | sendResponse({ success: true });
1146 | } else if (request.type === 'SHOW_ALL') {
1147 | settings.keywords = [];
1148 | settings.blacklist = [];
1149 | settings.hideSoldOut = false;
1150 |
1151 | // 通知 popup 更新標籤
1152 | chrome.runtime.sendMessage({
1153 | type: 'UPDATE_POPUP_LABELS',
1154 | settings: {
1155 | keywords: [],
1156 | blacklist: [],
1157 | hideSoldOut: false
1158 | }
1159 | });
1160 |
1161 | showAllAreas();
1162 | showFilterStatus();
1163 | sendResponse({ success: true });
1164 | }
1165 | }
1166 |
1167 | // 處理KKTIX的消息
1168 | if (site === 'kktix') {
1169 | if (request.action === 'updateFilter') {
1170 | settings.keywords = request.keywords || [];
1171 | settings.blacklist = request.blacklist || []; // 確保更新黑名單
1172 | settings.showAllPrices = false;
1173 | if (request.settings && request.settings.hideSoldOut !== undefined) {
1174 | settings.hideSoldOut = request.settings.hideSoldOut;
1175 | }
1176 |
1177 | // 立即執行篩選
1178 | filterTickets();
1179 | showFilterStatus();
1180 | sendResponse({ success: true });
1181 | return true;
1182 | }
1183 | if (request.action === 'showAllPrices') {
1184 | settings.keywords = [];
1185 | settings.blacklist = []; // 清空黑名單
1186 | settings.showAllPrices = true;
1187 | settings.hideSoldOut = false;
1188 |
1189 | filterTickets();
1190 | showFilterStatus();
1191 | sendResponse({ success: true });
1192 | return true;
1193 | }
1194 | if (request.action === 'updateBlacklist') { // 新增:處理黑名單更新
1195 | settings.blacklist = request.blacklist || [];
1196 | filterTickets();
1197 | showFilterStatus();
1198 | sendResponse({ success: true });
1199 | return true;
1200 | }
1201 | }
1202 |
1203 | // 處理ibon的消息
1204 | if (site === 'ibon') {
1205 | if (request.type === 'UPDATE_IBON_SETTINGS') {
1206 | console.log('收到ibon設定更新:', request.settings);
1207 | // 重置所有設定
1208 | settings = {
1209 | keywords: request.settings.keywords || [],
1210 | blacklist: request.settings.blacklist || [],
1211 | hideSoldOut: request.settings.hideSoldOut || false,
1212 | isProcessing: false
1213 | };
1214 |
1215 | // 清除所有過濾狀態
1216 | showAllAreas();
1217 |
1218 | // 更新顯示狀態
1219 | showFilterStatus();
1220 |
1221 | // 如果有新的篩選條件,則重新應用
1222 | if (settings.keywords.length > 0 || settings.blacklist.length > 0 || settings.hideSoldOut) {
1223 | filterTickets();
1224 | }
1225 |
1226 | sendResponse({ success: true });
1227 | return true;
1228 | }
1229 | }
1230 |
1231 | // 處理cityline的消息
1232 | if (site === 'cityline') {
1233 | if (request.type === 'UPDATE_CITYLINE_SETTINGS') {
1234 | settings.keywords = request.settings.keywords || [];
1235 | settings.blacklist = request.settings.blacklist || []; // 添加黑名單設定
1236 | settings.hideSoldOut = request.settings.hideSoldOut || false;
1237 | filterTickets();
1238 | showFilterStatus();
1239 | sendResponse({ success: true });
1240 | }
1241 | }
1242 |
1243 | // 處理年代售票網站的消息
1244 | if (site === 'ticket') {
1245 | if (request.type === 'UPDATE_TICKET_SETTINGS') {
1246 | settings.keywords = request.settings.keywords || [];
1247 | settings.hideSoldOut = request.settings.hideSoldOut || false;
1248 | filterTickets();
1249 | showFilterStatus();
1250 | sendResponse({ success: true });
1251 | }
1252 | }
1253 |
1254 | // 處理遠大售票平台的消息
1255 | if (site === 'ticketplus') {
1256 | if (request.type === 'UPDATE_TICKETPLUS_SETTINGS') {
1257 | settings.keywords = request.settings.keywords || [];
1258 | settings.blacklist = request.settings.blacklist || [];
1259 | settings.hideSoldOut = request.settings.hideSoldOut || false;
1260 | filterTickets();
1261 | showFilterStatus();
1262 | sendResponse({ success: true });
1263 | }
1264 | }
1265 |
1266 | // 在 ibon 票券頁面時觸發重整
1267 | if (site === 'ibon' && location.href.includes('UTK0201_000.aspx')) {
1268 | console.log("ibon票券頁面,自動重整頁面");
1269 | setTimeout(() => location.reload(), 100); // 簡短延遲後重整
1270 | }
1271 | } catch (error) {
1272 | console.log('處理訊息時發生錯誤:', error);
1273 | sendResponse({ success: false, error: error.message });
1274 | }
1275 | return true;
1276 | });
1277 |
1278 | // 初始執行
1279 | const site = getCurrentSite();
1280 | if (site === 'tixcraft' || site === 'kktix' || site === 'ibon' || site === 'cityline' || site === 'ticket' || site === 'ticketplus') {
1281 | if (document.readyState === 'complete') {
1282 | showFilterStatus();
1283 | filterTickets();
1284 | } else {
1285 | window.addEventListener('load', () => {
1286 | showFilterStatus();
1287 | filterTickets();
1288 | });
1289 | }
1290 | }
1291 |
1292 | // 監聽DOM變化
1293 | let filterDebounceTimer = null;
1294 | let statusDebounceTimer = null;
1295 | const observer = new MutationObserver((mutations) => {
1296 | const site = getCurrentSite();
1297 | if (!site) return;
1298 |
1299 | // 使用不同的計時器處理篩選和狀態顯示
1300 | if (filterDebounceTimer) {
1301 | clearTimeout(filterDebounceTimer);
1302 | }
1303 | if (statusDebounceTimer) {
1304 | clearTimeout(statusDebounceTimer);
1305 | }
1306 |
1307 | // 根據網站定義不同的延遲時間
1308 | const debounceTime = getDebounceTime();
1309 |
1310 | // 篩選功能的防抖
1311 | filterDebounceTimer = setTimeout(() => {
1312 | if (site === 'tixcraft' && window.location.href.includes('/ticket/area/')) {
1313 | filterTickets();
1314 | } else if (site === 'kktix' && document.querySelector('.ticket-unit')) {
1315 | filterTickets();
1316 | } else if (site === 'ibon') {
1317 | filterTickets();
1318 | } else if (site === 'cityline' && document.querySelector('.price-box1')) {
1319 | filterTickets();
1320 | } else if (site === 'ticket') {
1321 | filterTickets();
1322 | } else if (site === 'ticketplus') {
1323 | filterTickets();
1324 | }
1325 | }, debounceTime);
1326 |
1327 | // 狀態顯示的防抖,使用相同的延遲時間
1328 | statusDebounceTimer = setTimeout(() => {
1329 | showFilterStatus();
1330 | }, debounceTime);
1331 | });
1332 |
1333 | // 使用更高效的觀察配置
1334 | observer.observe(document.body, {
1335 | childList: true,
1336 | subtree: true,
1337 | attributes: true,
1338 | attributeFilter: ['style', 'class', 'disabled'],
1339 | characterData: false
1340 | });
1341 |
1342 | // 處理頁面從背景切回前景時的情況
1343 | document.addEventListener('visibilitychange', function() {
1344 | if (document.visibilityState === 'visible') {
1345 | loadSettings();
1346 | }
1347 | });
1348 |
1349 | // 顯示時間
1350 | function showServerTime() {
1351 | chrome.storage.local.get(['showServerTime', 'extensionEnabled'], function(result) {
1352 | if (result.showServerTime === false || result.extensionEnabled === false) {
1353 | const existingDisplay = document.querySelector('.server-time-display');
1354 | if (existingDisplay) {
1355 | existingDisplay.remove();
1356 | }
1357 |
1358 | // 時間顯示框被移除,更新篩選條件框位置
1359 | const notification = document.querySelector('.ticket-filter-notification');
1360 | if (notification) {
1361 | notification.style.top = '10px';
1362 | }
1363 |
1364 | return;
1365 | }
1366 |
1367 | let timeDisplay = document.querySelector('.server-time-display');
1368 | if (!timeDisplay) {
1369 | timeDisplay = document.createElement('div');
1370 | timeDisplay.className = 'server-time-display';
1371 | document.body.appendChild(timeDisplay);
1372 |
1373 | // 時間顯示框添加後,更新篩選條件框位置
1374 | const notification = document.querySelector('.ticket-filter-notification');
1375 | if (notification) {
1376 | notification.style.top = '50px';
1377 | }
1378 | }
1379 |
1380 | // 定時更新顯示本地時間(CST)
1381 | function updateDisplay() {
1382 | const currentTime = new Date();
1383 | timeDisplay.textContent = `本地時間:${currentTime.toLocaleString('zh-TW', {
1384 | timeZone: 'Asia/Taipei',
1385 | hour12: false,
1386 | hour: '2-digit',
1387 | minute: '2-digit',
1388 | second: '2-digit'
1389 | })}`;
1390 | }
1391 |
1392 | // 每秒更新一次時間
1393 | const displayInterval = setInterval(updateDisplay, 1000);
1394 |
1395 | // 立即顯示時間
1396 | updateDisplay();
1397 |
1398 | // 清理函數
1399 | function cleanup() {
1400 | clearInterval(displayInterval);
1401 | }
1402 |
1403 | // 監聽元素移除
1404 | const observer = new MutationObserver((mutations) => {
1405 | if (!document.contains(timeDisplay)) {
1406 | cleanup();
1407 | observer.disconnect();
1408 | }
1409 | });
1410 |
1411 | observer.observe(document.body, {
1412 | childList: true,
1413 | subtree: true
1414 | });
1415 | });
1416 | }
1417 |
1418 | // 監聽設置變化
1419 | chrome.storage.onChanged.addListener(function(changes, namespace) {
1420 | if (changes.showServerTime) {
1421 | if (changes.showServerTime.newValue === false) {
1422 | const existingDisplay = document.querySelector('.server-time-display');
1423 | if (existingDisplay) {
1424 | existingDisplay.remove();
1425 | }
1426 | } else {
1427 | showServerTime();
1428 | }
1429 | // 重新顯示篩選狀態,以更新位置
1430 | showFilterStatus();
1431 | }
1432 |
1433 | // 當篩選條件顯示設置改變時,重新顯示篩選狀態
1434 | if (changes.showFilterStatus !== undefined) {
1435 | if (changes.showFilterStatus.newValue === false) {
1436 | // 如果設置為不顯示,移除現有的篩選條件顯示
1437 | const existingNotification = document.querySelector('.ticket-filter-notification');
1438 | if (existingNotification) {
1439 | existingNotification.remove();
1440 | }
1441 | } else {
1442 | // 如果設置為顯示,重新顯示篩選條件
1443 | showFilterStatus();
1444 | }
1445 | }
1446 |
1447 | // 監聽擴展啟用狀態的變化
1448 | if (changes.extensionEnabled !== undefined) {
1449 | const site = getCurrentSite();
1450 | if (!site) return;
1451 |
1452 | if (changes.extensionEnabled.newValue === false) {
1453 | // 擴展被停用
1454 | // 1. 移除所有顯示
1455 | const notification = document.querySelector('.ticket-filter-notification');
1456 | const timeDisplay = document.querySelector('.server-time-display');
1457 | if (notification) notification.remove();
1458 | if (timeDisplay) timeDisplay.remove();
1459 |
1460 | // 2. 重置所有設定
1461 | settings.keywords = [];
1462 | settings.blacklist = []; // 確保黑名單也被清空
1463 | settings.hideSoldOut = false;
1464 | settings.showAllPrices = true;
1465 | settings.isProcessing = false;
1466 |
1467 | // 3. 強制顯示所有區域
1468 | showAllAreas();
1469 |
1470 | // 4. 對於 ibon 網站,額外處理
1471 | if (site === 'ibon') {
1472 | // 移除所有由擴充功能添加的樣式
1473 | function removeStyles(element) {
1474 | if (element.shadowRoot) {
1475 | const styles = element.shadowRoot.querySelectorAll('style.extension-filter-style');
1476 | styles.forEach(style => style.remove());
1477 |
1478 | // 清除所有票券的隱藏狀態
1479 | const rows = element.shadowRoot.querySelectorAll('tr[id^="B"]');
1480 | rows.forEach(row => {
1481 | row.style.removeProperty('display');
1482 | row.classList.remove('hidden-by-extension');
1483 | Array.from(row.children).forEach(cell => {
1484 | cell.style.removeProperty('display');
1485 | });
1486 | });
1487 | }
1488 | Array.from(element.children || []).forEach(removeStyles);
1489 | }
1490 | removeStyles(document.documentElement);
1491 | }
1492 | } else {
1493 | // 擴展被啟用,重新初始化所有功能
1494 | loadSettings();
1495 | showServerTime();
1496 | showFilterStatus();
1497 | filterTickets();
1498 | }
1499 | }
1500 | });
1501 |
1502 | // 在頁面加載完成後顯示時間(只保留这一个初始化点)
1503 | if (document.readyState === 'complete') {
1504 | showServerTime();
1505 | } else {
1506 | window.addEventListener('load', showServerTime);
1507 | }
1508 |
1509 | // 设置页面加载策略
1510 | document.documentElement.setAttribute('pageLoadStrategy', 'eager');
1511 |
1512 | // 在頁面載入和切換時重新載入設定
1513 | document.addEventListener('DOMContentLoaded', () => {
1514 | const site = getCurrentSite();
1515 | if (site === 'ibon') {
1516 | chrome.storage.local.get(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], (result) => {
1517 | settings.keywords = result.ibonKeywords || [];
1518 | settings.blacklist = result.ibonBlacklist || [];
1519 | settings.hideSoldOut = result.ibonHideSoldOut || false;
1520 | filterTickets();
1521 | showFilterStatus();
1522 | });
1523 | }
1524 | });
1525 |
1526 | // 根據網站定義不同的延遲時間
1527 | function getDebounceTime() {
1528 | const site = getCurrentSite();
1529 | switch(site) {
1530 | case 'ibon':
1531 | return 250; // ibon 網站需要較長的延遲
1532 | case 'ticketplus':
1533 | return 50; // 遠大售票也需要稍長延遲
1534 | default:
1535 | return 50; // 其他網站使用短延遲
1536 | }
1537 | }
--------------------------------------------------------------------------------
/earlyLoader.js:
--------------------------------------------------------------------------------
1 | // 在页面加载的最早阶段执行
2 | (function() {
3 | // 判断当前网站类型
4 | const isIbon = window.location.hostname.includes('ibon.com.tw');
5 | const isTixcraft = window.location.href.includes('tixcraft.com/ticket/area/');
6 | const isKktix = window.location.hostname.includes('kktix.com');
7 | const isCityline = window.location.hostname.includes('cityline.com');
8 | const isEra = window.location.hostname.includes('ticket.com.tw');
9 |
10 | // 仅在适用的网站上应用筛选
11 | if (!isIbon && !isTixcraft && !isKktix && !isCityline && !isEra) return;
12 |
13 | // 特別處理 ibon 網站
14 | if (isIbon) {
15 | const styleElement = document.createElement('style');
16 | styleElement.id = 'ticket-filter-early-style-ibon';
17 | styleElement.textContent = `
18 | /* ibon 網站 - 使用不透明度和顯示過渡效果 */
19 | body {
20 | opacity: 0.01 !important;
21 | transition: opacity 0.3s ease-out;
22 | }
23 | body.filter-ready {
24 | opacity: 1 !important;
25 | }
26 | `;
27 | document.documentElement.appendChild(styleElement);
28 |
29 | // 監聽過濾完成的消息
30 | window.addEventListener('message', function(event) {
31 | if (event.data.type === 'FILTER_APPLIED') {
32 | document.body.classList.add('filter-ready');
33 | }
34 | });
35 |
36 | // 安全機制:確保不會永久隱藏
37 | setTimeout(() => {
38 | document.body.classList.add('filter-ready');
39 | }, 800);
40 |
41 | return; // 對 ibon 網站使用專門的處理,不執行後續代碼
42 | }
43 |
44 | // 为不同网站使用不同的隐藏方式
45 | if (isTixcraft) {
46 | // 拓元网站 - 使用visibility隐藏
47 | const styleElement = document.createElement('style');
48 | styleElement.id = 'ticket-filter-early-style';
49 | styleElement.textContent = `
50 | /* 拓元网站 */
51 | .area-list li,
52 | .zone-label[data-id] {
53 | visibility: hidden !important;
54 | }
55 | `;
56 | document.documentElement.appendChild(styleElement);
57 | } else if (isKktix) {
58 | // KKTIX网站 - 使用较轻量的透明度处理,避免页面空白
59 | const styleElement = document.createElement('style');
60 | styleElement.id = 'ticket-filter-early-style-kktix';
61 | styleElement.textContent = `
62 | /* KKTIX网站 - 只减少不透明度而不是完全隐藏 */
63 | .ticket-unit {
64 | opacity: 0.01;
65 | transition: opacity 0.3s ease-in;
66 | }
67 | `;
68 | document.documentElement.appendChild(styleElement);
69 |
70 | // 设置一个较短的超时,确保KKTIX可以快速显示
71 | setTimeout(() => {
72 | // 如果筛选还没完成,先显示所有元素
73 | if (!window.ticketFilterComplete) {
74 | document.querySelectorAll('.ticket-unit').forEach(el => {
75 | el.style.opacity = '1';
76 | });
77 | }
78 | }, 800); // 800ms后如果筛选未完成就显示
79 | } else if (isCityline) {
80 | // Cityline网站 - 使用更快速的处理方式
81 | const styleElement = document.createElement('style');
82 | styleElement.id = 'ticket-filter-early-style-cityline';
83 | styleElement.textContent = `
84 | /* Cityline网站 - 使用更高效的隐藏方式并添加过渡效果 */
85 | .price-box1, .form-check {
86 | visibility: hidden !important;
87 | opacity: 0 !important;
88 | transition: opacity 0.15s ease-out !important;
89 | }
90 |
91 | /* 隐藏票价区域,确保用户在筛选完成前看不到 */
92 | .price-box1 > div.price {
93 | visibility: hidden !important;
94 | }
95 |
96 | /* 应用更快的过渡效果,在筛选完成后显示 */
97 | .price-box1.filter-ready, .form-check.filter-ready {
98 | visibility: visible !important;
99 | opacity: 1 !important;
100 | transition: opacity 0.15s ease-out !important;
101 | }
102 | `;
103 | document.documentElement.appendChild(styleElement);
104 |
105 | // 创建一个变量记录筛选是否已经开始
106 | window.citylineFilterStarted = false;
107 |
108 | // 通知 content.js 脚本立即开始筛选,不要等待
109 | setTimeout(() => {
110 | window.postMessage({ type: 'CITYLINE_READY_FOR_FILTER' }, '*');
111 | }, 10);
112 |
113 | // 立即运行 MutationObserver 以捕获任何动态添加的票券元素
114 | const citylineObserver = new MutationObserver((mutations) => {
115 | // 如果筛选已经完成,不进行处理
116 | if (window.ticketFilterComplete) return;
117 |
118 | // 筛选已经开始但尚未完成
119 | if (window.citylineFilterStarted) {
120 | let ticketElementsFound = false;
121 |
122 | // 仅处理新添加的节点
123 | for (const mutation of mutations) {
124 | if (mutation.addedNodes && mutation.addedNodes.length) {
125 | for (const node of mutation.addedNodes) {
126 | if (node.nodeType === 1) { // 元素节点
127 | const ticketElements = node.querySelectorAll ?
128 | node.querySelectorAll('.price-box1, .form-check') : [];
129 |
130 | if (ticketElements.length > 0) {
131 | ticketElementsFound = true;
132 | // 这里不做任何处理,保持其隐藏状态
133 | }
134 |
135 | // 如果节点本身就是票券元素
136 | if (node.matches && (node.matches('.price-box1') || node.matches('.form-check'))) {
137 | ticketElementsFound = true;
138 | }
139 | }
140 | }
141 | }
142 | }
143 |
144 | // 如果找到票券元素,通知 content.js 有新的票券要处理
145 | if (ticketElementsFound) {
146 | window.postMessage({ type: 'NEW_TICKETS_FOUND' }, '*');
147 | }
148 | } else {
149 | // 筛选尚未开始,但看到了票券元素,通知 content.js 立即开始筛选
150 | const ticketElements = document.querySelectorAll('.price-box1, .form-check');
151 | if (ticketElements.length > 0) {
152 | window.citylineFilterStarted = true;
153 | window.postMessage({ type: 'CITYLINE_READY_FOR_FILTER' }, '*');
154 | }
155 | }
156 | });
157 |
158 | // 使用更高效的配置,只观察必要的变化
159 | citylineObserver.observe(document.body, {
160 | childList: true,
161 | subtree: true,
162 | attributes: false,
163 | characterData: false
164 | });
165 |
166 | // 设置一个较短的超时,确保Cityline可以更快地显示
167 | setTimeout(() => {
168 | // 如果筛选还没完成,但处理已经开始,给予更多时间
169 | if (window.citylineFilterStarted && !window.ticketFilterComplete) {
170 | // 再等待一段时间,因为筛选已经开始
171 | return;
172 | }
173 |
174 | // 如果筛选还没完成且未开始,为避免永久隐藏,显示所有元素
175 | if (!window.ticketFilterComplete) {
176 | document.querySelectorAll('.price-box1, .form-check').forEach(el => {
177 | el.classList.add('filter-ready');
178 | });
179 | }
180 | }, 800); // 800ms后检查状态
181 |
182 | // 设置最终超时作为安全机制
183 | setTimeout(() => {
184 | if (!window.ticketFilterComplete) {
185 | document.querySelectorAll('.price-box1, .form-check').forEach(el => {
186 | el.classList.add('filter-ready');
187 | });
188 | // 断开观察者以提高性能
189 | citylineObserver.disconnect();
190 | }
191 | }, 1500); // 1.5秒后如果筛选未完成就显示
192 | } else if (isEra) {
193 | // 年代售票网站 - 使用visibility隐藏
194 | const styleElement = document.createElement('style');
195 | styleElement.id = 'ticket-filter-early-style-era';
196 | styleElement.textContent = `
197 | /* 年代售票网站 */
198 | .area-list li {
199 | visibility: hidden !important;
200 | }
201 | .area-list li.filter-ready {
202 | visibility: visible !important;
203 | transition: opacity 0.2s ease-out;
204 | }
205 | `;
206 | document.documentElement.appendChild(styleElement);
207 | }
208 |
209 | // 记录是否已收到筛选完成消息
210 | let filterApplied = false;
211 |
212 | // Shadow DOM处理相关变量
213 | let shadowObserver = null;
214 | const shadowStyleCache = new Set(); // 缓存所有添加的style元素
215 |
216 | // 处理Shadow DOM的函数 - 为ibon网站
217 | function handleIbonShadowDOM() {
218 | // 尝试向shadowRoot中注入样式的函数
219 | function injectStyleToShadowRoot(shadowRoot) {
220 | if (!shadowRoot) return;
221 |
222 | // 避免重复注入
223 | if (shadowRoot.querySelector('#ticket-filter-shadow-style')) return;
224 |
225 | try {
226 | const shadowStyle = document.createElement('style');
227 | shadowStyle.id = 'ticket-filter-shadow-style';
228 | shadowStyle.textContent = `
229 | /* ibon网站 - 仅在筛选未完成前隐藏 */
230 | tr[id^="B"] {
231 | opacity: 0;
232 | transition: opacity 0.2s;
233 | }
234 | `;
235 | shadowRoot.appendChild(shadowStyle);
236 | shadowStyleCache.add(shadowRoot); // 记录已注入样式的shadowRoot
237 | } catch (err) {
238 | // 静默错误
239 | }
240 | }
241 |
242 | // 递归处理所有可能包含shadowRoot的元素
243 | function checkShadowRoots(element) {
244 | if (!element) return;
245 |
246 | // 处理当前元素的shadowRoot
247 | if (element.shadowRoot) {
248 | injectStyleToShadowRoot(element.shadowRoot);
249 | }
250 |
251 | // 递归处理子元素
252 | const children = element.children;
253 | if (children && children.length) {
254 | for (let i = 0; i < children.length; i++) {
255 | checkShadowRoots(children[i]);
256 | }
257 | }
258 | }
259 |
260 | // 初始处理
261 | checkShadowRoots(document);
262 |
263 | // 监听DOM变化以处理动态添加的元素
264 | shadowObserver = new MutationObserver((mutations) => {
265 | if (filterApplied) {
266 | shadowObserver.disconnect();
267 | return;
268 | }
269 |
270 | for (const mutation of mutations) {
271 | if (mutation.addedNodes && mutation.addedNodes.length) {
272 | for (const node of mutation.addedNodes) {
273 | if (node.nodeType === 1) { // 元素节点
274 | checkShadowRoots(node);
275 | }
276 | }
277 | }
278 | }
279 | });
280 |
281 | shadowObserver.observe(document.documentElement, {
282 | childList: true,
283 | subtree: true
284 | });
285 | }
286 |
287 | // 显示所有元素的函数
288 | function showAllElements() {
289 | filterApplied = true;
290 | // 告诉window对象筛选已完成
291 | window.ticketFilterComplete = true;
292 |
293 | // 针对不同网站处理样式
294 | if (isTixcraft) {
295 | const earlyStyle = document.getElementById('ticket-filter-early-style');
296 | if (earlyStyle) {
297 | earlyStyle.remove();
298 | }
299 | } else if (isKktix) {
300 | // 对于KKTIX,修改样式使元素显示出来,而不是移除
301 | const kktixStyle = document.getElementById('ticket-filter-early-style-kktix');
302 | if (kktixStyle) {
303 | kktixStyle.textContent = `
304 | .ticket-unit {
305 | opacity: 1 !important;
306 | transition: opacity 0.2s ease-out;
307 | }
308 | `;
309 | }
310 |
311 | // 直接修改所有票券元素的样式
312 | document.querySelectorAll('.ticket-unit').forEach(el => {
313 | el.style.opacity = '1';
314 | });
315 | } else if (isCityline) {
316 | // 对于Cityline,使用class切换来显示元素
317 | document.querySelectorAll('.price-box1, .form-check').forEach(el => {
318 | el.classList.add('filter-ready');
319 | });
320 |
321 | // 确保所有需要显示的元素都被正确显示(使用更短的延迟)
322 | setTimeout(() => {
323 | // 获取当前所有应该显示的元素(不隐藏的)
324 | document.querySelectorAll('.form-check').forEach(el => {
325 | if (el.style.display !== 'none') {
326 | el.style.visibility = 'visible';
327 | el.style.opacity = '1';
328 | }
329 | });
330 | }, 10); // 更短的延迟确保 DOM 快速更新
331 | } else if (isEra) {
332 | // 对于年代售票,使用class切换来显示元素
333 | document.querySelectorAll('.area-list li').forEach(el => {
334 | el.classList.add('filter-ready');
335 | });
336 | }
337 |
338 | // 处理ibon的Shadow DOM
339 | if (isIbon) {
340 | // 停止观察者
341 | if (shadowObserver) {
342 | shadowObserver.disconnect();
343 | }
344 |
345 | // 从Shadow DOM中移除样式,或者将所有tr设为可见
346 | shadowStyleCache.forEach(shadowRoot => {
347 | try {
348 | const style = shadowRoot.querySelector('#ticket-filter-shadow-style');
349 | if (style) {
350 | // 修改样式使元素显示,而不是直接移除
351 | style.textContent = `
352 | tr[id^="B"] {
353 | opacity: 1 !important;
354 | transition: opacity 0.2s;
355 | }
356 | `;
357 | }
358 | } catch (err) {
359 | // 静默错误
360 | }
361 | });
362 |
363 | // 额外措施:通过contentWindow直接执行脚本使票券可见
364 | try {
365 | const showAllRows = `
366 | function makeAllRowsVisible(root) {
367 | if (root.shadowRoot) {
368 | const rows = root.shadowRoot.querySelectorAll('tr[id^="B"]');
369 | rows.forEach(row => {
370 | row.style.removeProperty('display');
371 | row.style.opacity = '1';
372 | row.style.visibility = 'visible';
373 | });
374 | }
375 |
376 | if (root.children) {
377 | Array.from(root.children).forEach(child => {
378 | makeAllRowsVisible(child);
379 | });
380 | }
381 | }
382 |
383 | makeAllRowsVisible(document.documentElement);
384 | `;
385 |
386 | const scriptTag = document.createElement('script');
387 | scriptTag.textContent = showAllRows;
388 | document.documentElement.appendChild(scriptTag);
389 | document.documentElement.removeChild(scriptTag);
390 | } catch (e) {
391 | // 静默错误
392 | }
393 | }
394 | }
395 |
396 | // 如果是ibon网站,启用Shadow DOM处理
397 | if (isIbon) {
398 | // 延迟一点点再执行,等待iframe和shadowDOM创建
399 | setTimeout(() => {
400 | handleIbonShadowDOM();
401 | }, 10);
402 | }
403 |
404 | // 监听content.js加载完成的消息
405 | window.addEventListener('message', function(event) {
406 | // 筛选应用完成
407 | if (event.data && event.data.type === 'FILTER_APPLIED') {
408 | showAllElements();
409 | }
410 |
411 | // content.js已加载但尚未完成筛选,延长超时时间
412 | if (event.data && event.data.type === 'CONTENT_JS_LOADED' && !filterApplied) {
413 | // Cityline网站的特殊处理,主动通知可以开始筛选
414 | if (isCityline) {
415 | setTimeout(() => {
416 | window.postMessage({ type: 'CITYLINE_READY_FOR_FILTER' }, '*');
417 | }, 10);
418 | }
419 |
420 | // 设置更长的超时时间
421 | setTimeout(() => {
422 | if (!filterApplied) { // 如果还没有应用过筛选
423 | showAllElements();
424 | }
425 | }, 2000); // 2秒后如果筛选未完成就显示
426 | }
427 | });
428 |
429 | // 设置安全超时,防止永久隐藏
430 | setTimeout(() => {
431 | if (!filterApplied) {
432 | showAllElements();
433 | }
434 | }, 1500); // 更短的超时时间,确保元素不会长时间隐藏
435 | })();
--------------------------------------------------------------------------------
/images/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder220224/Ticket-Filter/40b0b02eaa8bc42f7ed5218640143233015e6d24/images/icon128.png
--------------------------------------------------------------------------------
/images/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder220224/Ticket-Filter/40b0b02eaa8bc42f7ed5218640143233015e6d24/images/icon16.png
--------------------------------------------------------------------------------
/images/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder220224/Ticket-Filter/40b0b02eaa8bc42f7ed5218640143233015e6d24/images/icon48.png
--------------------------------------------------------------------------------
/injected.js:
--------------------------------------------------------------------------------
1 | // 立即執行的初始化函數
2 | (function() {
3 | // 保存原始方法
4 | Element.prototype._attachShadow = Element.prototype.attachShadow;
5 |
6 | // 重寫 attachShadow 方法
7 | Element.prototype.attachShadow = function () {
8 | try {
9 | return this._attachShadow({ mode: "open" });
10 | } catch (e) {
11 | console.warn('Shadow root 轉換失敗:', e);
12 | return this._attachShadow({ mode: "closed" });
13 | }
14 | };
15 |
16 | // 嘗試轉換已存在的 shadow roots
17 | function convertExistingShadowRoots() {
18 | try {
19 | const elements = document.getElementsByTagName('*');
20 | for (let i = 0; i < elements.length; i++) {
21 | const element = elements[i];
22 | if (element.shadowRoot === null) {
23 | const shadowRootAttr = element.getAttribute('shadowroot');
24 | if (shadowRootAttr === 'closed') {
25 | try {
26 | element._attachShadow({ mode: 'open' });
27 | } catch (e) {
28 | // 忽略錯誤,繼續處理下一個元素
29 | }
30 | }
31 | }
32 | }
33 | } catch (e) {
34 | console.warn('轉換現有 shadow roots 時發生錯誤:', e);
35 | }
36 | }
37 |
38 | // 創建一個 MutationObserver 來監視 DOM 變化
39 | const observer = new MutationObserver((mutations) => {
40 | convertExistingShadowRoots();
41 | });
42 |
43 | // 開始監視整個文檔
44 | observer.observe(document.documentElement || document.body || document, {
45 | childList: true,
46 | subtree: true,
47 | attributes: true,
48 | attributeFilter: ['shadowroot']
49 | });
50 |
51 | // 立即執行一次轉換
52 | convertExistingShadowRoots();
53 |
54 | // 在各種可能的時機點執行轉換
55 | ['DOMContentLoaded', 'load', 'pageshow'].forEach(event => {
56 | window.addEventListener(event, convertExistingShadowRoots, true);
57 | });
58 |
59 | // 監聽頁面可見性變化
60 | document.addEventListener('visibilitychange', () => {
61 | if (document.visibilityState === 'visible') {
62 | convertExistingShadowRoots();
63 | }
64 | });
65 |
66 | // 監聽 history 變化
67 | let lastUrl = location.href;
68 | new MutationObserver(() => {
69 | const url = location.href;
70 | if (url !== lastUrl) {
71 | lastUrl = url;
72 | convertExistingShadowRoots();
73 | }
74 | }).observe(document, {subtree: true, childList: true});
75 |
76 | // 定期檢查(較短間隔)
77 | setInterval(convertExistingShadowRoots, 100);
78 | })();
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "搶票柴柴-售票網站篩選器",
4 | "version": "1.3.1",
5 | "description": "在售票網站上快速篩選您想要的區域票券",
6 | "permissions": [
7 | "storage",
8 | "activeTab",
9 | "scripting",
10 | "webRequest",
11 | "webRequestBlocking"
12 | ],
13 | "host_permissions": [
14 | "*://*.tixcraft.com/*",
15 | "*://*.kktix.com/*",
16 | "*://kktix.com/*",
17 | "*://*.kktix.cc/*",
18 | "*://*.ibon.com.tw/*",
19 | "*://ticket.ibon.com.tw/*",
20 | "*://orders.ibon.com.tw/*",
21 | "*://*.cityline.com/*",
22 | "*://www.cityline.com/*",
23 | "*://*.ticket.com.tw/*",
24 | "*://*.ticketplus.com.tw/*"
25 | ],
26 | "content_scripts": [
27 | {
28 | "matches": [
29 | "*://*.tixcraft.com/*",
30 | "*://*.kktix.com/*",
31 | "*://*.kktix.cc/*",
32 | "*://*.ibon.com.tw/*",
33 | "*://ticket.ibon.com.tw/*",
34 | "*://orders.ibon.com.tw/*",
35 | "*://*.cityline.com/*",
36 | "*://www.cityline.com/*",
37 | "*://*.ticket.com.tw/*",
38 | "*://*.ticketplus.com.tw/*"
39 | ],
40 | "js": ["content.js"]
41 | },
42 | {
43 | "matches": [
44 | "*://*.ibon.com.tw/*",
45 | "*://ticket.ibon.com.tw/*",
46 | "*://orders.ibon.com.tw/*"
47 | ],
48 | "js": ["shadowInject.js"],
49 | "run_at": "document_start",
50 | "all_frames": true,
51 | "world": "MAIN"
52 | },
53 | {
54 | "matches": [
55 | "*://*.tixcraft.com/ticket/area/*",
56 | "*://*.kktix.com/*",
57 | "*://*.kktix.cc/*",
58 | "*://*.ibon.com.tw/*",
59 | "*://*.cityline.com/*",
60 | "*://*.ticket.com.tw/*",
61 | "*://*.ticketplus.com.tw/*"
62 | ],
63 | "js": ["earlyLoader.js"],
64 | "run_at": "document_start",
65 | "all_frames": true
66 | },
67 | {
68 | "matches": ["*://*.ticket.com.tw/*"],
69 | "js": ["ticketLoader.js"],
70 | "run_at": "document_start",
71 | "all_frames": true
72 | }
73 | ],
74 | "background": {
75 | "service_worker": "background.js",
76 | "type": "module"
77 | },
78 | "web_accessible_resources": [{
79 | "resources": ["injected.js", "styles.css"],
80 | "matches": [
81 | "*://orders.ibon.com.tw/*",
82 | "*://*.tixcraft.com/*",
83 | "*://*.kktix.com/*",
84 | "*://*.kktix.cc/*",
85 | "*://*.ibon.com.tw/*",
86 | "*://*.cityline.com/*",
87 | "*://*.ticket.com.tw/*",
88 | "*://*.ticketplus.com.tw/*"
89 | ]
90 | }],
91 | "action": {
92 | "default_popup": "popup.html",
93 | "default_icon": {
94 | "16": "images/icon16.png",
95 | "48": "images/icon48.png",
96 | "128": "images/icon128.png"
97 | }
98 | },
99 | "icons": {
100 | "16": "images/icon16.png",
101 | "48": "images/icon48.png",
102 | "128": "images/icon128.png"
103 | },
104 | "content_security_policy": {
105 | "extension_pages": "script-src 'self'; object-src 'self'",
106 | "sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';"
107 | }
108 | }
--------------------------------------------------------------------------------
/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
97 | 搜尋格式:3200,A區 (同時符合) 或 4500+3200 (任一符合) 98 |
99 | `; 100 | 101 | // 將說明插入到輸入框上方 102 | const exampleText = areaFilter.previousElementSibling; 103 | if (exampleText) { 104 | exampleText.parentNode.insertBefore(helpText, exampleText.nextSibling); 105 | } 106 | 107 | let keywords = new Set(); 108 | let blacklist = new Set(); 109 | let currentSite = null; 110 | 111 | // 檢查當前網站類型 112 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 113 | if (tabs[0]) { 114 | const url = tabs[0].url; 115 | if (url.includes('tixcraft.com')) { 116 | currentSite = 'tixcraft'; 117 | loadTixcraftSettings(); 118 | document.body.setAttribute('data-site', 'tixcraft'); 119 | } else if (url.includes('kktix.com')) { 120 | currentSite = 'kktix'; 121 | loadKKTIXSettings(); 122 | document.body.setAttribute('data-site', 'kktix'); 123 | } else if (url.includes('ibon.com.tw')) { 124 | currentSite = 'ibon'; 125 | loadIbonSettings(); 126 | document.body.setAttribute('data-site', 'ibon'); 127 | } else if (url.includes('cityline.com')) { 128 | currentSite = 'cityline'; 129 | loadCitylineSettings(); 130 | document.body.setAttribute('data-site', 'cityline'); 131 | } else if (url.includes('ticket.com.tw')) { 132 | currentSite = 'ticket'; 133 | loadTicketSettings(); 134 | document.body.setAttribute('data-site', 'ticket'); 135 | } else if (url.includes('ticketplus.com.tw')) { 136 | currentSite = 'ticketplus'; 137 | loadTicketPlusSettings(); 138 | document.body.setAttribute('data-site', 'ticketplus'); 139 | } 140 | } 141 | }); 142 | 143 | // 載入拓元設定 144 | function loadTixcraftSettings() { 145 | chrome.storage.local.get(['keywords', 'blacklist', 'hideSoldOut'], (result) => { 146 | if (result.keywords) { 147 | keywords = new Set(result.keywords); 148 | renderKeywords(); 149 | updateFilterLabel(); 150 | } 151 | if (result.blacklist) { 152 | blacklist = new Set(result.blacklist); 153 | renderBlacklist(); 154 | } 155 | if (result.hideSoldOut !== undefined) { 156 | showSoldOut.checked = result.hideSoldOut; 157 | } 158 | }); 159 | } 160 | 161 | // 載入KKTIX設定 162 | function loadKKTIXSettings() { 163 | chrome.storage.sync.get(['targetKeywords', 'blacklist', 'hideSoldOut'], (result) => { 164 | if (result.targetKeywords) { 165 | keywords = new Set(result.targetKeywords); 166 | renderKeywords(); 167 | updateFilterLabel(); 168 | } 169 | if (result.blacklist) { 170 | blacklist = new Set(result.blacklist); 171 | renderBlacklist(); 172 | } 173 | if (result.hideSoldOut !== undefined) { 174 | showSoldOut.checked = result.hideSoldOut; 175 | } 176 | }); 177 | } 178 | 179 | // 載入ibon設定 180 | function loadIbonSettings() { 181 | chrome.storage.local.get(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], (result) => { 182 | if (result.ibonKeywords) { 183 | keywords = new Set(result.ibonKeywords); 184 | renderKeywords(); 185 | updateFilterLabel(); 186 | } 187 | if (result.ibonBlacklist) { 188 | blacklist = new Set(result.ibonBlacklist); 189 | renderBlacklist(); 190 | } 191 | if (result.ibonHideSoldOut !== undefined) { 192 | showSoldOut.checked = result.ibonHideSoldOut; 193 | } 194 | }); 195 | } 196 | 197 | // 載入Cityline設定 198 | function loadCitylineSettings() { 199 | chrome.storage.local.get(['citylineKeywords', 'citylineBlacklist', 'citylineHideSoldOut'], (result) => { 200 | if (result.citylineKeywords) { 201 | keywords = new Set(result.citylineKeywords); 202 | renderKeywords(); 203 | updateFilterLabel(); 204 | } 205 | if (result.citylineBlacklist) { 206 | blacklist = new Set(result.citylineBlacklist); 207 | renderBlacklist(); 208 | } 209 | if (result.citylineHideSoldOut !== undefined) { 210 | showSoldOut.checked = result.citylineHideSoldOut; 211 | } 212 | }); 213 | } 214 | 215 | // 載入年代售票設定 216 | function loadTicketSettings() { 217 | chrome.storage.local.get(['ticketKeywords', 'ticketHideSoldOut'], (result) => { 218 | if (result.ticketKeywords) { 219 | keywords = new Set(result.ticketKeywords); 220 | renderKeywords(); 221 | updateFilterLabel(); 222 | } 223 | if (result.ticketHideSoldOut !== undefined) { 224 | showSoldOut.checked = result.ticketHideSoldOut; 225 | } 226 | }); 227 | } 228 | 229 | // 載入遠大售票平台設定 230 | function loadTicketPlusSettings() { 231 | chrome.storage.local.get(['ticketplusKeywords', 'ticketplusBlacklist', 'ticketplusHideSoldOut'], (result) => { 232 | if (result.ticketplusKeywords) { 233 | keywords = new Set(result.ticketplusKeywords); 234 | renderKeywords(); 235 | updateFilterLabel(); 236 | } 237 | if (result.ticketplusBlacklist) { 238 | blacklist = new Set(result.ticketplusBlacklist); 239 | renderBlacklist(); 240 | } 241 | if (result.ticketplusHideSoldOut !== undefined) { 242 | showSoldOut.checked = result.ticketplusHideSoldOut; 243 | } 244 | }); 245 | } 246 | 247 | // Update filter label 248 | function updateFilterLabel() { 249 | if (keywords.size > 0) { 250 | currentFilter.style.display = 'inline-block'; 251 | filterText.textContent = Array.from(keywords).join('、'); 252 | } else { 253 | currentFilter.style.display = 'none'; 254 | } 255 | } 256 | 257 | // Render keyword tags 258 | function renderKeywords() { 259 | keywordsContainer.innerHTML = ''; 260 | 261 | if (keywords.size === 0) { 262 | keywordsContainer.innerHTML = '尚未設定篩選條件'; 263 | 264 | // 添加這行來隱藏標籤 265 | if (currentFilter) { 266 | currentFilter.style.display = 'none'; 267 | } 268 | if (filterText) { 269 | filterText.textContent = ''; 270 | } 271 | return; 272 | } 273 | 274 | keywords.forEach(keyword => { 275 | const tag = document.createElement('div'); 276 | tag.className = 'keyword-tag'; 277 | tag.innerHTML = ` 278 | ${keyword} 279 | × 280 | `; 281 | keywordsContainer.appendChild(tag); 282 | }); 283 | 284 | // Add event listeners for remove buttons 285 | document.querySelectorAll('.keyword-tag .remove').forEach(removeBtn => { 286 | removeBtn.addEventListener('click', (e) => { 287 | const keyword = e.target.dataset.keyword; 288 | keywords.delete(keyword); 289 | renderKeywords(); 290 | updateFilterLabel(); 291 | applyFilters(); 292 | }); 293 | }); 294 | 295 | // 更新關鍵字顯示標籤 296 | if (currentFilter && filterText) { 297 | currentFilter.style.display = 'inline-block'; 298 | filterText.textContent = Array.from(keywords).join('、'); 299 | } 300 | } 301 | 302 | // Render blacklist tags 303 | function renderBlacklist() { 304 | blacklistContainer.innerHTML = ''; 305 | 306 | if (blacklist.size === 0) { 307 | blacklistContainer.innerHTML = '尚未設定黑名單'; 308 | const currentBlacklist = document.getElementById('currentBlacklist'); 309 | if (currentBlacklist) { 310 | currentBlacklist.style.display = 'none'; 311 | } 312 | return; 313 | } 314 | 315 | blacklist.forEach(keyword => { 316 | const tag = document.createElement('div'); 317 | tag.className = 'keyword-tag'; 318 | tag.innerHTML = ` 319 | ${keyword} 320 | × 321 | `; 322 | blacklistContainer.appendChild(tag); 323 | }); 324 | 325 | // 更新黑名單顯示標籤 326 | const currentBlacklist = document.getElementById('currentBlacklist'); 327 | const blacklistText = document.getElementById('blacklistText'); 328 | if (currentBlacklist && blacklistText) { 329 | currentBlacklist.style.display = 'inline-block'; 330 | blacklistText.textContent = Array.from(blacklist).join('、'); 331 | } 332 | 333 | // Add event listeners for remove buttons 334 | document.querySelectorAll('#blacklistContainer .keyword-tag .remove').forEach(removeBtn => { 335 | removeBtn.addEventListener('click', (e) => { 336 | const keyword = e.target.dataset.keyword; 337 | blacklist.delete(keyword); 338 | renderBlacklist(); 339 | applyFilters(); 340 | }); 341 | }); 342 | } 343 | 344 | // 自定義警告框函數 345 | function showCustomAlert(message) { 346 | return new Promise((resolve) => { 347 | const overlay = document.querySelector('.alert-overlay'); 348 | const alert = document.querySelector('.custom-alert'); 349 | const content = alert.querySelector('.alert-content'); 350 | const confirmBtn = alert.querySelector('.alert-confirm'); 351 | const cancelBtn = alert.querySelector('.alert-cancel'); 352 | 353 | content.textContent = message; 354 | overlay.style.display = 'block'; 355 | alert.style.display = 'block'; 356 | 357 | const handleConfirm = () => { 358 | overlay.style.display = 'none'; 359 | alert.style.display = 'none'; 360 | cleanup(); 361 | resolve(true); 362 | }; 363 | 364 | const handleCancel = () => { 365 | overlay.style.display = 'none'; 366 | alert.style.display = 'none'; 367 | cleanup(); 368 | resolve(false); 369 | }; 370 | 371 | const cleanup = () => { 372 | confirmBtn.removeEventListener('click', handleConfirm); 373 | cancelBtn.removeEventListener('click', handleCancel); 374 | overlay.removeEventListener('click', handleCancel); 375 | }; 376 | 377 | confirmBtn.addEventListener('click', handleConfirm); 378 | cancelBtn.addEventListener('click', handleCancel); 379 | overlay.addEventListener('click', handleCancel); 380 | }); 381 | } 382 | 383 | // 修改添加關鍵字的函數 384 | async function addKeywordToFilter() { 385 | const value = areaFilter.value.trim(); 386 | if (value) { 387 | const hasConflict = Array.from(blacklist).some(blacklistItem => { 388 | if (blacklistItem === value) return true; 389 | 390 | // 處理 OR 條件 (+) 391 | const blacklistOrParts = blacklistItem.split('+').map(p => p.trim()); 392 | const valueOrParts = value.split('+').map(p => p.trim()); 393 | 394 | // 處理 AND 條件 (,) 395 | const blacklistAndParts = blacklistOrParts.flatMap(p => p.split(',').map(p => p.trim())); 396 | const valueAndParts = valueOrParts.flatMap(p => p.split(',').map(p => p.trim())); 397 | 398 | return blacklistAndParts.some(bp => valueAndParts.includes(bp)); 399 | }); 400 | 401 | if (hasConflict) { 402 | const shouldAdd = await showCustomAlert('此條件與黑名單中的條件重複或衝突,是否仍要新增?'); 403 | if (!shouldAdd) return; 404 | } 405 | keywords.add(value); 406 | areaFilter.value = ''; 407 | renderKeywords(); 408 | updateFilterLabel(); 409 | applyFilters(); 410 | } 411 | } 412 | 413 | // Add keyword on Enter 414 | if (areaFilter) { 415 | areaFilter.addEventListener('keypress', (e) => { 416 | if (e.key === 'Enter') { 417 | e.preventDefault(); 418 | addKeywordToFilter(); 419 | } 420 | }); 421 | } 422 | 423 | // Add keyword on button click 424 | if (addKeyword) { 425 | addKeyword.addEventListener('click', (e) => { 426 | e.preventDefault(); 427 | addKeywordToFilter(); 428 | }); 429 | } 430 | 431 | // 修改添加黑名單的函數 432 | async function addBlacklistKeyword() { 433 | const value = blacklistFilter.value.trim(); 434 | if (value) { 435 | const hasConflict = Array.from(keywords).some(keyword => { 436 | if (keyword === value) return true; 437 | 438 | // 處理 OR 條件 (+) 439 | const keywordOrParts = keyword.split('+').map(p => p.trim()); 440 | const valueOrParts = value.split('+').map(p => p.trim()); 441 | 442 | // 處理 AND 條件 (,) 443 | const keywordAndParts = keywordOrParts.flatMap(p => p.split(',').map(p => p.trim())); 444 | const valueAndParts = valueOrParts.flatMap(p => p.split(',').map(p => p.trim())); 445 | 446 | return keywordAndParts.some(kp => valueAndParts.includes(kp)); 447 | }); 448 | 449 | if (hasConflict) { 450 | const shouldAdd = await showCustomAlert('此條件與關鍵字篩選中的條件重複或衝突,是否仍要新增?'); 451 | if (!shouldAdd) return; 452 | } 453 | blacklist.add(value); 454 | blacklistFilter.value = ''; 455 | renderBlacklist(); 456 | applyFilters(); 457 | } 458 | } 459 | 460 | // Add blacklist on Enter 461 | if (blacklistFilter) { 462 | blacklistFilter.addEventListener('keypress', (e) => { 463 | if (e.key === 'Enter') { 464 | e.preventDefault(); 465 | addBlacklistKeyword(); 466 | } 467 | }); 468 | } 469 | 470 | // Add blacklist on button click 471 | if (addBlacklist) { 472 | addBlacklist.addEventListener('click', (e) => { 473 | e.preventDefault(); 474 | addBlacklistKeyword(); 475 | }); 476 | } 477 | 478 | // Show all button click event 479 | if (showAll) { 480 | showAll.addEventListener('click', (e) => { 481 | e.preventDefault(); 482 | keywords.clear(); 483 | blacklist.clear(); 484 | renderKeywords(); 485 | renderBlacklist(); 486 | showSoldOut.checked = false; 487 | 488 | if (currentSite === 'tixcraft') { 489 | chrome.storage.local.set({ 490 | keywords: [], 491 | blacklist: [], 492 | hideSoldOut: false 493 | }, () => { 494 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 495 | if (tabs[0]) { 496 | chrome.tabs.sendMessage(tabs[0].id, { 497 | type: 'SHOW_ALL' 498 | }); 499 | } 500 | }); 501 | }); 502 | } else if (currentSite === 'kktix') { 503 | // 先清除所有設定 504 | chrome.storage.sync.remove(['targetKeywords', 'blacklist', 'showAllPrices', 'hideSoldOut'], () => { 505 | // 設置新的空值 506 | chrome.storage.sync.set({ 507 | targetKeywords: [], 508 | blacklist: [], 509 | showAllPrices: true, 510 | hideSoldOut: false 511 | }, () => { 512 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 513 | if (tabs[0]) { 514 | chrome.tabs.sendMessage(tabs[0].id, { 515 | action: 'showAllPrices', 516 | settings: { 517 | hideSoldOut: false 518 | } 519 | }, () => { 520 | // 強制重新載入設定 521 | loadKKTIXSettings(); 522 | // 強制更新標籤顯示 523 | renderKeywords(); 524 | renderBlacklist(); 525 | updateFilterLabel(); 526 | }); 527 | } 528 | }); 529 | }); 530 | }); 531 | } else if (currentSite === 'ibon') { 532 | // 先清除 storage 533 | chrome.storage.local.remove(['ibonKeywords', 'ibonBlacklist', 'ibonHideSoldOut'], () => { 534 | // 然後設置新的空值 535 | chrome.storage.local.set({ 536 | ibonKeywords: [], 537 | ibonBlacklist: [], 538 | ibonHideSoldOut: false 539 | }, () => { 540 | // 發送消息到 content script 541 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 542 | if (tabs[0]) { 543 | chrome.tabs.sendMessage(tabs[0].id, { 544 | type: 'UPDATE_IBON_SETTINGS', 545 | settings: { 546 | keywords: [], 547 | blacklist: [], 548 | hideSoldOut: false 549 | } 550 | }, () => { 551 | // 強制重新載入設定 552 | loadIbonSettings(); 553 | }); 554 | } 555 | }); 556 | }); 557 | }); 558 | } else if (currentSite === 'cityline') { 559 | chrome.storage.local.set({ 560 | citylineKeywords: [], 561 | citylineBlacklist: [], 562 | citylineHideSoldOut: false 563 | }, () => { 564 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 565 | if (tabs[0]) { 566 | chrome.tabs.sendMessage(tabs[0].id, { 567 | type: 'UPDATE_CITYLINE_SETTINGS', 568 | settings: { 569 | keywords: [], 570 | blacklist: [], 571 | hideSoldOut: false 572 | } 573 | }); 574 | } 575 | }); 576 | }); 577 | } else if (currentSite === 'ticket') { 578 | chrome.storage.local.set({ 579 | ticketKeywords: [], 580 | ticketHideSoldOut: false 581 | }, () => { 582 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 583 | if (tabs[0]) { 584 | chrome.tabs.sendMessage(tabs[0].id, { 585 | type: 'UPDATE_TICKET_SETTINGS', 586 | settings: { 587 | keywords: [], 588 | hideSoldOut: false 589 | } 590 | }); 591 | } 592 | }); 593 | }); 594 | } else if (currentSite === 'ticketplus') { 595 | chrome.storage.local.set({ 596 | ticketplusKeywords: [], 597 | ticketplusBlacklist: [], 598 | ticketplusHideSoldOut: false 599 | }, () => { 600 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 601 | if (tabs[0]) { 602 | chrome.tabs.sendMessage(tabs[0].id, { 603 | type: 'UPDATE_TICKETPLUS_SETTINGS', 604 | settings: { 605 | keywords: [], 606 | blacklist: [], 607 | hideSoldOut: false 608 | } 609 | }); 610 | } 611 | }); 612 | }); 613 | } 614 | }); 615 | } 616 | 617 | // Show sold out checkbox change event 618 | if (showSoldOut) { 619 | showSoldOut.addEventListener('change', () => { 620 | applyFilters(); 621 | }); 622 | } 623 | 624 | // Apply filters 625 | function applyFilters() { 626 | const keywordArray = Array.from(keywords); 627 | const blacklistArray = Array.from(blacklist); 628 | const site = document.body.getAttribute('data-site'); 629 | 630 | if (site === 'tixcraft') { 631 | const settings = { 632 | keywords: keywordArray, 633 | blacklist: blacklistArray, 634 | hideSoldOut: showSoldOut.checked 635 | }; 636 | chrome.storage.local.set({ 637 | keywords: keywordArray, 638 | blacklist: blacklistArray, 639 | hideSoldOut: showSoldOut.checked 640 | }, () => { 641 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 642 | if (tabs[0]) { 643 | chrome.tabs.sendMessage(tabs[0].id, { 644 | type: 'UPDATE_SETTINGS', 645 | settings: settings 646 | }); 647 | } 648 | }); 649 | }); 650 | } else if (site === 'kktix') { 651 | const settings = { 652 | keywords: keywordArray, 653 | blacklist: blacklistArray, 654 | hideSoldOut: showSoldOut.checked 655 | }; 656 | chrome.storage.sync.set({ 657 | targetKeywords: keywordArray, 658 | blacklist: blacklistArray, 659 | showAllPrices: false, 660 | hideSoldOut: showSoldOut.checked 661 | }, () => { 662 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 663 | if (tabs[0]) { 664 | chrome.tabs.sendMessage(tabs[0].id, { 665 | action: 'updateFilter', 666 | keywords: keywordArray, 667 | blacklist: blacklistArray, 668 | settings: settings 669 | }); 670 | } 671 | }); 672 | }); 673 | } else if (site === 'ibon') { 674 | const settings = { 675 | keywords: keywordArray, 676 | blacklist: blacklistArray, 677 | hideSoldOut: showSoldOut.checked 678 | }; 679 | chrome.storage.local.set({ 680 | ibonKeywords: keywordArray, 681 | ibonBlacklist: blacklistArray, 682 | ibonHideSoldOut: showSoldOut.checked 683 | }, () => { 684 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 685 | if (tabs[0]) { 686 | chrome.tabs.sendMessage(tabs[0].id, { 687 | type: 'UPDATE_IBON_SETTINGS', 688 | settings: settings 689 | }); 690 | } 691 | }); 692 | }); 693 | } else if (site === 'cityline') { 694 | const settings = { 695 | keywords: keywordArray, 696 | blacklist: blacklistArray, 697 | hideSoldOut: showSoldOut.checked 698 | }; 699 | chrome.storage.local.set({ 700 | citylineKeywords: keywordArray, 701 | citylineBlacklist: blacklistArray, 702 | citylineHideSoldOut: showSoldOut.checked 703 | }, () => { 704 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 705 | if (tabs[0]) { 706 | chrome.tabs.sendMessage(tabs[0].id, { 707 | type: 'UPDATE_CITYLINE_SETTINGS', 708 | settings: settings 709 | }); 710 | } 711 | }); 712 | }); 713 | } else if (site === 'ticket') { 714 | const settings = { 715 | keywords: keywordArray, 716 | hideSoldOut: showSoldOut.checked 717 | }; 718 | 719 | // 儲存設定 720 | chrome.storage.local.set({ 721 | ticketKeywords: keywordArray, 722 | ticketHideSoldOut: showSoldOut.checked 723 | }, () => { 724 | // 發送設定到content.js 725 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 726 | if (tabs[0]) { 727 | chrome.tabs.sendMessage(tabs[0].id, { 728 | type: 'UPDATE_TICKET_SETTINGS', 729 | settings: settings 730 | }); 731 | } 732 | }); 733 | }); 734 | } else if (site === 'ticketplus') { 735 | const settings = { 736 | keywords: keywordArray, 737 | blacklist: blacklistArray, 738 | hideSoldOut: showSoldOut.checked 739 | }; 740 | 741 | chrome.storage.local.set({ 742 | ticketplusKeywords: keywordArray, 743 | ticketplusBlacklist: blacklistArray, 744 | ticketplusHideSoldOut: showSoldOut.checked 745 | }, () => { 746 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 747 | if (tabs[0]) { 748 | chrome.tabs.sendMessage(tabs[0].id, { 749 | type: 'UPDATE_TICKETPLUS_SETTINGS', 750 | settings: settings 751 | }); 752 | } 753 | }); 754 | }); 755 | } 756 | } 757 | 758 | // 处理服务器时间显示开关 759 | showServerTime.addEventListener('change', function(e) { 760 | chrome.storage.local.set({ showServerTime: e.target.checked }); 761 | }); 762 | 763 | // 加载时设置开关状态 764 | chrome.storage.local.get(['showServerTime'], function(result) { 765 | // 如果从未设置过,默认为开启 766 | const serverTimeEnabled = result.showServerTime === undefined ? true : result.showServerTime; 767 | showServerTime.checked = serverTimeEnabled; 768 | }); 769 | 770 | // 设置面板相关 771 | const settingsPanel = document.querySelector('.settings-panel'); 772 | 773 | // 点击设置图标显示/隐藏设置面板 774 | settingsIcon.addEventListener('click', (e) => { 775 | e.stopPropagation(); 776 | settingsPanel.classList.toggle('show'); 777 | }); 778 | 779 | // 点击其他地方关闭设置面板 780 | document.addEventListener('click', (e) => { 781 | if (!settingsPanel.contains(e.target) && !settingsIcon.contains(e.target)) { 782 | settingsPanel.classList.remove('show'); 783 | } 784 | }); 785 | 786 | // 处理筛选条件显示设置 787 | showFilterStatus.addEventListener('change', (e) => { 788 | chrome.storage.local.set({ showFilterStatus: e.target.checked }); 789 | }); 790 | 791 | // 加载筛选条件显示设置 792 | chrome.storage.local.get(['showFilterStatus'], function(result) { 793 | const showFilterStatus = result.showFilterStatus === undefined ? true : result.showFilterStatus; 794 | document.getElementById('showFilterStatus').checked = showFilterStatus; 795 | }); 796 | 797 | // 监听设置变化 798 | chrome.storage.onChanged.addListener(function(changes, namespace) { 799 | if (changes.showServerTime) { 800 | if (changes.showServerTime.newValue === false) { 801 | const existingDisplay = document.querySelector('.server-time-display'); 802 | if (existingDisplay) { 803 | existingDisplay.remove(); 804 | } 805 | } else { 806 | showServerTime(); 807 | } 808 | // 重新显示筛选状态,以更新位置 809 | showFilterStatus(); 810 | } 811 | 812 | // 当筛选条件显示设置改变时,重新显示筛选状态 813 | if (changes.showFilterStatus !== undefined) { 814 | if (changes.showFilterStatus.newValue === false) { 815 | // 如果设置为不显示,移除现有的筛选条件显示 816 | const existingNotification = document.querySelector('.ticket-filter-notification'); 817 | if (existingNotification) { 818 | existingNotification.remove(); 819 | } 820 | } else { 821 | // 如果设置为显示,重新显示筛选条件 822 | showFilterStatus(); 823 | } 824 | } 825 | }); 826 | }); -------------------------------------------------------------------------------- /shadowInject.js: -------------------------------------------------------------------------------- 1 | // 在最早期重寫 attachShadow 2 | const script = document.createElement('script'); 3 | script.textContent = ` 4 | // 立即重寫 attachShadow,不允許 closed 模式 5 | (function() { 6 | const originalAttachShadow = Element.prototype.attachShadow; 7 | Element.prototype.attachShadow = function(init) { 8 | // 強制使用 open 模式 9 | return originalAttachShadow.call(this, { mode: 'open' }); 10 | }; 11 | 12 | // 處理已存在的 shadowRoot 13 | function processExistingShadowRoots(root) { 14 | if (!root) return; 15 | 16 | if (root instanceof Element) { 17 | // 如果元素有 closed shadowRoot,嘗試重新創建為 open 18 | if (root.shadowRoot === null && root.getAttribute('shadowroot') === 'closed') { 19 | try { 20 | root.attachShadow({ mode: 'open' }); 21 | } catch(e) {} 22 | } 23 | } 24 | 25 | // 遞迴處理子元素 26 | const children = root.children; 27 | if (children) { 28 | for (let i = 0; i < children.length; i++) { 29 | processExistingShadowRoots(children[i]); 30 | } 31 | } 32 | } 33 | 34 | // 設置 MutationObserver 35 | const observer = new MutationObserver((mutations) => { 36 | for (const mutation of mutations) { 37 | if (mutation.addedNodes) { 38 | mutation.addedNodes.forEach(node => { 39 | if (node.nodeType === 1) { 40 | processExistingShadowRoots(node); 41 | } 42 | }); 43 | } 44 | } 45 | }); 46 | 47 | // 開始觀察 48 | observer.observe(document.documentElement || document.body || document, { 49 | childList: true, 50 | subtree: true 51 | }); 52 | 53 | // 初始處理 54 | processExistingShadowRoots(document.documentElement); 55 | 56 | // 定期檢查 57 | setInterval(() => { 58 | processExistingShadowRoots(document.documentElement); 59 | }, 100); 60 | })(); 61 | `; 62 | (document.head || document.documentElement).appendChild(script); 63 | script.remove(); 64 | 65 | // 注入主要處理腳本 66 | const injectedScript = document.createElement('script'); 67 | injectedScript.src = chrome.runtime.getURL('injected.js'); 68 | (document.head || document.documentElement).appendChild(injectedScript); -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 300px; 3 | padding: 15px; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 5 | } 6 | 7 | .container { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 15px; 11 | } 12 | 13 | h2 { 14 | margin: 0 0 15px 0; 15 | color: #333; 16 | font-size: 18px; 17 | text-align: center; 18 | } 19 | 20 | h3 { 21 | margin: 0 0 10px 0; 22 | color: #666; 23 | font-size: 14px; 24 | } 25 | 26 | .filter-section { 27 | padding: 10px; 28 | background-color: #f8f9fa; 29 | border-radius: 5px; 30 | } 31 | 32 | input[type="text"], 33 | input[type="number"] { 34 | width: 100%; 35 | padding: 8px; 36 | border: 1px solid #ddd; 37 | border-radius: 4px; 38 | box-sizing: border-box; 39 | } 40 | 41 | .price-range { 42 | display: flex; 43 | align-items: center; 44 | gap: 10px; 45 | } 46 | 47 | .price-range input { 48 | width: 45%; 49 | } 50 | 51 | small { 52 | display: block; 53 | margin-top: 5px; 54 | color: #666; 55 | font-size: 12px; 56 | } 57 | 58 | button { 59 | width: 100%; 60 | padding: 10px; 61 | background-color: #007bff; 62 | color: white; 63 | border: none; 64 | border-radius: 4px; 65 | cursor: pointer; 66 | font-size: 14px; 67 | transition: background-color 0.2s; 68 | } 69 | 70 | button:hover { 71 | background-color: #0056b3; 72 | } 73 | 74 | label { 75 | display: flex; 76 | align-items: center; 77 | gap: 8px; 78 | cursor: pointer; 79 | } 80 | 81 | /* 高亮显示匹配的座位 */ 82 | .seat-highlight { 83 | background-color: #90EE90 !important; 84 | border: 2px solid #32CD32 !important; 85 | } --------------------------------------------------------------------------------