Chapter 10 Advanced Chrome APIs
Haiyue
45min
Chapter 10: Advanced Chrome APIs
Learning Objectives
- Master advanced usage of common Chrome extension APIs
- Learn tab, bookmark, and history management
- Implement network requests and cookie operations
- Apply notification, menu, and shortcut features
10.1 Tabs API Tab Management
10.1.1 Basic Tab Operations
// tabs-manager.js - Tab Manager
class TabsManager {
constructor() {
this.activeTabsCache = new Map();
this.tabGroups = new Map();
this.setupEventListeners();
}
setupEventListeners() {
// Listen for tab changes
chrome.tabs.onCreated.addListener((tab) => {
this.onTabCreated(tab);
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
this.onTabUpdated(tabId, changeInfo, tab);
});
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
this.onTabRemoved(tabId, removeInfo);
});
chrome.tabs.onActivated.addListener((activeInfo) => {
this.onTabActivated(activeInfo);
});
chrome.tabs.onMoved.addListener((tabId, moveInfo) => {
this.onTabMoved(tabId, moveInfo);
});
}
// Get current active tab
async getCurrentTab() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
return tabs[0];
}
// Get all tabs
async getAllTabs() {
return await chrome.tabs.query({});
}
// Find tabs by URL pattern
async findTabsByUrl(urlPattern) {
const allTabs = await this.getAllTabs();
return allTabs.filter(tab => {
if (typeof urlPattern === 'string') {
return tab.url.includes(urlPattern);
} else if (urlPattern instanceof RegExp) {
return urlPattern.test(tab.url);
}
return false;
});
}
// Create new tab
async createTab(options = {}) {
const defaultOptions = {
active: true,
pinned: false
};
const tabOptions = { ...defaultOptions, ...options };
try {
const tab = await chrome.tabs.create(tabOptions);
console.log('Tab created:', tab.id);
return tab;
} catch (error) {
console.error('Failed to create tab:', error);
throw error;
}
}
// Duplicate tab
async duplicateTab(tabId) {
try {
const tab = await chrome.tabs.duplicate(tabId);
console.log('Tab duplicated:', tab.id);
return tab;
} catch (error) {
console.error('Failed to duplicate tab:', error);
throw error;
}
}
// Update tab
async updateTab(tabId, updateProperties) {
try {
const tab = await chrome.tabs.update(tabId, updateProperties);
console.log('Tab updated:', tabId);
return tab;
} catch (error) {
console.error('Failed to update tab:', error);
throw error;
}
}
// Close tab
async closeTab(tabId) {
try {
await chrome.tabs.remove(tabId);
console.log('Tab closed:', tabId);
return true;
} catch (error) {
console.error('Failed to close tab:', error);
return false;
}
}
// Batch close tabs
async closeTabs(tabIds) {
try {
await chrome.tabs.remove(tabIds);
console.log('Batch closed tabs:', tabIds.length);
return true;
} catch (error) {
console.error('Batch close failed:', error);
return false;
}
}
// Move tab
async moveTab(tabId, moveProperties) {
try {
const tabs = await chrome.tabs.move(tabId, moveProperties);
console.log('Tab moved:', tabId);
return tabs;
} catch (error) {
console.error('Failed to move tab:', error);
throw error;
}
}
// Toggle pin tab
async togglePinTab(tabId) {
try {
const tab = await chrome.tabs.get(tabId);
const updatedTab = await chrome.tabs.update(tabId, {
pinned: !tab.pinned
});
console.log(`Tab ${tabId} ${updatedTab.pinned ? 'pinned' : 'unpinned'}`);
return updatedTab;
} catch (error) {
console.error('Failed to toggle pin state:', error);
throw error;
}
}
// Toggle mute
async toggleMuteTab(tabId) {
try {
const tab = await chrome.tabs.get(tabId);
const updatedTab = await chrome.tabs.update(tabId, {
muted: !tab.mutedInfo.muted
});
console.log(`Tab ${tabId} ${updatedTab.mutedInfo.muted ? 'muted' : 'unmuted'}`);
return updatedTab;
} catch (error) {
console.error('Failed to toggle mute state:', error);
throw error;
}
}
// Advanced feature: Tab group management
async createTabGroup(tabIds, options = {}) {
try {
const groupId = await chrome.tabs.group({ tabIds });
if (options.title) {
await chrome.tabGroups.update(groupId, {
title: options.title,
color: options.color || 'blue'
});
}
this.tabGroups.set(groupId, {
id: groupId,
title: options.title || `Group ${groupId}`,
color: options.color || 'blue',
tabIds: tabIds,
createdAt: Date.now()
});
console.log('Tab group created:', groupId);
return groupId;
} catch (error) {
console.error('Failed to create tab group:', error);
throw error;
}
}
// Event handlers
onTabCreated(tab) {
console.log('Tab created:', tab.id, tab.url);
this.activeTabsCache.set(tab.id, tab);
// Send creation event notification
chrome.runtime.sendMessage({
action: 'tabCreated',
tab: tab
});
}
onTabUpdated(tabId, changeInfo, tab) {
console.log('Tab updated:', tabId, changeInfo);
// Update cache
this.activeTabsCache.set(tabId, tab);
// Check URL change
if (changeInfo.url) {
this.handleUrlChange(tab, changeInfo.url);
}
// Check status change
if (changeInfo.status === 'complete') {
this.handlePageLoadComplete(tab);
}
// Send update event notification
chrome.runtime.sendMessage({
action: 'tabUpdated',
tabId: tabId,
changeInfo: changeInfo,
tab: tab
});
}
onTabRemoved(tabId, removeInfo) {
console.log('Tab removed:', tabId);
// Clean up cache
this.activeTabsCache.delete(tabId);
// Send removal event notification
chrome.runtime.sendMessage({
action: 'tabRemoved',
tabId: tabId,
removeInfo: removeInfo
});
}
onTabActivated(activeInfo) {
console.log('Tab activated:', activeInfo.tabId);
// Send activation event notification
chrome.runtime.sendMessage({
action: 'tabActivated',
activeInfo: activeInfo
});
}
onTabMoved(tabId, moveInfo) {
console.log('Tab moved:', tabId, moveInfo);
}
handleUrlChange(tab, newUrl) {
// Handle URL change logic
console.log(`Tab ${tab.id} URL changed: ${newUrl}`);
// Add URL filtering, redirection features here
if (this.shouldBlockUrl(newUrl)) {
this.redirectTab(tab.id, 'chrome://newtab/');
}
}
handlePageLoadComplete(tab) {
// Handle page load complete
console.log(`Tab ${tab.id} load complete: ${tab.title}`);
// Execute content script injection here
this.injectContentScript(tab.id);
}
shouldBlockUrl(url) {
// URL filtering logic
const blockedPatterns = [
/.*\.malicious-site\.com/,
/.*dangerous-content.*/
];
return blockedPatterns.some(pattern => pattern.test(url));
}
async redirectTab(tabId, newUrl) {
try {
await chrome.tabs.update(tabId, { url: newUrl });
console.log(`Tab ${tabId} redirected to: ${newUrl}`);
} catch (error) {
console.error('Redirection failed:', error);
}
}
async injectContentScript(tabId) {
try {
await chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['content-script.js']
});
console.log(`Content script injected to tab ${tabId}`);
} catch (error) {
console.error('Failed to inject content script:', error);
}
}
// Get tab statistics
async getTabStats() {
const allTabs = await this.getAllTabs();
const stats = {
total: allTabs.length,
active: 0,
pinned: 0,
muted: 0,
loading: 0,
byDomain: new Map(),
byWindow: new Map()
};
allTabs.forEach(tab => {
if (tab.active) stats.active++;
if (tab.pinned) stats.pinned++;
if (tab.mutedInfo && tab.mutedInfo.muted) stats.muted++;
if (tab.status === 'loading') stats.loading++;
// Statistics by domain
try {
const domain = new URL(tab.url).hostname;
stats.byDomain.set(domain, (stats.byDomain.get(domain) || 0) + 1);
} catch (e) {
// Ignore invalid URLs
}
// Statistics by window
const windowId = tab.windowId;
stats.byWindow.set(windowId, (stats.byWindow.get(windowId) || 0) + 1);
});
return {
...stats,
byDomain: Object.fromEntries(stats.byDomain),
byWindow: Object.fromEntries(stats.byWindow)
};
}
// Remove duplicate tabs
async removeDuplicateTabs() {
const allTabs = await this.getAllTabs();
const urlMap = new Map();
const duplicateTabs = [];
// Find duplicate tabs
allTabs.forEach(tab => {
if (urlMap.has(tab.url)) {
duplicateTabs.push(tab.id);
} else {
urlMap.set(tab.url, tab.id);
}
});
if (duplicateTabs.length > 0) {
await this.closeTabs(duplicateTabs);
console.log(`Closed ${duplicateTabs.length} duplicate tabs`);
}
return duplicateTabs.length;
}
}
// Global tabs manager instance
const tabsManager = new TabsManager();
// Export for other modules
window.tabsManager = tabsManager;
10.1.2 Tab Object Properties Reference
Tab Object Properties
| Property | Type | Description |
|---|---|---|
id | number | Unique identifier for the tab |
url | string | URL of the tab |
title | string | Title of the tab |
active | boolean | Whether it is the current active tab |
pinned | boolean | Whether it is pinned |
muted | boolean | Whether it is muted |
status | string | Loading status: “loading”, “complete”, “unloaded” |
windowId | number | ID of the window containing the tab |
index | number | Position index in the window |
favIconUrl | string | Website icon URL |
width | number | Tab width (optional) |
height | number | Tab height (optional) |
incognito | boolean | Whether in incognito mode |
highlighted | boolean | Whether highlighted |
selected | boolean | Whether selected (deprecated, kept for compatibility) |
audible | boolean | Whether playing audio |
discarded | boolean | Whether discarded to save memory |
autoDiscardable | boolean | Whether auto-discard is allowed |
groupId | number | Tab group ID (-1 means ungrouped) |
openerTabId | number | ID of the tab that opened this tab (optional) |
pendingUrl | string | URL to be loaded (optional) |
sessionId | string | Session ID (optional) |
Tab Object Interface Definition
interface TabStatus {
LOADING: "loading";
COMPLETE: "complete";
UNLOADED: "unloaded";
}
interface Tab {
id: number;
url: string;
title?: string;
active?: boolean;
pinned?: boolean;
muted?: boolean;
status?: TabStatus;
windowId?: number;
index?: number;
favIconUrl?: string;
width?: number;
height?: number;
incognito?: boolean;
highlighted?: boolean;
selected?: boolean; // deprecated but kept for compatibility
audible?: boolean;
discarded?: boolean;
autoDiscardable?: boolean;
groupId?: number;
openerTabId?: number;
pendingUrl?: string;
sessionId?: string;
}
(Continue with remaining sections of CE-010.md…)
10.2 Bookmarks API Bookmark Management
10.2.1 Bookmark Operations Implementation
// bookmarks-manager.js - Bookmarks Manager
class BookmarksManager {
constructor() {
this.bookmarksCache = new Map();
this.bookmarkFolders = new Map();
this.setupEventListeners();
}
setupEventListeners() {
chrome.bookmarks.onCreated.addListener((id, bookmark) => {
this.onBookmarkCreated(id, bookmark);
});
chrome.bookmarks.onRemoved.addListener((id, removeInfo) => {
this.onBookmarkRemoved(id, removeInfo);
});
chrome.bookmarks.onChanged.addListener((id, changeInfo) => {
this.onBookmarkChanged(id, changeInfo);
});
chrome.bookmarks.onMoved.addListener((id, moveInfo) => {
this.onBookmarkMoved(id, moveInfo);
});
chrome.bookmarks.onChildrenReordered.addListener((id, reorderInfo) => {
this.onChildrenReordered(id, reorderInfo);
});
}
// Get bookmark tree
async getBookmarkTree() {
try {
const tree = await chrome.bookmarks.getTree();
return tree[0]; // Return root node
} catch (error) {
console.error('Failed to get bookmark tree:', error);
throw error;
}
}
// Search bookmarks
async searchBookmarks(query) {
try {
const results = await chrome.bookmarks.search(query);
return results.filter(bookmark => bookmark.url); // Only return actual bookmarks
} catch (error) {
console.error('Failed to search bookmarks:', error);
throw error;
}
}
// Create bookmark
async createBookmark(bookmarkInfo) {
const { title, url, parentId } = bookmarkInfo;
try {
// Check if bookmark with same URL already exists
const existing = await this.searchBookmarks(url);
if (existing.length > 0) {
console.warn('Bookmark already exists:', url);
return existing[0];
}
const bookmark = await chrome.bookmarks.create({
title: title || 'Untitled Bookmark',
url: url,
parentId: parentId
});
console.log('Bookmark created:', bookmark.id);
return bookmark;
} catch (error) {
console.error('Failed to create bookmark:', error);
throw error;
}
}
// Create bookmark folder
async createBookmarkFolder(name, parentId) {
try {
const folder = await chrome.bookmarks.create({
title: name,
parentId: parentId
});
console.log('Bookmark folder created:', folder.id);
return folder;
} catch (error) {
console.error('Failed to create bookmark folder:', error);
throw error;
}
}
// Update bookmark
async updateBookmark(id, changes) {
try {
const updatedBookmark = await chrome.bookmarks.update(id, changes);
console.log('Bookmark updated:', id);
return updatedBookmark;
} catch (error) {
console.error('Failed to update bookmark:', error);
throw error;
}
}
// Delete bookmark
async deleteBookmark(id) {
try {
await chrome.bookmarks.remove(id);
console.log('Bookmark deleted:', id);
return true;
} catch (error) {
console.error('Failed to delete bookmark:', error);
return false;
}
}
// Move bookmark
async moveBookmark(id, destination) {
try {
const movedBookmark = await chrome.bookmarks.move(id, destination);
console.log('Bookmark moved:', id);
return movedBookmark;
} catch (error) {
console.error('Failed to move bookmark:', error);
throw error;
}
}
// Get full path of bookmark
async getBookmarkPath(bookmarkId) {
try {
const path = [];
let currentId = bookmarkId;
while (currentId) {
const bookmarks = await chrome.bookmarks.get(currentId);
const bookmark = bookmarks[0];
if (!bookmark) break;
path.unshift(bookmark);
currentId = bookmark.parentId;
// Avoid infinite loop
if (path.length > 10) break;
}
return path;
} catch (error) {
console.error('Failed to get bookmark path:', error);
return [];
}
}
// Get all bookmarks in folder
async getBookmarksInFolder(folderId, includeSubfolders = false) {
try {
const children = await chrome.bookmarks.getChildren(folderId);
const bookmarks = [];
for (const child of children) {
if (child.url) {
// This is a bookmark
bookmarks.push(child);
} else if (includeSubfolders) {
// This is a folder, recursively get bookmarks
const subBookmarks = await this.getBookmarksInFolder(child.id, true);
bookmarks.push(...subBookmarks);
}
}
return bookmarks;
} catch (error) {
console.error('Failed to get folder bookmarks:', error);
return [];
}
}
// Find duplicate bookmarks
async findDuplicateBookmarks() {
try {
const allBookmarks = await this.searchBookmarks('');
const urlMap = new Map();
const duplicates = [];
allBookmarks.forEach(bookmark => {
if (bookmark.url) {
if (urlMap.has(bookmark.url)) {
duplicates.push({
original: urlMap.get(bookmark.url),
duplicate: bookmark
});
} else {
urlMap.set(bookmark.url, bookmark);
}
}
});
return duplicates;
} catch (error) {
console.error('Failed to find duplicate bookmarks:', error);
return [];
}
}
// Clean duplicate bookmarks
async cleanDuplicateBookmarks() {
const duplicates = await this.findDuplicateBookmarks();
const removedCount = duplicates.length;
for (const { duplicate } of duplicates) {
await this.deleteBookmark(duplicate.id);
}
console.log(`Cleaned ${removedCount} duplicate bookmarks`);
return removedCount;
}
// Export bookmarks
async exportBookmarks() {
try {
const tree = await this.getBookmarkTree();
const exportData = {
version: '1.0',
exportDate: new Date().toISOString(),
bookmarks: this.flattenBookmarkTree(tree)
};
return exportData;
} catch (error) {
console.error('Failed to export bookmarks:', error);
throw error;
}
}
// Flatten bookmark tree
flattenBookmarkTree(node, path = []) {
const bookmarks = [];
if (node.children) {
for (const child of node.children) {
if (child.url) {
bookmarks.push({
title: child.title,
url: child.url,
path: [...path, node.title].filter(p => p),
id: child.id,
dateAdded: child.dateAdded,
dateGroupModified: child.dateGroupModified
});
} else {
bookmarks.push(...this.flattenBookmarkTree(child, [...path, node.title]));
}
}
}
return bookmarks;
}
// Group bookmarks by domain
async groupBookmarksByDomain() {
try {
const allBookmarks = await this.searchBookmarks('');
const domainGroups = new Map();
allBookmarks.forEach(bookmark => {
try {
const url = new URL(bookmark.url);
const domain = url.hostname;
if (!domainGroups.has(domain)) {
domainGroups.set(domain, []);
}
domainGroups.get(domain).push(bookmark);
} catch (error) {
// Ignore invalid URLs
}
});
return Object.fromEntries(domainGroups);
} catch (error) {
console.error('Failed to group by domain:', error);
return {};
}
}
// Bookmark statistics
async getBookmarkStats() {
try {
const tree = await this.getBookmarkTree();
const stats = {
totalBookmarks: 0,
totalFolders: 0,
byDomain: new Map(),
orphanedBookmarks: 0,
recentBookmarks: 0
};
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const countBookmarks = (node) => {
if (node.url) {
stats.totalBookmarks++;
// Count recent bookmarks
if (node.dateAdded && node.dateAdded > oneWeekAgo) {
stats.recentBookmarks++;
}
// Statistics by domain
try {
const domain = new URL(node.url).hostname;
stats.byDomain.set(domain, (stats.byDomain.get(domain) || 0) + 1);
} catch (error) {
stats.orphanedBookmarks++;
}
} else {
stats.totalFolders++;
}
if (node.children) {
node.children.forEach(countBookmarks);
}
};
countBookmarks(tree);
return {
...stats,
byDomain: Object.fromEntries(stats.byDomain)
};
} catch (error) {
console.error('Failed to get bookmark stats:', error);
return null;
}
}
// Event handlers
onBookmarkCreated(id, bookmark) {
console.log('Bookmark created:', id, bookmark.title);
this.bookmarksCache.set(id, bookmark);
// Send creation event notification
chrome.runtime.sendMessage({
action: 'bookmarkCreated',
bookmark: bookmark
});
}
onBookmarkRemoved(id, removeInfo) {
console.log('Bookmark removed:', id);
this.bookmarksCache.delete(id);
// Send removal event notification
chrome.runtime.sendMessage({
action: 'bookmarkRemoved',
bookmarkId: id,
removeInfo: removeInfo
});
}
onBookmarkChanged(id, changeInfo) {
console.log('Bookmark changed:', id, changeInfo);
// Update cache
const cached = this.bookmarksCache.get(id);
if (cached) {
Object.assign(cached, changeInfo);
}
// Send change event notification
chrome.runtime.sendMessage({
action: 'bookmarkChanged',
bookmarkId: id,
changeInfo: changeInfo
});
}
onBookmarkMoved(id, moveInfo) {
console.log('Bookmark moved:', id, moveInfo);
// Send move event notification
chrome.runtime.sendMessage({
action: 'bookmarkMoved',
bookmarkId: id,
moveInfo: moveInfo
});
}
onChildrenReordered(id, reorderInfo) {
console.log('Bookmark children reordered:', id);
// Send reorder event notification
chrome.runtime.sendMessage({
action: 'bookmarkChildrenReordered',
parentId: id,
reorderInfo: reorderInfo
});
}
}
// Global bookmarks manager instance
const bookmarksManager = new BookmarksManager();
// Export for other modules
window.bookmarksManager = bookmarksManager;
10.3 History API History Records
10.3.1 History Management
// history-manager.js - History Manager
class HistoryManager {
constructor() {
this.historyCache = new Map();
this.setupEventListeners();
}
setupEventListeners() {
chrome.history.onVisited.addListener((historyItem) => {
this.onHistoryVisited(historyItem);
});
chrome.history.onVisitRemoved.addListener((removed) => {
this.onHistoryRemoved(removed);
});
}
// Search history
async searchHistory(query, options = {}) {
const defaultOptions = {
text: query,
startTime: 0,
endTime: Date.now(),
maxResults: 100
};
const searchOptions = { ...defaultOptions, ...options };
try {
const results = await chrome.history.search(searchOptions);
console.log(`History search complete: ${results.length} results`);
return results;
} catch (error) {
console.error('Failed to search history:', error);
throw error;
}
}
// Get visit records
async getVisits(url) {
try {
const visits = await chrome.history.getVisits({ url });
return visits;
} catch (error) {
console.error('Failed to get visit records:', error);
throw error;
}
}
// Add history item
async addHistoryItem(url, title) {
try {
await chrome.history.addUrl({
url: url,
title: title || ''
});
console.log('History item added:', url);
return true;
} catch (error) {
console.error('Failed to add history item:', error);
return false;
}
}
// Delete history item
async deleteHistoryItem(url) {
try {
await chrome.history.deleteUrl({ url });
console.log('History item deleted:', url);
return true;
} catch (error) {
console.error('Failed to delete history item:', error);
return false;
}
}
// Delete history range
async deleteHistoryRange(startTime, endTime) {
try {
await chrome.history.deleteRange({
startTime,
endTime
});
console.log(`History deleted: ${new Date(startTime)} - ${new Date(endTime)}`);
return true;
} catch (error) {
console.error('Failed to delete history range:', error);
return false;
}
}
// Clear all history
async clearAllHistory() {
try {
await chrome.history.deleteAll();
console.log('All history cleared');
return true;
} catch (error) {
console.error('Failed to clear history:', error);
return false;
}
}
// Get most visited sites
async getTopSites(limit = 10) {
try {
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const allHistory = await this.searchHistory('', {
startTime: oneMonthAgo,
maxResults: 1000
});
// Count visits by domain
const domainCounts = new Map();
allHistory.forEach(item => {
try {
const url = new URL(item.url);
const domain = url.hostname;
const count = domainCounts.get(domain) || 0;
domainCounts.set(domain, count + item.visitCount);
} catch (error) {
// Ignore invalid URLs
}
});
// Sort and return top N
const sortedDomains = Array.from(domainCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([domain, count]) => ({ domain, count }));
return sortedDomains;
} catch (error) {
console.error('Failed to get top sites:', error);
return [];
}
}
// Get history statistics
async getHistoryStats(days = 30) {
try {
const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
const history = await this.searchHistory('', {
startTime,
maxResults: 10000
});
const stats = {
totalVisits: 0,
uniqueUrls: new Set(),
uniqueDomains: new Set(),
visitsByDay: new Map(),
visitsByHour: new Map(),
topDomains: new Map()
};
history.forEach(item => {
stats.totalVisits += item.visitCount;
stats.uniqueUrls.add(item.url);
try {
const url = new URL(item.url);
const domain = url.hostname;
stats.uniqueDomains.add(domain);
// Count domain visits
const domainCount = stats.topDomains.get(domain) || 0;
stats.topDomains.set(domain, domainCount + item.visitCount);
// Count by day
if (item.lastVisitTime) {
const date = new Date(item.lastVisitTime);
const day = date.toISOString().split('T')[0];
const dayCount = stats.visitsByDay.get(day) || 0;
stats.visitsByDay.set(day, dayCount + 1);
// Count by hour
const hour = date.getHours();
const hourCount = stats.visitsByHour.get(hour) || 0;
stats.visitsByHour.set(hour, hourCount + 1);
}
} catch (error) {
// Ignore invalid URLs
}
});
return {
totalVisits: stats.totalVisits,
uniqueUrls: stats.uniqueUrls.size,
uniqueDomains: stats.uniqueDomains.size,
avgVisitsPerDay: stats.totalVisits / days,
visitsByDay: Object.fromEntries(stats.visitsByDay),
visitsByHour: Object.fromEntries(stats.visitsByHour),
topDomains: Object.fromEntries(
Array.from(stats.topDomains.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
)
};
} catch (error) {
console.error('Failed to get history stats:', error);
return null;
}
}
// Find similar visit patterns
async findSimilarVisitPatterns(targetUrl) {
try {
const targetVisits = await this.getVisits(targetUrl);
const targetTimes = targetVisits.map(v => new Date(v.visitTime).getHours());
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const recentHistory = await this.searchHistory('', {
startTime: oneWeekAgo,
maxResults: 500
});
const similarUrls = [];
for (const item of recentHistory) {
if (item.url === targetUrl) continue;
const visits = await this.getVisits(item.url);
const visitTimes = visits.map(v => new Date(v.visitTime).getHours());
// Calculate time pattern similarity
const similarity = this.calculateTimeSimilarity(targetTimes, visitTimes);
if (similarity > 0.5) {
similarUrls.push({
url: item.url,
title: item.title,
similarity: similarity,
visitCount: item.visitCount
});
}
}
return similarUrls.sort((a, b) => b.similarity - a.similarity);
} catch (error) {
console.error('Failed to find similar visit patterns:', error);
return [];
}
}
calculateTimeSimilarity(times1, times2) {
if (times1.length === 0 || times2.length === 0) return 0;
const set1 = new Set(times1);
const set2 = new Set(times2);
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
// Export history
async exportHistory(days = 30) {
try {
const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
const history = await this.searchHistory('', {
startTime,
maxResults: 10000
});
const exportData = {
exportDate: new Date().toISOString(),
dateRange: {
start: new Date(startTime).toISOString(),
end: new Date().toISOString()
},
totalItems: history.length,
history: history.map(item => ({
url: item.url,
title: item.title,
visitCount: item.visitCount,
lastVisitTime: new Date(item.lastVisitTime).toISOString()
}))
};
return exportData;
} catch (error) {
console.error('Failed to export history:', error);
throw error;
}
}
// Event handlers
onHistoryVisited(historyItem) {
console.log('Page visited:', historyItem.url);
// Update cache
this.historyCache.set(historyItem.id, historyItem);
// Send visit event notification
chrome.runtime.sendMessage({
action: 'historyVisited',
historyItem: historyItem
});
}
onHistoryRemoved(removed) {
console.log('History removed:', removed);
// Clean cache
if (removed.urls) {
removed.urls.forEach(url => {
// Remove corresponding records from cache
for (const [id, item] of this.historyCache.entries()) {
if (item.url === url) {
this.historyCache.delete(id);
}
}
});
} else {
// Clear all
this.historyCache.clear();
}
// Send removal event notification
chrome.runtime.sendMessage({
action: 'historyRemoved',
removed: removed
});
}
}
// Global history manager instance
const historyManager = new HistoryManager();
// Export for other modules
window.historyManager = historyManager;
10.4 Notifications API Notifications
10.4.1 Notification System Implementation
// notifications-manager.js - Notifications Manager
class NotificationsManager {
constructor() {
this.activeNotifications = new Map();
this.notificationQueue = [];
this.isProcessingQueue = false;
this.setupEventListeners();
}
setupEventListeners() {
chrome.notifications.onClicked.addListener((notificationId) => {
this.onNotificationClicked(notificationId);
});
chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
this.onButtonClicked(notificationId, buttonIndex);
});
chrome.notifications.onClosed.addListener((notificationId, byUser) => {
this.onNotificationClosed(notificationId, byUser);
});
chrome.notifications.onPermissionLevelChanged.addListener((level) => {
this.onPermissionLevelChanged(level);
});
}
// Create basic notification
async createNotification(options) {
const notificationId = this.generateNotificationId();
const notificationOptions = {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: 'Notification',
message: 'This is a notification message',
...options
};
try {
await chrome.notifications.create(notificationId, notificationOptions);
// Save notification info
this.activeNotifications.set(notificationId, {
id: notificationId,
options: notificationOptions,
createdAt: Date.now(),
actions: options.actions || []
});
console.log('Notification created:', notificationId);
return notificationId;
} catch (error) {
console.error('Failed to create notification:', error);
throw error;
}
}
// Create rich notification
async createRichNotification(options) {
const { title, message, items, progress, imageUrl, buttons } = options;
const notificationOptions = {
type: items ? 'list' : (progress !== undefined ? 'progress' : 'image'),
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: title || 'Rich Notification',
message: message || '',
...options
};
// Add list items
if (items) {
notificationOptions.items = items.map(item => ({
title: item.title || '',
message: item.message || ''
}));
}
// Add progress bar
if (progress !== undefined) {
notificationOptions.progress = Math.max(0, Math.min(100, progress));
}
// Add image
if (imageUrl) {
notificationOptions.imageUrl = imageUrl;
}
// Add buttons
if (buttons) {
notificationOptions.buttons = buttons.map(button => ({
title: button.title,
iconUrl: button.iconUrl || ''
}));
}
return await this.createNotification(notificationOptions);
}
// Update notification
async updateNotification(notificationId, options) {
try {
const notification = this.activeNotifications.get(notificationId);
if (!notification) {
throw new Error('Notification not found');
}
await chrome.notifications.update(notificationId, options);
// Update cache
notification.options = { ...notification.options, ...options };
notification.updatedAt = Date.now();
console.log('Notification updated:', notificationId);
return true;
} catch (error) {
console.error('Failed to update notification:', error);
return false;
}
}
// Clear notification
async clearNotification(notificationId) {
try {
await chrome.notifications.clear(notificationId);
this.activeNotifications.delete(notificationId);
console.log('Notification cleared:', notificationId);
return true;
} catch (error) {
console.error('Failed to clear notification:', error);
return false;
}
}
// Clear all notifications
async clearAllNotifications() {
const notificationIds = Array.from(this.activeNotifications.keys());
for (const id of notificationIds) {
await this.clearNotification(id);
}
console.log('All notifications cleared');
return notificationIds.length;
}
// Check permission level
async getPermissionLevel() {
return new Promise((resolve) => {
chrome.notifications.getPermissionLevel((level) => {
resolve(level);
});
});
}
// Queue notification system
async queueNotification(options, priority = 'normal') {
const notificationItem = {
id: this.generateNotificationId(),
options: options,
priority: priority,
timestamp: Date.now()
};
this.notificationQueue.push(notificationItem);
this.notificationQueue.sort((a, b) => {
const priorityOrder = { 'high': 0, 'normal': 1, 'low': 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
if (!this.isProcessingQueue) {
this.processNotificationQueue();
}
return notificationItem.id;
}
async processNotificationQueue() {
this.isProcessingQueue = true;
while (this.notificationQueue.length > 0) {
// Check current active notification count
if (this.activeNotifications.size >= 5) {
// Wait for some notifications to be cleared
await this.delay(2000);
continue;
}
const notificationItem = this.notificationQueue.shift();
try {
await this.createNotification(notificationItem.options);
await this.delay(500); // Prevent notifications from being too frequent
} catch (error) {
console.error('Queue notification processing failed:', error);
}
}
this.isProcessingQueue = false;
}
// Smart notification feature
async createSmartNotification(type, data) {
let notificationOptions;
switch (type) {
case 'download_complete':
notificationOptions = {
title: 'Download Complete',
message: `File "${data.filename}" download complete`,
buttons: [
{ title: 'Open File' },
{ title: 'Open Folder' }
],
actions: ['openFile', 'openFolder'],
data: data
};
break;
case 'new_email':
notificationOptions = {
type: 'list',
title: `New Email (${data.count})`,
message: data.count > 1 ? `You have ${data.count} new emails` : 'You have a new email',
items: data.emails.map(email => ({
title: email.sender,
message: email.subject
})),
buttons: [
{ title: 'View Email' }
],
actions: ['openEmail'],
data: data
};
break;
case 'reminder':
notificationOptions = {
title: 'Reminder',
message: data.message,
buttons: [
{ title: 'Done' },
{ title: 'Snooze' }
],
actions: ['markDone', 'snooze'],
data: data
};
break;
case 'progress':
notificationOptions = {
type: 'progress',
title: data.title || 'Progress Notification',
message: data.message,
progress: data.progress,
data: data
};
break;
default:
notificationOptions = {
title: 'Notification',
message: data.message || 'Default notification message',
data: data
};
}
return await this.createNotification(notificationOptions);
}
// Batch notifications
async createBatchNotifications(notifications, options = {}) {
const { delay = 1000, maxConcurrent = 3 } = options;
const results = [];
for (let i = 0; i < notifications.length; i += maxConcurrent) {
const batch = notifications.slice(i, i + maxConcurrent);
const batchPromises = batch.map(notif => this.createNotification(notif));
try {
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
// Delay between batches
if (i + maxConcurrent < notifications.length) {
await this.delay(delay);
}
} catch (error) {
console.error('Batch notification processing failed:', error);
}
}
const successful = results.filter(r => r.status === 'fulfilled').length;
console.log(`Batch notifications complete: ${successful}/${notifications.length}`);
return results;
}
// Event handlers
onNotificationClicked(notificationId) {
console.log('Notification clicked:', notificationId);
const notification = this.activeNotifications.get(notificationId);
if (notification && notification.actions) {
// Execute default action
this.executeAction('click', notification);
}
// Auto-clear notification
this.clearNotification(notificationId);
}
onButtonClicked(notificationId, buttonIndex) {
console.log('Notification button clicked:', notificationId, buttonIndex);
const notification = this.activeNotifications.get(notificationId);
if (notification && notification.actions && notification.actions[buttonIndex]) {
const action = notification.actions[buttonIndex];
this.executeAction(action, notification);
}
// Clear notification
this.clearNotification(notificationId);
}
onNotificationClosed(notificationId, byUser) {
console.log('Notification closed:', notificationId, byUser ? 'User closed' : 'Auto-closed');
this.activeNotifications.delete(notificationId);
// Send close event notification
chrome.runtime.sendMessage({
action: 'notificationClosed',
notificationId: notificationId,
byUser: byUser
});
}
onPermissionLevelChanged(level) {
console.log('Notification permission level changed:', level);
// Adjust notification strategy based on permission level
if (level === 'denied') {
this.notificationQueue = [];
this.clearAllNotifications();
}
}
executeAction(action, notification) {
const data = notification.data || {};
switch (action) {
case 'openFile':
// Open file logic
console.log('Open file:', data.filepath);
break;
case 'openFolder':
// Open folder logic
console.log('Open folder:', data.folder);
break;
case 'openEmail':
// Open email logic
chrome.tabs.create({ url: 'mailto:' });
break;
case 'markDone':
// Mark as done logic
console.log('Task completed:', data.taskId);
break;
case 'snooze':
// Snooze logic
setTimeout(() => {
this.createSmartNotification('reminder', data);
}, 15 * 60 * 1000); // Remind again after 15 minutes
break;
default:
console.log('Execute action:', action, data);
}
}
generateNotificationId() {
return `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Get notification statistics
getNotificationStats() {
const active = this.activeNotifications.size;
const queued = this.notificationQueue.length;
const typeStats = {};
for (const notification of this.activeNotifications.values()) {
const type = notification.options.type || 'basic';
typeStats[type] = (typeStats[type] || 0) + 1;
}
return {
active: active,
queued: queued,
total: active + queued,
byType: typeStats,
isProcessingQueue: this.isProcessingQueue
};
}
}
// Global notifications manager instance
const notificationsManager = new NotificationsManager();
// Export for other modules
window.notificationsManager = notificationsManager;
warning Permission Requirements Using these Chrome APIs requires declaring appropriate permissions in manifest.json:
tabs: Tab operationsbookmarks: Bookmark managementhistory: History recordsnotifications: Notification featuresactiveTab: Current tab access
tip Best Practices
- Error Handling: Always handle exceptions from API calls
- Permission Checks: Check permission status before using APIs
- Performance Optimization: Avoid frequent API calls, use caching
- User Experience: Provide clear feedback and progress indicators
- Privacy Protection: Handle sensitive data like history carefully
note Summary This chapter provided an in-depth introduction to the most commonly used APIs in Chrome extensions, including tab management, bookmark operations, history processing, and notification systems. Mastering these advanced API usages will enable you to develop powerful browser extensions. In the next chapter, we will learn about user interface design and experience optimization to create more user-friendly extension interfaces.