From ec12bd26de5b4292efc366ee41ef4c8d94e31fa9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 18:25:27 +0200 Subject: Adding upstream version 7.5.1+dfsg. Signed-off-by: Daniel Baumann --- src/_locales/en/messages.json | 180 ++ src/_locales/fr/messages.json | 180 ++ src/_locales/ru/messages.json | 180 ++ src/_locales/zh_CN/messages.json | 160 ++ src/_locales/zh_TW/messages.json | 180 ++ src/about.html | 85 + src/images/ericjung.png | Bin 0 -> 69568 bytes src/images/gray.svg | 197 +++ src/images/icon-off.svg | 23 + src/images/icon.svg | 231 +++ src/images/legacy-version.png | Bin 0 -> 125856 bytes src/images/logo.svg | 316 ++++ src/import-proxy-list.html | 114 ++ src/import.html | 96 + src/log.html | 128 ++ src/manifest.json | 54 + src/options.html | 140 ++ src/pattern-help.html | 119 ++ src/pattern-tester.html | 93 + src/patterns.html | 202 +++ src/popup.html | 64 + src/proxy.html | 193 ++ src/scripts/about.js | 26 + src/scripts/background.js | 275 +++ src/scripts/common.js | 63 + src/scripts/import-proxy-list.js | 240 +++ src/scripts/import.js | 440 +++++ src/scripts/jscolor-2.0.5.js | 1855 ++++++++++++++++++++ src/scripts/log.js | 151 ++ src/scripts/matcher.js | 116 ++ src/scripts/options.js | 380 ++++ src/scripts/pattern-help.js | 9 + src/scripts/pattern-tester.js | 77 + src/scripts/patterns.js | 283 +++ src/scripts/popup.js | 112 ++ src/scripts/proxy.js | 228 +++ src/scripts/utils.js | 324 ++++ src/styles/app.css | 629 +++++++ src/styles/images/animated-overlay.gif | Bin 0 -> 1738 bytes src/styles/images/ui-bg_flat_0_888888_40x100.png | Bin 0 -> 179 bytes src/styles/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 264 bytes src/styles/images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 260 bytes src/styles/images/ui-bg_glass_25_e1f0f5_1x400.png | Bin 0 -> 114 bytes src/styles/images/ui-bg_glass_55_444444_1x400.png | Bin 0 -> 121 bytes src/styles/images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 387 bytes src/styles/images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 259 bytes src/styles/images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 314 bytes src/styles/images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 314 bytes src/styles/images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 384 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 332 bytes .../images/ui-bg_inset-soft_95_fef1ec_1x100.png | Bin 0 -> 123 bytes src/styles/images/ui-icons_222222_256x240.png | Bin 0 -> 6837 bytes src/styles/images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4601 bytes src/styles/images/ui-icons_309bbf_256x240.png | Bin 0 -> 5355 bytes src/styles/images/ui-icons_444444_256x240.png | Bin 0 -> 7006 bytes src/styles/images/ui-icons_454545_256x240.png | Bin 0 -> 6973 bytes src/styles/images/ui-icons_555555_256x240.png | Bin 0 -> 7074 bytes src/styles/images/ui-icons_777620_256x240.png | Bin 0 -> 4676 bytes src/styles/images/ui-icons_777777_256x240.png | Bin 0 -> 7013 bytes src/styles/images/ui-icons_888888_256x240.png | Bin 0 -> 7044 bytes src/styles/images/ui-icons_bf3030_256x240.png | Bin 0 -> 4369 bytes src/styles/images/ui-icons_cc0000_256x240.png | Bin 0 -> 4632 bytes src/styles/images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4601 bytes src/styles/images/ui-icons_ffffff_256x240.png | Bin 0 -> 4369 bytes 64 files changed, 8143 insertions(+) create mode 100644 src/_locales/en/messages.json create mode 100644 src/_locales/fr/messages.json create mode 100644 src/_locales/ru/messages.json create mode 100644 src/_locales/zh_CN/messages.json create mode 100644 src/_locales/zh_TW/messages.json create mode 100644 src/about.html create mode 100644 src/images/ericjung.png create mode 100644 src/images/gray.svg create mode 100644 src/images/icon-off.svg create mode 100644 src/images/icon.svg create mode 100644 src/images/legacy-version.png create mode 100644 src/images/logo.svg create mode 100644 src/import-proxy-list.html create mode 100644 src/import.html create mode 100644 src/log.html create mode 100644 src/manifest.json create mode 100644 src/options.html create mode 100644 src/pattern-help.html create mode 100644 src/pattern-tester.html create mode 100644 src/patterns.html create mode 100644 src/popup.html create mode 100644 src/proxy.html create mode 100644 src/scripts/about.js create mode 100644 src/scripts/background.js create mode 100644 src/scripts/common.js create mode 100644 src/scripts/import-proxy-list.js create mode 100644 src/scripts/import.js create mode 100644 src/scripts/jscolor-2.0.5.js create mode 100644 src/scripts/log.js create mode 100644 src/scripts/matcher.js create mode 100644 src/scripts/options.js create mode 100644 src/scripts/pattern-help.js create mode 100644 src/scripts/pattern-tester.js create mode 100644 src/scripts/patterns.js create mode 100644 src/scripts/popup.js create mode 100644 src/scripts/proxy.js create mode 100644 src/scripts/utils.js create mode 100644 src/styles/app.css create mode 100644 src/styles/images/animated-overlay.gif create mode 100755 src/styles/images/ui-bg_flat_0_888888_40x100.png create mode 100644 src/styles/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 src/styles/images/ui-bg_flat_75_ffffff_40x100.png create mode 100755 src/styles/images/ui-bg_glass_25_e1f0f5_1x400.png create mode 100755 src/styles/images/ui-bg_glass_55_444444_1x400.png create mode 100644 src/styles/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 src/styles/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 src/styles/images/ui-bg_glass_75_dadada_1x400.png create mode 100644 src/styles/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100644 src/styles/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 src/styles/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100755 src/styles/images/ui-bg_inset-soft_95_fef1ec_1x100.png create mode 100644 src/styles/images/ui-icons_222222_256x240.png create mode 100644 src/styles/images/ui-icons_2e83ff_256x240.png create mode 100755 src/styles/images/ui-icons_309bbf_256x240.png create mode 100644 src/styles/images/ui-icons_444444_256x240.png create mode 100644 src/styles/images/ui-icons_454545_256x240.png create mode 100644 src/styles/images/ui-icons_555555_256x240.png create mode 100644 src/styles/images/ui-icons_777620_256x240.png create mode 100644 src/styles/images/ui-icons_777777_256x240.png create mode 100644 src/styles/images/ui-icons_888888_256x240.png create mode 100755 src/styles/images/ui-icons_bf3030_256x240.png create mode 100644 src/styles/images/ui-icons_cc0000_256x240.png create mode 100644 src/styles/images/ui-icons_cd0a0a_256x240.png create mode 100755 src/styles/images/ui-icons_ffffff_256x240.png (limited to 'src') diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json new file mode 100644 index 0000000..530565f --- /dev/null +++ b/src/_locales/en/messages.json @@ -0,0 +1,180 @@ +{ + "extensionName": { "message": "FoxyProxy Standard" }, + "extensionDescription": { "message": "Easy to use advanced Proxy Management tool for everyone" }, + + "extensionNameBasic": { "message": "FoxyProxy Basic" }, + + + "error": { "message": "Error" }, + "erroNoSettings": { "message": "No settings could be found. Please re-install FoxyProxy." }, + + "deleteAll": { "message": "Delete All" }, + "export": { "message": "Export Settings" }, + "import": { "message": "Import Settings" }, + "importProxyList": { "message": "Import Proxy List" }, + "log": { "message": "Log" }, + "myIP": { "message": "What's My IP?" }, + "deleteAllmessage": { "message": "All data deleted" }, + + "authError": { "message": "$1 is refusing connections\nCheck proxy username/password" }, + + + "delete": { "message": "Delete" }, + "deleteNot": { "message": "Do Not Delete" }, + "deleteBrowserData": { "message": "Delete Browser Data" }, + "deleteBrowserDataDescription": { "message": "Cache, cookies, indexedDB storage, DOM local storage, plugin data, service worker data." }, + "deleteBrowserDataNotDescription": { "message": "Stored passwords, browsing and form history, download history, webSQL, and server-bound certificates." }, + "done": { "message": "Done!" }, + + "about": { "message": "About" }, + "syncSettings": { "message": "Synchronize Settings" }, + "syncSettingsHelp": { "message": "Turn on to use Firefox Sync (settings are synchronized across all your Firefox browsers). Turn off to store settings locally. Notice you can have two different instances of settings, one sync and one local." }, +// "onOff": { "message": "On/Off" }, + "on": { "message": "On" }, + "off": { "message": "Off" }, + "confirmTransferToLocal": { "message": "Would you like to transfer the synced settings to your local profile?\nExisting Data will be merged." }, + "confirmTransferToSync": { "message": "Would you like to transfer the local settings to your synced profile?\nExisting Data will be merged." }, + + "modePatterns": { "message": "Use Enabled Proxies By Patterns and Order" }, + "modeDisabled": { "message": "Turn Off (Use Firefox Settings)" }, + "forAll": { "message": "for all URLs" }, + "noProxies": { "message": "You do not have any proxy settings."}, + + + + "edit": { "message": "Edit" }, + "patterns": { "message": "Patterns" }, + "confirmDelete": { "message": "Are you sure you want to delete?" }, + + "addProxy": { "message": "Add Proxy" }, + "editProxy": { "message": "Edit Proxy $1" }, + + + "editPatterns": { "message": "Edit Patterns" }, + "editPatternsFor": { "message": "Edit Patterns of $1" }, + + + "errorWas": { "message": "There was an error. Please try again." }, + "errorSlash": { "message": "No slash in wildcard patterns. You cannot match URL paths because of Firefox restrictions." }, + "errorEmpty": { "message": "Field can not be empty." }, + + + "importEnd": { "message": "Settings were successfully imported" }, + "patternsChanged": { "message": "Some patterns were changed because they contained slashes. Slashes in patterns are not supported anymore." }, + "importEndSlash": { "message": "Import finished. Slashes in patterns are not supported because of a Firefox bug. Please review your patterns and remove slashes, if any." }, + "errorUserPass": { "message": "Please enter both Username & Password." }, + "errorFetch": { "message": "There was an error with the operation" }, + + + "errorPattern": { "message": "Please enter a pattern" }, + "importBW": { "message": "Imported $1 white and $2 black patterns." }, + + "up": { "message": "move up" }, + "down": { "message": "move down" }, + + "disabled": { "message": "Disabled" }, + "active": { "message": "Enabled" }, + "activeNote": { "message": "FoxyProxy ignores everything on this page unless set to" }, + "noMatch": { "message": "No Match" }, + "none": { "message": "None" }, + + "name": { "message": "Name" }, + "pattern": { "message": "Pattern" }, + "type": { "message": "Type" }, + "protocol": { "message": "Protocol" }, + "whitePatterns": { "message": "White Patterns" }, + "blackPatterns": { "message": "Black Patterns" }, + "importedPattern": { "message": "This pattern was imported from an older version of FoxyProxy and changed during import. Here is the original, unchanged pattern:" }, + + + "patternMatch": { "message": "Pattern matches URL!" }, + "patternNotMatch": { "message": "Pattern does not match URL." }, + "errorProtocol": { "message": "Protocol does not match." }, + + + + "proxyType": { "message": "Proxy Type" }, + "color": { "message": "Color" }, + "patternShortcuts": { "message": "Pattern Shortcuts"}, + "title": { "message": "Title or Description (optional)" }, + "ip": { "message": "Proxy IP address or DNS name" }, + "port": { "message": "Port" }, + "username": { "message": "Username (optional)" }, + "password": { "message": "Password (optional)" }, + "togglePW": { "message": "Toggle Password" }, + + "addWhitelist": { "message": "Add whitelist pattern to match all URLs" }, + "noLocal": { "message": "Do not use for localhost and intranet/private IP addresses" }, + + + + "patternTester": { "message": "Pattern Tester" }, + "patternHelp": { "message": "Pattern Help" }, + "welcome": { "message": "Welcome" }, + "options": { "message": "Options" }, + "optionsPage": { "message": "FoxyProxy Options" }, + "enterUrl": { "message": "Enter URL to test" }, + "enterUrlNote": { "message": "(paths and query parameters are ignored)" }, + "patternDetail": { "message": "Enter pattern details." }, + "patternNote": { "message": "(no paths, no query parameters)" }, + "clickTest": { "message": "Click 'Test' when ready" }, + + + + + + + + "ok": { "message": "OK" }, + "clear": { "message": "Clear" }, + "refresh": { "message": "Refresh" }, + "cancel": { "message": "Cancel" }, + "back": { "message": "Back" }, + "test": { "message": "Test" }, + "help": { "message": "Help" }, + "importPatterns": { "message": "Import Patterns" }, + "exportPatterns": { "message": "Export Patterns" }, + "newWhite": { "message": "New White" }, + "newBlack": { "message": "New Black" }, + "save": { "message": "Save" }, + "add": { "message": "Add" }, + "saveAdd": { "message": "Save & Add Another" }, + "saveEditPattern": { "message": "Save & Edit Patterns" }, + "imported": { "message": "Imported" }, + "addBlacklistTip": { "message": "Add blacklist patterns for localhost, 127.0.0.1, 192.168.*.*, 172.16.*.*, & 10.*.*.*" }, + "addBlacklist": { "message": "Add black patterns to prevent this proxy being used for localhost & intranet/private IP addresses" }, + "addWhitelistTip": { "message": "Add whitelist pattern *" }, + "patternCheatSheet": { "message": "Pattern Cheat Sheet" }, + + "logSize": { "message": "Log Size" }, + "url": { "message": "URL" }, + "proxyTitle": { "message": "Proxy Title" }, + "proxyAddress": { "message": "Proxy Address" }, + "matchPattern": { "message": "Match Pattern" }, + "whiteBlack": { "message": "White/Black" }, + "white": { "message": "White" }, + "black": { "message": "Black" }, + "timestamp": { "message": "Timestamp" }, + "notApplicable": {"message": "n/a"}, + "matchedURLs": {"message": "URLs That Matched Patterns"}, + "unmatchedURLs": {"message": "URLs That Did Not Match Patterns"}, + + + "pasteList": { "message": "Paste a proxy list below."}, + "formats": {"message": "Formats"}, + "simple": { "message": "simple" }, + "complete": { "message": "complete" }, + "simpleFormat": { "message": "ip address/server and port are required. username:password are optional."}, + "ipPort": {"message": "ip:port"}, + "ipPortUsernamePassword": {"message": "ip:port:username:password"}, + "examples": {"message": "Examples"}, + "completeFormat": { "message": "With this format, you can specify all proxy settings. But only protocol and server are required."}, + "overwriteProxies": { "message": "Overwrite existing proxies" }, + "overwritProxiesHelp1": { "message": "Check to delete all existing proxies and replace them with proxies from this list." }, + "overwritProxiesHelp2": { "message": "Uncheck to append proxies in this list to existing proxies." }, + "confirmOverwrite": { "message": "Are you sure you want to overwrite existing proxies?" }, + "importsSkipped": { "message": "Skipped $1 lines because they could not be parsed:\n\n$2" }, + "importSucceeded": { + "message": "Read and stored $1 proxies." + } +} \ No newline at end of file diff --git a/src/_locales/fr/messages.json b/src/_locales/fr/messages.json new file mode 100644 index 0000000..9cafec3 --- /dev/null +++ b/src/_locales/fr/messages.json @@ -0,0 +1,180 @@ +{ + "extensionName": { "message": "FoxyProxy Standard" }, + "extensionDescription": { "message": "Gestionnaire de proxy avancé facile d'utilisation" }, + + "extensionNameBasic": { "message": "FoxyProxy Basique" }, + + + "error": { "message": "Erreur" }, + "erroNoSettings": { "message": "Aucun paramètre n'a été trouvé. Essayez de réinstaller FoxyProxy." }, + + "deleteAll": { "message": "Tous supprimer" }, + "export": { "message": "Exporter les paramètres" }, + "import": { "message": "Importer les paramètres" }, + "importProxyList": { "message": "Importer la liste des Proxy" }, + "log": { "message": "Journal" }, + "myIP": { "message": "Quel est mon IP ?" }, + "deleteAllmessage": { "message": "Toutes les données ont été supprimées" }, + + "authError": { "message": "$1 refuse la connection\nVérifiez les identifiants du proxy" }, + + + "delete": { "message": "Supprimer" }, + "deleteNot": { "message": "Ne Pas Supprimer" }, + "deleteBrowserData": { "message": "Supprimer les données du navigateur" }, + "deleteBrowserDataDescription": { "message": "Cache, cookies, base de donnée indexée, cache local du DOM, données du plugin, données du service." }, + "deleteBrowserDataNotDescription": { "message": "Mot de passe sauvegardés, historique et formulaire du navigateur, historique de téléchargement, webSQL, et certificats de serveur." }, + "done": { "message": "Fait !" }, + + "about": { "message": "A propos" }, + "syncSettings": { "message": "Synchroniser les Paramètres" }, + "syncSettingsHelp": { "message": "Activer pour utiliser Firefox Sync (les paramètres sont synchronisés entre tous vos navigateurs Firefox). Desactiver pour sauvegarder les paramètres localement. A noter que deux profils de paramètres peuvent coexister, un synchronisé et un local" }, +// "onOff": { "message": "Activer/Desactiver" }, + "on": { "message": "Activer" }, + "off": { "message": "Desactiver" }, + "confirmTransferToLocal": { "message": "Voulez-vous appliquez les paramètres synchronisés au profil local ?\nLes données existantes seront fusionnées." }, + "confirmTransferToSync": { "message": "Voulez-vous appliquez les paramètres locaux au profil synchronisé ?\nLes données existantes seront fusionnées." }, + + "modePatterns": { "message": "Utiliser les proxys activés par modèle et ordre" }, + "modeDisabled": { "message": "Désactiver (Utiliser les paramètres de firefox)" }, + "forAll": { "message": "pour toutes les URLs" }, + "noProxies": { "message": "Vous n'avez aucun paramètres de proxy."}, + + + + "edit": { "message": "Modifier" }, + "patterns": { "message": "Modèles" }, + "confirmDelete": { "message": "Etes-vous sûr de vouloir supprimer ?" }, + + "addProxy": { "message": "Ajouter un Proxy" }, + "editProxy": { "message": "Modifier le Proxy $1" }, + + + "editPatterns": { "message": "Editer les Modèles" }, + "editPatternsFor": { "message": "Editer les Modèles de $1" }, + + + "errorWas": { "message": "Une erreur est survenue. Merci de réessayer." }, + "errorSlash": { "message": "Pas de barre oblique dans des modèles génériques. Vous ne pouvez pas associer d'URL à cause des restrictions de Firefox." }, + "errorEmpty": { "message": "Le champ ne peut pas être vide." }, + + + "importEnd": { "message": "Les paramètres ont été correctement importés" }, + "patternsChanged": { "message": "Certains modèles ont été changés car ils contenaient des barres obliques. Elles ne sont plus supportées." }, + "importEndSlash": { "message": "Import fini. Les barres obliques ne sont plus supportées dans les modèles à cause d'un bug de Firefox. Merci de revoir votre modèle et de supprimer les éventuelles barres obliques." }, + "errorUserPass": { "message": "Merci d'entrer votre nom d'utilisateur et mot de passe." }, + "errorFetch": { "message": "Une erreur est survenue durant l'opération" }, + + + "errorPattern": { "message": "Merci d'entrer un modèle" }, + "importBW": { "message": "$1 modèle(s) d'inclusions et $2 modèle(s) d'exclusions ont été importés." }, + + "up": { "message": "remonter" }, + "down": { "message": "descendre" }, + + "disabled": { "message": "Désactiver" }, + "active": { "message": "Activer" }, + "activeNote": { "message": "FoxyProxy ignore tous sur cette page à moins d'être explicitement configuré pour" }, + "noMatch": { "message": "Pas de Correspondance" }, + "none": { "message": "Aucun" }, + + "name": { "message": "Nom" }, + "pattern": { "message": "Modèle" }, + "type": { "message": "Type" }, + "protocol": { "message": "Protocole" }, + "whitePatterns": { "message": "Modèle d'inclusion" }, + "blackPatterns": { "message": "Modèle d'exclusion" }, + "importedPattern": { "message": "Ce modèle a été importé à partir d'une ancienne version de FoxyProxy et a été modifié lors de l'import. Voici le modèle original non modifié:" }, + + + "patternMatch": { "message": "Ce modèle correspond à l'URL!" }, + "patternNotMatch": { "message": "Ce modèle ne correspond pas à l'URL." }, + "errorProtocol": { "message": "Le protocole ne correspond pas." }, + + + + "proxyType": { "message": "Type de Proxy" }, + "color": { "message": "Couleur" }, + "patternShortcuts": { "message": "Raccourcis de Modèle"}, + "title": { "message": "Nom ou Description (optionnel)" }, + "ip": { "message": "Adresse IP du Proxy ou nom du DNS" }, + "port": { "message": "Port" }, + "username": { "message": "Nom d'utilisateur (optionnel)" }, + "password": { "message": "Mot de passe (optionnel)" }, + "togglePW": { "message": "Afficher/Masquer le mot de passe" }, + + "addWhitelist": { "message": "Ajouter un modèle d'inclusion qui correspond à toutes les urls" }, + "noLocal": { "message": "Ne pas utiliser pour localhost et des adresses IP privées ou d'intranet" }, + + + + "patternTester": { "message": "Testeur de Modèles" }, + "patternHelp": { "message": "Aide Modèle" }, + "welcome": { "message": "Bienvenu" }, + "options": { "message": "Options" }, + "optionsPage": { "message": "Options FoxyProxy" }, + "enterUrl": { "message": "Entrer l'URL à tester" }, + "enterUrlNote": { "message": "(les chemins d'URL et les paramètres sont ignorés)" }, + "patternDetail": { "message": "Entrer les informations du modèle." }, + "patternNote": { "message": "(pas de chemins d'URL, pas de paramètres de requêtes)" }, + "clickTest": { "message": "Cliquez sur 'Test' lorsque vous êtes prêt" }, + + + + + + + + "ok": { "message": "Accepter" }, + "clear": { "message": "Effacer" }, + "refresh": { "message": "Rafraîchir" }, + "cancel": { "message": "Annuler" }, + "back": { "message": "Retour" }, + "test": { "message": "Tester" }, + "help": { "message": "Aide" }, + "importPatterns": { "message": "Importer les Modèles" }, + "exportPatterns": { "message": "Exporter les Modèles" }, + "newWhite": { "message": "Nouveau Modèle d'Inclusion" }, + "newBlack": { "message": "Nouveau Modèle d'Exclusion" }, + "save": { "message": "Sauvegarder" }, + "add": { "message": "Ajouter" }, + "saveAdd": { "message": "Sauvegarder et Ajouter un autre" }, + "saveEditPattern": { "message": "Sauvegarder et Modifier les Modèles" }, + "imported": { "message": "Importé" }, + "addBlacklistTip": { "message": "Ajouter des modèles d'exclusions pour localhost, 127.0.0.1, 192.168.*.*, 172.16.*.*, & 10.*.*.*" }, + "addBlacklist": { "message": "Ajouter des modèles d'exclusions pour empêcher ce proxy d'être utiliser pour localhost et des adresses IP privées ou d'intranet" }, + "addWhitelistTip": { "message": "Ajouter un modèle d'inclusion *" }, + "patternCheatSheet": { "message": "Aide-Mémoire pour les Modèles" }, + + "logSize": { "message": "Taille du Journal" }, + "url": { "message": "URL" }, + "proxyTitle": { "message": "Nom du Proxy" }, + "proxyAddress": { "message": "Adresse du Proxy" }, + "matchPattern": { "message": "Modèle de correspondance" }, + "whiteBlack": { "message": "Inclusion/Exclusion" }, + "white": { "message": "Inclusion" }, + "black": { "message": "Exclusion" }, + "timestamp": { "message": "Date" }, + "notApplicable": {"message": "s/o"}, + "matchedURLs": {"message": "URLs qui correspondent aux Modèles"}, + "unmatchedURLs": {"message": "URLs qui ne correspondent pas aux Modèles"}, + + + "pasteList": { "message": "Coller une liste de proxy ci-dessous."}, + "formats": {"message": "Formats"}, + "simple": { "message": "simple" }, + "complete": { "message": "complet" }, + "simpleFormat": { "message": "l'adresse ip ou nom du serveur et le port sont requis. login:mot de passe sont optionnels."}, + "ipPort": {"message": "ip:port"}, + "ipPortUsernamePassword": {"message": "ip:port:login:mot de passe"}, + "examples": {"message": "Exemples"}, + "completeFormat": { "message": "Dans ce format, vous pouvez spécifier tous les paramètres du proxy. Mais seulement le protocole et le nom du serveur sont requis."}, + "overwriteProxies": { "message": "Ecraser les proxys existants" }, + "overwritProxiesHelp1": { "message": "Cochez pour supprimer tous les proxys existants et les remplacer par ceux de la liste." }, + "overwritProxiesHelp2": { "message": "Décocher pour ajouter les proxys de la liste à ceux déjà existant." }, + "confirmOverwrite": { "message": "Etes-vous sûr de vouloir ecraser les proxys existants ?" }, + "importsSkipped": { "message": "$1 lignes ignorées car elles ne pouvaient pas être interprétées:\n\n$2" }, + "importSucceeded": { + "message": "$1 proxys interprétés et stockés." + } +} \ No newline at end of file diff --git a/src/_locales/ru/messages.json b/src/_locales/ru/messages.json new file mode 100644 index 0000000..56e4324 --- /dev/null +++ b/src/_locales/ru/messages.json @@ -0,0 +1,180 @@ +{ + "extensionName": { "message": "FoxyProxy Standard" }, + "extensionDescription": { "message": "Продвинутый, но простой в использовании инструмент для управления прокси для каждого" }, + + "extensionNameBasic": { "message": "FoxyProxy Basic" }, + + + "error": { "message": "Ошибка" }, + "erroNoSettings": { "message": "Настройки не найдены. Пожалуйста, переустановите FoxyProxy." }, + + "deleteAll": { "message": " Удалить всё" }, + "export": { "message": "Экспортировать настройки" }, + "import": { "message": "Импортировать настройки" }, + "importProxyList": { "message": "Импортировать список прокси" }, + "log": { "message": "Лог" }, + "myIP": { "message": "Какой у меня IP адрес?" }, + "deleteAllmessage": { "message": "Все данные удалены" }, + + "authError": { "message": "Отказ подключения к $1\nПроверьте имя пользователя и пароль" }, + + + "delete": { "message": "Удалить" }, + "deleteNot": { "message": "Не удалять" }, + "deleteBrowserData": { "message": "Удалить данные браузера" }, + "deleteBrowserDataDescription": { "message": "Кэш, куки, база индексов, хранилище DOM, данные расширения, служебные данные." }, + "deleteBrowserDataNotDescription": { "message": "Сохранённые пароли, история браузера и форм, исторя загрузок, webSQL, серверные сертификаты" }, + "done": { "message": "Готово!" }, + + "about": { "message": "О расширении" }, + "syncSettings": { "message": "Синхронизировать настройки" }, + "syncSettingsHelp": { "message": "Включите, чтобы использовать синхронизацию Firefox (настройки синхронизируются между всеми вашими браузерами Firefox). Отключите, чтобы хранить настройки локально. Обратите внимание, что вы можете иметь два набора настроек — синхронизированный и локальный." }, +// "onOff": { "message": "Вкл./Выкл." }, + "on": { "message": "Вкл." }, + "off": { "message": "Выкл." }, + "confirmTransferToLocal": { "message": "Хотите переместить синхронизированные настройки в локальный профиль?\nОни будут объединены с существующими данными." }, + "confirmTransferToSync": { "message": "Хотите переместить локальные настройки синхронизированный профиль?\nОни будут объединены с существующими данными." }, + + "modePatterns": { "message": "Использовать проки по шаблону и порядку" }, + "modeDisabled": { "message": "Откоючить (Использовать настройки Firefox)" }, + "forAll": { "message": "Для всех URL" }, + "noProxies": { "message": "У вас нет каких-либо прокси настроек."}, + + + + "edit": { "message": "Редактировать" }, + "patterns": { "message": "Шаблоны" }, + "confirmDelete": { "message": "Вы уверены, что хотите удалить?" }, + + "addProxy": { "message": "Добавить прокси" }, + "editProxy": { "message": "Редактировать прокси $1" }, + + + "editPatterns": { "message": "Редактировать шаблон" }, + "editPatternsFor": { "message": "Редактировать шаблон для $1" }, + + + "errorWas": { "message": "Ошибка. Пожалуйста, попробуйте снова." }, + "errorSlash": { "message": "Не разрешено использовать cимвол слэш в шаблонах. Вы не можете сопоставить URL пути вследствие ограничения Firefox." }, + "errorEmpty": { "message": "Поле не может быть пустым." }, + + + "importEnd": { "message": "Настройки успешно импортированы." }, + "patternsChanged": { "message": "Некоторые шаблоны были изменены из-за того, что они содержали символ слэш. Символы слэш в шаблонах более не поддерживаются." }, + "importEndSlash": { "message": "Импорт закончен. Символ слэш не поддерживается из-за проблемы в Firefox. Пожалуйста, проанализируйте ваши шаблоны и удалите символы слэш если вы их используете." }, + "errorUserPass": { "message": "Пожалуйста, введите имя пользователя и пароль." }, + "errorFetch": { "message": "Проблема с данным действием." }, + + + "errorPattern": { "message": "Пожалуйста, введите шаблон" }, + "importBW": { "message": "Импортировано $1 белых и $2 черных шаблонов." }, + + "up": { "message": "передвинуть выше" }, + "down": { "message": "передвинуть ниже" }, + + "disabled": { "message": "Отключено" }, + "active": { "message": "Включено" }, + "activeNote": { "message": "FoxyProxy игнорирует все настройки на этой странице если не включено обратное" }, + "noMatch": { "message": "Нет совпадения" }, + "none": { "message": "Ничего" }, + + "name": { "message": "Название" }, + "pattern": { "message": "Шаблон" }, + "type": { "message": "Тип" }, + "protocol": { "message": "Протокол" }, + "whitePatterns": { "message": "Белые шаблоны" }, + "blackPatterns": { "message": "Черные шаблоны" }, + "importedPattern": { "message": "Этот шаблон был импортирован из предыдущей версии FoxyProxy и был изменен в процессе импорта. Оригинальный, не измененный шаблон тут:" }, + + + "patternMatch": { "message": "Шаблон сопоставлен URL!" }, + "patternNotMatch": { "message": "Шаблон не сопоставлен URL." }, + "errorProtocol": { "message": "Протокол не совпадает." }, + + + + "proxyType": { "message": "Тип прокси" }, + "color": { "message": "Цвет" }, + "patternShortcuts": { "message": "Клавиатурная комбинация для шаблона"}, + "title": { "message": "Название или описание (опционально)" }, + "ip": { "message": " Прокси IP адрес или имя DNS" }, + "port": { "message": "Порт" }, + "username": { "message": "Имя пользователя (опционально)" }, + "password": { "message": "Пароль (опционально)" }, + "togglePW": { "message": "Показывать пароль" }, + + "addWhitelist": { "message": "Добавить whitelist шаблон совпадающий со всеми URL" }, + "noLocal": { "message": "Не использовать локальный и внутренний/частный IP адрес" }, + + + + "patternTester": { "message": "Тестер шаблонов" }, + "patternHelp": { "message": "Помощь с шаблонами" }, + "welcome": { "message": "Добро пожаловать" }, + "options": { "message": "Настройки" }, + "optionsPage": { "message": "Настройки FoxyProxy" }, + "enterUrl": { "message": "Введите URL на тест" }, + "enterUrlNote": { "message": "(параметры путей и запросов будут проигнорированы)" }, + "patternDetail": { "message": "Введите детали шаблона." }, + "patternNote": { "message": "(без путей и параметров запросов)" }, + "clickTest": { "message": "Нажмите 'Начать тест' как будете готовы" }, + + + + + + + + "ok": { "message": "OK" }, + "clear": { "message": "Очистить" }, + "refresh": { "message": "Обновить" }, + "cancel": { "message": "Отменить" }, + "back": { "message": "Назад" }, + "test": { "message": "Тест" }, + "help": { "message": "Помощь" }, + "importPatterns": { "message": "Импортировать шаблон" }, + "exportPatterns": { "message": "Экспортировать шаблон" }, + "newWhite": { "message": "Новый белый" }, + "newBlack": { "message": "Новый черный" }, + "save": { "message": "Сохранить" }, + "add": { "message": "Добавить" }, + "saveAdd": { "message": "Сохранить & добавить еще" }, + "saveEditPattern": { "message": "Сохранить & и редактировать шаблоны" }, + "imported": { "message": "Импортитовано" }, + "addBlacklistTip": { "message": "Добавть blacklist шаблоны для локального адреса, 127.0.0.1, 192.168.*.*, 172.16.*.*, & 10.*.*.*" }, + "addBlacklist": { "message": "Добавить black шаблоны чтобы предотвратить использование этого прокси для локального адреса и внутренних/частных IP адресов" }, + "addWhitelistTip": { "message": "Добавить whitelist шаблон *" }, + "patternCheatSheet": { "message": "Шпаргалка по шаблонам" }, + + "logSize": { "message": "Размер лога" }, + "url": { "message": "URL" }, + "proxyTitle": { "message": "Название прокси" }, + "proxyAddress": { "message": "Адрес прокси" }, + "matchPattern": { "message": "Шаблон для совпадения" }, + "whiteBlack": { "message": "Белый/Черный" }, + "white": { "message": "Белый" }, + "black": { "message": "Черный" }, + "timestamp": { "message": "Время" }, + "notApplicable": {"message": "n/a"}, + "matchedURLs": {"message": "URL удовлетворяющие шаблону"}, + "unmatchedURLs": {"message": "URLs не удовлетворяющие шаблону"}, + + + "pasteList": { "message": "Вставте список прокси ниже."}, + "formats": {"message": "Форматы"}, + "simple": { "message": "простой" }, + "complete": { "message": "полный" }, + "simpleFormat": { "message": "Необходимы ip адрес/сервер и порт. Имя пользователя и пароль — опционально."}, + "ipPort": {"message": "ip адрес:порт"}, + "ipPortUsernamePassword": {"message": "ip адрес:порт:имя пользователя:пароль"}, + "examples": {"message": "Примеры"}, + "completeFormat": { "message": "Используя этот формат вы можете указать все прокси настройки, но только протокол и адрес прокси являются необходимыми."}, + "overwriteProxies": { "message": "Перезаписать имеющиеся прокси" }, + "overwritProxiesHelp1": { "message": "Активируйте чтобы заменить все существующие прокси новыми из этого списка." }, + "overwritProxiesHelp2": { "message": "Дезактивируйте чтобы добавить прокси из списка к уже имеющимся." }, + "confirmOverwrite": { "message": "Вы уверены, что хотите удалить все ранее добавленные прокси?" }, + "importsSkipped": { "message": "Пропущено $1 линий потому что они не смогли быть обработаны:\n\n$2" }, + "importSucceeded": { + "message": "Считано и сохранено $1 прокси." + } +} diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json new file mode 100644 index 0000000..4fa0f0f --- /dev/null +++ b/src/_locales/zh_CN/messages.json @@ -0,0 +1,160 @@ +{ + "extensionName": { "message": "FoxyProxy 标准版" }, + "extensionDescription": { "message": "易于使用,适用于任何人的高级代理管理工具" }, + + "extensionNameBasic": { "message": "FoxyProxy 基础版" }, + + + "error": { "message": "错误" }, + "erroNoSettings": { "message": "未找到设置。请重新安装 FoxyProxy。" }, + + "deleteAll": { "message": "全部删除" }, + "export": { "message": "导出" }, + "import": { "message": "导入" }, + "log": { "message": "日志" }, + "myIP": { "message": "查询我的 IP" }, + "deleteAllmessage": { "message": "已删除所有数据" }, + + "authError": { "message": "$1 拒绝连接\n请检查代理用户名/密码" }, + + + "delete": { "message": "删除" }, + "deleteNot": { "message": "不要删除" }, + "deleteBrowserData": { "message": "删除浏览器数据" }, + "deleteBrowserDataDescription": { "message": "缓存、cookies、indexedDB 存储、DOM 本地存储、插件数据、service worker 数据。" }, + "deleteBrowserDataNotDescription": { "message": "已保存的密码、浏览和表单历史、下载历史、webSQL 和服务器绑定证书。" }, + "done": { "message": "完成!" }, + + "about": { "message": "关于" }, + "syncSettings": { "message": "同步设置" }, + "syncSettingsHelp": { "message": "开启以使用 Firefox 同步(将在你所有的 Firefox 浏览器间同步设置)。关闭则只在本地保存设置。注意你可以有两套不同的设置,一套同步的和一套本地的。" }, +// "onOff": { "message": "开启/关闭" }, + "on": { "message": "开启" }, + "off": { "message": "关闭" }, + "confirmTransferToLocal": { "message": "是否将已同步的设置传输到你的本地配置文件?\n将合并已有的数据。" }, + "confirmTransferToSync": { "message": "是否将本地设置传输到你的同步配置文件?\n将合并已有的数据。" }, + + "modePatterns": { "message": "按模式和顺序使用启用的代理" }, + "modeDisabled": { "message": "关闭(使用 Firefox 设置)" }, + "forAll": { "message": "对全部 URLs 使用" }, + "noProxies": { "message": "你没有设置任何代理。"}, + + + + "edit": { "message": "编辑" }, + "patterns": { "message": "模式" }, + "confirmDelete": { "message": "你确定要删除吗?" }, + + "addProxy": { "message": "添加代理" }, + "editProxy": { "message": "编辑代理 $1" }, + + + "editPatterns": { "message": "编辑模式" }, + "editPatternsFor": { "message": "编辑 $1 的模式" }, + + + "errorWas": { "message": "存在错误。请重试。" }, + "errorSlash": { "message": "通配符模式不能有斜线 /。因为 Firefox 的限制你无法匹配 URL 路径。" }, + "errorEmpty": { "message": "字段不能为空。" }, + + + "importEnd": { "message": "设置已成功导入" }, + "patternsChanged": { "message": "有些模式已做更改因为它们包含斜线。不再支持于模式里使用斜线。" }, + "importEndSlash": { "message": "导入已完成。由于 Firefox 缺陷,已不再支持于模式里使用斜线。请复查你的模式并移除所有斜线。" }, + "errorUserPass": { "message": "请输入用户名和密码。" }, + "errorFetch": { "message": "操作出现错误" }, + + + "errorPattern": { "message": "请输入一项模式" }, + "importBW": { "message": "已导入 $1 项白名单和 $2 项黑名单模式。" }, + + "up": { "message": "上移" }, + "down": { "message": "下移" }, + + "disabled": { "message": "已禁用" }, + "active": { "message": "已启用" }, + "activeNote": { "message": "FoxyProxy 将忽略此页面的所有选项,除非设为" }, + "noMatch": { "message": "无匹配" }, + "none": { "message": "无" }, + + "name": { "message": "名称" }, + "pattern": { "message": "模式" }, + "type": { "message": "类型" }, + "protocol": { "message": "协议" }, + "whitePatterns": { "message": "白名单模式" }, + "blackPatterns": { "message": "黑名单模式" }, + "importedPattern": { "message": "此模式来自自旧版本的 FoxyProxy,且在导入过程中更改。下面为未更改的原模式:" }, + + + "patternMatch": { "message": "模式与 URL 匹配!" }, + "patternNotMatch": { "message": "模式与 URL 不匹配。" }, + "errorProtocol": { "message": "协议不匹配。" }, + + + + "proxyType": { "message": "代理类型" }, + "color": { "message": "颜色" }, + "patternShortcuts": { "message": "模式快捷选项"}, + "title": { "message": "标题或描述(可选)" }, + "ip": { "message": "代理 IP 地址或 DNS 名称" }, + "port": { "message": "端口" }, + "username": { "message": "用户名(可选)" }, + "password": { "message": "密码(可选)" }, + "togglePW": { "message": "显示密码" }, + + "addWhitelist": { "message": "添加匹配全部 URLs 的白名单模式" }, + "noLocal": { "message": "不用于本地主机和内部网/私密 IP 地址" }, + + + + "patternTester": { "message": "模式测试器" }, + "patternHelp": { "message": "模式帮助" }, + "welcome": { "message": "欢迎" }, + "options": { "message": "选项" }, + "optionsPage": { "message": "FoxyProxy 选项" }, + "enterUrl": { "message": "输入要测试的 URL" }, + "enterUrlNote": { "message": "(已忽略路径和查询参数)" }, + "patternDetail": { "message": "输入模式详细信息。" }, + "patternNote": { "message": "(无路径,无查询参数)" }, + "clickTest": { "message": "准备好后点击“测试”" }, + + + + + + + + "ok": { "message": "确定" }, + "clear": { "message": "清除" }, + "refresh": { "message": "刷新" }, + "cancel": { "message": "取消" }, + "back": { "message": "返回" }, + "test": { "message": "测试" }, + "help": { "message": "帮助" }, + "importPatterns": { "message": "导入模式" }, + "exportPatterns": { "message": "导出模式" }, + "newWhite": { "message": "新增白名单" }, + "newBlack": { "message": "新增黑名单" }, + "save": { "message": "保存" }, + "add": { "message": "添加" }, + "saveAdd": { "message": "保存并添加另一个" }, + "saveEditPattern": { "message": "保存并编辑模式" }, + "imported": { "message": "已导入" }, + "addBlacklistTip": { "message": "把本地主机、127.0.0.1、192.168.*.*、172.16.*.* 和 10.*.*.* 添加到黑名单模式" }, + "addBlacklist": { "message": "添加黑名单模式可以防止代理用在本地主机和内部网/私密 IP 地址上" }, + "addWhitelistTip": { "message": "添加白名单模式 *" }, + "patternCheatSheet": { "message": "模式速查表" }, + + "logSize": { "message": "日志大小" }, + "url": { "message": "URL" }, + "proxyTitle": { "message": "代理标题" }, + "proxyAddress": { "message": "代理地址" }, + "matchPattern": { "message": "匹配模式" }, + "whiteBlack": { "message": "白/黑名单" }, + "white": { "message": "白名单" }, + "black": { "message": "黑名单" }, + "timestamp": { "message": "时间戳" }, + "notApplicable": {"message": "不适用"}, + "matchedURLs": {"message": "与模式匹配的 URLs"}, + "unmatchedURLs": {"message": "与模式不匹配的 URLs"} +} diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json new file mode 100644 index 0000000..9790391 --- /dev/null +++ b/src/_locales/zh_TW/messages.json @@ -0,0 +1,180 @@ +{ + "extensionName": { "message": "FoxyProxy Standard" }, + "extensionDescription": { "message": "任誰都能快速上手的進階代理伺服器管理工具" }, + + "extensionNameBasic": { "message": "FoxyProxy Basic" }, + + + "error": { "message": "錯誤" }, + "erroNoSettings": { "message": "找不到設定檔。 請重新安裝 FoxyProxy。" }, + + "deleteAll": { "message": "移除全部" }, + "export": { "message": "匯出設定" }, + "import": { "message": "匯入設定" }, + "importProxyList": { "message": "匯入代理伺服器清單" }, + "log": { "message": "日誌" }, + "myIP": { "message": "我的 IP 為何?" }, + "deleteAllmessage": { "message": "已將所有資料移除" }, + + "authError": { "message": "$1 拒絕連線\n請檢查代理伺服器的使用者帳號與密碼是否正確" }, + + + "delete": { "message": "移除" }, + "deleteNot": { "message": "不要移除" }, + "deleteBrowserData": { "message": "移除瀏覽器資料" }, + "deleteBrowserDataDescription": { "message": "快取、Cookies、indexedDB 儲存空間, DOM 本機儲存空間, 擴充套件資料、Service Worker 資料。" }, + "deleteBrowserDataNotDescription": { "message": "以儲存的密碼、瀏覽與表單歷史紀錄、下載紀錄、WebSQL 和 Server-Bound 認證。" }, + "done": { "message": "完成!" }, + + "about": { "message": "關於" }, + "syncSettings": { "message": "同步設定" }, + "syncSettingsHelp": { "message": "啟用此設定將會使用 Firefox Sync (設定將同步至所有的瀏覽器中)。 停用此設定僅將設定儲存於本機中。 小提醒:您可以儲存兩種設定,一種為本機設定,另一種為同步設定。" }, +// "onOff": { "message": "On/Off" }, + "on": { "message": "啟用" }, + "off": { "message": "停用" }, + "confirmTransferToLocal": { "message": "是否將您的設定同步至本機設定檔中?\n已經存在的資料將會被合併。" }, + "confirmTransferToSync": { "message": "是否將您的本機設定同步至同步設定檔中?\n已經存在的資料將會被合併。" }, + + "modePatterns": { "message": "以設定的規則與順序套用代理伺服器設定" }, + "modeDisabled": { "message": "停用 (使用 Firefox 內建設定)" }, + "forAll": { "message": "於所有網址上啟用" }, + "noProxies": { "message": "您尚未建立任何代理伺服器設定。"}, + + + + "edit": { "message": "編輯" }, + "patterns": { "message": "規則" }, + "confirmDelete": { "message": "您確定要移除此設定嗎?" }, + + "addProxy": { "message": "新增代理伺服器" }, + "editProxy": { "message": "編輯代理伺服器 - $1" }, + + + "editPatterns": { "message": "編輯規則" }, + "editPatternsFor": { "message": "編輯規則 - $1" }, + + + "errorWas": { "message": "發生錯誤。請重試。" }, + "errorSlash": { "message": "萬用字元不能包含斜線。 由於 Firefox 的限制,您無法對應到任何的網址或路徑。" }, + "errorEmpty": { "message": "該欄位不可留空。" }, + + + "importEnd": { "message": "設定檔匯入成功。" }, + "patternsChanged": { "message": "部分規則因含有斜線而被變更。 不支援斜線規則。" }, + "importEndSlash": { "message": "匯入完成。 由於 Firefox 的一隻 Bug ,已不支援斜線規則。 請重新檢查您的規則是否仍含有斜線,若有請將之移除。" }, + "errorUserPass": { "message": "請輸入使用者名稱和密碼。" }, + "errorFetch": { "message": "該操作行為發生錯誤。" }, + + + "errorPattern": { "message": "請輸入規則" }, + "importBW": { "message": "已匯入 $1 個白名單與 $2 個黑名單規則。" }, + + "up": { "message": "上移" }, + "down": { "message": "下移" }, + + "disabled": { "message": "已停用" }, + "active": { "message": "已啟用" }, + "activeNote": { "message": "FoxyProxy 將會略過此頁面上所有設定,除非設定為" }, + "noMatch": { "message": "沒有對應結果" }, + "none": { "message": "無" }, + + "name": { "message": "名稱" }, + "pattern": { "message": "規則" }, + "type": { "message": "類型" }, + "protocol": { "message": "協定" }, + "whitePatterns": { "message": "白名單規則" }, + "blackPatterns": { "message": "黑名單規則" }, + "importedPattern": { "message": "這個規則為舊版匯入的規則,且於匯入期間已被修改。 以下為其原始規則:" }, + + + "patternMatch": { "message": "規則與網址相對應!" }, + "patternNotMatch": { "message": "規則與網址無法對應。" }, + "errorProtocol": { "message": "協定不對應。" }, + + + + "proxyType": { "message": "代理伺服器類型" }, + "color": { "message": "顏色" }, + "patternShortcuts": { "message": "規則快捷鍵"}, + "title": { "message": "標題或描述(選填)" }, + "ip": { "message": "代理伺服器 IP 位址或 DNS 域名" }, + "port": { "message": "連接埠" }, + "username": { "message": "使用者名稱(選填)" }, + "password": { "message": "密碼(選填)" }, + "togglePW": { "message": "顯示或隱藏密碼" }, + + "addWhitelist": { "message": "新增白名單規則以對應到所有網址" }, + "noLocal": { "message": "請不要使用 localhost 或內部 / 私有 IP 位址" }, + + + + "patternTester": { "message": "規則測試器" }, + "patternHelp": { "message": "規則說明" }, + "welcome": { "message": "歡迎" }, + "options": { "message": "選項" }, + "optionsPage": { "message": "FoxyProxy 選項" }, + "enterUrl": { "message": "請輸入網址以進行測試" }, + "enterUrlNote": { "message": "(已略過路徑與查詢字串)" }, + "patternDetail": { "message": "輸入規則詳細資料。" }, + "patternNote": { "message": "(不包含路徑及查詢字串)" }, + "clickTest": { "message": "準備就緒後按下「測試」開始進行測試。" }, + + + + + + + + "ok": { "message": "確定" }, + "clear": { "message": "清除" }, + "refresh": { "message": "重新整理" }, + "cancel": { "message": "取消" }, + "back": { "message": "返回" }, + "test": { "message": "測試" }, + "help": { "message": "說明" }, + "importPatterns": { "message": "匯入規則" }, + "exportPatterns": { "message": "匯出規則" }, + "newWhite": { "message": "新增白名單" }, + "newBlack": { "message": "新增黑名單" }, + "save": { "message": "儲存" }, + "add": { "message": "新建" }, + "saveAdd": { "message": "儲存並新建" }, + "saveEditPattern": { "message": "儲存並編輯規則" }, + "imported": { "message": "已匯入" }, + "addBlacklistTip": { "message": "將 localhost、127.0.0.1、192.168.*.*、172.16.*.* 和 10.*.*.* 新增至黑名單規則中" }, + "addBlacklist": { "message": "新增黑名單規則可以防止這個代理伺服器設定被套用於 localhost 或內部 / 私有 IP 位址上。" }, + "addWhitelistTip": { "message": "將 * 新增至白名單規則中" }, + "patternCheatSheet": { "message": "規則速查表" }, + + "logSize": { "message": "日誌大小" }, + "url": { "message": "網址" }, + "proxyTitle": { "message": "代理伺服器標題" }, + "proxyAddress": { "message": "代理伺服器位址" }, + "matchPattern": { "message": "對應規則" }, + "whiteBlack": { "message": "白名單 / 黑名單" }, + "white": { "message": "白名單" }, + "black": { "message": "黑名單" }, + "timestamp": { "message": "時間戳" }, + "notApplicable": {"message": "不適用"}, + "matchedURLs": {"message": "與規則相對應的網址"}, + "unmatchedURLs": {"message": "與規則不對應的網址"}, + + + "pasteList": { "message": "在下方貼上代理伺服器清單。"}, + "formats": {"message": "格式"}, + "simple": { "message": "簡易版" }, + "complete": { "message": "完整版" }, + "simpleFormat": { "message": "IP 位址 / 伺服器及連接埠皆為必填。 username:password(使用者名稱與密碼)為選填。"}, + "ipPort": {"message": "ip:port"}, + "ipPortUsernamePassword": {"message": "ip:port:username:password"}, + "examples": {"message": "範例"}, + "completeFormat": { "message": "使用這種格式,您可以指定所有代理伺服器設定。其中僅有協定和伺服器為必填。"}, + "overwriteProxies": { "message": "覆蓋已經存在的代理伺服器" }, + "overwritProxiesHelp1": { "message": "若勾選此選項,則會將所有已存在的代理伺服器先行移除並取代為此清單上的代理伺服器設定。" }, + "overwritProxiesHelp2": { "message": "若不勾選此選項,則會將此清單內的代理伺服器新增至已經存在的代理伺服器清單之後。" }, + "confirmOverwrite": { "message": "您確定要覆蓋所有已存在的代理伺服器設定嗎?" }, + "importsSkipped": { "message": "因為無法解析,已略過 $1 行:\n\n$2" }, + "importSucceeded": { + "message": "讀取並匯入 $1 個代理伺服器設定" + } +} \ No newline at end of file diff --git a/src/about.html b/src/about.html new file mode 100644 index 0000000..2dd97d3 --- /dev/null +++ b/src/about.html @@ -0,0 +1,85 @@ + + + + + FoxyProxy + + + + + + + + +
+ + +
+ + +
+

Welcome!

+

FoxyProxy has been around for almost 15 years. So if you're upgrading from a legacy version, and your proxy settings are missing, please import them.
+ (This message only appears after FoxyProxy has upgraded or is first installed.)

+
+ +

Info

+

Edition:
+ Version:
+ Source Code: GitHub

+ +

Help

+

+ Open a Support Ticket (no registration required)
+ Email Support
+ Pattern Test & Pattern Help (no internet connection required)
+ What's my IP Address? +

+ +

Support Me

+

Please donate or buy dedicated VPN/Proxy Servers in over 100 countries (including such remote places like Reunion Island).

+

Thank you for using FoxyProxy!

+
+

Eric H. Jung
Denver, Colorado, USA

+ +

Release Notes for Recent Releases

+

Version 7.5.1

+
    +
  • French translation -- Thanks samuikaze.
  • +
  • Traditional Chinese translation -- Thanks Hugo-C.
  • +
  • Russian translation -- Thanks Vadim.
  • +
+

Version 7.5

+
    +
  • Import Proxy Lists! Long requested feature. See help.
  • +
  • Simplified Chinese translation -- Thanks FeralMeow.
  • +
  • Display Synchronize Settings for new installations when there are no proxies defined. Reported here.
  • +
  • Fixed: Import Settings bug especially painful for people using the example at AWS. + Reported here. Also fixed bug whereby spinner did not hide after import settings errors.
  • +
  • Minor security improvements -- do not accept embedded HTML/JS in other form fields
  • +
  • Turn Off (Use Firefox Settings) should be first in list. Reported here.
  • +
  • Fixed: Firefox overflow menu displays wrong info for FoxyProxy. Reported here.
  • +
+
+ +
+ +
+ + + + + diff --git a/src/images/ericjung.png b/src/images/ericjung.png new file mode 100644 index 0000000..36e0881 Binary files /dev/null and b/src/images/ericjung.png differ diff --git a/src/images/gray.svg b/src/images/gray.svg new file mode 100644 index 0000000..58e95c3 --- /dev/null +++ b/src/images/gray.svg @@ -0,0 +1,197 @@ +image/svg+xml \ No newline at end of file diff --git a/src/images/icon-off.svg b/src/images/icon-off.svg new file mode 100644 index 0000000..d655474 --- /dev/null +++ b/src/images/icon-off.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/src/images/icon.svg b/src/images/icon.svg new file mode 100644 index 0000000..17b1563 --- /dev/null +++ b/src/images/icon.svg @@ -0,0 +1,231 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/src/images/legacy-version.png b/src/images/legacy-version.png new file mode 100644 index 0000000..f58f65a Binary files /dev/null and b/src/images/legacy-version.png differ diff --git a/src/images/logo.svg b/src/images/logo.svg new file mode 100644 index 0000000..0239dcd --- /dev/null +++ b/src/images/logo.svg @@ -0,0 +1,316 @@ + + +image/svg+xml \ No newline at end of file diff --git a/src/import-proxy-list.html b/src/import-proxy-list.html new file mode 100644 index 0000000..dafef6e --- /dev/null +++ b/src/import-proxy-list.html @@ -0,0 +1,114 @@ + + + + + FoxyProxy + + + + + + + + +
+ + +
+ + +
+
+ + +
+
+

+



+
+
+
+

+ 78.205.12.1:6001
12.999.51.81:3128
foobar.com:12001
foobar.com:3128:kyleReese:hunterKiller +

+
+
+
+ / +
+ + +
+
+

+

+

* proxy://78.205.12.1:666
+ * proxy://johnConnor:hunterKiller@192.168.100.9:5192?color=663300
+ * socks://12.999.51.81?title=China&cc=CN&country=中国&proxyDns=false
+ * socks://12.999.51.81:331?patternExcludesIntranet=false
+ * https://192.168.100.9:5192?patternIncludesAll=false
+ * ssl://kyleReese:passw0rd@78.205.12.1:21?color=ff00bc&title=work%20proxy

+
+
+
+ +
+
+
+

+ + + + + +
+ +
+
+ +
+
+
+ +
+
+
+

+ + +
+ +
+
+ + + + + diff --git a/src/import.html b/src/import.html new file mode 100644 index 0000000..759ee48 --- /dev/null +++ b/src/import.html @@ -0,0 +1,96 @@ + + + + + FoxyProxy + + + + + + + + +
+ + +
+ + +
+ +
+ Import VPN/Proxies from FoxyProxy Purchase +

If you have a paid account with FoxyProxy, you can import your proxies here.

+ + + + + + + +
+ +
+ + +
+ Import Settings from FoxyProxy 6.0+ + +

FoxyProxy can use Firefox Sync to synchronize settings across different installations of Firefox. But if you don't use Firefox Sync or want to share your settings with friends, FoxyProxy settings. Then use this page to import those settings. By default, the file is called _YYYY-MM-DD.json.

+ + +

+ +
+ Import Settings from FoxyProxy 4.x and earlier + +

Firefox has completely changed the addon system since FoxyProxy 4.x and earlier. Older FoxyProxy versions used a pop-up window. It looked a little different on Windows, Mac, and Linux, but the essence was like the image below.

+ +

Unfortunately, due to Firefox 57+ limitations, we cannot read your FoxyProxy settings automatically. However, they are not lost. To import your old settings, please select the file foxyproxy.xml below. You can find it in the Firefox profile directory.

+ + +

+ +


+ (click image for larger picture)

+
+ + + +
+ +
+ +
+ + + + + diff --git a/src/log.html b/src/log.html new file mode 100644 index 0000000..d9212ab --- /dev/null +++ b/src/log.html @@ -0,0 +1,128 @@ + + + + + FoxyProxy + + + + + + + + +
+ + + +
+ + + +
+ +
+ + + + + + + +
+ +
Failed URLs display here, too!
+ A row in this log does not mean the URL successfully loaded. The log shows only how attempts were made to load URLs. If the proxy server was not responding or there was some other problem which prevented the URL from loading, the URL still displays here. This is a limitation of Firefox 57+. +
+ +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+ +

+
+ + + + + + + + + + + + + + +
+
+ +
+ + + +
+ +
+ + + + + diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..bec12f2 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,54 @@ +{ + "manifest_version": 2, + + "name": "__MSG_extensionName__", + "description": "__MSG_extensionDescription__", + "version": "7.5.1", + "default_locale": "en", + "homepage_url": "https://getfoxyproxy.org/", + "author": "Eric Jung", + + "icons": { + "16": "images/icon.svg", + "32": "images/icon.svg", + "48": "images/icon.svg", + "64": "images/icon.svg", + "128": "images/icon.svg" + }, + + "background": { + "scripts": ["scripts/utils.js", "scripts/background.js", "scripts/matcher.js"] + }, + + "options_ui": { + "page": "options.html", + "open_in_tab": true, + "browser_style": true + }, + + "browser_action": { + "default_icon": "images/icon.svg", + "default_title": "__MSG_extensionName__", + "default_popup": "popup.html", + "browser_style": true + }, + + "permissions": [ + "browsingData", + "proxy", + "storage", + "tabs", + "webRequest", + "webRequestBlocking", + "downloads", + "notifications", + "" + ], + + "browser_specific_settings": { + "gecko": { + "id": "foxyproxy@eric.h.jung", + "strict_min_version": "60.0" + } + } +} diff --git a/src/options.html b/src/options.html new file mode 100644 index 0000000..23057d0 --- /dev/null +++ b/src/options.html @@ -0,0 +1,140 @@ + + + + + FoxyProxy + + + + + + + + + +
+ + + +
+ + +
+ + + +
+
+ + + +
+ + +
+ + +
+ + + +
+
+ + +
+
+ + Please click to start. +
+ + +
+
+   + + + + +
+
+ + + + + + + +
+
+ +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/pattern-help.html b/src/pattern-help.html new file mode 100644 index 0000000..890d842 --- /dev/null +++ b/src/pattern-help.html @@ -0,0 +1,119 @@ + + + + + FoxyProxy Pattern Help + + + + + + + + +
Pattern Help
+ + +
+ +
+ +
+ + +
+
+
*
+
all domains
+
*.bbc.co.uk
+
exact domain and all subdomains
+
**.bbc.co.uk
+
subdomains only (not bbc.co.uk)
+
bbc.co.uk
+
exact domain only
+
+
+
+
Black patterns take precedence over white patterns. For example, a black pattern of * means nothing will match, regardless of any white patterns.
+
+
+
+ | + +
+ +

TL;DR

: show me the examples

+ +

What are they?

+

Patterns are a way to specify groups of URLs: a pattern matches a specific set of URLs. If a white pattern matches a URL the browser wants to load, the proxy for that white pattern is used to load the URL unless a black pattern also matches! Black patterns take precendence over white patterns and are always checked first. If both white and black patterns (in the same proxy) match a URL, that proxy is not used to load that URL.

+
Patterns are ignored unless FoxyProxy is set to Use Enabled Proxies By Patterns and Priority.
+ +

Ordering

+

Every URL is compared with the patterns for each proxy. The white/black patterns for the top-most (first) proxy are checked first, then the next set of white/black patterns are checked, and on down the list of proxies until there is a match. If there is no match, Firefox's native proxy settings are used to load the URL. Older versions of FoxyProxy had a Default proxy that acted as a catch-all to matches all URLs. You may still have Default in FoxyProxy if you upgraded from a previous version. +

You can re-order the proxies using the arrow buttons as you like, but the white/black patterns for the top-most (first) proxy are checked first.

+

If a black pattern matches, the proxy is not used for that URL even if a white pattern also matches. The black pattern takes priority. The URL may, however, load through another proxy you've defined if that proxy has a matching whitelist pattern and no matching blacklist pattern.

+

The order of white patterns and black patterns within a proxy do not matter.

+ +

Wildcards

+
+ Because of Firefox limitations, only URL domains, subdomains, and ports are recognized in patterns. Do not use paths or query parameters in patterns. Example: *.foxyproxy.com:30053 is OK but not *.foxyproxy.com:30053/help/* +
+ +

Examples

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PatternExample MatchesExample Non-Matches
*.foxyproxy.com
Match URLs at foxyproxy.com and all subdomains
foxyproxy.com/order.html
help.foxyproxy.com/index.html
foo.bar.foxyproxy.com
twostep.foxyproxy.com
mozilla.com
.foxyproxy.com
Match URLs at foxyproxy.com and all subdomains
(same as pattern above)
foxyproxy.com/order.html
help.foxyproxy.com/index.html
foo.bar.foxyproxy.com
twostep.foxyproxy.com
mozilla.com
**.foxyproxy.com
Match URLs only at subdomains of foxyproxy.com
help.foxyproxy.com
help.foxyproxy.com/index.html
foo.bar.foxyproxy.com
foxyproxy.com
foxyproxy.com
Match all URLs at foxyproxy.com but not subdomains
foxyproxy.com
foxyproxy.com/index.html
help.foxyproxy.com
*foo*
Match all URLs with a domain containing the letters foo
foo.com
foodle.com
one.befoo.org
bar.com
*foo*.comfoo.com
foodle.com
food.com
one.befoo.org
food.org
g?ogle.*
? matches any single character
google.com
grogle.org
google.com/maps
goog.com
.catsinsinks.com:8080
Port matching!
catsinsinks.com:8080
www.catsinsinks.com:8080
www.catsinsinks.com:8080/privacy
catinsinks.net
+ +

Notes

+

If a wildcard pattern begins with . or *. then it matches the main domain and all subdomains in a URL.

+

To match only subdomains, use ** instead of * in the beginning. Example: **.foxyproxy.com will match help.foxyproxy.com but will not match foxyproxy.com.

+ +
+ + + diff --git a/src/pattern-tester.html b/src/pattern-tester.html new file mode 100644 index 0000000..0757f5d --- /dev/null +++ b/src/pattern-tester.html @@ -0,0 +1,93 @@ + + + + + FoxyProxy + + + + + + + + +
+ + +
+ +
+ +
+ + +
+
+
*
+
all domains
+
*.bbc.co.uk
+
exact domain and all subdomains
+
**.bbc.co.uk
+
subdomains only (not bbc.co.uk)
+
bbc.co.uk
+
exact domain only
+
+
+
+
Black patterns take precedence over white patterns. For example, a black pattern of * means nothing will match, regardless of any white patterns.
+
+
+
+ | + +
+ +

Step 1

+ + + + +

Step 2

+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +

Step 3

+


+ + +
+
+ + + + + + diff --git a/src/patterns.html b/src/patterns.html new file mode 100644 index 0000000..0d3aa83 --- /dev/null +++ b/src/patterns.html @@ -0,0 +1,202 @@ + + + + + FoxyProxy + + + + + + + + +
+ + +
+ + +
+

+ +
+ + +
+ +
+
+ +
+
+
+ + +
+
+
*
+
all domains
+
*.bbc.co.uk
+
exact domain and all subdomains
+
**.bbc.co.uk
+
subdomains only (not bbc.co.uk)
+
bbc.co.uk
+
exact domain only
+
+
+
+
Black patterns take precedence over white patterns. For example, a black pattern of * means nothing will match, regardless of any white patterns.
+
+
+
+ | + + | + + + +
+
+ + +

+ +
+ + + + + + + + + + + + + + + + + + + + +
HTTP/s
+ + + + + + + + + + +
+
+ +
+ +
+ +
+ +

+
+ + + + + + + + + + +
HTTP/s
+
+ + +
+ +
+ +
+ +
+
+ + + +
+
+ + + + +
+
+ +

+ +
+ + + + + + + diff --git a/src/popup.html b/src/popup.html new file mode 100644 index 0000000..a049d6d --- /dev/null +++ b/src/popup.html @@ -0,0 +1,64 @@ + + + + + + + + + + +
FoxyProxy
+ + +
+ + + +
+ + +
    +
  • + + +
  • + +
  • +
+
+ + + +
+
+ + + + + \ No newline at end of file diff --git a/src/proxy.html b/src/proxy.html new file mode 100644 index 0000000..ed77bec --- /dev/null +++ b/src/proxy.html @@ -0,0 +1,193 @@ + + + + + FoxyProxy + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + +
+
+ + + + + +
+ + +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + +
+ +
+
+
+
+
+ + +
+
+ + + + +
+ + + + + + + + + + + + +
+
+ + +
+ + +
+ + +
+

Not supported

+

Due to Firefox limitation, PAC, WPAD, and System Settings are currently not supported, but they are due to be added.

+
+
+
+ +
+ + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/src/scripts/about.js b/src/scripts/about.js new file mode 100644 index 0000000..872a829 --- /dev/null +++ b/src/scripts/about.js @@ -0,0 +1,26 @@ +'use strict'; + +// ----------------- Internationalization ------------------ +document.querySelectorAll('[data-i18n]').forEach(node => { + let [text, attr] = node.dataset.i18n.split('|'); + text = chrome.i18n.getMessage(text); + attr ? node[attr] = text : node.appendChild(document.createTextNode(text)); +}); +// ----------------- /Internationalization ----------------- + +document.addEventListener('keyup', evt => { + if (evt.keyCode === 27) { + location.href = '/options.html'; + } +}); + +const manifest = chrome.runtime.getManifest(); +document.querySelector('#version').textContent = manifest.version; +document.querySelector('#edition').textContent = 'FoxyProxy ' + (FOXYPROXY_BASIC ? 'Basic' : 'Standard'); +document.querySelector('button').addEventListener('click', () => location.href = '/options.html'); + +// --- remove nodes completely for FP Basic +FOXYPROXY_BASIC && document.querySelectorAll('.notForBasic').forEach(item => item.remove()); + +// --- welcome on install/update +location.search === '?welcome' && document.querySelector('.welcome').classList.remove('hide'); diff --git a/src/scripts/background.js b/src/scripts/background.js new file mode 100644 index 0000000..a60546b --- /dev/null +++ b/src/scripts/background.js @@ -0,0 +1,275 @@ +'use strict'; + +// ----- global +//const FF = typeof browser !== 'undefined'; // for later +let storageArea; // keeping track of sync +let bgDisable = false; + +// Start in disabled mode because it's going to take time to load setings from storage +let activeSettings = {mode: 'disabled'}; + +// ----------------- logger -------------------------------- +let logger; +function getLog() { return logger; } +class Logger { + + constructor(size = 100, active = false) { + this.size = size; + this.matchedList = []; + this.unmatchedList = []; + this.active = active; + } + + clear() { + this.matchedList = []; + this.unmatchedList = []; + } + + addMatched(item) { + this.matchedList.push(item); + this.matchedList = this.matchedList.slice(-this.size); // slice to the ending size entries + } + + addUnmatched(item) { + this.unmatchedList.push(item); + this.unmatchedList = this.unmatchedList.slice(-this.size); // slice to the ending size entries + } + + updateStorage() { + this.matchedList = this.matchedList.slice(-this.size); // slice to the ending size entries + this.unmatchedList = this.unmatchedList.slice(-this.size); // slice to the ending size entries + storageArea.set({logging: {size: this.size, active: this.active} }); + } +} +// ----------------- /logger ------------------------------- + +// --- registering persistent listener +// https://bugzilla.mozilla.org/show_bug.cgi?id=1359693 ...Resolution: --- ? WONTFIX +chrome.webRequest.onAuthRequired.addListener(sendAuth, {urls: ['*://*/*']}, ['blocking']); +chrome.webRequest.onCompleted.addListener(clearPending, {urls: ['*://*/*']}); +chrome.webRequest.onErrorOccurred.addListener(clearPending, {urls: ['*://*/*']}); + +chrome.runtime.onInstalled.addListener((details) => { // Installs Update Listener + // reason: install | update | browser_update | shared_module_update + switch (true) { + + case details.reason === 'install': + case details.reason === 'update' && /^(3\.|4\.|5\.5|5\.6)/.test(details.previousVersion): + chrome.tabs.create({url: '/about.html?welcome'}); + break; + } +}); + +// ----------------- User Preference ----------------------- +chrome.storage.local.get(null, result => { + // browserVersion is not used & runtime.getBrowserInfo() is not supported on Chrome + // sync is NOT set or it is false, use this result ELSE get it from storage.sync + // check both storage on start-up + if (!Object.keys(result)[0]) { // local is empty, check sync + + chrome.storage.sync.get(null, syncResult => { + if (!Object.keys(syncResult)[0]) { // sync is also empty + storageArea = chrome.storage.local; // set storage as local + process(result); + } + else { + chrome.storage.local.set({sync: true}); // save sync as true + storageArea = chrome.storage.sync; // set storage as sync + process(syncResult); + } + }); + } + else { + storageArea = result.sync ? chrome.storage.sync : chrome.storage.local; // cache for subsequent use + !result.sync ? process(result) : chrome.storage.sync.get(null, process); + } +}); +// ----------------- /User Preference ---------------------- + +function process(settings) { + + let update; + let prefKeys = Object.keys(settings); + + if (!settings || !prefKeys[0]) { // create default settings if there are no settings + // default + settings = { + mode: 'disabled', + logging: { + size: 100, + active: false + } + }; + update = true; + } + + // update storage then add Change Listener + if (update) { + storageArea.set(settings, () => chrome.storage.onChanged.addListener(storageOnChanged)); + } + else { + chrome.storage.onChanged.addListener(storageOnChanged); + } + + logger = settings.logging ? new Logger(settings.logging.size, settings.logging.active) : new Logger(); + setActiveSettings(settings); + console.log('background.js: loaded proxy settings from storage.'); +} + +function storageOnChanged(changes, area) { +// console.log(changes); + // update storageArea on sync on/off change from options + if (changes.hasOwnProperty('sync') && changes.sync.newValue !== changes.sync.oldValue) { + storageArea = changes.sync.newValue ? chrome.storage.sync : chrome.storage.local; + } + + // update logger from log + if (Object.keys(changes).length === 1 && changes.logging) { return; } + + + // mode change from bg + if(changes.mode && changes.mode.newValue === 'disabled' && bgDisable) { + bgDisable = false; + return; + } + + // default: changes from popup | options + storageArea.get(null, setActiveSettings); +} + +function proxyRequest(requestInfo) { + return findProxyMatch(requestInfo.url, activeSettings); +} + +function setActiveSettings(settings) { + browser.proxy.onRequest.hasListener(proxyRequest) && browser.proxy.onRequest.removeListener(proxyRequest); + + const pref = settings; + const prefKeys = Object.keys(pref).filter(item => !['mode', 'logging', 'sync'].includes(item)); // not for these + + // --- cache credentials in authData (only those with user/pass) + prefKeys.forEach(id => pref[id].username && pref[id].password && + (authData[pref[id].address] = {username: pref[id].username, password: pref[id].password}) ); + + const mode = settings.mode; + activeSettings = { // global + mode, + proxySettings: [] + }; + + if (mode === 'disabled' || (FOXYPROXY_BASIC && mode === 'patterns')){ + setDisabled(); + return; + } + + if (['patterns', 'random', 'roundrobin'].includes(mode)) { // we only support 'patterns' ATM + + // filter out the inactive proxy settings + prefKeys.forEach(id => pref[id].active && activeSettings.proxySettings.push(pref[id])); + activeSettings.proxySettings.sort((a, b) => a.index - b.index); // sort by index + + function processPatternObjects(patternObjects) { + return patternObjects.reduce((accumulator, patternObject) => { + patternObject = Utils.processPatternObject(patternObject); + patternObject && accumulator.push(patternObject); + return accumulator; + }, []); + } + + // Filter out the inactive patterns. that way, each comparison + // is a little faster (doesn't even know about inactive patterns). Also convert all patterns to reg exps. + for (const idx in activeSettings.proxySettings) { + activeSettings.proxySettings[idx].blackPatterns = processPatternObjects(activeSettings.proxySettings[idx].blackPatterns); + activeSettings.proxySettings[idx].whitePatterns = processPatternObjects(activeSettings.proxySettings[idx].whitePatterns); + } + browser.proxy.onRequest.addListener(proxyRequest, {urls: [""]}); + Utils.updateIcon('images/icon.svg', null, 'patterns', true); + console.log(activeSettings, "activeSettings in patterns mode"); + } + else { + // User has selected a proxy for all URLs (not patterns, disabled, random, round-robin modes). + // mode is set to the proxySettings id to use for all URLs. + if (settings[mode]) { + activeSettings.proxySettings = [settings[mode]]; + browser.proxy.onRequest.addListener(proxyRequest, {urls: [""]}); + const tmp = Utils.getProxyTitle(settings[mode]); + Utils.updateIcon('images/icon.svg', settings[mode].color, tmp, false, tmp, false); + console.log(activeSettings, "activeSettings in fixed mode"); + } + else { + // This happens if user deletes the current proxy and mode is "use this proxy for all URLs" + // Don't remove this block. + bgDisable = true; + storageArea.set({mode: 'disabled'}); // only in case of error, otherwise mode is already set + setDisabled(); + console.error(`Error: mode is set to ${mode} but no active proxySetting is found with that id. Disabling Due To Error`); + } + } +} + + +function setDisabled(isError) { + browser.proxy.onRequest.hasListener(proxyRequest) && browser.proxy.onRequest.removeListener(proxyRequest); + chrome.runtime.sendMessage({mode: 'disabled'}); // Update the options.html UI if it's open + Utils.updateIcon('images/icon-off.svg', null, 'disabled', true); + console.log('******* disabled mode'); +} + + +// ----------------- Proxy Authentication ------------------ +// ----- session global +let authData = {}; +let authPending = {}; + +async function sendAuth(request) { + // Do nothing if this not proxy auth request: + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired + // "Take no action: the listener can do nothing, just observing the request. If this happens, it will + // have no effect on the handling of the request, and the browser will probably just ask the user to log in." + if (!request.isProxy) return; + + // --- already sent once and pending + if (authPending[request.requestId]) { return {cancel: true}; } + + // --- authData credentials not yet populated from storage + if(!Object.keys(authData)[0]) { await getAuth(request); } + + // --- first authentication + // According to https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired : + // "request.challenger.host is the requested host instead of the proxy requesting the authentication" + // But in my tests (Fx 69.0.1 MacOS), it is indeed the proxy requesting the authentication + // TODO: test in future Fx releases to see if that changes. + // console.log(request.challenger.host, "challenger host"); + if (authData[request.challenger.host]) { + authPending[request.requestId] = 1; // prevent bad authentication loop + return {authCredentials: authData[request.challenger.host]}; + } + // --- no user/pass set for the challenger.host, leave the authentication to the browser +} + +async function getAuth(request) { + + await new Promise(resolve => { + chrome.storage.local.get(null, result => { + const host = result.hostData[request.challenger.host]; + if (host && host.username) { // cache credentials in authData + authData[host] = {username: host.username, password: host.password}; + } + resolve(); + }); + }); +} + +function clearPending(request) { + + if(!authPending[request.requestId]) { return; } + + if (request.error) { + const host = request.proxyInfo && request.proxyInfo.host ? request.proxyInfo.host : request.ip; + Utils.notify(chrome.i18n.getMessage('authError', host)); + console.error(request.error); + return; // auth will be sent again + } + + delete authPending[request.requestId]; // no error +} \ No newline at end of file diff --git a/src/scripts/common.js b/src/scripts/common.js new file mode 100644 index 0000000..bd516c8 --- /dev/null +++ b/src/scripts/common.js @@ -0,0 +1,63 @@ +'use strict'; + +// ----------------- Pattern Check ------------------ + +function checkPattern(pattern, type) { + + const pat = pattern.value; + + if (!pat) { + pattern.classList.add('invalid'); + pattern.focus(); + showResult(chrome.i18n.getMessage('errorEmpty'), true); + return; + } + + const patternTypeSet = { + '1': 'wildcard', + '2': 'regex' + } + + let regex; + + switch (patternTypeSet[type.value]) { + + // RegEx + case 'regex': + try { regex = new RegExp(pat); } + catch (e) { + pattern.classList.add('invalid'); + showResult(e.message, true); + return false; + } + break; + + // wildcard + default: + if (pat.includes('/')) { + pattern.classList.add('invalid'); + showResult(chrome.i18n.getMessage('errorSlash'), true); + return false; + } + + try { regex = new RegExp(Utils.wildcardToRegExp(pat)); } + catch (e) { + pattern.classList.add('invalid'); + showResult(e.message, true); + return false; + } + } + + // --- pattern is valid + return regex; +} + + + + +function showResult(text, fail) { + + fail && result.classList.add('alert'); + result.textContent = text; + result.classList.remove('hide'); +} \ No newline at end of file diff --git a/src/scripts/import-proxy-list.js b/src/scripts/import-proxy-list.js new file mode 100644 index 0000000..a8bd838 --- /dev/null +++ b/src/scripts/import-proxy-list.js @@ -0,0 +1,240 @@ +'use strict'; + +// ----------------- Internationalization ------------------ +document.querySelectorAll('[data-i18n]').forEach(node => { + let [text, attr] = node.dataset.i18n.split('|'); + text = chrome.i18n.getMessage(text); + attr ? node[attr] = text : node.appendChild(document.createTextNode(text)); +}); +// ----------------- /Internationalization ----------------- + +document.addEventListener('keyup', evt => { + if (evt.keyCode === 27) { + location.href = '/options.html'; + } +}); + +// ----------------- Spinner ------------------------------- +const spinner = document.querySelector('.spinner'); +function hideSpinner() { + + spinner.classList.remove('on'); + setTimeout(() => { spinner.style.display = 'none'; }, 600); +} + +function showSpinner() { + + spinner.style.display = 'flex'; + spinner.classList.add('on'); +} +// ----------------- /spinner ------------------------------ +document.addEventListener('DOMContentLoaded', () => { + hideSpinner(); +}); + +// addEventListener for all buttons & handle together +document.querySelectorAll('button').forEach(item => item.addEventListener('click', process)); + +let proxiesAdded = 0; // Global to this module in case user does multiple bulk imports before closing import-bulk.html + +function process(e) { + switch (this.id || this.dataset.i18n) { + case 'back': location.href = '/options.html'; break; + case 'import': imp0rt(); break; + } +} + +function imp0rt() { + const {parsedList, skippedList} = parseList(document.getElementById('proxyList').value); + if (parsedList.length > 0) { + if (document.querySelector('#overwrite').checked) { + if (confirm(chrome.i18n.getMessage('confirmOverwrite'))) { + showSpinner(); + chrome.storage.local.clear(() => chrome.storage.sync.clear(() => { + hideSpinner(); + storeProxies(parsedList); + })); + } + else { + return; + } + } + else { + storeProxies(parsedList); + } + } + if (skippedList.length > 0) { + alert(`${chrome.i18n.getMessage('importsSkipped', [skippedList.length + "", skippedList.toString()])}`); + } + if (parsedList.length > 0) { + alert(`${chrome.i18n.getMessage('importSucceeded', [parsedList.length])}`); + } + location.href = '/options.html'; +} + +function parseList(rawList) { + const parsedList = [], skippedList = [], colors = ['#663300', '#284B63', '#C99656', '#7B758C', '#171E1D']; + if (!rawList) { + return {parsedList, skippedList}; + } + rawList.split('\n').forEach((item) => { + if (!item) { + return; // continue to next + } + let p, patternIncludesAll = true, patternExcludesIntranet = true; + // Is this line simple or complete format? + let protocol = item.match(/.+:\/\//); // null for strings like 127.0.0.1:3128 (simple format) + if (protocol) { + // This line is uses 'complete' format + let url; + try { + // In Firefox 78.0.2, the built-in javascript URL class will not parse URLs with custom schemes/protocols + // like socks://127.0.0.1. However, Chrome 84.0.4147.89 and Node 14.5.0 both do. In order to be compatible + // with Firefox, let's replace the scheme/protocol with 'http'. We could also instead write our own parsing + // logic with a regular expression, but that does not seems necessary. + if (protocol[0] !== 'http://' && protocol[0] !== 'https://') { + item = 'http://' + item.substring(protocol[0].length); + url = new URL(item); + protocol = protocol[0].substring(0, protocol[0].length-2); //strip ending // + } + else { + url = new URL(item); + protocol = url.protocol; + } + } + catch (e) { + console.log(e); + // URL couldn't be parsed + skippedList.push(item); + return; // continue to next + } + const type = protocol === 'proxy:' || protocol === 'http:' ? PROXY_TYPE_HTTP : + protocol === 'ssl:' || protocol === 'https:' ? PROXY_TYPE_HTTPS : + protocol === 'socks:' || protocol === 'socks5:' ? PROXY_TYPE_SOCKS5 : + protocol === 'socks4:' ? PROXY_TYPE_SOCKS4 : -1; + if (type === -1) { + console.log("unknown protocol"); + skippedList.push(item); + return; // continue to next + } + + // If color not specified in the URL, then rotate among the ones in the colors array. + const color = url.searchParams.get('color') ? + ('#' + url.searchParams.get('color')) : colors[parsedList.length % colors.length]; + + const title = url.searchParams.get('title'); + const countryCode = url.searchParams.get('countryCode') || url.searchParams.get('cc'); + const country = url.searchParams.get('country') || countryCode; + + // If paramName url param is not specified or it's specified and not 'false', then paramValue should equal true. + // We assume true in case the param is absent, which may be counterintuitive, but this fcn is used for params that + // we want to assume true when absent. + function parseBooleanParam(url, paramName, aliasParamName) { + const paramValue = url.searchParams.get(paramName) || (aliasParamName && url.searchParams.get(aliasParamName)); + return paramValue ? !(paramValue.toLowerCase() === 'false') : true; + } + const proxyDNS = parseBooleanParam(url, 'proxyDns'); + const active = parseBooleanParam(url, 'enabled', 'active'); + + patternIncludesAll = parseBooleanParam(url, 'patternIncludesAll'); + patternExcludesIntranet = parseBooleanParam(url, 'patternExcludesIntranet'); + + // the URL class sets port === '' if not specified on the URL or it's an invalid port e.g. contains alpha chars + let port = url.port; + if (port === '') { + // Default ports are 3128 for HTTP proxy, 443 for tls/ssl/https proxy, 1080 for socks4/5 + port = type === PROXY_TYPE_HTTP ? 3128 : type === PROXY_TYPE_HTTPS ? 443 : 1080; + } + + console.log(url); + // the URL class sets username and password === '' if not specified on the URL + p = {type, username: url.username, password: url.password, address: url.hostname, port, color, title, proxyDNS, active, countryCode, country}; + } + else { + // simple + const splitItem = item.split(':'); + // Split always returns an array no matter what + p = {address: splitItem[0], port: splitItem[1], username: splitItem[2], password: splitItem[3], color: colors[parsedList.length % colors.length]}; + } + + const proxy = makeProxy(p, patternIncludesAll, patternExcludesIntranet); + if (proxy) { + parsedList.push(proxy); + } + else { + skippedList.push(item); + } + + }); //forEach + + return {parsedList, skippedList}; +} + +function makeProxy({type = PROXY_TYPE_HTTP, username, password, address, port, color, title, proxyDNS, active = true, countryCode, country}, + patternIncludesAll, patternExcludesIntranet) { + + port = port*1; // convert to digit + if (!port || port < 1) { // is port NaN or less than 1 + console.log("port is NaN or less than 1"); + return null; + } + + // strip bad chars from all input except username, password, type, proxyDNS, and active + // (those last 3 are forced to boolean types before we are called) + // If we do strip bad chars from usernams or password, auth could fail. + address = Utils.stripBadChars(address); + color = Utils.stripBadChars(color); + title = Utils.stripBadChars(title); + countryCode = Utils.stripBadChars(countryCode); + country = Utils.stripBadChars(country); + + if (!address) { + console.log("no address"); + return null; + } + + const proxy = {type, address, port, color, active}; + + // Only set the properties needed. null and undefined props seem to be saved if set, so don't set them. + function setPropertyIfHasValue(prop, value, proxy) { + if (value || value === 0) { + proxy[prop] = value; + } + } + setPropertyIfHasValue('username', username, proxy); + setPropertyIfHasValue('password', password, proxy); + setPropertyIfHasValue('title', title, proxy); + setPropertyIfHasValue('cc', countryCode, proxy); + setPropertyIfHasValue('country', country, proxy); + + if (type === PROXY_TYPE_SOCKS5) { + // Only set if socks5 + proxy.proxyDNS = proxyDNS; + } + + if (FOXYPROXY_BASIC) { + proxy.whitePatterns = proxy.blackPatterns = []; + } + else { + proxy.whitePatterns = patternIncludesAll ? [PATTERN_ALL_WHITE] : []; + proxy.blackPatterns = patternExcludesIntranet ? [...blacklistSet] : []; + } + return proxy; +} + +function storeProxies(parsedList) { + const sync = localStorage.getItem('sync') === 'true'; + const storageArea = !sync ? chrome.storage.local : chrome.storage.sync; + + for (const idx in parsedList) { + const proxy = parsedList[idx]; + console.log(proxy); + // Get the nextIndex given to us by options.js and add by the number of proxies we've added. + // This ensures this proxy setting is last in list of all proxy settings. + + proxy.index = (localStorage.getItem('nextIndex')) + (++proxiesAdded); + storageArea.set({[Utils.getUniqueId()]: proxy}, () => { + console.log(`stored proxy`); + }); + } +} diff --git a/src/scripts/import.js b/src/scripts/import.js new file mode 100644 index 0000000..17ec6f5 --- /dev/null +++ b/src/scripts/import.js @@ -0,0 +1,440 @@ +'use strict'; + +// ----------------- Internationalization ------------------ +document.querySelectorAll('[data-i18n]').forEach(node => { + let [text, attr] = node.dataset.i18n.split('|'); + text = chrome.i18n.getMessage(text); + attr ? node[attr] = text : node.appendChild(document.createTextNode(text)); +}); +// ----------------- /Internationalization ----------------- +document.addEventListener('keyup', evt => { + if (evt.keyCode === 27) { + close(); + } +}); + +// ----------------- Spinner ------------------------------- +const spinner = document.querySelector('.spinner'); +function hideSpinner() { + + spinner.classList.remove('on'); + setTimeout(() => { spinner.style.display = 'none'; }, 600); +} + +function showSpinner() { + + spinner.style.display = 'flex'; + spinner.classList.add('on'); +} +// ----------------- /spinner ------------------------------ +hideSpinner(); + +// addEventListener for all buttons & handle together +document.querySelectorAll('button').forEach(item => item.addEventListener('click', process)); +document.querySelectorAll('input[type="file"]').forEach(item => item.addEventListener('change', process)); + +function process(e) { + + switch (this.id || this.dataset.i18n) { + // click + case 'back': close(); break; + case 'export': Utils.exportFile(); break; + + case 'togglePW|title': + const inp = this.previousElementSibling; + inp.type = inp.type === 'password' ? 'text' : 'password'; + break; + + // change + case 'importFP': + showSpinner(); + foxyProxyImport(); + break; + + case 'importJson': + showSpinner(); + Utils.importFile(e.target.files[0], ['application/json'], 1024*1024*10, 'json', importJson); // 10mb + hideSpinner(); // hide spinner in case importJson() was not called due to error + break; + case 'importXml': + showSpinner(); + Utils.importFile(e.target.files[0], ['text/xml'], 1024*1024*10, 'xml', importXml); // 10mb + hideSpinner(); // hide spinner in case importXml() was not called due to error + break; + } +} + +function importJson(result) { + + if (!result) { // user cancelled + hideSpinner(); + return; + } + + // --- convert pre v7.0 export to db format + if (result.hasOwnProperty('proxySettings')) { + result = prepareForStorage(result); + } + + save(result, end); +} + +function save(result, callback) { + + // Remove 'browserVersion', 'foxyProxyVersion', 'foxyProxyEdition' if they exist + // We don't need those imported. + delete result.browserVersion; + delete result.foxyProxyVersion; + delete result.foxyProxyEdition; + + const storageArea = result.sync ? chrome.storage.sync : chrome.storage.local; + + // clear the storages and set new + chrome.storage.local.clear(() => chrome.storage.sync.clear(() => { + + if (result.sync) { + chrome.storage.local.set({sync: true}); // save sync state + delete result.sync; + } + + storageArea.set(result, callback); // save to target + })); +} + + +function end() { + hideSpinner(); + Utils.notify(chrome.i18n.getMessage('importEnd')); + location.href = '/options.html'; +} + + +function importXml(doc) { + + let lastResortFound = false; + // base format + const pref = { + mode: 'disabled', + logging: { + size: 100, + active: false + } + }; + + const FP = doc.querySelector('foxyproxy'); + if (!FP) { + // Don't use Utils.notify() because at least on macOS, + // the message is too long and cut off + alert('There is an error with the XML file (missing )'); + hideSpinner(); + return; + } + + const mode = FP.getAttribute('mode'); + mode && (pref.mode = mode); + + const badModes = []; + + const proxies = doc.getElementsByTagName('proxy'); + let patternsEdited = false; + + const LASTRESORT = 'k20d21508277536715'; + const DEFAULT_PROXY_SETTING = { + index: Number.MAX_SAFE_INTEGER, + id: LASTRESORT, + active: true, + title: 'Default', + notes: 'These are the settings that are used when no patterns match a URL.', + color: '#0055E5', + type: PROXY_TYPE_NONE, + whitePatterns: [PATTERN_ALL_WHITE], + blackPatterns: [] + }; + + doc.querySelectorAll('proxy').forEach((item, index) => { + + const proxy = {}; + // type a.k.a. mode + const oldType = item.getAttribute('mode'); + // Deactivate from patterns mode any unsupported types/modes + const allowedType = ['manual', 'direct'].includes(oldType); + proxy.active = allowedType ? item.getAttribute('enabled') === 'true' : false; + // switch is faster than a series of if/else + switch (oldType) { + + case 'system': + badModes.push(item); + proxy.type = PROXY_TYPE_SYSTEM; + break; + + case 'auto': + badModes.push(item); + if (item.getAttribute('autoconfMode') === 'pac') { // PAC + proxy.type = PROXY_TYPE_PAC; + proxy.pacURL = item.querySelector('autoconf').getAttribute('url'); + } + else { // WPAD + proxy.type = PROXY_TYPE_WPAD; + proxy.pacURL = 'http://wpad/wpad.dat'; + } + break; + + case 'direct': + proxy.type = PROXY_TYPE_NONE; + break; + + case 'manual': + const manualconf = item.querySelector('manualconf'); + proxy.address = manualconf.getAttribute('host'); + proxy.port = parseInt(manualconf.getAttribute('port')); + proxy.username = manualconf.getAttribute('username'); + proxy.password = manualconf.getAttribute('password'); + // There appears to be a bug in 4.6.5 and possibly earlier versions: socksversion is always 5, never 4 + if (manualconf.getAttribute('isSocks') === 'true') { + proxy.type = PROXY_TYPE_SOCKS5; + if (item.getAttribute('proxyDNS') === 'true') { proxy.proxyDNS = true; } + } + else if (manualconf.getAttribute('isHttps') === 'true') { proxy.type = PROXY_TYPE_HTTPS; } + else { proxy.type = PROXY_TYPE_HTTP; } + break; + } + + proxy.title = item.getAttribute('name'); + proxy.color = item.getAttribute('color'); + + let newId; + const oldId = item.getAttribute('id'); + if (item.getAttribute('lastresort') === 'true') { + lastResortFound = true; + newId = LASTRESORT; // this is a string + proxy.index = Number.MAX_SAFE_INTEGER; + if (!allowedType) { proxy.type = PROXY_TYPE_NONE; } + } + else { + proxy.index = index; + newId = 'import-' + oldId; + } + + if (pref.mode === oldId) { + // If the old top-level mode points to a proxy setting with an unsupported mode (e.g. WPAD), + // we have to change the new top-level mode otherwise nothing will work w/o user intervention + pref.mode = !allowedType ? PROXY_TYPE_NONE : newId; // Update mode to the new id ("import-" prefix) + } + proxy.whitePatterns = []; + proxy.blackPatterns = []; + + item.querySelectorAll('match').forEach(mtch => { + + const newPattern = {}; + /* + "whitePatterns": [ + { + "title": "all URLs", + "active": true, + "pattern": "*", + "type": 1, + "protocols": 1 + } + ] + + */ + newPattern.title = mtch.getAttribute('name'); + newPattern.active = mtch.getAttribute('enabled') === 'true'; + newPattern.importedPattern = newPattern.pattern = mtch.getAttribute('pattern'); + newPattern.type = mtch.getAttribute('isRegEx') === 'true' ? PATTERN_TYPE_REGEXP : PATTERN_TYPE_WILDCARD; + // Do some simple parsing but only for wildcards. Anything else is going to fail. + if (newPattern.type === PATTERN_TYPE_WILDCARD) { + + switch (true) { + + case newPattern.pattern.startsWith('http://'): + newPattern.protocols = PROTOCOL_HTTP; + newPattern.pattern = newPattern.pattern.substring(7); + break; + + case newPattern.pattern.startsWith('https://'): + newPattern.protocols = PROTOCOL_HTTPS; + newPattern.pattern = newPattern.pattern.substring(8); + break; + + case newPattern.pattern.startsWith('*://'): + newPattern.protocols = PROTOCOL_ALL; + newPattern.pattern = newPattern.pattern.substring(4); + break; + + default: + newPattern.protocols = PROTOCOL_ALL; + } + + // Clip everything after slashes; it can't be used anymore: https://bugzilla.mozilla.org/show_bug.cgi?id=1337001 + const idx = newPattern.pattern.indexOf('/'); + if (idx > -1) { + newPattern.pattern = newPattern.pattern.substring(0, idx); + patternsEdited = true; + } + } + else { // e.g. ^https?://(?:[^:@/]+(?::[^@/]+)?@)?(?:localhost|127\.\d+\.\d+\.\d+)(?::\d+)?(?:/.*)?$ + + switch (true) { + + case newPattern.pattern.indexOf('^http://') === 1: + newPattern.protocols = PROTOCOL_HTTP; + newPattern.pattern = '^' + newPattern.pattern.substring(8); + break; + + case newPattern.pattern.indexOf('^https://') === 1: + newPattern.protocols = PROTOCOL_HTTPS; + newPattern.pattern = '^' + newPattern.pattern.substring(9); + break; + + case newPattern.pattern.indexOf('^https?://') === 1: + newPattern.protocols = PROTOCOL_ALL; + newPattern.pattern = '^' + newPattern.pattern.substring(10); + break; + + default: + newPattern.protocols = PROTOCOL_ALL; + } + } + + mtch.getAttribute('isBlackList') === 'true' ? proxy.blackPatterns.push(newPattern) : proxy.whitePatterns.push(newPattern); + }); + + pref[newId] = proxy; + }); + + if (!lastResortFound) { pref[LASTRESORT] = DEFAULT_PROXY_SETTING; } + + save(pref, () => endXML(patternsEdited)); +} + +function endXML(patternsEdited) { + + hideSpinner(); + if (patternsEdited) { + // Don't use Utils.notify() because at least on macOS, + // the message is too long and cut off + alert(chrome.i18n.getMessage('patternsChanged')); + location.href = '/options.html'; + } + else { + // Don't use Utils.notify() because at least on macOS, + // the message is too long and cut off + alert(chrome.i18n.getMessage('importEndSlash')); + location.href = '/options.html'; + } +} + + +function prepareForStorage(settings) { + + if (!settings.hasOwnProperty('proxySettings') || !settings.proxySettings[0]) { + alert('Imported file doesn not have any proxies.'); + return null; + } + + // base format + const ret = { + mode: 'disabled', + logging: { + size: 100, + active: false + } + }; + + settings.mode && (ret.mode = settings.mode); + settings.logging && (ret.logging = settings.logging); + + let idx = 0; + settings.proxySettings.forEach(item => { + + const id = item.id; + item.index = idx++; + delete item.id; // Don't need id + ret[id] = item; + }); + + return ret; +} + + +// ----------------- FoxyProxy Import ---------------------- +function foxyProxyImport() { + + // --- check user/pass + const username = document.querySelector('#username').value.trim(); + const password = document.querySelector('#password').value.trim(); + if (!username || !password) { + hideSpinner(); + alert(chrome.i18n.getMessage('errorUserPass')); + return; + } + + // --- generate the form post data + const usernamePassword = { 'username': username, 'password': password }; + const formBody = []; + for (const property in usernamePassword) { + const encodedKey = encodeURIComponent(property); + const encodedValue = encodeURIComponent(usernamePassword[property]); + formBody.push(encodedKey + "=" + encodedValue); + } + + // --- fetch data + fetch('https://getfoxyproxy.org/webservices/get-accounts.php', + { method: 'POST', + body: formBody.join("&"), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + } + }) + .then(response => response.json()) + .then(response => { + if (!Array.isArray(response) || !response[0] || !response[0].hostname) { + hideSpinner(); + Utils.notify(chrome.i18n.getMessage('errorFetch')); + return; + } + + const sync = localStorage.getItem('sync') === 'true'; + const storageArea = !sync ? chrome.storage.local : chrome.storage.sync; + storageArea.get(null, result => { + + response.forEach(item => { + const hostname = item.hostname.substring(0, item.hostname.indexOf('.getfoxyproxy.org')); + + if (hostname && item.ipaddress && item.port && item.port[0] && item.country_code && item.country) { + + // --- creating proxy + result[Math.random().toString(36).substring(7) + new Date().getTime()] = { + index: -1, + active: item.active, + title: hostname, + color: '#ff9900', + type: 1, // HTTP + address: item.ipaddress, + port: item.port[0], + username: item.username, + password: item.password, + cc: item.country_code, + country: item.country, + whitePatterns: [], + blackPatterns: [] + }; + } + }); + + storageArea.set(result, end); // save to target + }); + }) + .catch(error => { + hideSpinner(); + Utils.notify(chrome.i18n.getMessage('errorFetch')); + }); + +} +// ----------------- /FoxyProxy Import --------------------- + +function close() { + document.querySelector('#password').value = ''; /* prevent Firefox's save password prompt */ + location.href = '/options.html'; +} \ No newline at end of file diff --git a/src/scripts/jscolor-2.0.5.js b/src/scripts/jscolor-2.0.5.js new file mode 100644 index 0000000..5c77177 --- /dev/null +++ b/src/scripts/jscolor-2.0.5.js @@ -0,0 +1,1855 @@ +/** + * jscolor - JavaScript Color Picker + * + * @link http://jscolor.com + * @license For open source use: GPLv3 + * For commercial use: JSColor Commercial License + * @author Jan Odvarko + * @version 2.0.5 + * + * See usage examples at http://jscolor.com/examples/ + */ + + +"use strict"; + + +if (!window.jscolor) { window.jscolor = (function () { + + +var jsc = { + + + register : function () { + jsc.attachDOMReadyEvent(jsc.init); + jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown); + jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart); + jsc.attachEvent(window, 'resize', jsc.onWindowResize); + }, + + + init : function () { + if (jsc.jscolor.lookupClass) { + jsc.jscolor.installByClassName(jsc.jscolor.lookupClass); + } + }, + + + tryInstallOnElements : function (elms, className) { + var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i'); + + for (var i = 0; i < elms.length; i += 1) { + if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') { + if (jsc.isColorAttrSupported) { + // skip inputs of type 'color' if supported by the browser + continue; + } + } + var m; + if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) { + var targetElm = elms[i]; + var optsStr = null; + + var dataOptions = jsc.getDataAttr(targetElm, 'jscolor'); + if (dataOptions !== null) { + optsStr = dataOptions; + } else if (m[4]) { + optsStr = m[4]; + } + + var opts = {}; + if (optsStr) { + try { + opts = (new Function ('return (' + optsStr + ')'))(); + } catch(eParseError) { + jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr); + } + } + targetElm.jscolor = new jsc.jscolor(targetElm, opts); + } + } + }, + + + isColorAttrSupported : (function () { + var elm = document.createElement('input'); + if (elm.setAttribute) { + elm.setAttribute('type', 'color'); + if (elm.type.toLowerCase() == 'color') { + return true; + } + } + return false; + })(), + + + isCanvasSupported : (function () { + var elm = document.createElement('canvas'); + return !!(elm.getContext && elm.getContext('2d')); + })(), + + + fetchElement : function (mixed) { + return typeof mixed === 'string' ? document.getElementById(mixed) : mixed; + }, + + + isElementType : function (elm, type) { + return elm.nodeName.toLowerCase() === type.toLowerCase(); + }, + + + getDataAttr : function (el, name) { + var attrName = 'data-' + name; + var attrValue = el.getAttribute(attrName); + if (attrValue !== null) { + return attrValue; + } + return null; + }, + + + attachEvent : function (el, evnt, func) { + if (el.addEventListener) { + el.addEventListener(evnt, func, false); + } else if (el.attachEvent) { + el.attachEvent('on' + evnt, func); + } + }, + + + detachEvent : function (el, evnt, func) { + if (el.removeEventListener) { + el.removeEventListener(evnt, func, false); + } else if (el.detachEvent) { + el.detachEvent('on' + evnt, func); + } + }, + + + _attachedGroupEvents : {}, + + + attachGroupEvent : function (groupName, el, evnt, func) { + if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + jsc._attachedGroupEvents[groupName] = []; + } + jsc._attachedGroupEvents[groupName].push([el, evnt, func]); + jsc.attachEvent(el, evnt, func); + }, + + + detachGroupEvents : function (groupName) { + if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) { + var evt = jsc._attachedGroupEvents[groupName][i]; + jsc.detachEvent(evt[0], evt[1], evt[2]); + } + delete jsc._attachedGroupEvents[groupName]; + } + }, + + + attachDOMReadyEvent : function (func) { + var fired = false; + var fireOnce = function () { + if (!fired) { + fired = true; + func(); + } + }; + + if (document.readyState === 'complete') { + setTimeout(fireOnce, 1); // async + return; + } + + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fireOnce, false); + + // Fallback + window.addEventListener('load', fireOnce, false); + + } else if (document.attachEvent) { + // IE + document.attachEvent('onreadystatechange', function () { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', arguments.callee); + fireOnce(); + } + }) + + // Fallback + window.attachEvent('onload', fireOnce); + + // IE7/8 + if (document.documentElement.doScroll && window == window.top) { + var tryScroll = function () { + if (!document.body) { return; } + try { + document.documentElement.doScroll('left'); + fireOnce(); + } catch (e) { + setTimeout(tryScroll, 1); + } + }; + tryScroll(); + } + } + }, + + + warn : function (msg) { + if (window.console && window.console.warn) { + window.console.warn(msg); + } + }, + + + preventDefault : function (e) { + if (e.preventDefault) { e.preventDefault(); } + e.returnValue = false; + }, + + + captureTarget : function (target) { + // IE + if (target.setCapture) { + jsc._capturedTarget = target; + jsc._capturedTarget.setCapture(); + } + }, + + + releaseTarget : function () { + // IE + if (jsc._capturedTarget) { + jsc._capturedTarget.releaseCapture(); + jsc._capturedTarget = null; + } + }, + + + fireEvent : function (el, evnt) { + if (!el) { + return; + } + if (document.createEvent) { + var ev = document.createEvent('HTMLEvents'); + ev.initEvent(evnt, true, true); + el.dispatchEvent(ev); + } else if (document.createEventObject) { + var ev = document.createEventObject(); + el.fireEvent('on' + evnt, ev); + } else if (el['on' + evnt]) { // alternatively use the traditional event model + el['on' + evnt](); + } + }, + + + classNameToList : function (className) { + return className.replace(/^\s+|\s+$/g, '').split(/\s+/); + }, + + + // The className parameter (str) can only contain a single class name + hasClass : function (elm, className) { + if (!className) { + return false; + } + return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' '); + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + setClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + if (!jsc.hasClass(elm, classList[i])) { + elm.className += (elm.className ? ' ' : '') + classList[i]; + } + } + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + unsetClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + var repl = new RegExp( + '^\\s*' + classList[i] + '\\s*|' + + '\\s*' + classList[i] + '\\s*$|' + + '\\s+' + classList[i] + '(\\s+)', + 'g' + ); + elm.className = elm.className.replace(repl, '$1'); + } + }, + + + getStyle : function (elm) { + return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle; + }, + + + setStyle : (function () { + var helper = document.createElement('div'); + var getSupportedProp = function (names) { + for (var i = 0; i < names.length; i += 1) { + if (names[i] in helper.style) { + return names[i]; + } + } + }; + var props = { + borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']), + boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow']) + }; + return function (elm, prop, value) { + switch (prop.toLowerCase()) { + case 'opacity': + var alphaOpacity = Math.round(parseFloat(value) * 100); + elm.style.opacity = value; + elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')'; + break; + default: + elm.style[props[prop]] = value; + break; + } + }; + })(), + + + setBorderRadius : function (elm, value) { + jsc.setStyle(elm, 'borderRadius', value || '0'); + }, + + + setBoxShadow : function (elm, value) { + jsc.setStyle(elm, 'boxShadow', value || 'none'); + }, + + + getElementPos : function (e, relativeToViewport) { + var x=0, y=0; + var rect = e.getBoundingClientRect(); + x = rect.left; + y = rect.top; + if (!relativeToViewport) { + var viewPos = jsc.getViewPos(); + x += viewPos[0]; + y += viewPos[1]; + } + return [x, y]; + }, + + + getElementSize : function (e) { + return [e.offsetWidth, e.offsetHeight]; + }, + + + // get pointer's X/Y coordinates relative to viewport + getAbsPointerPos : function (e) { + if (!e) { e = window.event; } + var x = 0, y = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + x = e.changedTouches[0].clientX; + y = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + x = e.clientX; + y = e.clientY; + } + return { x: x, y: y }; + }, + + + // get pointer's X/Y coordinates relative to target element + getRelPointerPos : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + var targetRect = target.getBoundingClientRect(); + + var x = 0, y = 0; + + var clientX = 0, clientY = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + clientX = e.changedTouches[0].clientX; + clientY = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + clientX = e.clientX; + clientY = e.clientY; + } + + x = clientX - targetRect.left; + y = clientY - targetRect.top; + return { x: x, y: y }; + }, + + + getViewPos : function () { + var doc = document.documentElement; + return [ + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) + ]; + }, + + + getViewSize : function () { + var doc = document.documentElement; + return [ + (window.innerWidth || doc.clientWidth), + (window.innerHeight || doc.clientHeight), + ]; + }, + + + redrawPosition : function () { + + if (jsc.picker && jsc.picker.owner) { + var thisObj = jsc.picker.owner; + + var tp, vp; + + if (thisObj.fixed) { + // Fixed elements are positioned relative to viewport, + // therefore we can ignore the scroll offset + tp = jsc.getElementPos(thisObj.targetElement, true); // target pos + vp = [0, 0]; // view pos + } else { + tp = jsc.getElementPos(thisObj.targetElement); // target pos + vp = jsc.getViewPos(); // view pos + } + + var ts = jsc.getElementSize(thisObj.targetElement); // target size + var vs = jsc.getViewSize(); // view size + var ps = jsc.getPickerOuterDims(thisObj); // picker size + var a, b, c; + switch (thisObj.position.toLowerCase()) { + case 'left': a=1; b=0; c=-1; break; + case 'right':a=1; b=0; c=1; break; + case 'top': a=0; b=1; c=-1; break; + default: a=0; b=1; c=1; break; + } + var l = (ts[b]+ps[b])/2; + + // compute picker position + if (!thisObj.smartPosition) { + var pp = [ + tp[a], + tp[b]+ts[b]-l+l*c + ]; + } else { + var pp = [ + -vp[a]+tp[a]+ps[a] > vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + + var x = pp[a]; + var y = pp[b]; + var positionValue = thisObj.fixed ? 'fixed' : 'absolute'; + var contractShadow = + (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) && + (pp[1] + ps[1] < tp[1] + ts[1]); + + jsc._drawPosition(thisObj, x, y, positionValue, contractShadow); + } + }, + + + _drawPosition : function (thisObj, x, y, positionValue, contractShadow) { + var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px + + jsc.picker.wrap.style.position = positionValue; + jsc.picker.wrap.style.left = x + 'px'; + jsc.picker.wrap.style.top = y + 'px'; + + jsc.setBoxShadow( + jsc.picker.boxS, + thisObj.shadow ? + new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) : + null); + }, + + + getPickerDims : function (thisObj) { + var displaySlider = !!jsc.getSliderComponent(thisObj); + var dims = [ + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width + + (displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0), + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height + + (thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0) + ]; + return dims; + }, + + + getPickerOuterDims : function (thisObj) { + var dims = jsc.getPickerDims(thisObj); + return [ + dims[0] + 2 * thisObj.borderWidth, + dims[1] + 2 * thisObj.borderWidth + ]; + }, + + + getPadToSliderPadding : function (thisObj) { + return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness)); + }, + + + getPadYComponent : function (thisObj) { + switch (thisObj.mode.charAt(1).toLowerCase()) { + case 'v': return 'v'; break; + } + return 's'; + }, + + + getSliderComponent : function (thisObj) { + if (thisObj.mode.length > 2) { + switch (thisObj.mode.charAt(2).toLowerCase()) { + case 's': return 's'; break; + case 'v': return 'v'; break; + } + } + return null; + }, + + + onDocumentMouseDown : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse'); + } else { + // Mouse is outside the picker controls -> hide the color picker! + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onDocumentTouchStart : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'touch'); + } else { + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onWindowResize : function (e) { + jsc.redrawPosition(); + }, + + + onParentScroll : function (e) { + // hide the picker when one of the parent elements is scrolled + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + }, + + + _pointerMoveEvent : { + mouse: 'mousemove', + touch: 'touchmove' + }, + _pointerEndEvent : { + mouse: 'mouseup', + touch: 'touchend' + }, + + + _pointerOrigin : null, + _capturedTarget : null, + + + onControlPointerStart : function (e, target, controlName, pointerType) { + var thisObj = target._jscInstance; + + jsc.preventDefault(e); + jsc.captureTarget(target); + + var registerDragEvents = function (doc, offset) { + jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType], + jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset)); + jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType], + jsc.onDocumentPointerEnd(e, target, controlName, pointerType)); + }; + + registerDragEvents(document, [0, 0]); + + if (window.parent && window.frameElement) { + var rect = window.frameElement.getBoundingClientRect(); + var ofs = [-rect.left, -rect.top]; + registerDragEvents(window.parent.window.document, ofs); + } + + var abs = jsc.getAbsPointerPos(e); + var rel = jsc.getRelPointerPos(e); + jsc._pointerOrigin = { + x: abs.x - rel.x, + y: abs.y - rel.y + }; + + switch (controlName) { + case 'pad': + // if the slider is at the bottom, move it up + switch (jsc.getSliderComponent(thisObj)) { + case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break; + case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break; + } + jsc.setPad(thisObj, e, 0, 0); + break; + + case 'sld': + jsc.setSld(thisObj, e, 0); + break; + } + + jsc.dispatchFineChange(thisObj); + }, + + + onDocumentPointerMove : function (e, target, controlName, pointerType, offset) { + return function (e) { + var thisObj = target._jscInstance; + switch (controlName) { + case 'pad': + if (!e) { e = window.event; } + jsc.setPad(thisObj, e, offset[0], offset[1]); + jsc.dispatchFineChange(thisObj); + break; + + case 'sld': + if (!e) { e = window.event; } + jsc.setSld(thisObj, e, offset[1]); + jsc.dispatchFineChange(thisObj); + break; + } + } + }, + + + onDocumentPointerEnd : function (e, target, controlName, pointerType) { + return function (e) { + var thisObj = target._jscInstance; + jsc.detachGroupEvents('drag'); + jsc.releaseTarget(); + // Always dispatch changes after detaching outstanding mouse handlers, + // in case some user interaction will occur in user's onchange callback + // that would intrude with current mouse events + jsc.dispatchChange(thisObj); + }; + }, + + + dispatchChange : function (thisObj) { + if (thisObj.valueElement) { + if (jsc.isElementType(thisObj.valueElement, 'input')) { + jsc.fireEvent(thisObj.valueElement, 'change'); + } + } + }, + + + dispatchFineChange : function (thisObj) { + if (thisObj.onFineChange) { + var callback; + if (typeof thisObj.onFineChange === 'string') { + callback = new Function (thisObj.onFineChange); + } else { + callback = thisObj.onFineChange; + } + callback.call(thisObj); + } + }, + + + setPad : function (thisObj, e, ofsX, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth; + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var xVal = x * (360 / (thisObj.width - 1)); + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getPadYComponent(thisObj)) { + case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break; + case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break; + } + }, + + + setSld : function (thisObj, e, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getSliderComponent(thisObj)) { + case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break; + case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break; + } + }, + + + _vmlNS : 'jsc_vml_', + _vmlCSS : 'jsc_vml_css_', + _vmlReady : false, + + + initVML : function () { + if (!jsc._vmlReady) { + // init VML namespace + var doc = document; + if (!doc.namespaces[jsc._vmlNS]) { + doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml'); + } + if (!doc.styleSheets[jsc._vmlCSS]) { + var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image']; + var ss = doc.createStyleSheet(); + ss.owningElement.id = jsc._vmlCSS; + for (var i = 0; i < tags.length; i += 1) { + ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);'); + } + } + jsc._vmlReady = true; + } + }, + + + createPalette : function () { + + var paletteObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, type) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0); + hGrad.addColorStop(0 / 6, '#F00'); + hGrad.addColorStop(1 / 6, '#FF0'); + hGrad.addColorStop(2 / 6, '#0F0'); + hGrad.addColorStop(3 / 6, '#0FF'); + hGrad.addColorStop(4 / 6, '#00F'); + hGrad.addColorStop(5 / 6, '#F0F'); + hGrad.addColorStop(6 / 6, '#F00'); + + ctx.fillStyle = hGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height); + switch (type.toLowerCase()) { + case 's': + vGrad.addColorStop(0, 'rgba(255,255,255,0)'); + vGrad.addColorStop(1, 'rgba(255,255,255,1)'); + break; + case 'v': + vGrad.addColorStop(0, 'rgba(0,0,0,0)'); + vGrad.addColorStop(1, 'rgba(0,0,0,1)'); + break; + } + ctx.fillStyle = vGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + paletteObj.elm = canvas; + paletteObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var hGrad = document.createElement(jsc._vmlNS + ':fill'); + hGrad.type = 'gradient'; + hGrad.method = 'linear'; + hGrad.angle = '90'; + hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0' + + var hRect = document.createElement(jsc._vmlNS + ':rect'); + hRect.style.position = 'absolute'; + hRect.style.left = -1 + 'px'; + hRect.style.top = -1 + 'px'; + hRect.stroked = false; + hRect.appendChild(hGrad); + vmlContainer.appendChild(hRect); + + var vGrad = document.createElement(jsc._vmlNS + ':fill'); + vGrad.type = 'gradient'; + vGrad.method = 'linear'; + vGrad.angle = '180'; + vGrad.opacity = '0'; + + var vRect = document.createElement(jsc._vmlNS + ':rect'); + vRect.style.position = 'absolute'; + vRect.style.left = -1 + 'px'; + vRect.style.top = -1 + 'px'; + vRect.stroked = false; + vRect.appendChild(vGrad); + vmlContainer.appendChild(vRect); + + var drawFunc = function (width, height, type) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + hRect.style.width = + vRect.style.width = + (width + 1) + 'px'; + hRect.style.height = + vRect.style.height = + (height + 1) + 'px'; + + // Colors must be specified during every redraw, otherwise IE won't display + // a full gradient during a subsequential redraw + hGrad.color = '#F00'; + hGrad.color2 = '#F00'; + + switch (type.toLowerCase()) { + case 's': + vGrad.color = vGrad.color2 = '#FFF'; + break; + case 'v': + vGrad.color = vGrad.color2 = '#000'; + break; + } + }; + + paletteObj.elm = vmlContainer; + paletteObj.draw = drawFunc; + } + + return paletteObj; + }, + + + createSliderGradient : function () { + + var sliderObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, color1, color2) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + sliderObj.elm = canvas; + sliderObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var grad = document.createElement(jsc._vmlNS + ':fill'); + grad.type = 'gradient'; + grad.method = 'linear'; + grad.angle = '180'; + + var rect = document.createElement(jsc._vmlNS + ':rect'); + rect.style.position = 'absolute'; + rect.style.left = -1 + 'px'; + rect.style.top = -1 + 'px'; + rect.stroked = false; + rect.appendChild(grad); + vmlContainer.appendChild(rect); + + var drawFunc = function (width, height, color1, color2) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + rect.style.width = (width + 1) + 'px'; + rect.style.height = (height + 1) + 'px'; + + grad.color = color1; + grad.color2 = color2; + }; + + sliderObj.elm = vmlContainer; + sliderObj.draw = drawFunc; + } + + return sliderObj; + }, + + + leaveValue : 1<<0, + leaveStyle : 1<<1, + leavePad : 1<<2, + leaveSld : 1<<3, + + + BoxShadow : (function () { + var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) { + this.hShadow = hShadow; + this.vShadow = vShadow; + this.blur = blur; + this.spread = spread; + this.color = color; + this.inset = !!inset; + }; + + BoxShadow.prototype.toString = function () { + var vals = [ + Math.round(this.hShadow) + 'px', + Math.round(this.vShadow) + 'px', + Math.round(this.blur) + 'px', + Math.round(this.spread) + 'px', + this.color + ]; + if (this.inset) { + vals.push('inset'); + } + return vals.join(' '); + }; + + return BoxShadow; + })(), + + + // + // Usage: + // var myColor = new jscolor( [, ]) + // + + jscolor : function (targetElement, options) { + + // General options + // + this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB() + this.valueElement = targetElement; // element that will be used to display and input the color code + this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor + this.required = true; // whether the associated text can be left empty + this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace) + this.hash = false; // whether to prefix the HEX color code with # symbol + this.uppercase = true; // whether to show the color code in upper case + this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code) + this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it + this.overwriteImportant = false; // whether to overwrite colors of styleElement using !important + this.minS = 0; // min allowed saturation (0 - 100) + this.maxS = 100; // max allowed saturation (0 - 100) + this.minV = 0; // min allowed value (brightness) (0 - 100) + this.maxV = 100; // max allowed value (brightness) (0 - 100) + + // Accessing the picked color + // + this.hsv = [0, 0, 100]; // read-only [0-360, 0-100, 0-100] + this.rgb = [255, 255, 255]; // read-only [0-255, 0-255, 0-255] + + // Color Picker options + // + this.width = 181; // width of color palette (in px) + this.height = 101; // height of color palette (in px) + this.showOnClick = true; // whether to display the color picker when user clicks on its target element + this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls + this.position = 'bottom'; // left | right | top | bottom - position relative to the target element + this.smartPosition = true; // automatically change picker position when there is not enough space for it + this.sliderSize = 16; // px + this.crossSize = 8; // px + this.closable = false; // whether to display the Close button + this.closeText = 'Close'; + this.buttonColor = '#000000'; // CSS color + this.buttonHeight = 18; // px + this.padding = 12; // px + this.backgroundColor = '#FFFFFF'; // CSS color + this.borderWidth = 1; // px + this.borderColor = '#BBBBBB'; // CSS color + this.borderRadius = 8; // px + this.insetWidth = 1; // px + this.insetColor = '#BBBBBB'; // CSS color + this.shadow = true; // whether to display shadow + this.shadowBlur = 15; // px + this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color + this.pointerColor = '#4C4C4C'; // px + this.pointerBorderColor = '#FFFFFF'; // px + this.pointerBorderWidth = 1; // px + this.pointerThickness = 2; // px + this.zIndex = 1000; + this.container = null; // where to append the color picker (BODY element by default) + + + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + this[opt] = options[opt]; + } + } + + + this.hide = function () { + if (isPickerOwner()) { + detachPicker(); + } + }; + + + this.show = function () { + drawPicker(); + }; + + + this.redraw = function () { + if (isPickerOwner()) { + drawPicker(); + } + }; + + + this.importColor = function () { + if (!this.valueElement) { + this.exportColor(); + } else { + if (jsc.isElementType(this.valueElement, 'input')) { + if (!this.refine) { + if (!this.fromString(this.valueElement.value, jsc.leaveValue)) { + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + } + } else if (!this.required && /^\s*$/.test(this.valueElement.value)) { + this.valueElement.value = ''; + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + + } else if (this.fromString(this.valueElement.value)) { + // managed to import color successfully from the value -> OK, don't do anything + } else { + this.exportColor(); + } + } else { + // not an input element -> doesn't have any value + this.exportColor(); + } + } + }; + + + this.exportColor = function (flags) { + if (!(flags & jsc.leaveValue) && this.valueElement) { + var value = this.toString(); + if (this.uppercase) { value = value.toUpperCase(); } + if (this.hash) { value = '#' + value; } + + if (jsc.isElementType(this.valueElement, 'input')) { + this.valueElement.value = value; + } else { + this.valueElement.innerHTML = value; + } + } + if (!(flags & jsc.leaveStyle)) { + if (this.styleElement) { + var bgColor = '#' + this.toString(); + var fgColor = this.isLight() ? '#000' : '#FFF'; + + this.styleElement.style.backgroundImage = 'none'; + this.styleElement.style.backgroundColor = bgColor; + this.styleElement.style.color = fgColor; + + if (this.overwriteImportant) { + this.styleElement.setAttribute('style', + 'background: ' + bgColor + ' !important; ' + + 'color: ' + fgColor + ' !important;' + ); + } + } + } + if (!(flags & jsc.leavePad) && isPickerOwner()) { + redrawPad(); + } + if (!(flags & jsc.leaveSld) && isPickerOwner()) { + redrawSld(); + } + }; + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + this.fromHSV = function (h, s, v, flags) { // null = don't change + if (h !== null) { + if (isNaN(h)) { return false; } + h = Math.max(0, Math.min(360, h)); + } + if (s !== null) { + if (isNaN(s)) { return false; } + s = Math.max(0, Math.min(100, this.maxS, s), this.minS); + } + if (v !== null) { + if (isNaN(v)) { return false; } + v = Math.max(0, Math.min(100, this.maxV, v), this.minV); + } + + this.rgb = HSV_RGB( + h===null ? this.hsv[0] : (this.hsv[0]=h), + s===null ? this.hsv[1] : (this.hsv[1]=s), + v===null ? this.hsv[2] : (this.hsv[2]=v) + ); + + this.exportColor(flags); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + this.fromRGB = function (r, g, b, flags) { // null = don't change + if (r !== null) { + if (isNaN(r)) { return false; } + r = Math.max(0, Math.min(255, r)); + } + if (g !== null) { + if (isNaN(g)) { return false; } + g = Math.max(0, Math.min(255, g)); + } + if (b !== null) { + if (isNaN(b)) { return false; } + b = Math.max(0, Math.min(255, b)); + } + + var hsv = RGB_HSV( + r===null ? this.rgb[0] : r, + g===null ? this.rgb[1] : g, + b===null ? this.rgb[2] : b + ); + if (hsv[0] !== null) { + this.hsv[0] = Math.max(0, Math.min(360, hsv[0])); + } + if (hsv[2] !== 0) { + this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1])); + } + this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2])); + + // update RGB according to final HSV, as some values might be trimmed + var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]); + this.rgb[0] = rgb[0]; + this.rgb[1] = rgb[1]; + this.rgb[2] = rgb[2]; + + this.exportColor(flags); + }; + + + this.fromString = function (str, flags) { + var m; + if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) { + // HEX notation + // + + if (m[1].length === 6) { + // 6-char notation + this.fromRGB( + parseInt(m[1].substr(0,2),16), + parseInt(m[1].substr(2,2),16), + parseInt(m[1].substr(4,2),16), + flags + ); + } else { + // 3-char notation + this.fromRGB( + parseInt(m[1].charAt(0) + m[1].charAt(0),16), + parseInt(m[1].charAt(1) + m[1].charAt(1),16), + parseInt(m[1].charAt(2) + m[1].charAt(2),16), + flags + ); + } + return true; + + } else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) { + var params = m[1].split(','); + var re = /^\s*(\d*)(\.\d+)?\s*$/; + var mR, mG, mB; + if ( + params.length >= 3 && + (mR = params[0].match(re)) && + (mG = params[1].match(re)) && + (mB = params[2].match(re)) + ) { + var r = parseFloat((mR[1] || '0') + (mR[2] || '')); + var g = parseFloat((mG[1] || '0') + (mG[2] || '')); + var b = parseFloat((mB[1] || '0') + (mB[2] || '')); + this.fromRGB(r, g, b, flags); + return true; + } + } + return false; + }; + + + this.toString = function () { + return ( + (0x100 | Math.round(this.rgb[0])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[1])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[2])).toString(16).substr(1) + ); + }; + + + this.toHEXString = function () { + return '#' + this.toString().toUpperCase(); + }; + + + this.toRGBString = function () { + return ('rgb(' + + Math.round(this.rgb[0]) + ',' + + Math.round(this.rgb[1]) + ',' + + Math.round(this.rgb[2]) + ')' + ); + }; + + + this.isLight = function () { + return ( + 0.213 * this.rgb[0] + + 0.715 * this.rgb[1] + + 0.072 * this.rgb[2] > + 255 / 2 + ); + }; + + + this._processParentElementsInDOM = function () { + if (this._linkedElementsProcessed) { return; } + this._linkedElementsProcessed = true; + + var elm = this.targetElement; + do { + // If the target element or one of its parent nodes has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // Ensure to attach onParentScroll only once to each parent element + // (multiple targetElements can share the same parent nodes) + // + // Note: It's not just offsetParents that can be scrollable, + // that's why we loop through all parent nodes + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body')); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + // returns: [ 0-360, 0-100, 0-100 ] + // + function RGB_HSV (r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if (m === 0) { return [ null, 0, 100 * v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ + 60 * (h===6?0:h), + 100 * (m/v), + 100 * v + ]; + } + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + // returns: [ 0-255, 0-255, 0-255 ] + // + function HSV_RGB (h, s, v) { + var u = 255 * (v / 100); + + if (h === null) { + return [ u, u, u ]; + } + + h /= 60; + s /= 100; + + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = u * (1 - s); + var n = u * (1 - s * f); + switch (i) { + case 6: + case 0: return [u,n,m]; + case 1: return [n,u,m]; + case 2: return [m,u,n]; + case 3: return [m,n,u]; + case 4: return [n,m,u]; + case 5: return [u,m,n]; + } + } + + + function detachPicker () { + jsc.unsetClass(THIS.targetElement, THIS.activeClass); + jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap); + delete jsc.picker.owner; + } + + + function drawPicker () { + + // At this point, when drawing the picker, we know what the parent elements are + // and we can do all related DOM operations, such as registering events on them + // or checking their positioning + THIS._processParentElementsInDOM(); + + if (!jsc.picker) { + jsc.picker = { + owner: null, + wrap : document.createElement('div'), + box : document.createElement('div'), + boxS : document.createElement('div'), // shadow area + boxB : document.createElement('div'), // border + pad : document.createElement('div'), + padB : document.createElement('div'), // border + padM : document.createElement('div'), // mouse/touch area + padPal : jsc.createPalette(), + cross : document.createElement('div'), + crossBY : document.createElement('div'), // border Y + crossBX : document.createElement('div'), // border X + crossLY : document.createElement('div'), // line Y + crossLX : document.createElement('div'), // line X + sld : document.createElement('div'), + sldB : document.createElement('div'), // border + sldM : document.createElement('div'), // mouse/touch area + sldGrad : jsc.createSliderGradient(), + sldPtrS : document.createElement('div'), // slider pointer spacer + sldPtrIB : document.createElement('div'), // slider pointer inner border + sldPtrMB : document.createElement('div'), // slider pointer middle border + sldPtrOB : document.createElement('div'), // slider pointer outer border + btn : document.createElement('div'), + btnT : document.createElement('span') // text + }; + + jsc.picker.pad.appendChild(jsc.picker.padPal.elm); + jsc.picker.padB.appendChild(jsc.picker.pad); + jsc.picker.cross.appendChild(jsc.picker.crossBY); + jsc.picker.cross.appendChild(jsc.picker.crossBX); + jsc.picker.cross.appendChild(jsc.picker.crossLY); + jsc.picker.cross.appendChild(jsc.picker.crossLX); + jsc.picker.padB.appendChild(jsc.picker.cross); + jsc.picker.box.appendChild(jsc.picker.padB); + jsc.picker.box.appendChild(jsc.picker.padM); + + jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm); + jsc.picker.sldB.appendChild(jsc.picker.sld); + jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB); + jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB); + jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB); + jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS); + jsc.picker.box.appendChild(jsc.picker.sldB); + jsc.picker.box.appendChild(jsc.picker.sldM); + + jsc.picker.btn.appendChild(jsc.picker.btnT); + jsc.picker.box.appendChild(jsc.picker.btn); + + jsc.picker.boxB.appendChild(jsc.picker.box); + jsc.picker.wrap.appendChild(jsc.picker.boxS); + jsc.picker.wrap.appendChild(jsc.picker.boxB); + } + + var p = jsc.picker; + + var displaySlider = !!jsc.getSliderComponent(THIS); + var dims = jsc.getPickerDims(THIS); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var padToSliderPadding = jsc.getPadToSliderPadding(THIS); + var borderRadius = Math.min( + THIS.borderRadius, + Math.round(THIS.padding * Math.PI)); // px + var padCursor = 'crosshair'; + + // wrap + p.wrap.style.clear = 'both'; + p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.zIndex = THIS.zIndex; + + // picker + p.box.style.width = dims[0] + 'px'; + p.box.style.height = dims[1] + 'px'; + + p.boxS.style.position = 'absolute'; + p.boxS.style.left = '0'; + p.boxS.style.top = '0'; + p.boxS.style.width = '100%'; + p.boxS.style.height = '100%'; + jsc.setBorderRadius(p.boxS, borderRadius + 'px'); + + // picker border + p.boxB.style.position = 'relative'; + p.boxB.style.border = THIS.borderWidth + 'px solid'; + p.boxB.style.borderColor = THIS.borderColor; + p.boxB.style.background = THIS.backgroundColor; + jsc.setBorderRadius(p.boxB, borderRadius + 'px'); + + // IE hack: + // If the element is transparent, IE will trigger the event on the elements under it, + // e.g. on Canvas or on elements with border + p.padM.style.background = + p.sldM.style.background = + '#FFF'; + jsc.setStyle(p.padM, 'opacity', '0'); + jsc.setStyle(p.sldM, 'opacity', '0'); + + // pad + p.pad.style.position = 'relative'; + p.pad.style.width = THIS.width + 'px'; + p.pad.style.height = THIS.height + 'px'; + + // pad palettes (HSV and HVS) + p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS)); + + // pad border + p.padB.style.position = 'absolute'; + p.padB.style.left = THIS.padding + 'px'; + p.padB.style.top = THIS.padding + 'px'; + p.padB.style.border = THIS.insetWidth + 'px solid'; + p.padB.style.borderColor = THIS.insetColor; + + // pad mouse area + p.padM._jscInstance = THIS; + p.padM._jscControlName = 'pad'; + p.padM.style.position = 'absolute'; + p.padM.style.left = '0'; + p.padM.style.top = '0'; + p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px'; + p.padM.style.height = dims[1] + 'px'; + p.padM.style.cursor = padCursor; + + // pad cross + p.cross.style.position = 'absolute'; + p.cross.style.left = + p.cross.style.top = + '0'; + p.cross.style.width = + p.cross.style.height = + crossOuterSize + 'px'; + + // pad cross border Y and X + p.crossBY.style.position = + p.crossBX.style.position = + 'absolute'; + p.crossBY.style.background = + p.crossBX.style.background = + THIS.pointerBorderColor; + p.crossBY.style.width = + p.crossBX.style.height = + (2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.crossBY.style.height = + p.crossBX.style.width = + crossOuterSize + 'px'; + p.crossBY.style.left = + p.crossBX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px'; + p.crossBY.style.top = + p.crossBX.style.left = + '0'; + + // pad cross line Y and X + p.crossLY.style.position = + p.crossLX.style.position = + 'absolute'; + p.crossLY.style.background = + p.crossLX.style.background = + THIS.pointerColor; + p.crossLY.style.height = + p.crossLX.style.width = + (crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px'; + p.crossLY.style.width = + p.crossLX.style.height = + THIS.pointerThickness + 'px'; + p.crossLY.style.left = + p.crossLX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px'; + p.crossLY.style.top = + p.crossLX.style.left = + THIS.pointerBorderWidth + 'px'; + + // slider + p.sld.style.overflow = 'hidden'; + p.sld.style.width = THIS.sliderSize + 'px'; + p.sld.style.height = THIS.height + 'px'; + + // slider gradient + p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000'); + + // slider border + p.sldB.style.display = displaySlider ? 'block' : 'none'; + p.sldB.style.position = 'absolute'; + p.sldB.style.right = THIS.padding + 'px'; + p.sldB.style.top = THIS.padding + 'px'; + p.sldB.style.border = THIS.insetWidth + 'px solid'; + p.sldB.style.borderColor = THIS.insetColor; + + // slider mouse area + p.sldM._jscInstance = THIS; + p.sldM._jscControlName = 'sld'; + p.sldM.style.display = displaySlider ? 'block' : 'none'; + p.sldM.style.position = 'absolute'; + p.sldM.style.right = '0'; + p.sldM.style.top = '0'; + p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px'; + p.sldM.style.height = dims[1] + 'px'; + p.sldM.style.cursor = 'default'; + + // slider pointer inner and outer border + p.sldPtrIB.style.border = + p.sldPtrOB.style.border = + THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor; + + // slider pointer outer border + p.sldPtrOB.style.position = 'absolute'; + p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.sldPtrOB.style.top = '0'; + + // slider pointer middle border + p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor; + + // slider pointer spacer + p.sldPtrS.style.width = THIS.sliderSize + 'px'; + p.sldPtrS.style.height = sliderPtrSpace + 'px'; + + // the Close button + function setBtnBorder () { + var insetColors = THIS.insetColor.split(/\s+/); + var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1]; + p.btn.style.borderColor = outsetColor; + } + p.btn.style.display = THIS.closable ? 'block' : 'none'; + p.btn.style.position = 'absolute'; + p.btn.style.left = THIS.padding + 'px'; + p.btn.style.bottom = THIS.padding + 'px'; + p.btn.style.padding = '0 15px'; + p.btn.style.height = THIS.buttonHeight + 'px'; + p.btn.style.border = THIS.insetWidth + 'px solid'; + setBtnBorder(); + p.btn.style.color = THIS.buttonColor; + p.btn.style.font = '12px sans-serif'; + p.btn.style.textAlign = 'center'; + try { + p.btn.style.cursor = 'pointer'; + } catch(eOldIE) { + p.btn.style.cursor = 'hand'; + } + p.btn.onmousedown = function () { + THIS.hide(); + }; + p.btnT.style.lineHeight = THIS.buttonHeight + 'px'; + p.btnT.innerHTML = ''; + p.btnT.appendChild(document.createTextNode(THIS.closeText)); + + // place pointers + redrawPad(); + redrawSld(); + + // If we are changing the owner without first closing the picker, + // make sure to first deal with the old owner + if (jsc.picker.owner && jsc.picker.owner !== THIS) { + jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass); + } + + // Set the new picker owner + jsc.picker.owner = THIS; + + // The redrawPosition() method needs picker.owner to be set, that's why we call it here, + // after setting the owner + if (jsc.isElementType(container, 'body')) { + jsc.redrawPosition(); + } else { + jsc._drawPosition(THIS, 0, 0, 'relative', false); + } + + if (p.wrap.parentNode != container) { + container.appendChild(p.wrap); + } + + jsc.setClass(THIS.targetElement, THIS.activeClass); + } + + + function redrawPad () { + // redraw the pad pointer + switch (jsc.getPadYComponent(THIS)) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1)); + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var ofs = -Math.floor(crossOuterSize / 2); + jsc.picker.cross.style.left = (x + ofs) + 'px'; + jsc.picker.cross.style.top = (y + ofs) + 'px'; + + // redraw the slider + switch (jsc.getSliderComponent(THIS)) { + case 's': + var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]); + var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]); + var color1 = 'rgb(' + + Math.round(rgb1[0]) + ',' + + Math.round(rgb1[1]) + ',' + + Math.round(rgb1[2]) + ')'; + var color2 = 'rgb(' + + Math.round(rgb2[0]) + ',' + + Math.round(rgb2[1]) + ',' + + Math.round(rgb2[2]) + ')'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + case 'v': + var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100); + var color1 = 'rgb(' + + Math.round(rgb[0]) + ',' + + Math.round(rgb[1]) + ',' + + Math.round(rgb[2]) + ')'; + var color2 = '#000'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + } + } + + + function redrawSld () { + var sldComponent = jsc.getSliderComponent(THIS); + if (sldComponent) { + // redraw the slider pointer + switch (sldComponent) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px'; + } + } + + + function isPickerOwner () { + return jsc.picker && jsc.picker.owner === THIS; + } + + + function blurValue () { + THIS.importColor(); + } + + + // Find the target element + if (typeof targetElement === 'string') { + var id = targetElement; + var elm = document.getElementById(id); + if (elm) { + this.targetElement = elm; + } else { + jsc.warn('Could not find target element with ID \'' + id + '\''); + } + } else if (targetElement) { + this.targetElement = targetElement; + } else { + jsc.warn('Invalid target element: \'' + targetElement + '\''); + } + + if (this.targetElement._jscLinkedInstance) { + jsc.warn('Cannot link jscolor twice to the same element. Skipping.'); + return; + } + this.targetElement._jscLinkedInstance = this; + + // Find the value element + this.valueElement = jsc.fetchElement(this.valueElement); + // Find the style element + this.styleElement = jsc.fetchElement(this.styleElement); + + var THIS = this; + var container = + this.container ? + jsc.fetchElement(this.container) : + document.getElementsByTagName('body')[0]; + var sliderPtrSpace = 3; // px + + // For BUTTON elements it's important to stop them from sending the form when clicked + // (e.g. in Safari) + if (jsc.isElementType(this.targetElement, 'button')) { + if (this.targetElement.onclick) { + var origCallback = this.targetElement.onclick; + this.targetElement.onclick = function (evt) { + origCallback.call(this, evt); + return false; + }; + } else { + this.targetElement.onclick = function () { return false; }; + } + } + + /* + var elm = this.targetElement; + do { + // If the target element or one of its offsetParents has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // attach onParentScroll so that we can recompute the picker position + // when one of the offsetParents is scrolled + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body')); + */ + + // valueElement + if (this.valueElement) { + if (jsc.isElementType(this.valueElement, 'input')) { + var updateField = function () { + THIS.fromString(THIS.valueElement.value, jsc.leaveValue); + jsc.dispatchFineChange(THIS); + }; + jsc.attachEvent(this.valueElement, 'keyup', updateField); + jsc.attachEvent(this.valueElement, 'input', updateField); + jsc.attachEvent(this.valueElement, 'blur', blurValue); + this.valueElement.setAttribute('autocomplete', 'off'); + } + } + + // styleElement + if (this.styleElement) { + this.styleElement._jscOrigStyle = { + backgroundImage : this.styleElement.style.backgroundImage, + backgroundColor : this.styleElement.style.backgroundColor, + color : this.styleElement.style.color + }; + } + + if (this.value) { + // Try to set the color from the .value option and if unsuccessful, + // export the current color + this.fromString(this.value) || this.exportColor(); + } else { + this.importColor(); + } + } + +}; + + +//================================ +// Public properties and methods +//================================ + + +// By default, search for all elements with class="jscolor" and install a color picker on them. +// +// You can change what class name will be looked for by setting the property jscolor.lookupClass +// anywhere in your HTML document. To completely disable the automatic lookup, set it to null. +// +jsc.jscolor.lookupClass = 'jscolor'; + + +jsc.jscolor.installByClassName = function (className) { + var inputElms = document.getElementsByTagName('input'); + var buttonElms = document.getElementsByTagName('button'); + + jsc.tryInstallOnElements(inputElms, className); + jsc.tryInstallOnElements(buttonElms, className); +}; + + +jsc.register(); + + +return jsc.jscolor; + + +})(); } diff --git a/src/scripts/log.js b/src/scripts/log.js new file mode 100644 index 0000000..5641fa8 --- /dev/null +++ b/src/scripts/log.js @@ -0,0 +1,151 @@ +'use strict'; + +// ----------------- Internationalization ------------------ +document.querySelectorAll('[data-i18n]').forEach(node => { + let [text, attr] = node.dataset.i18n.split('|'); + text = chrome.i18n.getMessage(text); + attr ? node[attr] = text : node.appendChild(document.createTextNode(text)); +}); +// ----------------- /Internationalization ----------------- + +document.addEventListener('keyup', evt => { + if (evt.keyCode === 27) { + // We either came from /options.html or were opened as a new tab from popup.html (in that case, do nothing) + history.back(); + } +}); + +// ----------------- Spinner ------------------------------- +const spinner = document.querySelector('.spinner'); +function hideSpinner() { + + spinner.classList.remove('on'); + setTimeout(() => { spinner.style.display = 'none'; }, 600); +} + +function showSpinner() { + + spinner.style.display = 'flex'; + spinner.classList.add('on'); +} +// ----------------- /spinner ------------------------------ + +// ----- global +let logger; +const onOff = document.querySelector('#onOff'); +const logSize = document.querySelector('#logSize'); + +chrome.runtime.getBackgroundPage(bg => { + + logger = bg.getLog(); + onOff.checked = logger.active; + logSize.value = logger.size; + renderMatchedLog(); // log content will be shown if there are any, regardless of onOff + renderUnmatchedLog(); // log content will be shown if there are any, regardless of onOff + hideSpinner(); +}); + +onOff.addEventListener('change', (e) => { + + logger.active = onOff.checked; + logger.updateStorage(); +}); + +logSize.addEventListener('change', (e) => { + + logSize.value = logSize.value*1 || logger.size; // defaults on bad number entry + if (logger.size !== logSize.value) { // update on change + logger.size = logSize.value; + logger.updateStorage(); + } +}); + +document.querySelectorAll('button').forEach(item => item.addEventListener('click', process)); + +function process () { + + switch (this.dataset.i18n) { + + case 'back': location.href = '/options.html'; break; + case 'refresh': + renderMatchedLog(); + renderUnmatchedLog(); + break; + case 'clear': + logger.clear(); + renderMatchedLog(); + renderUnmatchedLog(); + break; + } +} + +function renderMatchedLog() { + + // ----- templates & containers + const docfrag = document.createDocumentFragment(); + const tr = document.querySelector('tr.matchedtemplate'); + const tbody = tr.parentNode.nextElementSibling; + tbody.textContent = ''; // clearing the content + + const forAll = chrome.i18n.getMessage('forAll'); + const NA = chrome.i18n.getMessage('notApplicable'); + + logger.matchedList.forEach(item => { + + const pattern = item.matchedPattern ? + (item.matchedPattern === 'all' ? forAll : item.matchedPattern) : 'No matches'; + + // Build a row for this log entry by cloning the tr containing 7 td + const row = tr.cloneNode(true); + row.className = item.matchedPattern ? 'success' : 'secondary'; // this will rest class .tamplate as well + const td = row.children; + + const a = td[0].children[0]; + a.href = item.url; + a.textContent = item.url; + + td[1].textContent = item.title || NA; + td[2].style.backgroundColor = item.color || 'blue'; + td[3].textContent = item.address || NA; + td[4].textContent = pattern; + td[5].textContent = item.whiteBlack || NA; + td[6].textContent = formatInt(item.timestamp); + + docfrag.appendChild(row); + }); + + tbody.appendChild(docfrag); +} + +function renderUnmatchedLog() { + + // ----- templates & containers + const docfrag = document.createDocumentFragment(); + const tr = document.querySelector('tr.unmatchedtemplate'); + const tbody = tr.parentNode.nextElementSibling; + tbody.textContent = ''; // clearing the content + + logger.unmatchedList.forEach(item => { + // Build a row for this log entry by cloning the tr containing 2 td + const row = tr.cloneNode(true); + const td = row.children; + const a = td[0].children[0]; + + a.href = item.url; + a.textContent = item.url; + td[1].textContent = formatInt(item.timestamp); + + docfrag.appendChild(row); + }); + + tbody.appendChild(docfrag); +} + +function formatInt(d) { + // International format based on user locale + // you can delete the other function if you like this + // you can adjust the content via the object properties + return new Intl.DateTimeFormat(navigator.language, + {weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', + hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false}).format(new Date(d)); +} diff --git a/src/scripts/matcher.js b/src/scripts/matcher.js new file mode 100644 index 0000000..19cd49d --- /dev/null +++ b/src/scripts/matcher.js @@ -0,0 +1,116 @@ +'use strict'; + +const schemeSet = { + all : 1, + http: 2, + https: 4 +}; +// Shortcuts so we dont perform i18n lookups for every non-match +const FOR_ALL = {originalPattern: chrome.i18n.getMessage('forAll')} +const NOMATCH_TEXT = chrome.i18n.getMessage('noMatch'); +const NONE_TEXT = chrome.i18n.getMessage('none'); +const NOMATCH_COLOR = '#D3D3D3'; +const WHITE = chrome.i18n.getMessage('white'); +const BLACK = chrome.i18n.getMessage('black'); + +function findProxyMatch(url, activeSettings) { + // note: we've already thrown out inactive settings and inactive patterns in background.js. + // we're not iterating over them + + if (activeSettings.mode === 'patterns') { + // Unfortunately, since Firefox 57 and some releases afterwards, we were unable + // to get anything of the URL except scheme, port, and host (because of Fx's PAC + // implementation). Now we have access to rest of URL, like pre-57, but users + // have written their patterns not anticipating that. Need to do more research + // before using other parts of URL. For now, we ignore the other parts. + const parsedUrl = new URL(url); + const scheme = parsedUrl.protocol.substring(0, parsedUrl.protocol.length-1); // strip the colon + const hostPort = parsedUrl.host; // This includes port if one is specified + + for (const proxy of activeSettings.proxySettings) { + + // Check black patterns first + const blackMatch = proxy.blackPatterns.find(item => + (item.protocols === schemeSet.all || item.protocols === schemeSet[scheme]) && + item.pattern.test(hostPort)); + + if (blackMatch) { + sendToMatchedLog(url, proxy, Utils.getProxyTitle(proxy), blackMatch, BLACK); + continue; // if blacklist matched, continue to the next proxy + } + + const whiteMatch = proxy.whitePatterns.find(item => + (item.protocols === schemeSet.all || item.protocols === schemeSet[scheme]) && + item.pattern.test(hostPort)); + + if (whiteMatch) { + // found a whitelist match, end here + const title = Utils.getProxyTitle(proxy); + Utils.updateIcon('images/icon.svg', proxy.color, title, false, title, false); + sendToMatchedLog(url, proxy, title, whiteMatch, WHITE); + return prepareSetting(proxy); + } + } + // no white matches in any settings + sendToUnmatchedLog(url); + Utils.updateIcon('images/gray.svg', null, NOMATCH_TEXT, false, NOMATCH_TEXT, false); + return {type: 'direct'}; + } + else if (activeSettings.mode === 'disabled') { + // Generally we won't get to this block because our proxy handler is turned off in this mode. + // We will get here at startup and also if there is a race condition between removing our listener + // (when switching to disabled mode) and handaling requests. + return {type: 'direct'}; + } + else { + // Fixed mode -- use 1 proxy for all URLs + const p = activeSettings.proxySettings[0]; + const title = Utils.getProxyTitle(p); + Utils.updateIcon('images/icon.svg', p.color, title, false, title, false); + sendToMatchedLog(url, p, title, FOR_ALL); + return prepareSetting(p); + } +} + +const typeSet = { + 1: 'http', // PROXY_TYPE_HTTP + 2: 'https', // PROXY_TYPE_HTTPS + 3: 'socks', // PROXY_TYPE_SOCKS5 + 4: 'socks4', // PROXY_TYPE_SOCKS4 + 5: 'direct' // PROXY_TYPE_NONE +}; + +function prepareSetting(proxy) { + const ret = { + type: typeSet[proxy.type] || typeSet[5], // If 'direct', all other properties of this object are ignored. + host: proxy.address, + port: proxy.port + }; + proxy.username && (ret.username = proxy.username); + proxy.password && (ret.password = proxy.password); + proxy.proxyDNS && (ret.proxyDNS = proxy.proxyDNS); // Only useful for SOCKS + //if ((proxy.type === PROXY_TYPE_HTTP || proxy.type === PROXY_TYPE_HTTPS) && proxy.username && proxy.password) { + // Using wireshark, I do not see this header being sent, contrary to + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo + //ret.proxyAuthorizationHeader = 'Basic ' + btoa(proxy.username + ":" + proxy.password); + //} + return ret; +} + +function sendToMatchedLog(url, proxy, title, matchedPattern, whiteBlack) { + // log only the data that is needed for display + logger && logger.active && logger.addMatched({ + url, + title, + color: proxy.color, + address: proxy.address, + // Log should display whatever user typed, not our processed version of the pattern + matchedPattern: matchedPattern.originalPattern, + whiteBlack, + timestamp: Date.now() + }); +} + +function sendToUnmatchedLog(url) { + logger && logger.active && logger.addUnmatched({url, timestamp: Date.now()}); +} diff --git a/src/scripts/options.js b/src/scripts/options.js new file mode 100644 index 0000000..14d7eab --- /dev/null +++ b/src/scripts/options.js @@ -0,0 +1,380 @@ +'use strict'; + +// ----------------- Internationalization ------------------ +document.querySelectorAll('[data-i18n]').forEach(node => { + let [text, attr] = node.dataset.i18n.split('|'); + text = chrome.i18n.getMessage(text); + attr ? node[attr] = text : node.appendChild(document.createTextNode(text)); +}); +// ----------------- /Internationalization ----------------- + +// ----- global +const accounts = document.querySelector('#accounts'); +const mode = document.querySelector('#mode'); +const syncOnOff = document.querySelector('#syncOnOff'); +const popup = document.querySelector('.popup'); +const popupMain = popup.children[0]; + +let storageArea, minIndex = Number.MAX_SAFE_INTEGER; + +// ----------------- User Preference ----------------------- +chrome.storage.local.get(null, result => { + // if sync is NOT set or it is false, use this result + syncOnOff.checked = result.sync; + localStorage.setItem('sync', syncOnOff.checked); + storageArea = result.sync ? chrome.storage.sync : chrome.storage.local; + result.sync ? chrome.storage.sync.get(null, processOptions) : processOptions(result); +}); +// ----------------- /User Preference ---------------------- + +// ----------------- Spinner ------------------------------- +const spinner = document.querySelector('.spinner'); +function hideSpinner() { + + spinner.classList.remove('on'); + setTimeout(() => { spinner.style.display = 'none'; }, 600); +} + +function showSpinner() { + + spinner.style.display = 'flex'; + spinner.classList.add('on'); +} +// ----------------- /spinner ------------------------------ + + +// ----- add Listeners for menu +document.querySelectorAll('nav a').forEach(item => item.addEventListener('click', process)); +function process() { + + switch (this.dataset.i18n) { + + case 'add': + localStorage.removeItem('id'); // clear localStorage; this indicates an add not an edit + localStorage.setItem('nextIndex', minIndex); // index to use for this proxy so that it's added to the beginning + location.href = '/proxy.html'; + break; + case 'export': Utils.exportFile(); break; + case 'import': location.href = '/import.html'; break; + case 'importProxyList': location.href = '/import-proxy-list.html'; break; + case 'log': location.href = '/log.html'; break; + case 'about': location.href = '/about.html'; break; + + case 'deleteAll': + if (confirm(chrome.i18n.getMessage('confirmDelete'))) { + showSpinner(); + chrome.storage.local.clear(() => chrome.storage.sync.clear(() => { + hideSpinner(); + Utils.notify(chrome.i18n.getMessage('deleteAllmessage')); + location.href = '/options.html'; + })); + } + break; + + case 'deleteBrowserData': + const h4 = document.createElement('h4'); + const p = document.createElement('p'); + popupMain.children[0].textContent = chrome.i18n.getMessage('deleteBrowserData'); + let h = h4.cloneNode(); + h.textContent = chrome.i18n.getMessage('deleteNot'); + let p1 = p.cloneNode(); + p1.textContent = chrome.i18n.getMessage('deleteBrowserDataNotDescription'); + popupMain.children[1].appendChild(h); + popupMain.children[1].appendChild(p1); + + h = h4.cloneNode(); + h.textContent = chrome.i18n.getMessage('delete'); + p1 = p.cloneNode(); + p1.textContent = chrome.i18n.getMessage('deleteBrowserDataDescription'); + popupMain.children[1].appendChild(h); + popupMain.children[1].appendChild(p1); + + popupMain.children[2].children[0].addEventListener('click', closePopup); + popupMain.children[2].children[1].addEventListener('click', () => // Not cancelled + chrome.browsingData.remove({}, { + //appcache: true, + cache: true, + cookies: true, + downloads: false, + //fileSystems: true, + formData: false, + history: false, + indexedDB: true, + localStorage: true, + pluginData: true, + //passwords: true, + //webSQL: true, + //serverBoundCertificates: true, + serviceWorkers: true + }, () => { + Utils.notify(chrome.i18n.getMessage('done')); + closePopup(); + } + )); + showPopup(); + break; + } +} + +// ----- add Listeners for initial elements +mode.addEventListener('change', selectMode); +function selectMode() { + + // set color + mode.style.color = mode.children[mode.selectedIndex].style.color; + + console.log(mode, "selectMode"); + // we already know the state of sync | this is set when manually changing the select + // it is undefined when mode is switched from toolbar popup or on startup + this && storageArea.set({mode: mode.value}); + + // --- change the state of success/secondary + // change all success -> secondary + document.querySelectorAll('.success').forEach(item => item.classList.replace('success', 'secondary')); + + switch (mode.value) { + + case 'patterns': + document.querySelectorAll('input[name="onOff"]:checked').forEach(item => { + const node = item.parentNode.parentNode; + node.classList.replace('secondary', 'success'); // FF49, Ch 61 + }); + break; + + case 'disabled': // do nothing + break; + + default: + const node = document.getElementById(mode.value); + node && node.classList.replace('secondary', 'success'); + } +} + +syncOnOff.addEventListener('change', () => { + const useSync = syncOnOff.checked; + // sync value always CHECKED locally + // data is merged, replacing exisitng and adding new ones + localStorage.setItem('sync', syncOnOff.checked); + storageArea = syncOnOff.checked ? chrome.storage.sync : chrome.storage.local; + if (useSync && confirm(chrome.i18n.getMessage('confirmTransferToSync'))) { + showSpinner(); + chrome.storage.local.set({sync: true}); // save sync state + chrome.storage.local.get(null, result => { // get source + delete result.sync; + chrome.storage.sync.set(result, hideSpinner); // save to target + }); // get source & save to target + } + else if (!useSync && confirm(chrome.i18n.getMessage('confirmTransferToLocal'))) { + showSpinner(); + chrome.storage.sync.get(null, result => { // get source + result.sync = false; // set sync = false + chrome.storage.local.set(result, hideSpinner); // save to target + }); + } +}); + + +chrome.runtime.onMessage.addListener((message, sender) => { // from popup or bg +// console.log(message); + if(!message.mode || message.mode === mode.value) { return; } // change if it is different + mode.value = message.mode; + selectMode(); +}); + +function processOptions(pref) { + // --- reset + accounts.textContent = ''; + + // remove all