迁移到 Manifest V3 并精简项目
- manifest.json 升级到 MV3:Service Worker + action,权限改用 declarativeNetRequest - background.js 重写为 Service Worker,用 DNR 动态/session 规则修改请求/响应头, 支持 URL 正则、资源类型过滤、Tab 锁、暂停;badge/右键菜单改用 chrome.action/onClicked - main.js 存储层从 localStorage 迁移到 chrome.storage.local(Proxy 同步适配 + 手动 bootstrap),并对旧 MV2 数据做一次性自动迁移;改动即时持久化 - popup.html 去掉 404 的 associate.js 与自动 ng-app;移除侧边栏无用入口 - 清理死代码与冗余资源:openCloudBackup、cloudbackupdialog 模板、推广 tips、 _DS_Store、8 个未引用图标 - README 用中文重写,说明项目来源、MV3 改造与使用方法
@@ -1,59 +1,79 @@
|
||||
# ModHeader browser extension
|
||||
# ModHeader(Manifest V3 版)
|
||||
|
||||
<h3 align="center">
|
||||
ModHeader is a browser extension that allow you to modify HTTP request and response headers.
|
||||
</h3>
|
||||
<h3 align="center">
|
||||
<img src="https://mod-header.appspot.com/android-icon-96x96.png" width="24px" alt="ModHeader" />
|
||||
<a href="https://mod-header.appspot.com/">
|
||||
Official Website
|
||||
</a>
|
||||
</h3>
|
||||
<p align="center">
|
||||
<a href="https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj">
|
||||
<img src="https://mod-header.appspot.com/images/chrome_1x.png" srcset="https://mod-header.appspot.com/images/chrome_2x.png 2x">
|
||||
</a>
|
||||
<a href="https://addons.mozilla.org/firefox/addon/modheader-firefox/">
|
||||
<img src="https://mod-header.appspot.com/images/firefox_1x.png" srcset="https://mod-header.appspot.com/images/firefox_2x.png 2x">
|
||||
</a>
|
||||
</p>
|
||||
一个用于**修改 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)
|
||||
<a href="https://www.buymeacoffee.com/scCieVdfj">
|
||||
<img src="https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png" alt="Buy Me A Coffee">
|
||||
</a>
|
||||
本仓库在其早期源码基础上做了以下改造:
|
||||
|
||||
## 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
|
||||
## 功能
|
||||
|
||||
<img src="https://mod-header.appspot.com/images/ss1.png">
|
||||
- 增加 / 修改 / 删除**请求头**与**响应头**
|
||||
- 按 **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** 许可。
|
||||
|
||||
@@ -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: ["<all_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: ["<all_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();
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<md-dialog flex="70">
|
||||
<md-toolbar>
|
||||
<div class="md-toolbar-tools">
|
||||
<h2>Cloud backup</h2>
|
||||
</div>
|
||||
</md-toolbar>
|
||||
<md-dialog-content>
|
||||
<div layout="row" flex
|
||||
ng-repeat="data in savedData | orderBy:'-timeInMs'">
|
||||
<md-button ng-click="selectProfiles(data.profiles)" class="md-primary">
|
||||
Backup at {{data.timeInMs | date:'short'}} -
|
||||
<span ng-repeat="profile in data.profiles">
|
||||
{{profile.title}}{{$last ? '' : ','}}
|
||||
</span>
|
||||
</md-button>
|
||||
</div>
|
||||
</md-dialog-content>
|
||||
<div class="md-actions" layout="row">
|
||||
<md-button ng-click="closeDialog()" class="md-primary">
|
||||
Cancel
|
||||
</md-button>
|
||||
</div>
|
||||
</md-dialog>
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 136 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M9.56 8.1c-1.6-.51-2.66-.71-2.66-1.88 0-.83.72-1.62 2.1-1.62 1.59 0 2.1.88 2.1 1.94H13c0-1.79-1.17-3.09-3-3.44V1H8v2.11c-1.58.32-3 1.37-3 3.12 0 2.25 1.78 2.8 4 3.52 1.88.61 2.25 1.04 2.25 2.09 0 .9-.67 1.56-2.25 1.56-1.2 0-2.25-.84-2.25-2.06h-2c0 1.88 1.38 3.2 3.25 3.56V17h2v-2.07c2.04-.29 3.2-1.49 3.2-3.1 0-1.87-.94-2.87-3.64-3.73z"/></svg>
|
||||
|
Before Width: | Height: | Size: 436 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="#ffffff" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
|
Before Width: | Height: | Size: 217 B |
@@ -1,4 +0,0 @@
|
||||
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 265 B |
@@ -1,4 +0,0 @@
|
||||
<svg height="18" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 327 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 281 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 230 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 418 B |
@@ -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",
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
{
|
||||
"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": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@
|
||||
<script src="scripts/angular-animate.min.js"></script>
|
||||
<script src="scripts/angular-aria.min.js"></script>
|
||||
<script src="scripts/angular-material.min.js"></script>
|
||||
<script src="scripts/associate.js"></script>
|
||||
<script src="scripts/main.js"></script>
|
||||
</head>
|
||||
<body ng-csp ng-app="modheader-popup" ng-controller="AppController">
|
||||
<body ng-csp ng-controller="AppController">
|
||||
<md-toolbar>
|
||||
<div class="md-toolbar-tools">
|
||||
<md-button class="md-icon-button" aria-label="Settings" md-no-ink
|
||||
@@ -157,23 +156,6 @@
|
||||
<md-icon md-svg-src="images/ic_add_18px.svg"></md-icon>
|
||||
<p>Add Profile</p>
|
||||
</md-list-item>
|
||||
<md-list-item ng-click="profileService.openCloudBackup($event)">
|
||||
<md-icon md-svg-src="images/ic_cloud_download_18px.svg"></md-icon>
|
||||
<p>Cloud backup</p>
|
||||
</md-list-item>
|
||||
<md-divider></md-divider>
|
||||
<md-list-item ng-click="openLink('https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=3XFKZ8PCRB8P6¤cy_code=USD&amount=5&source=url')">
|
||||
<md-icon md-svg-src="images/ic_attach_money_18px.svg"></md-icon>
|
||||
<p>Donate</p>
|
||||
</md-list-item>
|
||||
<md-list-item ng-click="openLink('https://github.com/hao1300/modheader')">
|
||||
<md-icon md-svg-src="images/ic_code_18px.svg"></md-icon>
|
||||
<p>Source code</p>
|
||||
</md-list-item>
|
||||
<md-list-item ng-click="openLink('https://mod-header.appspot.com/help.html')">
|
||||
<md-icon md-svg-src="images/ic_help_24px.svg"></md-icon>
|
||||
<p>Help</p>
|
||||
</md-list-item>
|
||||
</md-list>
|
||||
</md-sidenav>
|
||||
<md-content class="main-content" ng-class="{disabled: dataSource.isPaused}">
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
const SPECIAL_CHARS = '^$&+?.()|{}[]/'.split('');
|
||||
const browser = chrome;
|
||||
|
||||
// Synchronous, in-memory mirror of chrome.storage.local. It is preloaded
|
||||
// before AngularJS bootstraps (see the bottom of this file) so that the
|
||||
// existing synchronous `localStorage`-style access keeps working, while every
|
||||
// write is mirrored back to chrome.storage.local (the source of truth in MV3).
|
||||
const _storeCache = {};
|
||||
const store = new Proxy(_storeCache, {
|
||||
get(target, prop) {
|
||||
if (prop === 'getItem') {
|
||||
return (key) => target[key];
|
||||
}
|
||||
if (prop === 'setItem') {
|
||||
return (key, value) => {
|
||||
target[key] = String(value);
|
||||
chrome.storage.local.set({ [key]: String(value) });
|
||||
};
|
||||
}
|
||||
if (prop === 'removeItem') {
|
||||
return (key) => {
|
||||
delete target[key];
|
||||
chrome.storage.local.remove(key);
|
||||
};
|
||||
}
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop, value) {
|
||||
target[prop] = String(value);
|
||||
chrome.storage.local.set({ [prop]: String(value) });
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const modHeader = angular.module('modheader-popup', ['ngMaterial']);
|
||||
modHeader.config(['$compileProvider', function ($compileProvider) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
@@ -43,9 +75,9 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
|
||||
dataSource.addFilter = function(filters) {
|
||||
let urlRegex = '';
|
||||
if (localStorage.currentTabUrl) {
|
||||
if (store.currentTabUrl) {
|
||||
const parser = document.createElement('a');
|
||||
parser.href = localStorage.currentTabUrl;
|
||||
parser.href = store.currentTabUrl;
|
||||
urlRegex = parser.origin + '/.*';
|
||||
}
|
||||
filters.push({
|
||||
@@ -81,7 +113,7 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
|
||||
dataSource.pause = function() {
|
||||
dataSource.isPaused = true;
|
||||
localStorage.isPaused = true;
|
||||
store.isPaused = true;
|
||||
$mdToast.show(
|
||||
$mdToast.simple()
|
||||
.content('ModHeader paused')
|
||||
@@ -92,7 +124,7 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
|
||||
dataSource.play = function() {
|
||||
dataSource.isPaused = false;
|
||||
localStorage.removeItem('isPaused');
|
||||
store.removeItem('isPaused');
|
||||
$mdToast.show(
|
||||
$mdToast.simple()
|
||||
.content('ModHeader unpaused')
|
||||
@@ -102,8 +134,8 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
};
|
||||
|
||||
dataSource.lockToTab = function() {
|
||||
dataSource.lockedTabId = localStorage.activeTabId;
|
||||
localStorage.lockedTabId = dataSource.lockedTabId;
|
||||
dataSource.lockedTabId = store.activeTabId;
|
||||
store.lockedTabId = dataSource.lockedTabId;
|
||||
$mdToast.show(
|
||||
$mdToast.simple()
|
||||
.content('Restricted ModHeader to the current tab')
|
||||
@@ -114,7 +146,7 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
|
||||
dataSource.unlockAllTab = function() {
|
||||
dataSource.lockedTabId = null;
|
||||
localStorage.removeItem('lockedTabId');
|
||||
store.removeItem('lockedTabId');
|
||||
$mdToast.show(
|
||||
$mdToast.simple()
|
||||
.content('Applying ModHeader to all tabs')
|
||||
@@ -152,8 +184,8 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
dataSource.predicate = '';
|
||||
dataSource.reverse = false;
|
||||
|
||||
if (localStorage.profiles) {
|
||||
dataSource.profiles = angular.fromJson(localStorage.profiles);
|
||||
if (store.profiles) {
|
||||
dataSource.profiles = angular.fromJson(store.profiles);
|
||||
for (let profile of dataSource.profiles) {
|
||||
fixProfile(profile);
|
||||
}
|
||||
@@ -182,23 +214,23 @@ modHeader.factory('dataSource', function($mdToast) {
|
||||
profile.appendMode = '';
|
||||
}
|
||||
});
|
||||
if (localStorage.selectedProfile) {
|
||||
dataSource.selectedProfile = dataSource.profiles[Number(localStorage.selectedProfile)];
|
||||
if (store.selectedProfile) {
|
||||
dataSource.selectedProfile = dataSource.profiles[Number(store.selectedProfile)];
|
||||
}
|
||||
if (!dataSource.selectedProfile) {
|
||||
dataSource.selectedProfile = dataSource.profiles[0];
|
||||
}
|
||||
if (localStorage.isPaused) {
|
||||
dataSource.isPaused = localStorage.isPaused;
|
||||
if (store.isPaused) {
|
||||
dataSource.isPaused = store.isPaused;
|
||||
}
|
||||
if (localStorage.lockedTabId) {
|
||||
dataSource.lockedTabId = localStorage.lockedTabId;
|
||||
if (store.lockedTabId) {
|
||||
dataSource.lockedTabId = store.lockedTabId;
|
||||
}
|
||||
dataSource.save = function() {
|
||||
var serializedProfiles = angular.toJson(dataSource.profiles);
|
||||
var selectedProfileIndex = dataSource.profiles.indexOf(dataSource.selectedProfile);
|
||||
localStorage.profiles = serializedProfiles;
|
||||
localStorage.selectedProfile = selectedProfileIndex;
|
||||
store.profiles = serializedProfiles;
|
||||
store.selectedProfile = selectedProfileIndex;
|
||||
};
|
||||
return dataSource;
|
||||
});
|
||||
@@ -338,71 +370,6 @@ modHeader.factory('profileService', function(
|
||||
}
|
||||
};
|
||||
|
||||
profileService.openCloudBackup = (event) => {
|
||||
const parentEl = angular.element(document.body);
|
||||
$mdDialog.show({
|
||||
parent: parentEl,
|
||||
targetEvent: event,
|
||||
focusOnOpen: false,
|
||||
templateUrl: 'cloudbackupdialog.tmpl.html',
|
||||
controller: DialogController_
|
||||
}).then((profiles) => {
|
||||
if (!profiles) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dataSource.profiles = profiles;
|
||||
dataSource.selectedProfile = dataSource.profiles[0];
|
||||
dataSource.save();
|
||||
|
||||
$mdToast.show(
|
||||
$mdToast.simple()
|
||||
.content('Profiles successfully import')
|
||||
.position('top')
|
||||
.hideDelay(1000)
|
||||
);
|
||||
} catch (e) {
|
||||
$mdToast.show(
|
||||
$mdToast.simple()
|
||||
.content('Failed to import profiles')
|
||||
.position('top')
|
||||
.hideDelay(1000)
|
||||
);
|
||||
}
|
||||
});
|
||||
function DialogController_($scope, $mdDialog) {
|
||||
browser.storage.sync.get(null, (items) => {
|
||||
let savedData = [];
|
||||
if (!items) {
|
||||
items = [];
|
||||
}
|
||||
for (const key in items) {
|
||||
try {
|
||||
const serializedProfiles = items[key];
|
||||
const profiles = angular.fromJson(serializedProfiles);
|
||||
for (let profile of profiles) {
|
||||
fixProfile(profile);
|
||||
}
|
||||
savedData.push({
|
||||
'timeInMs': key,
|
||||
'profiles': profiles,
|
||||
});
|
||||
} catch(e) {
|
||||
// skip invalid profile.
|
||||
}
|
||||
}
|
||||
$scope.savedData = savedData;
|
||||
});
|
||||
|
||||
$scope.selectProfiles = function(profiles) {
|
||||
$mdDialog.hide(profiles);
|
||||
};
|
||||
|
||||
$scope.closeDialog = function() {
|
||||
$mdDialog.hide();
|
||||
};
|
||||
}
|
||||
};
|
||||
return profileService;
|
||||
});
|
||||
|
||||
@@ -558,6 +525,15 @@ modHeader.controller('AppController', function(
|
||||
$scope.dataSource = dataSource;
|
||||
$scope.profileService = profileService;
|
||||
|
||||
// chrome.storage.local writes are async and may not flush during the popup's
|
||||
// unload, so persist eagerly on every change instead of relying on onunload.
|
||||
$scope.$watch('dataSource.profiles', function() {
|
||||
dataSource.save();
|
||||
}, true);
|
||||
$scope.$watch('dataSource.selectedProfile', function() {
|
||||
dataSource.save();
|
||||
});
|
||||
|
||||
const tips = [
|
||||
{text: 'Tip: You can switch between multiple profile'},
|
||||
{text: 'Tip: You can export your profile to share with others'},
|
||||
@@ -569,19 +545,6 @@ modHeader.controller('AppController', function(
|
||||
{text: 'Tip: Go to profile setting to toggle comment column'},
|
||||
{text: 'Tip: Append header value to existing one in profile setting'},
|
||||
{text: 'Tip: Pause button will temporarily pause all modifications'},
|
||||
{text: 'Tip: Go to cloud backup to retrieve your auto-synced profile'},
|
||||
{
|
||||
text: 'If you like ModHeader, please consider donating',
|
||||
buttonText: 'Donate',
|
||||
url: 'https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=3XFKZ8PCRB8P6¤cy_code=USD&amount=5&source=url'
|
||||
},
|
||||
{
|
||||
text: 'Enjoying ModHeader, leave us a review',
|
||||
buttonText: 'Review',
|
||||
url: navigator.userAgent.indexOf('Firefox') >= 0
|
||||
? 'https://addons.mozilla.org/firefox/addon/modheader-firefox/'
|
||||
: 'https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj'
|
||||
},
|
||||
];
|
||||
const tip = tips[Math.floor(Math.random() * tips.length)];
|
||||
$mdToast.show({
|
||||
@@ -602,3 +565,21 @@ modHeader.controller('ToastCtrl', function($mdToast, $mdDialog, $document, $scop
|
||||
browser.tabs.create({url: url});
|
||||
};
|
||||
});
|
||||
|
||||
// Preload chrome.storage.local into the synchronous cache, migrate any legacy
|
||||
// MV2 localStorage data on first run, then bootstrap AngularJS manually.
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
let items = await chrome.storage.local.get(null);
|
||||
if ((!items || !items.profiles)
|
||||
&& window.localStorage && window.localStorage.getItem('profiles')) {
|
||||
const legacy = {};
|
||||
for (let i = 0; i < window.localStorage.length; ++i) {
|
||||
const key = window.localStorage.key(i);
|
||||
legacy[key] = window.localStorage.getItem(key);
|
||||
}
|
||||
await chrome.storage.local.set(legacy);
|
||||
items = Object.assign({}, items, legacy);
|
||||
}
|
||||
Object.assign(_storeCache, items || {});
|
||||
angular.bootstrap(document.body, ['modheader-popup']);
|
||||
});
|
||||
|
||||