Skip to content

Commit a38c46c

Browse files
feat: add spot search
1 parent e1465fa commit a38c46c

File tree

5 files changed

+681
-2
lines changed

5 files changed

+681
-2
lines changed

manifest.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"storage",
5151
"contextMenus",
5252
"scripting",
53-
"sidePanel"
53+
"sidePanel",
54+
"bookmarks"
5455
],
5556
"host_permissions": [
5657
"http://*/*",
@@ -65,6 +66,15 @@
6566
"windows": "Alt+Shift+W"
6667
},
6768
"description": "Merge windows"
69+
},
70+
"spot-search": {
71+
"suggested_key": {
72+
"default": "Alt+Shift+E",
73+
"linux": "Alt+Shift+E",
74+
"mac": "Command+Shift+E",
75+
"windows": "Alt+Shift+E"
76+
},
77+
"description": "Search tabs and bookmarks"
6878
}
6979
}
7080
}

src/background.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { TabGroupsService } from './background/TabGroupsService';
88
import { TabHiveService } from './background/TabHiveService';
99
import { WindowService } from './background/WindowService';
1010
import { ContextMenuService } from './background/ContextMenuService';
11+
import { SpotSearchService } from './background/SpotSearchService';
1112

1213
// Initialize services (Dependency Injection)
1314
const tabRulesService = new TabRulesService();
1415
const tabGroupsService = new TabGroupsService();
1516
const tabHiveService = new TabHiveService();
1617
const windowService = new WindowService();
1718
const contextMenuService = new ContextMenuService();
19+
const spotSearchService = new SpotSearchService();
1820

