├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .gitignore ├── LICENSE ├── PRIVACY.md ├── README.md ├── chrome ├── _locales │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── ko │ │ └── messages.json │ ├── pt │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ └── zh │ │ └── messages.json ├── css │ ├── icons-dark.css │ ├── icons.css │ ├── popup-dark.css │ ├── popup.css │ ├── toggle-dark.css │ └── toggle.css ├── images │ ├── 128x.png │ ├── 32x.png │ ├── 48x.png │ ├── bug.svg │ ├── color-scheme.svg │ ├── disabled.png │ ├── icon.png │ ├── observer.svg │ └── setup.svg ├── js │ ├── background.js │ ├── boot.js │ ├── color-scheme.js │ ├── data.js │ ├── popup.js │ ├── summary.js │ └── toggle.js ├── manifest.json └── views │ └── popup.html ├── preview ├── chrome-ignore-host.png ├── chrome-prevent.png ├── firefox-ignore-host.png └── firefox-prevent.png └── promo.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Describe the bug 2 | description: A clear and concise description of what the bug is. 3 | labels: ["bug", "triage"] 4 | assignees: 5 | - brcontainer 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: Describe the bug 15 | description: A clear and concise description of what the bug is. 16 | placeholder: Tell us what you see! 17 | value: "A bug happened!" 18 | validations: 19 | required: true 20 | - type: dropdown 21 | id: browsers 22 | attributes: 23 | label: What browsers are you seeing the problem on? 24 | multiple: true 25 | options: 26 | - Firefox 27 | - Microsoft Edge 28 | - Chrome 29 | - Opera 30 | - Safari 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: steps 35 | attributes: 36 | label: To Reproduce 37 | description: Steps to reproduce the behavior 38 | placeholder: | 39 | 1. Go to '...' 40 | 2. Click on '....' 41 | 3. Scroll down to '....' 42 | 4. See error 43 | value: | 44 | 1. 45 | - type: textarea 46 | id: context 47 | attributes: 48 | label: Additional context 49 | description: Add any other context about the problem here 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Describe the feature request 2 | description: Suggest an idea for this project 3 | labels: ["enhancement"] 4 | assignees: 5 | - brcontainer 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to suggest a new feature! 11 | 12 | Before continuing we recommend that you understand that the suggested resources must be within the limits of the webextensions APIs, for example, tab information needs to be provided by API: https://developer.chrome.com/docs/extensions/reference/, if your new resource needs information that it does not have in the tab it is likely that it cannot be developed. 13 | - type: dropdown 14 | id: browsers 15 | attributes: 16 | label: Does it depend on a specific browser? 17 | multiple: true 18 | options: 19 | - Firefox 20 | - Microsoft Edge 21 | - Chrome 22 | - Opera 23 | - Safari 24 | - type: textarea 25 | id: related 26 | attributes: 27 | label: Related to a problem? 28 | description: Is your feature request related to a problem? Please describe. 29 | - type: textarea 30 | id: wants 31 | attributes: 32 | label: Describe the solution you'd like 33 | description: A clear and concise description of what you want to happen. 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: alternatives 38 | attributes: 39 | label: Describe alternatives you've considered 40 | description: A clear and concise description of any alternative solutions or features you've considered. 41 | - type: textarea 42 | id: context 43 | attributes: 44 | label: Additional context 45 | description: Add any other context about the problem here 46 | - type: checkboxes 47 | id: supported 48 | attributes: 49 | label: Will the API support this feature? 50 | description: I confirm that the suggested feature requested is within the possibilities of what the Chrome extensions API allows (https://developer.chrome.com/docs/extensions/reference/). 51 | options: 52 | - label: I agree 53 | required: true 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | *.crx 17 | *.nex 18 | 19 | # Windows shortcuts 20 | *.lnk 21 | 22 | # ========================= 23 | # Operating System Files 24 | # ========================= 25 | 26 | # OSX 27 | # ========================= 28 | 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear in the root of a volume 37 | .DocumentRevisions-V100 38 | .fseventsd 39 | .Spotlight-V100 40 | .TemporaryItems 41 | .Trashes 42 | .VolumeIcon.icns 43 | 44 | # Directories potentially created on remote AFP share 45 | .AppleDB 46 | .AppleDesktop 47 | Network Trash Folder 48 | Temporary Items 49 | .apdisk 50 | 51 | # others 52 | *.pem 53 | *.psd 54 | chrome/*.zip 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Guilherme Nascimento 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 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | WE DO NOT STORE ANY DATA FROM WEBSITES (and we don't even have servers for that), the only data stored is your settings for this extension and are stored only on your computer, the settings: 4 | 5 | - https://github.com/brcontainer/prevent-duplicate-tabs/#configs 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Prevents from automatically creating new tabs or repeating tabs when two or more tabs have the same addresses 4 | 5 | The process of closing repeated tabs is automatic based on settings that you can determine within the extension/add-on, it is also possible to quickly disable and activate the add-on without having to access the extensions panel. 6 | 7 | You can add a host or URL to specify as an exception so that the extension/add-on does not close repeated tabs. 8 | 9 | The extension/add-on has support for closing repeated tabs in incognito/anonymous mode, but this works in isolation and will not share any information in anonymous mode with normal mode, being completely separate actions. 10 | 11 | Among the configurations you can choose to consider or disregard querystring and hashes in URLs as a factor to differentiate them. 12 | 13 | ![promocional](promo.png) 14 | 15 | ## Supported browsers 16 | 17 | - Chrome 30+ 18 | - Firefox 48+ 19 | - Edge (Chromium) 20 | - Opera 40+ 21 | 22 | ## Install 23 | 24 | - [Chrome](https://chrome.google.com/webstore/detail/prevent-duplicate-tabs/eednccpckdkpojaiemedoejdngappaag) 25 | - [Firefox](https://addons.mozilla.org/firefox/addon/smart-prevent-duplicate-tabs/) 26 | - [Edge](https://microsoftedge.microsoft.com/addons/detail/prevent-duplicate-tabs/iijplllphnkkeepcinimpjobncicbbmb) 27 | - [Opera](https://addons.opera.com/en/extensions/details/prevent-duplicate-tabs/) 28 | 29 | ## Configs 30 | 31 | ### Sort order 32 | 33 | It is also possible to configure whether you prefer to close the oldest tabs and whether you want to prevent the active tab (currently visible) from being closed: 34 | 35 | Configuration | Description 36 | --- | --- 37 | `close olders` | Close the old tabs and keep the most recent one with the same URL 38 | `keep the tab that is active` | Don't close active tabs 39 | 40 | ### URLs 41 | 42 | Configuration | Description 43 | --- | --- 44 | `only http` | Close only tabs with HTTP(s) urls 45 | `querystring` | If disabled (off) it's ignore querystring in URLs, then this `http://foo/bar?baz` will be equivalent to this `http://foo/bar` 46 | `hash` | If disabled (off) it's ignore hash in URLs, then this `http://foo/bar#baz` will be equivalent to this `http://foo/bar` 47 | 48 | ### Others 49 | 50 | Configuration | Description 51 | --- | --- 52 | `incognito` | Check repeated anonymous tabs (requires you to manually enable in your browser) 53 | `windows` | Close only repeated tabs from the same window (recommended) 54 | `containers` | If enabled it will ignore repeated tabs in different containers, treating them as if they were non-repeated tabs (recommended), only Firefox 55 | 56 | ### Events 57 | 58 | The closing of the tabs works based on events from the Chrome API and you can disable or reactivate such events within the extension/add-on: 59 | 60 | Event | Description 61 | --- | --- 62 | `start` | Close repeated tabs when your browser is launched 63 | `update` | Close repeated tabs when a tab is updated, more details in [`tabs.onUpdated.addListener`](https://developer.chrome.com/extensions/tabs#event-onUpdated) 64 | `create` | Close repeated tabs when a new tab is created, more details in [`tabs.onCreated.addListener`](https://developer.chrome.com/extensions/tabs#event-onCreated) 65 | `replace` | Close repeated tabs when a tab is replaced with another tab due to prerendering or instant, more details in [`tabs.onReplaced.addListener`](https://developer.chrome.com/extensions/tabs#event-onReplaced) 66 | `attach` | Close repeated tabs when a tab is attached to another window or detached (creating a new window), more details in [`tabs.onReplaced.addListener`](https://developer.chrome.com/extensions/tabs#event-onAttached) 67 | `datachange` | Close repeated tabs when you change `sort order`, `URLs`, `others` and `events` configurations from extension 68 | 69 | ## Languages 70 | 71 | Supported languages: 72 | 73 | - [Chinese (zh)](chrome/_locales/zh/messages.json) 74 | - [English (en)](chrome/_locales/en/messages.json) 75 | - [French (fr)](chrome/_locales/fr/messages.json) 76 | - [German (de)](chrome/_locales/de/messages.json) 77 | - [Japanese (ja)](chrome/_locales/ja/messages.json) 78 | - [Korean (ko)](chrome/_locales/ko/messages.json) 79 | - [Portuguese (pt)](chrome/_locales/pt/messages.json) (and [Brazilian Portuguese (pt_BR)](chrome/_locales/pt_BR/messages.json)) 80 | - [Spanish (es)](chrome/_locales/es/messages.json) 81 | 82 | I used automatic translations to generate the texts (`messages.json`), so there may be errors, the project is open-source, so feel free to send a pull-request with corrections in the translations. 83 | -------------------------------------------------------------------------------- /chrome/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "ausschalten" 4 | }, 5 | "title": { 6 | "message": "die Einstellungen" 7 | }, 8 | "action_title": { 9 | "message": "Aktionen" 10 | }, 11 | "ignore_host": { 12 | "message": "Ignorieren Sie diesen Host" 13 | }, 14 | "reconsider_host": { 15 | "message": "Überdenken Sie diesen Host" 16 | }, 17 | "ignore_host_description": { 18 | "message": "Verhindern von doppelten Registerkarten, deren Adresse die Domäne `{host}` enthält" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "Wiederholte Registerkarten mit der Domäne `{host}` werden NICHT als Duplikate betrachtet" 22 | }, 23 | "ignore_url": { 24 | "message": "Ignorieren Sie diese URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "Überdenken Sie diese URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "Verhindern von doppelten Registerkarten mit der Adresse `{url}`" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "Wiederholte Registerkarten mit der Adresse `{url}` werden NICHT als Duplikate betrachtet" 34 | }, 35 | "sort_order_title": { 36 | "message": "Sortierung" 37 | }, 38 | "close_olders_title": { 39 | "message": "Ältere schließen" 40 | }, 41 | "close_olders_description": { 42 | "message": "Schließen Sie die alten Registerkarten und behalten Sie die neueste mit dem gleichen URL bei" 43 | }, 44 | "keep_active_title": { 45 | "message": "Behalten Sie die aktive Registerkarte bei" 46 | }, 47 | "keep_active_description": { 48 | "message": "Schließen Sie keine aktiven Registerkarten" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "Nur HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "Schließen Sie nur Registerkarten mit HTTP-URLs" 58 | }, 59 | "querystring_title": { 60 | "message": "querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "Wenn deaktiviert (schalte aus), wird querystring in der URL ignoriert, dann entspricht dieser `http://foo/bar?baz` diesem `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "hash" 67 | }, 68 | "hash_description": { 69 | "message": "Wenn deaktiviert (schalte aus), wird hash in der URL ignoriert, dann entspricht dieser `http://foo/bar#baz` diesem `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "Andere" 73 | }, 74 | "incognito_title": { 75 | "message": "Inkognito" 76 | }, 77 | "incognito_description": { 78 | "message": "Überprüfen Sie wiederholte Registerkarten im privaten Modus (erfordert die manuelle Aktivierung in Ihrem Browser)" 79 | }, 80 | "windows_title": { 81 | "message": "Fenster" 82 | }, 83 | "windows_description": { 84 | "message": "Schließen Sie nur wiederholte Registerkarten aus demselben Fenster (empfohlen)" 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "Wenn diese Option aktiviert ist, werden wiederholte Registerkarten in verschiedenen \"Containers\" ignoriert und so behandelt, als wären sie nicht wiederholte Registerkarten (empfohlen)" 91 | }, 92 | "events_title": { 93 | "message": "Veranstaltungen" 94 | }, 95 | "start_event_title": { 96 | "message": "start" 97 | }, 98 | "start_event_description": { 99 | "message": "Schließen Sie wiederholte Registerkarten, wenn Ihr Browser gestartet wird" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "Schließen Sie wiederholte Registerkarten, wenn eine Registerkarte aktualisiert wird" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "Schließen Sie wiederholte Registerkarten, wenn eine neue Registerkarte erstellt wird" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "Schließen Sie wiederholte Registerkarten, wenn eine Registerkarte aufgrund von Vorrendern oder sofortigem Ersetzen durch eine andere ersetzt wird" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "Schließen Sie wiederholte Registerkarten, wenn eine Registerkarte an ein anderes Fenster angehängt oder getrennt wird (Erstellen eines neuen Fensters)." 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "Schließen Sie wiederholte Registerkarten, wenn Sie die Konfiguration von \"Sortierung\", \"Veranstaltungen\", \"URLs\" und \"Andere\" ändern" 130 | }, 131 | "color_scheme_title": { 132 | "message": "Erscheinung" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "Version {version}" 145 | }, 146 | "report": { 147 | "message": "Problem melden" 148 | }, 149 | "unavailable": { 150 | "message": "nicht verfügbar" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "Turn off" 4 | }, 5 | "title": { 6 | "message": "settings" 7 | }, 8 | "actions_title": { 9 | "message": "Actions" 10 | }, 11 | "ignore_host": { 12 | "message": "Ignore this host" 13 | }, 14 | "reconsider_host": { 15 | "message": "Reconsider this host" 16 | }, 17 | "ignore_host_description": { 18 | "message": "Preventing duplicate tabs whose address contains the `{host}` host" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "Repeated tabs with `{host}` host will NOT be considered duplicates" 22 | }, 23 | "ignore_url": { 24 | "message": "Ignore this URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "Reconsider this URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "Preventing duplicate tabs with the `{url}` address" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "Repeated tabs with `{url}` address will NOT be considered duplicates" 34 | }, 35 | "sort_order_title": { 36 | "message": "Sort order" 37 | }, 38 | "close_olders_title": { 39 | "message": "Close olders" 40 | }, 41 | "close_olders_description": { 42 | "message": "Close the old tabs and keep the most recent one with the same URL" 43 | }, 44 | "keep_active_title": { 45 | "message": "Preserve active tab" 46 | }, 47 | "keep_active_description": { 48 | "message": "Don't close active tabs" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "Only HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "Close only tabs with HTTP(s) urls" 58 | }, 59 | "querystring_title": { 60 | "message": "Querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "If disabled (off) it's ignore querystring in URLs, then this `http://foo/bar?baz` will be equivalent to this `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "Hash" 67 | }, 68 | "hash_description": { 69 | "message": "If disabled (off) it's ignore hash in URLs, then this `http://foo/bar#baz` will be equivalent to this `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "Others" 73 | }, 74 | "incognito_title": { 75 | "message": "Incognito" 76 | }, 77 | "incognito_description": { 78 | "message": "Check repeated anonymous tabs (requires you to manually enable in your browser)" 79 | }, 80 | "windows_title": { 81 | "message": "Windows" 82 | }, 83 | "windows_description": { 84 | "message": "Close only repeated tabs from the same window (recommended)" 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "If enabled it will ignore repeated tabs in different containers, treating them as if they were non-repeated tabs (recommended)" 91 | }, 92 | "events_title": { 93 | "message": "Events" 94 | }, 95 | "start_event_title": { 96 | "message": "start" 97 | }, 98 | "start_event_description": { 99 | "message": "Close repeated tabs when your browser is launched" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "Close repeated tabs when a tab is updated" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "Close repeated tabs when a new tab is created" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "Close repeated tabs when a tab is replaced with another tab due to prerendering or instant" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "Close repeated tabs when a tab is attached to another window or detached (creating a new window)" 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "Close repeated tabs when you change \"sort order\", \"URLs\", \"others\" and \"events\" configurations from extension" 130 | }, 131 | "color_scheme_title": { 132 | "message": "Choose theme" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "Version {version}" 145 | }, 146 | "report": { 147 | "message": "report a issue" 148 | }, 149 | "unavailable": { 150 | "message": "unavailable" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "desactivar" 4 | }, 5 | "title": { 6 | "message": "ajustes" 7 | }, 8 | "actions_title": { 9 | "message": "Comportamiento" 10 | }, 11 | "ignore_host": { 12 | "message": "Ignorar este anfitrión" 13 | }, 14 | "reconsider_host": { 15 | "message": "Reconsidere este anfitrión" 16 | }, 17 | "ignore_host_description": { 18 | "message": "Evitar pestañas duplicadas cuya dirección contiene el dominio {host}" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "Las pestañas repetidas con el dominio {host} NO se considerarán duplicadas" 22 | }, 23 | "ignore_url": { 24 | "message": "Ignorar esta URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "Reconsidere esta URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "Evitar pestañas duplicadas con la dirección {url}" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "Las pestañas repetidas con la dirección {url} NO se considerarán duplicadas" 34 | }, 35 | "sort_order_title": { 36 | "message": "Orden de clasificación" 37 | }, 38 | "close_olders_title": { 39 | "message": "Cerrar viejos" 40 | }, 41 | "close_olders_description": { 42 | "message": "Cerrar las pestañas antiguas y conserva la más reciente con el mismo URL" 43 | }, 44 | "keep_active_title": { 45 | "message": "Conservar pestaña activa" 46 | }, 47 | "keep_active_description": { 48 | "message": "No cierres las pestañas activas" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "Solo HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "Cerrar solo pestañas con URL HTTP" 58 | }, 59 | "querystring_title": { 60 | "message": "querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "Si está desactivado, ignora querystring en las URL, entonces este `http://foo/bar?baz` será equivalente a `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "hash" 67 | }, 68 | "hash_description": { 69 | "message": "Si está desactivado, ignora querystring en las URL, entonces este `http://foo/bar#baz` será equivalente a `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "Otros" 73 | }, 74 | "incognito_title": { 75 | "message": "Incógnito" 76 | }, 77 | "incognito_description": { 78 | "message": "Verifique las pestañas repetidas en modo privado (requiere que las habilite manualmente en su navegador)" 79 | }, 80 | "windows_title": { 81 | "message": "Ventana" 82 | }, 83 | "windows_description": { 84 | "message": "Cierre solo las pestañas repetidas de la misma ventana (recomendado) " 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "Si está habilitado, ignorará las pestañas repetidas en diferentes containers, tratándolas como si fueran pestañas no repetidas (recomendado)." 91 | }, 92 | "events_title": { 93 | "message": "Eventos" 94 | }, 95 | "start_event_title": { 96 | "message": "comienzo" 97 | }, 98 | "start_event_description": { 99 | "message": "Cerrar las pestañas repetidas cuando se inicia su navegador" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "Cerrar pestañas repetidas cuando se actualiza una pestaña" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "Cerrar pestañas repetidas cuando se crea una nueva pestaña" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "Cerrar las pestañas repetidas cuando una pestaña se reemplaza con otra pestaña debido a la reproducción previa o instantánea" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "Cerrar pestañas repetidas cuando una pestaña se adjunta a otra ventana o se separa (creando una nueva ventana)" 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "Cerrar las pestañas repetidas cuando cambie la configuración de \"Orden de clasificación\", \"URLs\", \"otros\" y \"eventos\"" 130 | }, 131 | "color_scheme_title": { 132 | "message": "Tema" 133 | }, 134 | "color_scheme_description": { 135 | "message": "Elegir tema" 136 | }, 137 | "color_scheme_dark": { 138 | "message": "dark" 139 | }, 140 | "color_scheme_device": { 141 | "message": "device" 142 | }, 143 | "color_scheme_light": { 144 | "message": "light" 145 | }, 146 | "version": { 147 | "message": "Versión {version}" 148 | }, 149 | "report": { 150 | "message": "informar un problema" 151 | }, 152 | "unavailable": { 153 | "message": "indisponible" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /chrome/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "Éteindre" 4 | }, 5 | "title": { 6 | "message": "paramètres" 7 | }, 8 | "actions_title": { 9 | "message": "Actions" 10 | }, 11 | "ignore_host": { 12 | "message": "Ignorer cet hôte" 13 | }, 14 | "reconsider_host": { 15 | "message": "Reconsidérer cet hôte" 16 | }, 17 | "ignore_host_description": { 18 | "message": "Empêcher les onglets en double dont l'adresse contient le domaine {host}" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "Les onglets répétés avec le domaine {host} ne seront PAS considérés comme des doublons" 22 | }, 23 | "ignore_url": { 24 | "message": "Ignorer cette URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "Reconsidérer cette URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "Empêcher les onglets en double avec l'adresse {url}" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "Les onglets répétés avec l'adresse {url} ne seront PAS considérés comme des doublons" 34 | }, 35 | "sort_order_title": { 36 | "message": "Ordre de tri" 37 | }, 38 | "close_olders_title": { 39 | "message": "Fermer les anciens" 40 | }, 41 | "close_olders_description": { 42 | "message": "Fermez les anciens onglets et conservez le plus récent avec le même URL" 43 | }, 44 | "keep_active_title": { 45 | "message": "Conserver l'onglet actif" 46 | }, 47 | "keep_active_description": { 48 | "message": "Ne fermez pas les onglets actifs" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "Seulement HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "Fermer uniquement les onglets avec des URL HTTP" 58 | }, 59 | "querystring_title": { 60 | "message": "querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "S'il est désactivé (éteindre), il ignore querystring dans les URL, alors ce `http://foo/bar?baz` sera équivalent à `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "hash" 67 | }, 68 | "hash_description": { 69 | "message": "S'il est désactivé (éteindre), il ignore querystring dans les URL, alors ce `http://foo/bar#baz` sera équivalent à `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "Autres" 73 | }, 74 | "incognito_title": { 75 | "message": "Incognito" 76 | }, 77 | "incognito_description": { 78 | "message": "Vérifier les onglets répétés en mode privé (vous oblige à activer manuellement dans votre navigateur)" 79 | }, 80 | "windows_title": { 81 | "message": "Fenêtre" 82 | }, 83 | "windows_description": { 84 | "message": "Fermer uniquement les onglets répétés de la même fenêtre (recommandé)" 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "S'il est activé, il ignorera les onglets répétés dans différents «containers», les traitant comme s'il s'agissait d'onglets non répétés (conseillé)" 91 | }, 92 | "events_title": { 93 | "message": "Événements" 94 | }, 95 | "start_event_title": { 96 | "message": "Début" 97 | }, 98 | "start_event_description": { 99 | "message": "Fermez les onglets répétés lorsque votre navigateur est lancé" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "Fermer les onglets répétés lorsqu'un onglet est mis à jour" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "Fermer les onglets répétés lorsqu'un nouvel onglet est créé" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "Fermez les onglets répétés lorsqu'un onglet est remplacé par un autre en raison d'un prérendu ou instantané" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "Fermez les onglets répétés lorsqu'un onglet est attaché à une autre fenêtre ou détaché (création d'une nouvelle fenêtre)." 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "Fermez les onglets répétés lorsque vous modifiez la configuration de \"ordre de tri\", \"URLs\", \"autres\" et \"événements\"" 130 | }, 131 | "color_scheme_title": { 132 | "message": "Choisir un thème" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "Version {version}" 145 | }, 146 | "report": { 147 | "message": "signaler un problème" 148 | }, 149 | "unavailable": { 150 | "message": "indisponible" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "消す" 4 | }, 5 | "title": { 6 | "message": "設定" 7 | }, 8 | "actions_title": { 9 | "message": "行動" 10 | }, 11 | "ignore_host": { 12 | "message": "このホストを無視" 13 | }, 14 | "reconsider_host": { 15 | "message": "このホストを再検討" 16 | }, 17 | "ignore_host_description": { 18 | "message": "アドレスに `{host}` ドメインが含まれるタブの重複を防止する" 19 | }, 20 | "reconsider_host_description": { 21 | "message": " `{host}` ドメインで繰り返されるタブは重複とは見なされません" 22 | }, 23 | "ignore_url": { 24 | "message": "このURLを無視" 25 | }, 26 | "reconsider_url": { 27 | "message": "このURLを再検討する" 28 | }, 29 | "ignore_url_description": { 30 | "message": "URLアドレスを使用したタブの重複の防止" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "URLアドレスで繰り返されるタブは重複とは見なされません" 34 | }, 35 | "sort_order_title": { 36 | "message": "ソート順" 37 | }, 38 | "close_olders_title": { 39 | "message": "年上の人を閉じる" 40 | }, 41 | "close_olders_description": { 42 | "message": "古いタブを閉じ、最新のタブを同じURLで保持します" 43 | }, 44 | "keep_active_title": { 45 | "message": "アクティブなタブを保持" 46 | }, 47 | "keep_active_description": { 48 | "message": "アクティブなタブを閉じないでください" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "HTTPのみ" 55 | }, 56 | "only_http_description": { 57 | "message": "HTTP URLのあるタブのみを閉じる" 58 | }, 59 | "querystring_title": { 60 | "message": "「__uerystring」" 61 | }, 62 | "querystring_description": { 63 | "message": "無効(消す)の場合、URLのquerystringは無視されます。この `http://foo/bar#baz` は、この `http://foo/bar` と同等になります。" 64 | }, 65 | "hash_title": { 66 | "message": "Hash" 67 | }, 68 | "hash_description": { 69 | "message": "無効(消す)の場合、URLのhashは無視されます。この `http://foo/bar?baz` は、この `http://foo/bar` と同等になります。" 70 | }, 71 | "others_title": { 72 | "message": "その他" 73 | }, 74 | "incognito_title": { 75 | "message": "シークレット" 76 | }, 77 | "incognito_description": { 78 | "message": "プライベートモードで繰り返しタブを確認する(ブラウザーで手動で有効にする必要があります)" 79 | }, 80 | "windows_title": { 81 | "message": "ウィンドウズ" 82 | }, 83 | "windows_description": { 84 | "message": "同じウィンドウから繰り返されるタブのみを閉じる(推奨)" 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "有効にすると、異なる「containers」で繰り返されるタブが無視され、繰り返されないタブであるかのように扱われます。(推奨)" 91 | }, 92 | "events_title": { 93 | "message": "イベント" 94 | }, 95 | "start_event_title": { 96 | "message": "start" 97 | }, 98 | "start_event_description": { 99 | "message": "ブラウザの起動時に繰り返されるタブを閉じます" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "タブが更新されたときに繰り返されるタブを閉じる" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "新しいタブが作成されたときに繰り返されるタブを閉じる" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "事前レンダリングまたはインスタントのためにタブが別のタブに置き換えられた場合、繰り返されるタブを閉じる" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "タブが別のウィンドウにアタッチされているか、切り離されている(新しいウィンドウを作成している)ときに、繰り返されるタブを閉じます" 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "「ソート順」、「イベント」、「URL」、および「その他」の構成を変更するときに、繰り返されるタブを閉じる" 130 | }, 131 | "color_scheme_title": { 132 | "message": "外貌" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "バージョン{version}" 145 | }, 146 | "report": { 147 | "message": "問題を報告する" 148 | }, 149 | "unavailable": { 150 | "message": "利用不可" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "끄다" 4 | }, 5 | "title": { 6 | "message": "설정" 7 | }, 8 | "actions_title": { 9 | "message": "행위" 10 | }, 11 | "ignore_host": { 12 | "message": "이 호스트 무시" 13 | }, 14 | "reconsider_host": { 15 | "message": "이 호스트 재검토" 16 | }, 17 | "ignore_host_description": { 18 | "message": "주소에 `{host}`도메인이 포함 된 중복 탭 방지" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "`{host}`도메인이있는 반복 된 탭은 중복으로 간주되지 않습니다" 22 | }, 23 | "ignore_url": { 24 | "message": "이 URL 무시" 25 | }, 26 | "reconsider_url": { 27 | "message": "이 URL 재검토" 28 | }, 29 | "ignore_url_description": { 30 | "message": "`{url}`주소로 중복 탭 방지" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "`{url}`주소가있는 반복 탭은 중복으로 간주되지 않습니다" 34 | }, 35 | "sort_order_title": { 36 | "message": "정렬 순서" 37 | }, 38 | "close_olders_title": { 39 | "message": "가까운 노인" 40 | }, 41 | "close_olders_description": { 42 | "message": "이전 탭을 닫고 동일한 `http://foo/bar` 로 최신 탭을 유지합니다" 43 | }, 44 | "keep_active_title": { 45 | "message": "활성 탭 유지" 46 | }, 47 | "keep_active_description": { 48 | "message": "활성 탭을 닫지 마십시오" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "HTTP 만" 55 | }, 56 | "only_http_description": { 57 | "message": "HTTP URL이있는 탭만 닫기" 58 | }, 59 | "querystring_title": { 60 | "message": "querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "비활성화 된 경우 (끄다) URL의 querystring를 무시하고이 `http://foo/bar?baz` 는이 NOHHHASSHH와 동일합니다" 64 | }, 65 | "hash_title": { 66 | "message": "Hash" 67 | }, 68 | "hash_description": { 69 | "message": "비활성화 된 경우 (끄다) URL의 querystring를 무시하고이 `http://foo/bar#baz` 는이 `http://foo/bar` 와 동일합니다" 70 | }, 71 | "others_title": { 72 | "message": "기타" 73 | }, 74 | "incognito_title": { 75 | "message": "가명" 76 | }, 77 | "incognito_description": { 78 | "message": "비공개 모드에서 반복되는 탭 확인 (브라우저에서 수동으로 활성화해야 함)" 79 | }, 80 | "windows_title": { 81 | "message": "창을" 82 | }, 83 | "windows_description": { 84 | "message": "같은 창에서 반복되는 탭만 닫기 (권장)" 85 | }, 86 | "containers_title": { 87 | "message": "컨테이너" 88 | }, 89 | "containers_description": { 90 | "message": "활성화하면 다른 \"컨테이너\"의 반복 된 탭을 무시하고 반복되지 않는 탭인 것처럼 처리합니다. (추천)" 91 | }, 92 | "events_title": { 93 | "message": "이벤트" 94 | }, 95 | "start_event_title": { 96 | "message": "start" 97 | }, 98 | "start_event_description": { 99 | "message": "브라우저가 시작될 때 반복되는 탭 닫기" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "탭이 업데이트되면 반복되는 탭 닫기" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "새 탭을 만들 때 반복되는 탭 닫기" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "사전 렌더링 또는 인스턴트로 인해 탭이 다른 탭으로 교체 될 때 반복되는 탭 닫기" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "탭이 다른 창에 연결되거나 분리 (새 창 생성) 될 때 반복되는 탭을 닫습니다" 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "\"정렬 순서\", \"이벤트\", \"URL\"및 \"기타\"의 구성을 변경할 때 반복되는 탭을 닫습니다" 130 | }, 131 | "color_scheme_title": { 132 | "message": "테마 선택" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "버전 {version}" 145 | }, 146 | "report": { 147 | "message": "문제보고" 148 | }, 149 | "unavailable": { 150 | "message": "없는" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "Desativar" 4 | }, 5 | "title": { 6 | "message": "Configurações" 7 | }, 8 | "actions_title": { 9 | "message": "Ações" 10 | }, 11 | "ignore_host": { 12 | "message": "Ignorar esse domínio" 13 | }, 14 | "reconsider_host": { 15 | "message": "Reconsiderar esse domínio" 16 | }, 17 | "ignore_host_description": { 18 | "message": "Prevenir guias duplicadas cujo endereço contém o domínio `{host}`" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "Guias repetidas com domínio `{host}` NÃO serão consideradas duplicatas" 22 | }, 23 | "ignore_url": { 24 | "message": "Ignorar essa URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "Reconsiderar essa URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "Prevenir guias duplicadas com o endereço `{url}`" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "Guias repetidas com o endereço `{url}` NÃO serão consideradas duplicatas" 34 | }, 35 | "sort_order_title": { 36 | "message": "Ordem de classificação" 37 | }, 38 | "close_olders_title": { 39 | "message": "Fechar mais antigas" 40 | }, 41 | "close_olders_description": { 42 | "message": "Fechar as guias antigas e mantenha a mais recente com a mesma URL" 43 | }, 44 | "keep_active_title": { 45 | "message": "Preservar guia ativa" 46 | }, 47 | "keep_active_description": { 48 | "message": "Não fechar as guias ativas" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "Apenas HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "Fechar apenas guias com urls HTTP(s)" 58 | }, 59 | "querystring_title": { 60 | "message": "Querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "Se desativado irá ignorar querystring em URLs, então este `http://foo/bar?baz` será equivalente a este `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "Hash" 67 | }, 68 | "hash_description": { 69 | "message": "Se desativado irá ignorar hash em URLs, então este `http://foo/bar#baz` será equivalente a este `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "Outros" 73 | }, 74 | "incognito_title": { 75 | "message": "Incognito" 76 | }, 77 | "incognito_description": { 78 | "message": "Verifique as guias anônimas repetidas (requer que você as habilite manualmente em seu navegador)" 79 | }, 80 | "windows_title": { 81 | "message": "Janelas" 82 | }, 83 | "windows_description": { 84 | "message": "Feche apenas as guias repetidas da mesma janela (recomendado)" 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "Se habilitado irá ignorar guias repetidas em containers diferentes, tratando como se fossem guias não repetidas (recomendado)" 91 | }, 92 | "events_title": { 93 | "message": "Eventos" 94 | }, 95 | "start_event_title": { 96 | "message": "start" 97 | }, 98 | "start_event_description": { 99 | "message": "Fechar as guias repetidas quando o navegador for iniciado" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "Fechar guias repetidas quando uma guia é atualizada" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "Fechar as guias repetidas quando uma nova guia for criada" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "Fechar as guias repetidas quando uma guia for substituída por outra devido a pré-renderização ou instantâneo" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "Fechar guias repetidas quando uma guia é anexada a outra janela ou desanexada (criando uma nova janela)" 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "Fechar as guias repetidas ao alterar as configurações de \"ordem de classificação\", \"URLs\", \"outros\" e \"eventos\" da extensão" 130 | }, 131 | "color_scheme_title": { 132 | "message": "Aspecto" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "Versão {version}" 145 | }, 146 | "report": { 147 | "message": "relatar um problema" 148 | }, 149 | "unavailable": { 150 | "message": "indisponível" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/pt_BR/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "Desativar" 4 | }, 5 | "title": { 6 | "message": "Configurações" 7 | }, 8 | "actions_title": { 9 | "message": "Ações" 10 | }, 11 | "ignore_host": { 12 | "message": "Ignorar esse domínio" 13 | }, 14 | "reconsider_host": { 15 | "message": "Reconsiderar esse domínio" 16 | }, 17 | "ignore_host_description": { 18 | "message": "Prevenir abas duplicadas cujo endereço contém o domínio `{host}`" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "Abas repetidas com domínio `{host}` NÃO serão consideradas duplicatas" 22 | }, 23 | "ignore_url": { 24 | "message": "Ignorar essa URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "Reconsiderar essa URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "Prevenir abas duplicadas com o endereço `{url}`" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "Abas repetidas com o endereço `{url}` NÃO serão consideradas duplicatas" 34 | }, 35 | "sort_order_title": { 36 | "message": "Ordem de classificação" 37 | }, 38 | "close_olders_title": { 39 | "message": "Fechar mais antigas" 40 | }, 41 | "close_olders_description": { 42 | "message": "Fechar as abas antigas e mantenha a mais recente com a mesma URL" 43 | }, 44 | "keep_active_title": { 45 | "message": "Preservar guia ativa" 46 | }, 47 | "keep_active_description": { 48 | "message": "Não fechar as abas ativas" 49 | }, 50 | "urls_title": { 51 | "message": "URLs" 52 | }, 53 | "only_http_title": { 54 | "message": "Apenas HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "Fechar apenas abas com urls HTTP(s)" 58 | }, 59 | "querystring_title": { 60 | "message": "Querystring" 61 | }, 62 | "querystring_description": { 63 | "message": "Se desativado irá ignorar querystring em URLs, então este `http://foo/bar?baz` será equivalente a este `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "Hash" 67 | }, 68 | "hash_description": { 69 | "message": "Se desativado irá ignorar hash em URLs, então este `http://foo/bar#baz` será equivalente a este `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "Outros" 73 | }, 74 | "incognito_title": { 75 | "message": "Incognito" 76 | }, 77 | "incognito_description": { 78 | "message": "Verifique as abas anônimas repetidas (requer que você as habilite manualmente em seu navegador)" 79 | }, 80 | "windows_title": { 81 | "message": "Janelas" 82 | }, 83 | "windows_description": { 84 | "message": "Feche apenas as abas repetidas da mesma janela (recomendado)" 85 | }, 86 | "containers_title": { 87 | "message": "Containers" 88 | }, 89 | "containers_description": { 90 | "message": "Se habilitado irá ignorar abas repetidas em containers diferentes, tratando como se fossem abas não repetidas (recomendado)" 91 | }, 92 | "events_title": { 93 | "message": "Eventos" 94 | }, 95 | "start_event_title": { 96 | "message": "start" 97 | }, 98 | "start_event_description": { 99 | "message": "Fechar as abas repetidas quando o navegador for iniciado" 100 | }, 101 | "update_event_title": { 102 | "message": "update" 103 | }, 104 | "update_event_description": { 105 | "message": "Fechar abas repetidas quando uma aba é atualizada" 106 | }, 107 | "create_event_title": { 108 | "message": "create" 109 | }, 110 | "create_event_description": { 111 | "message": "Fechar as abas repetidas quando uma nova aba for criada" 112 | }, 113 | "replace_event_title": { 114 | "message": "replace" 115 | }, 116 | "replace_event_description": { 117 | "message": "Fechar as abas repetidas quando uma aba for substituída por outra devido a pré-renderização ou instantâneo" 118 | }, 119 | "attached_event_title": { 120 | "message": "attach" 121 | }, 122 | "attached_event_description": { 123 | "message": "Fechar abas repetidas quando uma guia é anexada a outra janela ou desanexada (criando uma nova janela)" 124 | }, 125 | "datachange_event_title": { 126 | "message": "datachange" 127 | }, 128 | "datachange_event_description": { 129 | "message": "Fechar as abas repetidas ao alterar as configurações de \"ordem de classificação\", \"URLs\", \"outros\" e \"eventos\" da extensão" 130 | }, 131 | "color_scheme_title": { 132 | "message": "Aparência" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "dark" 136 | }, 137 | "color_scheme_device": { 138 | "message": "device" 139 | }, 140 | "color_scheme_light": { 141 | "message": "light" 142 | }, 143 | "version": { 144 | "message": "Versão {version}" 145 | }, 146 | "report": { 147 | "message": "relatar um problema" 148 | }, 149 | "unavailable": { 150 | "message": "indisponível" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/_locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "turn_off": { 3 | "message": "关闭" 4 | }, 5 | "title": { 6 | "message": "设置" 7 | }, 8 | "actions_title": { 9 | "message": "动作" 10 | }, 11 | "ignore_host": { 12 | "message": "忽略此域名" 13 | }, 14 | "reconsider_host": { 15 | "message": "恢复此域名" 16 | }, 17 | "ignore_host_description": { 18 | "message": "阻止包含`{host}`域名的重复标签页" 19 | }, 20 | "reconsider_host_description": { 21 | "message": "含有`{host}`域名的重复标签页将忽略重复项" 22 | }, 23 | "ignore_url": { 24 | "message": "忽略此URL" 25 | }, 26 | "reconsider_url": { 27 | "message": "恢复此URL" 28 | }, 29 | "ignore_url_description": { 30 | "message": "阻止 `{url}` 地址重复标签页" 31 | }, 32 | "reconsider_url_description": { 33 | "message": "地址为 `{url}` 的重复标签页将被忽略" 34 | }, 35 | "sort_order_title": { 36 | "message": "排序" 37 | }, 38 | "close_olders_title": { 39 | "message": "关闭旧标签" 40 | }, 41 | "close_olders_description": { 42 | "message": "关闭旧标签并使用该地址的最新标签" 43 | }, 44 | "keep_active_title": { 45 | "message": "保留活动的标签页" 46 | }, 47 | "keep_active_description": { 48 | "message": "不要关闭活动的标签页" 49 | }, 50 | "urls_title": { 51 | "message": "网址" 52 | }, 53 | "only_http_title": { 54 | "message": "仅HTTP" 55 | }, 56 | "only_http_description": { 57 | "message": "仅关闭HTTP(s)网址的标签" 58 | }, 59 | "querystring_title": { 60 | "message": "请求参数" 61 | }, 62 | "querystring_description": { 63 | "message": "如果禁用,则忽略URL中的参数,例如 `http://foo/bar?baz` 将等同于 `http://foo/bar`" 64 | }, 65 | "hash_title": { 66 | "message": "Hash" 67 | }, 68 | "hash_description": { 69 | "message": "如果禁用,则忽略URL中的(#),例如 `http://foo/bar#baz` 将等同于 `http://foo/bar`" 70 | }, 71 | "others_title": { 72 | "message": "其他" 73 | }, 74 | "incognito_title": { 75 | "message": "无痕模式" 76 | }, 77 | "incognito_description": { 78 | "message": "检查重复的无痕标签(需要在浏览器中手动开启)" 79 | }, 80 | "windows_title": { 81 | "message": "窗口" 82 | }, 83 | "windows_description": { 84 | "message": "仅关闭同一窗口中的重复标签页(推荐)" 85 | }, 86 | "containers_title": { 87 | "message": "群组" 88 | }, 89 | "containers_description": { 90 | "message": "如果启用,它将忽略不同分组中的重复标签页,将它们视为非重复标签页处理(推荐)" 91 | }, 92 | "events_title": { 93 | "message": "事件" 94 | }, 95 | "start_event_title": { 96 | "message": "启动" 97 | }, 98 | "start_event_description": { 99 | "message": "启动浏览器时关闭重复的标签页" 100 | }, 101 | "update_event_title": { 102 | "message": "刷新" 103 | }, 104 | "update_event_description": { 105 | "message": "刷新标签页时关闭重复的标签页" 106 | }, 107 | "create_event_title": { 108 | "message": "新建" 109 | }, 110 | "create_event_description": { 111 | "message": "打开新标签页时关闭重复的标签页" 112 | }, 113 | "replace_event_title": { 114 | "message": "覆盖" 115 | }, 116 | "replace_event_description": { 117 | "message": "当由于预渲染或即时加载导致一个标签页被另一个标签页替换时,关闭重复的标签页" 118 | }, 119 | "attached_event_title": { 120 | "message": "合并" 121 | }, 122 | "attached_event_description": { 123 | "message": "当标签页被合并到另一个窗口或从窗口分离(创建新窗口)时,关闭重复的标签页" 124 | }, 125 | "datachange_event_title": { 126 | "message": "修改配置" 127 | }, 128 | "datachange_event_description": { 129 | "message": "当您在扩展程序中更改\"排序顺序\"、\"网址\"、\"其他\"和\"事件\"配置时,关闭重复的标签页" 130 | }, 131 | "color_scheme_title": { 132 | "message": "选择主题颜色" 133 | }, 134 | "color_scheme_dark": { 135 | "message": "深色" 136 | }, 137 | "color_scheme_device": { 138 | "message": "设备" 139 | }, 140 | "color_scheme_light": { 141 | "message": "浅色" 142 | }, 143 | "version": { 144 | "message": "版本{version}" 145 | }, 146 | "report": { 147 | "message": "报告问题" 148 | }, 149 | "unavailable": { 150 | "message": "不可用" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /chrome/css/icons-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | .ico.color-scheme-device { 10 | background-image: url(../images/color-scheme.svg#invert-device); 11 | } 12 | .ico.color-scheme-light { 13 | background-image: url(../images/color-scheme.svg#invert-light); 14 | } 15 | .ico.color-scheme-dark { 16 | background-image: url(../images/color-scheme.svg#invert-dark); 17 | } 18 | -------------------------------------------------------------------------------- /chrome/css/icons.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | .ico { 10 | background: center center no-repeat; 11 | background-size: contain; 12 | display: inline-block; 13 | vertical-align: middle; 14 | height: 24px; 15 | width: 24px; 16 | } 17 | .ico.setup { 18 | background-position: right center; 19 | background-image: url(../images/setup.svg); 20 | } 21 | .ico.bug { 22 | background-image: url(../images/bug.svg); 23 | height: 16px; 24 | width: 16px; 25 | margin-right: 5px; 26 | } 27 | .ico.knowed { 28 | background-image: url(../images/observer.svg#knowed); 29 | } 30 | .ico.ignored { 31 | background-image: url(../images/observer.svg#ignored); 32 | } 33 | .ico.ignored { 34 | background-image: url(../images/observer.svg#ignored); 35 | } 36 | 37 | .ico.color-scheme-device { 38 | background-image: url(../images/color-scheme.svg#device); 39 | } 40 | .ico.color-scheme-light { 41 | background-image: url(../images/color-scheme.svg#light); 42 | } 43 | .ico.color-scheme-dark { 44 | background-image: url(../images/color-scheme.svg#dark); 45 | } 46 | -------------------------------------------------------------------------------- /chrome/css/popup-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | body { 10 | background-color: #303030 !important; 11 | } 12 | 13 | body, a { 14 | color: #fff; 15 | } 16 | 17 | var { 18 | font-style: normal; 19 | } 20 | 21 | h1 { 22 | background-color: #404040; 23 | } 24 | 25 | h1 > .global-config { 26 | color: #c0c0c0; 27 | } 28 | 29 | h1::after { 30 | border-color: rgba(250,247,247,0); 31 | border-bottom-color: #333; 32 | } 33 | 34 | h1::before { 35 | border-color: rgba(222,218,218,0); 36 | border-bottom-color: #252525; 37 | } 38 | 39 | .ico.setup { 40 | background-image: url(../images/setup.svg); 41 | } 42 | .ico.bug { 43 | background-image: url(../images/bug.svg); 44 | } 45 | .ico.knowed { 46 | background-image: url(../images/observer.svg#invert-knowed); 47 | } 48 | .ico.ignored { 49 | background-image: url(../images/observer.svg#invert-ignored); 50 | } 51 | 52 | code { 53 | color: #fff; 54 | background-color: rgba(255,255,255,.2); 55 | } 56 | 57 | .group { 58 | background-color: #404040; 59 | box-shadow: 0 2px 3px rgba(0,0,0,.04); 60 | } 61 | 62 | .group:hover { 63 | box-shadow: 0 2px 3px rgba(0,0,0,.04), 64 | 0 0 8px 0 rgba(0,0,0,.04), 65 | 0 0 15px 0 rgba(0,0,0,.01), 66 | 0 0 20px 4px rgba(0,0,0,.02); 67 | } 68 | 69 | .row:hover { 70 | background-color: #565555; 71 | } 72 | 73 | .themes input:checked + div::after { 74 | background: #fff; 75 | } 76 | 77 | main > footer, main > footer a, .row > .col > p { 78 | color: #b1a9a9; 79 | } 80 | 81 | h1, .group, .row, main > footer { 82 | border-color: #252525; 83 | } 84 | 85 | [data-ignored] button { 86 | background: rgba(0,0,0,0); 87 | } 88 | 89 | [data-ignored] button:hover { 90 | background-color: rgba(0,0,0,.05); 91 | } 92 | 93 | .row.data-ignored { 94 | background-color: #860e0e; 95 | } 96 | 97 | .row.data-ignored:hover { 98 | background-color: #ab1212; 99 | } 100 | 101 | html, html * { 102 | scrollbar-color: rgba(0,0,0,.5) rgba(0,0,0,.2); 103 | } 104 | 105 | ::-webkit-scrollbar { 106 | background-color: rgba(0,0,0,.2); 107 | } 108 | 109 | ::-webkit-scrollbar-thumb { 110 | background-color: rgba(0,0,0,.3); 111 | } 112 | 113 | ::-webkit-scrollbar-thumb:hover { 114 | background-color: rgba(0,0,0,.9); 115 | } 116 | 117 | html:target h1, 118 | html:target body, 119 | html:target .row { 120 | background-color: transparent; 121 | } 122 | 123 | :focus, .toggle > input:focus ~ label::after { 124 | outline: .15rem solid #fc612d; 125 | } 126 | -------------------------------------------------------------------------------- /chrome/css/popup.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | html, body { 10 | padding: 0; 11 | margin: 0; 12 | } 13 | 14 | html:not(:target), html:not(:target) body { 15 | overflow: hidden; 16 | width: 320px; 17 | } 18 | 19 | main { 20 | padding-top: 15px; 21 | overflow: hidden; 22 | overflow-y: auto; 23 | min-height: 300px; 24 | max-height: 400px; 25 | scroll-behavior: smooth; 26 | } 27 | 28 | * { 29 | font-family: "Helvetica Neue", Helvetica, "lucida grande", tahoma, Arial, sans-serif; 30 | -webkit-box-sizing: border-box; 31 | -moz-box-sizing: border-box; 32 | box-sizing: border-box; 33 | } 34 | 35 | body { 36 | padding: 57px 0 0 0; 37 | background-color: #faf7f7 !important; 38 | font-size: 16px; 39 | -webkit-touch-callout: none; 40 | -webkit-user-select: none; 41 | -khtml-user-select: none; 42 | -moz-user-select: none; 43 | -ms-user-select: none; 44 | user-select: none; 45 | } 46 | 47 | body, a { 48 | color: #4F4949; 49 | } 50 | 51 | var { 52 | font-style: normal; 53 | } 54 | 55 | h1, h2, h3, summary { 56 | text-transform: uppercase; 57 | letter-spacing: 1px; 58 | padding: 0; 59 | margin: 0; 60 | } 61 | 62 | h1 { 63 | position: fixed; 64 | left: 0; 65 | top: 0; 66 | z-index: 100; 67 | width: 320px; 68 | height: 57px; 69 | margin: 0; 70 | padding: 7px 6px; 71 | font-size: 10pt; 72 | font-weight: bolder; 73 | border-bottom: 1px solid; 74 | background-color: #fff; 75 | } 76 | 77 | h1 > .ico { 78 | vertical-align: middle; 79 | width: 36px; 80 | height: 36px; 81 | } 82 | 83 | h1 > span { 84 | font-size: 8pt; 85 | vertical-align: middle; 86 | position: relative; 87 | top: 4px; 88 | } 89 | 90 | h1 > .global-config { 91 | margin: 10px 4px 0 0; 92 | float: right; 93 | font-size: 8pt; 94 | color: #c0c0c0; 95 | } 96 | 97 | h1 > .global-config > span, 98 | h1 > .global-config > .toggle { 99 | vertical-align: middle; 100 | padding: 0; 101 | } 102 | 103 | h1 > .global-config > .toggle { 104 | margin-left: 5px; 105 | font-size: 12px; 106 | } 107 | 108 | h1::after, h1::before { 109 | bottom: -1px; 110 | left: 19px; 111 | border: solid transparent; 112 | content: ""; 113 | height: 0; 114 | width: 0; 115 | position: absolute; 116 | pointer-events: none; 117 | } 118 | 119 | h1::after { 120 | border-color: rgba(250,247,247,0); 121 | border-bottom-color: #faf7f7; 122 | border-width: 5px; 123 | margin-left: -5px; 124 | } 125 | 126 | h1::before { 127 | border-color: rgba(222,218,218,0); 128 | border-bottom-color: #dedada; 129 | border-width: 6px; 130 | margin-left: -6px; 131 | } 132 | 133 | h2, summary { 134 | font-weight: bold; 135 | font-size: 10pt; 136 | padding: 10px 12px; 137 | } 138 | 139 | h3 { 140 | font-size: 12px; 141 | padding: 5px 0; 142 | } 143 | 144 | details { 145 | padding: 0 0 6px 0; 146 | } 147 | 148 | summary { 149 | cursor: pointer; 150 | } 151 | 152 | button * { 153 | pointer-events: none; 154 | } 155 | 156 | code { 157 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 158 | padding: .2em .4em; 159 | margin: 0; 160 | color: #383535; 161 | font-size: 85%; 162 | font-weight: bold; 163 | background-color: rgba(27,31,35,.05); 164 | border-radius: 3px; 165 | overflow: hidden; 166 | max-width: 100%; 167 | text-overflow: ellipsis; 168 | display: inline-block; 169 | white-space: nowrap; 170 | vertical-align: text-bottom; 171 | line-height: .9; 172 | } 173 | 174 | .hide { 175 | display: none; 176 | } 177 | 178 | .group { 179 | margin: 0 10px; 180 | background-color: #fff; 181 | border: 1px solid; 182 | border-radius: 3px; 183 | box-shadow: 0 2px 3px rgba(0,0,0,.04); 184 | transition: all .1s ease; 185 | } 186 | 187 | .group:hover { 188 | box-shadow: 0 2px 3px rgba(0,0,0,.04), 189 | 0 0 8px 0 rgba(0,0,0,.04), 190 | 0 0 15px 0 rgba(0,0,0,.01), 191 | 0 0 20px 4px rgba(0,0,0,.02); 192 | } 193 | 194 | .row { 195 | padding: 5px 0 12px 0; 196 | border-bottom: 1px solid; 197 | transition: all .1s ease; 198 | } 199 | 200 | .row:hover { 201 | background-color: #efffff; 202 | } 203 | 204 | .row:last-child { 205 | border-bottom: none; 206 | } 207 | 208 | .row > .col:first-child { 209 | float: left; 210 | width: 74px; 211 | text-align: center; 212 | padding-top: 5px; 213 | } 214 | 215 | .row > .col:last-child { 216 | margin: 0 8px 0 75px; 217 | } 218 | 219 | .row > .col > p { 220 | padding: 0; 221 | margin: 0; 222 | font-size: 80%; 223 | line-height: 1.3; 224 | } 225 | 226 | .row > .col { 227 | position: relative; 228 | } 229 | 230 | .row > .col > .warn { 231 | position: absolute; 232 | right: -4px; 233 | top: 0; 234 | border-radius: 1rem; 235 | background: #fc0; 236 | padding: .15rem .5rem; 237 | font-size: 55%; 238 | text-transform: uppercase; 239 | color: #000; 240 | font-weight: bold; 241 | letter-spacing: .05rem; 242 | } 243 | 244 | .grid { 245 | padding: 4px; 246 | } 247 | 248 | .grid > * { 249 | float: left; 250 | width: 32.333%; 251 | margin-right: 1%; 252 | } 253 | 254 | .grid > :first-child { 255 | margin-left: .5%; 256 | } 257 | 258 | .grid > :last-child { 259 | margin-right: 0; 260 | } 261 | 262 | .grid::after { 263 | clear: both; 264 | display: block; 265 | content: ""; 266 | height: 0; 267 | } 268 | 269 | .themes label { 270 | position: relative; 271 | overflow: hidden; 272 | height: 100px; 273 | border-radius: .3em; 274 | cursor: pointer; 275 | text-align: center; 276 | } 277 | 278 | .themes label * { 279 | text-align: inherit; 280 | transition: all .1s ease-out; 281 | } 282 | 283 | .themes label > div { 284 | position: relative; 285 | height: 100%; 286 | font-size: 0; 287 | } 288 | 289 | .themes h4 { 290 | position: absolute; 291 | left: 0; 292 | margin: 0; 293 | bottom: 28px; 294 | width: 100%; 295 | font-size: 8pt; 296 | } 297 | 298 | .themes input:not(:checked) + div { 299 | opacity: .3; 300 | } 301 | 302 | .themes i { 303 | height: 32% !important; 304 | width: 32% !important; 305 | margin: 0 0 35px 0 !important; 306 | } 307 | 308 | .themes label > div::before { 309 | display: inline-block; 310 | content: ""; 311 | height: 100%; 312 | width: 0; 313 | vertical-align: middle; 314 | } 315 | 316 | .themes label > div::after { 317 | position: absolute; 318 | bottom: 14px; 319 | left: 50%; 320 | margin-left: -4px; 321 | content: ""; 322 | height: 8px; 323 | width: 8px; 324 | border-radius: 8px; 325 | background: rgba(0,0,0,0); 326 | } 327 | 328 | .themes input:checked + div::after { 329 | background: #404040; 330 | } 331 | 332 | .themes label > div:hover { 333 | background: rgba(255,255,255,.5); 334 | background: rgba(0,0,0,.2); 335 | } 336 | 337 | .themes input { 338 | opacity: 0; 339 | position: absolute; 340 | left: -100px; 341 | } 342 | 343 | main > footer { 344 | padding: 16px 4px; 345 | margin: 12px 12px 0 12px; 346 | text-align: center; 347 | border-top: 1px solid; 348 | font-size: 80%; 349 | } 350 | 351 | main > footer a { 352 | text-decoration: none; 353 | float: right; 354 | } 355 | 356 | main > footer #version { 357 | float: left; 358 | } 359 | 360 | main > footer, main > footer a, .row > .col > p { 361 | color: #928b8b; 362 | } 363 | 364 | h1, .group, .row, main > footer { 365 | border-color: #dedada; 366 | } 367 | 368 | .row::after, main > footer::after { 369 | clear: both; 370 | height: 0; 371 | display: block; 372 | content: " "; 373 | } 374 | 375 | [data-show]:not(.data-show) { 376 | display: none; 377 | } 378 | 379 | [data-ignored] button { 380 | background: rgba(0,0,0,0); 381 | outline: none; 382 | border: none; 383 | border-radius: 24px; 384 | margin: 9px auto 0 auto; 385 | padding: 12px 0; 386 | height: 48px; 387 | width: 48px; 388 | cursor: pointer; 389 | transform: scale(1); 390 | transition: all .1s ease-out; 391 | } 392 | 393 | [data-ignored] button:hover { 394 | transform: scale(1.1); 395 | background-color: rgba(0,0,0,.05); 396 | } 397 | 398 | .row.data-ignored { 399 | background-color: #ffdfdf; 400 | } 401 | 402 | .row.data-ignored:hover { 403 | background-color: #ffcece; 404 | } 405 | 406 | .row.data-ignored .ico.knowed, 407 | .row.data-ignored span.knowed, 408 | .row:not(.data-ignored) .ico.ignored, 409 | .row:not(.data-ignored) span.ignored { 410 | display: none; 411 | } 412 | 413 | html, html * { 414 | scrollbar-color: rgba(0,0,0,.2) #dfdfdf; 415 | scrollbar-width: thin; 416 | } 417 | 418 | ::-webkit-scrollbar { 419 | background-color: #dfdfdf; 420 | width: 6px; 421 | } 422 | 423 | ::-webkit-scrollbar-thumb { 424 | background-color: rgba(0,0,0,.2); 425 | } 426 | 427 | ::-webkit-scrollbar-thumb:hover { 428 | background-color: rgba(0,0,0,.5); 429 | } 430 | 431 | html:target, html:target body { 432 | max-height: initial; 433 | height: auto; 434 | width: auto; 435 | } 436 | 437 | html:target main { 438 | overflow: visible; 439 | min-height: 1px; 440 | max-height: 5000px; 441 | } 442 | 443 | html:target h1 { 444 | position: static; 445 | position: relative; 446 | background-color: transparent; 447 | padding-right: 2px; 448 | padding-left: 2px; 449 | margin-right: 8px; 450 | margin-left: 8px; 451 | width: auto; 452 | } 453 | 454 | html:target body { 455 | background-color: transparent; 456 | padding: 0; 457 | } 458 | 459 | html:target h1::before, html:target h1::after { 460 | display: none; 461 | } 462 | 463 | html:target .row { 464 | background-color: #fff8f8; 465 | } 466 | 467 | html:target #actions, #actions:not(.show) { 468 | display: none; 469 | } 470 | 471 | .support-containers { 472 | display: none; 473 | } 474 | 475 | .supported-containers { 476 | display: block; 477 | } 478 | 479 | :focus { 480 | outline: none; 481 | } 482 | 483 | :focus-visible, .toggle > input:focus-visible ~ label::after { 484 | outline: .15rem solid #634dff; 485 | } 486 | -------------------------------------------------------------------------------- /chrome/css/toggle-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | .toggle > label { 10 | background-color: #6c7073; 11 | box-shadow: 0 0 1px rgba(0,0,0,.3) inset; 12 | } 13 | 14 | .toggle > label::after { 15 | background: #fff; 16 | } 17 | 18 | .toggle > input:checked ~ label { 19 | background-color: #648bec; 20 | } 21 | 22 | .toggle > input:disabled ~ label { 23 | background-color: #d5d5d5; 24 | } 25 | 26 | .toggle > input:disabled ~ label::after { 27 | background-color: rgba(255,255,255,.3); 28 | } 29 | 30 | .toggle.red > input:checked ~ label { 31 | background-color: #e20b0b; 32 | } 33 | -------------------------------------------------------------------------------- /chrome/css/toggle.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | .toggle { 10 | position: relative; 11 | display: inline-block; 12 | z-index: 1; 13 | } 14 | 15 | .toggle > input { 16 | position: absolute; 17 | bottom: -0.82em; 18 | left: 2em; 19 | opacity: 0; 20 | pointer-events: none; 21 | } 22 | 23 | .toggle > label { 24 | position: relative; 25 | display: block; 26 | height: 1.92em; 27 | width: 3.5em; 28 | background-color: #dbddde; 29 | border-radius: 100px; 30 | cursor: pointer; 31 | transition: all .3s ease; 32 | } 33 | 34 | .toggle > label::before, .toggle > label::after { 35 | position: absolute; 36 | z-index: 2; 37 | left: .13em; 38 | top: .13em; 39 | display: block; 40 | width: 1.65em; 41 | height: 1.65em; 42 | transition: .3s ease; 43 | transition-property: left, color; 44 | } 45 | 46 | .toggle > label::before { 47 | position: absolute; 48 | left: 2.25em; 49 | top: .32em; 50 | content: "\2718"; 51 | font-variant: small-caps; 52 | font-size: .85em; 53 | text-align: center; 54 | color: #868171; 55 | } 56 | 57 | .toggle > input:checked ~ label::before { 58 | left: .2em; 59 | color: #fff; 60 | content: "\2714"; 61 | } 62 | 63 | .toggle > label::after { 64 | border-radius: inherit; 65 | background: #fff; 66 | content: ""; 67 | } 68 | 69 | .toggle > input:checked ~ label { 70 | background-color: #4cda64; 71 | } 72 | 73 | .toggle > input:checked ~ label::after { 74 | left: 1.73em; 75 | } 76 | 77 | .toggle > input:disabled ~ label { 78 | background-color: #d5d5d5; 79 | pointer-events: none; 80 | } 81 | 82 | .toggle > input:disabled ~ label::after { 83 | background-color: rgba(255,255,255,.3); 84 | } 85 | 86 | .toggle.red > input:checked ~ label { 87 | background-color: #e20b0b; 88 | } -------------------------------------------------------------------------------- /chrome/images/128x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/chrome/images/128x.png -------------------------------------------------------------------------------- /chrome/images/32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/chrome/images/32x.png -------------------------------------------------------------------------------- /chrome/images/48x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/chrome/images/48x.png -------------------------------------------------------------------------------- /chrome/images/bug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /chrome/images/color-scheme.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 25 | 28 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chrome/images/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/chrome/images/disabled.png -------------------------------------------------------------------------------- /chrome/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/chrome/images/icon.png -------------------------------------------------------------------------------- /chrome/images/observer.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /chrome/images/setup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 18 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /chrome/js/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | (function (w, u) { 10 | "use strict"; 11 | 12 | if (typeof browser === "undefined") { 13 | w.browser = chrome; 14 | } else if (!w.browser) { 15 | w.browser = browser; 16 | } 17 | 18 | var ignoreds, 19 | timeout, 20 | isHttpRE = /^https?:\/\/\w/i, 21 | isNewTabRE = /^(about:blank|chrome:\/+?(newtab|startpageshared)\/?)$/i, 22 | removeHashRE = /#[\s\S]+?$/, 23 | removeQueryRE = /\?[\s\S]+?$/, 24 | browser = w.browser, 25 | configs = { 26 | "turnoff": false, 27 | "old": true, 28 | "active": true, 29 | "start": true, 30 | "replace": true, 31 | "update": true, 32 | "create": true, 33 | "attach": true, 34 | "datachange": true, 35 | "http": true, 36 | "query": true, 37 | "hash": false, 38 | "incognito": false, 39 | "windows": true, 40 | "containers": true 41 | }; 42 | 43 | 44 | var legacyConfigs = Object.keys(configs); 45 | 46 | function checkTabs(type) { 47 | if (configs[type]) { 48 | browser.tabs.query(configs.windows ? { "lastFocusedWindow": true } : {}, preGetTabs); 49 | } 50 | } 51 | 52 | function preGetTabs(tabs) { 53 | if (timeout) clearTimeout(timeout); 54 | 55 | timeout = setTimeout(getTabs, 50, tabs); 56 | } 57 | 58 | function isIgnored(url) { 59 | return ignoreds && (ignoreds.urls.indexOf(url) !== -1 || ignoreds.hosts.indexOf(new URL(url).host) !== -1); 60 | } 61 | 62 | function isDisabled() { 63 | return configs.turnoff || ( 64 | !configs.start && 65 | !configs.replace && 66 | !configs.update && 67 | !configs.create && 68 | !configs.attach && 69 | !configs.datachange 70 | ); 71 | } 72 | 73 | function getTabs(tabs) { 74 | if (isDisabled()) return; 75 | 76 | var tab, 77 | url, 78 | groupTabs = {}, 79 | onlyHttp = configs.http, 80 | ignoreHash = !configs.hash, 81 | ignoreQuery = !configs.query, 82 | ignoreIncognitos = !configs.incognito, 83 | diffWindows = configs.windows, 84 | diffContainers = configs.containers; 85 | 86 | for (var i = tabs.length - 1; i >= 0; i--) { 87 | tab = tabs[i]; 88 | url = tab.url; 89 | 90 | if ( 91 | tab.pinned || 92 | url === "" || 93 | isNewTabRE.test(url) || 94 | (ignoreIncognitos && tab.incognito) || 95 | (onlyHttp && !isHttpRE.test(url)) || 96 | isIgnored(url) 97 | ) { 98 | continue; 99 | } 100 | 101 | if (ignoreHash) url = url.replace(removeHashRE, ""); 102 | 103 | if (ignoreQuery) url = url.replace(removeQueryRE, ""); 104 | 105 | var prefix; 106 | 107 | if (tab.incognito) { 108 | prefix = "incognito"; 109 | } else if (diffContainers && tab.cookieStoreId) { 110 | prefix = String(tab.cookieStoreId); 111 | } else { 112 | prefix = "normal"; 113 | } 114 | 115 | if (diffWindows) { 116 | url = prefix + "::" + tab.windowId + "::" + url; 117 | } else { 118 | url = prefix + "::" + url; 119 | } 120 | 121 | if (!groupTabs[url]) groupTabs[url] = []; 122 | 123 | groupTabs[url].push({ "id": tab.id, "actived": tab.active }); 124 | } 125 | 126 | for (var url in groupTabs) { 127 | closeTabs(groupTabs[url]); 128 | } 129 | 130 | groupTabs = tabs = null; 131 | } 132 | 133 | function sortTabs(tab, nextTab) { 134 | if (configs.active && (tab.actived || nextTab.actived)) { 135 | return tab.actived ? -1 : 1; 136 | } 137 | 138 | return configs.old && tab.id < nextTab.id ? 1 : -1; 139 | } 140 | 141 | function closeTabs(tabs) { 142 | var j = tabs.length; 143 | 144 | if (j < 2) return; 145 | 146 | tabs = tabs.sort(sortTabs); 147 | 148 | for (var i = 1; i < j; i++) { 149 | browser.tabs.remove(tabs[i].id, empty); 150 | } 151 | } 152 | 153 | function createEvent(type, timeout) { 154 | return function (tab) { 155 | setTimeout(checkTabs, timeout, type); 156 | setTimeout(toggleIgnoreIcon, 100, tab.id || tab.tabId || tab, tab.url); 157 | }; 158 | } 159 | 160 | function getConfigs() { 161 | return { 162 | "turnoff": getStorage("turnoff", configs.turnoff), 163 | "old": getStorage("old", configs.old), 164 | "active": getStorage("active", configs.active), 165 | "start": getStorage("start", configs.start), 166 | "replace": getStorage("replace", configs.replace), 167 | "update": getStorage("update", configs.update), 168 | "create": getStorage("create", configs.create), 169 | "attach": getStorage("attach", configs.attach), 170 | "datachange": getStorage("datachange", configs.datachange), 171 | "http": getStorage("http", configs.http), 172 | "query": getStorage("query", configs.query), 173 | "hash": getStorage("hash", configs.hash), 174 | "incognito": getStorage("incognito", configs.incognito), 175 | "windows": getStorage("windows", configs.windows), 176 | "containers": getStorage("containers", configs.containers) 177 | }; 178 | } 179 | 180 | function getExtraData() 181 | { 182 | var data = []; 183 | 184 | for (var key in localStorage) { 185 | if (key.indexOf("data:") === 0) { 186 | data.push({ 187 | "id": key.substr(5), 188 | "value": getStorage(key) 189 | }); 190 | } 191 | } 192 | 193 | return data; 194 | } 195 | 196 | function getIgnored() { 197 | var hosts = getStorage("hosts"), 198 | urls = getStorage("urls"); 199 | 200 | return ignoreds = { 201 | "urls": Array.isArray(urls) ? urls : [], 202 | "hosts": Array.isArray(hosts) ? hosts : [] 203 | }; 204 | } 205 | 206 | function toggleIgnoreData(type, ignore, value) { 207 | var changed = true, 208 | storage = type + "s", 209 | contents = getStorage(storage); 210 | 211 | if (!Array.isArray(contents)) contents = []; 212 | 213 | var index = contents.indexOf(value); 214 | 215 | if (ignore && index === -1) { 216 | contents.push(value); 217 | } else if (!ignore && index !== -1) { 218 | contents.splice(index, 1); 219 | } else { 220 | changed = false; 221 | } 222 | 223 | if (changed) { 224 | setStorage(storage, contents); 225 | ignoreds[storage] = contents; 226 | } 227 | 228 | contents = null; 229 | } 230 | 231 | function toggleIgnoreIcon(tab, url) { 232 | if (!url) { 233 | browser.tabs.get(tab, function (tab) { 234 | if (tab) { 235 | var url = tab.url || tab.pendingUrl; 236 | setTimeout(toggleIgnoreIcon, url ? 0 : 500, tab.id, url); 237 | } 238 | }); 239 | } else { 240 | var icon; 241 | 242 | if (isDisabled() || isIgnored(url)) { 243 | icon = "/images/disabled.png"; 244 | } else { 245 | icon = "/images/icon.png"; 246 | } 247 | 248 | browser.browserAction.setIcon({ 249 | "tabId": tab, 250 | "path": icon 251 | }); 252 | } 253 | } 254 | 255 | function updateCurrentIcon(tabs) { 256 | if (tabs && tabs[0]) toggleIgnoreIcon(tabs[0].id, tabs[0].url); 257 | } 258 | 259 | if (!getStorage("firstrun")) { 260 | for (var config in configs) { 261 | setStorage(config, configs[config]); 262 | } 263 | 264 | setStorage("firstrun", true); 265 | } else { 266 | configs = getConfigs(); 267 | } 268 | 269 | setTimeout(checkTabs, 100, "start"); 270 | setTimeout(getIgnored, 200); 271 | 272 | browser.tabs.onAttached.addListener(createEvent("attach", 500)); 273 | browser.tabs.onCreated.addListener(createEvent("create", 10)); 274 | browser.tabs.onReplaced.addListener(createEvent("replace", 10)); 275 | browser.tabs.onUpdated.addListener(createEvent("update", 10)); 276 | 277 | browser.tabs.onActivated.addListener(function (tab) { 278 | toggleIgnoreIcon(tab.tabId); 279 | }); 280 | 281 | browser.runtime.onMessage.addListener(function (request, sender, sendResponse) { 282 | if (request.ignore !== u) { 283 | toggleIgnoreData(request.type, request.ignore, request.value); 284 | toggleIgnoreIcon(request.tabId, request.url); 285 | } else if (request.setup) { 286 | configs[request.setup] = request.enable; 287 | setStorage(request.setup, request.enable); 288 | browser.tabs.query({ "active": true, "lastFocusedWindow": true }, updateCurrentIcon); 289 | } else if (request.data) { 290 | var key = "data:" + request.data; 291 | configs[key] = request.value; 292 | setStorage(key, request.value); 293 | } else if (request.extra) { 294 | sendResponse(getExtraData()); 295 | } else if (request.configs) { 296 | sendResponse(getConfigs()); 297 | } else if (request.ignored) { 298 | sendResponse(getIgnored()); 299 | } 300 | 301 | if (request.setup || request.ignore !== u) { 302 | if (configs.datachange) setTimeout(checkTabs, 10, "datachange"); 303 | } 304 | }); 305 | 306 | setTimeout(async function () { 307 | var store = {}; 308 | 309 | for (var i = 0, j = localStorage.length; i < j; i++) { 310 | var key = localStorage.key(i); 311 | 312 | if (key === "urls" || key.indexOf("data:") === 0 || legacyConfigs.includes(key)) { 313 | try { 314 | var item = JSON.parse(localStorage.getItem(key)); 315 | 316 | if (key === "urls" && Array.isArray(item.value)) { 317 | store[key] = item.value; 318 | } else if ("value" in item) { 319 | store[key] = item.value; 320 | } 321 | } catch (ee) {} 322 | } 323 | } 324 | 325 | await browser.storage.local.set(store); 326 | }, 1000); 327 | })(window); 328 | -------------------------------------------------------------------------------- /chrome/js/boot.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | function empty() {} 10 | 11 | function setStorage(key, value) { 12 | localStorage.setItem(key, JSON.stringify({ "value": value })); 13 | } 14 | 15 | function getStorage(key, fallback) { 16 | var value = localStorage[key]; 17 | 18 | if (!value || value[0] !== "{" || value.substr(-1) !== "}") { 19 | return fallback; 20 | } 21 | 22 | var current = JSON.parse(value); 23 | 24 | return current ? current.value : fallback; 25 | } 26 | 27 | function runtimeConnected() { 28 | return !!browser && browser.runtime && browser.runtime.sendMessage; 29 | } 30 | 31 | function sendMessage(message, callback) { 32 | if (runtimeConnected()) { 33 | browser.runtime.sendMessage(null, message, {}, callback || empty); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chrome/js/color-scheme.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | function setColorScheme(darkPrefer, userPrefer) { 10 | var disable, links = document.querySelectorAll("link[href*='-dark.css']"); 11 | 12 | switch (userPrefer || getStorage("data:color-scheme")) { 13 | case "dark": 14 | disable = false; 15 | break; 16 | case "light": 17 | disable = true; 18 | break; 19 | default: 20 | disable = !darkPrefer; 21 | } 22 | 23 | for (var i = links.length - 1; i >= 0; i--) { 24 | links[i].rel = disable ? "preload" : "stylesheet"; 25 | } 26 | } 27 | 28 | (function () { 29 | var media = window.matchMedia("(prefers-color-scheme: dark)"); 30 | 31 | if (!getStorage("data:color-scheme")) { 32 | setStorage("data:color-scheme", "default"); 33 | } 34 | 35 | setColorScheme(media.matches); 36 | 37 | document.addEventListener("change:data", function (e) { 38 | if (e.detail.data === "color-scheme") { 39 | setTimeout(setColorScheme, 100, media.matches, e.detail.value); 40 | } 41 | }); 42 | 43 | media.onchange = function (e) { 44 | setColorScheme(e.matches); 45 | }; 46 | })(); 47 | -------------------------------------------------------------------------------- /chrome/js/data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | (function (w, d) { 10 | "use strict"; 11 | 12 | if (typeof browser === "undefined") { 13 | w.browser = chrome; 14 | } else if (!w.browser) { 15 | w.browser = browser; 16 | } 17 | 18 | var tabId, 19 | sync = false, 20 | browser = w.browser, 21 | isHttpRE = /^https?:\/\/\w/i, 22 | variables = { 23 | "url": null, 24 | "host": null, 25 | "version": browser.runtime.getManifest().version 26 | }; 27 | 28 | function applyIgnoredData(hosts, urls) { 29 | var http = isHttpRE.test(variables.url); 30 | 31 | if (!http) return; 32 | 33 | var actions = d.getElementById("actions"), 34 | ignoreds = d.querySelectorAll("[data-ignored]"); 35 | 36 | actions.style.display = "block"; 37 | 38 | for (var i = ignoreds.length - 1; i >= 0; i--) { 39 | var el = ignoreds[i], data = el.dataset.ignored; 40 | 41 | if (data.indexOf("urls[") === 0) { 42 | if (urls.indexOf(variables.url) !== -1) el.classList.toggle("data-ignored", true); 43 | } else if (data.indexOf("!urls[") === 0) { 44 | if (urls.indexOf(variables.url) === -1) el.classList.toggle("data-ignored", true); 45 | } else if (data.indexOf("hosts[") === 0) { 46 | if (hosts.indexOf(variables.host) !== -1) el.classList.toggle("data-ignored", true); 47 | } else if (data.indexOf("!hosts[") === 0) { 48 | if (hosts.indexOf(variables.host) === -1) el.classList.toggle("data-ignored", true); 49 | } 50 | } 51 | 52 | applyEvents(); 53 | } 54 | 55 | function applyEvents() { 56 | var els = d.querySelectorAll("[data-ignored] .col:first-child > button"); 57 | 58 | for (var i = els.length - 1; i >= 0; i--) { 59 | els[i].addEventListener("click", addRemoveUrl); 60 | } 61 | } 62 | 63 | function addRemoveUrl(e) { 64 | var target = e.target; 65 | 66 | if (target && w.runtimeConnected()) { 67 | sync = true; 68 | 69 | var type = target.dataset.type, 70 | ignore = !target.closest("[data-ignored]").classList.contains("data-ignored"); 71 | 72 | sendMessage({ 73 | "type": type, 74 | "value": type === "url" ? variables.url : variables.host, 75 | "ignore": ignore, 76 | "tabId": tabId, 77 | "url": variables.url 78 | }); 79 | 80 | toggleIgnore(type, ignore); 81 | } 82 | } 83 | 84 | function toggleIgnore(type, ignore) { 85 | d.querySelector("[data-type='" + type + "']") 86 | .closest("[data-ignored]").classList 87 | .toggle("data-ignored", ignore); 88 | } 89 | 90 | function applyIgnoredVars(vars) { 91 | var query; 92 | 93 | if (vars) { 94 | query = "var[name='" + vars.join("'], var[name='") + "']"; 95 | } else { 96 | query = "var[name]"; 97 | } 98 | 99 | var vars = d.querySelectorAll(query); 100 | 101 | for (var i = vars.length - 1; i >= 0; i--) { 102 | var el = vars[i], key = el.getAttribute("name"), value = variables[key]; 103 | 104 | if (key && value) { 105 | el.textContent = value; 106 | el.removeAttribute("name"); 107 | } 108 | } 109 | } 110 | 111 | function containerConfigs(tab) { 112 | if (tab.cookieStoreId) { 113 | var configs = d.querySelectorAll(".support-containers"); 114 | 115 | for (var i = configs.length - 1; i >= 0; i--) { 116 | configs[i].classList.toggle("supported-containers", true); 117 | } 118 | } 119 | } 120 | 121 | sendMessage({ "ignored": true }, function (response) { 122 | if (response) { 123 | browser.tabs.query({ active: true, lastFocusedWindow: true }, function (tabs) { 124 | if (tabs[0]) { 125 | tabId = tabs[0].id; 126 | 127 | variables.url = tabs[0].url; 128 | variables.host = new URL(variables.url).host; 129 | 130 | applyIgnoredVars(["url", "host"]); 131 | applyIgnoredData(response.hosts, response.urls); 132 | 133 | containerConfigs(tabs[0]); 134 | } 135 | }); 136 | } 137 | }); 138 | 139 | browser.runtime.onMessage.addListener(function (request, sender, sendResponse) { 140 | if (!sync && request.type) toggleIgnore(request.type, request.ignore); 141 | 142 | sync = false; 143 | }); 144 | 145 | applyIgnoredVars(); 146 | })(window, document); 147 | -------------------------------------------------------------------------------- /chrome/js/popup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | (function (w, d) { 10 | "use strict"; 11 | 12 | if (typeof browser === "undefined") { 13 | w.browser = chrome; 14 | } else if (!w.browser) { 15 | w.browser = browser; 16 | } 17 | 18 | var debugMode = false, 19 | browser = w.browser, 20 | manifest = browser.runtime.getManifest(); 21 | 22 | if (browser.runtime.id && !("requestUpdateCheck" in browser.runtime)) { 23 | if (/@temporary-addon$/.test(browser.runtime.id)) debugMode = true; 24 | } else if (!("update_url" in manifest)) { 25 | debugMode = true; 26 | } 27 | 28 | function disableEvent(e) { 29 | e.preventDefault(); 30 | return false; 31 | } 32 | 33 | if (!debugMode) { 34 | d.oncontextmenu = disableEvent; 35 | d.ondragstart = disableEvent; 36 | } 37 | 38 | function markdown(message) { 39 | return message 40 | .replace(/(^|\s|[>])_(.*?)_($|\s|[<])/g, '$1$2<\/i>$3') 41 | .replace(/(^|\s|[>])`(.*?)`($|\s|[<])/g, '$1$2<\/code>$3') 42 | .replace(/\{([a-z])(\w+)?\}/gi, '<\/var>') 43 | .replace(/(^|\s|[>])\*(.*?)\*($|\s|[<])/g, '$1$2<\/strong>$3'); 44 | } 45 | 46 | var locales = d.querySelectorAll("[data-i18n]"); 47 | 48 | for (var i = locales.length - 1; i >= 0; i--) { 49 | var el = locales[i], message = browser.i18n.getMessage(el.dataset.i18n); 50 | 51 | if (message) el.innerHTML = markdown(message); 52 | } 53 | 54 | d.addEventListener("click", function (e) { 55 | if (e.button !== 0) return; 56 | 57 | var el = e.target; 58 | 59 | if (el.nodeName !== "A") { 60 | el = el.closest("a[href]"); 61 | 62 | if (!el) return; 63 | } 64 | 65 | var protocol = el.protocol; 66 | 67 | if (protocol === "http:" || protocol === "https:") { 68 | e.preventDefault(); 69 | 70 | browser.tabs.create({ "url": el.href }); 71 | } 72 | }); 73 | 74 | if (browser.extension && browser.extension.isAllowedIncognitoAccess) { 75 | var incognitoWarn = d.getElementById("incognito_warn"); 76 | 77 | browser.extension.isAllowedIncognitoAccess(function (allowed) { 78 | incognitoWarn.classList.toggle("hide", allowed === true); 79 | }); 80 | } 81 | 82 | var se = d.scrollingElement || d.body; 83 | 84 | setTimeout(function () { 85 | se.style.transform = "scale(2)"; 86 | 87 | setTimeout(function () { 88 | se.style.transform = "scale(1)"; 89 | 90 | setTimeout(function () { 91 | se.style.transform = null; 92 | }, 20); 93 | }, 20); 94 | }, 10); 95 | })(window, document); 96 | -------------------------------------------------------------------------------- /chrome/js/summary.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | (function (w, d, u) { 10 | "use strict"; 11 | 12 | function setEvent(widget, key) { 13 | widget.addEventListener("toggle", function () { 14 | setStorage(key, widget.open); 15 | }); 16 | } 17 | 18 | var details = d.querySelectorAll("details"); 19 | 20 | for (var i = details.length - 1; i >= 0; i--) { 21 | var widget = details[i], 22 | summary = widget.querySelector("[data-i18n]"), 23 | key = "details:" + summary.getAttribute("data-i18n"), 24 | stored = getStorage(key); 25 | 26 | if (typeof stored === "boolean") { 27 | widget.open = stored; 28 | } 29 | 30 | setEvent(widget, key); 31 | } 32 | })(window, document); 33 | -------------------------------------------------------------------------------- /chrome/js/toggle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Prevent Duplicate Tabs 3 | * Copyright (c) 2023 Guilherme Nascimento (brcontainer@yahoo.com.br) 4 | * Released under the MIT license 5 | * 6 | * https://github.com/brcontainer/prevent-duplicate-tabs 7 | */ 8 | 9 | (function (w, d, u) { 10 | "use strict"; 11 | 12 | if (typeof browser === "undefined") { 13 | w.browser = chrome; 14 | } else if (!w.browser) { 15 | w.browser = browser; 16 | } 17 | 18 | var sync = false, browser = w.browser; 19 | var dataChange = { "data": u, "value": u }; 20 | var dataEvent = new CustomEvent("change:data", { "detail": dataChange }); 21 | 22 | function changeSwitch(e) { 23 | if (runtimeConnected()) { 24 | sync = true; 25 | 26 | sendMessage({ "enable": this.checked, "setup": this.id }); 27 | } 28 | } 29 | 30 | function changeRadio(e) { 31 | if (runtimeConnected()) { 32 | sync = true; 33 | 34 | sendMessage({ "data": this.name, "value": this.value }); 35 | triggerEvent(this.name, this.value); 36 | } 37 | } 38 | 39 | function updateRadio(id, value, response, trigger) { 40 | var els = d.querySelectorAll("input[type=radio][name='" + id + "']"); 41 | 42 | for (var i = els.length - 1; i >= 0; i--) { 43 | var current = els[i]; 44 | 45 | current.disabled = false; 46 | 47 | if (current.value === value) current.checked = true; 48 | } 49 | } 50 | 51 | var timeoutEvent = 0; 52 | 53 | function triggerEvent(data, value) { 54 | clearTimeout(timeoutEvent); 55 | 56 | timeoutEvent = setTimeout(function () { 57 | dataChange.data = data; 58 | dataChange.value = value; 59 | 60 | d.dispatchEvent(dataEvent); 61 | }, 100); 62 | } 63 | 64 | sendMessage({ "configs": true }, function (response) { 65 | var current, toggles = d.querySelectorAll(".toggle input[type=checkbox]"); 66 | 67 | for (var i = toggles.length - 1; i >= 0; i--) { 68 | current = toggles[i]; 69 | current.checked = !!response[current.id]; 70 | current.disabled = false; 71 | } 72 | }); 73 | 74 | sendMessage({ "extra": true }, function (response) { 75 | for (var i = response.length - 1; i >= 0; i--) { 76 | updateRadio(response[i].id, response[i].value, response[i], false); 77 | } 78 | }); 79 | 80 | browser.runtime.onMessage.addListener(function (request, sender, sendResponse) { 81 | if (!sync) { 82 | if (request.setup) { 83 | d.getElementById(request.setup).checked = request.enable; 84 | } else if (request.data) { 85 | updateRadio(request.data, request.value, request, true); 86 | triggerEvent(request.data, request.value); 87 | } 88 | } 89 | 90 | sync = false; 91 | }); 92 | 93 | var toggles = d.querySelectorAll(".toggle input[type=checkbox]"); 94 | 95 | for (var i = toggles.length - 1; i >= 0; i--) { 96 | toggles[i].addEventListener("change", changeSwitch); 97 | } 98 | 99 | var radios = d.querySelectorAll(".radio input[type=radio]"); 100 | 101 | for (var i = radios.length - 1; i >= 0; i--) { 102 | radios[i].addEventListener("change", changeRadio); 103 | } 104 | })(window, document); 105 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Guilherme Nascimento", 3 | "name": "Prevent Duplicate Tabs", 4 | "description": "Prevent duplicate tabs", 5 | "version": "0.7.11", 6 | "manifest_version": 2, 7 | "default_locale": "en", 8 | "icons": { 9 | "16": "images/icon.png", 10 | "32": "images/32x.png", 11 | "48": "images/48x.png", 12 | "128": "images/128x.png" 13 | }, 14 | "browser_action": { 15 | "default_icon": "images/icon.png", 16 | "default_popup": "views/popup.html#popup" 17 | }, 18 | "options_ui": { 19 | "page": "views/popup.html#config" 20 | }, 21 | "background": { 22 | "scripts": [ 23 | "js/boot.js", 24 | "js/background.js" 25 | ] 26 | }, 27 | "permissions": [ 28 | "tabs", 29 | "storage" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /chrome/views/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prevent Duplicate Tabs 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |

30 | 31 |
32 |
33 |

34 |
35 |
36 |
37 | 41 |
42 |
43 |

44 | 45 | 46 |

47 |

48 | 49 | 50 |

51 |
52 |
53 | 54 |
55 |
56 | 60 |
61 |
62 |

63 | 64 | 65 |

66 |

67 | 68 | 69 |

70 |
71 |
72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 |

87 |

88 |
89 |
90 | 91 |
92 |
93 |
94 | 95 | 96 |
97 |
98 |
99 |

100 |

101 |
102 |
103 |
104 |
105 | 106 |
107 | 108 |
109 |
110 |
111 |
112 | 113 | 114 |
115 |
116 |
117 |

118 |

119 |
120 |
121 | 122 |
123 |
124 |
125 | 126 | 127 |
128 |
129 |
130 |

131 |

132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 | 140 |
141 |
142 |
143 |

144 |

145 |
146 |
147 |
148 |
149 | 150 |
151 | 152 |
153 |
154 |
155 |
156 | 157 | 158 |
159 |
160 |
161 |

162 |

163 | 164 |
165 |
166 |
167 |
168 |
169 | 170 | 171 |
172 |
173 |
174 |

175 |

176 |
177 |
178 |
179 |
180 |
181 | 182 | 183 |
184 |
185 |
186 |

