diff --git a/README.md b/README.md
index 0059fbe..93ac343 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,79 @@
-# ModHeader browser extension
+# ModHeader(Manifest V3 版)
-
- ModHeader is a browser extension that allow you to modify HTTP request and response headers.
-
-
-
-
-
-
-
-
-
-
+一个用于**修改 HTTP 请求头和响应头**的浏览器扩展。
-## Donation
+## 项目来源
-If you find ModHeader useful, please consider making a donation. If you use it for your company project, please ask your company to make a monthly donation!
+本项目 fork/整理自开源扩展 [ModHeader](https://github.com/hao1300/modheader)(原作者 Hao Nguyen,MIT 许可)。原始代码基于 **Manifest V2**,依赖已被 Chrome 废弃的阻塞式 `webRequest` API。
-[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=3XFKZ8PCRB8P6¤cy_code=USD&amount=5&source=url)
-
-
-
+本仓库在其早期源码基础上做了以下改造:
-## Features
+- **迁移到 Manifest V3**:后台页改为 Service Worker,请求/响应头修改改用 `declarativeNetRequest`(DNR)动态规则实现。
+- **存储层迁移**:状态从 `localStorage` 迁移到 `chrome.storage.local`(MV3 的 Service Worker 无法访问 `localStorage`),并保留对旧数据的一次性自动迁移。
+- **精简**:移除了云备份、捐赠、推广提示等无用功能与相关死代码、冗余资源。
-* Add/modify/remove request headers and response headers
-* Enable header modification based on URL/resource type
-* Add comments to header
-* Multiple different profiles
-* Sorting headers by name, value, or comments
-* Append value to existing request or response header
-* Export and import header
-* Clone profile
-* Cloud backup
-* Tab locking!
+前端仍保留原有的 AngularJS + Angular Material 界面。
-## Screenshots
+## 功能
-
+- 增加 / 修改 / 删除**请求头**与**响应头**
+- 按 **URL 正则**或**资源类型**限定生效范围(Filter)
+- 多套 **Profile** 配置,可快速切换
+- 给每条 header 添加**备注**
+- 按名称 / 值 / 备注**排序**
+- 在已有 header 值后**追加**(拼接 / 逗号拼接)
+- **导出 / 导入** Profile(JSON)
+- **克隆** Profile
+- **Tab 锁**:仅对指定标签页生效
+- **暂停**:一键临时停用全部修改
-## Forking and contribution
+## 安装 / 本地开发
-Feel free to send pull requests to add new features to ModHeader. It will benefit everyone! That said, I reserve the rights to reject pull requests that does not seem useful, or if they add too much complexity for very little benefits.
+无需任何构建工具,直接加载 `src` 目录即可:
-You may fork and redistribute ModHeader for a small group of friends / colleagues, but please do not impersonate ModHeader, or try to sell it for a profit. If you use ModHeader in any commercial product, please let me know.
+1. 浏览器打开 `chrome://extensions`
+2. 打开右上角**开发者模式**
+3. 点击**加载已解压的扩展程序**,选择本仓库的 `src` 目录
+4. 修改代码后,回到该页面点扩展卡片上的**刷新(↻)**即可重新加载
-## Installation
+## 使用方法
-ModHeader does not require any extra tool for building. Simply load the src directory into Chrome / Firefox to start local development.
+1. 点击工具栏中的 ModHeader 图标打开弹窗。
+2. 点右上角 **+**:
+ - **Request header**:添加请求头(如 `User-Agent`、`Authorization` 等)
+ - **Response header**:添加响应头(如 `Access-Control-Allow-Origin`)
+ - **Filter**:添加生效条件(URL 正则或资源类型)
+3. 勾选每行左侧的复选框以**启用**该条修改;填写 Name / Value。
+4. 打开目标网站并刷新,在 DevTools 的 **Network** 面板中即可确认头已被修改。
+5. 图标上的**数字角标**表示当前生效的 header 数量。
-## Selenium usage
+### 生效范围说明
-If you need to use ModHeader for Selenium tests, please visit: https://github.com/hao1300/modheader_selenium
+- **不加 Filter**:对所有 URL 生效。
+- **URL Pattern**:填写正则(如 `https://example\.com/.*`),仅匹配的请求生效;多条之间为「或」的关系。
+- **Resource Type**:仅对选中的资源类型(main_frame、xmlhttprequest 等)生效。
+- **Tab 锁**:点弹窗中的 “Tab lock”,修改将只作用于当前标签页。
+
+## 目录结构
+
+```
+src/
+├── manifest.json # MV3 清单
+├── background.js # Service Worker:DNR 规则生成、角标、右键菜单
+├── popup.html # 弹窗界面
+├── scripts/
+│ ├── main.js # AngularJS 逻辑 + storage.local 适配层
+│ └── angular*.min.js # AngularJS 1.7.8 及其模块(官方,MIT)
+├── styles/ # 样式
+├── images/ # 图标
+└── *.tmpl.html # 各对话框模板
+```
+
+## 已知限制
+
+- `declarativeNetRequest` 的 `append` 操作对部分请求头有限制,遇到不支持的 header 时该规则会失败并在 Service Worker 控制台打印错误。
+- Tab 锁通过 DNR 的 **session 规则** + `tabIds` 实现,浏览器重启后会失效(符合预期)。
+
+## 许可
+
+沿用原项目的 **MIT** 许可。
diff --git a/src/background.js b/src/background.js
index 78a4762..317e63e 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,397 +1,307 @@
-const browser = chrome;
-const SPECIAL_CHARS = '^$&+?.()|{}[]/'.split('');
-const MAX_PROFILES_IN_CLOUD = 50;
-const CHROME_VERSION = getChromeVersion();
-const EXTRA_REQUEST_HEADERS = new Set(['accept-language', 'accept-encoding', 'referer', 'cookie']);
-const EXTRA_RESPONSE_HEADERS = new Set(['set-cookie']);
-let currentProfile;
-let tabUrls = {};
-
-/**
- * Check whether the current request url pass the given list of filters.
- */
-function passFilters_(url, type, filters) {
- if (!filters) {
- return true;
- }
- let allowUrls = false;
- let hasUrlFilters = false;
- let allowTypes = false;
- let hasResourceTypeFilters = false;
- for (let filter of filters) {
- if (filter.enabled) {
- switch (filter.type) {
- case 'urls':
- hasUrlFilters = true;
- if (url.search(filter.urlRegex) == 0) {
- allowUrls = true;
- }
- break;
- case 'types':
- hasResourceTypeFilters = true;
- if (filter.resourceType.indexOf(type) >= 0) {
- allowTypes = true;
- }
- break;
- }
- }
- }
- return (!hasUrlFilters || allowUrls)
- && (!hasResourceTypeFilters || allowTypes);
-};
-
-function loadSelectedProfile_() {
- let appendMode = false;
- let headers = [];
- let respHeaders = [];
- let filters = [];
- if (localStorage.profiles) {
- const profiles = JSON.parse(localStorage.profiles);
- if (!localStorage.selectedProfile) {
- localStorage.selectedProfile = 0;
- }
- const selectedProfile = profiles[localStorage.selectedProfile];
-
- function filterEnabledHeaders_(headers) {
- let output = [];
- for (let header of headers) {
- // Overrides the header if it is enabled and its name is not empty.
- if (header.enabled && header.name) {
- output.push({name: header.name, value: header.value});
- }
- }
- return output;
- };
- for (let filter of selectedProfile.filters) {
- if (filter.urlPattern) {
- const urlPattern = filter.urlPattern;
- const joiner = [];
- for (let i = 0; i < urlPattern.length; ++i) {
- let c = urlPattern.charAt(i);
- if (SPECIAL_CHARS.indexOf(c) >= 0) {
- c = '\\' + c;
- } else if (c == '\\') {
- c = '\\\\';
- } else if (c == '*') {
- c = '.*';
- }
- joiner.push(c);
- }
- filter.urlRegex = joiner.join('');
- }
- filters.push(filter);
- }
- appendMode = selectedProfile.appendMode;
- headers = filterEnabledHeaders_(selectedProfile.headers);
- respHeaders = filterEnabledHeaders_(selectedProfile.respHeaders);
- }
- return {
- appendMode: appendMode,
- headers: headers,
- respHeaders: respHeaders,
- filters: filters
- };
-};
-
-function modifyHeader(source, dest) {
- if (!source.length) {
- return;
- }
- // Create an index map so that we can more efficiently override
- // existing header.
- const indexMap = {};
- for (const index in dest) {
- const header = dest[index];
- indexMap[header.name.toLowerCase()] = index;
- }
- for (let header of source) {
- const index = indexMap[header.name.toLowerCase()];
- if (index !== undefined) {
- if (!currentProfile.appendMode) {
- dest[index].value = header.value;
- } else if (currentProfile.appendMode == 'comma') {
- if (dest[index].value) {
- dest[index].value += ',';
- }
- dest[index].value += header.value;
- } else {
- dest[index].value += header.value;
- }
- } else {
- dest.push({name: header.name, value: header.value});
- indexMap[header.name.toLowerCase()] = dest.length - 1;
- }
- }
-};
-
-function modifyRequestHeaderHandler_(details) {
- if (localStorage.isPaused) {
- return {};
- }
- if (details.type == 'main_frame' && details.url && details.tabId) {
- tabUrls[details.tabId] = details.url;
- localStorage.activeTabId = details.tabId;
- browser.tabs.get(details.tabId, onTabUpdated);
- }
- if (!localStorage.lockedTabId
- || localStorage.lockedTabId == details.tabId) {
- if (currentProfile
- && passFilters_(details.url, details.type, currentProfile.filters)) {
- modifyHeader(currentProfile.headers, details.requestHeaders);
- }
- }
- return {requestHeaders: details.requestHeaders};
-};
-
-function modifyResponseHeaderHandler_(details) {
- if (localStorage.isPaused) {
- return {};
- }
- if (!localStorage.lockedTabId
- || localStorage.lockedTabId == details.tabId) {
- if (currentProfile
- && passFilters_(details.url, details.type, currentProfile.filters)) {
- const serializedOriginalResponseHeaders = JSON.stringify(details.responseHeaders);
- const responseHeaders = JSON.parse(serializedOriginalResponseHeaders);
- modifyHeader(currentProfile.respHeaders, responseHeaders);
- if (JSON.stringify(responseHeaders) != serializedOriginalResponseHeaders) {
- return {responseHeaders: responseHeaders};
- }
- }
- }
-};
-
-function getChromeVersion() {
- let pieces = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);
- if (pieces == null || pieces.length != 5) {
- return {};
- }
- pieces = pieces.map(piece => parseInt(piece, 10));
- return {
- major: pieces[1],
- minor: pieces[2],
- build: pieces[3],
- patch: pieces[4]
- };
-}
-
-function setupHeaderModListener() {
- browser.webRequest.onBeforeSendHeaders.removeListener(modifyRequestHeaderHandler_);
- browser.webRequest.onHeadersReceived.removeListener(modifyResponseHeaderHandler_);
-
- // Chrome 72+ requires 'extraHeaders' to be added for some headers to be modifiable.
- // Older versions break with it.
- if (currentProfile.headers.length > 0) {
- let requiresExtraRequestHeaders = false;
- if (CHROME_VERSION.major >= 72) {
- for (let header of currentProfile.headers) {
- if (EXTRA_REQUEST_HEADERS.has(header.name.toLowerCase())) {
- requiresExtraRequestHeaders = true;
- break;
- }
- }
- }
- browser.webRequest.onBeforeSendHeaders.addListener(
- modifyRequestHeaderHandler_,
- {urls: [""]},
- requiresExtraRequestHeaders ? ['requestHeaders', 'blocking', 'extraHeaders'] : ['requestHeaders', 'blocking']
- );
- }
- if (currentProfile.respHeaders.length > 0) {
- let requiresExtraResponseHeaders = false;
- if (CHROME_VERSION.major >= 72) {
- for (let header of currentProfile.respHeaders) {
- if (EXTRA_RESPONSE_HEADERS.has(header.name.toLowerCase())) {
- requiresExtraResponseHeaders = true;
- break;
- }
- }
- }
- browser.webRequest.onHeadersReceived.addListener(
- modifyResponseHeaderHandler_,
- {urls: [""]},
- requiresExtraResponseHeaders ? ['responseHeaders', 'blocking', 'extraHeaders'] : ['responseHeaders', 'blocking']
- );
- }
-}
-
-function onTabUpdated(tab) {
- if (tab.active) {
- delete localStorage.currentTabUrl;
- // Since we don't have access to the "tabs" permission, we may not have
- // access to the url property all the time. So, match it against the URL
- // found during request modification.
- let url = tab.url;
- if (url) {
- tabUrls[tab.id] = url;
- } else {
- url = tabUrls[tab.id];
- }
- localStorage.activeTabId = tab.id;
-
- // Only set the currentTabUrl property if the tab is active and the window
- // is in focus.
- browser.windows.get(tab.windowId, {}, (win) => {
- if (win.focused) {
- localStorage.currentTabUrl = url;
- }
- });
- if (!url) {
- return;
- }
- resetBadgeAndContextMenu();
- }
-}
-
-browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
- onTabUpdated(tab);
-});
-
-browser.tabs.onActivated.addListener((activeInfo) => {
- browser.tabs.get(activeInfo.tabId, onTabUpdated);
-});
-
-browser.windows.onFocusChanged.addListener((windowId) => {
- if (windowId == browser.windows.WINDOW_ID_NONE) {
- return;
- }
- browser.windows.get(windowId, {populate: true}, (win) => {
- for (let tab of win.tabs) {
- onTabUpdated(tab);
- }
- });
-});
-
-function saveStorageToCloud() {
- browser.storage.sync.get(null, (items) => {
- const keys = items ? Object.keys(items) : [];
- keys.sort();
- if (keys.length == 0 ||
- items[keys[keys.length - 1]] != localStorage.profiles) {
- const data = {};
- data[Date.now()] = localStorage.profiles;
- browser.storage.sync.set(data);
- localStorage.savedToCloud = true;
- }
- if (keys.length >= MAX_PROFILES_IN_CLOUD) {
- browser.storage.sync.remove(keys.slice(0, keys.length - MAX_PROFILES_IN_CLOUD));
- }
- });
-}
-
-function createContextMenu() {
- if (localStorage.isPaused) {
- browser.contextMenus.update(
- 'pause',
- {
- title: 'Unpause ModHeader',
- contexts: ['browser_action'],
- onclick: () => {
- localStorage.removeItem('isPaused');
- resetBadgeAndContextMenu();
- }
- });
- } else {
- browser.contextMenus.update(
- 'pause',
- {
- title: 'Pause ModHeader',
- contexts: ['browser_action'],
- onclick: () => {
- localStorage.isPaused = true;
- resetBadgeAndContextMenu();
- }
- });
- }
- if (localStorage.lockedTabId) {
- browser.contextMenus.update(
- 'lock',
- {
- title: 'Unlock to all tabs',
- contexts: ['browser_action'],
- onclick: () => {
- localStorage.removeItem('lockedTabId');
- resetBadgeAndContextMenu();
- }
- });
- } else {
- browser.contextMenus.update(
- 'lock',
- {
- title: 'Lock to this tab',
- contexts: ['browser_action'],
- onclick: () => {
- localStorage.lockedTabId = localStorage.activeTabId;
- resetBadgeAndContextMenu();
- }
- });
- }
-}
-
-function resetBadgeAndContextMenu() {
- if (localStorage.isPaused) {
- browser.browserAction.setIcon({path: 'icon_bw.png'});
- browser.browserAction.setBadgeText({text: '\u275A\u275A'});
- browser.browserAction.setBadgeBackgroundColor({color: '#666'});
- } else {
- const numHeaders = (currentProfile.headers.length + currentProfile.respHeaders.length);
- if (numHeaders == 0) {
- browser.browserAction.setBadgeText({text: ''});
- browser.browserAction.setIcon({path: 'icon_bw.png'});
- } else if (localStorage.lockedTabId
- && localStorage.lockedTabId != localStorage.activeTabId) {
- browser.browserAction.setIcon({path: 'icon_bw.png'});
- browser.browserAction.setBadgeText({text: '\uD83D\uDD12'});
- browser.browserAction.setBadgeBackgroundColor({color: '#ff8e8e'});
- } else {
- browser.browserAction.setIcon({path: 'icon.png'});
- browser.browserAction.setBadgeText({text: numHeaders.toString()});
- browser.browserAction.setBadgeBackgroundColor({color: '#db4343'});
- }
- }
- createContextMenu();
-}
-
-function initializeStorage() {
- currentProfile = loadSelectedProfile_();
- setupHeaderModListener();
-
- window.addEventListener('storage', function(e) {
- currentProfile = loadSelectedProfile_();
- setupHeaderModListener();
- resetBadgeAndContextMenu();
- if (e.key == 'profiles') {
- saveStorageToCloud();
- }
- });
-
- // Async initialization.
- setTimeout(() => {
- if (localStorage.profiles && !localStorage.savedToCloud) {
- saveStorageToCloud();
- }
-
- if (!localStorage.profiles) {
- browser.storage.sync.get(null, (items) => {
- const keys = items ? Object.keys(items) : [];
- keys.sort();
- if (keys.length > 0) {
- localStorage.profiles = items[keys[keys.length - 1]];
- localStorage.savedToCloud = true;
- }
- });
- }
- }, 100);
-}
-browser.contextMenus.create({
- id: 'pause',
- title: 'Pause',
- contexts: ['browser_action'],
-});
-browser.contextMenus.create({
- id: 'lock',
- title: 'Lock',
- contexts: ['browser_action'],
-});
-initializeStorage();
-resetBadgeAndContextMenu();
+// ModHeader background service worker (Manifest V3).
+// Header modification is done through the declarativeNetRequest (DNR) API
+// since MV3 no longer allows blocking webRequest.
+
+const SPECIAL_CHARS = '^$&+?.()|{}[]/'.split('');
+const RULE_ID = 1;
+
+// -- Storage helpers ---------------------------------------------------------
+
+async function getState() {
+ return chrome.storage.local.get([
+ 'profiles', 'selectedProfile', 'isPaused', 'lockedTabId', 'activeTabId'
+ ]);
+}
+
+function compileUrlPattern(urlPattern) {
+ const joiner = [];
+ for (let i = 0; i < urlPattern.length; ++i) {
+ let c = urlPattern.charAt(i);
+ if (SPECIAL_CHARS.indexOf(c) >= 0) {
+ c = '\\' + c;
+ } else if (c == '\\') {
+ c = '\\\\';
+ } else if (c == '*') {
+ c = '.*';
+ }
+ joiner.push(c);
+ }
+ return joiner.join('');
+}
+
+function loadSelectedProfile(state) {
+ if (!state.profiles) {
+ return null;
+ }
+ let profiles;
+ try {
+ profiles = JSON.parse(state.profiles);
+ } catch (e) {
+ return null;
+ }
+ const index = Number(state.selectedProfile) || 0;
+ const profile = profiles[index];
+ if (!profile) {
+ return null;
+ }
+
+ const filterEnabled = (headers) => (headers || [])
+ .filter(h => h.enabled && h.name)
+ .map(h => ({ name: h.name, value: h.value || '' }));
+
+ const filters = [];
+ for (const filter of profile.filters || []) {
+ const normalized = Object.assign({}, filter);
+ // Backward compatibility: convert legacy urlPattern to urlRegex.
+ if (normalized.urlPattern) {
+ normalized.urlRegex = compileUrlPattern(normalized.urlPattern);
+ delete normalized.urlPattern;
+ }
+ filters.push(normalized);
+ }
+
+ return {
+ appendMode: profile.appendMode,
+ headers: filterEnabled(profile.headers),
+ respHeaders: filterEnabled(profile.respHeaders),
+ filters
+ };
+}
+
+// -- DNR rule generation -----------------------------------------------------
+
+function anchor(regex) {
+ return regex.startsWith('^') ? regex : '^' + regex;
+}
+
+function buildRule(profile) {
+ const operation = profile.appendMode ? 'append' : 'set';
+ const requestHeaders = profile.headers.map(
+ h => ({ header: h.name, operation, value: h.value }));
+ const responseHeaders = profile.respHeaders.map(
+ h => ({ header: h.name, operation, value: h.value }));
+ if (!requestHeaders.length && !responseHeaders.length) {
+ return null;
+ }
+
+ const condition = {};
+ const urlRegexes = [];
+ const resourceTypes = [];
+ for (const filter of profile.filters) {
+ if (!filter.enabled) {
+ continue;
+ }
+ if (filter.type === 'urls' && filter.urlRegex) {
+ urlRegexes.push(filter.urlRegex);
+ } else if (filter.type === 'types' && filter.resourceType) {
+ resourceTypes.push(...filter.resourceType);
+ }
+ }
+ if (urlRegexes.length === 1) {
+ condition.regexFilter = anchor(urlRegexes[0]);
+ } else if (urlRegexes.length > 1) {
+ condition.regexFilter = urlRegexes.map(r => `(${anchor(r)})`).join('|');
+ }
+ if (resourceTypes.length) {
+ condition.resourceTypes = [...new Set(resourceTypes)];
+ }
+
+ const action = { type: 'modifyHeaders' };
+ if (requestHeaders.length) {
+ action.requestHeaders = requestHeaders;
+ }
+ if (responseHeaders.length) {
+ action.responseHeaders = responseHeaders;
+ }
+ return { id: RULE_ID, priority: 1, action, condition };
+}
+
+async function clearAllRules() {
+ const [dynamic, session] = await Promise.all([
+ chrome.declarativeNetRequest.getDynamicRules(),
+ chrome.declarativeNetRequest.getSessionRules()
+ ]);
+ await Promise.all([
+ chrome.declarativeNetRequest.updateDynamicRules({
+ removeRuleIds: dynamic.map(r => r.id)
+ }),
+ chrome.declarativeNetRequest.updateSessionRules({
+ removeRuleIds: session.map(r => r.id)
+ })
+ ]);
+}
+
+async function updateRules(state, profile) {
+ await clearAllRules();
+ if (state.isPaused || !profile) {
+ return;
+ }
+ const rule = buildRule(profile);
+ if (!rule) {
+ return;
+ }
+ try {
+ if (state.lockedTabId) {
+ // tabIds conditions are only allowed on session-scoped rules.
+ rule.condition.tabIds = [Number(state.lockedTabId)];
+ await chrome.declarativeNetRequest.updateSessionRules({ addRules: [rule] });
+ } else {
+ await chrome.declarativeNetRequest.updateDynamicRules({ addRules: [rule] });
+ }
+ } catch (e) {
+ console.error('ModHeader: failed to apply rules', e, rule);
+ }
+}
+
+// -- Badge / icon ------------------------------------------------------------
+
+async function updateBadge(state, profile) {
+ if (state.isPaused) {
+ await chrome.action.setIcon({ path: 'icon_bw.png' });
+ await chrome.action.setBadgeText({ text: '❚❚' });
+ await chrome.action.setBadgeBackgroundColor({ color: '#666' });
+ return;
+ }
+ const numHeaders = profile
+ ? profile.headers.length + profile.respHeaders.length
+ : 0;
+ if (numHeaders === 0) {
+ await chrome.action.setBadgeText({ text: '' });
+ await chrome.action.setIcon({ path: 'icon_bw.png' });
+ } else if (state.lockedTabId
+ && String(state.lockedTabId) !== String(state.activeTabId)) {
+ await chrome.action.setIcon({ path: 'icon_bw.png' });
+ await chrome.action.setBadgeText({ text: '🔒' });
+ await chrome.action.setBadgeBackgroundColor({ color: '#ff8e8e' });
+ } else {
+ await chrome.action.setIcon({ path: 'icon.png' });
+ await chrome.action.setBadgeText({ text: numHeaders.toString() });
+ await chrome.action.setBadgeBackgroundColor({ color: '#db4343' });
+ }
+}
+
+// -- Context menu ------------------------------------------------------------
+
+async function updateContextMenu(state) {
+ try {
+ await chrome.contextMenus.update('pause', {
+ title: state.isPaused ? 'Unpause ModHeader' : 'Pause ModHeader'
+ });
+ await chrome.contextMenus.update('lock', {
+ title: state.lockedTabId ? 'Unlock to all tabs' : 'Lock to this tab'
+ });
+ } catch (e) {
+ // Menu may not exist yet; ignore.
+ }
+}
+
+// -- Main refresh ------------------------------------------------------------
+
+async function refresh() {
+ const state = await getState();
+ const profile = loadSelectedProfile(state);
+ await updateRules(state, profile);
+ await updateBadge(state, profile);
+ await updateContextMenu(state);
+}
+
+async function refreshBadgeOnly() {
+ const state = await getState();
+ const profile = loadSelectedProfile(state);
+ await updateBadge(state, profile);
+}
+
+// -- Tab tracking ------------------------------------------------------------
+
+async function onActiveTab(tab) {
+ if (!tab || tab.id === undefined) {
+ return;
+ }
+ const update = { activeTabId: tab.id };
+ if (tab.url) {
+ update.currentTabUrl = tab.url;
+ }
+ await chrome.storage.local.set(update);
+ await refreshBadgeOnly();
+}
+
+chrome.tabs.onActivated.addListener(async ({ tabId }) => {
+ try {
+ const tab = await chrome.tabs.get(tabId);
+ await onActiveTab(tab);
+ } catch (e) { /* tab gone */ }
+});
+
+chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (tab && tab.active) {
+ onActiveTab(tab);
+ }
+});
+
+chrome.windows.onFocusChanged.addListener(async (windowId) => {
+ if (windowId === chrome.windows.WINDOW_ID_NONE) {
+ return;
+ }
+ try {
+ const [tab] = await chrome.tabs.query({ active: true, windowId });
+ if (tab) {
+ await onActiveTab(tab);
+ }
+ } catch (e) { /* no window */ }
+});
+
+// -- Storage change ----------------------------------------------------------
+
+chrome.storage.onChanged.addListener((changes, area) => {
+ if (area !== 'local') {
+ return;
+ }
+ if (changes.profiles || changes.selectedProfile
+ || changes.isPaused || changes.lockedTabId) {
+ refresh();
+ } else if (changes.activeTabId) {
+ refreshBadgeOnly();
+ }
+});
+
+// -- Context menu clicks -----------------------------------------------------
+
+chrome.contextMenus.onClicked.addListener(async (info, tab) => {
+ const state = await getState();
+ if (info.menuItemId === 'pause') {
+ if (state.isPaused) {
+ await chrome.storage.local.remove('isPaused');
+ } else {
+ await chrome.storage.local.set({ isPaused: 'true' });
+ }
+ } else if (info.menuItemId === 'lock') {
+ if (state.lockedTabId) {
+ await chrome.storage.local.remove('lockedTabId');
+ } else if (tab) {
+ await chrome.storage.local.set({ lockedTabId: tab.id });
+ }
+ }
+ // refresh() is triggered by the resulting storage change event.
+});
+
+// -- Lifecycle ---------------------------------------------------------------
+
+chrome.runtime.onInstalled.addListener(() => {
+ chrome.contextMenus.removeAll(() => {
+ chrome.contextMenus.create({
+ id: 'pause',
+ title: 'Pause ModHeader',
+ contexts: ['action']
+ });
+ chrome.contextMenus.create({
+ id: 'lock',
+ title: 'Lock to this tab',
+ contexts: ['action']
+ });
+ });
+ refresh();
+});
+
+chrome.runtime.onStartup.addListener(() => {
+ refresh();
+});
diff --git a/src/cloudbackupdialog.tmpl.html b/src/cloudbackupdialog.tmpl.html
deleted file mode 100644
index 8067ae6..0000000
--- a/src/cloudbackupdialog.tmpl.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
Cloud backup
-
-
-
-
-
- Backup at {{data.timeInMs | date:'short'}} -
-
- {{profile.title}}{{$last ? '' : ','}}
-
-
-
-
-
-
- Cancel
-
-
-
diff --git a/src/images/ic_add_24px.svg b/src/images/ic_add_24px.svg
deleted file mode 100644
index 58f73e9..0000000
--- a/src/images/ic_add_24px.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_attach_money_18px.svg b/src/images/ic_attach_money_18px.svg
deleted file mode 100644
index 4f80545..0000000
--- a/src/images/ic_attach_money_18px.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_clear_18px.svg b/src/images/ic_clear_18px.svg
deleted file mode 100644
index dc4aaa9..0000000
--- a/src/images/ic_clear_18px.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_clear_white_24px.svg b/src/images/ic_clear_white_24px.svg
deleted file mode 100644
index 5704800..0000000
--- a/src/images/ic_clear_white_24px.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_cloud_download_18px.svg b/src/images/ic_cloud_download_18px.svg
deleted file mode 100644
index f84a4ca..0000000
--- a/src/images/ic_cloud_download_18px.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_cloud_upload_24px.svg b/src/images/ic_cloud_upload_24px.svg
deleted file mode 100644
index 69a9622..0000000
--- a/src/images/ic_cloud_upload_24px.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_code_18px.svg b/src/images/ic_code_18px.svg
deleted file mode 100644
index c8ee47f..0000000
--- a/src/images/ic_code_18px.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/images/ic_help_18px.svg b/src/images/ic_help_18px.svg
deleted file mode 100644
index f099fd2..0000000
--- a/src/images/ic_help_18px.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/src/manifest.json b/src/manifest.json
index 60cc82d..aa7fc4d 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,27 +1,29 @@
-{
- "name": "ModHeader",
- "version": "2.3.2",
- "manifest_version": 2,
- "description": "Modify request and response headers",
- "background": {
- "scripts": [ "background.js" ]
- },
- "browser_action": {
- "default_icon": "icon.png",
- "default_title": "Modify Headers",
- "default_popup": "popup.html"
- },
- "icons": {
- "16": "icon_16.png",
- "48": "icon_48.png",
- "128": "icon_128.png"
- },
- "incognito": "spanning",
- "permissions": [
- "contextMenus",
- "storage",
- "webRequest",
- "webRequestBlocking",
- ""
- ]
-}
\ No newline at end of file
+{
+ "name": "ModHeader",
+ "version": "3.0.0",
+ "manifest_version": 3,
+ "description": "Modify request and response headers",
+ "background": {
+ "service_worker": "background.js"
+ },
+ "action": {
+ "default_icon": "icon.png",
+ "default_title": "Modify Headers",
+ "default_popup": "popup.html"
+ },
+ "icons": {
+ "16": "icon_16.png",
+ "48": "icon_48.png",
+ "128": "icon_128.png"
+ },
+ "incognito": "spanning",
+ "permissions": [
+ "contextMenus",
+ "storage",
+ "tabs",
+ "declarativeNetRequest"
+ ],
+ "host_permissions": [
+ ""
+ ]
+}
diff --git a/src/popup.html b/src/popup.html
index abaae99..262652c 100644
--- a/src/popup.html
+++ b/src/popup.html
@@ -7,10 +7,9 @@
-
-
+