1921
// =============================================================================
2022
// TAB EVENT LISTENERS
@@ -102,6 +104,31 @@ chrome.runtime.onMessage.addListener(async (message, sender) => {
102104
return;
103105
}
104106

107+
// Handle spot search requests
108+
if (message.action === 'spotSearch') {
109+
const results = await spotSearchService.search(message.query);
110+
if (sender.tab?.id) {
111+
await chrome.tabs.sendMessage(sender.tab.id, {
112+
action: 'spotSearchResults',
113+
tabs: results.tabs,
114+
bookmarks: results.bookmarks,
115+
});
116+
}
117+
return;
118+
}
119+
120+
// Handle spot search activate tab
121+
if (message.action === 'spotSearchActivateTab') {
122+
await spotSearchService.activateTab(message.tabId, message.windowId);
123+
return;
124+
}
125+
126+
// Handle spot search open bookmark
127+
if (message.action === 'spotSearchOpenBookmark') {
128+
await spotSearchService.openBookmark(message.url);
129+
return;
130+
}
131+
105132
if (!sender.tab) return;
106133

107134
const tab = sender.tab as chrome.tabs.Tab;
@@ -236,9 +263,34 @@ chrome.storage.onChanged.addListener((changes, areaName) => {
236263
// COMMAND HANDLERS
237264
// =============================================================================
238265

239-
chrome.commands.onCommand.addListener(async (command) => {
266+
chrome.commands.onCommand.addListener(async (command, tab) => {
267+
console.log('[Tabee] 🔍 Command received:', command);
268+
240269
if (command === 'merge-windows') {
241270
await windowService.mergeAllWindows();
271+
} else if (command === 'spot-search') {
272+
console.log('[Tabee] 🔍 Spot search command triggered');
273+
console.log('[Tabee] 🔍 Tab:', tab);
274+
275+
// Toggle spot search in the active tab
276+
if (!tab?.id) {
277+
console.log('[Tabee] ❌ No tab ID found');
278+
return;
279+
}
280+
281+
// Skip chrome:// and about: pages
282+
if (tab.url && (tab.url.startsWith('chrome://') || tab.url.startsWith('about:'))) {
283+
console.log('[Tabee] ❌ Cannot open spot search on chrome:// or about: pages');
284+
return;
285+
}
286+
287+
console.log('[Tabee] 🔍 Sending toggleSpotSearch message to tab', tab.id);
288+
try {
289+
await chrome.tabs.sendMessage(tab.id, { action: 'toggleSpotSearch' });
290+
console.log('[Tabee] ✅ Message sent successfully');
291+
} catch (error) {
292+
console.error('[Tabee] ❌ Error toggling spot search:', error);
293+
}
242294
}
243295
});
244296

@@ -265,5 +317,9 @@ chrome.action.onClicked.addListener(async (tab) => {
265317
// Initialize Tab Hive auto-close tracking when extension loads
266318
tabHiveService.initialize();
267319

320+
// Log that background script is loaded
321+
console.log('[Tabee] 🐝 Background service worker loaded and ready');
322+
console.log('[Tabee] 🔍 Spot search command handler registered');
323+
268324
// Export for use in other modules
269325
export { tabHiveService };
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Service responsible for spot search functionality
3+
* Handles searching tabs and bookmarks
4+
*/
5+
6+
export interface SpotSearchTab {
7+
id?: number;
8+
title?: string;
9+
url?: string;
10+
favIconUrl?: string;
11+
windowId?: number;
12+
groupId?: number;
13+
groupTitle?: string;
14+
groupColor?: string;
15+
}
16+
17+
export interface SpotSearchBookmark {
18+
id: string;
19+
title: string;
20+
url?: string;
21+
}
22+
23+
export interface SpotSearchResults {
24+
tabs: SpotSearchTab[];
25+
bookmarks: SpotSearchBookmark[];
26+
}
27+
28+
export class SpotSearchService {
29+
/**
30+
* Search tabs and bookmarks based on query
31+
* @param query - Search query (optional)
32+
* @returns Search results containing tabs and bookmarks
33+
*/
34+
async search(query?: string): Promise<SpotSearchResults> {
35+
const lowerQuery = query?.toLowerCase().trim();
36+
37+
// Get all tabs
38+
const allTabs = await chrome.tabs.query({});
39+
const groups = await chrome.tabGroups.query({});
40+
41+
// Filter and map tabs
42+
const tabs = allTabs
43+
.map((tab) => {
44+
let tabGroup = undefined;
45+
46+
if (tab.groupId && tab.groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE) {
47+
tabGroup = groups.find((group) => group.id === tab.groupId);
48+
}
49+
50+
return {
51+
id: tab.id,
52+
title: tab.title,
53+
url: tab.url,
54+
favIconUrl: tab.favIconUrl,
55+
windowId: tab.windowId,
56+
groupId: tab.groupId,
57+
groupTitle: tabGroup?.title,
58+
groupColor: tabGroup?.color,
59+
};
60+
})
61+
.filter((tab) => {
62+
// Filter out chrome:// URLs
63+
if (tab.url?.startsWith('chrome://') || tab.url?.startsWith('about:')) {
64+
return false;
65+
}
66+
67+
// If no query, return all tabs
68+
if (!lowerQuery) {
69+
return true;
70+
}
71+
72+
// Search in title, URL, and group title
73+
return (
74+
tab.title?.toLowerCase().includes(lowerQuery) ||
75+
tab.url?.toLowerCase().includes(lowerQuery) ||
76+
tab.groupTitle?.toLowerCase().includes(lowerQuery)
77+
);
78+
});
79+
80+
// Search bookmarks
81+
let bookmarks: SpotSearchBookmark[] = [];
82+
if (lowerQuery) {
83+
const rawBookmarks = await chrome.bookmarks.search({ query: lowerQuery });
84+
bookmarks = rawBookmarks
85+
.filter((bookmark) => bookmark.url) // Only bookmarks with URLs
86+
.map((bookmark) => ({
87+
id: bookmark.id,
88+
title: bookmark.title,
89+
url: bookmark.url,
90+
}));
91+
}
92+
93+
return { tabs, bookmarks };
94+
}
95+
96+
/**
97+
* Activate a specific tab
98+
* @param tabId - ID of the tab to activate
99+
* @param windowId - ID of the window containing the tab
100+
*/
101+
async activateTab(tabId: number, windowId: number): Promise<void> {
102+
await chrome.tabs.update(tabId, { active: true });
103+
await chrome.windows.update(windowId, { focused: true });
104+
}
105+
106+
/**
107+
* Open a bookmark in a new tab
108+
* @param url - URL of the bookmark to open
109+
*/
110+
async openBookmark(url: string): Promise<void> {
111+
await chrome.tabs.create({ url });
112+
}
113+
}

src/content.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TitleService } from './content/TitleService';
1010
import { IconService } from './content/IconService';
1111
import { StorageService } from './content/StorageService';
1212
import { RuleApplicationService } from './content/RuleApplicationService';
13+
import { SpotSearchUI } from './content/SpotSearchUI';
1314

1415
// ============================================================
1516
// Service Initialization
@@ -21,6 +22,16 @@ const iconService = new IconService();
2122
const storageService = new StorageService(regexService);
2223
const ruleApplicationService = new RuleApplicationService(titleService, iconService);
2324

25+
// Spot Search UI
26+
console.log('[Tabee Content] 🔍 Initializing Spot Search UI...');
27+
const spotSearchUI = new SpotSearchUI();
28+
try {
29+
spotSearchUI.init();
30+
console.log('[Tabee Content] ✅ Spot Search UI initialized');
31+
} catch (error) {
32+
console.error('[Tabee Content] ❌ Failed to initialize Spot Search UI:', error);
33+
}
34+
2435
// ============================================================
2536
// Initial Rule Application
2637
// ============================================================
@@ -41,6 +52,8 @@ const ruleApplicationService = new RuleApplicationService(titleService, iconServ
4152
// ============================================================
4253

4354
chrome.runtime.onMessage.addListener(async function (request) {
55+
console.log('[Tabee Content] 📨 Message received:', request.action);
56+
4457
if (request.action === 'openPrompt') {
4558
const title = prompt(
4659
'Enter the new title, a Tab rule will be automatically created for you based on current URL'
@@ -57,5 +70,14 @@ chrome.runtime.onMessage.addListener(async function (request) {
5770
await ruleApplicationService.applyRule(request.rule, false);
5871
} else if (request.action === 'ungroupTab') {
5972
await chrome.tabs.ungroup(request.tabId);
73+
} else if (request.action === 'toggleSpotSearch') {
74+
console.log('[Tabee Content] 🔍 Toggling spot search UI...');
75+
// Toggle spot search UI
76+
spotSearchUI.toggle();
77+
console.log('[Tabee Content] ✅ Spot search toggled');
78+
} else if (request.action === 'spotSearchResults') {
79+
console.log('[Tabee Content] 🔍 Displaying search results:', request.tabs.length, 'tabs,', request.bookmarks.length, 'bookmarks');
80+
// Display search results
81+
spotSearchUI.displayResults(request.tabs, request.bookmarks);
6082
}
6183
});

0 commit comments

Comments
 (0)