├── .gitignore ├── FAQ.md ├── Gruntfile.js ├── LICENSE ├── PRIVACY.md ├── README.md ├── package-lock.json ├── package.json ├── screenshots ├── 2.2.1 │ ├── basic │ │ ├── global-settings-tab.png │ │ ├── logo.jpg │ │ ├── proxies-tab.png │ │ ├── proxy-details.png │ │ └── proxy-settings.png │ └── standard │ │ ├── about-box.png │ │ ├── auto-add-tab.png │ │ ├── global-settings-tab.png │ │ ├── logging-tab.png │ │ ├── logo.jpg │ │ ├── pattern-template-reference-tooltip.png │ │ ├── proxies-tab.png │ │ ├── proxy-settings-details.png │ │ ├── proxy-settings-general.png │ │ ├── proxy-settings-url-patterns-add-edit-pattern-popup.png │ │ ├── proxy-settings-url-patterns.png │ │ ├── quick-add-popup.png │ │ ├── quick-add-tab.png │ │ ├── toolbar-icon-menu.png │ │ └── wildcard-reference-tooltip.png ├── 8.1 │ ├── dark-theme │ │ ├── 1280x800 │ │ │ ├── import-tab-1280x800.jpg │ │ │ ├── import-tab-1280x800.png │ │ │ ├── log-tab.jpg │ │ │ ├── log-tab.png │ │ │ ├── options-tab-1280x800.jpg │ │ │ ├── options-tab-1280x800.png │ │ │ ├── pattern-tester-tab-1280x800.jpg │ │ │ ├── pattern-tester-tab-1280x800.png │ │ │ ├── popup-1280x800.jpg │ │ │ ├── popup-1280x800.png │ │ │ ├── proxies-tab-1280x800.jpg │ │ │ └── proxies-tab-1280x800.png │ │ ├── import-tab.jpg │ │ ├── import-tab.png │ │ ├── log-tab.jpg │ │ ├── log-tab.png │ │ ├── options-tab.jpg │ │ ├── options-tab.png │ │ ├── pattern-tester-tab.jpg │ │ ├── pattern-tester-tab.png │ │ ├── popup.jpg │ │ ├── popup.png │ │ ├── proxies-tab.jpg │ │ └── proxies-tab.png │ ├── export-for-screenshots-2023-11-21.json │ └── light-theme │ │ ├── 1280x800 │ │ ├── import-tab.jpg │ │ ├── import-tab.png │ │ ├── log-tab.jpg │ │ ├── log-tab.png │ │ ├── options-tab.jpg │ │ ├── options-tab.png │ │ ├── pattern-tester-tab.jpg │ │ ├── pattern-tester-tab.png │ │ ├── popup.jpg │ │ ├── popup.png │ │ ├── proxies-tab.jpg │ │ └── proxies-tab.png │ │ ├── import-tab.jpg │ │ ├── import-tab.png │ │ ├── log-tab.jpg │ │ ├── log-tab.png │ │ ├── options-tab.jpg │ │ ├── options-tab.png │ │ ├── pattern-tester-tab.jpg │ │ ├── pattern-tester-tab.png │ │ ├── popup.jpg │ │ ├── popup.png │ │ ├── proxies-tab.jpg │ │ └── proxies-tab.png └── recommended.png └── src ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fa │ └── messages.json ├── fr │ └── messages.json ├── ja │ └── messages.json ├── pl │ └── messages.json ├── pt_BR │ └── messages.json ├── ru │ └── messages.json ├── uk │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── content ├── about.html ├── action.js ├── app.js ├── authentication.js ├── background.js ├── browsing-data.js ├── bulk-edit.js ├── color.js ├── commands.js ├── default.css ├── drag-drop.js ├── flag.js ├── get-location.js ├── help.html ├── i18n.js ├── iframe.css ├── import-account.js ├── import-export.js ├── import-list.js ├── import-older.js ├── import-url.js ├── incognito-access.js ├── location.js ├── log.css ├── log.js ├── menus.js ├── migrate.js ├── nav.css ├── nav.js ├── on-request.js ├── options-filter.js ├── options-popup.css ├── options-popup.js ├── options-proxies.js ├── options-show.css ├── options.css ├── options.html ├── options.js ├── pac.js ├── pattern.js ├── ping.js ├── popup-filter.js ├── popup.css ├── popup.html ├── popup.js ├── progress-bar.css ├── progress-bar.js ├── proxy.js ├── schema.json ├── show.js ├── spinner.css ├── spinner.js ├── sync.js ├── test.js ├── tester.css ├── tester.js ├── theme.css ├── theme.js ├── toggle-switch.css ├── toggle.js └── webrtc.js ├── image ├── beaker.svg ├── bin.svg ├── container.svg ├── ericjung.png ├── filter.svg ├── icon-off.png ├── icon-off.svg ├── icon.png ├── icon.svg ├── icon128.png ├── icon32.png ├── icon48.png ├── logo.svg ├── power.svg └── privateBrowsing.svg ├── manifest-basic.json └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | exports/ 2 | node_modules/ 3 | 4 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | **Why is there a [CryptoJS](https://code.google.com/archive/p/crypto-js/) library in [lib](https://github.com/foxyproxy/browser-extension/tree/main/src/lib)?** *(removed in v9.0)*
4 | The CryptoJS library was already included in [FoxyProxy 3.x](https://github.com/foxyproxy/Foxyproxy_Chrome/blob/f1bca1c50dfa30908c79a9ea477f31eda2abacf4/app/scripts/stored-credentials.js#L4) to encrypt user credentials. It is needed to migrate encrypted settings from the old version (which had no updates for many years, as you mentioned). It is not used to encrypt anything; only to decrypt old data when upgrading from version 3.x to 8.x.
5 | The library is not used in Firefox, and will be removed once users migrate to v8+. 6 | 7 |
8 | 💻 Settings disappeared after the upgrade to v8 9 | 10 | Using Firefox and you've lost all FoxyProxy settings? 11 | 12 | FoxyProxy Basic 8.0 was first released in Sep 2023 as a trial run since it had fewer users (26k on Chrome & Firefox). 13 | We waited for 2 months for any feedback & bug reports before releasing FoxyProxy Standard. 14 | Unfortunately, we didn't get any bug report about the data migration sync issue, otherwise we would have fixed it before releasing the standard version. 15 | FoxyProxy 8.2 went online on Dec 6th. 16 | Due to a bug in version 8.2, previous settings of some users were not migrated after the upgrade. 17 | Versions 8.3-8.6 created with fixes for the bugs immediately, but due to the AMO approval waiting time, version 8.6 came online on Dec 12th. 18 | 19 | Previous settings were not deleted and are recoverable. The following options are available if FoxyProxy updated from 7.* and you have encountered the update bug. 20 |
21 | 22 |
23 | Retrieve Settings and Keep version 8.2 24 | 25 | From [this comment](https://github.com/foxyproxy/browser-extension/issues/45#issuecomment-1838719332): 26 | 27 | ### Look for old data 28 | 29 | 1. Go to the FoxyProxy Options page 30 | 2. Open the Dev Tools (F12) 31 | 3. Go to the Console tab 32 | 4. Type the following and hit ENTER 33 | 34 | 35 | ### With Sync 36 | 37 | ```js 38 | browser.storage.sync.get().then(console.log) 39 | ``` 40 | 41 | If above has some data, then in the Console tab, type the following and hit ENTER 42 | 43 | ```js 44 | browser.storage.sync.get().then(pref => { 45 | const data = JSON.stringify(pref, null, 2); 46 | const blob = new Blob([data], {type: 'application/json'}); 47 | browser.downloads.download({ 48 | url: URL.createObjectURL(blob), 49 | filename: 'FoxyProxy_sync.json', 50 | saveAs: true, 51 | conflictAction: 'uniquify' 52 | }) 53 | .catch(() => {}); 54 | }); 55 | ``` 56 | 57 | ### Without Sync 58 | 59 | ```js 60 | browser.storage.local.get().then(console.log) 61 | ``` 62 | 63 | If above has some data, then in the Console tab, type the following and hit ENTER 64 | 65 | ```js 66 | browser.storage.local.get().then(pref => { 67 | const data = JSON.stringify(pref, null, 2); 68 | const blob = new Blob([data], {type: 'application/json'}); 69 | browser.downloads.download({ 70 | url: URL.createObjectURL(blob), 71 | filename: 'FoxyProxy_local.json', 72 | saveAs: true, 73 | conflictAction: 'uniquify' 74 | }) 75 | .catch(() => {}); 76 | }); 77 | ``` 78 | ### Import data 79 | 80 | 1. Go to **Import Tab -> Import from older versions** 81 | 2. Import the `FoxyProxy_sync.json` or `FoxyProxy_local.json` file that you have saved 82 | 3. Click SAVE to save the data 83 |
84 | 85 |
86 | Downgrade to 7.* 87 | 88 | Downgrade may retrieve old settings. 89 | 90 | 1. Download 7.5.1 (or older) from https://addons.mozilla.org/firefox/addon/foxyproxy-standard/versions/ 91 | 2. Click the file; firefox will ask you to install that addon. Confirm 92 | 3. Go to Firefox settings, addons (about:addons), FoxyProxy, check that it shows version 7.* 93 | 4. **Important**: On that same page, set "Allow automatic updates" to off 94 | 95 | The settings bug is expected to be fixed in the latest release. 96 | Check [About](https://foxyproxy.github.io/browser-extension/src/content/about.html) for more information. 97 | 98 |
99 | 100 | 101 | 102 | 103 | ### 📱 Firefox for Android 104 | 105 | Firefox for Android ignored disabling `extensions.update.enabled` (due to a [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1872169)). Therefore, installation of an older version from AMO will get updated. The bug is fixed in Firefox 123. 106 | 107 |
108 | Downgrade or Beta Installation 109 | 110 | 111 | - Download 7.5.1 (or older) from https://addons.mozilla.org/firefox/addon/foxyproxy-standard/versions/ 112 | - Make the file available to the Android device through [Android File Transfer](https://www.android.com/filetransfer/), adb, Android Studio, or a similar tool 113 | - Install [Firefox Nightly for Developers](https://play.google.com/store/apps/details?id=org.mozilla.fenix&hl=en&gl=US) on Android 114 | - Enable Debug Menu 115 | - Go to:` menu -> Settings -> About Firefox Nightly` 116 | - Tap a few times on the Firefox icon to enable debug menu 117 | - Navigate to: `about:config` 118 | - Find `xpinstall.signatures.required` 119 | - Toggle to `false` 120 | - Find (or add) `extensions.update.enabled` 121 | - Toggle to `false` 122 | - Install add-on from file 123 | - Go to: `menu -> Settings -> Advanced -> Install add-on from file` and select the `.zip` file you transferred to the android device 124 | - Check "Allow in private browsing" then "Okay, Got it" 125 | 126 | #### See also: 127 | - [Downgrade instructions](https://github.com/foxyproxy/browser-extension/issues/107) 128 | - [Beta instructions](https://github.com/foxyproxy/browser-extension#beta-installation-guide) 129 | 130 |
131 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | const target = grunt.option('target'); 3 | grunt.initConfig({ 4 | compress: { 5 | main: { 6 | options: { 7 | archive: `foxyproxy-${target}.zip` 8 | }, 9 | files: [ 10 | {expand: true, cwd: 'src/', src: ['**/*', '!.DS_Store', '!manifest-*', '!*.zip', '!screenshots/**'], dest: '/', filter: 'isFile'} 11 | ] 12 | } 13 | } 14 | }); 15 | 16 | if (!target) { 17 | grunt.fail.fatal('--target command-line arg expected to be one of: chrome-standard, chrome-basic, firefox-standard, firefox-basic. Example: grunt --target=chrome-standard'); 18 | } 19 | let manifestSuffix = target; 20 | if (target === 'chrome-standard') { 21 | manifestSuffix = 'chrome'; 22 | } 23 | else if (target === 'firefox-standard') { 24 | manifestSuffix = 'firefox'; 25 | } 26 | grunt.file.copy(`src/manifest-${manifestSuffix}.json`, 'src/manifest.json'); 27 | grunt.loadNpmTasks('grunt-contrib-compress'); 28 | grunt.registerTask('default', ['compress']); 29 | }; 30 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | ### FoxyProxy extension 4 | 5 | FoxyProxy extension is free and open source software. We care about user privacy, and we do not collect or share any user data. We do not advertise. We do not use web beacons, analytics software, cookies, or any other tracking methods. All user data usages are listed below and the [source code](https://github.com/foxyproxy/browser-extension) is available to examine for verification. 6 | 7 | ### Your Personal Information 8 | 9 | FoxyProxy extension does not collect any personal identifiable information (PII), visited sites, traffic, or any other usage. 10 | All user-entered data (such as proxy server IP addresses) are stored locally in the browser and not logged or sent anywhere else. 11 | Once the extension is uninstalled, all data is deleted by the browser. 12 | 13 | 14 | ### Authentication Data 15 | 16 | User-entered authentication data (proxy username & passwords) are sent only to the proxy servers the user has selected. 17 | 18 | ### Sync 19 | 20 | FoxyProxy extension allows you to synchronize options across devices. Syncing is optional and disabled by default. If enabled, transfer of your data is processed and handled by the browser and not by FoxyProxy. Data is sent by the browser from one FoxyProxy instance to the others. 21 | 22 | ### Log 23 | 24 | Log page displays the network connection details as it happens (live). Data displayed on this page is not stored or sent anywhere and is gone once the log page is closed or refreshed. 25 | 26 | ### Proxies: Get Location 27 | 28 | The optional 'Get Location' button is an extra feature to get the geolocation of proxy servers. It is only invoked if the user decides to use this feature. Proxy hostnames are sent to `getfoxyproxy.org` to get country and city information. That info is populated in the extension. No other data is sent or stored. 29 | 30 | ### Toolbar Popup: IP 31 | 32 | The optional 'IP' button on the toolbar popup is an extra feature which sends a request to the `getfoxyproxy.org` to display the current location of the network connection. It is often used to test that proxy servers are responding. No other data is sent or stored. It is only invoked if the user decides to use this feature. 33 | 34 | ### Toolbar Popup: Location 35 | 36 | The optional 'Location' button opens `getfoxyproxy.org/geoip/`in a new tab, which shows the status of the current network connection. Additionally, it can show if the real IP leaks via WebRTC. No other data is sent or stored. It is only invoked if the user decides to use this feature 37 | 38 | 39 | ### Contact 40 | 41 | If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us. 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-extension", 3 | "devDependencies": { 4 | "grunt": "1.6.1", 5 | "grunt-cli": "^1.4.3", 6 | "grunt-contrib-compress": "2.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /screenshots/2.2.1/basic/global-settings-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/basic/global-settings-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/basic/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/basic/logo.jpg -------------------------------------------------------------------------------- /screenshots/2.2.1/basic/proxies-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/basic/proxies-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/basic/proxy-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/basic/proxy-details.png -------------------------------------------------------------------------------- /screenshots/2.2.1/basic/proxy-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/basic/proxy-settings.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/about-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/about-box.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/auto-add-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/auto-add-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/global-settings-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/global-settings-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/logging-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/logging-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/logo.jpg -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/pattern-template-reference-tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/pattern-template-reference-tooltip.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/proxies-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/proxies-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/proxy-settings-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/proxy-settings-details.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/proxy-settings-general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/proxy-settings-general.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/proxy-settings-url-patterns-add-edit-pattern-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/proxy-settings-url-patterns-add-edit-pattern-popup.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/proxy-settings-url-patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/proxy-settings-url-patterns.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/quick-add-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/quick-add-popup.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/quick-add-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/quick-add-tab.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/toolbar-icon-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/toolbar-icon-menu.png -------------------------------------------------------------------------------- /screenshots/2.2.1/standard/wildcard-reference-tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/2.2.1/standard/wildcard-reference-tooltip.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/import-tab-1280x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/import-tab-1280x800.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/import-tab-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/import-tab-1280x800.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/log-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/log-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/log-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/log-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/options-tab-1280x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/options-tab-1280x800.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/options-tab-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/options-tab-1280x800.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/pattern-tester-tab-1280x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/pattern-tester-tab-1280x800.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/pattern-tester-tab-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/pattern-tester-tab-1280x800.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/popup-1280x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/popup-1280x800.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/popup-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/popup-1280x800.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/proxies-tab-1280x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/proxies-tab-1280x800.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/1280x800/proxies-tab-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/1280x800/proxies-tab-1280x800.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/import-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/import-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/import-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/import-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/log-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/log-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/log-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/log-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/options-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/options-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/options-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/options-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/pattern-tester-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/pattern-tester-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/pattern-tester-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/pattern-tester-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/popup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/popup.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/popup.png -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/proxies-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/proxies-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/dark-theme/proxies-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/dark-theme/proxies-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/export-for-screenshots-2023-11-21.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pattern", 3 | "sync": true, 4 | "proxyDNS": true, 5 | "passthrough": "ericjung.net\nreddit.com", 6 | "data": [ 7 | { 8 | "active": true, 9 | "title": "iceland server", 10 | "color": "#00bfff", 11 | "type": "http", 12 | "hostname": "ehj-5010.getfoxyproxy.org", 13 | "port": "2112", 14 | "username": "ericjung", 15 | "password": "aaaaaaaaaaa", 16 | "cc": "IS", 17 | "city": "Reykjavik", 18 | "include": [ 19 | { 20 | "type": "wildcard", 21 | "title": "eric jung", 22 | "pattern": "*://ericjung.net/*", 23 | "active": true 24 | }, 25 | { 26 | "type": "wildcard", 27 | "title": "check my ip", 28 | "pattern": "*://ip.me/*", 29 | "active": true 30 | }, 31 | { 32 | "type": "regex", 33 | "title": "", 34 | "pattern": "^https://whatismyipaddress\\\\.com/", 35 | "active": true 36 | } 37 | ], 38 | "exclude": [], 39 | "pac": "" 40 | }, 41 | { 42 | "active": true, 43 | "title": "india-901", 44 | "color": "#b22222", 45 | "type": "http", 46 | "hostname": "ehj-5011.getfoxyproxy.org", 47 | "port": "1337", 48 | "username": "", 49 | "password": "", 50 | "cc": "IN", 51 | "city": "Bangalore", 52 | "include": [ 53 | { 54 | "type": "regex", 55 | "title": "iplocation", 56 | "pattern": ".*://www.iplocation.net/.*", 57 | "active": true 58 | }, 59 | { 60 | "type": "regex", 61 | "title": "playwright automation", 62 | "pattern": "^https://playwright\\\\.dev/", 63 | "active": true 64 | } 65 | ], 66 | "exclude": [], 67 | "pac": "" 68 | }, 69 | { 70 | "active": true, 71 | "title": "TOR", 72 | "color": "#66cdaa", 73 | "type": "socks5", 74 | "hostname": "127.0.0.1", 75 | "port": "9050", 76 | "username": "", 77 | "password": "", 78 | "cc": "JP", 79 | "city": "", 80 | "include": [ 81 | { 82 | "type": "wildcard", 83 | "title": "", 84 | "pattern": "*.onion/*", 85 | "active": true 86 | } 87 | ], 88 | "exclude": [], 89 | "pac": "" 90 | }, 91 | { 92 | "active": true, 93 | "title": "polish torrent proxy", 94 | "color": "#b0e0e6", 95 | "type": "http", 96 | "hostname": "ehj-5014.getfoxyproxy.org", 97 | "port": "2112", 98 | "username": "ericjung", 99 | "password": "aaaaaaaaaaaaa", 100 | "cc": "PL", 101 | "city": "Warsaw", 102 | "include": [], 103 | "exclude": [], 104 | "pac": "" 105 | }, 106 | { 107 | "active": true, 108 | "title": "london dragonfly", 109 | "color": "#7b68ee", 110 | "type": "http", 111 | "hostname": "ehj-5098.getfoxyproxy.org", 112 | "port": "2112", 113 | "username": "ericjung", 114 | "password": "aaaaaaaaaaaaaaa", 115 | "cc": "GB", 116 | "city": "London", 117 | "include": [], 118 | "exclude": [], 119 | "pac": "" 120 | }, 121 | { 122 | "active": true, 123 | "title": "my fav Latvia server", 124 | "color": "#4b0082", 125 | "type": "http", 126 | "hostname": "ehj-5008.getfoxyproxy.org", 127 | "port": "2112", 128 | "username": "ericjung", 129 | "password": "aaaaaaaaaaaaaa", 130 | "cc": "LV", 131 | "city": "Riga", 132 | "include": [], 133 | "exclude": [], 134 | "pac": "" 135 | }, 136 | { 137 | "active": true, 138 | "title": "estonia 1999", 139 | "color": "#2f4f4f", 140 | "type": "http", 141 | "hostname": "ehj-5007.getfoxyproxy.org", 142 | "port": "2112", 143 | "username": "ericjung", 144 | "password": "aaaaaaaaaaaa", 145 | "cc": "EE", 146 | "city": "Tallinn", 147 | "include": [], 148 | "exclude": [], 149 | "pac": "" 150 | }, 151 | { 152 | "active": true, 153 | "title": "Auckland 3", 154 | "color": "#40e0d0", 155 | "type": "http", 156 | "hostname": "ehj-5006.getfoxyproxy.org", 157 | "port": "2112", 158 | "username": "ericjung", 159 | "password": "aaaaaaaaaaaaaa", 160 | "cc": "NZ", 161 | "city": "Auckland", 162 | "include": [], 163 | "exclude": [], 164 | "pac": "" 165 | }, 166 | { 167 | "active": true, 168 | "title": "Mexico", 169 | "color": "#d8bfd8", 170 | "type": "http", 171 | "hostname": "ehj-5005.getfoxyproxy.org", 172 | "port": "2112", 173 | "username": "ejung", 174 | "password": "aaaaaaaaaaaaaa", 175 | "cc": "MX", 176 | "city": "Mexico City", 177 | "include": [], 178 | "exclude": [], 179 | "pac": "" 180 | }, 181 | { 182 | "active": true, 183 | "title": "ablearcher", 184 | "color": "#6a5acd", 185 | "type": "http", 186 | "hostname": "ehj-5004.getfoxyproxy.org", 187 | "port": "2112", 188 | "username": "ericjung", 189 | "password": "aaaaaaaaaaaaaaa", 190 | "cc": "PY", 191 | "city": "Asuncion", 192 | "include": [], 193 | "exclude": [], 194 | "pac": "" 195 | }, 196 | { 197 | "active": true, 198 | "title": "mike's slovenia proxy", 199 | "color": "#ffb6c1", 200 | "type": "http", 201 | "hostname": "ehj-5000.getfoxyproxy.org", 202 | "port": "2112", 203 | "username": "ejung", 204 | "password": "aaaaaaaaaaaaa", 205 | "cc": "SI", 206 | "city": "Ljubljana", 207 | "include": [], 208 | "exclude": [], 209 | "pac": "" 210 | }, 211 | { 212 | "active": true, 213 | "title": "south africa 12", 214 | "color": "#d2691e", 215 | "type": "http", 216 | "hostname": "ehj-5001.getfoxyproxy.org", 217 | "port": "2112", 218 | "username": "ejung", 219 | "password": "aaaaaaaaaaaaa", 220 | "cc": "ZA", 221 | "city": "Johannesburg", 222 | "include": [], 223 | "exclude": [], 224 | "pac": "" 225 | }, 226 | { 227 | "active": true, 228 | "title": "ehj-509", 229 | "color": "#f5f5dc", 230 | "type": "http", 231 | "hostname": "ehj-5002.getfoxyproxy.org", 232 | "port": "2112", 233 | "username": "ejung", 234 | "password": "aaaaaaaaaaaaa", 235 | "cc": "ES", 236 | "city": "Madrid", 237 | "include": [], 238 | "exclude": [], 239 | "pac": "" 240 | }, 241 | { 242 | "active": true, 243 | "title": "ehj-89", 244 | "color": "#8b0000", 245 | "type": "http", 246 | "hostname": "ehj-5003.getfoxyproxy.org", 247 | "port": "2112", 248 | "username": "ejung", 249 | "password": "aaaaaaaaaaaaa", 250 | "cc": "US", 251 | "city": "New York", 252 | "include": [], 253 | "exclude": [], 254 | "pac": "" 255 | }, 256 | { 257 | "active": true, 258 | "title": "Privoxy", 259 | "color": "#a52a2a", 260 | "type": "http", 261 | "hostname": "127.0.0.1", 262 | "port": "8118", 263 | "username": "", 264 | "password": "", 265 | "cc": "", 266 | "city": "", 267 | "include": [], 268 | "exclude": [], 269 | "pac": "" 270 | } 271 | ], 272 | "container": { 273 | "incognito": "127.0.0.1:9050", 274 | "container-1": "ehj-5010.getfoxyproxy.org:2112", 275 | "container-2": "ehj-5001.getfoxyproxy.org:2112", 276 | "container-3": "ehj-5000.getfoxyproxy.org:2112", 277 | "container-4": "ehj-5014.getfoxyproxy.org:2112" 278 | }, 279 | "commands": { 280 | "setProxy": "ehj-5011.getfoxyproxy.org:1337", 281 | "setTabProxy": "ehj-5008.getfoxyproxy.org:2112", 282 | "quickAdd": "127.0.0.1:8118" 283 | } 284 | } -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/import-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/import-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/import-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/import-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/log-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/log-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/log-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/log-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/options-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/options-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/options-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/options-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/pattern-tester-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/pattern-tester-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/pattern-tester-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/pattern-tester-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/popup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/popup.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/popup.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/proxies-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/proxies-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/1280x800/proxies-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/1280x800/proxies-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/import-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/import-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/import-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/import-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/log-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/log-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/log-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/log-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/options-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/options-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/options-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/options-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/pattern-tester-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/pattern-tester-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/pattern-tester-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/pattern-tester-tab.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/popup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/popup.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/popup.png -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/proxies-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/proxies-tab.jpg -------------------------------------------------------------------------------- /screenshots/8.1/light-theme/proxies-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/8.1/light-theme/proxies-tab.png -------------------------------------------------------------------------------- /screenshots/recommended.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/screenshots/recommended.png -------------------------------------------------------------------------------- /src/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "Einfach zu bedienendes, fortschrittliches Proxy-Management-Tool für jedermann" }, 5 | 6 | "proxyByPatterns": { "message": "Proxy by Patterns" }, 7 | "disable": { "message": "Deaktivieren" }, 8 | "disabled": { "message": "Deaktiviert" }, 9 | "ip": { "message": "IP" }, 10 | "location": { "message": "Ort" }, 11 | 12 | "options": { "message": "Optionen" }, 13 | "proxy": { "message": "Proxy" }, 14 | "tester": { "message": "Tester" }, 15 | "log": { "message": "Log" }, 16 | "help": { "message": "Hilfe" }, 17 | "about": { "message": "Über" }, 18 | "import": { "message": "Import" }, 19 | "export": { "message": "Export" }, 20 | "save": { "message": "Speichern" }, 21 | "addHostTo": { "message": "Host Muster hinzufügen zu..." }, 22 | "incognitoAccess": { "message": "Proxy-Einstellungen erfordern die Erlaubnis zur Verwendung beim privaten Surfen." }, 23 | 24 | "enableSync": { "message": "Synchronisation einschalten" }, 25 | "enableSyncDescription": { "message": "Synchronisieren globaler Ausnahmen, Proxys & Muster" }, 26 | "syncError": { "message": "Synchronisationsfehler" }, 27 | 28 | "proxyDNS": { "message": "Proxy DNS" }, 29 | "proxyDnsDescription": { "message": "Verwenden Sie den Proxy-Server, um bestimmte DNS-Anfragen aufzulösen (nur für SOCKS in Firefox)" }, 30 | 31 | "globalExclude": { "message": "Globale Ausnahmen" }, 32 | "globalExcludeDescription": { "message": "Die hier eingegebenen Muster werden nicht weitergegeben." }, 33 | 34 | "deleteBrowserData": { "message": "Browserdaten löschen" }, 35 | "confirmDeleteBrowserData": { "message": "Sind Sie sicher, dass Sie Cookies, indexedDB-Speicher und lokalen DOM-Speicher löschen möchten?" }, 36 | "restoreDefaults": { "message": "Standardeinstellungen wiederherstellen" }, 37 | "restoreDefaultsConfirm": { "message": "Sind Sie sicher, dass Sie alle Einstellungen auf die Standardwerte zurücksetzen wollen?" }, 38 | "limitWebRTC": { "message": "WebRTC einschränken" }, 39 | "resetWebRTC": { "message": "WebRTC zurücksetzen" }, 40 | 41 | "foxyProxyAccount": { "message": "FoxyProxy Konto" }, 42 | "username": { "message": "Benutzername" }, 43 | "password": { "message": "Passwort" }, 44 | "togglePW": { "message": "Passwortsichtbarkeit umschalten" }, 45 | "importAs": { "message": "Importieren als" }, 46 | "userPassError": { "message": "Fehlender Benutzername oder Passwort" }, 47 | 48 | "proxyList": { "message": "Proxy Liste" }, 49 | "proxyTypeError": { "message": "Unbekannter Typ" }, 50 | "proxyPortError": { "message": "Fehlender Port" }, 51 | "proxyDuplicateError": { "message": "Doppelter Hostname:Port oder PAC URL" }, 52 | 53 | "fromURL": { "message": "Von URL" }, 54 | "fromOlderVersions": { "message": "Von alten Versionen" }, 55 | 56 | "getLocation": { "message": "Standort ermitteln" }, 57 | "add": { "message": "Hinzufügen" }, 58 | "title": { "message": "Titel" }, 59 | "type": { "message": "Typ" }, 60 | "color": { "message": "Farbe" }, 61 | "random": { "message": "Zufällig" }, 62 | "hostname": { "message": "Hostname" }, 63 | "port": { "message": "Port" }, 64 | "city": { "message": "Stadt" }, 65 | "country": { "message": "Land" }, 66 | 67 | "pacUrlError": { "message": "Fehlende PAC-URL für PAC-Typ" }, 68 | "hostnamePortError": { "message": "Fehlender Hostname - Port" }, 69 | 70 | "pattern": { "message": "Muster" }, 71 | "patterns": { "message": "Muster" }, 72 | "quickAdd": { "message": "Schnelles Hinzufügen" }, 73 | "include": { "message": "Einbeziehen" }, 74 | "exclude": { "message": "Ausschließen" }, 75 | "all": { "message": "Alle" }, 76 | "plainHost": { "message": "Einfacher Host" }, 77 | "wildcard": { "message": "Wildcard" }, 78 | "regexError": { "message": "Fehler beim Erstellen eines regulären Ausdrucks für ein Muster" }, 79 | 80 | "delete": { "message": "Löschen" }, 81 | "deleteConfirm": { "message": "Sind Sie sicher, dass Sie löschen wollen?" }, 82 | 83 | "result": { "message": "Ergebnis" }, 84 | "test": { "message": "Test" }, 85 | 86 | "documentURL": { "message": "Dokumenten-URL" }, 87 | "method": { "message": "Methode" }, 88 | "time": { "message": "Zeit" }, 89 | 90 | "error": { "message": "Bei dem Vorgang ist ein Fehler aufgetreten" }, 91 | "fileParseError": { "message": "Es gab einen Fehler beim Parsen der Datei" }, 92 | "fileReadError": { "message": "Es gab einen Fehler beim Lesen der Datei" }, 93 | "fileSizeError": { "message": "Die Dateigröße ist größer als die erlaubten $1kb" }, 94 | "fileTypeError": { "message": "Nicht unterstütztes Dateiformat" } 95 | } 96 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "Easy to use advanced Proxy Management tool for everyone" }, 5 | 6 | "proxyByPatterns": { "message": "Proxy by Patterns" }, 7 | "disable": { "message": "Disable" }, 8 | "ip": { "message": "IP" }, 9 | "location": { "message": "Location" }, 10 | "quickAdd": { "message": "Quick Add" }, 11 | "includeHost": { "message": "Include Host" }, 12 | "excludeHost": { "message": "Exclude Host" }, 13 | "tabProxy": { "message": "Tab Proxy" }, 14 | "more": { "message": "More" }, 15 | 16 | "setTabProxy": { "message": "Set Tab Proxy" }, 17 | "unsetTabProxy": { "message": "Unset Tab Proxy" }, 18 | "openLinkTabProxy": { "message": "Open Link in New Tab Proxy" }, 19 | 20 | "incognitoAccessError": { "message": "Firefox requires private browsing permission for proxy settings." }, 21 | "controlledByOtherExtensions": { "message": "Settings are controlled by other extensions." }, 22 | 23 | "enableSync": { "message": "Enable Sync" }, 24 | "enableSyncDescription": { "message": "Synchronise global exclude, proxies & patterns" }, 25 | "syncError": { "message": "Sync Error" }, 26 | "autoBackup": { "message": "Auto Backup" }, 27 | "autoBackupDescription": { "message": "Automatically backup settings to the Downloads folder on save" }, 28 | "theme": { "message": "Theme" }, 29 | "default": { "message": "Default" }, 30 | 31 | "globalExclude": { "message": "Global Exclude" }, 32 | "globalExcludeDescription": { "message": "List of hosts that should not be proxied" }, 33 | 34 | "container": { "message": "Container" }, 35 | "containerDescription": { "message": "Incognito and Container proxy (Firefox only)" }, 36 | "incognito": { "message": "Incognito" }, 37 | "addContainerPrompt": { "message": "Please enter the Container ID number." }, 38 | 39 | "shortcut": { "message": "Keyboard Shortcut" }, 40 | "shortcutDescription": { "message": "Preselect a proxy to associate with a keyboard shortcut action" }, 41 | "setProxy": { "message": "Set Proxy" }, 42 | 43 | 44 | "deleteBrowsingData": { "message": "Delete Browsing Data" }, 45 | "deleteBrowsingDataConfirm": { "message": "Are you sure you want to delete cookies, indexedDB storage, DOM local storage?" }, 46 | "restoreDefaults": { "message": "Restore Defaults" }, 47 | "restoreDefaultsConfirm": { "message": "Are you sure you want to restore all settings to Defaults?" }, 48 | 49 | "limitWebRTC": { "message": "Limit WebRTC" }, 50 | "limitWebRTCDescription": { "message": "Change WebRTC browser settings" }, 51 | "publicInterfaceOnly": { "message": "Public Interface Only" }, 52 | "publicPrivateInterfaces": { "message": "Public and Private Interfaces" }, 53 | "disableNonProxied": { "message": "Disable Non-Proxied" }, 54 | "proxyOnly": { "message": "Proxy Only" }, 55 | 56 | "proxies": { "message": "Proxies" }, 57 | "add": { "message": "Add" }, 58 | "getLocation": { "message": "Get Location" }, 59 | "ping": { "message": "Ping" }, 60 | 61 | "duplicate": { "message": "Duplicate" }, 62 | 63 | "title": { "message": "Title" }, 64 | "type": { "message": "Type" }, 65 | "server": { "message": "Server" }, 66 | "username": { "message": "Username" }, 67 | "password": { "message": "Password" }, 68 | "togglePassword": { "message": "Toggle Password" }, 69 | "userPassError": { "message": "Missing Username or Password" }, 70 | "view": { "message": "View" }, 71 | "color": { "message": "Color" }, 72 | "random": { "message": "Random" }, 73 | "hostname": { "message": "Hostname" }, 74 | "port": { "message": "Port" }, 75 | "hostnamePortError": { "message": "Missing Hostname - Port" }, 76 | "city": { "message": "City" }, 77 | "country": { "message": "Country" }, 78 | "proxyDNS": { "message": "Proxy DNS" }, 79 | "storeLocally": { "message": "Store Locally" }, 80 | 81 | "pattern": { "message": "Pattern" }, 82 | "patterns": { "message": "Patterns" }, 83 | "include": { "message": "Include" }, 84 | "exclude": { "message": "Exclude" }, 85 | "all": { "message": "All" }, 86 | "plainHost": { "message": "Plain Host" }, 87 | "wildcard": { "message": "Wildcard" }, 88 | "regexError": { "message": "Error creating RegExp for pattern" }, 89 | 90 | "delete": { "message": "Delete" }, 91 | "deleteConfirm": { "message": "Are you sure you want to delete?" }, 92 | 93 | "bulkEdit": { "message": "Bulk Edit" }, 94 | "openAll": { "message": "Open All" }, 95 | "closeAll": { "message": "Close All" }, 96 | "setType": { "message": "Set Type" }, 97 | "setPort": { "message": "Set Port" }, 98 | "setTitle": { "message": "Set Title" }, 99 | "setUsername": { "message": "Set Username" }, 100 | "setPassword": { "message": "Set Password" }, 101 | "moveProxy": { "message": "Move Proxy" }, 102 | "movePattern": { "message": "Move Pattern" }, 103 | "deleteProxy": { "message": "Delete Proxy" }, 104 | 105 | 106 | "importFoxyProxyAccount": { "message": "Import FoxyProxy Account" }, 107 | "mainServer": { "message": "Main Server" }, 108 | "altServer": { "message": "Alt Server" }, 109 | 110 | 111 | "importProxyList": { "message": "Import Proxy List" }, 112 | "importFromURL": { "message": "Import from URL" }, 113 | "importFromOlderVersions": { "message": "Import from older versions" }, 114 | 115 | "tester": { "message": "Pattern Tester" }, 116 | "testerDescription": { "message": "Test a pattern against URLs" }, 117 | "test": { "message": "Test" }, 118 | "result": { "message": "Result" }, 119 | "back": { "message": "Back" }, 120 | "invalidPatternError": { "message": "Invalid Pattern" }, 121 | "schemeError": { "message": "'*' in scheme must be the only character" }, 122 | "unsupportedSchemeError": { "message": "Unsupported scheme" }, 123 | "hostError": { "message": "'*' in host must be at the start" }, 124 | "hostDotError": { "message": "'*' in host must be the only character or be followed by '.'" }, 125 | "portError": { "message": "Host must not include a port number" }, 126 | "proxyByPatternsDescription": { "message": "Test a URL against active proxy patterns" }, 127 | 128 | "log": { "message": "Log" }, 129 | "documentUrl": { "message": "Document URL" }, 130 | "url": { "message": "URL" }, 131 | "method": { "message": "Method" }, 132 | "proxy": { "message": "Proxy" }, 133 | "time": { "message": "Time" }, 134 | "notAvailable": { "message": "Proxy data is not available on Chrome (Firefox Only)" }, 135 | "getAssociatedDomains": { "message": "Get Associated Domains" }, 136 | 137 | "about": { "message": "About" }, 138 | "error": { "message": "There was an error with the operation" }, 139 | "export": { "message": "Export" }, 140 | "fileParseError": { "message": "There was an error parsing the file" }, 141 | "fileReadError": { "message": "There was an error with reading the file" }, 142 | "fileTypeError": { "message": "Unsupported File Format" }, 143 | "help": { "message": "Help" }, 144 | "import": { "message": "Import" }, 145 | "options": { "message": "Options" }, 146 | "saveOptions": { "message": "Save" } 147 | } -------------------------------------------------------------------------------- /src/_locales/fa/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "ابزاری آسان و پیشرفتهٔ مدیریت پیشکار برای همه" }, 5 | 6 | 7 | "proxyByPatterns": { "message": "پیشکار بر اساس الگو‌ها" }, 8 | "disable": { "message": "از کار انداختن" }, 9 | "ip": { "message": "آی‌پی" }, 10 | "location": { "message": "مکان" }, 11 | 12 | "options": { "message": "گزینه‌ها" }, 13 | "proxy": { "message": "پیشکار" }, 14 | "tester": { "message": "آزمایشگر" }, 15 | "log": { "message": "گزارشات" }, 16 | "help": { "message": "راهنما" }, 17 | "about": { "message": "درباره" }, 18 | "import": { "message": "درون‌ریزی" }, 19 | "export": { "message": "برون‌ریزی" }, 20 | "save": { "message": "ذخیره" }, 21 | 22 | "enableSync": { "message": "به کار انداختن همگام‌سازی" }, 23 | "enableSyncDescription": { "message": "همگام‌سازی استثناهای عمومی، پیشکارها و الگوها" }, 24 | "syncError": { "message": "خطای همگام‌سازی" }, 25 | 26 | "globalExclude": { "message": "استثناهای عمومی" }, 27 | "globalExcludeDescription": { "message": "الگوهایی که اینجا وارد می‌کنید، از پیشکار رد نخواهند شد." }, 28 | "globalExcludeError": { "message": "خطای الگوی استثناهای عمومی" }, 29 | 30 | "deleteBrowserData": { "message": "حذف دادهٔ مرورگر" }, 31 | "confirmDeleteBrowserData": { "message": "آیا مطمئنید که می‌خواهید کوکی‌ها، فضای ذخیره‌سازی indexedDB و فضای ذخیره‌سازی محلی DOM را حذف کنید؟" }, 32 | "restoreDefaults": { "message": "بازگردانی به پیش‌گزیده‌ها" }, 33 | "restoreDefaultsConfirm": { "message": "آیا مطمئنید که می‌خواهید همه تنظیمات را به حالت پیش‌گزیده بازگردانید؟" }, 34 | "limitWebRTC": { "message": "محدود کردن WebRTC" }, 35 | "resetWebRTC": { "message": "بازنشانی WebRTC" }, 36 | 37 | 38 | "foxyProxyAccount": { "message": "حساب فاکسی پروکسی" }, 39 | "username": { "message": "نام کاربری" }, 40 | "password": { "message": "گذرواژه" }, 41 | "togglePW": { "message": "تغییر حالت گذرواژه" }, 42 | "importAs": { "message": "درون‌ریزی به عنوان" }, 43 | "domain": { "message": "دامنه" }, 44 | "userPassError": { "message": "لطفاً نام کاربری و گذرواژه را تکمیل کنید." }, 45 | 46 | "proxyList": { "message": "فهرست پیشکار" }, 47 | "proxyTypeError": { "message": "گونهٔ ناشناخته" }, 48 | "proxyPortError": { "message": "درگاه مشخص نشده" }, 49 | "regexError": { "message": "خطا در ایجاد عبارت باقاعده برای الگو" }, 50 | 51 | "addHostTo": { "message": "افزودن الگوی میزبان به ..." }, 52 | 53 | "incognitoAccess": { "message": "تنظیمات پیشکار به مجوز مرور خصوصی نیاز دارد." }, 54 | 55 | 56 | 57 | 58 | "fromOlderVersions": { "message": "از نگارش‌های قدیمی‌تر" }, 59 | 60 | "getLocation": { "message": "دریافت مکان" }, 61 | "add": { "message": "افزودن" }, 62 | "title": { "message": "عنوان" }, 63 | "type": { "message": "گونه" }, 64 | "color": { "message": "رنگ" }, 65 | "random": { "message": "تصادفی" }, 66 | "hostname": { "message": "نام میزبان" }, 67 | "port": { "message": "درگاه" }, 68 | "city": { "message": "شهر" }, 69 | "country": { "message": "کشور" }, 70 | "proxyDNS": { "message": "پیشکار ساناد (فقط ساکس۵)" }, 71 | "pacUrlError": { "message": "نشانی PAC برای گونهٔ PAC مشخص نشده" }, 72 | "hostnamePortError": { "message": "نام میزبان یا درگاه مشخص نشده" }, 73 | 74 | "pattern": { "message": "الگو" }, 75 | "patterns": { "message": "الگوها" }, 76 | "quickAdd": { "message": "افزودن سریع" }, 77 | "include": { "message": "شامل" }, 78 | "exclude": { "message": "بجز" }, 79 | "all": { "message": "همه" }, 80 | "plainHost": { "message": "میزبان ساده" }, 81 | "wildcard": { "message": "نویسهٔ همگانی" }, 82 | "patternError": { "message": "خطای الگو" }, 83 | 84 | 85 | "delete": { "message": "حذف" }, 86 | "deleteConfirm": { "message": "آیا مطمئنید که می‌خواهید حذفش کنید؟" }, 87 | 88 | "result": { "message": "نتیجه" }, 89 | "test": { "message": "آزمودن" }, 90 | 91 | "documentURL": { "message": "نشانی مستندات" }, 92 | "method": { "message": "شیوه" }, 93 | "time": { "message": "زمان" }, 94 | 95 | "selectCountry": { "message": "گزینش کشور" }, 96 | "cancel": { "message": "لغو" }, 97 | "ok": { "message": "قبول" }, 98 | 99 | "error": { "message": "هنگام انجام عملیات خطایی رخ داد" }, 100 | "fileParseError": { "message": "هنگام تجزیهٔ پرونده خطایی رخ داد" }, 101 | "fileReadError": { "message": "هنگام خواندن پرونده خطایی رخ داد" }, 102 | "fileSizeError": { "message": "پرونده با اندازهٔ بزرگتر از 1$ کیلوبایت مجاز نیست" }, 103 | "fileTypeError": { "message": "قالب پرونده پشتیبانی نشده" } 104 | } 105 | -------------------------------------------------------------------------------- /src/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "誰でも使いやすい先進的なプロキシー管理ツール" }, 5 | 6 | "proxyByPatterns": { "message": "パターンによるプロキシー" }, 7 | "disable": { "message": "無効" }, 8 | "ip": { "message": "IP" }, 9 | "location": { "message": "位置情報" }, 10 | "quickAdd": { "message": "クイック追加" }, 11 | "excludeHost": { "message": "ホストを除外" }, 12 | "setTabProxy": { "message": "タブプロキシーを設定" }, 13 | "unsetTabProxy": { "message": "タブプロキシーを解除" }, 14 | "more": { "message": "詳細" }, 15 | 16 | "incognitoAccessError": { "message": "Firefox でプロキシーを設定するためにはプライベートブラウジングでの許可設定が必要です" }, 17 | "controlledByOtherExtensions": { "message": "他の拡張機能によって設定が制御されています" }, 18 | 19 | "enableSync": { "message": "同期を有効化" }, 20 | "enableSyncDescription": { "message": "グローバル除外、プロキシー、パターンを同期する" }, 21 | "syncError": { "message": "同期エラー" }, 22 | "autoBackup": { "message": "自動バックアップ" }, 23 | "autoBackupDescription": { "message": "保存時に設定をダウンロードフォルダーに自動でバックアップする" }, 24 | "theme": { "message": "テーマ" }, 25 | "default": { "message": "デフォルト" }, 26 | 27 | "globalExclude": { "message": "グローバル除外" }, 28 | "globalExcludeDescription": { "message": "プロキシーを使用しないホストのリスト" }, 29 | 30 | "container": { "message": "コンテナー" }, 31 | "containerDescription": { "message": "Incognito/コンテナープロキシー (Firefox のみ)" }, 32 | "incognito": { "message": "Incognito" }, 33 | 34 | "shortcut": { "message": "キーボードショートカット" }, 35 | "shortcutDescription": { "message": "キーボードショートカットアクションに関連付けるプロキシーをあらかじめ選択する" }, 36 | "setProxy": { "message": "プロキシーを設定" }, 37 | 38 | 39 | "deleteBrowsingData": { "message": "閲覧データを削除" }, 40 | "deleteBrowsingDataConfirm": { "message": "本当に Cookie、indexedDB ストレージ、DOM ローカルストレージを削除しますか?" }, 41 | "restoreDefaults": { "message": "規定値を復元" }, 42 | "restoreDefaultsConfirm": { "message": "本当に全ての設定を規定値に復元しますか?" }, 43 | "limitWebRTC": { "message": "WebRTC を制限" }, 44 | "limitWebRTCDescription": { "message": "WebRTC ブラウザー設定を切り替える" }, 45 | 46 | "proxies": { "message": "プロキシー" }, 47 | "add": { "message": "追加" }, 48 | "getLocation": { "message": "位置情報を取得" }, 49 | 50 | "duplicate": { "message": "複製" }, 51 | 52 | "title": { "message": "タイトル" }, 53 | "type": { "message": "種類" }, 54 | "color": { "message": "色" }, 55 | "random": { "message": "ランダム" }, 56 | "hostname": { "message": "ホスト名" }, 57 | "port": { "message": "ポート" }, 58 | "city": { "message": "市区町村" }, 59 | "country": { "message": "国" }, 60 | "proxyDNS": { "message": "DNS でプロキシーを使用" }, 61 | "hostnamePortError": { "message": "ホスト名 - ポートが入力されていません" }, 62 | 63 | "pattern": { "message": "パターン" }, 64 | "patterns": { "message": "パターン" }, 65 | "include": { "message": "含む" }, 66 | "exclude": { "message": "除く" }, 67 | "all": { "message": "全て" }, 68 | "plainHost": { "message": "プレーンホスト" }, 69 | "wildcard": { "message": "ワイルドカード" }, 70 | "regexError": { "message": "パターンの正規表現を作成中にエラーが発生しました" }, 71 | 72 | "delete": { "message": "削除" }, 73 | "deleteConfirm": { "message": "本当に削除しますか?" }, 74 | 75 | "importFoxyProxyAccount": { "message": "FoxyProxy アカウントからインポート" }, 76 | "username": { "message": "ユーザー名" }, 77 | "password": { "message": "パスワード" }, 78 | "togglePassword": { "message": "パスワードを切り替え" }, 79 | "userPassError": { "message": "ユーザー名もしくはパスワードが入力されていません" }, 80 | "storeLocally": { "message": "ローカルに保存" }, 81 | "view": { "message": "閲覧" }, 82 | 83 | "importProxyList": { "message": "プロキシーリストをインポート" }, 84 | "importFromURL": { "message": "URL からインポート" }, 85 | "importFromOlderVersions": { "message": "以前のバージョンからインポート" }, 86 | 87 | "tester": { "message": "パターンテスター" }, 88 | "test": { "message": "テスト" }, 89 | "result": { "message": "結果" }, 90 | "back": { "message": "戻る" }, 91 | 92 | "log": { "message": "ログ" }, 93 | "documentUrl": { "message": "ドキュメント URL" }, 94 | "url": { "message": "URL" }, 95 | "method": { "message": "メソッド" }, 96 | "proxy": { "message": "プロキシー" }, 97 | "time": { "message": "時刻" }, 98 | "notAvailable": { "message": "Chrome では利用できません (Firefox のみ)" }, 99 | 100 | "about": { "message": "About" }, 101 | "error": { "message": "操作中にエラーが発生しました" }, 102 | "export": { "message": "エクスポート" }, 103 | "fileParseError": { "message": "ファイルの解析中にエラーが発生しました" }, 104 | "fileReadError": { "message": "ファイルの読み取り中にエラーが発生しました" }, 105 | "fileTypeError": { "message": "サポートされていないファイルフォーマットです" }, 106 | "help": { "message": "ヘルプ" }, 107 | "import": { "message": "インポート" }, 108 | "options": { "message": "オプション" }, 109 | "saveOptions": { "message": "保存" } 110 | } -------------------------------------------------------------------------------- /src/_locales/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "Łatwe w użyciu zaawansowane narzędzie do zarządzania proxy dla każdego" }, 5 | 6 | 7 | "proxyByPatterns": { "message": "Proxy według wzorców" }, 8 | "disable": { "message": "Wyłącz" }, 9 | "ip": { "message": "IP" }, 10 | "location": { "message": "Lokalizacja" }, 11 | 12 | "options": { "message": "Opcje" }, 13 | "proxy": { "message": "Proxy" }, 14 | "tester": { "message": "Tester" }, 15 | "log": { "message": "Dziennik" }, 16 | "help": { "message": "Pomoc" }, 17 | "about": { "message": "O aplikacji" }, 18 | "import": { "message": "Importuj" }, 19 | "export": { "message": "Eksportuj" }, 20 | "save": { "message": "Zapisz" }, 21 | 22 | "enableSync": { "message": "Włącz synchronizację" }, 23 | "enableSyncDescription": { "message": "Synchronizacja globalnych wykluczeń, proxy i wzorców" }, 24 | "syncError": { "message": "Błąd synchronizacji" }, 25 | 26 | "globalExclude": { "message": "Globalne wykluczenia" }, 27 | "globalExcludeDescription": { "message": "Wzorce wprowadzone tutaj nigdy nie będą pośredniczyć" }, 28 | "globalExcludeError": { "message": "Błąd globalnego wzorca wykluczeń" }, 29 | 30 | "deleteBrowserData": { "message": "Usuń dane przeglądarki" }, 31 | "confirmDeleteBrowserData": { "message": "Czy na pewno chcesz usunąć ciasteczka, magazyn indexedDB, lokalny magazyn DOM?" }, 32 | "restoreDefaults": { "message": "Przywróć ustawienia domyślne" }, 33 | "restoreDefaultsConfirm": { "message": "Czy na pewno chcesz przywrócić wszystkie ustawienia do wartości domyślnych?" }, 34 | "limitWebRTC": { "message": "Ogranicz WebRTC" }, 35 | "resetWebRTC": { "message": "Zresetuj WebRTC" }, 36 | 37 | 38 | "foxyProxyAccount": { "message": "Konto FoxyProxy" }, 39 | "username": { "message": "Nazwa użytkownika" }, 40 | "password": { "message": "Hasło" }, 41 | "togglePW": { "message": "Przełącz hasło" }, 42 | "importAs": { "message": "Zaimportuj jako" }, 43 | "domain": { "message": "Domena" }, 44 | "userPassError": { "message": "Proszę wpisać nazwę użytkownika oraz hasło." }, 45 | 46 | "proxyList": { "message": "Lista Proxy" }, 47 | "proxyTypeError": { "message": "Nieznany typ" }, 48 | "proxyPortError": { "message": "Brak portu" }, 49 | "regexError": { "message": "Błąd tworzenia regexp dla wzorca" }, 50 | 51 | "addHostTo": { "message": "Dodaj wzorzec hosta do ..." }, 52 | 53 | "incognitoAccess": { "message": "Ustawienia proxy wymagają zezwolenia do trybu prywatnego." }, 54 | 55 | 56 | 57 | 58 | "fromOlderVersions": { "message": "Ze starszych wersji" }, 59 | 60 | "getLocation": { "message": "Uzyskaj lokalizację" }, 61 | "add": { "message": "Dodaj" }, 62 | "title": { "message": "Nazwa" }, 63 | "type": { "message": "Typ" }, 64 | "color": { "message": "Kolor" }, 65 | "random": { "message": "Losowo" }, 66 | "hostname": { "message": "Nazwa hosta" }, 67 | "port": { "message": "Port" }, 68 | "city": { "message": "Miasto" }, 69 | "country": { "message": "Kraj" }, 70 | "proxyDNS": { "message": "Proxy DNS (tylko SOCKS 5)" }, 71 | "pacUrlError": { "message": "Brak adresu URl dla typu PAC" }, 72 | "hostnamePortError": { "message": "Brak nazwy hosta - portu" }, 73 | 74 | "pattern": { "message": "Wzorzec" }, 75 | "quickAdd": { "message": "Szybko dodaj" }, 76 | "include": { "message": "Uwzględnij" }, 77 | "exclude": { "message": "Wyklucz" }, 78 | "all": { "message": "Wszystko" }, 79 | "plainHost": { "message": "Zwykły host" }, 80 | "wildcard": { "message": "Wildcard" }, 81 | "patternError": { "message": "Błąd wzorca" }, 82 | 83 | 84 | "delete": { "message": "Usuń" }, 85 | "deleteConfirm": { "message": "Czy na pewno chcesz usunąć?" }, 86 | 87 | "result": { "message": "Wynik" }, 88 | "test": { "message": "Testuj" }, 89 | 90 | "documentURL": { "message": "URL dokumentu" }, 91 | "method": { "message": "Metoda" }, 92 | "time": { "message": "Czas" }, 93 | 94 | "selectCountry": { "message": "Wybierz kraj" }, 95 | "cancel": { "message": "Anuluj" }, 96 | "ok": { "message": "OK" }, 97 | 98 | "error": { "message": "Wystąpił błąd podczas realizacji zadania." }, 99 | "fileParseError": { "message": "Wystąpił błąd podczas przetwarzania pliku" }, 100 | "fileReadError": { "message": "Wystąpił błąd w odczycie pliku" }, 101 | "fileSizeError": { "message": "Rozmiar pliku jest większy niż dozwolony $1kb" }, 102 | "fileTypeError": { "message": "Nieobsługiwany format pliku" } 103 | } 104 | -------------------------------------------------------------------------------- /src/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "Простой инструмент управления прокси" }, 5 | 6 | "proxyByPatterns": { "message": "Прокси из шаблона" }, 7 | "disable": { "message": "Отключить" }, 8 | "ip": { "message": "IP" }, 9 | "location": { "message": "Расположение" }, 10 | "quickAdd": { "message": "Быстрое добавление" }, 11 | "excludeHost": { "message": "Исключить хост" }, 12 | "setTabProxy": { "message": "Установить прокси вкладки" }, 13 | "unsetTabProxy": { "message": "Отключить прокси вкладки" }, 14 | "more": { "message": "Больше" }, 15 | 16 | "incognitoAccessError": { "message": "Firefox требует разрешения на приватный просмотр для настроек прокси." }, 17 | "controlledByOtherExtensions": { "message": "Настройки контролируются другими расширениями." }, 18 | 19 | "enableSync": { "message": "Включить синхронизацию" }, 20 | "enableSyncDescription": { "message": "Синхронизировать исключения, список прокси и шаблоны" }, 21 | "syncError": { "message": "Ошибка синхронизации" }, 22 | "autoBackup": { "message": "Автоматическое резервное копирование" }, 23 | "autoBackupDescription": { "message": "Автоматическое резервное копирование настроек в папку «Загрузки» при сохранении." }, 24 | "theme": { "message": "Тема" }, 25 | "default": { "message": "Стандартная" }, 26 | 27 | "globalExclude": { "message": "Глобальные исключения" }, 28 | "globalExcludeDescription": { "message": "Список хостов (доменов), которые не следует проксировать" }, 29 | 30 | "container": { "message": "Контейнер" }, 31 | "containerDescription": { "message": "Инкогнито и контейнерный прокси (только для Firefox)" }, 32 | "incognito": { "message": "Инкогнито" }, 33 | 34 | "shortcut": { "message": "Сочетание клавиш" }, 35 | "shortcutDescription": { "message": "Предварительный выбор прокси для ассоциации с действием сочетания клавиш" }, 36 | "setProxy": { "message": "Установить прокси" }, 37 | 38 | 39 | "deleteBrowserData": { "message": "Удалить данные браузера" }, 40 | "deleteBrowsingDataConfirm": { "message": "Вы уверены, что хотите удалить cookies, хранилище IndexedDB и хранилище DOM?" }, 41 | "restoreDefaults": { "message": "Восстановить настройки по умолчанию" }, 42 | "restoreDefaultsConfirm": { "message": "Вы уверены, что хотите восстановить все настройки по умолчанию?" }, 43 | "limitWebRTC": { "message": "Ограничить WebRTC" }, 44 | "limitWebRTCDescription": { "message": "Переключить настройки WebRTC браузера" }, 45 | 46 | "proxies": { "message": "Прокси" }, 47 | "add": { "message": "Добавить" }, 48 | "getLocation": { "message": "Узнать местонахождение" }, 49 | 50 | "duplicate": { "message": "Дублировать" }, 51 | 52 | "title": { "message": "Название" }, 53 | "type": { "message": "Тип прокси" }, 54 | "color": { "message": "Цвет" }, 55 | "random": { "message": "Случайный" }, 56 | "hostname": { "message": "Прокси IP адрес или имя DNS" }, 57 | "port": { "message": "Порт" }, 58 | "city": { "message": "Город" }, 59 | "country": { "message": "Страна" }, 60 | "proxyDNS": { "message": "Прокси DNS" }, 61 | "hostnamePortError": { "message": "Отсутствует имя хоста - Порт" }, 62 | 63 | "pattern": { "message": "Шаблон" }, 64 | "patterns": { "message": "Шаблоны" }, 65 | "include": { "message": "Включить" }, 66 | "exclude": { "message": "Исключить" }, 67 | "all": { "message": "Все" }, 68 | "plainHost": { "message": "Обычный хост" }, 69 | "wildcard": { "message": "Шаблон хоста" }, 70 | "regexError": { "message": "Ошибка регулярного выражения для шаблона" }, 71 | 72 | "delete": { "message": "Удалить" }, 73 | "deleteConfirm": { "message": "Действительно хотите удалить?" }, 74 | 75 | "importFoxyProxyAccount": { "message": "Импортировать FoxyProxy аккаунт" }, 76 | "username": { "message": "Имя пользователя" }, 77 | "password": { "message": "Пароль пользователя" }, 78 | "togglePassword": { "message": "Переключить видимость" }, 79 | "userPassError": { "message": "Введите имя пользователя и пароль." }, 80 | "storeLocally": { "message": "Сохранить локально" }, 81 | "view": { "message": "Просмотр" }, 82 | 83 | "importProxyList": { "message": "Импортировать список прокси" }, 84 | "importFromURL": { "message": "Импортировать из URL" }, 85 | "importFromOlderVersions": { "message": "Импортировать из старых версий" }, 86 | 87 | "tester": { "message": "Проверить" }, 88 | "result": { "message": "Результат" }, 89 | "test": { "message": "Тест" }, 90 | "back": { "message": "Назад" }, 91 | 92 | "log": { "message": "Журнал" }, 93 | "documentUrl": { "message": "Document URL" }, 94 | "url": { "message": "URL" }, 95 | "method": { "message": "Метод" }, 96 | "proxy": { "message": "Список прокси" }, 97 | "time": { "message": "Время" }, 98 | "notAvailable": { "message": "Прокси не доступны в Chrome (только в Firefox)" }, 99 | 100 | "about": { "message": "О программе" }, 101 | "error": { "message": "Ошибка выполнения операции" }, 102 | "export": { "message": "Экспортировать настройки" }, 103 | "fileParseError": { "message": "Ошибка содержимого файла" }, 104 | "fileReadError": { "message": "Ошибка чтения файла" }, 105 | "fileTypeError": { "message": "Неподдерживаемый формат файла" }, 106 | "help": { "message": "Помощь" }, 107 | "import": { "message": "Импортировать настройки" }, 108 | "options": { "message": "Настройки" }, 109 | "saveOptions": { "message": "Save" } 110 | } -------------------------------------------------------------------------------- /src/_locales/uk/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "Простий у використанні просунутий засіб керування проксі для кожного" }, 5 | 6 | 7 | "proxyByPatterns": { "message": "Проксіювання згідно з шаблонами" }, 8 | "disable": { "message": "Вимкнути" }, 9 | "ip": { "message": "IP" }, 10 | "location": { "message": "Місцезнаходження" }, 11 | 12 | "options": { "message": "Опції" }, 13 | "proxy": { "message": "Проксі-сервер" }, 14 | "tester": { "message": "Тестувальник" }, 15 | "log": { "message": "Журнал" }, 16 | "help": { "message": "Допомога" }, 17 | "about": { "message": "Про" }, 18 | "import": { "message": "Імпортувати" }, 19 | "export": { "message": "Експортувати" }, 20 | "save": { "message": "Зберегти" }, 21 | 22 | "enableSync": { "message": "Увімкнути синхронізацію" }, 23 | "enableSyncDescription": { "message": "Синхронізувати глобальні винятки, проксі-сервери та шаблони" }, 24 | "syncError": { "message": "Помилка синхронізації" }, 25 | 26 | "proxyDNS": { "message": "Проксіювати DNS" }, 27 | "proxyDnsDescription": { "message": "Використовувати цей проксі-сервер, щоб здійснювати певні DNS-запити (лише для SOCKS у Firefox)" }, 28 | 29 | "globalExclude": { "message": "Глобальні винятки" }, 30 | "globalExcludeDescription": { "message": "Вказані тут шаблони ніколи не проксіюватимуться" }, 31 | "globalExcludeError": { "message": "Помилка в шаблоні-винятку" }, 32 | 33 | "deleteBrowserData": { "message": "Видалити дані з браузера" }, 34 | "confirmDeleteBrowserData": { "message": "Ви точно бажаєте видалити файли-куки, сховище індексованої БД, локальне сховище DOM?" }, 35 | "restoreDefaults": { "message": "Відновити налаштування за замовчуванням" }, 36 | "restoreDefaultsConfirm": { "message": "Ви точно хочете відновити усі налаштування до їх типових значень?" }, 37 | "limitWebRTC": { "message": "Обмежити WebRTC" }, 38 | "resetWebRTC": { "message": "Скинути WebRTC" }, 39 | 40 | 41 | "foxyProxyAccount": { "message": "Обліковий запис FoxyProxy" }, 42 | "username": { "message": "Ім’я користувача" }, 43 | "password": { "message": "Пароль" }, 44 | "togglePW": { "message": "Показати/сховати пароль" }, 45 | "importAs": { "message": "Імпортувати як" }, 46 | "domain": { "message": "Домен" }, 47 | "userPassError": { "message": "Будь ласка, введіть і ім’я користувача, і пароль." }, 48 | 49 | "proxyList": { "message": "Перелік проксі-серверів" }, 50 | "proxyTypeError": { "message": "Невідомий тип" }, 51 | "proxyPortError": { "message": "Бракує порту" }, 52 | "importURL": { "message": "Імпортувати за URL-адресою" }, 53 | 54 | "fromOlderVersions": { "message": "Зі старіших версій" }, 55 | 56 | "regexError": { "message": "Помилка створення регулярного виразу для шаблону" }, 57 | 58 | "addHostTo": { "message": "Додати шаблон хосту до ..." }, 59 | 60 | "incognitoAccess": { "message": "Для налаштування проксі-сервера необхідний дозвіл приватного перегляду." }, 61 | 62 | 63 | 64 | 65 | 66 | 67 | "getLocation": { "message": "З’ясувати місцезнаходження" }, 68 | "add": { "message": "Додати" }, 69 | "title": { "message": "Заголовок" }, 70 | "type": { "message": "Тип" }, 71 | "color": { "message": "Колір" }, 72 | "random": { "message": "Випадковий" }, 73 | "hostname": { "message": "Ім’я хоста" }, 74 | "port": { "message": "Порт" }, 75 | "city": { "message": "Місто" }, 76 | "country": { "message": "Країна" }, 77 | 78 | "pacUrlError": { "message": "Бракує URL-адреси PAC для типу PAC" }, 79 | "hostnamePortError": { "message": "Бракує імені хоста чи порта" }, 80 | 81 | "pattern": { "message": "Шаблон" }, 82 | "patterns": { "message": "Шаблони" }, 83 | "quickAdd": { "message": "Швидке додавання" }, 84 | "include": { "message": "Включити" }, 85 | "exclude": { "message": "Виключити" }, 86 | "all": { "message": "Усі" }, 87 | "plainHost": { "message": "Звичайний хост" }, 88 | "wildcard": { "message": "Шаблон" }, 89 | "patternError": { "message": "Помилка в шаблоні" }, 90 | 91 | 92 | "delete": { "message": "Видалити" }, 93 | "deleteConfirm": { "message": "Ви справді хочете це видалити?" }, 94 | 95 | "result": { "message": "Результат" }, 96 | "test": { "message": "Тестувати" }, 97 | 98 | "documentURL": { "message": "URL-адреса документу" }, 99 | "method": { "message": "Метод" }, 100 | "time": { "message": "Час" }, 101 | 102 | "selectCountry": { "message": "Оберіть країну" }, 103 | "cancel": { "message": "Скасувати" }, 104 | "ok": { "message": "Гаразд" }, 105 | 106 | "error": { "message": "Трапилася помилка із цією дією" }, 107 | "fileParseError": { "message": "Сталася помилка під час розбору вмісту файлу" }, 108 | "fileReadError": { "message": "Сталася помилка читання файлу" }, 109 | "fileSizeError": { "message": "Розмір файлу перевищує дозволені $1кб" }, 110 | "fileTypeError": { "message": "Непідтримуваний формат файлу" } 111 | } 112 | -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy 基础版" }, 4 | "extensionDescription": { "message": "易于使用,适用于任何人的高级代理管理工具" }, 5 | 6 | 7 | "error": { "message": "错误" }, 8 | "erroNoSettings": { "message": "未找到设置。请重新安装 FoxyProxy。" }, 9 | 10 | "deleteAll": { "message": "全部删除" }, 11 | "export": { "message": "导出" }, 12 | "import": { "message": "导入" }, 13 | "log": { "message": "日志" }, 14 | "myIP": { "message": "查询我的 IP" }, 15 | "deleteAllmessage": { "message": "已删除所有数据" }, 16 | 17 | "authError": { "message": "$1 拒绝连接\n请检查代理用户名/密码" }, 18 | 19 | 20 | "delete": { "message": "删除" }, 21 | "deleteNot": { "message": "不要删除" }, 22 | "deleteBrowserData": { "message": "删除浏览器数据" }, 23 | "deleteBrowserDataDescription": { "message": "缓存、cookies、indexedDB 存储、DOM 本地存储、插件数据、service worker 数据。" }, 24 | "deleteBrowserDataNotDescription": { "message": "已保存的密码、浏览和表单历史、下载历史、webSQL 和服务器绑定证书。" }, 25 | "done": { "message": "完成!" }, 26 | 27 | "about": { "message": "关于" }, 28 | "syncSettings": { "message": "同步设置" }, 29 | "syncSettingsHelp": { "message": "开启以使用 Firefox 同步(将在你所有的 Firefox 浏览器间同步设置)。关闭则只在本地保存设置。注意你可以有两套不同的设置,一套同步的和一套本地的。" }, 30 | 31 | "on": { "message": "开启" }, 32 | "off": { "message": "关闭" }, 33 | "confirmTransferToLocal": { "message": "是否将已同步的设置传输到你的本地配置文件?\n将合并已有的数据。" }, 34 | "confirmTransferToSync": { "message": "是否将本地设置传输到你的同步配置文件?\n将合并已有的数据。" }, 35 | 36 | "modePatterns": { "message": "按模式和顺序使用启用的代理" }, 37 | "modeDisabled": { "message": "关闭(使用 Firefox 设置)" }, 38 | "forAll": { "message": "对全部 URLs 使用" }, 39 | "noProxies": { "message": "你没有设置任何代理。"}, 40 | 41 | 42 | 43 | "edit": { "message": "编辑" }, 44 | "pattern": { "message": "模式" }, 45 | "confirmDelete": { "message": "你确定要删除吗?" }, 46 | 47 | "addProxy": { "message": "添加代理" }, 48 | "editProxy": { "message": "编辑代理 $1" }, 49 | 50 | 51 | "editPatterns": { "message": "编辑模式" }, 52 | "editPatternsFor": { "message": "编辑 $1 的模式" }, 53 | 54 | 55 | "errorWas": { "message": "存在错误。请重试。" }, 56 | "errorSlash": { "message": "通配符模式不能有斜线 /。因为 Firefox 的限制你无法匹配 URL 路径。" }, 57 | "errorEmpty": { "message": "字段不能为空。" }, 58 | 59 | 60 | "importEnd": { "message": "设置已成功导入" }, 61 | "patternsChanged": { "message": "有些模式已做更改因为它们包含斜线。不再支持于模式里使用斜线。" }, 62 | "importEndSlash": { "message": "导入已完成。由于 Firefox 缺陷,已不再支持于模式里使用斜线。请复查你的模式并移除所有斜线。" }, 63 | "userPassError": { "message": "请输入用户名和密码。" }, 64 | "errorFetch": { "message": "操作出现错误" }, 65 | 66 | 67 | "errorPattern": { "message": "请输入一项模式" }, 68 | "importBW": { "message": "已导入 $1 项白名单和 $2 项黑名单模式。" }, 69 | 70 | "up": { "message": "上移" }, 71 | "down": { "message": "下移" }, 72 | 73 | "disabled": { "message": "已禁用" }, 74 | "active": { "message": "已启用" }, 75 | "activeNote": { "message": "FoxyProxy 将忽略此页面的所有选项,除非设为" }, 76 | "noMatch": { "message": "无匹配" }, 77 | "none": { "message": "无" }, 78 | 79 | "name": { "message": "名称" }, 80 | 81 | "type": { "message": "类型" }, 82 | "protocol": { "message": "协议" }, 83 | "whitePatterns": { "message": "白名单模式" }, 84 | "blackPatterns": { "message": "黑名单模式" }, 85 | "importedPattern": { "message": "此模式来自自旧版本的 FoxyProxy,且在导入过程中更改。下面为未更改的原模式:" }, 86 | 87 | 88 | "patternMatch": { "message": "模式与 URL 匹配!" }, 89 | "patternNotMatch": { "message": "模式与 URL 不匹配。" }, 90 | "errorProtocol": { "message": "协议不匹配。" }, 91 | 92 | 93 | 94 | "proxyType": { "message": "代理类型" }, 95 | "color": { "message": "颜色" }, 96 | "patternShortcuts": { "message": "模式快捷选项"}, 97 | "title": { "message": "标题或描述(可选)" }, 98 | "ip": { "message": "代理 IP 地址或 DNS 名称" }, 99 | "port": { "message": "端口" }, 100 | "username": { "message": "用户名(可选)" }, 101 | "password": { "message": "密码(可选)" }, 102 | "togglePW": { "message": "显示密码" }, 103 | 104 | "addWhitelist": { "message": "添加匹配全部 URLs 的白名单模式" }, 105 | "noLocal": { "message": "不用于本地主机和内部网/私密 IP 地址" }, 106 | 107 | 108 | 109 | "patternTester": { "message": "模式测试器" }, 110 | "patternHelp": { "message": "模式帮助" }, 111 | "welcome": { "message": "欢迎" }, 112 | "options": { "message": "选项" }, 113 | "optionsPage": { "message": "FoxyProxy 选项" }, 114 | "enterUrl": { "message": "输入要测试的 URL" }, 115 | "enterUrlNote": { "message": "(已忽略路径和查询参数)" }, 116 | "patternDetail": { "message": "输入模式详细信息。" }, 117 | "patternNote": { "message": "(无路径,无查询参数)" }, 118 | "clickTest": { "message": "准备好后点击“测试”" }, 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | "ok": { "message": "确定" }, 127 | "clear": { "message": "清除" }, 128 | "refresh": { "message": "刷新" }, 129 | "cancel": { "message": "取消" }, 130 | "back": { "message": "返回" }, 131 | "test": { "message": "测试" }, 132 | "help": { "message": "帮助" }, 133 | "importPatterns": { "message": "导入模式" }, 134 | "exportPatterns": { "message": "导出模式" }, 135 | "newWhite": { "message": "新增白名单" }, 136 | "newBlack": { "message": "新增黑名单" }, 137 | "save": { "message": "保存" }, 138 | "add": { "message": "添加" }, 139 | "saveAdd": { "message": "保存并添加另一个" }, 140 | "saveEditPattern": { "message": "保存并编辑模式" }, 141 | "imported": { "message": "已导入" }, 142 | "addBlacklistTip": { "message": "把本地主机、127.0.0.1、192.168.*.*、172.16.*.* 和 10.*.*.* 添加到黑名单模式" }, 143 | "addBlacklist": { "message": "添加黑名单模式可以防止代理用在本地主机和内部网/私密 IP 地址上" }, 144 | "addWhitelistTip": { "message": "添加白名单模式 *" }, 145 | "patternCheatSheet": { "message": "模式速查表" }, 146 | 147 | "logSize": { "message": "日志大小" }, 148 | "url": { "message": "URL" }, 149 | "proxyTitle": { "message": "代理标题" }, 150 | "proxyAddress": { "message": "代理地址" }, 151 | "matchPattern": { "message": "匹配模式" }, 152 | "whiteBlack": { "message": "白/黑名单" }, 153 | "white": { "message": "白名单" }, 154 | "black": { "message": "黑名单" }, 155 | "timestamp": { "message": "时间戳" }, 156 | "notApplicable": {"message": "不适用"}, 157 | "matchedURLs": {"message": "与模式匹配的 URLs"}, 158 | "unmatchedURLs": {"message": "与模式不匹配的 URLs"} 159 | } 160 | -------------------------------------------------------------------------------- /src/_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "FoxyProxy" }, 3 | "extensionNameBasic": { "message": "FoxyProxy Basic" }, 4 | "extensionDescription": { "message": "任誰都能快速上手的進階代理伺服器管理工具" }, 5 | 6 | "error": { "message": "錯誤" }, 7 | "erroNoSettings": { "message": "找不到設定檔。 請重新安裝 FoxyProxy。" }, 8 | 9 | "deleteAll": { "message": "移除全部" }, 10 | "export": { "message": "匯出設定" }, 11 | "import": { "message": "匯入設定" }, 12 | "importProxyList": { "message": "匯入代理伺服器清單" }, 13 | "log": { "message": "日誌" }, 14 | "myIP": { "message": "我的 IP 為何?" }, 15 | "deleteAllmessage": { "message": "已將所有資料移除" }, 16 | 17 | "authError": { "message": "$1 拒絕連線\n請檢查代理伺服器的使用者帳號與密碼是否正確" }, 18 | 19 | 20 | "delete": { "message": "移除" }, 21 | "deleteNot": { "message": "不要移除" }, 22 | "deleteBrowserData": { "message": "移除瀏覽器資料" }, 23 | "deleteBrowserDataDescription": { "message": "快取、Cookies、indexedDB 儲存空間, DOM 本機儲存空間, 擴充套件資料、Service Worker 資料。" }, 24 | "deleteBrowserDataNotDescription": { "message": "以儲存的密碼、瀏覽與表單歷史紀錄、下載紀錄、WebSQL 和 Server-Bound 認證。" }, 25 | "done": { "message": "完成!" }, 26 | 27 | "about": { "message": "關於" }, 28 | "syncSettings": { "message": "同步設定" }, 29 | "syncSettingsHelp": { "message": "啟用此設定將會使用 Firefox Sync (設定將同步至所有的瀏覽器中)。 停用此設定僅將設定儲存於本機中。 小提醒:您可以儲存兩種設定,一種為本機設定,另一種為同步設定。" }, 30 | 31 | "on": { "message": "啟用" }, 32 | "off": { "message": "停用" }, 33 | "confirmTransferToLocal": { "message": "是否將您的設定同步至本機設定檔中?\n已經存在的資料將會被合併。" }, 34 | "confirmTransferToSync": { "message": "是否將您的本機設定同步至同步設定檔中?\n已經存在的資料將會被合併。" }, 35 | 36 | "modePatterns": { "message": "以設定的規則與順序套用代理伺服器設定" }, 37 | "modeDisabled": { "message": "停用 (使用 Firefox 內建設定)" }, 38 | "forAll": { "message": "於所有網址上啟用" }, 39 | "noProxies": { "message": "您尚未建立任何代理伺服器設定。"}, 40 | 41 | 42 | 43 | "edit": { "message": "編輯" }, 44 | "patterns": { "message": "規則" }, 45 | "confirmDelete": { "message": "您確定要移除此設定嗎?" }, 46 | 47 | "addProxy": { "message": "新增代理伺服器" }, 48 | "editProxy": { "message": "編輯代理伺服器 - $1" }, 49 | 50 | 51 | "editPatterns": { "message": "編輯規則" }, 52 | "editPatternsFor": { "message": "編輯規則 - $1" }, 53 | 54 | 55 | "errorWas": { "message": "發生錯誤。請重試。" }, 56 | "errorSlash": { "message": "萬用字元不能包含斜線。 由於 Firefox 的限制,您無法對應到任何的網址或路徑。" }, 57 | "errorEmpty": { "message": "該欄位不可留空。" }, 58 | 59 | 60 | "importEnd": { "message": "設定檔匯入成功。" }, 61 | "patternsChanged": { "message": "部分規則因含有斜線而被變更。 不支援斜線規則。" }, 62 | "importEndSlash": { "message": "匯入完成。 由於 Firefox 的一隻 Bug ,已不支援斜線規則。 請重新檢查您的規則是否仍含有斜線,若有請將之移除。" }, 63 | "userPassError": { "message": "請輸入使用者名稱和密碼。" }, 64 | "errorFetch": { "message": "該操作行為發生錯誤。" }, 65 | 66 | 67 | "errorPattern": { "message": "請輸入規則" }, 68 | "importBW": { "message": "已匯入 $1 個白名單與 $2 個黑名單規則。" }, 69 | 70 | "up": { "message": "上移" }, 71 | "down": { "message": "下移" }, 72 | 73 | "disabled": { "message": "已停用" }, 74 | "active": { "message": "已啟用" }, 75 | "activeNote": { "message": "FoxyProxy 將會略過此頁面上所有設定,除非設定為" }, 76 | "noMatch": { "message": "沒有對應結果" }, 77 | "none": { "message": "無" }, 78 | 79 | "name": { "message": "名稱" }, 80 | "pattern": { "message": "規則" }, 81 | "type": { "message": "類型" }, 82 | "protocol": { "message": "協定" }, 83 | "whitePatterns": { "message": "白名單規則" }, 84 | "blackPatterns": { "message": "黑名單規則" }, 85 | "importedPattern": { "message": "這個規則為舊版匯入的規則,且於匯入期間已被修改。 以下為其原始規則:" }, 86 | 87 | 88 | "patternMatch": { "message": "規則與網址相對應!" }, 89 | "patternNotMatch": { "message": "規則與網址無法對應。" }, 90 | "errorProtocol": { "message": "協定不對應。" }, 91 | 92 | 93 | 94 | "proxyType": { "message": "代理伺服器類型" }, 95 | "color": { "message": "顏色" }, 96 | "patternShortcuts": { "message": "規則快捷鍵"}, 97 | "title": { "message": "標題或描述(選填)" }, 98 | "ip": { "message": "代理伺服器 IP 位址或 DNS 域名" }, 99 | "port": { "message": "連接埠" }, 100 | "username": { "message": "使用者名稱(選填)" }, 101 | "password": { "message": "密碼(選填)" }, 102 | "togglePW": { "message": "顯示或隱藏密碼" }, 103 | 104 | "addWhitelist": { "message": "新增白名單規則以對應到所有網址" }, 105 | "noLocal": { "message": "請不要使用 localhost 或內部 / 私有 IP 位址" }, 106 | 107 | 108 | 109 | "patternTester": { "message": "規則測試器" }, 110 | "patternHelp": { "message": "規則說明" }, 111 | "welcome": { "message": "歡迎" }, 112 | "options": { "message": "選項" }, 113 | "optionsPage": { "message": "FoxyProxy 選項" }, 114 | "enterUrl": { "message": "請輸入網址以進行測試" }, 115 | "enterUrlNote": { "message": "(已略過路徑與查詢字串)" }, 116 | "patternDetail": { "message": "輸入規則詳細資料。" }, 117 | "patternNote": { "message": "(不包含路徑及查詢字串)" }, 118 | "clickTest": { "message": "準備就緒後按下「測試」開始進行測試。" }, 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | "ok": { "message": "確定" }, 127 | "clear": { "message": "清除" }, 128 | "refresh": { "message": "重新整理" }, 129 | "cancel": { "message": "取消" }, 130 | "back": { "message": "返回" }, 131 | "test": { "message": "測試" }, 132 | "help": { "message": "說明" }, 133 | "importPatterns": { "message": "匯入規則" }, 134 | "exportPatterns": { "message": "匯出規則" }, 135 | "newWhite": { "message": "新增白名單" }, 136 | "newBlack": { "message": "新增黑名單" }, 137 | "save": { "message": "儲存" }, 138 | "add": { "message": "新建" }, 139 | "saveAdd": { "message": "儲存並新建" }, 140 | "saveEditPattern": { "message": "儲存並編輯規則" }, 141 | "imported": { "message": "已匯入" }, 142 | "addBlacklistTip": { "message": "將 localhost、127.0.0.1、192.168.*.*、172.16.*.* 和 10.*.*.* 新增至黑名單規則中" }, 143 | "addBlacklist": { "message": "新增黑名單規則可以防止這個代理伺服器設定被套用於 localhost 或內部 / 私有 IP 位址上。" }, 144 | "addWhitelistTip": { "message": "將 * 新增至白名單規則中" }, 145 | "patternCheatSheet": { "message": "規則速查表" }, 146 | 147 | "logSize": { "message": "日誌大小" }, 148 | "url": { "message": "網址" }, 149 | "proxyTitle": { "message": "代理伺服器標題" }, 150 | "proxyAddress": { "message": "代理伺服器位址" }, 151 | "matchPattern": { "message": "對應規則" }, 152 | "whiteBlack": { "message": "白名單 / 黑名單" }, 153 | "white": { "message": "白名單" }, 154 | "black": { "message": "黑名單" }, 155 | "timestamp": { "message": "時間戳" }, 156 | "notApplicable": {"message": "不適用"}, 157 | "matchedURLs": {"message": "與規則相對應的網址"}, 158 | "unmatchedURLs": {"message": "與規則不對應的網址"}, 159 | 160 | 161 | "pasteList": { "message": "在下方貼上代理伺服器清單。"}, 162 | "formats": {"message": "格式"}, 163 | "simple": { "message": "簡易版" }, 164 | "complete": { "message": "完整版" }, 165 | "simpleFormat": { "message": "IP 位址 / 伺服器及連接埠皆為必填。 username:password(使用者名稱與密碼)為選填。"}, 166 | "ipPort": {"message": "ip:port"}, 167 | "ipPortUsernamePassword": {"message": "ip:port:username:password"}, 168 | "examples": {"message": "範例"}, 169 | "completeFormat": { "message": "使用這種格式,您可以指定所有代理伺服器設定。其中僅有協定和伺服器為必填。"}, 170 | "overwriteProxies": { "message": "覆蓋已經存在的代理伺服器" }, 171 | "overwritProxiesHelp1": { "message": "若勾選此選項,則會將所有已存在的代理伺服器先行移除並取代為此清單上的代理伺服器設定。" }, 172 | "overwritProxiesHelp2": { "message": "若不勾選此選項,則會將此清單內的代理伺服器新增至已經存在的代理伺服器清單之後。" }, 173 | "confirmOverwrite": { "message": "您確定要覆蓋所有已存在的代理伺服器設定嗎?" }, 174 | "importsSkipped": { "message": "因為無法解析,已略過 $1 行:\n\n$2" }, 175 | "importSucceeded": { 176 | "message": "讀取並匯入 $1 個代理伺服器設定" 177 | } 178 | } -------------------------------------------------------------------------------- /src/content/action.js: -------------------------------------------------------------------------------- 1 | // import {Flag} from './flag.js'; 2 | import {Location} from './location.js'; 3 | 4 | export class Action { 5 | 6 | // https://github.com/w3c/webextensions/issues/72#issuecomment-1848874359 7 | // 'prefers-color-scheme' detection in Chrome background service worker 8 | static dark = false; 9 | 10 | static set(pref) { 11 | // --- set action/browserAction 12 | let title = ''; 13 | let text = ''; 14 | let color = this.dark ? '#444' : '#fff'; 15 | switch (pref.mode) { 16 | case 'disable': 17 | title = browser.i18n.getMessage('disable'); 18 | text = '⛔'; 19 | break; 20 | 21 | case 'direct': 22 | title = 'DIRECT'; 23 | text = '⮕'; 24 | break; 25 | 26 | case 'pattern': 27 | title = browser.i18n.getMessage('proxyByPatterns'); 28 | text = '🌐'; 29 | break; 30 | 31 | default: 32 | const item = pref.data.find(i => pref.mode === (i.type === 'pac' ? i.pac : `${i.hostname}:${i.port}`)); 33 | if (item) { 34 | // Chrome 113-114 started having a bug showing unicode flags 35 | // const flag = Flag.get(item.cc); 36 | // const host = flag + ' ' + [item.hostname, item.port].filter(Boolean).join(':'); 37 | const host = [item.hostname, item.port].filter(Boolean).join(':'); 38 | title = [item.title, host, item.city, Location.get(item.cc)].filter(Boolean).join('\n'); 39 | // text = item.cc ? flag : item.hostname; 40 | text = item.title || item.hostname; 41 | color = item.color; 42 | } 43 | } 44 | 45 | browser.action.setBadgeBackgroundColor({color}); 46 | browser.action.setTitle({title}); 47 | browser.action.setBadgeText({text}); 48 | } 49 | } -------------------------------------------------------------------------------- /src/content/app.js: -------------------------------------------------------------------------------- 1 | // ---------- Polyfill (Side Effect) ----------------------- 2 | // Promise based 'browser' namespace is used to avoid conflict 3 | // Firefox 'chrome' API: MV2 callback | MV3 promise 4 | // Firefox/Edge: browser namespace | Chrome/Opera: chrome namespace 5 | globalThis.browser ??= chrome; 6 | 7 | // ---------- Default Preferences -------------------------- 8 | export const pref = { 9 | mode: 'disable', 10 | sync: false, 11 | autoBackup: false, 12 | passthrough: '', 13 | theme: '', 14 | container: {}, 15 | commands: {}, 16 | data: [] 17 | }; 18 | // ---------- /Default Preferences ------------------------- 19 | 20 | // ---------- App ------------------------------------------ 21 | export class App { 22 | 23 | // https://github.com/foxyproxy/firefox-extension/issues/220 24 | // navigator.userAgent identification fails in custom userAgent and browser forks 25 | // Chrome does not support runtime.getBrowserInfo() 26 | // getURL: moz-extension: | chrome-extension: | safari-web-extension: 27 | static firefox = browser.runtime.getURL('').startsWith('moz-extension:'); 28 | static basic = browser.runtime.getManifest().name === browser.i18n.getMessage('extensionNameBasic'); 29 | static android = navigator.userAgent.includes('Android'); 30 | 31 | // ---------- User Preferences --------------------------- 32 | // not syncing mode & sync (to have a choice), data (will be broken into parts) 33 | static syncProperties = Object.keys(pref).filter(i => !['mode', 'sync', 'data'].includes(i)); 34 | 35 | static defaultPref = JSON.stringify(pref); 36 | 37 | static getDefaultPref() { 38 | return JSON.parse(this.defaultPref); 39 | } 40 | 41 | static getPref() { 42 | // update pref with the saved version 43 | return browser.storage.local.get().then(result => { 44 | Object.keys(result).forEach(i => pref[i] = result[i]); 45 | }); 46 | } 47 | 48 | // ---------- Helper functions --------------------------- 49 | // https://bugs.chromium.org/p/chromium/issues/detail?id=478654 50 | // Add support for SVG images in Web Notifications API -> CH107 51 | // https://bugs.chromium.org/p/chromium/issues/detail?id=1353252 52 | // svg broken from bg service worker 53 | static notify(message, title = browser.i18n.getMessage('extensionName'), id = '') { 54 | browser.notifications.create(id, { 55 | type: 'basic', 56 | iconUrl: '/image/icon48.png', 57 | title, 58 | message 59 | }); 60 | } 61 | 62 | static equal(a, b) { 63 | return JSON.stringify(a) === JSON.stringify(b); 64 | } 65 | 66 | static parseURL(url) { 67 | // rebuild file:// 68 | url.startsWith('file://') && (url = 'http' + url.substring(4)); 69 | 70 | try { url = new URL(url); } 71 | catch (error) { 72 | alert(`${url} ➜ ${error.message}`); 73 | return {}; 74 | } 75 | 76 | // check protocol 77 | if (!['http:', 'https:', 'file:'].includes(url.protocol)) { 78 | alert(`${url} ➜ Unsupported Protocol ${url.protocol}`); 79 | return {}; 80 | } 81 | 82 | return url; 83 | } 84 | 85 | static allowedTabProxy(url) { 86 | return /^https?:\/\/.+|^about:(blank|newtab)$/.test(url); 87 | } 88 | } -------------------------------------------------------------------------------- /src/content/authentication.js: -------------------------------------------------------------------------------- 1 | // webRequest.onAuthRequired: Firefox HTTP/HTTPS/WS/WSS | Chrome: HTTP/HTTPS 2 | // 'webRequestAuthProvider' permission Chrome 108, Firefox 126 3 | 4 | export class Authentication { 5 | 6 | static { 7 | this.data = {}; 8 | // prevent bad authentication loop 9 | this.pending = {}; 10 | // webRequest.onAuthRequired is only called for HTTP and HTTPS/TLS proxy servers 11 | const urls = ['']; 12 | browser.webRequest.onAuthRequired.addListener(e => this.process(e), {urls}, ['blocking']); 13 | browser.webRequest.onCompleted.addListener(e => this.clearPending(e), {urls}); 14 | browser.webRequest.onErrorOccurred.addListener(e => this.clearPending(e), {urls}); 15 | } 16 | 17 | static init(data) { 18 | // reset data 19 | this.data = {}; 20 | data.forEach(i => { 21 | const {hostname, port, username, password} = i; 22 | hostname && port && username && password && 23 | (this.data[`${hostname}:${port}`] = {username, password}); 24 | }); 25 | } 26 | 27 | static process(e) { 28 | // true for Proxy-Authenticate, false for WWW-Authenticate 29 | if (!e.isProxy) { return; } 30 | 31 | // sending message to log.js 32 | browser.runtime.sendMessage({id: 'onAuthRequired', e}); 33 | 34 | // already sent once and pending 35 | if (this.pending[e.requestId]) { 36 | return {cancel: true}; 37 | } 38 | 39 | const {host, port} = e.challenger; 40 | const authCredentials = this.data[`${host}:${port}`]; 41 | if (authCredentials) { 42 | // prevent bad authentication loop 43 | this.pending[e.requestId] = 1; 44 | return {authCredentials}; 45 | } 46 | } 47 | 48 | static clearPending(e) { 49 | delete this.pending[e.requestId]; 50 | } 51 | } -------------------------------------------------------------------------------- /src/content/background.js: -------------------------------------------------------------------------------- 1 | import {Sync} from "./sync.js"; 2 | import {Migrate} from './migrate.js'; 3 | import {Proxy} from './proxy.js'; 4 | import './commands.js'; 5 | 6 | // ---------- Process Preferences -------------------------- 7 | class ProcessPref { 8 | 9 | static { 10 | this.init(); 11 | } 12 | 13 | static async init() { 14 | const pref = await browser.storage.local.get(); 15 | 16 | // storage sync -> local update 17 | await Sync.get(pref); 18 | 19 | // migrate after storage sync check 20 | await Migrate.init(pref); 21 | 22 | // set proxy 23 | Proxy.set(pref); 24 | 25 | // add listener after migrate 26 | Sync.init(pref); 27 | } 28 | } 29 | // ---------- /Process Preferences ------------------------- 30 | 31 | // ---------- Initialisation ------------------------------- 32 | // browser.runtime.onInstalled.addListener(e => { 33 | // // show help 34 | // ['install', 'update'].includes(e.reason) && browser.tabs.create({url: '/content/help.html'}); 35 | // }); -------------------------------------------------------------------------------- /src/content/browsing-data.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | // ---------- browsingData (Side Effect) ------------------- 4 | class BrowsingData { 5 | 6 | static { 7 | document.querySelector('#deleteBrowsingData').addEventListener('click', () => this.process()); 8 | this.init(); 9 | } 10 | 11 | static async init() { 12 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request 13 | // Any permissions granted are retained by the extension, even over upgrade and disable/enable cycling. 14 | // check if permission is granted 15 | this.permission = await browser.permissions.contains({permissions: ['browsingData']}); 16 | } 17 | 18 | static async process() { 19 | if (!this.permission) { 20 | // request permission 21 | // Chrome appears to return true, granted silently without a popup prompt 22 | this.permission = await browser.permissions.request({permissions: ['browsingData']}); 23 | if (!this.permission) { return; } 24 | } 25 | 26 | if (!confirm(browser.i18n.getMessage('deleteBrowsingDataConfirm'))) { return; } 27 | 28 | browser.browsingData.remove({}, { 29 | cookies: true, 30 | indexedDB: true, 31 | localStorage: true 32 | }) 33 | .catch(error => App.notify(browser.i18n.getMessage('deleteBrowsingData') + '\n\n' + error.message)); 34 | } 35 | } -------------------------------------------------------------------------------- /src/content/bulk-edit.js: -------------------------------------------------------------------------------- 1 | // ---------- Bulk Edit (Side Effect) ---------------------- 2 | class BulkEdit { 3 | 4 | static { 5 | const div = document.querySelector('.bulk-edit'); 6 | [this.t1, this.t2, this.s1, this.s2, this.select] = div.children; 7 | 8 | this.t1.addEventListener('change', () => this.toggleProxy('t1')); 9 | this.s1.addEventListener('change', () => this.toggleProxy('s1')); 10 | this.t2.addEventListener('change', () => this.togglePattern()); 11 | this.select.addEventListener('change', () => this.process()); 12 | } 13 | 14 | static toggleProxy(i) { 15 | // remove previous selection 16 | document.querySelector(`details.proxy.${i}`)?.classList.remove(i); 17 | const n = this.getNumber(i); 18 | if (!n) { return; } 19 | 20 | document.querySelector(`details.proxy:nth-of-type(${n})`)?.classList.add(i); 21 | 22 | // reselect t2 23 | this.togglePattern(); 24 | } 25 | 26 | static togglePattern() { 27 | // remove previous selection 28 | const prev = document.querySelector('.pattern-row.t2'); 29 | if (prev) { 30 | prev.classList.remove('t2'); 31 | prev.closest('details').open = false; 32 | } 33 | 34 | const n = this.getNumber('t2'); 35 | if (!n) { return; } 36 | 37 | const t = this.getNumber('t1') || this.getNumber('s1'); 38 | if (!t) { return; } 39 | 40 | const elem = document.querySelector(`details.proxy:nth-of-type(${t}) .pattern-row:nth-of-type(${n})`); 41 | if (elem) { 42 | elem.classList.add('t2'); 43 | elem.closest('details').open = true; 44 | } 45 | } 46 | 47 | static process() { 48 | if (!this.select.value) { return; } 49 | 50 | const id = this.select.value; 51 | switch (id) { 52 | case 'openAll': 53 | case 'closeAll': 54 | this.getProxies().forEach(i => i.open = id === 'openAll'); 55 | break; 56 | 57 | case 'setType': 58 | case 'setPort': 59 | case 'setTitle': 60 | case 'setUsername': 61 | case 'setPassword': 62 | let s2 = this.s2.value.trim(); 63 | if (!s2) { break; } 64 | 65 | const ref = id.substring(3).toLowerCase(); 66 | if (ref === 'type') { 67 | s2 = s2.toLowerCase(); 68 | if (!['http', 'https', 'socks4', 'socks5', 'quic', 'pac', 'direct'].includes(s2)) { break; } 69 | } 70 | 71 | document.querySelectorAll(`[data-id="${ref}"]`).forEach(i => 72 | s2.startsWith('+') ? i.value += s2.substring(1) : i.value = s2); 73 | break; 74 | 75 | case 'deleteProxy': 76 | this.deleteProxy(); 77 | this.reset(); 78 | break; 79 | 80 | case 'moveProxy': 81 | this.moveProxy(); 82 | this.reset(); 83 | break; 84 | 85 | case 'movePattern': 86 | this.movePattern(); 87 | this.reset(); 88 | break; 89 | } 90 | 91 | // --- reset 92 | this.select.selectedIndex = 0; 93 | } 94 | 95 | static reset() { 96 | document.querySelectorAll('details.proxy:is(.t1, .s1), .pattern-row.t2').forEach(i => 97 | i.classList.remove('t1', 't2', 's1')); 98 | // ['t1', 't2', 's1'].forEach(i => this[i].value = ''); 99 | } 100 | 101 | static getProxies() { 102 | return document.querySelectorAll('details.proxy'); 103 | } 104 | 105 | static getNumber(i) { 106 | return this[i].checkValidity() && this[i].value ? this[i].value : null; 107 | } 108 | 109 | static getSourceNumbers() { 110 | const n = this.s2.value.match(/\d+-\d+|\d+/g); 111 | if (!n) { return; } 112 | 113 | let arr = []; 114 | n.forEach(i => { 115 | // check if number range e.g. 5-8 116 | const [a, b] = i.split('-'); 117 | b ? arr.push(...Array.from({length: b - a + 1}, (_, i) => (a * 1) + i)) : arr.push(a); 118 | }); 119 | 120 | // map to index (-1), sort, remove duplicates 121 | arr = [...new Set(arr.map(i => i - 1).sort((a, b) => a - b))]; 122 | return arr.length ? arr : null; 123 | } 124 | 125 | static deleteProxy() { 126 | const n = this.getSourceNumbers(); 127 | if (!n) { return; } 128 | 129 | const p = this.getProxies(); 130 | n.forEach(i => p[i]?.remove()); 131 | } 132 | 133 | static moveProxy() { 134 | let n = this.getSourceNumbers(); 135 | if (!n) { return; } 136 | 137 | const t1 = this.t1.value - 1; 138 | const p = this.getProxies(); 139 | 140 | // filter target, map to elements, filter non-existing 141 | n = n.filter(i => i !== t1).map(i => p[i]).filter(Boolean); 142 | if (!n[0]) { return; } 143 | 144 | // before target or after all 145 | p[t1] ? p[t1].before(...n) : p[0].parentElement.append(...n); 146 | } 147 | 148 | static movePattern() { 149 | const t1 = this.t1.value - 1; 150 | const s1 = this.s1.value - 1; 151 | 152 | switch (true) { 153 | // move withing the same proxy 154 | case t1 === -1 || t1 === s1: 155 | s1 !== -1 && this.movePatternWithin(s1); 156 | break; 157 | 158 | // move all patterns to target 159 | case s1 === -1: 160 | this.movePatternAll(t1); 161 | break; 162 | 163 | // move source patterns to target 164 | default: 165 | this.movePatternSome(t1, s1); 166 | } 167 | } 168 | 169 | static movePatternWithin(s1) { 170 | let n = this.getSourceNumbers(); 171 | if (!n) { return; } 172 | 173 | const p = this.getProxies(); 174 | if (!p[s1]) { return; } 175 | 176 | const t2 = this.t2.value - 1; 177 | 178 | // filter target, map to elements, filter non-existing 179 | const pat = p[s1].querySelectorAll('.pattern-row'); 180 | n = n.filter(i => i !== t2).map(i => pat[i]).filter(Boolean); 181 | if (!n[0]) { return; } 182 | 183 | pat[t2] ? pat[t2].before(...n) : pat[0].parentElement.append(...n); 184 | } 185 | 186 | static movePatternAll(t1) { 187 | const n = this.getSourceNumbers(); 188 | if (!n) { return; } 189 | 190 | const p = this.getProxies(); 191 | if (!p[t1]) { return; } 192 | 193 | // filter target, map to elements 194 | const pat = []; 195 | n.filter(i => i !== t1).forEach(i => p[i] && pat.push(...p[i].querySelectorAll('.pattern-row'))); 196 | 197 | const target = p[t1].querySelector('.pattern-box'); 198 | const row = target.children?.[this.t2.value - 1]; 199 | row ? row.before(...pat) : target.append(...pat); 200 | } 201 | 202 | static movePatternSome(t1, s1) { 203 | let n = this.getSourceNumbers(); 204 | if (!n) { return; } 205 | 206 | const p = this.getProxies(); 207 | if (!p[t1] || !p[s1]) { return; } 208 | 209 | // map to elements, filter non-existing 210 | const pat = p[s1].querySelectorAll('.pattern-row'); 211 | n = n.map(i => pat[i]).filter(Boolean); 212 | if (!n[0]) { return; } 213 | 214 | const target = p[t1].querySelector('.pattern-box'); 215 | const row = target.children?.[this.t2.value - 1]; 216 | row ? row.before(...n) : target.append(...n); 217 | } 218 | } -------------------------------------------------------------------------------- /src/content/color.js: -------------------------------------------------------------------------------- 1 | export class Color { 2 | 3 | static getRandom() { 4 | return this.colors[Math.floor(Math.random() * this.colors.length)]; 5 | } 6 | 7 | static colors = [ 8 | '#faebd7', '#00ffff', '#7fffd4', '#f5f5dc', '#ffe4c4', '#ffebcd', '#0000ff', '#8a2be2', '#a52a2a', '#deb887', 9 | '#5f9ea0', '#7fff00', '#d2691e', '#ff7f50', '#6495ed', '#fff8dc', '#dc143c', '#00008b', '#008b8b', '#b8860b', 10 | '#a9a9a9', '#006400', '#bdb76b', '#8b008b', '#556b2f', '#ff8c00', '#9932cc', '#8b0000', '#e9967a', '#8fbc8f', 11 | '#483d8b', '#2f4f4f', '#00ced1', '#9400d3', '#ff1493', '#00bfff', '#696969', '#1e90ff', '#b22222', '#228b22', 12 | '#ff00ff', '#ffd700', '#daa520', '#808080', '#008000', '#adff2f', '#ff69b4', '#cd5c5c', '#4b0082', '#f0e68c', 13 | '#7cfc00', '#fffacd', '#add8e6', '#f08080', '#e0ffff', '#fafad2', '#d3d3d3', '#90ee90', '#ffb6c1', '#ffa07a', 14 | '#20b2aa', '#87cefa', '#778899', '#b0c4de', '#00ff00', '#32cd32', '#800000', '#66cdaa', '#0000cd', '#ba55d3', 15 | '#9370db', '#3cb371', '#7b68ee', '#00fa9a', '#48d1cc', '#c71585', '#191970', '#ffe4e1', '#ffe4b5', '#ffdead', 16 | '#000080', '#fdf5e6', '#808000', '#6b8e23', '#ffa500', '#ff4500', '#da70d6', '#eee8aa', '#98fb98', '#afeeee', 17 | '#db7093', '#ffefd5', '#ffdab9', '#cd853f', '#ffc0cb', '#dda0dd', '#b0e0e6', '#800080', '#ff0000', '#bc8f8f', 18 | '#4169e1', '#8b4513', '#fa8072', '#f4a460', '#2e8b57', '#fff5ee', '#a0522d', '#87ceeb', '#6a5acd', '#708090', 19 | '#00ff7f', '#4682b4', '#d2b48c', '#008080', '#d8bfd8', '#ff6347', '#40e0d0', '#ee82ee', '#f5deb3', '#ffff00', 20 | '#9acd32']; 21 | } -------------------------------------------------------------------------------- /src/content/commands.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/commands/onCommand 2 | // https://developer.chrome.com/docs/extensions/reference/commands/#event-onCommand 3 | // Chrome commands returns command, tab 4 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1843866 5 | // Add tab parameter to commands.onCommand (fixed in Firefox 126) 6 | 7 | import {App} from './app.js'; 8 | import {Proxy} from './proxy.js'; 9 | import {OnRequest} from './on-request.js'; 10 | 11 | // ---------- Commands (Side Effect) ------------------------ 12 | class Commands { 13 | 14 | static { 15 | // commands is not supported on Android 16 | browser.commands?.onCommand.addListener((...e) => this.process(...e)); 17 | } 18 | 19 | static async process(name, tab) { 20 | // firefox only Tab Proxy 21 | const tabProxy = ['setTabProxy', 'unsetTabProxy'].includes(name); 22 | if (!App.firefox && tabProxy) { return; } 23 | 24 | const pref = await browser.storage.local.get(); 25 | 26 | // only Tab Proxy allowed for storage.managed 27 | if (pref.managed && !tabProxy) { return; } 28 | 29 | const host = pref.commands[name]; 30 | let proxy; 31 | 32 | switch (name) { 33 | case 'proxyByPatterns': 34 | this.set(pref, 'pattern'); 35 | break; 36 | 37 | case 'disable': 38 | this.set(pref, 'disable'); 39 | break; 40 | 41 | case 'setProxy': 42 | host && this.set(pref, host); 43 | break; 44 | 45 | case 'includeHost': 46 | case 'excludeHost': 47 | if (!host) { break; } 48 | 49 | proxy = this.findProxy(pref, host); 50 | proxy && Proxy.includeHost(pref, proxy, tab, name); 51 | break; 52 | 53 | case 'setTabProxy': 54 | if (!host) { break; } 55 | 56 | proxy = this.findProxy(pref, host); 57 | proxy && OnRequest.setTabProxy(tab, proxy); 58 | break; 59 | 60 | case 'unsetTabProxy': 61 | OnRequest.setTabProxy(tab); 62 | break; 63 | } 64 | } 65 | 66 | static findProxy(pref, host) { 67 | return host && pref.data.find(i => i.active && host === `${i.hostname}:${i.port}`); 68 | } 69 | 70 | static set(pref, mode) { 71 | pref.mode = mode; 72 | // save mode 73 | browser.storage.local.set({mode}); 74 | // set proxy without menus update 75 | Proxy.set(pref, true); 76 | } 77 | } -------------------------------------------------------------------------------- /src/content/default.css: -------------------------------------------------------------------------------- 1 | /* ----- Light Theme ----- */ 2 | :root { 3 | --color: #000; 4 | --bg: #fff; 5 | --alt-bg: #f5f5f5; 6 | --hover: #eaeaea; 7 | --highlight: #f90; 8 | 9 | --body-bg: #630; 10 | --header: #c60; 11 | 12 | --nav-bg: #420; 13 | --nav-hover: #851; 14 | --nav-color: cornsilk; 15 | 16 | --btn-bg: #f90; 17 | --btn-hover: #e70; 18 | 19 | --link: #e70; 20 | --border: #ddd; 21 | /* --shadow: #0004; */ 22 | --dim: #777; 23 | --tr: #f5f5f5; 24 | } 25 | 26 | /* ----- Dark Theme ----- */ 27 | @media screen and (prefers-color-scheme: dark) { 28 | :root { 29 | --color: #fff; 30 | --bg: #420; 31 | --alt-bg: #666; 32 | --hover: #444; 33 | 34 | /* --body-bg: #630; */ 35 | --header: #e70; 36 | 37 | --btn-bg: #f90; 38 | --btn-hover: #e70; 39 | 40 | --link: #f90; 41 | --border: #777; 42 | /* --shadow: #fff8; */ 43 | --dim: #ccc; 44 | --tr: #531; 45 | } 46 | } 47 | 48 | /* ----- General ----- */ 49 | body { 50 | color: var(--color); 51 | background-color: var(--body-bg); 52 | padding: 0; 53 | margin: 0; 54 | font-family: sans-serif; 55 | } 56 | 57 | * { 58 | box-sizing: border-box; 59 | } 60 | 61 | article { 62 | padding: 0; 63 | margin: 0; 64 | } 65 | 66 | section { 67 | padding: 0; 68 | } 69 | 70 | a { 71 | color: var(--link); 72 | text-decoration: none; 73 | } 74 | 75 | select, 76 | textarea, 77 | input[type="number"], 78 | input[type="text"], 79 | input[type="password"], 80 | input[type="url"] { 81 | width: 100%; 82 | color: inherit; 83 | background-color: var(--alt-bg); 84 | border: 1px solid var(--border); 85 | border-radius: 0.3em; 86 | } 87 | 88 | :is(select, 89 | input[type="number"], 90 | input[type="text"], 91 | input[type="password"], 92 | input[type="url"]):hover { 93 | background-color: var(--hover); 94 | } 95 | 96 | label[for], 97 | input[type="checkbox"], 98 | summary, 99 | .pointer { 100 | cursor: pointer; 101 | } 102 | 103 | ::placeholder { 104 | opacity: 0.5; 105 | color: inherit; 106 | font-style: italic; 107 | } 108 | 109 | .invalid, 110 | input:invalid { 111 | box-shadow: 1px 1px 4px #f20, -1px -1px 4px #f20; 112 | } 113 | 114 | /* ----- Buttons ----- */ 115 | button, 116 | label.flat { 117 | background-color: var(--btn-bg); 118 | border: none; 119 | color: inherit; 120 | cursor: pointer; 121 | text-align: center; 122 | white-space: nowrap; 123 | } 124 | 125 | button.flat, 126 | label.flat { 127 | display: inline-block; 128 | font-size: 0.9em; 129 | color: #fff; 130 | border-radius: 5px; 131 | padding: 0.4em 1em; 132 | min-width: 8em; 133 | } 134 | 135 | button:hover, 136 | label.flat:hover { 137 | background-color: var(--btn-hover); 138 | } 139 | 140 | button:disabled, 141 | select:disabled { 142 | cursor: not-allowed; 143 | opacity: 0.4; 144 | } 145 | 146 | button.plain { 147 | background-color: unset; 148 | padding: 0; 149 | margin: 0; 150 | min-width: 1em; 151 | } 152 | 153 | button[type="submit"] { 154 | display: table; 155 | color: #fff; 156 | font-size:0.9em; 157 | border-radius: 5px; 158 | padding: 0.5em 5em; 159 | margin: 1em auto 0; 160 | } 161 | /* ----- /Buttons ----- */ 162 | -------------------------------------------------------------------------------- /src/content/drag-drop.js: -------------------------------------------------------------------------------- 1 | // ---------- Drag and Drop (Side Effect) ------------------ 2 | class Drag { 3 | 4 | static { 5 | this.proxyDiv = document.querySelector('div.proxy-div'); 6 | this.proxyDiv.addEventListener("dragstart", e => this.dragstart(e)); 7 | this.proxyDiv.addEventListener('dragover', e => this.dragover(e)); 8 | this.proxyDiv.addEventListener('dragend', e => this.dragend(e)); 9 | this.target = null; 10 | } 11 | 12 | static dragstart(e) { 13 | if (e.target.localName === 'input') { 14 | e.preventDefault(); 15 | e.stopPropagation(); 16 | } 17 | } 18 | 19 | static dragover(e) { 20 | this.target = e.target.closest('details'); 21 | } 22 | 23 | static dragend(e) { 24 | if (!this.target) { return; } 25 | 26 | const arr = [...this.proxyDiv.children]; 27 | arr.indexOf(e.target) > arr.indexOf(this.target) ? this.target.before(e.target) : this.target.after(e.target); 28 | this.target = null; 29 | } 30 | } -------------------------------------------------------------------------------- /src/content/flag.js: -------------------------------------------------------------------------------- 1 | // ---------- Unicode flag --------------------------------- 2 | export class Flag { 3 | 4 | static get(cc) { 5 | cc = /^[A-Z]{2}$/i.test(cc) && cc.toUpperCase(); 6 | return cc ? String.fromCodePoint(...[...cc].map(i => i.charCodeAt() + 127397)) : '🌎'; 7 | } 8 | 9 | static show(item) { 10 | switch (true) { 11 | case !!item.cc: 12 | return this.get(item.cc); 13 | 14 | case item.type === 'direct': 15 | return '⮕'; 16 | 17 | case this.isLocal(item.hostname): 18 | return '🖥️'; 19 | 20 | default: 21 | return '🌎'; 22 | } 23 | } 24 | 25 | static isLocal(host) { 26 | // check local network 27 | const isIP = /^[\d.:]+$/.test(host); 28 | switch (true) { 29 | // --- localhost & 30 | // case host === 'localhost': 31 | // plain hostname (no dots) 32 | case !host.includes('.'): 33 | // *.localhost 34 | case host.endsWith('.localhost'): 35 | 36 | // --- IPv4 37 | // case host === '127.0.0.1': 38 | // 127.0.0.1 up to 127.255.255.254 39 | case isIP && host.startsWith('127.'): 40 | // 169.254.0.0/16 - 169.254.0.0 to 169.254.255.255 41 | case isIP && host.startsWith('169.254.'): 42 | // 192.168.0.0/16 - 192.168.0.0 to 192.168.255.255 43 | case isIP && host.startsWith('192.168.'): 44 | 45 | // --- IPv6 46 | // case host === '[::1]': 47 | // literal IPv6 [::1]:80 with/without port 48 | case host.startsWith('[::1]'): 49 | // literal IPv6 [FE80::]/10 50 | case host.toUpperCase().startsWith('[FE80::]'): 51 | return true; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/content/get-location.js: -------------------------------------------------------------------------------- 1 | import {Spinner} from './spinner.js'; 2 | 3 | // ---------- Get Location (Side Effect) ------------------- 4 | class GetLocation { 5 | 6 | static { 7 | this.proxyDiv = document.querySelector('div.proxy-div'); 8 | document.querySelector('.proxy-top button[data-i18n="getLocation"]').addEventListener('click', () => this.process()); 9 | } 10 | 11 | static async process() { 12 | const ignore = ['127.0.0.1', 'localhost']; 13 | 14 | let {data} = await browser.storage.local.get({data: []}); 15 | data = data.filter(i => i.type !== 'direct' && !ignore.includes(i.hostname)).map(i => i.hostname); 16 | if (!data[0]) { return; } 17 | 18 | // remove duplicates 19 | const hosts = [...new Set(data)]; 20 | 21 | Spinner.show(); 22 | 23 | fetch('https://getfoxyproxy.org/webservices/lookup.php?' + hosts.join('&')) 24 | .then(response => response.json()) 25 | .then(json => this.updateLocation(json)) 26 | .catch(error => { 27 | Spinner.hide(); 28 | alert(error); 29 | }); 30 | } 31 | 32 | static updateLocation(json) { 33 | // update display 34 | this.proxyDiv.querySelectorAll('[data-id="cc"], [data-id="city"]').forEach(i => { 35 | const {hostname, id} = i.dataset; 36 | // cache old value to compare 37 | const value = i.value; 38 | json[hostname]?.[id] && (i.value = json[hostname][id]); 39 | // dispatch change event 40 | id === 'cc' && i.value !== value && i.dispatchEvent(new Event('change')); 41 | }); 42 | 43 | Spinner.hide(); 44 | } 45 | } -------------------------------------------------------------------------------- /src/content/i18n.js: -------------------------------------------------------------------------------- 1 | // ---------- Internationalization (Side Effect) ----------- 2 | class I18n { 3 | 4 | static { 5 | document.querySelectorAll('template').forEach(i => this.set(i.content)); 6 | this.set(); 7 | // show after 8 | // document.body.style.opacity = 1; 9 | } 10 | 11 | static set(target = document) { 12 | target.querySelectorAll('[data-i18n]').forEach(elem => { 13 | let [text, attr] = elem.dataset.i18n.split('|'); 14 | text = browser.i18n.getMessage(text); 15 | attr ? elem.setAttribute(attr, text) : elem.append(text); 16 | }); 17 | } 18 | } -------------------------------------------------------------------------------- /src/content/iframe.css: -------------------------------------------------------------------------------- 1 | @import 'default.css'; 2 | @import 'theme.css'; 3 | 4 | /* ----- General ----- */ 5 | :root { 6 | --nav-height: 2.5rem; 7 | } 8 | 9 | html { 10 | scroll-padding-top: calc(var(--nav-height) + 0.5rem); 11 | } 12 | 13 | body { 14 | /* Chrome sets font-size to 75% (16px x 75% = 12px) */ 15 | font-size: unset; 16 | } 17 | 18 | img { 19 | vertical-align: text-bottom; 20 | } 21 | 22 | article { 23 | padding: 2em; 24 | background-color: var(--bg); 25 | } 26 | 27 | 28 | /* ----- h1-h5 ----- */ 29 | h2 { 30 | color: var(--header); 31 | font-size: 2.5em; 32 | border-bottom: 1px solid var(--border); 33 | font-weight: normal; 34 | } 35 | 36 | h2:first-of-type { 37 | margin-top: 0; 38 | } 39 | 40 | h3 { 41 | font-size: 1.5em; 42 | font-weight: normal; 43 | } 44 | 45 | h4 { 46 | font-size: 1.2em; 47 | } 48 | 49 | h5 { 50 | font-size: 1em; 51 | } 52 | 53 | :not(h2) + h3 { 54 | margin-top: 1.5em; 55 | } 56 | 57 | :is(h1, h2, h3, h4, h5) span { 58 | color: var(--dim); 59 | font-size: 0.8em; 60 | font-style: italic; 61 | font-weight: normal; 62 | margin-left: 0.5em; 63 | } 64 | /* ----- /h1-h5 ----- */ 65 | 66 | p { 67 | margin-top: 0.5em; 68 | margin-bottom: 0.5em; 69 | } 70 | 71 | pre { 72 | border-left: 3px solid #ccc; 73 | padding: 0.5em 1em; 74 | } 75 | 76 | code { 77 | padding: 0 0.3em; 78 | background-color: var(--hover); 79 | font-size: 1.1em; 80 | } 81 | 82 | blockquote { 83 | color: var(--color); 84 | padding: 1em 3.5em; 85 | font-style: italic; 86 | position: relative; 87 | font-size: 0.9em; 88 | } 89 | 90 | blockquote::before, 91 | blockquote::after { 92 | color: #ccc; 93 | opacity: 0.6; 94 | font-size: 4em; 95 | position: absolute; 96 | content: '❝'; 97 | top: 0; 98 | left: 0.1em; 99 | } 100 | 101 | blockquote::after { 102 | bottom: -0.5em; 103 | right: 0.5em; 104 | } 105 | 106 | cite { 107 | display: block; 108 | margin-top: 1em; 109 | color: #999; 110 | } 111 | 112 | cite::before { 113 | content: '— source: '; 114 | } 115 | 116 | img.figure { 117 | border-radius: 1em; 118 | border: 4px solid var(--nav-hover); 119 | width: 140px; 120 | } 121 | 122 | dt { 123 | font-size: 1.1em; 124 | } 125 | 126 | dd + dt { 127 | margin-top: 1em; 128 | } 129 | 130 | dd > dl { 131 | margin: 1em auto; 132 | } 133 | 134 | ol, ul { 135 | margin: 0; 136 | } 137 | 138 | th span, 139 | dt span, 140 | dd span, 141 | li span { 142 | margin-left: 0.5em; 143 | color: var(--dim); 144 | font-style: italic; 145 | font-weight: normal; 146 | letter-spacing: normal; 147 | font-size: 0.9em; 148 | } 149 | 150 | mark { 151 | color: var(--header); 152 | background-color: unset; 153 | } 154 | 155 | .scroll { 156 | max-height: 25em; 157 | overflow: auto; 158 | } 159 | 160 | /* ----- About ----- */ 161 | .about dt { 162 | display: table; 163 | border-bottom: 1px solid var(--border); 164 | margin-bottom: 0.2em; 165 | min-width: 15vw; 166 | font-weight: bold; 167 | } 168 | /* ----- /About ----- */ 169 | 170 | /* ----- Navigation ----- */ 171 | nav { 172 | background-color: var(--bg); 173 | height: var(--nav-height); 174 | position: sticky; 175 | top: 0; 176 | z-index: 1; 177 | box-shadow: 0 3px 6px #0004; 178 | display: grid; 179 | grid-auto-flow: column; 180 | justify-content: start; 181 | align-items: center; 182 | } 183 | 184 | nav a { 185 | color: var(--color); 186 | padding: 0.5em 1em; 187 | } 188 | 189 | nav a:hover { 190 | background-color: var(--hover); 191 | } 192 | 193 | /* ----- /Navigation ----- */ 194 | 195 | /* ----- Table ----- */ 196 | table { 197 | border-collapse: collapse; 198 | border: 1px solid var(--border); 199 | margin-bottom: 1em; 200 | width: calc(100% - 2.5rem); 201 | font-size: 0.9em; 202 | } 203 | 204 | caption { 205 | padding: 0.5em; 206 | } 207 | 208 | tr:nth-child(2n) { 209 | background-color: var(--alt-bg); 210 | } 211 | 212 | th, 213 | td { 214 | border: 1px solid var(--border); 215 | vertical-align: top; 216 | padding: 0.5em; 217 | } 218 | 219 | thead th { 220 | font-size: 1.2em; 221 | } 222 | 223 | tbody th { 224 | min-width: 10em; 225 | text-align: left; 226 | } 227 | 228 | td pre, 229 | .code td, 230 | td.code { 231 | font-family: monospace; 232 | font-size: 1.2em; 233 | } 234 | 235 | .slim th, 236 | .slim td { 237 | padding: 0.2em 0.5em; 238 | } 239 | /* ----- /Table ----- */ 240 | 241 | /* ----- note, footnote, warning, experimental ----- */ 242 | .note, 243 | .warning { 244 | border: 1px solid var(--border); 245 | border-radius: 0.5em; 246 | border-left: 3px solid #17f; 247 | padding: 0.3em 0.5em 0.3em 2em; 248 | margin-top: 0.5em; 249 | position: relative; 250 | display: table; 251 | } 252 | 253 | .note::before, 254 | .warning::before { 255 | content: 'ⓘ'; 256 | color: #17f; 257 | position: absolute; 258 | top: 0; 259 | left: 0.5em; 260 | } 261 | 262 | .warning { 263 | border-left-color: #f90; 264 | } 265 | 266 | .warning::before { 267 | content: '⚠️'; 268 | } 269 | 270 | .experimental::after { 271 | content: ''; 272 | background: url('../image/beaker.svg') no-repeat center / contain; 273 | display: inline-block; 274 | width: 1em; 275 | height: 1em; 276 | margin-left: 0.5em; 277 | } 278 | 279 | .footnote { 280 | font-size: 0.9em; 281 | font-style: italic; 282 | } 283 | 284 | /* ----- span links ----- */ 285 | .chrome-extension, 286 | .moz-extension { 287 | cursor: pointer; 288 | font-style: normal; 289 | font-size: 0.8em; 290 | display: none; 291 | } 292 | 293 | /* ----- Translate ----- */ 294 | .translate { 295 | display: grid; 296 | grid-auto-flow: column; 297 | justify-content: end; 298 | align-items: center; 299 | gap: 0.5em; 300 | padding: 1em 1em 0; 301 | background-color: var(--bg); 302 | } 303 | 304 | .translate select { 305 | width: auto; 306 | } 307 | 308 | .translate input[type="submit"] { 309 | color: inherit; 310 | background-color: var(--alt-bg); 311 | border: 1px solid var(--border); 312 | border-radius: 0.3em; 313 | padding: 0.2em 0.5em; 314 | } 315 | /* ----- /Translate ----- */ -------------------------------------------------------------------------------- /src/content/import-account.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | import {Proxies} from './options-proxies.js'; 3 | import {Spinner} from './spinner.js'; 4 | import {Toggle} from './toggle.js'; 5 | import {Nav} from './nav.js'; 6 | 7 | // ---------- Import FoxyProxy Account (Side Effect) ------- 8 | class ImportAccount { 9 | 10 | static { 11 | this.username = document.querySelector('.import-account #username'); 12 | this.password = document.querySelector('.import-account #password'); 13 | Toggle.password(this.password.nextElementSibling); 14 | document.querySelector('.import-account button[data-i18n="import"]').addEventListener('click', () => this.process()); 15 | } 16 | 17 | static async process() { 18 | // --- check username/password 19 | const username = this.username.value.trim(); 20 | const password = this.password.value.trim(); 21 | if (!username || !password) { 22 | alert(browser.i18n.getMessage('userPassError')); 23 | return; 24 | } 25 | 26 | Spinner.show(); 27 | 28 | const options = [...document.querySelectorAll('.import-account .account-options select')].map(i => i.value); 29 | // Array(3) [ "https", "hostname", "alt" ] 30 | const url = options.includes('alt') ? 31 | 'https://bilestoad.com/webservices/get-accounts.php' : 32 | 'https://getfoxyproxy.org/webservices/get-accounts.php'; 33 | const ip = options.includes('ip'); 34 | const socks = options.includes('socks5'); 35 | const https = options.includes('https'); 36 | 37 | const proxyDiv = document.querySelector('div.proxy-div'); 38 | const docFrag = document.createDocumentFragment(); 39 | 40 | // --- fetch data 41 | const data = await this.getAccount(url, username, password); 42 | if (data) { 43 | data.forEach(i => { 44 | // proxy template 45 | const pxy = { 46 | active: true, 47 | title: i.hostname.split('.')[0], 48 | type: socks ? 'socks5' : https ? 'https' : 'http', 49 | hostname: ip ? i.ip : i.hostname, 50 | port: socks ? i.socks5_port : https ? i.ssl_port : i.port[0], 51 | username: i.username, 52 | password: i.password, 53 | // convert UK to ISO 3166-1 GB 54 | cc: i.country_code === 'UK' ? 'GB' : i.country_code, 55 | city: i.city, 56 | // random color will be set 57 | color: '', 58 | pac: '', 59 | pacString: '', 60 | proxyDNS: true, 61 | include: [], 62 | exclude: [], 63 | tabProxy: [], 64 | }; 65 | 66 | docFrag.append(Proxies.addProxy(pxy)); 67 | }); 68 | 69 | proxyDiv.append(docFrag); 70 | Nav.get('proxies'); 71 | } 72 | 73 | Spinner.hide(); 74 | } 75 | 76 | static async getAccount(url, username, password) { 77 | // --- fetch data 78 | return fetch(url, { 79 | method: 'POST', 80 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 81 | body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}` 82 | }) 83 | .then(response => response.json()) 84 | .then(data => { 85 | if (!Array.isArray(data) || !data[0]?.hostname) { 86 | App.notify(browser.i18n.getMessage('error')); 87 | return; 88 | } 89 | 90 | // import active accounts only 91 | data = data.filter(i => i.active === 'true'); 92 | // sort by country 93 | data.sort((a, b) => a.country.localeCompare(b.country)); 94 | return data; 95 | }) 96 | .catch(error => App.notify(browser.i18n.getMessage('error') + '\n\n' + error.message)); 97 | } 98 | } -------------------------------------------------------------------------------- /src/content/import-export.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | // ---------- Import/Export Preferences -------------------- 4 | export class ImportExport { 5 | 6 | // pref references the same object in the memory and its value gets updated 7 | static init(pref, callback) { 8 | this.callback = callback; 9 | document.getElementById('file').addEventListener('change', e => this.import(e, pref)); 10 | document.getElementById('export').addEventListener('click', () => this.export(pref)); 11 | } 12 | 13 | // import preferences 14 | static import(e, pref) { 15 | const file = e.target.files[0]; 16 | switch (true) { 17 | case !file: App.notify(browser.i18n.getMessage('error')); 18 | return; 19 | // check file MIME type 20 | case !['text/plain', 'application/json'].includes(file.type): 21 | App.notify(browser.i18n.getMessage('fileTypeError')); 22 | return; 23 | } 24 | 25 | this.fileReader(file, r => this.readData(r, pref)); 26 | } 27 | 28 | static readData(data, pref) { 29 | try { data = JSON.parse(data); } 30 | catch { 31 | // display the error 32 | App.notify(browser.i18n.getMessage('fileParseError')); 33 | return; 34 | } 35 | 36 | // update pref with the saved version 37 | Object.keys(pref).forEach(i => Object.hasOwn(data, i) && (pref[i] = data[i])); 38 | 39 | // successful import 40 | this.callback(); 41 | } 42 | 43 | // export preferences 44 | static export(pref, saveAs = true, folder = '') { 45 | const data = JSON.stringify(pref, null, 2); 46 | const filename = `${folder}${browser.i18n.getMessage('extensionName')}_${new Date().toISOString().substring(0, 10)}.json`; 47 | this.saveFile({data, filename, type: 'application/json', saveAs}); 48 | } 49 | 50 | static saveFile({data, filename, saveAs = true, type = 'text/plain'}) { 51 | if (!browser.downloads) { 52 | const a = document.createElement('a'); 53 | a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(data); 54 | a.setAttribute('download', filename); 55 | a.dispatchEvent(new MouseEvent('click')); 56 | return; 57 | } 58 | 59 | const blob = new Blob([data], {type}); 60 | browser.downloads.download({ 61 | url: URL.createObjectURL(blob), 62 | filename, 63 | saveAs, 64 | conflictAction: 'uniquify' 65 | }) 66 | // eslint-disable-next-line no-console 67 | .catch(console.log); 68 | } 69 | 70 | static fileReader(file, callback) { 71 | const reader = new FileReader(); 72 | reader.onloadend = () => callback(reader.result); 73 | reader.onerror = () => App.notify(browser.i18n.getMessage('fileReadError')); 74 | reader.readAsText(file); 75 | } 76 | } -------------------------------------------------------------------------------- /src/content/import-list.js: -------------------------------------------------------------------------------- 1 | import {Proxies} from './options-proxies.js'; 2 | import {Nav} from './nav.js'; 3 | 4 | // ---------- Import List (Side Effect) -------------------- 5 | class ImportList { 6 | 7 | static { 8 | this.textarea = document.querySelector('.import-proxy-list textarea'); 9 | document.querySelector('.import-proxy-list button').addEventListener('click', () => this.process()); 10 | } 11 | 12 | static process() { 13 | this.textarea.value = this.textarea.value.trim(); 14 | if (!this.textarea.value) { return; } 15 | 16 | const proxyDiv = document.querySelector('div.proxy-div'); 17 | const docFrag = document.createDocumentFragment(); 18 | 19 | for (const item of this.textarea.value.split(/\n+/)) { 20 | // simple vs Extended format 21 | const pxy = item.includes('://') ? this.parseExtended(item) : this.parseSimple(item); 22 | // end on error 23 | if (!pxy) { return; } 24 | 25 | docFrag.append(Proxies.addProxy(pxy)); 26 | } 27 | 28 | proxyDiv.append(docFrag); 29 | Nav.get('proxies'); 30 | } 31 | 32 | static parseSimple(item) { 33 | // example.com:3128:user:pass 34 | const [hostname, port, username = '', password = ''] = item.split(':'); 35 | if (!hostname || !(port * 1)) { 36 | alert(`Error: ${item}`); 37 | return; 38 | } 39 | 40 | const type = port === '443' ? 'https' : 'http'; 41 | 42 | // proxy template 43 | const pxy = { 44 | active: true, 45 | title: '', 46 | type, 47 | hostname, 48 | port, 49 | username, 50 | password, 51 | cc: '', 52 | city: '', 53 | color: '', 54 | pac: '', 55 | pacString: '', 56 | proxyDNS: true, 57 | include: [], 58 | exclude: [], 59 | tabProxy: [], 60 | }; 61 | 62 | return pxy; 63 | } 64 | 65 | static parseExtended(item) { 66 | // https://user:password@78.205.12.1:21?color=ff00bc&title=work%20proxy 67 | // https://example.com:443?active=false&title=Work&username=abcd&password=1234&cc=US&city=Miami 68 | let url; 69 | try { url = new URL(item); } 70 | catch (error) { 71 | alert(`${error}\n\n${item}`); 72 | return; 73 | } 74 | 75 | // convert old schemes to type 76 | let type = url.protocol.slice(0, -1); 77 | const scheme = { 78 | proxy: 'http', 79 | ssl: 'https', 80 | socks: 'socks5', 81 | }; 82 | scheme[type] && (type = scheme[type]); 83 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1851426 84 | // Reland URL: protocol setter needs to be more restrictive around file (fixed in Firefox 120) 85 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1603699 86 | // Enable DefaultURI use for unknown schemes (fixed in Firefox 122) 87 | // missing hostname, port with socks protocol (#120) 88 | !url.hostname && (url = new URL('http:' + item.substring(url.protocol.length))); 89 | 90 | const {hostname, port, username, password} = url; 91 | // set to pram, can be overridden in searchParams 92 | const pram = {type, hostname, port, username, password}; 93 | 94 | // prepare object, make parameter keys case-insensitive 95 | for (const [key, value] of url.searchParams) { 96 | pram[key.toLowerCase()] = value; 97 | } 98 | 99 | // fix missing default port 100 | const defaultPort = { 101 | http: '80', 102 | https: '443', 103 | ws: '80', 104 | wss: '443' 105 | }; 106 | !pram.port && defaultPort[type] && (pram.port = defaultPort[type]); 107 | 108 | // proxy template 109 | const pxy = { 110 | // defaults to true 111 | active: pram.active !== 'false', 112 | title: pram.title || '', 113 | type: pram.type.toLowerCase(), 114 | hostname: pram.hostname, 115 | port: pram.port, 116 | username: decodeURIComponent(pram.username), 117 | password: decodeURIComponent(pram.password), 118 | cc: (pram.cc || pram.countrycode || '').toUpperCase(), 119 | city: pram.city || '', 120 | color: pram.color ? '#' + pram.color : '', 121 | pac: pram.pac || (pram.type === 'pac' && url.origin + url.pathname) || '', 122 | pacString: '', 123 | // defaults to true 124 | proxyDNS: pram.proxydns !== 'false', 125 | include: [], 126 | exclude: [], 127 | tabProxy: [], 128 | }; 129 | 130 | return pxy; 131 | } 132 | } -------------------------------------------------------------------------------- /src/content/import-older.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | import {Migrate} from './migrate.js'; 3 | 4 | // ---------- Import Older Preferences --------------------- 5 | export class ImportOlder { 6 | 7 | // pref references the same object in the memory and its value gets updated 8 | static init(pref, callback) { 9 | this.callback = callback; 10 | document.querySelector('.import-from-older input').addEventListener('change', e => this.process(e, pref)); 11 | } 12 | 13 | static process(e, pref) { 14 | const file = e.target.files[0]; 15 | switch (true) { 16 | case !file: App.notify(browser.i18n.getMessage('error')); return; 17 | // check file MIME type 18 | case !['text/plain', 'application/json'].includes(file.type): 19 | App.notify(browser.i18n.getMessage('fileTypeError')); 20 | return; 21 | } 22 | 23 | const reader = new FileReader(); 24 | reader.onloadend = () => this.parseJSON(reader.result, pref); 25 | reader.onerror = () => App.notify(browser.i18n.getMessage('fileReadError')); 26 | reader.readAsText(file); 27 | } 28 | 29 | static parseJSON(data, pref) { 30 | try { data = JSON.parse(data); } 31 | catch { 32 | // display the error 33 | App.notify(browser.i18n.getMessage('fileParseError')); 34 | return; 35 | } 36 | 37 | data = Object.hasOwn(data, 'settings') ? Migrate.convert3(data) : Migrate.convert7(data); 38 | // update pref with the saved version 39 | Object.keys(pref).forEach(i => Object.hasOwn(data, i) && (pref[i] = data[i])); 40 | 41 | this.callback(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/content/import-url.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | import {Spinner} from './spinner.js'; 3 | 4 | // ---------- Import from URL ------------------------------ 5 | export class ImportUrl { 6 | 7 | static { 8 | this.input = document.querySelector('.import-from-url input'); 9 | } 10 | 11 | // pref references the same object in the memory and its value gets updated 12 | static init(pref, callback) { 13 | this.callback = callback; 14 | document.querySelector('.import-from-url button').addEventListener('click', () => this.process(pref)); 15 | } 16 | 17 | static process(pref) { 18 | this.input.value = this.input.value.trim(); 19 | if (!this.input.value) { return; } 20 | 21 | Spinner.show(); 22 | 23 | // --- fetch data 24 | fetch(this.input.value) 25 | .then(response => response.json()) 26 | .then(data => { 27 | // update pref with the saved version 28 | Object.keys(pref).forEach(i => Object.hasOwn(data, i) && (pref[i] = data[i])); 29 | 30 | this.callback(); 31 | Spinner.hide(); 32 | }) 33 | .catch(error => { 34 | App.notify(browser.i18n.getMessage('error') + '\n\n' + error.message); 35 | Spinner.hide(); 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /src/content/incognito-access.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | // ---------- Incognito Access (Side Effect) --------------- 4 | class IncognitoAccess { 5 | 6 | static { 7 | // https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/proxy/settings 8 | // Changing proxy settings requires private browsing window access because proxy settings affect private and non-private windows. 9 | // https://github.com/w3c/webextensions/issues/429 10 | // Inconsistency: incognito in proxy.settings 11 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1725981 12 | // proxy.settings is not supported on Android 13 | App.firefox && !App.android && browser.extension.isAllowedIncognitoAccess() 14 | .then(response => !response && alert(browser.i18n.getMessage('incognitoAccessError'))); 15 | } 16 | } -------------------------------------------------------------------------------- /src/content/location.js: -------------------------------------------------------------------------------- 1 | export class Location { 2 | 3 | static get(cc = '') { 4 | return this.countryCode[cc] || ''; 5 | } 6 | 7 | // ISO 3166-1 country code 8 | static countryCode = { 9 | AA: "", 10 | AD: "Andorra", 11 | AE: "United Arab Emirates", 12 | AF: "Afghanistan", 13 | AG: "Antigua and Barbuda", 14 | AI: "Anguilla", 15 | AL: "Albania", 16 | AM: "Armenia", 17 | AO: "Angola", 18 | AQ: "Antarctica", 19 | AR: "Argentina", 20 | AS: "American Samoa", 21 | AT: "Austria", 22 | AU: "Australia", 23 | AW: "Aruba", 24 | AX: "Åland Islands", 25 | AZ: "Azerbaijan", 26 | BA: "Bosnia and Herzegovina", 27 | BB: "Barbados", 28 | BD: "Bangladesh", 29 | BE: "Belgium", 30 | BF: "Burkina Faso", 31 | BG: "Bulgaria", 32 | BH: "Bahrain", 33 | BI: "Burundi", 34 | BJ: "Benin", 35 | BL: "Saint Barthélemy", 36 | BM: "Bermuda", 37 | BN: "Brunei Darussalam", 38 | BO: "Bolivia, Plurinational State of", 39 | BQ: "Bonaire, Sint Eustatius and Saba", 40 | BR: "Brazil", 41 | BS: "Bahamas", 42 | BT: "Bhutan", 43 | BV: "Bouvet Island", 44 | BW: "Botswana", 45 | BY: "Belarus", 46 | BZ: "Belize", 47 | CA: "Canada", 48 | CC: "Cocos [Keeling] Islands", 49 | CD: "Congo, the Democratic Republic of the", 50 | CF: "Central African Republic", 51 | CG: "Congo, Republic of the", 52 | CH: "Switzerland", 53 | CI: "Ivory Coast (Côte d'Ivoire)", 54 | CK: "Cook Islands", 55 | CL: "Chile", 56 | CM: "Cameroon", 57 | CN: "China", 58 | CO: "Colombia", 59 | CR: "Costa Rica", 60 | CU: "Cuba", 61 | CV: "Cabo Verde", 62 | CW: "Curaçao", 63 | CX: "Christmas Island", 64 | CY: "Cyprus", 65 | CZ: "Czechia", 66 | DE: "Germany", 67 | DJ: "Djibouti", 68 | DK: "Denmark", 69 | DM: "Dominica", 70 | DO: "Dominican Republic", 71 | DZ: "Algeria", 72 | EC: "Ecuador", 73 | EE: "Estonia", 74 | EG: "Egypt", 75 | EH: "Western Sahara", 76 | ER: "Eritrea", 77 | ES: "Spain", 78 | ET: "Ethiopia", 79 | EU: "European Union", 80 | FI: "Finland", 81 | FJ: "Fiji", 82 | FK: "Falkland Islands (Malvinas)", 83 | FM: "Micronesia, Federated States of", 84 | FO: "Faroe Islands", 85 | FR: "France", 86 | GA: "Gabon", 87 | GB: "United Kingdom", 88 | GD: "Grenada", 89 | GE: "Georgia", 90 | GF: "French Guiana", 91 | GG: "Guernsey", 92 | GH: "Ghana", 93 | GI: "Gibraltar", 94 | GL: "Greenland", 95 | GM: "Gambia", 96 | GN: "Guinea", 97 | GP: "Guadeloupe", 98 | GQ: "Equatorial Guinea", 99 | GR: "Greece", 100 | GS: "South Georgia and the South Sandwich Islands", 101 | GT: "Guatemala", 102 | GU: "Guam", 103 | GW: "Guinea-Bissau", 104 | GY: "Guyana", 105 | HK: "Hong Kong", 106 | HM: "Heard Island and McDonald Islands", 107 | HN: "Honduras", 108 | HR: "Croatia", 109 | HT: "Haiti", 110 | HU: "Hungary", 111 | ID: "Indonesia", 112 | IE: "Ireland", 113 | IL: "Israel", 114 | IM: "Isle of Man", 115 | IN: "India", 116 | IO: "British Indian Ocean Territory", 117 | IQ: "Iraq", 118 | IR: "Iran, Islamic Republic Of", 119 | IS: "Iceland", 120 | IT: "Italy", 121 | JE: "Jersey", 122 | JM: "Jamaica", 123 | JO: "Jordan (Hashemite Kingdom of Jordan)", 124 | JP: "Japan", 125 | KE: "Kenya", 126 | KG: "Kyrgyzstan", 127 | KH: "Cambodia", 128 | KI: "Kiribati", 129 | KM: "Comoros", 130 | KN: "St Kitts and Nevis", 131 | KP: "North Korea", 132 | KR: "South Korea", 133 | KW: "Kuwait", 134 | KY: "Cayman Islands", 135 | KZ: "Kazakhstan", 136 | LA: "Laos (Lao People's Democratic Republic)", 137 | LB: "Lebanon", 138 | LC: "Saint Lucia", 139 | LI: "Liechtenstein", 140 | LK: "Sri Lanka", 141 | LR: "Liberia", 142 | LS: "Lesotho", 143 | LT: "Republic of Lithuania", 144 | LU: "Luxembourg", 145 | LV: "Latvia", 146 | LY: "Libya", 147 | MA: "Morocco", 148 | MC: "Monaco", 149 | MD: "Moldova, Republic of", 150 | ME: "Montenegro", 151 | MF: "Saint Martin (French part)", 152 | MG: "Madagascar", 153 | MH: "Marshall Islands", 154 | MK: "North Macedonia", 155 | ML: "Mali", 156 | MM: "Myanmar", 157 | MN: "Mongolia", 158 | MO: "Macao", 159 | MP: "Northern Mariana Islands", 160 | MQ: "Martinique", 161 | MR: "Mauritania", 162 | MS: "Montserrat", 163 | MT: "Malta", 164 | MU: "Mauritius", 165 | MV: "Maldives", 166 | MW: "Malawi", 167 | MX: "Mexico", 168 | MY: "Malaysia", 169 | MZ: "Mozambique", 170 | NA: "Namibia", 171 | NC: "New Caledonia", 172 | NE: "Niger", 173 | NF: "Norfolk Island", 174 | NG: "Nigeria", 175 | NI: "Nicaragua", 176 | NL: "Netherlands", 177 | NO: "Norway", 178 | NP: "Nepal", 179 | NR: "Nauru", 180 | NU: "Niue", 181 | NZ: "New Zealand", 182 | OM: "Oman", 183 | PA: "Panama", 184 | PE: "Peru", 185 | PF: "French Polynesia", 186 | PG: "Papua New Guinea", 187 | PH: "Philippines", 188 | PK: "Pakistan", 189 | PL: "Poland", 190 | PM: "Saint Pierre and Miquelon", 191 | PN: "Pitcairn Islands", 192 | PR: "Puerto Rico", 193 | PS: "Palestine", 194 | PT: "Portugal", 195 | PW: "Palau", 196 | PY: "Paraguay", 197 | QA: "Qatar", 198 | RE: "Réunion", 199 | RO: "Romania", 200 | RS: "Serbia", 201 | RU: "Russia (Russian Federation)", 202 | RW: "Rwanda", 203 | SA: "Saudi Arabia", 204 | SB: "Solomon Islands", 205 | SC: "Seychelles", 206 | SD: "Sudan", 207 | SE: "Sweden", 208 | SG: "Singapore", 209 | SH: "Saint Helena", 210 | SI: "Slovenia", 211 | SJ: "Svalbard and Jan Mayen", 212 | SK: "Slovakia", 213 | SL: "Sierra Leone", 214 | SM: "San Marino", 215 | SN: "Senegal", 216 | SO: "Somalia", 217 | SR: "Suriname", 218 | SS: "South Sudan", 219 | ST: "São Tomé and Príncipe", 220 | SV: "El Salvador", 221 | SX: "Sint Maarten (Dutch part)", 222 | SY: "Syria", 223 | SZ: "Eswatini", 224 | TC: "Turks and Caicos Islands", 225 | TD: "Chad", 226 | TF: "French Southern Territories", 227 | TG: "Togo", 228 | TH: "Thailand", 229 | TJ: "Tajikistan", 230 | TK: "Tokelau", 231 | TL: "Democratic Republic of Timor-Leste", 232 | TM: "Turkmenistan", 233 | TN: "Tunisia", 234 | TO: "Tonga", 235 | TR: "Türkiye", 236 | TT: "Trinidad and Tobago", 237 | TV: "Tuvalu", 238 | TW: "Taiwan", 239 | TZ: "Tanzania", 240 | UA: "Ukraine", 241 | UG: "Uganda", 242 | UM: "U.S. Minor Outlying Islands", 243 | US: "United States of America", 244 | UY: "Uruguay", 245 | UZ: "Uzbekistan", 246 | VA: "Vatican City", 247 | VC: "Saint Vincent and the Grenadines", 248 | VE: "Venezuela", 249 | VG: "British Virgin Islands", 250 | VI: "U.S. Virgin Islands", 251 | VN: "Vietnam", 252 | VU: "Vanuatu", 253 | WF: "Wallis and Futuna", 254 | WS: "Samoa", 255 | XK: "Kosovo", 256 | YE: "Yemen", 257 | YT: "Mayotte", 258 | ZA: "South Africa", 259 | ZM: "Zambia", 260 | ZW: "Zimbabwe", 261 | }; 262 | } -------------------------------------------------------------------------------- /src/content/log.css: -------------------------------------------------------------------------------- 1 | /* ----- Log ----- */ 2 | /* ----- Light Theme ----- */ 3 | :root { 4 | --mark: #080; 5 | } 6 | 7 | /* ----- Dark Theme ----- */ 8 | @media screen and (prefers-color-scheme: dark) { 9 | :root { 10 | --mark: #0c0; 11 | } 12 | } 13 | 14 | section.log { 15 | padding: 0 0.5em; 16 | max-width: unset; 17 | } 18 | 19 | .log .domain { 20 | display: grid; 21 | grid-template-columns: auto auto; 22 | gap: 0.5em; 23 | place-content: end; 24 | } 25 | 26 | .log table { 27 | border-collapse: collapse; 28 | margin: 0 auto; 29 | width: 100%; 30 | background-color: var(--bg); 31 | } 32 | 33 | .log thead { 34 | position: sticky; 35 | top: 0; 36 | } 37 | 38 | .log thead th { 39 | color: #fff; 40 | background-color: #999; 41 | padding: 0.5em 0.2em; 42 | font-size: 0.9em; 43 | font-weight: normal; 44 | text-align: left; 45 | } 46 | 47 | .log thead th img { 48 | width: 1em; 49 | vertical-align: unset; 50 | } 51 | 52 | .log tr:hover { 53 | background-color: var(--hover) !important; 54 | } 55 | 56 | .log tr:has(td[title*="407 Proxy"]) { 57 | color: var(--mark); 58 | } 59 | 60 | .log tbody tr:nth-of-type(even) { 61 | background-color: var(--tr); 62 | } 63 | 64 | .log td { 65 | font-size: 0.8em; 66 | padding: 0.2em; 67 | } 68 | 69 | .log td:nth-of-type(6), 70 | .log td:nth-of-type(7), 71 | .log td:nth-of-type(12) { 72 | overflow: hidden; 73 | text-overflow: ellipsis; 74 | white-space: nowrap; 75 | max-width: 20em; 76 | } 77 | 78 | .log td:nth-of-type(7) { 79 | max-width: 40em; 80 | } 81 | 82 | .log td:nth-of-type(8) { 83 | border-left: 2px solid var(--border); 84 | } 85 | 86 | .log td:nth-of-type(12) { 87 | max-width: 5em; 88 | } 89 | 90 | .log td.incognito::before { 91 | content: ''; 92 | width: 1em; 93 | height: 1em; 94 | display: inline-block; 95 | vertical-align: text-bottom; 96 | background: url('../image/privateBrowsing.svg') no-repeat center / contain; 97 | } 98 | 99 | .log tbody { 100 | counter-reset: n; 101 | } 102 | 103 | .log tbody tr td:first-child::before { 104 | display: inline-block; 105 | color: #aaa; 106 | min-width: 1.5em; 107 | text-align: right; 108 | vertical-align: middle; 109 | margin-right: 0.4em; 110 | pointer-events: none; 111 | counter-increment: n; 112 | content: counter(n); 113 | font-size: 0.8em; 114 | } -------------------------------------------------------------------------------- /src/content/log.js: -------------------------------------------------------------------------------- 1 | import {Flag} from './flag.js'; 2 | import {Pattern} from './pattern.js'; 3 | import {Popup} from './options-popup.js'; 4 | import {Nav} from './nav.js'; 5 | 6 | export class Log { 7 | 8 | static { 9 | this.trTemplate = document.querySelector('.log template').content.firstElementChild; 10 | this.tbody = document.querySelector('.log tbody'); 11 | // used to find proxy 12 | this.proxyCache = {}; 13 | this.mode = 'disable'; 14 | 15 | browser.webRequest.onBeforeRequest.addListener(e => this.process(e), {urls: ['']}); 16 | 17 | // onAuthRequired message from authentication.js 18 | browser.runtime.onMessage.addListener((...e) => this.onMessage(...e)); 19 | 20 | // Get Associated Domains 21 | this.input = document.querySelector('.log input'); 22 | document.querySelector('.log button').addEventListener('click', () => this.getDomains()); 23 | this.select = document.querySelector('.popup select.popup-log-proxy'); 24 | this.select.addEventListener('change', () => this.addPatterns()); 25 | } 26 | 27 | static onMessage(message) { 28 | const {id, e} = message; 29 | if (id !== 'onAuthRequired') { return; } 30 | 31 | const tr = this.tbody.children[199] || this.trTemplate.cloneNode(true); 32 | const [, time, container, method, reqType, doc, url, title, type, host, port, pattern] = tr.children; 33 | 34 | time.textContent = new Date(e.timeStamp).toLocaleTimeString(); 35 | container.classList.toggle('incognito', !!e.incognito); 36 | container.textContent = e.cookieStoreId?.startsWith('firefox-container-') ? 'C' + e.cookieStoreId.substring(18) : ''; 37 | method.textContent = e.method; 38 | reqType.textContent = e.statusCode; 39 | this.prepareOverflow(doc, e.statusLine || ''); 40 | this.prepareOverflow(url, decodeURIComponent(e.url)); 41 | 42 | const info = e.challenger || {host: '', port: ''}; 43 | const item = this.proxyCache[`${info.host}:${info.port}`]; 44 | const flag = item?.cc ? Flag.get(item.cc) + ' ' : ''; 45 | title.textContent = flag + (item?.title || ''); 46 | title.style.borderLeftColor = item?.color || 'var(--border)'; 47 | type.textContent = item?.type || ''; 48 | host.textContent = info.host; 49 | port.textContent = info.port; 50 | pattern.textContent = ''; 51 | 52 | // in reverse order, new on top 53 | this.tbody.prepend(tr); 54 | } 55 | 56 | static process(e) { 57 | const tr = this.tbody.children[199] || this.trTemplate.cloneNode(true); 58 | const [, time, container, method, reqType, doc, url, title, type, host, port, pattern] = tr.children; 59 | 60 | // shortened forms similar to Developer Tools 61 | const shortType = { 62 | 'main_frame': 'html', 63 | 'sub_frame': 'iframe', 64 | image: 'img', 65 | script: 'js', 66 | stylesheet: 'css', 67 | websocket: 'ws', 68 | xmlhttprequest: 'xhr', 69 | }; 70 | 71 | time.textContent = new Date(e.timeStamp).toLocaleTimeString(); 72 | container.classList.toggle('incognito', !!e.incognito); 73 | container.textContent = e.cookieStoreId?.startsWith('firefox-container-') ? 'C' + e.cookieStoreId.substring(18) : ''; 74 | method.textContent = e.method; 75 | reqType.textContent = shortType[e.type] || e.type; 76 | // For a top-level document, documentUrl is undefined, chrome uses e.initiator 77 | this.prepareOverflow(doc, e.documentUrl || e.initiator || ''); 78 | this.prepareOverflow(url, decodeURIComponent(e.url)); 79 | 80 | const info = e.proxyInfo || {host: '', port: '', type: ''}; 81 | const item = this.proxyCache[`${info.host}:${info.port}`]; 82 | const flag = item?.cc ? Flag.get(item.cc) + ' ' : ''; 83 | title.textContent = flag + (item?.title || ''); 84 | title.style.borderLeftColor = item?.color || 'var(--border)'; 85 | type.textContent = info.type; 86 | host.textContent = info.host; 87 | port.textContent = info.port; 88 | 89 | // show matching pattern in pattern mode 90 | const pat = this.mode === 'pattern' && item?.include.find(i => new RegExp(Pattern.get(i.pattern, i.type), 'i').test(e.url)); 91 | const text = pat?.title || pat?.pattern || ''; 92 | this.prepareOverflow(pattern, text); 93 | 94 | // in reverse order, new on top 95 | this.tbody.prepend(tr); 96 | } 97 | 98 | // set title, in case text overflows 99 | static prepareOverflow(elem, value) { 100 | elem.textContent = value; 101 | elem.title = value; 102 | } 103 | 104 | static getDomains() { 105 | this.select.classList.add('on'); 106 | const input = this.input.value.trim(); 107 | if (!input) { 108 | // allow showing empty popup 109 | Popup.show(''); 110 | return; 111 | } 112 | 113 | // search Document URL column 114 | let list = document.querySelectorAll(`.log table td:nth-child(6)[title*="${input}" i]`); 115 | list = [...list].map(i => i.nextElementSibling.title.split(/\/+/)[1]); 116 | list = [...new Set(list)].sort(); 117 | 118 | // true -> show select 119 | Popup.show(list.join('\n')); 120 | } 121 | 122 | static addPatterns() { 123 | const host = this.select.value; 124 | if (!host) { return; } 125 | 126 | const text = this.select.previousElementSibling.value.trim(); 127 | if (!text) { return; } 128 | 129 | const title = this.input.value.trim(); 130 | const data = text.split(/\s+/).map(i => ({ 131 | include: 'include', 132 | type: 'wildcard', 133 | title, 134 | pattern: `://${i}/`, 135 | active: true 136 | })); 137 | 138 | Popup.hide(); 139 | Nav.get('proxies'); 140 | 141 | const ev = new CustomEvent('importPatternCustom', { 142 | detail: {host, data} 143 | }); 144 | window.dispatchEvent(ev); 145 | } 146 | } -------------------------------------------------------------------------------- /src/content/menus.js: -------------------------------------------------------------------------------- 1 | // Using contextMenus namespace for compatibility with Chrome 2 | // It's not possible to create tools menu items (contexts: ["tools_menu"]) using the contextMenus namespace. 3 | 4 | import {App} from './app.js'; 5 | import {Proxy} from './proxy.js'; 6 | import {OnRequest} from './on-request.js'; 7 | import {Flag} from './flag.js'; 8 | 9 | // ---------- Context Menu --------------------------------- 10 | export class Menus { 11 | 12 | static { 13 | // contextMenus is not supported on Android 14 | browser.contextMenus?.onClicked.addListener((...e) => this.process(...e)); 15 | this.data = []; 16 | } 17 | 18 | static init(pref) { 19 | // not available on Android 20 | if (!browser.contextMenus) { return; } 21 | 22 | this.pref = pref; 23 | 24 | // https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/extensions/api/context_menus.json 25 | // chrome.contextMenus not promise yet -> Uncaught TypeError: Cannot read properties of undefined (reading 'then') 26 | browser.contextMenus.removeAll(() => this.addMenus(pref.data)); 27 | } 28 | 29 | static addMenus(data) { 30 | // not for PAC, limit to 10 31 | this.data = data.filter(i => i.active && i.type !== 'pac').slice(0, 10); 32 | if (!this.data[0]) { return; } 33 | 34 | // --- create contextMenus 35 | // https://searchfox.org/mozilla-central/source/browser/components/extensions/parent/ext-menus.js#756 36 | // https://searchfox.org/mozilla-central/source/browser/components/extensions/parent/ext-menus.js#625-636 37 | // contexts defaults to ['page'], 'all' is also added in Firefox but not in Chrome 38 | // https://github.com/w3c/webextensions/issues/774 39 | // Inconsistency: contextMenus/Menus 40 | // child menu inherits parent's contexts but chrome has a problem with inheriting in "action" contextMenus 41 | const {basic, firefox} = App; 42 | const allowedPattern = !basic && !this.pref.managed; 43 | const documentUrlPatterns = ['http://*/*', 'https://*/*']; 44 | // menus.create requires an id for non-persistent background scripts. 45 | this.contextMenus = [ 46 | ...(allowedPattern ? [{id: 'includeHost', documentUrlPatterns}] : []), 47 | ...(allowedPattern ? [{id: 'excludeHost', documentUrlPatterns}] : []), 48 | ...(allowedPattern && firefox ? [{id: 'sep', type: 'separator', documentUrlPatterns}] : []), 49 | ...(firefox ? [{id: 'tabProxy'}] : []), 50 | ...(firefox ? [{parentId: 'tabProxy', id: 'tabProxy' + this.data.length, title: '\u00A0'}] : []), 51 | ...(firefox ? [{id: 'openLinkTabProxy', contexts: ['link']}] : []), 52 | ]; 53 | 54 | allowedPattern && this.addProxies('includeHost'); 55 | allowedPattern && this.addProxies('excludeHost'); 56 | firefox && this.addProxies('tabProxy'); 57 | firefox && this.addProxies('openLinkTabProxy'); 58 | 59 | this.contextMenus.forEach(i => { 60 | // always use the same ID for i18n 61 | i.type !== 'separator' && (i.title ||= browser.i18n.getMessage(i.id)); 62 | // add contexts 63 | // !i.parentId && (i.contexts ||= ['all']); 64 | i.contexts ||= ['all']; 65 | 66 | browser.contextMenus.create(i); 67 | }); 68 | } 69 | 70 | static addProxies(parentId) { 71 | this.data.forEach((i, index) => 72 | this.contextMenus.push({ 73 | parentId, 74 | id: parentId + index, 75 | title: Flag.get(i.cc) + ' ' + (i.title || `${i.hostname}:${i.port}`) 76 | }) 77 | ); 78 | } 79 | 80 | static async process(info, tab) { 81 | const pref = this.pref; 82 | const id = info.parentMenuItemId; 83 | const index = info.menuItemId.substring(id.length); 84 | const proxy = this.data[index]; 85 | switch (id) { 86 | case 'includeHost': 87 | case 'excludeHost': 88 | Proxy.includeHost(pref, proxy, tab, id); 89 | break; 90 | 91 | // --- firefox only 92 | case 'setTabProxy': 93 | OnRequest.setTabProxy(tab, proxy); 94 | break; 95 | 96 | case 'openLinkTabProxy': 97 | tab = await browser.tabs.create({}); 98 | OnRequest.setTabProxy(tab, proxy); 99 | browser.tabs.update(tab.id, {url: info.linkUrl}); 100 | break; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/content/nav.css: -------------------------------------------------------------------------------- 1 | /* ----- Navigation ----- */ 2 | input[name="nav"], 3 | input[type="checkbox"].control { 4 | display: none; 5 | } 6 | 7 | .nav { 8 | background-color: var(--nav-bg); 9 | } 10 | 11 | nav { 12 | display: grid; 13 | grid-auto-flow: column; 14 | justify-content: start; 15 | align-items: end; 16 | color: var(--nav-color); 17 | height: var(--nav-height); 18 | max-width: var(--max-width); 19 | margin: 0 auto; 20 | } 21 | 22 | nav img { 23 | width: 2em; 24 | margin: 0.2em 0.5em; 25 | } 26 | 27 | nav > label { 28 | padding: 0.5em 1em; 29 | /* transition: 0.5s; */ 30 | border-radius: 0.5em 0.5em 0 0; 31 | } 32 | 33 | nav > label:hover { 34 | background-color: var(--nav-hover); 35 | } 36 | 37 | /* nav > label img { 38 | width: 1em; 39 | } 40 | 41 | nav label a { 42 | color: inherit; 43 | } 44 | 45 | nav label a:hover { 46 | text-decoration: none; 47 | } */ 48 | 49 | #nav1:checked ~ article section:nth-of-type(1), 50 | #nav2:checked ~ article section:nth-of-type(2), 51 | #nav3:checked ~ article section:nth-of-type(3), 52 | #nav4:checked ~ article section:nth-of-type(4), 53 | #nav5:checked ~ article section:nth-of-type(5), 54 | #nav6:checked ~ article section:nth-of-type(6), 55 | #nav7:checked ~ article section:nth-of-type(7), 56 | #nav8:checked ~ article section:nth-of-type(8) { 57 | display: block; 58 | /* animation: sect 0.5s ease-in-out; */ 59 | } 60 | 61 | #nav1:checked ~ .nav label[for="nav1"], 62 | #nav2:checked ~ .nav label[for="nav2"], 63 | #nav3:checked ~ .nav label[for="nav3"], 64 | #nav4:checked ~ .nav label[for="nav4"], 65 | #nav5:checked ~ .nav label[for="nav5"], 66 | #nav6:checked ~ .nav label[for="nav6"], 67 | #nav7:checked ~ .nav label[for="nav7"], 68 | #nav8:checked ~ .nav label[for="nav8"] { 69 | background-color: var(--body-bg); 70 | } 71 | 72 | /* @keyframes sect { 73 | 0% { opacity: 0; } 74 | 100% { opacity: 1; } 75 | } */ -------------------------------------------------------------------------------- /src/content/nav.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | export class Nav { 4 | 5 | static { 6 | document.querySelectorAll('label[for^="nav"]').forEach(i => 7 | this[i.dataset.i18n] = i.control); 8 | } 9 | 10 | static get(pram = location.search.substring(1)) { 11 | pram && this[pram] && (this[pram].checked = true); 12 | } 13 | 14 | static { 15 | // --- openShortcutSettings FF137 16 | const shortcut = document.querySelector('.shortcut-link'); 17 | // commands is not supported on Android 18 | if (!App.firefox || browser.commands?.openShortcutSettings) { 19 | shortcut.style.display = 'unset'; 20 | shortcut.addEventListener('click', () => 21 | App.firefox ? browser.commands?.openShortcutSettings() : 22 | browser.tabs.create({url: 'chrome://extensions/shortcuts'}) 23 | ); 24 | } 25 | 26 | // help document 27 | const help = document.querySelector('iframe[src="help.html"]').contentDocument; 28 | 29 | // --- data-link 30 | const helpLink = help.querySelector('.nav-link'); 31 | document.querySelectorAll('[data-link]').forEach(i => i.addEventListener('click', e => { 32 | const {link} = e.target.dataset; 33 | if (!link) { return; } 34 | 35 | Nav.get('help'); 36 | helpLink.href = link; 37 | helpLink.click(); 38 | })); 39 | 40 | // --- Extension link in the Help 41 | // not for Firefox 42 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1956860 43 | 44 | // chrome 45 | if (!App.firefox) { 46 | const link = help.querySelector('.chrome-extension'); 47 | link.style.display = 'unset'; 48 | link.addEventListener('click', () => 49 | browser.tabs.create({url: 'chrome://extensions/?id=' + location.hostname})); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/content/options-filter.js: -------------------------------------------------------------------------------- 1 | // ---------- Filter Proxy (Side Effect) ------------------- 2 | class Filter { 3 | 4 | static { 5 | this.list = document.querySelector('div.proxy-div'); 6 | const filter = document.querySelector('.filter'); 7 | filter.addEventListener('input', e => this.process(e)); 8 | } 9 | 10 | static process(e) { 11 | const str = e.target.value.toLowerCase().trim(); 12 | const elem = [...this.list.children]; 13 | if (!str) { 14 | elem.forEach(i => i.classList.remove('off')); 15 | return; 16 | } 17 | 18 | elem.forEach(item => { 19 | const proxyBox = item.children[1].children[0]; 20 | const title = proxyBox.children[1].value; 21 | const hostname = proxyBox.children[3].value; 22 | const port = ':' + proxyBox.children[7].value; 23 | item.classList.toggle('off', ![title, hostname, port].some(i => i.toLowerCase().includes(str))); 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/content/options-popup.css: -------------------------------------------------------------------------------- 1 | /* ----- Popup ----- */ 2 | .popup { 3 | display: none; 4 | background-color: #0003; 5 | margin: 0; 6 | width: 100%; 7 | height: 100%; 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | z-index: 10; 12 | transition: all 0.5s ease-in-out; 13 | } 14 | 15 | .popup.on { 16 | display: grid; 17 | place-content: center; 18 | animation: fade-in 0.5s ease-in-out; 19 | } 20 | 21 | .popup span { 22 | background-color: var(--alt-bg); 23 | display: grid; 24 | place-content: center end; 25 | padding: 0 0.8em; 26 | cursor: pointer; 27 | font-size: 0.9em; 28 | border-radius: 0.5em 0.5em 0 0; 29 | /* set height to maintain the same result in chrome/firefox */ 30 | height: 2em; 31 | } 32 | 33 | .popup span:hover { 34 | background-color: var(--hover); 35 | } 36 | 37 | .popup textarea { 38 | width: 50vw; 39 | height: 50vh; 40 | resize: both; 41 | padding: 0.5em; 42 | } 43 | 44 | .popup select { 45 | display: none; 46 | } 47 | 48 | .popup select.on { 49 | display: unset; 50 | } 51 | 52 | @keyframes fade-in { 53 | 0% { opacity: 0; } 54 | 100% { opacity: 1; } 55 | } -------------------------------------------------------------------------------- /src/content/options-popup.js: -------------------------------------------------------------------------------- 1 | export class Popup { 2 | 3 | static { 4 | this.popup = document.querySelector('.popup'); 5 | [this.close, this.textarea] = this.popup.children; 6 | this.close.addEventListener('click', () => this.hide()); 7 | } 8 | 9 | static show(text) { 10 | this.textarea.value += text + '\n'; 11 | this.popup.classList.add('on'); 12 | } 13 | 14 | static hide() { 15 | this.popup.classList.remove('on'); 16 | this.textarea.value = ''; 17 | [...this.popup.children].forEach(i => { 18 | if (i.nodeName === 'SELECT') { 19 | i.selectedIndex = 0; 20 | i.classList.remove('on'); 21 | } 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /src/content/options-show.css: -------------------------------------------------------------------------------- 1 | /* ----- show/hide elements ----- */ 2 | details.proxy[data-type="direct"] :is( 3 | [data-i18n="port"], [data-id="port"], 4 | [data-i18n="username"], [data-id="username"], 5 | [data-i18n="password"], .password, 6 | [data-i18n="country"], [data-id="cc"], 7 | [data-i18n="city"], [data-id="city"], 8 | [data-type="pac"], .pac) { 9 | opacity: 0.3; 10 | pointer-events: none; 11 | user-select: none; 12 | } 13 | 14 | details.proxy[data-type="pac"] :is( 15 | [data-i18n="port"], [data-id="port"], 16 | [data-i18n="username"], [data-id="username"], 17 | [data-i18n="password"], .password) { 18 | opacity: 0.3; 19 | pointer-events: none; 20 | user-select: none; 21 | } 22 | 23 | details.proxy[data-type="pac"] :is(.pattern-head, .pattern-box) { 24 | display: none; 25 | } 26 | 27 | details.proxy:not([data-type="pac"]) :is([data-type="pac"], .pac) { 28 | opacity: 0.3; 29 | pointer-events: none; 30 | user-select: none; 31 | } 32 | 33 | details.proxy[data-type="socks4"] :is( 34 | [data-i18n="username"], [data-id="username"], 35 | [data-i18n="password"], .password) { 36 | opacity: 0.3; 37 | pointer-events: none; 38 | user-select: none; 39 | } 40 | 41 | details.proxy:not([data-type="socks5"]) :is([data-i18n="proxyDNS"], [data-id="proxyDNS"]) { 42 | opacity: 0.3; 43 | pointer-events: none; 44 | user-select: none; 45 | } 46 | 47 | /* --- Chrome/Firefox --- */ 48 | body.chrome .firefox, 49 | body:not(.chrome) .chrome, 50 | body.chrome details.proxy[data-type="socks5"] :is( 51 | [data-i18n="username"], [data-id="username"], 52 | [data-i18n="password"], .password) { 53 | opacity: 0.3; 54 | pointer-events: none; 55 | user-select: none; 56 | } 57 | 58 | caption.firefox { 59 | color: var(--nav-color); 60 | visibility: hidden; 61 | } 62 | 63 | body.chrome caption.firefox { 64 | visibility: visible; 65 | opacity: 1; 66 | } 67 | 68 | /* --- Basic --- */ 69 | .basic :is(.pattern-head, .pattern-box) { 70 | display: none; 71 | } -------------------------------------------------------------------------------- /src/content/pac.js: -------------------------------------------------------------------------------- 1 | import {Spinner} from './spinner.js'; 2 | import {Popup} from './options-popup.js'; 3 | 4 | export class PAC { 5 | 6 | static async view(url) { 7 | if (!url) { return; } 8 | 9 | const text = await this.get(url); 10 | Popup.show(text); 11 | } 12 | 13 | static async get(url) { 14 | Spinner.show(); 15 | const text = await fetch(url) 16 | .then(response => response.text()) 17 | .catch(error => error); 18 | Spinner.hide(); 19 | return text; 20 | } 21 | } -------------------------------------------------------------------------------- /src/content/pattern.js: -------------------------------------------------------------------------------- 1 | export class Pattern { 2 | 3 | // showError from options.js, not from migrate.js 4 | static validate(str, type, showError) { 5 | // --- match pattern 6 | if (type === 'match') { 7 | if (this.validMatchPattern(str)) { return true; } 8 | 9 | // not valid 10 | if (showError) { 11 | const error = this.checkMatchPattern(str); 12 | error && alert([browser.i18n.getMessage('regexError'), str, error].join('\n')); 13 | } 14 | return; 15 | } 16 | 17 | // --- wildcard & regex 18 | const pat = this.get(str, type); 19 | try { 20 | new RegExp(pat); 21 | return true; 22 | } 23 | catch (error) { 24 | showError && alert([browser.i18n.getMessage('regexError'), str, error].join('\n')); 25 | } 26 | } 27 | 28 | static get(str, type) { 29 | return type === 'wildcard' ? this.convertWildcard(str) : 30 | type === 'match' ? this.convertMatchPattern(str) : str; 31 | } 32 | 33 | // convert wildcard to regex string 34 | static convertWildcard(str) { 35 | // catch all 36 | if (str === '*') { return '\\w+'; } 37 | 38 | // no need to add scheme as search parameters are encoded url=https%3A%2F%2F 39 | // escape regular expression special characters, minus * ? 40 | return str.replace(/[.+^${}()|[\]\\]/g, '\\$&') 41 | .replace(/^\*|\*$/g, '') // trim start/end * 42 | .replaceAll('*', '.*') 43 | .replaceAll('?', '.'); 44 | } 45 | 46 | // convert match pattern to regex string 47 | static convertMatchPattern(str) { 48 | // catch all 49 | if (str === '') { return '\\w+'; } 50 | 51 | // escape regular expression special characters, minus * 52 | str = str.replace(/[.+?^${}()|[\]\\]/g, '\\$&') 53 | .replace('*://', '.+://') // convert * scheme 54 | .replace('://*\\.', '://(.+\\.)?') // match domains & subdomains 55 | .replaceAll('*', '.*'); 56 | 57 | // match pattern matches the whole URL 58 | return '^' + str + '$'; 59 | } 60 | 61 | static checkMatchPattern(str) { 62 | // catch all 63 | if (str === '') { return; } 64 | 65 | const [, scheme, host] = str.match(/^(.+):\/\/([^/]+)\/(.*)$/) || []; 66 | 67 | switch (true) { 68 | case !scheme || !host: 69 | // Invalid Pattern 70 | return browser.i18n.getMessage('invalidPatternError'); 71 | 72 | case !['*', 'http', 'https', 'ws', 'wss'].includes(scheme): 73 | // "*" in scheme must be the only character | Unsupported scheme 74 | const msg = scheme.includes('*') ? 'schemeError' : 'unsupportedSchemeError'; 75 | return browser.i18n.getMessage(msg); 76 | 77 | case host.substring(1).includes('*'): 78 | // "*" in host must be at the start 79 | return browser.i18n.getMessage('hostError'); 80 | 81 | case host.startsWith('*') && !host.startsWith('*.'): 82 | // "*" in host must be the only character or be followed by "." 83 | return browser.i18n.getMessage('hostDotError'); 84 | 85 | case host.includes(':'): 86 | // Host must not include a port number 87 | return browser.i18n.getMessage('portError'); 88 | } 89 | } 90 | 91 | // --- test match pattern validity 92 | static validMatchPattern(p) { 93 | // file: is not valid for proxying purpose 94 | return p === '' || 95 | /^(https?|\*):\/\/(\*|\*\.[^*:/]+|[^*:/]+)\/.*$/i.test(p); 96 | } 97 | 98 | static getPassthrough(str) { 99 | if (!str) { return [[], [], []]; } 100 | 101 | // RegExp string 102 | const regex = []; 103 | // 10.0.0.0/24 -> [ip, mask] e.g ['10.0.0.0', '255.255.255.0'] 104 | const ipMask = []; 105 | // 10.0.0.0/24 -> [start, end] e.g. ['010000000000', '010000000255'] 106 | const stEnd = []; 107 | 108 | str.split(/[\s,;]+/).forEach(i => { 109 | // The literal string matches simple hostnames (no dots) 110 | if (i === '') { 111 | regex.push('.+://[^.]+/'); 112 | return; 113 | } 114 | 115 | // --- CIDR 116 | const [, ip, , mask] = i.match(/^(\d+(\.\d+){3})\/(\d+)$/) || []; 117 | if (ip && mask) { 118 | const netmask = this.getNetmask(mask); 119 | ipMask.push(ip, netmask); 120 | stEnd.push(this.getRange(ip, netmask)); 121 | return; 122 | } 123 | 124 | // --- pattern 125 | i = i.replaceAll('.', '\\.') // literal '.' 126 | .replaceAll('*', '.*'); // wildcard 127 | 128 | // starting with '.' 129 | i.startsWith('\\.') && (i = '.+://.+' + i); 130 | // add scheme 131 | !i.includes('://') && (i = '.+://' + i); 132 | // add start assertion 133 | // !i.startsWith('^') && (i = '^' + i); 134 | // add pathname 135 | i += '/'; 136 | regex.push(i); 137 | }); 138 | 139 | return [regex, ipMask, stEnd]; 140 | } 141 | 142 | // ---------- CIDR --------------------------------------- 143 | // convert mask to netmask 144 | static getNetmask(mask) { 145 | return [...Array(4)].map(() => { 146 | const n = Math.min(mask, 8); 147 | mask -= n; 148 | return 256 - Math.pow(2, 8 - n); 149 | }).join('.'); 150 | } 151 | 152 | // convert to padded start & end 153 | static getRange(ip, mask) { 154 | // ip array 155 | let st = ip.split('.'); 156 | // mask array 157 | const ma = mask.split('.'); 158 | // netmask wildcard array 159 | let end = st.map((v, i) => Math.min(v - ma[i] + 255, 255) + ''); 160 | st = st.map(i => i.padStart(3, '0')).join(''); 161 | end = end.map(i => i.padStart(3, '0')).join(''); 162 | 163 | return [st, end]; 164 | } 165 | } -------------------------------------------------------------------------------- /src/content/ping.js: -------------------------------------------------------------------------------- 1 | import {Popup} from './options-popup.js'; 2 | 3 | // ---------- Ping (Side Effect) --------------------------- 4 | class Ping { 5 | 6 | static { 7 | document.querySelector('.proxy-top button[data-i18n="ping"]').addEventListener('click', () => this.process()); 8 | } 9 | 10 | static async process() { 11 | let {data} = await browser.storage.local.get({data: []}); 12 | data = data.filter(i => i.active); 13 | if (!data[0]) { return; } 14 | 15 | // --- text formatting 16 | const n = 4; 17 | const pType = Math.max(...data.map(i => i.type.length)) + n; 18 | const pHost = Math.max(...data.map(i => 19 | (i.title || i.pac || `${i.hostname}:${parseInt(i.port)}`).length)) + n; 20 | // performance.now() Firefox 280160 | Chrome 447156.4000000004 21 | const format = n => new Intl.NumberFormat().format(n.toFixed()).padStart(8, ' '); 22 | const dash = '--- --'.padStart(11, ' '); 23 | 24 | data.forEach(i => { 25 | const t = performance.now(); 26 | const host = `${i.hostname}:${parseInt(i.port)}`; 27 | const url = i.pac || (i.type.startsWith('http') ? `${i.type}://${host}/` : `http://${host}/`); 28 | const target = i.type.padEnd(pType, ' ') + (i.title || i.pac || host).padEnd(pHost, ' '); 29 | 30 | if (['direct'].includes(i.type)) { 31 | Popup.show(`${target}${dash} ${i.type}`); 32 | return; 33 | } 34 | 35 | // Chrome a network request timeouts at 300 seconds, while in Firefox at 90 seconds. 36 | // AbortSignal.timeout FF100, Ch124 37 | fetch(url, {method: 'HEAD', cache: 'no-store', signal: AbortSignal.timeout(5000)}) 38 | .then(r => { 39 | const st = ![200, 400].includes(r.status) ? ` ${r.status} ${r.statusText}` : ''; 40 | Popup.show(`${target}${format(performance.now() - t)} ms${st}`); 41 | }) 42 | .catch(e => Popup.show(`${target}${dash} ${e.message}`)); 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /src/content/popup-filter.js: -------------------------------------------------------------------------------- 1 | // ---------- Filter Proxy (Side Effect) ------------------- 2 | class Filter { 3 | 4 | static { 5 | this.list = document.querySelector('div.list'); 6 | const filter = document.querySelector('.filter'); 7 | filter.addEventListener('input', e => this.filterProxy(e)); 8 | } 9 | 10 | static filterProxy(e) { 11 | const str = e.target.value.toLowerCase().trim(); 12 | const elem = [...this.list.children].slice(2); // not the first 2 13 | if (!str) { 14 | elem.forEach(i => i.classList.remove('off')); 15 | return; 16 | } 17 | 18 | elem.forEach(item => { 19 | const title = item.children[1].textContent; 20 | const host = item.children[3].value; // input radio 21 | item.classList.toggle('off', ![title, host].some(i => i.toLowerCase().includes(str))); 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /src/content/popup.css: -------------------------------------------------------------------------------- 1 | @import 'default.css'; 2 | @import 'theme.css'; 3 | 4 | /* ----- Light Theme ----- */ 5 | :root { 6 | --filter: opacity(0.4) grayscale(1); 7 | --selected: #fec8; 8 | } 9 | 10 | /* for the default theme */ 11 | :root:not([class]) { 12 | --nav-bg: #630; 13 | } 14 | 15 | /* ----- Dark Theme ----- */ 16 | @media screen and (prefers-color-scheme: dark) { 17 | :root { 18 | --filter: opacity(1) grayscale(1); 19 | --selected: #222; 20 | } 21 | } 22 | 23 | /* ----- General ----- */ 24 | body { 25 | opacity: 0; 26 | font-size: 12px; 27 | width: 25em; 28 | background-color: var(--bg); 29 | transition: opacity 0.5s; 30 | } 31 | 32 | /* 33 | https://bugzilla.mozilla.org/show_bug.cgi?id=1883896 34 | Remove UA styles for :is(article, aside, nav, section) h1 (Nightly only) Firefox 125 35 | 36 | https://bugzilla.mozilla.org/show_bug.cgi?id=1885509 37 | Remove UA styles for :is(article, aside, nav, section) h1 (staged rollout) 38 | */ 39 | h1 { 40 | color: var(--nav-color); 41 | background-color: var(--nav-bg); 42 | margin: 0; 43 | padding: 0.5em; 44 | font-size: 1.2em; 45 | } 46 | 47 | h1 img { 48 | width: 1.5em; 49 | vertical-align: text-bottom; 50 | } 51 | 52 | /* ----- Buttons ----- */ 53 | div.popup-buttons { 54 | display: grid; 55 | grid-auto-flow: column; 56 | column-gap: 0.1em; 57 | } 58 | 59 | button { 60 | color: #fff; 61 | border: none; 62 | padding: 0.8em; 63 | /* font-weight: bold; */ 64 | } 65 | 66 | button:hover { 67 | background-color: var(--btn-hover); 68 | } 69 | /* ----- /Buttons ----- */ 70 | 71 | /* ----- Main Display ----- */ 72 | div.list { 73 | padding-top: 0.5em; 74 | min-height: 15em; 75 | max-height: 30em; 76 | overflow-y: auto; 77 | scrollbar-width: thin; 78 | } 79 | 80 | div.list label { 81 | display: grid; 82 | grid-template-columns: 2.5em auto 1fr; 83 | column-gap: 0.5em; 84 | padding: 0.2em 0.5em; 85 | cursor: pointer; 86 | } 87 | 88 | div.list label:hover { 89 | background-color: var(--hover); 90 | } 91 | 92 | div.list label.off { 93 | display: none; 94 | } 95 | 96 | div.list label:has(input[name="server"]:checked) { 97 | background-color: var(--selected); 98 | } 99 | 100 | .flag img { 101 | width: 1.2em; 102 | } 103 | 104 | .flag img.off { 105 | filter: var(--filter); 106 | } 107 | 108 | .flag { 109 | grid-row: span 2; 110 | font-size: 1.8em; 111 | line-height: 1em; 112 | place-self: start center; 113 | } 114 | 115 | .title { 116 | color: var(--header); 117 | font-size: 1.2em; 118 | overflow: hidden; 119 | text-overflow: ellipsis; 120 | white-space: nowrap; 121 | } 122 | 123 | .port { 124 | color: var(--dim); 125 | place-self: end start; 126 | } 127 | 128 | .data { 129 | grid-column: span 2; 130 | overflow: hidden; 131 | text-overflow: ellipsis; 132 | white-space: pre; 133 | color: var(--dim); 134 | } 135 | 136 | .data.off { 137 | display: none; 138 | } 139 | 140 | input[name="server"] { 141 | display: none; 142 | } 143 | 144 | /* --- more section --- */ 145 | summary { 146 | background-color: var(--alt-bg); 147 | padding: 0.2em 0.5em; 148 | margin-bottom: 0.1em; 149 | } 150 | 151 | div.host { 152 | display: grid; 153 | grid-template-columns: 1fr 1fr; 154 | padding: 0.5em; 155 | gap: 0.3em 0.5em; 156 | align-items: center; 157 | } 158 | 159 | div.host input.filter { 160 | background: url('../image/filter.svg') no-repeat left 0.5em center/1em; 161 | padding-left: 2em; 162 | } 163 | 164 | div.host button { 165 | background-color: unset; 166 | border-radius: 5px; 167 | border: 1px solid var(--border); 168 | color: var(--color); 169 | font-weight: normal; 170 | padding: 0.2em; 171 | } 172 | 173 | div.host button:hover { 174 | background-color: var(--hover); 175 | } 176 | 177 | 178 | /* ----- show/hide elements ----- */ 179 | /* --- Chrome --- */ 180 | .chrome .firefox { 181 | opacity: 0.3; 182 | pointer-events: none; 183 | user-select: none; 184 | } 185 | 186 | /* ---managed --- */ 187 | .managed .local { 188 | opacity: 0.3; 189 | pointer-events: none; 190 | user-select: none; 191 | } 192 | 193 | /* --- Basic --- */ 194 | .basic .not-basic { 195 | display: none; 196 | } 197 | 198 | /* --- scheme --- */ 199 | .not-http select.http, 200 | .not-tab-proxy .tab-proxy { 201 | opacity: 0.3; 202 | pointer-events: none; 203 | user-select: none; 204 | } 205 | /* ----- /show/hide elements ----- */ -------------------------------------------------------------------------------- /src/content/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Popup 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | 15 |
16 | 21 | 22 | 27 |
28 | 29 |
30 | 31 |
32 | 33 | 34 | 38 | 39 | 42 | 43 | 46 |
47 |
48 | 49 | 55 | 56 | 57 | 58 | 67 | 68 |
69 |
70 | 71 | -------------------------------------------------------------------------------- /src/content/popup.js: -------------------------------------------------------------------------------- 1 | import {pref, App} from './app.js'; 2 | import {Location} from './location.js'; 3 | import {Flag} from './flag.js'; 4 | import './popup-filter.js'; 5 | import './show.js'; 6 | import './i18n.js'; 7 | 8 | // ---------- User Preferences ----------------------------- 9 | await App.getPref(); 10 | 11 | // ---------- Popup ---------------------------------------- 12 | class Popup { 13 | 14 | static { 15 | // --- theme 16 | pref.theme && (document.documentElement.className = pref.theme); 17 | // show after 18 | document.body.style.opacity = 1; 19 | 20 | document.querySelectorAll('button').forEach(i => i.addEventListener('click', e => this.processButtons(e))); 21 | 22 | this.list = document.querySelector('div.list'); 23 | 24 | // --- Include/Exclude Host (not for storage.managed) 25 | this.includeHost = document.querySelector('select#includeHost'); 26 | !pref.managed && this.includeHost.addEventListener('change', e => this.includeExclude(e)); 27 | this.excludeHost = document.querySelector('select#excludeHost'); 28 | !pref.managed && this.excludeHost.addEventListener('change', e => this.includeExclude(e)); 29 | 30 | // --- Tab Proxy (firefox only) 31 | this.tabProxy = document.querySelector('select#tabProxy'); 32 | App.firefox && this.tabProxy.addEventListener('change', e => { 33 | if (!this.tab) { return; } 34 | 35 | const {value, selectedOptions} = e.target; 36 | const proxy = value && this.proxyCache[selectedOptions[0].dataset.index]; 37 | browser.runtime.sendMessage({id: 'setTabProxy', proxy, tab: this.tab}); 38 | }); 39 | 40 | // disable buttons on storage.managed 41 | pref.managed && document.body.classList.add('managed'); 42 | 43 | // --- store details open toggle 44 | const details = document.querySelector('details'); 45 | // defaults to true 46 | details.open = localStorage.getItem('more') !== 'false'; 47 | details.addEventListener('toggle', () => localStorage.setItem('more', details.open)); 48 | 49 | this.process(); 50 | } 51 | 52 | static includeExclude(e) { 53 | const {id, value} = e.target; 54 | if (!value) { return; } 55 | // proxy object reference to pref is lost in chrome when sent from popup.js 56 | browser.runtime.sendMessage({id, pref, host: value, tab: this.tab}); 57 | // reset select option 58 | e.target.selectedIndex = 0; 59 | } 60 | 61 | static checkProxyByPatterns() { 62 | // check if there are patterns 63 | if (!pref.data.some(i => i.active && (i.include[0] || i.tabProxy?.[0]))) { 64 | // hide option if there are no patterns 65 | this.list.children[0].style.display = 'none'; 66 | // show as disable 67 | pref.mode === 'pattern' && (pref.mode = 'disable'); 68 | } 69 | 70 | pref.mode === 'pattern' && (this.list.children[0].children[2].checked = true); 71 | } 72 | 73 | static async process() { 74 | this.checkProxyByPatterns(); 75 | 76 | const labelTemplate = document.querySelector('template').content.firstElementChild; 77 | const docFrag = document.createDocumentFragment(); 78 | 79 | pref.data.filter(i => i.active).forEach(i => { 80 | const id = i.type === 'pac' ? i.pac : `${i.hostname}:${i.port}`; 81 | const label = labelTemplate.cloneNode(true); 82 | const [flag, title, port, radio, data] = label.children; 83 | flag.textContent = Flag.show(i); 84 | title.textContent = i.title || i.hostname; 85 | port.textContent = !i.title ? i.port : ''; 86 | radio.value = i.type === 'direct' ? 'direct' : id; 87 | radio.checked = id === pref.mode; 88 | data.textContent = [i.city, Location.get(i.cc)].filter(Boolean).join(', ') || ' '; 89 | docFrag.append(label); 90 | }); 91 | 92 | this.list.append(docFrag); 93 | this.list.addEventListener('click', e => 94 | // fires twice (click & label -> input) 95 | e.target.name === 'server' && this.processSelect(e.target.value, e) 96 | ); 97 | 98 | // --- Add Hosts to select 99 | // used to find proxy, filter out PAC, limit to 10 100 | this.proxyCache = pref.data.filter(i => i.active && i.type !== 'pac').slice(0, 10); 101 | 102 | this.proxyCache.forEach((i, index) => { 103 | const flag = Flag.show(i); 104 | const value = `${i.hostname}:${i.port}`; 105 | const opt = new Option(flag + ' ' + (i.title || value), value); 106 | opt.dataset.index = index; 107 | // supported on Chrome, not on Firefox 108 | // opt.style.color = item.color; 109 | docFrag.append(opt); 110 | }); 111 | 112 | this.includeHost.append(docFrag.cloneNode(true)); 113 | this.excludeHost.append(docFrag.cloneNode(true)); 114 | this.tabProxy.append(docFrag); 115 | 116 | // get active tab 117 | [this.tab] = await browser.tabs.query({currentWindow: true, active: true}); 118 | 119 | // --- show/hide selects 120 | document.body.classList.toggle('not-http', !this.tab.url.startsWith('http')); 121 | 122 | // Check Tab proxy (Firefox only) 123 | const allowedTabProxy = App.firefox && App.allowedTabProxy(this.tab.url); 124 | allowedTabProxy && this.checkTabProxy(); 125 | document.body.classList.toggle('not-tab-proxy', !allowedTabProxy); 126 | } 127 | 128 | static checkTabProxy() { 129 | browser.runtime.sendMessage({id: 'getTabProxy', tab: this.tab}) 130 | .then(i => i && (this.tabProxy.value = `${i.hostname}:${i.port}`)); 131 | } 132 | 133 | static processSelect(mode, e) { 134 | // disregard re-click 135 | if (mode === pref.mode) { return; } 136 | // not for storage.managed 137 | if (pref.managed) { return; } 138 | 139 | // check 'prefers-color-scheme' since it is not available in background service worker 140 | const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; 141 | 142 | // save mode 143 | pref.mode = mode; 144 | browser.storage.local.set({mode}); 145 | browser.runtime.sendMessage({id: 'setProxy', pref, dark, noDataChange: true}); 146 | } 147 | 148 | static processButtons(e) { 149 | switch (e.target.dataset.i18n) { 150 | case 'options': 151 | browser.runtime.openOptionsPage(); 152 | break; 153 | 154 | case 'location': 155 | browser.tabs.create({url: 'https://getfoxyproxy.org/geoip/'}); 156 | break; 157 | 158 | case 'ip': 159 | // sending message to the background script to complete even if popup gets closed 160 | browser.runtime.sendMessage({id: 'getIP'}); 161 | break; 162 | 163 | case 'log': 164 | browser.tabs.create({url: '/content/options.html?log'}); 165 | break; 166 | } 167 | 168 | window.close(); 169 | } 170 | } -------------------------------------------------------------------------------- /src/content/progress-bar.css: -------------------------------------------------------------------------------- 1 | /* ----- Progress Bar ----- */ 2 | .progress-bar { 3 | opacity: 0; 4 | height: 2px; 5 | width: 0; 6 | background: #3bb3e0 linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3); 7 | background-size: 900% 900%; 8 | transition: opacity 1s ease-in-out, width 0s ease-in-out 1s; 9 | animation: rainbow 3s ease infinite; 10 | } 11 | 12 | .progress-bar.on { 13 | width: 100%; 14 | opacity: 1; 15 | transition: opacity 1s ease-in-out, width 2s ease-in-out; 16 | } 17 | 18 | @keyframes rainbow { 19 | 0% { background-position: 0% 100%; } 20 | 50% { background-position: 100% 200%; } 21 | 100% { background-position: 0% 100%; } 22 | } -------------------------------------------------------------------------------- /src/content/progress-bar.js: -------------------------------------------------------------------------------- 1 | // ---------- Progress Bar --------------------------------- 2 | export class ProgressBar { 3 | 4 | static bar = document.querySelector('.progress-bar'); 5 | 6 | static show() { 7 | this.bar.classList.toggle('on'); 8 | setTimeout(() => this.bar.classList.toggle('on'), 2000); 9 | } 10 | } -------------------------------------------------------------------------------- /src/content/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["mode", "data"], 4 | "properties": { 5 | "mode": {"type": "string"}, 6 | "sync": {"type": "boolean"}, 7 | "autoBackup": {"type": "boolean"}, 8 | "passthrough": {"type": "string"}, 9 | "theme": {"type": "string"}, 10 | "data": { 11 | "type": "array", 12 | "minItems": 1, 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "active": {"type": "boolean"}, 17 | "title": {"type": "string"}, 18 | "type": {"type": "string", "enum": ["http", "https", "socks4", "socks5", "pac", "direct"]}, 19 | "hostname": {"type": "string", "format": "hostname"}, 20 | "port": {"type": "string", "pattern": "^\\d*$"}, 21 | "username": {"type": "string"}, 22 | "password": {"type": "string"}, 23 | "cc": {"type": "string", "pattern": "^([A-Z]{2}|)$"}, 24 | "city": {"type": "string"}, 25 | "color": {"type": "string"}, 26 | "pac": {"type": "string"}, 27 | "pacString": {"type": "string"}, 28 | "proxyDNS": {"type": "boolean"}, 29 | "include": { 30 | "type": "array", 31 | "id": "include", 32 | "items": { 33 | "type": "object", 34 | "properties": { 35 | "type": {"type": "string", "enum": ["wildcard", "regex", "match"]}, 36 | "title": {"type": "string"}, 37 | "pattern": {"type": "string"}, 38 | "active": {"type": "boolean"} 39 | }, 40 | "required": ["type", "title", "pattern", "active"] 41 | } 42 | }, 43 | "exclude": {"$ref": "include"}, 44 | "tabProxy": {"$ref": "include"} 45 | } 46 | } 47 | }, 48 | "container": { 49 | "type": "object", 50 | "properties": { 51 | "incognito": {"type": "string"}, 52 | "container-1": {"type": "string"}, 53 | "container-2": {"type": "string"} 54 | } 55 | }, 56 | "commands": { 57 | "type": "object", 58 | "properties": { 59 | "setProxy": {"type": "string"}, 60 | "setTabProxy": {"type": "string"}, 61 | "includeHost": {"type": "string"}, 62 | "excludeHost": {"type": "string"} 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/content/show.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | // ---------- Show (Side Effect) --------------------------- 4 | class Show { 5 | 6 | static { 7 | const {basic, firefox} = App; 8 | basic && document.body.classList.add('basic'); 9 | !firefox && document.body.classList.add('chrome'); 10 | 11 | const elem = document.querySelector('img[src="../image/icon.svg"]'); 12 | elem?.addEventListener('click', () => { 13 | basic && document.body.classList.toggle('basic'); 14 | !firefox && document.body.classList.toggle('chrome'); 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /src/content/spinner.css: -------------------------------------------------------------------------------- 1 | /* ----- Spinner ----- */ 2 | div.spinner { 3 | display: none; 4 | grid-auto-flow: column; 5 | align-items: center; 6 | justify-content: center; 7 | background-color: #0003; 8 | margin: 0; 9 | width: 100%; 10 | height: 100%; 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | z-index: 1; 15 | transition: all 0.5s ease-in-out; 16 | } 17 | 18 | .spinner::before { 19 | content: ''; 20 | width: 8em; 21 | height: 8em; 22 | border: 1em solid #ddd; 23 | border-color: #ddd var(--btn-bg) #ddd var(--btn-bg); 24 | border-radius: 50%; 25 | animation: spin 1.5s linear infinite; 26 | margin-left: -2em; 27 | } 28 | 29 | .spinner::after { 30 | content: ''; 31 | background-image: url('../image/icon.svg'); 32 | background-repeat: no-repeat; 33 | background-size: contain; 34 | width: 6em; 35 | height: 6em; 36 | margin-left: -8em; 37 | opacity: 0.6; 38 | } 39 | 40 | .spinner.on { 41 | display: grid; 42 | animation: fade-in 0.5s ease-in-out; 43 | } 44 | 45 | @keyframes spin { 46 | 0% { transform: rotate(0deg); } 47 | 100% { transform: rotate(360deg); } 48 | } 49 | 50 | @keyframes fade-in { 51 | 0% { opacity: 0; } 52 | 100% { opacity: 1; } 53 | } -------------------------------------------------------------------------------- /src/content/spinner.js: -------------------------------------------------------------------------------- 1 | // ---------- Spinner -------------------------------------- 2 | export class Spinner { 3 | 4 | static spinner = document.querySelector('.spinner'); 5 | 6 | static show() { 7 | this.spinner.classList.add('on'); 8 | } 9 | 10 | static hide() { 11 | this.spinner.classList.remove('on'); 12 | } 13 | } -------------------------------------------------------------------------------- /src/content/sync.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | // ---------- Storage Sync --------------------------------- 4 | export class Sync { 5 | 6 | static init(pref) { 7 | // not for storage.managed 8 | if (pref.managed) { return; } 9 | 10 | // Firefox 101 (2022-05-31), Chrome 73 11 | browser.storage.sync.onChanged.addListener(e => this.onChanged(e)); 12 | } 13 | 14 | static async onChanged(changes) { 15 | const pref = await browser.storage.local.get(); 16 | this.getSync(pref); 17 | } 18 | 19 | static async get(pref) { 20 | // check storage.managed 21 | await this.getManaged(pref); 22 | // check storage.sync 23 | await this.getSync(pref); 24 | } 25 | 26 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1868153 27 | // On Firefox storage.managed returns undefined if not found 28 | static async getManaged(pref) { 29 | const result = await browser.storage.managed.get().catch(() => {}); 30 | if (!Array.isArray(result?.data) || !result.data[0]) { 31 | // storage.managed not found, clean up 32 | if (Object.hasOwn(pref, 'managed')) { 33 | delete pref.managed; 34 | await browser.storage.local.remove('managed'); 35 | } 36 | return; 37 | } 38 | 39 | // get default pref 40 | const db = App.getDefaultPref(); 41 | // revert pref to default values 42 | Object.keys(db).forEach(i => pref[i] = db[i]); 43 | 44 | // set data from storage.managed 45 | Object.keys(result).forEach(i => Object.hasOwn(pref, i) && (pref[i] = result[i])); 46 | // set pref.managed to use in options.js, popup.js 47 | pref.managed = true; 48 | // no sync for storage.managed 49 | pref.sync = false; 50 | 51 | // --- update database 52 | await browser.storage.local.set(pref); 53 | } 54 | 55 | static hasOldData(obj) { 56 | // FP v3 OR FP v7, null value causes an error 57 | return Object.hasOwn(obj, 'settings') || Object.values(obj).some(i => i && Object.hasOwn(i, 'address')); 58 | } 59 | 60 | static async getSync(pref) { 61 | if (!pref.sync) { return; } 62 | if (pref.managed) { return; } 63 | 64 | const syncPref = await browser.storage.sync.get(); 65 | 66 | // check sync from old version 3-7 67 | // (local has no data OR has old data) AND sync has old data 68 | if ((!Object.keys(pref)[0] || this.hasOldData(pref)) && this.hasOldData(syncPref)) { 69 | // set sync data to pref, to migrate next in background.js 70 | Object.keys(syncPref).forEach(i => pref[i] = syncPref[i]); 71 | return; 72 | } 73 | 74 | // convert object to array & filter proxies 75 | const data = Object.values(syncPref).filter(i => Object.hasOwn(i, 'hostname')); 76 | 77 | const obj = {}; 78 | if (data[0] && !App.equal(pref.data, data)) { 79 | obj.data = data; 80 | pref.data = data; 81 | } 82 | 83 | App.syncProperties.forEach(i => { 84 | if (Object.hasOwn(syncPref, i)) { 85 | obj[i] = syncPref[i]; 86 | pref[i] = syncPref[i]; 87 | } 88 | }); 89 | 90 | // update saved pref 91 | Object.keys(obj)[0] && await browser.storage.local.set(obj); 92 | } 93 | } -------------------------------------------------------------------------------- /src/content/test.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | import {Popup} from './options-popup.js'; 3 | 4 | // ---------- Proxy Text (Side Effect) --------------------------- 5 | class ProxyTest { 6 | 7 | static { 8 | document.querySelector('.proxy-top button[data-i18n="test"]').addEventListener('click', () => this.selectOptions()); 9 | this.popupProxy = document.querySelector('.popup select.popup-test-proxy'); 10 | this.popupServer = document.querySelector('.popup select.popup-server'); 11 | this.popupServer.addEventListener('change', () => this.process()); 12 | } 13 | 14 | static selectOptions() { 15 | if (this.popupProxy.options.length < 2) { 16 | Popup.show('Did not find a suitable proxy'); 17 | Popup.show('Ending the test'); 18 | return; 19 | } 20 | 21 | this.popupProxy.classList.add('on'); 22 | this.popupServer.classList.add('on'); 23 | 24 | !App.firefox && Popup.show('On Chrome, proxy authentication must be done before starting the test'); 25 | Popup.show('Please select a proxy (or the first one will be selected) and then a server for the test\n'); 26 | } 27 | 28 | static async process(e) { 29 | this.server = this.popupServer.value; 30 | if (!this.server) { return; } 31 | 32 | Popup.show('Starting the proxy Test\n'); 33 | 34 | // check 'prefers-color-scheme' since it is not available in background service worker 35 | this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches; 36 | 37 | // --- get the IP check server 38 | const serverText = this.popupServer.selectedOptions[0].textContent; 39 | Popup.show(`IP check server: ${serverText}`); 40 | 41 | // --- get the proxy for the test 42 | // selected proxy or the first proxy 43 | !this.popupProxy.value && (this.popupProxy.selectedIndex = 1); 44 | const id = this.popupProxy.value; 45 | const host = this.popupProxy.selectedOptions[0].textContent; 46 | 47 | const pref = await browser.storage.local.get(); 48 | const pxy = pref.data.find(i => id === `${i.hostname}:${i.port}`); 49 | 50 | Popup.show(`Testing proxy: ${host}\n`); 51 | 52 | // --- get real IP 53 | Popup.show('Setting mode to "Disable" to get your real IP'); 54 | pref.mode = 'disable'; 55 | await this.setProxy(pref); 56 | const realIP = await this.getIP(); 57 | 58 | // --- test Tab Proxy with mode disable 59 | App.firefox && await this.tabProxy(pxy, realIP); 60 | 61 | // --- test single proxy 62 | Popup.show(`Setting mode to "${host}"`); 63 | pref.mode = `${pxy.hostname}:${pxy.port}`; 64 | await this.setProxy(pref); 65 | await this.getIP(); 66 | 67 | // --- test Proxy by Patterns 68 | Popup.show('Setting mode to "Proxy by Patterns"'); 69 | // adding patterns to test 70 | pxy.include = [ 71 | { 72 | type: 'wildcard', 73 | title: 'test', 74 | pattern: new URL(this.server).hostname, 75 | active: true 76 | }, 77 | ]; 78 | pref.mode = 'pattern'; 79 | await this.setProxy(pref); 80 | await this.getIP(); 81 | 82 | // --- reset to the original state 83 | this.reset(); 84 | } 85 | 86 | static async setProxy(pref) { 87 | // await runtime.sendMessage resolves early on Chrome 88 | App.firefox ? await this.sendMessage(pref) : await this.chromeSendMessage(pref); 89 | } 90 | 91 | static sendMessage(pref) { 92 | return browser.runtime.sendMessage({id: 'setProxy', pref, dark: this.dark, noDataChange: true}); 93 | } 94 | 95 | static async chromeSendMessage(pref) { 96 | await new Promise(resolve => { 97 | const listener = () => { 98 | browser.proxy.settings.onChange.removeListener(listener); 99 | resolve(); 100 | }; 101 | browser.proxy.settings.onChange.addListener(listener); 102 | this.sendMessage(pref); 103 | }); 104 | } 105 | 106 | static async tabProxy(pxy, realIP) { 107 | Popup.show('Setting Tab Proxy with mode "Disable"'); 108 | const tab = await browser.tabs.create({active: false}); 109 | await browser.runtime.sendMessage({id: 'setTabProxy', proxy: pxy, tab}); 110 | await new Promise(resolve => { 111 | const listener = e => { 112 | browser.tabs.remove(tab.id); 113 | browser.webRequest.onBeforeRequest.removeListener(listener); 114 | Popup.show(`Your IP: ${e.proxyInfo.host || realIP}\n`); 115 | resolve(); 116 | }; 117 | browser.webRequest.onBeforeRequest.addListener(listener, {urls: [''], tabId: tab.id}); 118 | browser.tabs.update(tab.id, {url: this.server}); 119 | }); 120 | } 121 | 122 | static async reset() { 123 | Popup.show('Resetting options to their original state'); 124 | const pref = await browser.storage.local.get(); 125 | await this.setProxy(pref); 126 | Popup.show('Ending the proxy test\n'); 127 | 128 | // reset select elements 129 | this.popupProxy.selectedIndex = 0; 130 | this.popupServer.selectedIndex = 0; 131 | } 132 | 133 | static async getIP() { 134 | // Chrome a network request timeouts at 300 seconds, while in Firefox at 90 seconds. 135 | // AbortSignal.timeout FF100, Ch124 136 | return fetch(this.server, {cache: 'no-store', signal: AbortSignal.timeout(5000)}) 137 | .then(r => r.ok ? r.text() : this.response(r)) 138 | .then(text => { 139 | // HTML response is not acceptable 140 | const ip = text.includes('<') ? 'undefined' : text.trim(); 141 | Popup.show(`Your IP: ${ip}\n`); 142 | return ip; 143 | }) 144 | .catch(e => Popup.show(`Your IP: undefined\nStatus: ${e.message}\n`)); 145 | } 146 | 147 | static response(r) { 148 | switch (r.status) { 149 | case 403: 150 | return 'undefined\nStatus: 403 Forbidden'; 151 | 152 | default: 153 | return `undefined\nStatus: ${r.status} ${r.statusText}`; 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /src/content/tester.css: -------------------------------------------------------------------------------- 1 | /* ----- Tester ----- */ 2 | .tester h3 { 3 | margin: 0; 4 | } 5 | 6 | .tester h3:nth-of-type(2) { 7 | margin-top: 1.5em; 8 | } 9 | 10 | .tester .tester-pattern { 11 | display: grid; 12 | grid-template-columns: auto 1fr; 13 | gap: 0.5em; 14 | } 15 | 16 | .tester input { 17 | padding: 0.5em; 18 | } 19 | 20 | .tester .buttons { 21 | display: grid; 22 | grid-auto-flow: column; 23 | gap: 0.5em; 24 | } 25 | 26 | .tester button:first-child{ 27 | justify-self: start; 28 | } 29 | 30 | .tester button:last-child { 31 | justify-self: end; 32 | } 33 | 34 | .tester pre { 35 | color: inherit; 36 | /* background-color: var(--alt-bg); */ 37 | border: 1px solid var(--border); 38 | border-radius: 0.3em; 39 | padding: 0.5em; 40 | font-size: 1.1em; 41 | min-height: 10em; 42 | background: linear-gradient(to right, var(--hover) 0, var(--hover) 2em, transparent 2em, transparent 100%); 43 | padding-left: 2.5em; 44 | } 45 | 46 | .tester pre span { 47 | margin-left: -2em; 48 | } 49 | 50 | .pass { 51 | color: var(--pass); 52 | } 53 | 54 | .fail { 55 | color: var(--fail); 56 | } 57 | 58 | :is(.pass, .fail)::before { 59 | content: '✔'; 60 | display: inline-block; 61 | width: 1em; 62 | margin-right: 1em; 63 | text-align: center; 64 | } 65 | 66 | .fail::before { 67 | content: '✘'; 68 | } 69 | -------------------------------------------------------------------------------- /src/content/tester.js: -------------------------------------------------------------------------------- 1 | import {Pattern} from './pattern.js'; 2 | import {Nav} from './nav.js'; 3 | 4 | export class Tester { 5 | 6 | static { 7 | this.select = document.querySelector('.tester select'); 8 | this.select.addEventListener('change', () => this.process()); 9 | [this.pattern, this.url] = document.querySelectorAll('.tester input'); 10 | [this.pre, this.pre2] = document.querySelectorAll('.tester pre'); 11 | 12 | // convert generated HTML to plaintext 13 | this.pre.addEventListener('input', e => { 14 | if ((e.data || e.inputType === 'insertFromPaste') && this.pre.matches('.tested')) { 15 | [...this.pre.children].forEach(i => i.replaceWith(i.textContent)); 16 | this.pre.classList.remove('tested'); 17 | } 18 | }); 19 | 20 | document.querySelector('.tester button[data-i18n="back"]').addEventListener('click', () => this.back()); 21 | document.querySelector('.tester button[data-i18n="test"]').addEventListener('click', () => this.process()); 22 | document.querySelector('.tester button.proxyByPatterns').addEventListener('click', () => this.processURL()); 23 | } 24 | 25 | static process() { 26 | this.pattern.value = this.pattern.value.trim(); 27 | const str = this.pre.textContent.trim(); 28 | if (!this.pattern.value || !str) { 29 | return; 30 | } 31 | 32 | // validate pattern 33 | const valid = Pattern.validate(this.pattern.value, this.select.value, true); 34 | if (!valid) { return; } 35 | 36 | // convert pattern to regex string if needed 37 | const pat = Pattern.get(this.pattern.value, this.select.value); 38 | const regex = new RegExp(pat, 'i'); 39 | const arr = []; 40 | str.split(/\s/).forEach(i => { 41 | const sp = document.createElement('span'); 42 | sp.textContent = i; 43 | i.trim() && (sp.className = regex.test(i) ? 'pass' : 'fail'); 44 | arr.push(sp, '\n'); 45 | }); 46 | 47 | this.pre.textContent = ''; 48 | this.pre.append(...arr); 49 | // mark pre, used for 'input' event 50 | this.pre.classList.add('tested'); 51 | } 52 | 53 | static back() { 54 | if (!this.target) { return; } 55 | 56 | // show Proxy tab 57 | Nav.get('proxies'); 58 | 59 | // go to target proxy 60 | const details = this.target.closest('details'); 61 | details.open = true; 62 | this.target.scrollIntoView({behavior: 'smooth'}); 63 | this.target.focus(); 64 | 65 | // reset 66 | this.target = null; 67 | } 68 | 69 | static async processURL() { 70 | const url = this.url.value.trim(); 71 | if (!url || !this.url.checkValidity()) { return; } 72 | 73 | let {data} = await browser.storage.local.get({data: []}); 74 | data = data.filter(i => i.active && !i.pac && (i.include[0] || i.exclude[0] || i.tabProxy?.[0])).map(item => { 75 | item.tabProxy ||= []; 76 | return { 77 | type: item.type, 78 | hostname: item.hostname, 79 | port: item.port, 80 | title: item.title, 81 | include: item.include.filter(i => i.active), 82 | exclude: item.exclude.filter(i => i.active), 83 | tabProxy: item.tabProxy.filter(i => i.active), 84 | }; 85 | }); 86 | 87 | const match = arr => arr.find(i => new RegExp(Pattern.get(i.pattern, i.type), 'i').test(url)); 88 | 89 | let arr = []; 90 | 91 | data.forEach(i => { 92 | const target = i.title || `${i.hostname}:${parseInt(i.port)}`; 93 | if (!match(i.exclude)) { 94 | const p = match(i.include); 95 | p && arr.push([target, p.type, p.pattern]); 96 | } 97 | const p = match(i.tabProxy); 98 | p && arr.push([target, p.type, p.pattern, '(Tab Proxy)']); 99 | }); 100 | 101 | if (!arr[0]) { 102 | this.pre2.textContent = '⮕ DIRECT'; 103 | return; 104 | } 105 | 106 | // --- text formatting 107 | const n = 4; 108 | const w = 'wildcard'.length + n; 109 | const p0 = Math.max(...arr.map(i => i[0].length)) + n; 110 | const p2 = Math.max(...arr.map(i => i[2].length)) + n; 111 | 112 | // undefined or null is converted to an empty string in join() 113 | arr = arr.map(i => [i[0].padEnd(p0, ' '), i[1].padEnd(w, ' '), i[2].padEnd(p2, ' '), i[3]].join('').trim()); 114 | this.pre2.textContent = arr.join('\n'); 115 | } 116 | } -------------------------------------------------------------------------------- /src/content/theme.css: -------------------------------------------------------------------------------- 1 | /* ---------- Alternative Themes ---------- */ 2 | 3 | /* ----- moonlight ----- */ 4 | :root.moonlight { 5 | --bg: #fff; 6 | --body-bg: #f9f9fb; 7 | 8 | --nav-bg: #d9d9db; 9 | --nav-hover: var(--hover); 10 | --nav-color: var(--color); 11 | } 12 | 13 | :root.moonlight .flat { 14 | color: inherit; 15 | background-color: var(--bg); 16 | border: 1px solid var(--border); 17 | } 18 | 19 | :root.moonlight .flat:hover { 20 | background-color: var(--hover); 21 | } 22 | 23 | :root.moonlight .pattern-head button span.plus { 24 | filter: unset; 25 | } 26 | 27 | :root.moonlight .popup-buttons button { 28 | color: inherit; 29 | background-color: var(--alt-bg); 30 | } 31 | 32 | :root.moonlight .popup-buttons button:hover { 33 | background-color: var(--hover); 34 | } 35 | 36 | @media screen and (prefers-color-scheme: dark) { 37 | :root.moonlight { 38 | --bg: #333; 39 | --body-bg: #444; 40 | --nav-bg: #000; 41 | } 42 | 43 | :root.moonlight .pattern-head button span.plus { 44 | filter: brightness(0) invert(1); 45 | } 46 | } 47 | /* ----- /moonlight ----- */ 48 | 49 | 50 | 51 | /* ----- alt ----- */ 52 | :root.alt { 53 | --body-bg: var(--bg); 54 | } 55 | /* ----- /alt ----- */ -------------------------------------------------------------------------------- /src/content/theme.js: -------------------------------------------------------------------------------- 1 | // ---------- Theme (Side Effect) -------------------------- 2 | class Theme { 3 | 4 | static { 5 | this.elem = [document, ...[...document.querySelectorAll('iframe')].map(i => i.contentDocument)]; 6 | document.getElementById('theme').addEventListener('change', e => this.set(e.target.value)); 7 | 8 | browser.storage.local.get('theme').then(i => { 9 | i.theme && this.set(i.theme); 10 | // show after 11 | document.body.style.opacity = 1; 12 | }); 13 | } 14 | 15 | static set(value) { 16 | this.elem.forEach(i => i.documentElement.className = value); 17 | } 18 | } -------------------------------------------------------------------------------- /src/content/toggle-switch.css: -------------------------------------------------------------------------------- 1 | /* ----- Toggle Switch ----- */ 2 | /* round() FF118, Ch125 */ 3 | .toggle { 4 | --toggle-dot: 12px; 5 | --toggle-dot-margin: 1px; 6 | --toggle-wh-ratio: 2.4; 7 | --toggle-w: calc(var(--toggle-dot) * var(--toggle-wh-ratio) + var(--toggle-dot-margin) * 2); 8 | --toggle-h: calc(var(--toggle-dot) + var(--toggle-dot-margin) * 2); 9 | --toggle-trans: calc(var(--toggle-w) - var(--toggle-h)); 10 | 11 | appearance: none; 12 | width: var(--toggle-w); 13 | height: var(--toggle-h); 14 | position: relative; 15 | border-radius: 50px; 16 | background-color: #ccc; 17 | transition: background-color 0.5s; 18 | } 19 | 20 | .toggle::before { 21 | content: ''; 22 | display: block; 23 | margin: var(--toggle-dot-margin); 24 | width: var(--toggle-dot); 25 | height: var(--toggle-dot); 26 | background-color: #fff; 27 | border-radius: 50%; 28 | color: #fff; 29 | transition: 0.5s; 30 | } 31 | 32 | .toggle:checked { 33 | background-color: var(--btn-bg); 34 | } 35 | 36 | .toggle:checked::before { 37 | transform: translateX(var(--toggle-trans)); 38 | } 39 | 40 | /* smaller toggle switch for patterns */ 41 | .pattern-row .toggle { 42 | --toggle-dot: 10px; 43 | } -------------------------------------------------------------------------------- /src/content/toggle.js: -------------------------------------------------------------------------------- 1 | // ---------- Toggle --------------------------------------- 2 | export class Toggle { 3 | 4 | static password(elem) { 5 | elem.addEventListener('click', () => { 6 | const input = elem.previousElementSibling; 7 | input.type = input.type === 'password' ? 'text' : 'password'; 8 | }); 9 | } 10 | } -------------------------------------------------------------------------------- /src/content/webrtc.js: -------------------------------------------------------------------------------- 1 | import {App} from './app.js'; 2 | 3 | // ---------- WebRTC (Side Effect) ------------------------- 4 | class WebRTC { 5 | 6 | static { 7 | this.webRTC = document.querySelector('#limitWebRTC'); 8 | // firefox only option 9 | !App.firefox && (this.webRTC.lastElementChild.disabled = true); 10 | this.webRTC.addEventListener('change', () => this.process()); 11 | this.init(); 12 | } 13 | 14 | static async init() { 15 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request 16 | // Any permissions granted are retained by the extension, even over upgrade and disable/enable cycling. 17 | // check if permission is granted 18 | this.permission = await browser.permissions.contains({permissions: ['privacy']}); 19 | 20 | // check webRTCIPHandlingPolicy 21 | if (this.permission) { 22 | const result = await browser.privacy.network.webRTCIPHandlingPolicy.get({}); 23 | this.webRTC.value = result.value; 24 | } 25 | } 26 | 27 | static async process() { 28 | if (!this.permission) { 29 | // request permission, Firefox for Android version 102 30 | this.permission = await browser.permissions.request({permissions: ['privacy']}); 31 | if (!this.permission) { return; } 32 | } 33 | 34 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1790270 35 | // WebRTC bypasses Network settings & proxy.onRequest 36 | // {"levelOfControl": "controllable_by_this_extension", "value": "default"} 37 | browser.privacy.network.webRTCIPHandlingPolicy.set({value: this.webRTC.value}); 38 | } 39 | } -------------------------------------------------------------------------------- /src/image/beaker.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/image/bin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/image/container.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/image/ericjung.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/src/image/ericjung.png -------------------------------------------------------------------------------- /src/image/filter.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/image/icon-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/src/image/icon-off.png -------------------------------------------------------------------------------- /src/image/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/src/image/icon.png -------------------------------------------------------------------------------- /src/image/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/src/image/icon128.png -------------------------------------------------------------------------------- /src/image/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/src/image/icon32.png -------------------------------------------------------------------------------- /src/image/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxyproxy/browser-extension/2eb9756159b6e4b9247a5b25f1cc8756dd91a304/src/image/icon48.png -------------------------------------------------------------------------------- /src/image/power.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/image/privateBrowsing.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/manifest-basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "__MSG_extensionNameBasic__", 5 | "description": "__MSG_extensionDescription__", 6 | "version": "9.2", 7 | "default_locale": "en", 8 | "homepage_url": "https://getfoxyproxy.org/", 9 | "author": "Eric Jung", 10 | 11 | "icons": { 12 | "16": "image/icon.png", 13 | "32": "image/icon32.png", 14 | "48": "image/icon48.png", 15 | "128": "image/icon128.png" 16 | }, 17 | 18 | "background": { 19 | "scripts": ["content/background.js"], 20 | "service_worker": "content/background.js", 21 | "type": "module" 22 | }, 23 | 24 | "options_ui": { 25 | "open_in_tab": true, 26 | "page": "content/options.html" 27 | }, 28 | 29 | "action": { 30 | "default_icon": { 31 | "16": "image/icon.png", 32 | "32": "image/icon32.png" 33 | }, 34 | "default_popup": "content/popup.html", 35 | "default_title": "__MSG_extensionName__" 36 | }, 37 | 38 | "permissions": [ 39 | "contextMenus", 40 | "downloads", 41 | "notifications", 42 | "proxy", 43 | "storage", 44 | "tabs", 45 | "webRequest", 46 | "webRequestAuthProvider" 47 | ], 48 | 49 | "host_permissions": [ 50 | "" 51 | ], 52 | 53 | "optional_permissions": [ 54 | "browsingData", 55 | "privacy" 56 | ], 57 | 58 | "commands": { 59 | "proxyByPatterns": { 60 | "description": "__MSG_proxyByPatterns__" 61 | }, 62 | "disable": { 63 | "description": "__MSG_disable__" 64 | }, 65 | "setProxy": { 66 | "description": "__MSG_setProxy__" 67 | }, 68 | "includeHost": { 69 | "description": "__MSG_includeHost__" 70 | }, 71 | "excludeHost": { 72 | "description": "__MSG_excludeHost__" 73 | }, 74 | "setTabProxy": { 75 | "description": "__MSG_setTabProxy__" 76 | }, 77 | "unsetTabProxy": { 78 | "description": "__MSG_unsetTabProxy__" 79 | } 80 | }, 81 | 82 | "storage": { 83 | "managed_schema": "content/schema.json" 84 | }, 85 | 86 | "minimum_chrome_version": "108", 87 | 88 | "browser_specific_settings": { 89 | "gecko": { 90 | "id": "foxyproxy-basic@eric.h.jung", 91 | "strict_min_version": "128.0" 92 | }, 93 | "gecko_android": { 94 | "strict_min_version": "128.0" 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "__MSG_extensionName__", 5 | "description": "__MSG_extensionDescription__", 6 | "version": "9.2", 7 | "default_locale": "en", 8 | "homepage_url": "https://getfoxyproxy.org/", 9 | "author": "Eric Jung", 10 | 11 | "icons": { 12 | "16": "image/icon.png", 13 | "32": "image/icon32.png", 14 | "48": "image/icon48.png", 15 | "128": "image/icon128.png" 16 | }, 17 | 18 | "background": { 19 | "scripts": ["content/background.js"], 20 | "service_worker": "content/background.js", 21 | "type": "module" 22 | }, 23 | 24 | "options_ui": { 25 | "open_in_tab": true, 26 | "page": "content/options.html" 27 | }, 28 | 29 | "action": { 30 | "default_icon": { 31 | "16": "image/icon.png", 32 | "32": "image/icon32.png" 33 | }, 34 | "default_popup": "content/popup.html", 35 | "default_title": "__MSG_extensionName__" 36 | }, 37 | 38 | "permissions": [ 39 | "contextMenus", 40 | "downloads", 41 | "notifications", 42 | "proxy", 43 | "storage", 44 | "tabs", 45 | "webRequest", 46 | "webRequestAuthProvider" 47 | ], 48 | 49 | "host_permissions": [ 50 | "" 51 | ], 52 | 53 | "optional_permissions": [ 54 | "browsingData", 55 | "privacy" 56 | ], 57 | 58 | "commands": { 59 | "proxyByPatterns": { 60 | "description": "__MSG_proxyByPatterns__" 61 | }, 62 | "disable": { 63 | "description": "__MSG_disable__" 64 | }, 65 | "setProxy": { 66 | "description": "__MSG_setProxy__" 67 | }, 68 | "includeHost": { 69 | "description": "__MSG_includeHost__" 70 | }, 71 | "excludeHost": { 72 | "description": "__MSG_excludeHost__" 73 | }, 74 | "setTabProxy": { 75 | "description": "__MSG_setTabProxy__" 76 | }, 77 | "unsetTabProxy": { 78 | "description": "__MSG_unsetTabProxy__" 79 | } 80 | }, 81 | 82 | "storage": { 83 | "managed_schema": "content/schema.json" 84 | }, 85 | 86 | "minimum_chrome_version": "108", 87 | 88 | "browser_specific_settings": { 89 | "gecko": { 90 | "id": "foxyproxy@eric.h.jung", 91 | "strict_min_version": "128.0" 92 | }, 93 | "gecko_android": { 94 | "strict_min_version": "128.0" 95 | } 96 | } 97 | } --------------------------------------------------------------------------------