├── public
├── images
│ └── auto-group-tabs.png
├── index.html
├── background
│ ├── configuration.js
│ ├── strategy.js
│ ├── background.js
│ └── utils.js
└── manifest.json
├── src
├── options
│ ├── OptionsPage.css
│ └── OptionsPage.js
├── popup
│ ├── PopupPage.css
│ └── PopupPage.js
├── i18n
│ ├── zh_CN.js
│ └── en_US.js
└── index.js
├── .gitignore
├── .eslintrc.json
├── README.md
├── LICENSE
└── package.json
/public/images/auto-group-tabs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marwincn/auto-group-tabs/HEAD/public/images/auto-group-tabs.png
--------------------------------------------------------------------------------
/src/options/OptionsPage.css:
--------------------------------------------------------------------------------
1 | .configPage {
2 | margin: 0 auto;
3 | max-width: 700px;
4 | overflow: auto;
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/src/popup/PopupPage.css:
--------------------------------------------------------------------------------
1 | .ant-form-vertical .ant-form-item-label {
2 | padding: 2px;
3 | }
4 |
5 | .ant-form-item {
6 | margin-bottom: 16px;
7 | }
8 |
9 | .mainPanel {
10 | width: 280px;
11 | height: 380px;
12 | overflow: auto;
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | build.zip
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:react/recommended"
9 | ],
10 | "parserOptions": {
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "ecmaVersion": 12,
15 | "sourceType": "module"
16 | },
17 | "plugins": [
18 | "react"
19 | ],
20 | "globals": {
21 | "chrome": true
22 | },
23 | "parser": "babel-eslint"
24 | }
25 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Auto Group Tabs
2 |
3 | Chrome浏览器在85版本发布了标签页分组功能,但是需要手动对标签页的分组进行管理。`Auto Group Tabs`是一款浏览器扩展,实现*新打开标签页*或*标签页地址切换*时自动进行分组,用户可以自定义分组的规则,如按照域名分组或按照标签页标题关键词分组。
4 |
5 | 可以在Chrome网上应用商店安装此扩展:[link](https://chrome.google.com/webstore/detail/auto-group-tabs/mnolhkkapjcaekdgopmfolekecfhgoob)
6 |
7 | 
8 |
9 | 
10 |
11 | # 自行构建程序
12 |
13 | 环境要求:
14 | * Mac OS 或 Linux 操作系统
15 | * 安装npm
16 |
17 | > 目前build脚本不支持在Windows环境上运行,如果你有想法欢迎提PR
18 |
19 |
20 | 下载依赖:
21 | ```shell
22 | npm install
23 | ```
24 |
25 |
26 | 构建:
27 | ```shell
28 | npm run build
29 | ```
30 |
31 |
32 | 构建完成后根目录下会出现`build/`目录,在浏览器扩展页面打开`开发者模式`后,点击`加载已解压的扩展程序`,选择刚才生成的build目录即可。
33 |
34 | 
--------------------------------------------------------------------------------
/src/i18n/zh_CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | group_all_tabs: "一键分组所有Tabs (Alt+G)",
3 | enable_auto_group: "是否开启自动分组:",
4 | min_number: "每个分组最小Tab数量:",
5 | group_strategy: "分组策略:",
6 | domain: "域名",
7 | domain_tip: "从 https://www.google.com/ 中匹配 'www.google.com'",
8 | sld: "二级域名",
9 | sld_tip: "从 https://www.google.com/ 中匹配 'google'",
10 | configuration: "自定义规则",
11 | go_to_config: "前往配置",
12 | config_page_title: "分组规则",
13 | save: "保存",
14 | edit: "编辑",
15 | config_title_fallback: "未匹配到自定义规则时:",
16 | config_title_custom_rule: "自定义分组规则:",
17 | option_none: "不分组",
18 | option_domain: "按域名分组",
19 | option_sld: "按二级域名分组",
20 | group_name: "分组名",
21 | patterns: "匹配规则",
22 | add_pattern: "添加匹配规则",
23 | add_rule: "添加自定义规则",
24 | group_name_validate_message: "分组名不能为空",
25 | pattern_validate_message: "匹配规则不能为空",
26 | tooltip_of_pattern: "匹配浏览器Tab页的域名,支持使用 * 进行模糊匹配",
27 | };
28 |
--------------------------------------------------------------------------------
/public/background/configuration.js:
--------------------------------------------------------------------------------
1 | import { getDomain } from "./utils.js";
2 |
3 | export const defaultConfiguration = {
4 | fallback: "none",
5 | rules: [],
6 | };
7 |
8 | export function getGroupKeyByConfig(url, configuration) {
9 | console.log(configuration.rules);
10 | for (let rule of configuration.rules) {
11 | for (let obj of rule.patterns) {
12 | if (obj.pattern) {
13 | if (isExpressionMatched(getDomain(url), obj.pattern)) {
14 | return rule.name;
15 | }
16 | }
17 | }
18 | }
19 | return null;
20 | }
21 |
22 | export function getGroupTitleByConfig(url, configuration) {
23 | return getGroupKeyByConfig(url, configuration);
24 | }
25 |
26 | function isExpressionMatched(url, expression) {
27 | // 转换表达式中的 * 为正则表达式中的 .*,并转义其他正则元字符
28 | const regexPattern = expression.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
29 | // 构建正则表达式
30 | const regex = new RegExp(`^${regexPattern}$`);
31 | // 检查域名是否匹配表达式
32 | return regex.test(url);
33 | }
34 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Auto Group Tabs",
3 | "description": "",
4 | "version": "1.1.9",
5 | "manifest_version": 3,
6 | "background": {
7 | "service_worker": "background/background.js",
8 | "type": "module"
9 | },
10 | "permissions": ["storage", "tabs", "tabGroups"],
11 | "action": {
12 | "default_popup": "index.html",
13 | "default_icon": {
14 | "16": "/images/auto-group-tabs.png",
15 | "32": "/images/auto-group-tabs.png",
16 | "48": "/images/auto-group-tabs.png",
17 | "128": "/images/auto-group-tabs.png"
18 | }
19 | },
20 | "options_page": "index.html#options",
21 | "icons": {
22 | "16": "/images/auto-group-tabs.png",
23 | "32": "/images/auto-group-tabs.png",
24 | "48": "/images/auto-group-tabs.png",
25 | "128": "/images/auto-group-tabs.png"
26 | },
27 | "commands": {
28 | "group_right_now": {
29 | "suggested_key": {
30 | "default": "Alt+G"
31 | },
32 | "description": "Group all tabs right now"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | HashRouter,
4 | Routes,
5 | Route
6 | } from "react-router-dom";
7 | import ReactDOM from "react-dom";
8 | import { IntlProvider } from "react-intl";
9 | import en_US from "./i18n/en_US";
10 | import zh_CN from "./i18n/zh_CN";
11 | import PopupPage from "./popup/PopupPage";
12 | import OptionsPage from "./options/OptionsPage";
13 |
14 | const locale = chrome.i18n.getUILanguage();
15 | const messages = {
16 | "zh-CN": zh_CN,
17 | "en-US": en_US,
18 | };
19 |
20 | // eslint-disable-next-line react/no-deprecated
21 | ReactDOM.render(
22 |
23 |
27 |
28 |
29 | } />
30 | } />
31 |
32 |
33 |
34 | ,
35 | document.getElementById("root")
36 | );
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 marwin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/i18n/en_US.js:
--------------------------------------------------------------------------------
1 | export default {
2 | group_all_tabs: "Group all tabs (Alt + G)",
3 | enable_auto_group: "Enable auto group tabs:",
4 | min_number: "Min-number of tabs per group:",
5 | group_strategy: "Group strategy:",
6 | domain: "Domain",
7 | domain_tip: "Full domain name. Match 'www.google.com' in https://www.google.com/",
8 | sld: "SLD",
9 | sld_tip: "Second Level Domain. Match 'google' in https://www.google.com/",
10 | configuration: "Custom",
11 | go_to_config: "Go to Configuration",
12 | config_page_title: "Grouping Rules",
13 | save: "Save",
14 | edit: "Edit",
15 | config_title_fallback: "When No Custom Rule Matches:",
16 | config_title_custom_rule: "Custom Grouping Rules:",
17 | option_none: "No Grouping",
18 | option_domain: "Group by Domain",
19 | option_sld: "Group by Second-Level Domain",
20 | group_name: "Group Name",
21 | patterns: "Matching Rules",
22 | add_pattern: "Add Matching Rule",
23 | add_rule: "Add Custom Rule",
24 | group_name_validate_message: "Group name can not be empty",
25 | pattern_validate_message: "Matching rule can not be empty",
26 | tooltip_of_pattern: "Match browser tab page domain names, supporting wildcard matching using *",
27 | };
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auto-group-tabs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^11.2.7",
8 | "@testing-library/user-event": "^12.8.3",
9 | "antd": "^5.16.0",
10 | "react": "^17.0.2",
11 | "react-dom": "^17.0.2",
12 | "react-intl": "^5.21.0",
13 | "react-router-dom": "^6.22.3",
14 | "react-scripts": "5.0.0",
15 | "web-vitals": "^1.1.2"
16 | },
17 | "scripts": {
18 | "test": "react-scripts test",
19 | "start": "react-scripts --openssl-legacy-provider start",
20 | "build": "INLINE_RUNTIME_CHUNK=false react-scripts --openssl-legacy-provider build"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ],
27 | "globals": {
28 | "chrome": true
29 | }
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "@types/chrome": "^0.0.188",
45 | "babel-eslint": "^10.1.0",
46 | "eslint": "^7.32.0",
47 | "eslint-plugin-react": "^7.26.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/public/background/strategy.js:
--------------------------------------------------------------------------------
1 | import { getGroupKeyByConfig, getGroupTitleByConfig } from "./configuration.js";
2 | import { getDomain, getSecDomain } from "./utils.js";
3 |
4 | // 不做分组的策略
5 | export const noGroupStrategy = {
6 | shloudGroup: () => {
7 | return false;
8 | },
9 | getGroupKey: () => {
10 | return null;
11 | },
12 | getGroupTitle: () => {
13 | return null;
14 | },
15 | querySameTabs: async () => {
16 | return [];
17 | },
18 | };
19 |
20 | // 根据域名分组的策略
21 | export const domainStrategy = {
22 | shloudGroup: (changeInfo, tab) => {
23 | return changeInfo.url && tab.url.match(/^https?:\/\/[^/]+\/.*/);
24 | },
25 | getGroupKey: (tab) => {
26 | return getDomain(tab.url);
27 | },
28 | getGroupTitle: (tab) => {
29 | return getDomain(tab.url);
30 | },
31 | querySameTabs: async (tab) => {
32 | const domain = getDomain(tab.url);
33 | let tabs;
34 | await chrome.tabs
35 | .query({
36 | windowId: chrome.windows.WINDOW_ID_CURRENT,
37 | pinned: false,
38 | })
39 | .then((allTabs) => {
40 | tabs = allTabs.filter((t) => t.url && domain === getDomain(t.url));
41 | });
42 | return tabs;
43 | },
44 | };
45 |
46 | // 根据二级域名分组的策略
47 | export const secDomainStrategy = {
48 | shloudGroup: (changeInfo, tab) => {
49 | return changeInfo.url && tab.url.match(/^https?:\/\/[^/]+\/.*/);
50 | },
51 | getGroupKey: (tab) => {
52 | return getSecDomain(tab.url);
53 | },
54 | getGroupTitle: (tab) => {
55 | return getSecDomain(tab.url);
56 | },
57 | querySameTabs: async (tab) => {
58 | const domain = getSecDomain(tab.url);
59 | let tabs;
60 | await chrome.tabs
61 | .query({
62 | windowId: chrome.windows.WINDOW_ID_CURRENT,
63 | pinned: false,
64 | })
65 | .then((allTabs) => {
66 | tabs = allTabs.filter((t) => t.url && domain === getSecDomain(t.url));
67 | });
68 | return tabs;
69 | },
70 | };
71 |
72 | // 根据配置文件分组的策略
73 | export const configStrategy = {
74 | shloudGroup: (changeInfo, tab) => {
75 | return changeInfo.url && tab.url.match(/^https?:\/\/[^/]+\/.*/);
76 | },
77 | getGroupKey: (tab, userConfig) => {
78 | const result = getGroupKeyByConfig(tab.url, userConfig.configuration);
79 | return result
80 | ? result
81 | : getFallbackStattegy(userConfig.configuration.fallback).getGroupKey(tab);
82 | },
83 | getGroupTitle: (tab, userConfig) => {
84 | const result = getGroupTitleByConfig(tab.url, userConfig.configuration);
85 | return result
86 | ? result
87 | : getFallbackStattegy(userConfig.configuration.fallback).getGroupTitle(
88 | tab
89 | );
90 | },
91 | querySameTabs: async (tab, userConfig) => {
92 | const domain = configStrategy.getGroupTitle(tab, userConfig);
93 | let tabs;
94 | await chrome.tabs
95 | .query({
96 | windowId: chrome.windows.WINDOW_ID_CURRENT,
97 | pinned: false,
98 | })
99 | .then((allTabs) => {
100 | tabs = allTabs.filter(
101 | (t) =>
102 | t.url &&
103 | domain === configStrategy.getGroupTitle(t, userConfig)
104 | );
105 | });
106 | return tabs;
107 | },
108 | };
109 |
110 | function getFallbackStattegy(fallback) {
111 | switch (fallback) {
112 | case 'none':
113 | return noGroupStrategy;
114 | case 'domain':
115 | return domainStrategy;
116 | case 'sld':
117 | return secDomainStrategy;
118 | default:
119 | return noGroupStrategy;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/popup/PopupPage.js:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Divider,
4 | Form,
5 | InputNumber,
6 | Radio,
7 | Switch,
8 | Alert,
9 | } from "antd";
10 | import React from "react";
11 | import "./PopupPage.css";
12 | import { injectIntl } from "react-intl";
13 | import PropTypes from "prop-types";
14 |
15 | class PopupPage extends React.Component {
16 | static propTypes = {
17 | intl: PropTypes.object,
18 | };
19 |
20 | constructor(props) {
21 | super(props);
22 | this.i18n = (key) => props.intl.formatMessage({ id: key });
23 | this.state = {
24 | enableAutoGroup: true,
25 | groupStrategy: 2,
26 | groupTabNum: 1,
27 | };
28 | }
29 |
30 | componentDidMount() {
31 | return chrome.storage.sync.get(Object.keys(this.state), (config) => {
32 | this.setState(config);
33 | });
34 | }
35 |
36 | onManuallyUpdateClick = () => {
37 | chrome.runtime.sendMessage({ groupRightNow: true });
38 | };
39 |
40 | onEnableAutoGroupChange = (value) => {
41 | const newState = { enableAutoGroup: value };
42 | this.setState(newState);
43 | chrome.storage.sync.set(newState);
44 | };
45 |
46 | onGroupTabNumChange = (value) => {
47 | const newState = { groupTabNum: value };
48 | this.setState(newState);
49 | chrome.storage.sync.set(newState);
50 | };
51 |
52 | onGroupStrategyChange = (e) => {
53 | const newState = { groupStrategy: e.target.value };
54 | this.setState(newState);
55 | chrome.storage.sync.set(newState);
56 | };
57 |
58 | render() {
59 | const groupStrategyOptions = [
60 | { label: this.i18n("domain"), value: 1 },
61 | { label: this.i18n("sld"), value: 2 },
62 | { label: this.i18n("configuration"), value: 3 },
63 | ];
64 |
65 | return (
66 |
67 |
69 |
76 |
77 |
78 |
79 |
83 |
84 |
85 |
90 |
91 |
92 |
98 |
99 | {this.state.groupStrategy === 1 && (
100 |
101 | )}
102 | {this.state.groupStrategy === 2 && (
103 |
104 | )}
105 | {this.state.groupStrategy === 3 && (
106 |
107 | )}
108 |
109 |
110 | );
111 | }
112 | }
113 |
114 | export default injectIntl(PopupPage);
115 |
--------------------------------------------------------------------------------
/public/background/background.js:
--------------------------------------------------------------------------------
1 | import { defaultConfiguration } from "./configuration.js";
2 | import { configStrategy, domainStrategy, secDomainStrategy } from "./strategy.js";
3 |
4 | // 默认配置
5 | const DEFAULT_CONFIG = {
6 | enableAutoGroup: true, // 是否启动自动分组
7 | groupTabNum: 1, // 满足多少个tab时才进行分组
8 | groupStrategy: 2, // 分组策略
9 | configuration: defaultConfiguration // 配置文件内容
10 | };
11 | // 全局的用户配置
12 | let userConfig = DEFAULT_CONFIG;
13 |
14 | // 定义分组策略
15 | const GROUP_STRATEGY_MAP = new Map();
16 | GROUP_STRATEGY_MAP.set(1, domainStrategy);
17 | GROUP_STRATEGY_MAP.set(2, secDomainStrategy);
18 | GROUP_STRATEGY_MAP.set(3, configStrategy);
19 |
20 | // 监听tab变更事件
21 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
22 | chrome.storage.sync.get(Object.keys(DEFAULT_CONFIG), (config) => {
23 | userConfig = { ...DEFAULT_CONFIG, ...config };
24 | // 判断是否开启自动分组
25 | if (!userConfig.enableAutoGroup) {
26 | return;
27 | }
28 |
29 | // 如果不是http协议则ungroup掉
30 | if (!(tab.url.startsWith("http") || tab.url.startsWith("https"))) {
31 | // 如果用户手动拖拽tab到group中则ungroup会抛异常
32 | try {
33 | chrome.tabs.ungroup([tabId]);
34 | } catch (e) {
35 | console.error(e);
36 | }
37 | }
38 |
39 | const strategy = GROUP_STRATEGY_MAP.get(userConfig.groupStrategy);
40 | // 如果满足group的条件,进行group
41 | if (strategy.shloudGroup(changeInfo, tab)) {
42 | groupTabs(tab, strategy);
43 | }
44 |
45 | // 如果有tab从分组中移除,需要判断group的数量是否还满足数量,如果不满足ungroup
46 | if (changeInfo.groupId && changeInfo.groupId === -1) {
47 | strategy.querySameTabs(tab, userConfig).then((tabs) => {
48 | const tabIds = tabs.map((t) => t.id);
49 | // 如果tab数量不满足设置最小数量进行ungroup
50 | if (tabs.length > 0 && tabs.length < userConfig.groupTabNum) {
51 | chrome.tabs.ungroup(tabIds);
52 | }
53 | });
54 | }
55 | });
56 | });
57 |
58 | function groupTabs(tab, strategy) {
59 | strategy.querySameTabs(tab, userConfig).then((tabs) => {
60 | if (tabs.length === 0) {
61 | console.log("no same tab for:" + tab);
62 | return;
63 | }
64 |
65 | const tabIds = tabs.map((t) => t.id);
66 | // 如果tab数量不满足设置最小数量进行ungroup
67 | if (tabIds.length < userConfig.groupTabNum) {
68 | chrome.tabs.ungroup(tabIds);
69 | return;
70 | }
71 | // 查询分组,如果分组存在则加入分组,否则新建分组
72 | const groupTitle = strategy.getGroupTitle(tab, userConfig);
73 | if (groupTitle) {
74 | chrome.tabGroups
75 | .query({
76 | title: groupTitle,
77 | windowId: chrome.windows.WINDOW_ID_CURRENT,
78 | })
79 | .then((tabGroups) => {
80 | if (tabGroups && tabGroups.length > 0) {
81 | chrome.tabs.group({ tabIds, groupId: tabGroups[0].id });
82 | } else {
83 | chrome.tabs.group({ tabIds }).then((groupId) => {
84 | chrome.tabGroups.update(groupId, { title: groupTitle });
85 | });
86 | }
87 | });
88 | }
89 | });
90 | }
91 |
92 | // 监听一键分组点击事件
93 | chrome.runtime.onMessage.addListener((request) => {
94 | if (request.groupRightNow) {
95 | groupAllTabs();
96 | }
97 | });
98 |
99 | // 监听一键group快捷键
100 | chrome.commands.onCommand.addListener((command) => {
101 | switch (command) {
102 | case "group_right_now": {
103 | groupAllTabs();
104 | }
105 | }
106 | });
107 |
108 | function groupAllTabs() {
109 | chrome.storage.sync.get(Object.keys(DEFAULT_CONFIG), (config) => {
110 | userConfig = { ...DEFAULT_CONFIG, ...config };
111 | chrome.tabs
112 | .query({ windowId: chrome.windows.WINDOW_ID_CURRENT, pinned: false })
113 | .then((tabs) => {
114 | const strategy = GROUP_STRATEGY_MAP.get(userConfig.groupStrategy);
115 | // 按groupTitle分组,key为groupTitle,value为tabs
116 | let tabGroups = {};
117 | tabs.forEach((tab) => {
118 | const groupTitle = strategy.getGroupTitle(tab, userConfig);
119 | if (groupTitle) {
120 | if (!tabGroups[groupTitle]) {
121 | tabGroups[groupTitle] = [];
122 | }
123 | tabGroups[groupTitle].push(tab);
124 | }
125 | });
126 | // 调用chrome API 进行tabs分组
127 | for (const groupTitle in tabGroups) {
128 | const tabIds = tabGroups[groupTitle].map((tab) => tab.id);
129 | if (tabGroups[groupTitle].length >= userConfig.groupTabNum) {
130 | chrome.tabs.group({ tabIds }).then((groupId) => {
131 | chrome.tabGroups.update(groupId, { title: groupTitle });
132 | });
133 | } else {
134 | chrome.tabs.ungroup(tabIds);
135 | }
136 | }
137 | });
138 | });
139 | }
140 |
141 | // function mergeSameTabs() {
142 | // chrome.tabs
143 | // .query({ windowId: chrome.windows.WINDOW_ID_CURRENT })
144 | // .then((tabs) => {
145 | // let tabGroups = {};
146 | // tabs.forEach((tab) => {
147 | // let key = tab.url;
148 | // if (key) {
149 | // key = key.split("#")[0];
150 | // if (!tabGroups[key]) {
151 | // tabGroups[key] = [tab];
152 | // } else {
153 | // chrome.tabs.remove(tab.id);
154 | // }
155 | // }
156 | // });
157 | // });
158 | // }
159 |
--------------------------------------------------------------------------------
/public/background/utils.js:
--------------------------------------------------------------------------------
1 | export function getDomain(url) {
2 | const re = /^(?:[a-zA-Z-]+):\/\/([^/:]+)(:\d+)?\/.*/;
3 | const match = url.match(re);
4 | return match ? match[1] : null;
5 | }
6 |
7 | export function getSecDomain(url) {
8 | const domain = getDomain(url);
9 | if (!domain) return null;
10 | // localhost地址或IP
11 | if (domain === "localhost" || domain.match(/^\d+\.\d+\.\d+\.\d+$/)) {
12 | return domain;
13 | }
14 | // 匹配二级域名
15 | const match = domain.match(
16 | /([^.]+)\.(?:(?:(?:com|net|org|edu|gov|asn|id|info|conf|oz|act|nsw|nt|qld|sa|tas|vic|wa|act\.edu|nsw\.edu|nt\.edu|qld\.edu|sa\.edu|tas\.edu|vic\.edu|wa\.edu|qld\.gov|sa\.gov|tas\.gov|vic\.gov|wa\.gov|blogspot\.com)\.au)|(?:(?:adm|adv|agr|am|arq|art|ato|b|bio|blog|bmd|cim|cng|cnt|com|coop|ecn|eco|edu|emp|eng|esp|etc|eti|far|flog|fm|fnd|fot|fst|g12|ggf|gov|imb|ind|inf|jor|jus|leg|lel|mat|med|mil|mp|mus|net|[\w\u0430-\u044f]\+\*nom|not|ntr|odo|org|ppg|pro|psc|psi|qsl|radio|rec|slg|srv|taxi|teo|tmp|trd|tur|tv|vet|vlog|wiki|zlg|blogspot\.com)\.br)|(?:(?:ac|com|edu|gov|net|org|mil|ah|bj|cq|fj|gd|gs|gz|gx|ha|hb|he|hi|hl|hn|jl|js|jx|ln|nm|nx|qh|sc|sd|sh|sn|sx|tj|xj|xz|yn|zj|hk|mo|tw|)\.cn)|(?:(?:betainabox|ar|br|cn|de|eu|gb|hu|jpn|kr|mex|no|qc|ru|sa|se|uk|us|uy|za|africa|gr|co|cloudcontrolled|cloudcontrolapp|dreamhosters|dyndns-at-home|dyndns-at-work|dyndns-blog|dyndns-free|dyndns-home|dyndns-ip|dyndns-mail|dyndns-office|dyndns-pics|dyndns-remote|dyndns-server|dyndns-web|dyndns-wiki|dyndns-work|blogdns|cechire|dnsalias|dnsdojo|doesntexist|dontexist|doomdns|dyn-o-saur|dynalias|est-a-la-maison|est-a-la-masion|est-le-patron|est-mon-blogueur|from-ak|from-al|from-ar|from-ca|from-ct|from-dc|from-de|from-fl|from-ga|from-hi|from-ia|from-id|from-il|from-in|from-ks|from-ky|from-ma|from-md|from-mi|from-mn|from-mo|from-ms|from-mt|from-nc|from-nd|from-ne|from-nh|from-nj|from-nm|from-nv|from-oh|from-ok|from-or|from-pa|from-pr|from-ri|from-sc|from-sd|from-tn|from-tx|from-ut|from-va|from-vt|from-wa|from-wi|from-wv|from-wy|getmyip|gotdns|hobby-site|homelinux|homeunix|iamallama|is-a-anarchist|is-a-blogger|is-a-bookkeeper|is-a-bulls-fan|is-a-caterer|is-a-chef|is-a-conservative|is-a-cpa|is-a-cubicle-slave|is-a-democrat|is-a-designer|is-a-doctor|is-a-financialadvisor|is-a-geek|is-a-green|is-a-guru|is-a-hard-worker|is-a-hunter|is-a-landscaper|is-a-lawyer|is-a-liberal|is-a-libertarian|is-a-llama|is-a-musician|is-a-nascarfan|is-a-nurse|is-a-painter|is-a-personaltrainer|is-a-photographer|is-a-player|is-a-republican|is-a-rockstar|is-a-socialist|is-a-student|is-a-teacher|is-a-techie|is-a-therapist|is-an-accountant|is-an-actor|is-an-actress|is-an-anarchist|is-an-artist|is-an-engineer|is-an-entertainer|is-certified|is-gone|is-into-anime|is-into-cars|is-into-cartoons|is-into-games|is-leet|is-not-certified|is-slick|is-uberleet|is-with-theband|isa-geek|isa-hockeynut|issmarterthanyou|likes-pie|likescandy|neat-url|saves-the-whales|selfip|sells-for-less|sells-for-u|servebbs|simple-url|space-to-rent|teaches-yoga|writesthisblog|firebaseapp|flynnhub|githubusercontent|ro|appspot|blogspot|codespot|googleapis|googlecode|pagespeedmobilizer|withgoogle|herokuapp|herokussl|4u|nfshost|operaunite|outsystemscloud|rhcloud|sinaapp|vipsinaapp|1kapp|hk|yolasite)\.com)|(?:(?:com|fuettertdasnetz|isteingeek|istmein|lebtimnetz|leitungsen|traeumtgerade|blogspot)\.de)|(?:(?:com|asso|nom|prd|presse|tm|aeroport|assedic|avocat|avoues|cci|chambagri|chirurgiens-dentistes|experts-comptables|geometre-expert|gouv|greta|huissier-justice|medecin|notaires|pharmacien|port|veterinaire|blogspot)\.fr)|(?:(?:org|edu|net|gov|mil|com)\.kz)|(?:(?:ae|us|dyndns|blogdns|blogsite|boldlygoingnowhere|dnsalias|dnsdojo|doesntexist|dontexist|doomdns|dvrdns|dynalias|endofinternet|endoftheinternet|from-me|game-host|go\.dyndns|gotdns|kicks-ass|misconfused|podzone|readmyblog|selfip|sellsyourhome|servebbs|serveftp|servegame|stuff-4-sale|webhop|eu|al\.eu|asso\.eu|at\.eu|au\.eu|be\.eu|bg\.eu|ca\.eu|cd\.eu|ch\.eu|cn\.eu|cy\.eu|cz\.eu|de\.eu|dk\.eu|edu\.eu|ee\.eu|es\.eu|fi\.eu|fr\.eu|gr\.eu|hr\.eu|hu\.eu|ie\.eu|il\.eu|in\.eu|int\.eu|is\.eu|it\.eu|jp\.eu|kr\.eu|lt\.eu|lu\.eu|lv\.eu|mc\.eu|me\.eu|mk\.eu|mt\.eu|my\.eu|net\.eu|ng\.eu|nl\.eu|no\.eu|nz\.eu|paris\.eu|pl\.eu|pt\.eu|q-a\.eu|ro\.eu|ru\.eu|se\.eu|si\.eu|sk\.eu|tr\.eu|uk\.eu|us\.eu|hk|za)\.org)|(?:(?:ac|com|edu|int|net|org|pp|adygeya|altai|amur|arkhangelsk|astrakhan|bashkiria|belgorod|bir|bryansk|buryatia|cbg|chel|chelyabinsk|chita|chukotka|chuvashia|dagestan|dudinka|e-burg|grozny|irkutsk|ivanovo|izhevsk|jar|joshkar-ola|kalmykia|kaluga|kamchatka|karelia|kazan|kchr|kemerovo|khabarovsk|khakassia|khv|kirov|koenig|komi|kostroma|krasnoyarsk|kuban|kurgan|kursk|lipetsk|magadan|mari|mari-el|marine|mordovia|msk|murmansk|nalchik|nnov|nov|novosibirsk|nsk|omsk|orenburg|oryol|palana|penza|perm|ptz|rnd|ryazan|sakhalin|samara|saratov|simbirsk|smolensk|spb|stavropol|stv|surgut|tambov|tatarstan|tom|tomsk|tsaritsyn|tsk|tula|tuva|tver|tyumen|udm|udmurtia|ulan-ude|vladikavkaz|vladimir|vladivostok|volgograd|vologda|voronezh|vrn|vyatka|yakutia|yamal|yaroslavl|yekaterinburg|yuzhno-sakhalinsk|amursk|baikal|cmw|fareast|jamal|kms|k-uralsk|kustanai|kuzbass|magnitka|mytis|nakhodka|nkz|norilsk|oskol|pyatigorsk|rubtsovsk|snz|syzran|vdonsk|zgrad|gov|mil|test|blogspot)\.ru)|(?:(?:com|edu|gov|in|net|org|cherkassy|cherkasy|chernigov|chernihiv|chernivtsi|chernovtsy|ck|cn|cr|crimea|cv|dn|dnepropetrovsk|dnipropetrovsk|dominic|donetsk|dp|if|ivano-frankivsk|kh|kharkiv|kharkov|kherson|khmelnitskiy|khmelnytskyi|kiev|kirovograd|km|kr|krym|ks|kv|kyiv|lg|lt|lugansk|lutsk|lv|lviv|mk|mykolaiv|nikolaev|od|odesa|odessa|pl|poltava|rivne|rovno|rv|sb|sebastopol|sevastopol|sm|sumy|te|ternopil|uz|uzhgorod|vinnica|vinnytsia|vn|volyn|yalta|zaporizhzhe|zaporizhzhia|zhitomir|zhytomyr|zp|zt|co|pp)\.ua)|(?:(?:ac|co|gov|ltd|me|net|nhs|org|plc|police|[\w\u0430-\u044f]\+\*sch|service\.gov|blogspot\.co)\.uk)|(?:(?:com|edu|gov|idv|net|org|blogspot|ltd|inc)\.hk)|(?:(?:ac|co|es|go|hs|kg|mil|ms|ne|or|pe|re|sc|busan|chungbuk|chungnam|daegu|daejeon|gangwon|gwangju|gyeongbuk|gyeonggi|gyeongnam|incheon|jeju|jeonbuk|jeonnam|seoul|ulsan|blogspot)\.kr)|(?:(?:ac|biz|co|desa|go|mil|my|net|or|sch|web)\.id)|(?:(?:com|net|org|gov|edu|ngo|mil|i)\.ph)|(?:(?:edu|gov|mil|com|net|org|idv|game|ebiz|club|blogspot)\.tw)|(?:(?:ac|ad|co|ed|go|gr|lg|ne|or|blogspot)\.jp)|(?:(?:com|net|org|edu|gov|int|ac|biz|info|name|pro|health)\.vn)|(?:(?:co|firm|net|org|gen|ind|nic|ac|edu|res|gov|mil|blogspot)\.in)|(?:(?:com|net|org|gov|edu|mil|name)\.my)|(?:(?:com|net|org|gov|edu|per|blogspot)\.sg)|(?:(?:edu|gov|riik|lib|med|com|pri|aip|org|fie)\.ee)|(?:(?:ac|co|go|in|mi|net|or)\.th)|[\w\u0430-\u044f]+)$/i
17 | );
18 | return match ? match[1] : domain;
19 | }
20 |
--------------------------------------------------------------------------------
/src/options/OptionsPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { injectIntl } from "react-intl";
3 | import PropTypes from "prop-types";
4 | import {
5 | Flex,
6 | Divider,
7 | Button,
8 | Select,
9 | Typography,
10 | Form,
11 | Input,
12 | Space,
13 | Card,
14 | } from "antd";
15 | import { DeleteOutlined } from "@ant-design/icons";
16 | import "./OptionsPage.css";
17 |
18 | class OptionsPage extends React.Component {
19 | static propTypes = {
20 | intl: PropTypes.object,
21 | };
22 |
23 | constructor(props) {
24 | super(props);
25 | this.i18n = (key) => props.intl.formatMessage({ id: key });
26 | this.form = React.createRef();
27 | this.state = {
28 | isEditting: false,
29 | isCreateModalOpen: false,
30 | isModifyModalOpen: false,
31 | };
32 | }
33 |
34 | componentDidMount = () => {
35 | chrome.storage.sync.get(["configuration"], (data) => {
36 | this.form.current.setFieldsValue(data.configuration);
37 | });
38 | };
39 |
40 | editOrSaveButtomOnClick = () => {
41 | if (!this.state.isEditting) {
42 | // 让表单变成编辑态
43 | this.setState({ isEditting: true });
44 | return;
45 | }
46 |
47 | const configuration = this.form.current.getFieldsValue();
48 | console.log(configuration);
49 | // 检查表单,然后保存配置信息,设置编辑状态为false
50 | this.form.current
51 | .validateFields({ recursive: true })
52 | .then(() => {
53 | chrome.storage.sync.set({ configuration: configuration }, () => {
54 | this.setState({ isEditting: false });
55 | });
56 | })
57 | .catch(() => {});
58 | };
59 |
60 | render() {
61 | return (
62 |
233 | );
234 | }
235 | }
236 |
237 | export default injectIntl(OptionsPage);
238 |
--------------------------------------------------------------------------------