187 |

188 |
189 |
190 |
191 |
192 | 193 |
194 | 195 |
196 |
197 |
198 |
199 | 200 | 201 |
202 |
203 |
204 |

205 |

206 |
207 |
208 | 209 |
210 |
211 |
212 | 213 | 214 |
215 |
216 |
217 |

218 |

219 |
220 |
221 | 222 |
223 |
224 |
225 | 226 | 227 |
228 |
229 |
230 |

231 |

232 |
233 |
234 | 235 |
236 |
237 |
238 | 239 | 240 |
241 |
242 |
243 |

244 |

245 |
246 |
247 | 248 |
249 |
250 |
251 | 252 | 253 |
254 |
255 |
256 |

257 |

258 |
259 |
260 | 261 |
262 |
263 |
264 | 265 | 266 |
267 |
268 |
269 |

270 |

271 |
272 |
273 |
274 |
275 | 276 |
277 | 278 |
279 |
280 | 287 | 294 | 301 |
302 |
303 |
304 | 305 | 312 |
313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /preview/chrome-ignore-host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/preview/chrome-ignore-host.png -------------------------------------------------------------------------------- /preview/chrome-prevent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/preview/chrome-prevent.png -------------------------------------------------------------------------------- /preview/firefox-ignore-host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/preview/firefox-ignore-host.png -------------------------------------------------------------------------------- /preview/firefox-prevent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/preview/firefox-prevent.png -------------------------------------------------------------------------------- /promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brcontainer/prevent-duplicate-tabs/27e80f5811a2034ae8173baa1bb299b1c1985bd8/promo.png --------------------------------------------------------------------------------