├── 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 | ![演示](https://i.loli.net/2021/10/06/LQKtSh7m1kjs9vM.gif) 8 | 9 | ![WDOHGb3dsvPEC68](https://i.loli.net/2021/10/10/WDOHGb3dsvPEC68.jpg) 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 | ![oRZdQ8buGXJDylp](https://i.loli.net/2021/10/06/oRZdQ8buGXJDylp.png) -------------------------------------------------------------------------------- /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 |
68 | 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 |
63 | 68 | 69 | 70 | {this.i18n("config_page_title")} 71 | 72 | 87 | 88 | 89 |
95 | 96 | {this.i18n("config_title_fallback")} 97 | 98 | 99 | 153 | 154 | 169 | 173 | {(patterns, patternOp) => ( 174 | 179 | {patterns.map((pattern) => ( 180 |
184 | 196 | 197 | 198 | {patterns.length > 1 ? ( 199 | { 202 | if (this.state.isEditting) { 203 | patternOp.remove(pattern.name); 204 | } 205 | }} 206 | /> 207 | ) : null} 208 |
209 | ))} 210 | 217 |
218 | )} 219 |
220 |
221 | 222 | ))} 223 | 224 | 227 | 228 | )} 229 | 230 |
231 |
232 |
233 | ); 234 | } 235 | } 236 | 237 | export default injectIntl(OptionsPage); 238 | --------------------------------------------------------------------------------