summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 16:25:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 16:25:27 +0000
commitec12bd26de5b4292efc366ee41ef4c8d94e31fa9 (patch)
tree30de89024109eff10978e6b95ebd9b403a3e37b7 /src
parentInitial commit. (diff)
downloadfoxyproxy-firefox-extension-upstream.tar.xz
foxyproxy-firefox-extension-upstream.zip
Adding upstream version 7.5.1+dfsg.upstream/7.5.1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src')
-rw-r--r--src/_locales/en/messages.json180
-rw-r--r--src/_locales/fr/messages.json180
-rw-r--r--src/_locales/ru/messages.json180
-rw-r--r--src/_locales/zh_CN/messages.json160
-rw-r--r--src/_locales/zh_TW/messages.json180
-rw-r--r--src/about.html85
-rw-r--r--src/images/ericjung.pngbin0 -> 69568 bytes
-rw-r--r--src/images/gray.svg197
-rw-r--r--src/images/icon-off.svg23
-rw-r--r--src/images/icon.svg231
-rw-r--r--src/images/legacy-version.pngbin0 -> 125856 bytes
-rw-r--r--src/images/logo.svg316
-rw-r--r--src/import-proxy-list.html114
-rw-r--r--src/import.html96
-rw-r--r--src/log.html128
-rw-r--r--src/manifest.json54
-rw-r--r--src/options.html140
-rw-r--r--src/pattern-help.html119
-rw-r--r--src/pattern-tester.html93
-rw-r--r--src/patterns.html202
-rw-r--r--src/popup.html64
-rw-r--r--src/proxy.html193
-rw-r--r--src/scripts/about.js26
-rw-r--r--src/scripts/background.js275
-rw-r--r--src/scripts/common.js63
-rw-r--r--src/scripts/import-proxy-list.js240
-rw-r--r--src/scripts/import.js440
-rw-r--r--src/scripts/jscolor-2.0.5.js1855
-rw-r--r--src/scripts/log.js151
-rw-r--r--src/scripts/matcher.js116
-rw-r--r--src/scripts/options.js380
-rw-r--r--src/scripts/pattern-help.js9
-rw-r--r--src/scripts/pattern-tester.js77
-rw-r--r--src/scripts/patterns.js283
-rw-r--r--src/scripts/popup.js112
-rw-r--r--src/scripts/proxy.js228
-rw-r--r--src/scripts/utils.js324
-rw-r--r--src/styles/app.css629
-rw-r--r--src/styles/images/animated-overlay.gifbin0 -> 1738 bytes
-rwxr-xr-xsrc/styles/images/ui-bg_flat_0_888888_40x100.pngbin0 -> 179 bytes
-rw-r--r--src/styles/images/ui-bg_flat_0_aaaaaa_40x100.pngbin0 -> 264 bytes
-rw-r--r--src/styles/images/ui-bg_flat_75_ffffff_40x100.pngbin0 -> 260 bytes
-rwxr-xr-xsrc/styles/images/ui-bg_glass_25_e1f0f5_1x400.pngbin0 -> 114 bytes
-rwxr-xr-xsrc/styles/images/ui-bg_glass_55_444444_1x400.pngbin0 -> 121 bytes
-rw-r--r--src/styles/images/ui-bg_glass_55_fbf9ee_1x400.pngbin0 -> 387 bytes
-rw-r--r--src/styles/images/ui-bg_glass_65_ffffff_1x400.pngbin0 -> 259 bytes
-rw-r--r--src/styles/images/ui-bg_glass_75_dadada_1x400.pngbin0 -> 314 bytes
-rw-r--r--src/styles/images/ui-bg_glass_75_e6e6e6_1x400.pngbin0 -> 314 bytes
-rw-r--r--src/styles/images/ui-bg_glass_95_fef1ec_1x400.pngbin0 -> 384 bytes
-rw-r--r--src/styles/images/ui-bg_highlight-soft_75_cccccc_1x100.pngbin0 -> 332 bytes
-rwxr-xr-xsrc/styles/images/ui-bg_inset-soft_95_fef1ec_1x100.pngbin0 -> 123 bytes
-rw-r--r--src/styles/images/ui-icons_222222_256x240.pngbin0 -> 6837 bytes
-rw-r--r--src/styles/images/ui-icons_2e83ff_256x240.pngbin0 -> 4601 bytes
-rwxr-xr-xsrc/styles/images/ui-icons_309bbf_256x240.pngbin0 -> 5355 bytes
-rw-r--r--src/styles/images/ui-icons_444444_256x240.pngbin0 -> 7006 bytes
-rw-r--r--src/styles/images/ui-icons_454545_256x240.pngbin0 -> 6973 bytes
-rw-r--r--src/styles/images/ui-icons_555555_256x240.pngbin0 -> 7074 bytes
-rw-r--r--src/styles/images/ui-icons_777620_256x240.pngbin0 -> 4676 bytes
-rw-r--r--src/styles/images/ui-icons_777777_256x240.pngbin0 -> 7013 bytes
-rw-r--r--src/styles/images/ui-icons_888888_256x240.pngbin0 -> 7044 bytes
-rwxr-xr-xsrc/styles/images/ui-icons_bf3030_256x240.pngbin0 -> 4369 bytes
-rw-r--r--src/styles/images/ui-icons_cc0000_256x240.pngbin0 -> 4632 bytes
-rw-r--r--src/styles/images/ui-icons_cd0a0a_256x240.pngbin0 -> 4601 bytes
-rwxr-xr-xsrc/styles/images/ui-icons_ffffff_256x240.pngbin0 -> 4369 bytes
64 files changed, 8143 insertions, 0 deletions
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="about">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ h3 { border-bottom: 1px solid #ddd; max-width: 20em; }
+ h3:not(:first-of-type) { margin-top: 2em; }
+ h4 { margin-bottom: 0; display: inline-block; }
+ p, img { margin-left: 1em; }
+ i.fa {
+ font-size: 1.3em;
+ margin-right: 0.4em;
+ width: 1em;
+ }
+ i.fa.fa-lock { color: #080; }
+ li.sub { margin-left: 2em; list-style-type: circle; }
+ li.sub + li:not(.sub) { margin-top: 1em; }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="about"></div>
+
+ <!-- main -->
+ <div class="prime">
+
+ <!-- welcome on install/update -->
+ <div class="welcome hide">
+ <h3>Welcome!</h3>
+ <p>FoxyProxy has been around for almost 15 years. So if you're upgrading from a legacy version, and your proxy settings are missing, please <a href="/import.html">import</a> them.<br>
+ <strong>(This message only appears after FoxyProxy has upgraded or is first installed.)</strong></p>
+ </div>
+
+ <h3>Info</h3>
+ <p><strong>Edition:</strong> <span id="edition"></span><br>
+ <strong>Version:</strong> <span id="version"></span><br>
+ <strong>Source Code:</strong> <a target="_blank" href="https://github.com/foxyproxy/firefox-extension">GitHub</a></p>
+
+ <h3>Help</h3>
+ <p>
+ <a href="https://support.getfoxyproxy.org/" target="_blank"><i class="fa fa-question"></i>Open a Support Ticket </a><i>(no registration required)</i><br>
+ <a href="mailto:support@getfoxyproxy.org"><i class="fa fa-envelope-o"></i>Email Support</a><br>
+ <span class="notForBasic"><a href="/pattern-tester.html"><i class="fa fa-flask"></i>Pattern Test</a> &amp; <a href="/pattern-help.html"><i class="fa fa-question-circle"></i>Pattern Help</a> <i>(no internet connection required)</i><br></span>
+ <a href="https://getfoxyproxy.org/geoip/" target="_blank"><i class="fa fa-globe"></i>What's my IP Address?</a>
+ </p>
+
+ <h3>Support Me</h3>
+ <p>Please <a href="https://www.paypal.me/ericjung2/5.99" target="_blank">donate</a> or <a href="https://getfoxyproxy.org/order/" target="_blank">buy dedicated VPN/Proxy Servers</a> in over 100 countries (including such remote places like <a href="https://wikipedia.org/wiki/Réunion" target="_blank">Reunion Island</a>).</p>
+ <p><strong>Thank you for using FoxyProxy!</strong></p>
+ <img src="/images/ericjung.png" alt=""><br>
+ <p>&mdash; <a href="mailto:eric.jung@getfoxyproxy.org">Eric H. Jung</a><br>Denver, Colorado, USA</p>
+
+ <h3>Release Notes for Recent Releases</h3>
+ <h4>Version 7.5.1</h4>
+ <ul>
+ <li>French translation -- Thanks <a target="_blank" href="https://github.com/samuikaze">samuikaze</a>.</li>
+ <li>Traditional Chinese translation -- Thanks <a target="_blank" href="https://github.com/Hugo-C">Hugo-C</a>.</li>
+ <li>Russian translation -- Thanks Vadim.</li>
+ </ul>
+ <h4>Version 7.5</h4>
+ <ul>
+ <li>Import Proxy Lists! Long requested feature. See <a href="https://github.com/foxyproxy/firefox-extension/wiki/Import-Proxy-List">help</a>.</li>
+ <li>Simplified Chinese translation -- Thanks <a target="_blank" href="https://github.com/wsxy162">FeralMeow</a>.</li>
+ <li>Display <strong>Synchronize Settings</strong> for new installations when there are no proxies defined. Reported <a href="https://github.com/foxyproxy/firefox-extension/issues/83">here</a>.</li>
+ <li>Fixed: <strong>Import Settings</strong> bug especially painful for people using the <a href="https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-connect-master-node-proxy.html">example at AWS</a>.
+ Reported <a href="https://github.com/foxyproxy/firefox-extension/issues/76">here</a>. Also fixed bug whereby spinner did not hide after import settings errors.</li>
+ <li>Minor security improvements -- do not accept embedded HTML/JS in other form fields</li>
+ <li><strong>Turn Off (Use Firefox Settings)</strong> should be first in list. Reported <a href="https://github.com/foxyproxy/firefox-extension/issues/78">here</a>.</li>
+ <li>Fixed: Firefox overflow menu displays wrong info for FoxyProxy. Reported <a href="https://github.com/foxyproxy/firefox-extension/issues/84">here</a>.</li>
+ </ul>
+ <div style="text-align: right;">
+ <button type="button" class="button" data-i18n="back">&#x25c1; </button>
+ </div>
+
+ </div>
+
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/about.js"></script>
+ </body>
+</html>
diff --git a/src/images/ericjung.png b/src/images/ericjung.png
new file mode 100644
index 0000000..36e0881
--- /dev/null
+++ b/src/images/ericjung.png
Binary files 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 @@
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xml:space="preserve"
+ viewBox="0 0 48 48"
+ height="48"
+ width="48"
+ version="1.1"
+ id="svg2837"><metadata
+ id="metadata2843"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs2841"><linearGradient
+ id="linearGradient2861"
+ spreadMethod="pad"
+ gradientTransform="matrix(-7.9998322,76.665482,76.665482,7.9998322,231.64795,260.98437)"
+ gradientUnits="userSpaceOnUse"
+ y2="0"
+ x2="1"
+ y1="0"
+ x1="0"><stop
+ id="stop2863"
+ offset="0"
+ style="stop-opacity:1;stop-color:#f89734" /><stop
+ id="stop2865"
+ offset="1"
+ style="stop-opacity:1;stop-color:#fbb371" /></linearGradient><radialGradient
+ id="radialGradient2881"
+ spreadMethod="pad"
+ gradientTransform="matrix(50.959351,23.761917,23.092804,-49.525528,208.03369,303.08252)"
+ gradientUnits="userSpaceOnUse"
+ r="1"
+ cy="0"
+ cx="0"
+ fy="0"
+ fx="0"><stop
+ id="stop2883"
+ offset="0"
+ style="stop-opacity:1;stop-color:#e9eaeb" /><stop
+ id="stop2885"
+ offset="1"
+ style="stop-opacity:1;stop-color:#838487" /></radialGradient><clipPath
+ id="clipPath2893"
+ clipPathUnits="userSpaceOnUse"><path
+ id="path2895"
+ d="M 0,576 576,576 576,0 0,0 0,576 z" /></clipPath><linearGradient
+ id="linearGradient2953"
+ spreadMethod="pad"
+ gradientTransform="matrix(34.282654,-15.727585,-15.727585,-34.282654,213.80908,320.43213)"
+ gradientUnits="userSpaceOnUse"
+ y2="0"
+ x2="1"
+ y1="0"
+ x1="0"><stop
+ id="stop2955"
+ offset="0"
+ style="stop-opacity:1;stop-color:#faa858" /><stop
+ id="stop2957"
+ offset="1"
+ style="stop-opacity:1;stop-color:#fbb371" /></linearGradient><clipPath
+ id="clipPath2965"
+ clipPathUnits="userSpaceOnUse"><path
+ id="path2967"
+ d="M 0,576 576,576 576,0 0,0 0,576 z" /></clipPath><linearGradient
+ y2="0"
+ x2="1"
+ y1="0"
+ x1="0"
+ spreadMethod="pad"
+ gradientTransform="matrix(-7.9998322,76.665482,76.665482,7.9998322,231.64795,260.98437)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient2917"
+ xlink:href="#linearGradient2861" /><radialGradient
+ r="1"
+ fy="0"
+ fx="0"
+ cy="0"
+ cx="0"
+ spreadMethod="pad"
+ gradientTransform="matrix(50.959351,23.761917,23.092804,-49.525528,208.03369,303.08252)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2919"
+ xlink:href="#radialGradient2881" /><linearGradient
+ y2="0"
+ x2="1"
+ y1="0"
+ x1="0"
+ spreadMethod="pad"
+ gradientTransform="matrix(34.282654,-15.727585,-15.727585,-34.282654,213.80908,320.43213)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient2921"
+ xlink:href="#linearGradient2953" /><filter
+ id="filter3790"
+ style="color-interpolation-filters:sRGB;"><feColorMatrix
+ id="feColorMatrix3792"
+ values="0.21 0.72 0.07 0 0 0.21 0.72 0.07 0 0 0.21 0.72 0.07 0 0 0 0 0 1 0 " /></filter></defs><g
+ transform="matrix(1.25,0,0,-1.25,-224.11415,370.89237)"
+ id="g2847"><g
+ style="filter:url(#filter3790)"
+ transform="matrix(0.42687693,0,0,0.42687693,102.75599,148.99767)"
+ id="g2870"><g
+ id="g2849"><g
+ id="g2851"><g
+ id="g2857"><g
+ id="g2859"><path
+ d="m 185.797,300.527 c 0,-22.396 18.681,-40.552 41.725,-40.552 l 0,0 c 23.044,0 41.725,18.156 41.725,40.552 l 0,0 c 0,22.397 -18.681,40.554 -41.725,40.554 l 0,0 c -23.044,0 -41.725,-18.157 -41.725,-40.554"
+ style="fill:url(#linearGradient2917);stroke:none"
+ id="path2867" /></g></g></g></g><g
+ id="g2869"><g
+ id="g2871"><g
+ id="g2877"><g
+ id="g2879"><path
+ d="m 211.943,333.94 c -18.988,-8.854 -27.403,-30.99 -18.798,-49.443 l 0,0 c 8.604,-18.453 30.971,-26.234 49.958,-17.381 l 0,0 c 18.986,8.854 27.401,30.99 18.797,49.442 l 0,0 c -6.194,13.284 -19.518,21.037 -33.58,21.037 l 0,0 c -5.473,0 -11.058,-1.175 -16.377,-3.655"
+ style="fill:url(#radialGradient2919);stroke:none"
+ id="path2887" /></g></g></g></g><g
+ id="g2889"><g
+ id="g2891"
+ clip-path="url(#clipPath2893)"><g
+ id="g2897"
+ transform="translate(261.9004,316.5576)"><path
+ d="m 0,0 c 8.604,-18.451 0.188,-40.588 -18.798,-49.441 -18.986,-8.854 -41.353,-1.072 -49.958,17.38 -8.605,18.454 -0.189,40.589 18.798,49.443 C -30.972,26.235 -8.605,18.454 0,0 z"
+ style="fill:none;stroke:#ffffff;stroke-width:1.76999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path2899" /></g><g
+ id="g2901"
+ transform="translate(253.9063,323.9209)"><path
+ d="m 0,0 c -0.38,6.903 -1.854,15.334 -6.533,20.806 -3.507,4.103 -8.978,-7.857 -10.103,-9.669 -1.38,-2.221 -2.706,-4.984 -4.8,-6.605 -2.945,-2.281 -6.431,-2.372 -9.833,-3.499 -5.391,-1.789 -10.688,-4.57 -14.938,-8.369 -5.214,-4.66 -10.154,-4.124 -16.67,-3.649 -2.711,0.199 -12.849,2.406 -11.04,-3.348 2.192,-6.973 7.778,-12.852 13.386,-17.248 5.302,-4.158 2.946,-8.926 -2.589,-11.503 5.72,-7.829 17.267,-8.914 26.01,-7.4 4.766,0.826 9.278,1.088 14.039,-0.052 3.533,-0.845 7.752,-4.399 11.49,-2.507 3.506,1.711 3.098,5.748 4.096,8.94 1.409,4.499 4.193,8.567 7.519,11.858 6.812,6.743 14.138,16.589 10.803,26.9 C 5.155,-8.506 -0.189,-7.015 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2903" /></g><g
+ id="g2905"
+ transform="translate(215.7368,321.6992)"><path
+ d="m 0,0 c 0,0 11.985,5.765 21.784,5.146 0,0 7.573,-2.107 11.927,-4.1 0,0 6.191,-4.964 7.201,-8.663 0,0 2.938,-8.511 -7.186,-19.298 0,0 -5.095,-6.523 -6.302,-12.757 0,0 -0.092,-4.189 -2.53,-5.651 0,0 -4.019,-1.141 -6.971,0.514 -2.393,0.678 -4.844,1.214 -7.298,1.626 -5.684,0.954 -11.46,0.006 -17.159,1.061 -4.659,0.859 -10.534,2.956 -11.796,8.135 0,0 -2.888,2.565 0.798,13.578 0,0 2.001,12.357 17.532,20.409"
+ style="fill:#f89734;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2907" /></g><g
+ id="g2909"
+ transform="translate(196.5059,295.9878)"><path
+ d="m 0,0 c 2.679,-1.011 4.179,-2.636 10.782,-1.915 3.055,0.334 5.723,3.269 8.877,2.476 8.535,-2.152 22.752,-17.621 22.222,-18.163 -0.955,-0.761 -11.555,14.2 -19.136,10.842 -1.809,-0.8 -4.551,-2.507 -6.534,-2.023 -0.958,0.237 -1.792,0.686 -2.52,1.358 -0.96,0.89 -1.642,2.731 -2.91,3.235 -1.728,0.688 -3.12,0.784 -4.914,1.167 -1.929,0.412 -3.938,1.262 -5.436,2.518"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2911" /></g><g
+ id="g2913"
+ transform="translate(243.0967,279.8789)"><path
+ d="m 0,0 c 0.714,-1.26 -0.438,-4.142 -0.984,-4.45 -0.544,-0.309 -3.745,0.106 -4.459,1.367 -0.713,1.258 0.327,2.263 1.83,3.114 C -2.109,0.883 -0.713,1.259 0,0"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2915" /></g><g
+ id="g2917"
+ transform="translate(206.8965,302.1777)"><path
+ d="M 0,0 C -0.872,1.155 2.11,3.59 2.733,3.646 3.357,3.701 6.101,2.003 6.231,0.562 6.361,-0.879 3.944,0.677 2.223,0.523 0.502,0.367 0.369,-0.49 0,0"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2919" /></g><g
+ id="g2921"
+ transform="translate(262.0864,311.4141)"><path
+ d="m 0,0 c -0.677,-9.71 -5.16,-14.594 -10.862,-21.908 -3.012,-3.861 -5.017,-8.005 -6.067,-12.766 -0.653,-2.955 -2.998,-5.396 -6.187,-5.296 -3.564,0.114 -6.928,2.23 -10.487,2.862 -4.641,0.826 -9.162,0.113 -13.801,-0.206 -4.671,-0.321 -8.985,0.475 -13.127,2.686 -0.069,0.036 -6.823,4.403 -6.476,4.649 4.916,3.463 2.528,8.458 -1.511,11.371 -5.349,3.857 -10.603,10.055 -12.841,16.365 -1.254,3.539 12.776,0.416 14.245,-0.23 4.956,-2.182 2.184,-9.504 1.431,-13.29 1.562,1.982 1.916,4.999 2.747,7.338 1.244,3.501 2.716,7.061 5.389,9.728 3.419,3.415 7.537,5.966 11.82,8.15 5.051,2.578 9.983,4.83 15.651,5.436 4.155,0.441 7.829,-0.055 11.682,-1.675 0.621,-0.264 4.807,-2.148 5.815,-1.898 -3.393,1.208 -10.569,4.446 -9.555,9.164 0.338,1.567 5.62,14.545 7.748,11.455 3.797,-5.514 5.733,-13.408 5.718,-20.002 C -8.68,6.954 -6.037,2.089 -0.348,4.037 -0.384,4.025 0.028,0.412 0,0 m -7.915,12.749 c -0.384,6.973 -1.874,15.488 -6.598,21.016 -3.544,4.145 -9.07,-7.937 -10.206,-9.767 -1.394,-2.243 -2.732,-5.033 -4.849,-6.672 -2.974,-2.305 -6.495,-2.395 -9.931,-3.535 -5.446,-1.808 -10.797,-4.617 -15.089,-8.453 -5.267,-4.707 -10.258,-4.165 -16.839,-3.686 -2.739,0.201 -12.979,2.429 -11.151,-3.382 2.213,-7.042 7.856,-12.98 13.52,-17.421 5.355,-4.199 2.976,-9.016 -2.615,-11.62 5.778,-7.908 17.443,-9.004 26.273,-7.474 4.815,0.834 9.372,1.096 14.18,-0.054 3.57,-0.851 7.831,-4.442 11.607,-2.531 3.541,1.728 3.129,5.806 4.139,9.031 1.422,4.543 4.234,8.652 7.594,11.977 C -1,-13.011 6.4,-3.065 3.032,7.352 -2.708,4.155 -8.107,5.663 -7.915,12.749"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2923" /></g><g
+ id="g2925"
+ transform="translate(240.9268,311.4058)"><path
+ d="m 0,0 c 0.288,1.418 -3.452,2.332 -4.038,2.113 -0.587,-0.218 -2.331,-2.933 -1.827,-4.29 0.502,-1.358 2.012,1.088 3.634,1.692 C -0.613,0.115 -0.123,-0.601 0,0"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2927" /></g><g
+ id="g2929"
+ transform="translate(215.5869,289.4712)"><path
+ d="M 0,0 C 0.181,0.136 0.365,0.415 0.297,0.387 0.506,0.79 0.527,1.284 0.305,1.718 -0.059,2.433 -0.937,2.72 -1.652,2.354 -2.369,1.99 -2.652,1.112 -2.286,0.396 -2.284,0.392 -2.281,0.389 -2.279,0.385 -2.35,0.414 -2.423,0.425 -2.492,0.459 -3.995,1.189 -4.734,2.865 -4.337,4.425 -4.47,4.253 -4.588,4.066 -4.688,3.865 -5.445,2.307 -4.793,0.431 -3.236,-0.324 c 0.9,-0.439 1.9,-0.392 2.733,0.017 0.058,0.021 0.117,0.039 0.174,0.068 C -0.204,-0.175 -0.098,-0.091 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2931" /></g><g
+ id="g2933"
+ transform="translate(201.3467,286.79)"><path
+ d="m 0,0 c 0,0 4.649,-8.012 17.098,-5.728 0,0 8.646,2.306 13.661,-2.572 -1.023,0.995 -1.912,2.037 -3.159,2.8 -3.88,2.374 -8.425,1.586 -12.74,2.007 C 11.729,-3.186 8.814,-2.129 6.012,-0.74 4.997,-0.237 -0.543,3.323 0,0"
+ style="fill:#f6ad44;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2935" /></g><g
+ id="g2937"
+ transform="translate(254.4907,311.8257)"><path
+ d="m 0,0 c 0,0 2.953,-8.739 -7.189,-15.087 0,0 -5.659,-4.735 -5.408,-11.635 -0.051,1.406 -0.211,2.757 0.021,4.15 0.385,2.318 0.466,4.79 1.388,6.944 0.89,2.08 2.775,3.179 4.241,4.747 1.653,1.768 2.733,4.008 3.344,6.417 C -3.336,-3.416 -1.854,2.765 0,0"
+ style="fill:#f6ad44;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2939" /></g></g></g><g
+ id="g2941"><g
+ id="g2943"><g
+ id="g2949"><g
+ id="g2951"><path
+ d="m 219.217,320.557 c -8.361,-3.819 -11.245,-7.843 -10.12,-9.455 l 0,0 c 1.232,-1.763 6.564,-3.975 10.462,-4.716 l 0,0 c 0.437,-0.084 0.615,0.54 0.788,1.164 l 0,0 c 0.175,0.625 0.345,1.251 0.769,1.164 l 0,0 c 2.49,-0.513 5.947,-3.826 7.154,-4.78 l 0,0 c 0.615,-0.486 1.778,-1.525 2.335,-1.306 l 0,0 c 0.448,0.18 -0.171,1.544 -0.27,2.567 l 0,0 c -0.129,1.34 -1.24,6.507 -0.799,8.453 l 0,0 c 0.16,0.711 0.67,0.604 1.192,0.498 l 0,0 c 0.53,-0.109 1.071,-0.219 1.268,0.538 l 0,0 c 1.018,3.921 1.895,7.666 0.749,9.206 l 0,0 c -0.292,0.393 -0.865,0.571 -1.664,0.571 l 0,0 c -2.414,0 -6.885,-1.63 -11.864,-3.904"
+ style="fill:url(#linearGradient2921);stroke:none"
+ id="path2959" /></g></g></g></g><g
+ id="g2961"><g
+ id="g2963"
+ clip-path="url(#clipPath2965)"><g
+ id="g3025"
+ transform="translate(250.4868,321.0234)"><path
+ d="m 0,0 c -1.953,-3.398 -2.12,-7.398 -9.162,-12.051 -1.332,-0.88 -2.181,-1.787 -2.848,-3.422 -3.239,-7.934 2.034,-25.066 2.768,-24.956 1.152,0.325 -5.615,14.689 -1.916,21.923 0.592,1.157 1.516,2.073 2.438,2.968 0.984,0.957 1.551,1.517 2.647,2.326 1.162,0.856 2.677,2.038 4.037,1.981 -0.91,0.038 -2.517,0.43 -3.432,0.469 0.717,1.218 2.645,3.181 3.182,4.031 1.328,2.1 1.92,4.192 2.286,6.731"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path3027" /></g><g
+ id="g3029"
+ transform="translate(245.6221,311.167)"><path
+ d="M 0,0 C 0.858,0.275 1.874,0.881 2.632,1.389 3.313,1.846 4.006,2.344 4.25,3.106 3.887,1.972 2.931,0.588 1.994,-0.055 1.517,-0.382 1.029,-0.701 0.617,-1.111 0.216,-1.509 -0.117,-1.482 -0.718,-1.59 -0.766,-1.337 -0.75,-1.075 -0.75,-0.813"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path3031" /></g></g></g></g></g></svg> \ 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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16" version="1.0">
+ <defs />
+ <g id="fp-toolbar-icon-wrapper">
+ <path
+ d="M 6.9528311,5.5392061 C 6.078693,5.9865447 5.1558911,7.1951946 4.1564529,7.411725 C 2.93869,7.6755556 0.32687179,6.9664456 0.15430293,7.5951656 C -0.047715679,8.331182 0.76226407,9.1562478 1.2788196,9.8302167 C 1.768913,10.46966 2.8447406,11.065076 3.2336907,11.770624 C 3.3555466,11.991667 3.327603,12.497042 3.2231514,12.726823 C 3.0441481,13.120608 2.4364022,12.999877 2.4186989,13.432075 C 2.3962947,13.979032 3.5542026,14.104873 3.9994847,14.423288 C 4.9507718,15.103545 7.7144436,14.39272 9.4389993,14.61604 C 10.221736,14.717399 11.148814,15.322006 11.847091,14.954095 C 12.396545,14.664595 12.407857,13.76107 12.638755,13.18453 C 13.274855,11.596224 15.339377,10.113117 15.809787,8.3770041 C 15.951135,7.8553401 16.051011,7.6443863 16.003543,6.5677102 C 15.985486,6.1627501 14.313661,6.6791933 14.181997,6.250809 C 13.877926,5.2614784 13.49069,1.5693024 12.492336,1.2418717 C 11.888559,1.0438517 10.685347,3.7418119 10.405836,4.0233778 C 9.5690224,4.8663364 8.0102037,4.9980972 6.9528311,5.5392061 z"
+ child="1" style="fill:#404141;fill-opacity:1;fill-rule:evenodd;"/>
+ <path
+ d="M 0.20568678,7.6645691 C 0.34341861,7.1394626 1.600145,7.6615473 2.1392725,7.8053546 C 2.6380008,7.938386 3.5316438,8.0278899 3.5818747,8.6214607 C 3.6321051,9.2150325 3.2384352,11.591761 3.2064979,11.559464 C 2.8655525,11.144845 2.7334657,11.050912 2.6112386,10.932857 C 2.3686973,10.726265 1.6615351,10.160608 1.428759,9.8838909 C 1.3015746,9.6805773 0.020150234,8.3719324 0.20568678,7.6645691 z M 12.549184,1.3457735 C 12.08794,1.0786303 11.823308,2.1859312 11.573845,2.6850383 C 11.343077,3.1467446 10.810782,3.9512951 11.303578,4.4207004 C 11.793649,4.8875075 13.965942,6.2236498 14.056531,6.1200186 C 13.950492,5.5038385 13.918276,5.4561008 13.887697,5.291173 C 13.836482,5.0149429 13.710036,4.3213368 13.585833,3.7901471 C 13.540272,3.5952949 13.181999,1.7122868 12.549184,1.3457735 z M 3.386261,12.385661 C 3.3570684,12.689383 3.2883504,12.877502 3.0628724,13.016513 C 2.7029847,13.2153 3.9697067,13.988726 4.538983,14.358195 C 5.205776,14.790955 7.8050167,14.411446 9.3377054,14.518677 C 9.8933234,14.536208 10.797184,15.013062 11.36776,14.979834 C 11.544589,14.969537 11.862801,14.88525 11.982838,14.754996 C 12.370109,14.334753 12.387909,13.452175 12.641905,12.940248 C 13.281597,11.650953 15.119024,10.063421 15.604578,8.7085325 C 15.68297,7.8356261 15.781428,6.5126663 15.520318,6.4870409 C 15.214754,6.4917744 14.561412,6.5346772 14.350326,6.479347 C 14.462374,6.9675086 14.625496,7.7809115 14.38129,8.4287458 C 14.285095,8.858654 13.966789,9.3695968 13.738912,9.7466211 C 13.372822,10.352321 12.684383,11.009494 12.364183,11.640657 C 12.224139,11.91671 12.208036,12.037577 12.087635,12.322745 C 12.06251,12.382254 11.982325,12.663208 11.945511,12.825914 C 11.90851,12.989448 11.906502,13.229117 11.9294,13.390753 C 11.944152,13.494889 11.975549,13.578024 11.973719,13.683186 C 11.970576,13.863807 11.914997,13.936036 11.788877,14.08214 C 11.682284,14.205627 11.555137,14.224132 11.39542,14.234519 C 11.300053,14.240721 11.130483,14.190616 11.036709,14.172199 C 10.876636,14.140759 10.662733,14.081069 10.502349,14.051254 C 10.22285,13.999299 9.9071515,13.847946 9.6320408,13.776307 C 9.3744382,13.709228 9.215233,13.569314 8.7517211,13.553732 C 8.3073216,13.538795 7.6077138,13.570923 5.3178657,13.413012 C 4.745423,13.21185 4.5368845,13.069772 4.2731323,12.917328 C 4.0311097,12.777446 3.4512118,12.2911 3.386261,12.385661 z M 5.9557144,10.876883 C 5.8265042,10.834827 5.8027499,10.862428 5.7567876,10.920405 C 5.6934488,11.033932 5.7252893,11.154962 5.752615,11.35181 C 5.922675,11.596936 6.1095328,11.882976 6.3873679,11.90327 C 6.773174,11.899855 6.9465841,11.754501 7.042559,11.476784 C 7.0639367,11.298034 6.933336,11.079698 6.7666373,11.003316 C 6.3332289,10.89082 6.3246031,11.190952 6.2272415,11.429915 C 5.8487682,11.245117 6.057563,10.972678 5.9557144,10.876883 z"
+ child="2" style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;"/>
+ <path
+ d="M 3.369765,10.680671 C 3.5727317,10.690432 3.8770401,10.969605 4.1132953,11.031118 C 4.4925298,11.079283 4.8603067,11.022199 5.1959193,11.063738 C 5.5239398,11.119623 5.7350317,11.971153 6.3058989,12.030581 C 6.9914773,12.052432 7.2804998,11.497762 7.6468781,11.523929 C 7.8440671,11.539268 8.1556597,11.449823 8.3579343,11.51788 C 9.2222769,11.865234 10.14038,12.776892 10.906425,13.608604 C 10.960941,13.744537 10.945608,13.977947 10.937125,14.144287 C 10.437546,14.093775 9.6034124,13.761082 9.0892825,13.590076 C 8.1621687,13.499422 6.3869294,13.581782 5.3053032,13.416568 C 4.8841641,13.294572 4.2593683,12.934856 3.9216324,12.67248 C 3.5606357,12.441678 3.410735,12.347246 3.3913281,12.366665 C 3.4268388,12.053346 3.3589432,11.771177 3.2107471,11.576049 C 3.2655485,11.298117 3.3390792,10.896635 3.369765,10.680671 z M 13.412942,5.8487646 C 13.5582,5.8853679 13.838101,6.1039924 14.06634,6.1334002 C 14.108924,6.3685674 14.169958,6.4159442 14.352896,6.4740975 C 14.437858,6.844685 14.527194,7.2873813 14.517468,7.6877218 C 14.498132,8.5231032 14.106452,9.1193122 13.782707,9.6704315 C 13.48933,10.218339 12.915157,10.777331 12.585977,11.283218 C 12.320513,11.666175 12.082532,12.245602 11.976013,12.723691 C 11.939245,12.8923 11.924561,13.171505 11.923572,13.320214 C 11.791451,13.255802 11.786234,13.235417 11.673201,13.084437 C 11.37019,12.442011 11.140744,11.386563 11.098681,11.008291 C 11.094289,10.494878 11.015465,9.6640618 11.087424,9.2377371 C 11.571074,8.5220689 11.897515,8.3404393 12.615171,7.6136001 C 12.696253,7.5130635 12.875624,7.4851031 12.998103,7.4448331 C 12.881467,7.3852593 12.819701,7.388757 12.694765,7.3895745 C 12.741964,7.2853626 13.220837,6.9620062 13.180894,6.9105011 C 13.138931,6.8563922 12.781818,7.1186342 12.547034,7.3201739 C 12.715029,7.0815709 13.00214,6.7836699 13.146761,6.5203061 C 13.281717,6.2073113 13.343088,6.0209861 13.412942,5.8487646 z M 10.644357,4.5191651 C 10.318327,4.5218002 10.202455,4.5312156 9.7247459,4.5949623 C 8.8728289,5.0115836 7.6898876,5.2104852 6.8841482,5.6596449 C 6.1840911,6.10177 5.477202,6.852413 4.6863901,7.3044758 C 4.2872429,7.6149107 3.717839,8.3309511 3.5860504,8.8772274 C 3.5637732,9.3694763 3.452209,10.02604 3.4059454,10.447378 C 3.6032888,10.662574 3.8623409,10.769955 4.1569797,10.838372 C 4.5569612,10.913486 5.3467496,10.874895 5.5682779,10.784535 C 5.9804438,10.562302 6.1134111,10.424144 6.4011658,10.348694 C 6.6867327,10.340809 6.8697623,10.377065 7.097337,10.436888 C 7.4531131,10.601524 7.7818208,10.743226 8.1999155,10.971296 C 8.9191953,11.45319 10.132824,12.561882 10.981433,13.410362 C 11.230277,13.38687 11.412467,13.293967 11.502534,13.202009 C 11.27667,12.553901 11.185208,12.270242 11.007959,11.611453 C 10.926818,11.342381 10.822322,11.038836 10.83247,10.650862 C 10.836338,10.015912 10.794339,9.8058101 10.819836,9.1660101 C 10.974159,8.7381282 10.980631,8.639325 11.189429,8.3576426 C 11.939782,7.6883549 12.136335,7.4770773 12.578704,6.9445246 C 12.832039,6.6012321 13.45392,5.7981166 13.221929,5.7259871 C 12.46368,5.2805256 11.755423,4.7883885 11.544567,4.62131 C 11.28974,4.5420101 10.970386,4.5165307 10.644357,4.5191651 z M 10.870413,7.1211481 C 10.994166,7.0975313 11.113385,7.1507515 11.237243,7.2173728 C 11.320969,7.2652226 11.412631,7.3647636 11.396272,7.4722275 C 11.365214,7.517055 11.032028,7.4535045 10.868432,7.5028614 C 10.744846,7.5401476 10.746225,7.7022038 10.7092,7.8388548 C 10.675269,7.9040084 10.542151,7.9673098 10.453766,7.9299163 C 10.382429,7.8676599 10.470005,7.5833976 10.512504,7.4588313 C 10.575693,7.3246788 10.683931,7.1721997 10.870413,7.1211481 z M 5.4984246,8.7197829 C 5.6289133,8.6821513 5.8154856,8.7204717 5.9696433,8.7942046 C 6.0841061,8.8613794 6.3509112,9.1640647 6.3294408,9.2931018 C 6.2648944,9.3540877 6.164821,9.3259996 6.0521167,9.2693162 C 5.8813095,9.1427944 5.8379278,9.0525661 5.6524934,9.0060516 C 5.5015084,9.0316759 5.464792,9.1630934 5.3725201,9.2855559 C 5.2946105,9.2872545 5.2469955,9.2769124 5.1731311,9.2035927 C 5.1388143,9.0319441 5.3582827,8.7843901 5.4984246,8.7197829 z"
+ id="fp-toolbar-icon-3"
+ child="3" style="fill:#e78500;fill-opacity:1;fill-rule:evenodd;"/>
+ <path
+ d="M 7.7752361,8.0713883 C 8.2357511,8.1047023 9.0181331,9.1274945 9.593674,9.1644079 C 9.7939088,8.7551967 9.1818749,7.5334182 9.4605917,7.0426773 C 9.587645,6.9199001 9.7368859,7.1054057 9.8679577,6.9869243 C 10.045674,6.5266328 10.102021,5.7007475 10.030153,5.155492 C 8.773076,4.9278955 5.9665723,6.2696832 5.4895382,7.3692693 C 5.6671886,8.2743091 6.9507561,8.3704147 7.6433022,8.5430959 C 7.7899242,8.4001393 7.6360949,8.1250716 7.7752361,8.0713883 z M 4.056517,11.806542 C 4.7190124,11.855627 5.326614,12.352907 6.1331704,12.520895 C 6.5947575,12.727856 7.510389,12.356139 8.5293994,12.693972 C 8.9015172,12.793461 9.4077764,12.943647 9.5694597,13.255056 C 9.5734496,13.322804 9.4814885,13.406637 9.4139446,13.41324 C 9.0316557,13.322426 8.8843475,13.142724 8.689763,13.105677 C 7.6159931,12.907688 6.3497738,13.228414 5.3747241,13.102508 C 4.7221956,12.884416 4.3320393,12.661591 4.0172704,12.110141 C 3.9810438,12.02575 3.9784869,11.85497 4.056517,11.806542 z M 13.631863,7.1964869 C 13.40808,7.5562295 13.377639,7.5804303 13.242968,8.3535247 C 13.007705,9.0153875 12.561448,9.2550924 11.885833,10.249827 C 11.74228,10.607264 11.621856,11.133286 11.706563,11.502287 C 11.758383,11.546109 11.88062,11.522795 11.92641,11.472704 C 12.054549,10.840679 12.358602,10.465567 12.477289,10.318745 C 13.367218,9.5516987 14.073007,9.1412775 14.100137,8.0541158 C 14.118124,7.6930443 14.119515,7.6267509 14.097288,7.3768352 C 13.982226,7.1461924 13.819367,7.1622228 13.631863,7.1964869 z"
+ child="4" style="fill:#ffffff;fill-opacity:0.20392157;fill-rule:evenodd;"/>
+ </g>
+ <g id="fp-toolbar-disabled-wrapper" wrapper="true">
+ <path
+ d="M 8,0 C 3.584,8.8817842e-16 -3.006854e-17,3.584 0,8 C 0,12.416 3.584,16 8,16 C 12.416,16 16,12.416 16,8 C 16,3.584 12.416,-1.8665625e-15 8,0 z M 7.8125,1 C 9.8281648,0.94927401 11.856778,1.7662746 13.28125,3.40625 C 15.588012,6.0619933 15.589233,9.9390689 13.28125,12.59375 L 3.40625,2.71875 C 4.6817864,1.6108274 6.2447607,1.0394535 7.8125,1 z M 2.71875,3.40625 L 12.59375,13.28125 C 9.6776547,15.812973 5.2504728,15.509845 2.71875,12.59375 C 0.41476947,9.9399733 0.41589633,6.0610044 2.71875,3.40625 z"
+ child="5" style="fill: #c80000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
+ </g>
+ </svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2837"
+ version="1.1"
+ inkscape:version="0.47pre4 r22446"
+ width="48"
+ height="48"
+ viewBox="0 0 48 48"
+ xml:space="preserve"
+ sodipodi:docname="24x24.svg"><metadata
+ id="metadata2843"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs2841"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ id="perspective2845" /><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-7.9998322,76.665482,76.665482,7.9998322,231.64795,260.98437)"
+ spreadMethod="pad"
+ id="linearGradient2861"><stop
+ style="stop-opacity:1;stop-color:#f89734"
+ offset="0"
+ id="stop2863" /><stop
+ style="stop-opacity:1;stop-color:#fbb371"
+ offset="1"
+ id="stop2865" /></linearGradient><radialGradient
+ fx="0"
+ fy="0"
+ cx="0"
+ cy="0"
+ r="1"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(50.959351,23.761917,23.092804,-49.525528,208.03369,303.08252)"
+ spreadMethod="pad"
+ id="radialGradient2881"><stop
+ style="stop-opacity:1;stop-color:#e9eaeb"
+ offset="0"
+ id="stop2883" /><stop
+ style="stop-opacity:1;stop-color:#838487"
+ offset="1"
+ id="stop2885" /></radialGradient><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath2893"><path
+ d="M 0,576 576,576 576,0 0,0 0,576 z"
+ id="path2895" /></clipPath><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(34.282654,-15.727585,-15.727585,-34.282654,213.80908,320.43213)"
+ spreadMethod="pad"
+ id="linearGradient2953"><stop
+ style="stop-opacity:1;stop-color:#faa858"
+ offset="0"
+ id="stop2955" /><stop
+ style="stop-opacity:1;stop-color:#fbb371"
+ offset="1"
+ id="stop2957" /></linearGradient><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath2965"><path
+ d="M 0,576 576,576 576,0 0,0 0,576 z"
+ id="path2967" /></clipPath><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2861"
+ id="linearGradient2917"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-7.9998322,76.665482,76.665482,7.9998322,231.64795,260.98437)"
+ spreadMethod="pad"
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#radialGradient2881"
+ id="radialGradient2919"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(50.959351,23.761917,23.092804,-49.525528,208.03369,303.08252)"
+ spreadMethod="pad"
+ cx="0"
+ cy="0"
+ fx="0"
+ fy="0"
+ r="1" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2953"
+ id="linearGradient2921"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(34.282654,-15.727585,-15.727585,-34.282654,213.80908,320.43213)"
+ spreadMethod="pad"
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0" /></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1152"
+ inkscape:window-height="793"
+ id="namedview2839"
+ showgrid="false"
+ inkscape:zoom="2.6222222"
+ inkscape:cx="135.88584"
+ inkscape:cy="27.404163"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g2847"
+ inkscape:showpageshadow="false"
+ showborder="false" /><g
+ id="g2847"
+ inkscape:groupmode="layer"
+ inkscape:label="FOXYlogo"
+ transform="matrix(1.25,0,0,-1.25,-224.11415,370.89237)"><g
+ id="g2870"
+ transform="matrix(0.42687693,0,0,0.42687693,102.75599,148.99767)"><g
+ id="g2849"><g
+ id="g2851"><g
+ id="g2857"><g
+ id="g2859"><path
+ id="path2867"
+ style="fill:url(#linearGradient2917);stroke:none"
+ d="m 185.797,300.527 c 0,-22.396 18.681,-40.552 41.725,-40.552 l 0,0 c 23.044,0 41.725,18.156 41.725,40.552 l 0,0 c 0,22.397 -18.681,40.554 -41.725,40.554 l 0,0 c -23.044,0 -41.725,-18.157 -41.725,-40.554" /></g></g></g></g><g
+ id="g2869"><g
+ id="g2871"><g
+ id="g2877"><g
+ id="g2879"><path
+ id="path2887"
+ style="fill:url(#radialGradient2919);stroke:none"
+ d="m 211.943,333.94 c -18.988,-8.854 -27.403,-30.99 -18.798,-49.443 l 0,0 c 8.604,-18.453 30.971,-26.234 49.958,-17.381 l 0,0 c 18.986,8.854 27.401,30.99 18.797,49.442 l 0,0 c -6.194,13.284 -19.518,21.037 -33.58,21.037 l 0,0 c -5.473,0 -11.058,-1.175 -16.377,-3.655" /></g></g></g></g><g
+ id="g2889"><g
+ clip-path="url(#clipPath2893)"
+ id="g2891"><g
+ transform="translate(261.9004,316.5576)"
+ id="g2897"><path
+ id="path2899"
+ style="fill:none;stroke:#ffffff;stroke-width:1.76999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 0,0 c 8.604,-18.451 0.188,-40.588 -18.798,-49.441 -18.986,-8.854 -41.353,-1.072 -49.958,17.38 -8.605,18.454 -0.189,40.589 18.798,49.443 C -30.972,26.235 -8.605,18.454 0,0 z" /></g><g
+ transform="translate(253.9063,323.9209)"
+ id="g2901"><path
+ id="path2903"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.38,6.903 -1.854,15.334 -6.533,20.806 -3.507,4.103 -8.978,-7.857 -10.103,-9.669 -1.38,-2.221 -2.706,-4.984 -4.8,-6.605 -2.945,-2.281 -6.431,-2.372 -9.833,-3.499 -5.391,-1.789 -10.688,-4.57 -14.938,-8.369 -5.214,-4.66 -10.154,-4.124 -16.67,-3.649 -2.711,0.199 -12.849,2.406 -11.04,-3.348 2.192,-6.973 7.778,-12.852 13.386,-17.248 5.302,-4.158 2.946,-8.926 -2.589,-11.503 5.72,-7.829 17.267,-8.914 26.01,-7.4 4.766,0.826 9.278,1.088 14.039,-0.052 3.533,-0.845 7.752,-4.399 11.49,-2.507 3.506,1.711 3.098,5.748 4.096,8.94 1.409,4.499 4.193,8.567 7.519,11.858 6.812,6.743 14.138,16.589 10.803,26.9 C 5.155,-8.506 -0.189,-7.015 0,0" /></g><g
+ transform="translate(215.7368,321.6992)"
+ id="g2905"><path
+ id="path2907"
+ style="fill:#f89734;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0,0 11.985,5.765 21.784,5.146 0,0 7.573,-2.107 11.927,-4.1 0,0 6.191,-4.964 7.201,-8.663 0,0 2.938,-8.511 -7.186,-19.298 0,0 -5.095,-6.523 -6.302,-12.757 0,0 -0.092,-4.189 -2.53,-5.651 0,0 -4.019,-1.141 -6.971,0.514 -2.393,0.678 -4.844,1.214 -7.298,1.626 -5.684,0.954 -11.46,0.006 -17.159,1.061 -4.659,0.859 -10.534,2.956 -11.796,8.135 0,0 -2.888,2.565 0.798,13.578 0,0 2.001,12.357 17.532,20.409" /></g><g
+ transform="translate(196.5059,295.9878)"
+ id="g2909"><path
+ id="path2911"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 2.679,-1.011 4.179,-2.636 10.782,-1.915 3.055,0.334 5.723,3.269 8.877,2.476 8.535,-2.152 22.752,-17.621 22.222,-18.163 -0.955,-0.761 -11.555,14.2 -19.136,10.842 -1.809,-0.8 -4.551,-2.507 -6.534,-2.023 -0.958,0.237 -1.792,0.686 -2.52,1.358 -0.96,0.89 -1.642,2.731 -2.91,3.235 -1.728,0.688 -3.12,0.784 -4.914,1.167 -1.929,0.412 -3.938,1.262 -5.436,2.518" /></g><g
+ transform="translate(243.0967,279.8789)"
+ id="g2913"><path
+ id="path2915"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.714,-1.26 -0.438,-4.142 -0.984,-4.45 -0.544,-0.309 -3.745,0.106 -4.459,1.367 -0.713,1.258 0.327,2.263 1.83,3.114 C -2.109,0.883 -0.713,1.259 0,0" /></g><g
+ transform="translate(206.8965,302.1777)"
+ id="g2917"><path
+ id="path2919"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C -0.872,1.155 2.11,3.59 2.733,3.646 3.357,3.701 6.101,2.003 6.231,0.562 6.361,-0.879 3.944,0.677 2.223,0.523 0.502,0.367 0.369,-0.49 0,0" /></g><g
+ transform="translate(262.0864,311.4141)"
+ id="g2921"><path
+ id="path2923"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -0.677,-9.71 -5.16,-14.594 -10.862,-21.908 -3.012,-3.861 -5.017,-8.005 -6.067,-12.766 -0.653,-2.955 -2.998,-5.396 -6.187,-5.296 -3.564,0.114 -6.928,2.23 -10.487,2.862 -4.641,0.826 -9.162,0.113 -13.801,-0.206 -4.671,-0.321 -8.985,0.475 -13.127,2.686 -0.069,0.036 -6.823,4.403 -6.476,4.649 4.916,3.463 2.528,8.458 -1.511,11.371 -5.349,3.857 -10.603,10.055 -12.841,16.365 -1.254,3.539 12.776,0.416 14.245,-0.23 4.956,-2.182 2.184,-9.504 1.431,-13.29 1.562,1.982 1.916,4.999 2.747,7.338 1.244,3.501 2.716,7.061 5.389,9.728 3.419,3.415 7.537,5.966 11.82,8.15 5.051,2.578 9.983,4.83 15.651,5.436 4.155,0.441 7.829,-0.055 11.682,-1.675 0.621,-0.264 4.807,-2.148 5.815,-1.898 -3.393,1.208 -10.569,4.446 -9.555,9.164 0.338,1.567 5.62,14.545 7.748,11.455 3.797,-5.514 5.733,-13.408 5.718,-20.002 C -8.68,6.954 -6.037,2.089 -0.348,4.037 -0.384,4.025 0.028,0.412 0,0 m -7.915,12.749 c -0.384,6.973 -1.874,15.488 -6.598,21.016 -3.544,4.145 -9.07,-7.937 -10.206,-9.767 -1.394,-2.243 -2.732,-5.033 -4.849,-6.672 -2.974,-2.305 -6.495,-2.395 -9.931,-3.535 -5.446,-1.808 -10.797,-4.617 -15.089,-8.453 -5.267,-4.707 -10.258,-4.165 -16.839,-3.686 -2.739,0.201 -12.979,2.429 -11.151,-3.382 2.213,-7.042 7.856,-12.98 13.52,-17.421 5.355,-4.199 2.976,-9.016 -2.615,-11.62 5.778,-7.908 17.443,-9.004 26.273,-7.474 4.815,0.834 9.372,1.096 14.18,-0.054 3.57,-0.851 7.831,-4.442 11.607,-2.531 3.541,1.728 3.129,5.806 4.139,9.031 1.422,4.543 4.234,8.652 7.594,11.977 C -1,-13.011 6.4,-3.065 3.032,7.352 -2.708,4.155 -8.107,5.663 -7.915,12.749" /></g><g
+ transform="translate(240.9268,311.4058)"
+ id="g2925"><path
+ id="path2927"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0.288,1.418 -3.452,2.332 -4.038,2.113 -0.587,-0.218 -2.331,-2.933 -1.827,-4.29 0.502,-1.358 2.012,1.088 3.634,1.692 C -0.613,0.115 -0.123,-0.601 0,0" /></g><g
+ transform="translate(215.5869,289.4712)"
+ id="g2929"><path
+ id="path2931"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.181,0.136 0.365,0.415 0.297,0.387 0.506,0.79 0.527,1.284 0.305,1.718 -0.059,2.433 -0.937,2.72 -1.652,2.354 -2.369,1.99 -2.652,1.112 -2.286,0.396 -2.284,0.392 -2.281,0.389 -2.279,0.385 -2.35,0.414 -2.423,0.425 -2.492,0.459 -3.995,1.189 -4.734,2.865 -4.337,4.425 -4.47,4.253 -4.588,4.066 -4.688,3.865 -5.445,2.307 -4.793,0.431 -3.236,-0.324 c 0.9,-0.439 1.9,-0.392 2.733,0.017 0.058,0.021 0.117,0.039 0.174,0.068 C -0.204,-0.175 -0.098,-0.091 0,0" /></g><g
+ transform="translate(201.3467,286.79)"
+ id="g2933"><path
+ id="path2935"
+ style="fill:#f6ad44;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0,0 4.649,-8.012 17.098,-5.728 0,0 8.646,2.306 13.661,-2.572 -1.023,0.995 -1.912,2.037 -3.159,2.8 -3.88,2.374 -8.425,1.586 -12.74,2.007 C 11.729,-3.186 8.814,-2.129 6.012,-0.74 4.997,-0.237 -0.543,3.323 0,0" /></g><g
+ transform="translate(254.4907,311.8257)"
+ id="g2937"><path
+ id="path2939"
+ style="fill:#f6ad44;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c 0,0 2.953,-8.739 -7.189,-15.087 0,0 -5.659,-4.735 -5.408,-11.635 -0.051,1.406 -0.211,2.757 0.021,4.15 0.385,2.318 0.466,4.79 1.388,6.944 0.89,2.08 2.775,3.179 4.241,4.747 1.653,1.768 2.733,4.008 3.344,6.417 C -3.336,-3.416 -1.854,2.765 0,0" /></g></g></g><g
+ id="g2941"><g
+ id="g2943"><g
+ id="g2949"><g
+ id="g2951"><path
+ id="path2959"
+ style="fill:url(#linearGradient2921);stroke:none"
+ d="m 219.217,320.557 c -8.361,-3.819 -11.245,-7.843 -10.12,-9.455 l 0,0 c 1.232,-1.763 6.564,-3.975 10.462,-4.716 l 0,0 c 0.437,-0.084 0.615,0.54 0.788,1.164 l 0,0 c 0.175,0.625 0.345,1.251 0.769,1.164 l 0,0 c 2.49,-0.513 5.947,-3.826 7.154,-4.78 l 0,0 c 0.615,-0.486 1.778,-1.525 2.335,-1.306 l 0,0 c 0.448,0.18 -0.171,1.544 -0.27,2.567 l 0,0 c -0.129,1.34 -1.24,6.507 -0.799,8.453 l 0,0 c 0.16,0.711 0.67,0.604 1.192,0.498 l 0,0 c 0.53,-0.109 1.071,-0.219 1.268,0.538 l 0,0 c 1.018,3.921 1.895,7.666 0.749,9.206 l 0,0 c -0.292,0.393 -0.865,0.571 -1.664,0.571 l 0,0 c -2.414,0 -6.885,-1.63 -11.864,-3.904" /></g></g></g></g><g
+ id="g2961"><g
+ clip-path="url(#clipPath2965)"
+ id="g2963"><g
+ transform="translate(250.4868,321.0234)"
+ id="g3025"><path
+ id="path3027"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 0,0 c -1.953,-3.398 -2.12,-7.398 -9.162,-12.051 -1.332,-0.88 -2.181,-1.787 -2.848,-3.422 -3.239,-7.934 2.034,-25.066 2.768,-24.956 1.152,0.325 -5.615,14.689 -1.916,21.923 0.592,1.157 1.516,2.073 2.438,2.968 0.984,0.957 1.551,1.517 2.647,2.326 1.162,0.856 2.677,2.038 4.037,1.981 -0.91,0.038 -2.517,0.43 -3.432,0.469 0.717,1.218 2.645,3.181 3.182,4.031 1.328,2.1 1.92,4.192 2.286,6.731" /></g><g
+ transform="translate(245.6221,311.167)"
+ id="g3029"><path
+ id="path3031"
+ style="fill:#5c5d5d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="M 0,0 C 0.858,0.275 1.874,0.881 2.632,1.389 3.313,1.846 4.006,2.344 4.25,3.106 3.887,1.972 2.931,0.588 1.994,-0.055 1.517,-0.382 1.029,-0.701 0.617,-1.111 0.216,-1.509 -0.117,-1.482 -0.718,-1.59 -0.766,-1.337 -0.75,-1.075 -0.75,-0.813" /></g></g></g></g></g></svg> \ 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
--- /dev/null
+++ b/src/images/legacy-version.png
Binary files 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg4071"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ xml:space="preserve"
+ width="119.26471"
+ height="145.2471"
+ viewBox="0 0 119.26471 145.2471"
+ sodipodi:docname="FOXYlogo.svg"><metadata
+ id="metadata4077"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs4075"><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath4085"><path
+ d="m 185.797,300.527 c 0,-22.396 18.681,-40.552 41.725,-40.552 l 0,0 c 23.044,0 41.725,18.156 41.725,40.552 l 0,0 c 0,22.397 -18.681,40.554 -41.725,40.554 l 0,0 c -23.044,0 -41.725,-18.157 -41.725,-40.554"
+ id="path4087"
+ inkscape:connector-curvature="0" /></clipPath><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-7.9998322,76.665482,76.665482,7.9998322,231.64795,260.98437)"
+ spreadMethod="pad"
+ id="linearGradient4093"><stop
+ style="stop-opacity:1;stop-color:#f7931d"
+ offset="0"
+ id="stop4095" /><stop
+ style="stop-opacity:1;stop-color:#fbb16b"
+ offset="1"
+ id="stop4097" /></linearGradient><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath4105"><path
+ d="m 211.943,333.94 c -18.988,-8.854 -27.403,-30.99 -18.798,-49.443 l 0,0 c 8.604,-18.453 30.971,-26.234 49.958,-17.381 l 0,0 c 18.986,8.854 27.401,30.99 18.797,49.442 l 0,0 c -6.194,13.284 -19.518,21.037 -33.58,21.037 l 0,0 c -5.473,0 -11.058,-1.175 -16.377,-3.655"
+ id="path4107"
+ inkscape:connector-curvature="0" /></clipPath><radialGradient
+ fx="0"
+ fy="0"
+ cx="0"
+ cy="0"
+ r="1"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(50.959351,23.761917,23.092804,-49.525528,208.03369,303.08252)"
+ spreadMethod="pad"
+ id="radialGradient4113"><stop
+ style="stop-opacity:1;stop-color:#e9e9ea"
+ offset="0"
+ id="stop4115" /><stop
+ style="stop-opacity:1;stop-color:#e9e9ea"
+ offset="0.70231894"
+ id="stop4117" /><stop
+ style="stop-opacity:1;stop-color:#7e8082"
+ offset="1"
+ id="stop4119" /></radialGradient><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath4127"><path
+ d="M 0,576 576,576 576,0 0,0 0,576 Z"
+ id="path4129"
+ inkscape:connector-curvature="0" /></clipPath><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath4179"><path
+ d="m 219.217,320.557 c -8.361,-3.819 -11.245,-7.843 -10.12,-9.455 l 0,0 c 1.232,-1.763 6.564,-3.975 10.462,-4.716 l 0,0 c 0.437,-0.084 0.615,0.54 0.788,1.164 l 0,0 c 0.175,0.625 0.345,1.251 0.769,1.164 l 0,0 c 2.49,-0.513 5.947,-3.826 7.154,-4.78 l 0,0 c 0.615,-0.486 1.778,-1.525 2.335,-1.306 l 0,0 c 0.448,0.18 -0.171,1.544 -0.27,2.567 l 0,0 c -0.129,1.34 -1.24,6.507 -0.799,8.453 l 0,0 c 0.16,0.711 0.67,0.604 1.192,0.498 l 0,0 c 0.53,-0.109 1.071,-0.219 1.268,0.538 l 0,0 c 1.018,3.921 1.895,7.666 0.749,9.206 l 0,0 c -0.292,0.393 -0.865,0.571 -1.664,0.571 l 0,0 c -2.414,0 -6.885,-1.63 -11.864,-3.904"
+ id="path4181"
+ inkscape:connector-curvature="0" /></clipPath><linearGradient
+ x1="0"
+ y1="0"
+ x2="1"
+ y2="0"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(34.282654,-15.727585,-15.727585,-34.282654,213.80908,320.43213)"
+ spreadMethod="pad"
+ id="linearGradient4187"><stop
+ style="stop-opacity:1;stop-color:#faa54f"
+ offset="0"
+ id="stop4189" /><stop
+ style="stop-opacity:1;stop-color:#fbb16b"
+ offset="1"
+ id="stop4191" /></linearGradient><clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath4199"><path
+ d="M 0,576 576,576 576,0 0,0 0,576 Z"
+ id="path4201"
+ inkscape:connector-curvature="0" /></clipPath></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1440"
+ inkscape:window-height="810"
+ id="namedview4073"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ showguides="true"
+ inkscape:zoom="2"
+ inkscape:cx="135.88583"
+ inkscape:cy="72.697884"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g4079" /><g
+ id="g4079"
+ inkscape:groupmode="layer"
+ inkscape:label="FOXYlogo"
+ transform="matrix(1.25,0,0,-1.25,-224.11416,432.54923)"><g
+ id="g4081"><g
+ id="g4083"
+ clip-path="url(#clipPath4085)"><g
+ id="g4089"><g
+ id="g4091"><path
+ d="m 185.797,300.527 c 0,-22.396 18.681,-40.552 41.725,-40.552 l 0,0 c 23.044,0 41.725,18.156 41.725,40.552 l 0,0 c 0,22.397 -18.681,40.554 -41.725,40.554 l 0,0 c -23.044,0 -41.725,-18.157 -41.725,-40.554"
+ style="fill:url(#linearGradient4093);stroke:none"
+ id="path4099"
+ inkscape:connector-curvature="0" /></g></g></g></g><g
+ id="g4101"><g
+ id="g4103"
+ clip-path="url(#clipPath4105)"><g
+ id="g4109"><g
+ id="g4111"><path
+ d="m 211.943,333.94 c -18.988,-8.854 -27.403,-30.99 -18.798,-49.443 l 0,0 c 8.604,-18.453 30.971,-26.234 49.958,-17.381 l 0,0 c 18.986,8.854 27.401,30.99 18.797,49.442 l 0,0 c -6.194,13.284 -19.518,21.037 -33.58,21.037 l 0,0 c -5.473,0 -11.058,-1.175 -16.377,-3.655"
+ style="fill:url(#radialGradient4113);stroke:none"
+ id="path4121"
+ inkscape:connector-curvature="0" /></g></g></g></g><g
+ id="g4123"><g
+ id="g4125"
+ clip-path="url(#clipPath4127)"><g
+ id="g4131"
+ transform="translate(261.9004,316.5576)"><path
+ d="m 0,0 c 8.604,-18.451 0.188,-40.588 -18.798,-49.441 -18.986,-8.854 -41.353,-1.072 -49.958,17.38 -8.605,18.454 -0.189,40.589 18.798,49.443 C -30.972,26.235 -8.605,18.454 0,0 Z"
+ style="fill:none;stroke:#ffffff;stroke-width:1.76999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4133"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4135"
+ transform="translate(253.9063,323.9209)"><path
+ d="m 0,0 c -0.38,6.903 -1.854,15.334 -6.533,20.806 -3.507,4.103 -8.978,-7.857 -10.103,-9.669 -1.38,-2.221 -2.706,-4.984 -4.8,-6.605 -2.945,-2.281 -6.431,-2.372 -9.833,-3.499 -5.391,-1.789 -10.688,-4.57 -14.938,-8.369 -5.214,-4.66 -10.154,-4.124 -16.67,-3.649 -2.711,0.199 -12.849,2.406 -11.04,-3.348 2.192,-6.973 7.778,-12.852 13.386,-17.248 5.302,-4.158 2.946,-8.926 -2.589,-11.503 5.72,-7.829 17.267,-8.914 26.01,-7.4 4.766,0.826 9.278,1.088 14.039,-0.052 3.533,-0.845 7.752,-4.399 11.49,-2.507 3.506,1.711 3.098,5.748 4.096,8.94 1.409,4.499 4.193,8.567 7.519,11.858 6.812,6.743 14.138,16.589 10.803,26.9 C 5.155,-8.506 -0.189,-7.015 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4137"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4139"
+ transform="translate(215.7368,321.6992)"><path
+ d="m 0,0 c 0,0 11.985,5.765 21.784,5.146 0,0 7.573,-2.107 11.927,-4.1 0,0 6.191,-4.964 7.201,-8.663 0,0 2.938,-8.511 -7.186,-19.298 0,0 -5.095,-6.523 -6.302,-12.757 0,0 -0.092,-4.189 -2.53,-5.651 0,0 -4.019,-1.141 -6.971,0.514 -2.393,0.678 -4.844,1.214 -7.298,1.626 -5.684,0.954 -11.46,0.006 -17.159,1.061 -4.659,0.859 -10.534,2.956 -11.796,8.135 0,0 -2.888,2.565 0.798,13.578 0,0 2.001,12.357 17.532,20.409"
+ style="fill:#f7931d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4141"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4143"
+ transform="translate(196.5059,295.9878)"><path
+ d="m 0,0 c 2.679,-1.011 4.179,-2.636 10.782,-1.915 3.055,0.334 5.723,3.269 8.877,2.476 8.535,-2.152 22.752,-17.621 22.222,-18.163 -0.955,-0.761 -11.555,14.2 -19.136,10.842 -1.809,-0.8 -4.551,-2.507 -6.534,-2.023 -0.958,0.237 -1.792,0.686 -2.52,1.358 -0.96,0.89 -1.642,2.731 -2.91,3.235 -1.728,0.688 -3.12,0.784 -4.914,1.167 -1.929,0.412 -3.938,1.262 -5.436,2.518"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4145"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4147"
+ transform="translate(243.0967,279.8789)"><path
+ d="m 0,0 c 0.714,-1.26 -0.438,-4.142 -0.984,-4.45 -0.544,-0.309 -3.745,0.106 -4.459,1.367 -0.713,1.258 0.327,2.263 1.83,3.114 C -2.109,0.883 -0.713,1.259 0,0"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4149"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4151"
+ transform="translate(206.8965,302.1777)"><path
+ d="M 0,0 C -0.872,1.155 2.11,3.59 2.733,3.646 3.357,3.701 6.101,2.003 6.231,0.562 6.361,-0.879 3.944,0.677 2.223,0.523 0.502,0.367 0.369,-0.49 0,0"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4153"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4155"
+ transform="translate(262.0864,311.4141)"><path
+ d="m 0,0 c -0.677,-9.71 -5.16,-14.594 -10.862,-21.908 -3.012,-3.861 -5.017,-8.005 -6.067,-12.766 -0.653,-2.955 -2.998,-5.396 -6.187,-5.296 -3.564,0.114 -6.928,2.23 -10.487,2.862 -4.641,0.826 -9.162,0.113 -13.801,-0.206 -4.671,-0.321 -8.985,0.475 -13.127,2.686 -0.069,0.036 -6.823,4.403 -6.476,4.649 4.916,3.463 2.528,8.458 -1.511,11.371 -5.349,3.857 -10.603,10.055 -12.841,16.365 -1.254,3.539 12.776,0.416 14.245,-0.23 4.956,-2.182 2.184,-9.504 1.431,-13.29 1.562,1.982 1.916,4.999 2.747,7.338 1.244,3.501 2.716,7.061 5.389,9.728 3.419,3.415 7.537,5.966 11.82,8.15 5.051,2.578 9.983,4.83 15.651,5.436 4.155,0.441 7.829,-0.055 11.682,-1.675 0.621,-0.264 4.807,-2.148 5.815,-1.898 -3.393,1.208 -10.569,4.446 -9.555,9.164 0.338,1.567 5.62,14.545 7.748,11.455 3.797,-5.514 5.733,-13.408 5.718,-20.002 C -8.68,6.954 -6.037,2.089 -0.348,4.037 -0.384,4.025 0.028,0.412 0,0 m -7.915,12.749 c -0.384,6.973 -1.874,15.488 -6.598,21.016 -3.544,4.145 -9.07,-7.937 -10.206,-9.767 -1.394,-2.243 -2.732,-5.033 -4.849,-6.672 -2.974,-2.305 -6.495,-2.395 -9.931,-3.535 -5.446,-1.808 -10.797,-4.617 -15.089,-8.453 -5.267,-4.707 -10.258,-4.165 -16.839,-3.686 -2.739,0.201 -12.979,2.429 -11.151,-3.382 2.213,-7.042 7.856,-12.98 13.52,-17.421 5.355,-4.199 2.976,-9.016 -2.615,-11.62 5.778,-7.908 17.443,-9.004 26.273,-7.474 4.815,0.834 9.372,1.096 14.18,-0.054 3.57,-0.851 7.831,-4.442 11.607,-2.531 3.541,1.728 3.129,5.806 4.139,9.031 1.422,4.543 4.234,8.652 7.594,11.977 C -1,-13.011 6.4,-3.065 3.032,7.352 -2.708,4.155 -8.107,5.663 -7.915,12.749"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4157"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4159"
+ transform="translate(240.9268,311.4058)"><path
+ d="m 0,0 c 0.288,1.418 -3.452,2.332 -4.038,2.113 -0.587,-0.218 -2.331,-2.933 -1.827,-4.29 0.502,-1.358 2.012,1.088 3.634,1.692 C -0.613,0.115 -0.123,-0.601 0,0"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4161"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4163"
+ transform="translate(215.5869,289.4712)"><path
+ d="M 0,0 C 0.181,0.136 0.365,0.415 0.297,0.387 0.506,0.79 0.527,1.284 0.305,1.718 -0.059,2.433 -0.937,2.72 -1.652,2.354 -2.369,1.99 -2.652,1.112 -2.286,0.396 -2.284,0.392 -2.281,0.389 -2.279,0.385 -2.35,0.414 -2.423,0.425 -2.492,0.459 -3.995,1.189 -4.734,2.865 -4.337,4.425 -4.47,4.253 -4.588,4.066 -4.688,3.865 -5.445,2.307 -4.793,0.431 -3.236,-0.324 c 0.9,-0.439 1.9,-0.392 2.733,0.017 0.058,0.021 0.117,0.039 0.174,0.068 C -0.204,-0.175 -0.098,-0.091 0,0"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4165"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4167"
+ transform="translate(201.3467,286.79)"><path
+ d="m 0,0 c 0,0 4.649,-8.012 17.098,-5.728 0,0 8.646,2.306 13.661,-2.572 -1.023,0.995 -1.912,2.037 -3.159,2.8 -3.88,2.374 -8.425,1.586 -12.74,2.007 C 11.729,-3.186 8.814,-2.129 6.012,-0.74 4.997,-0.237 -0.543,3.323 0,0"
+ style="fill:#f5aa35;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4169"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4171"
+ transform="translate(254.4907,311.8257)"><path
+ d="m 0,0 c 0,0 2.953,-8.739 -7.189,-15.087 0,0 -5.659,-4.735 -5.408,-11.635 -0.051,1.406 -0.211,2.757 0.021,4.15 0.385,2.318 0.466,4.79 1.388,6.944 0.89,2.08 2.775,3.179 4.241,4.747 1.653,1.768 2.733,4.008 3.344,6.417 C -3.336,-3.416 -1.854,2.765 0,0"
+ style="fill:#f5aa35;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4173"
+ inkscape:connector-curvature="0" /></g></g></g><g
+ id="g4175"><g
+ id="g4177"
+ clip-path="url(#clipPath4179)"><g
+ id="g4183"><g
+ id="g4185"><path
+ d="m 219.217,320.557 c -8.361,-3.819 -11.245,-7.843 -10.12,-9.455 l 0,0 c 1.232,-1.763 6.564,-3.975 10.462,-4.716 l 0,0 c 0.437,-0.084 0.615,0.54 0.788,1.164 l 0,0 c 0.175,0.625 0.345,1.251 0.769,1.164 l 0,0 c 2.49,-0.513 5.947,-3.826 7.154,-4.78 l 0,0 c 0.615,-0.486 1.778,-1.525 2.335,-1.306 l 0,0 c 0.448,0.18 -0.171,1.544 -0.27,2.567 l 0,0 c -0.129,1.34 -1.24,6.507 -0.799,8.453 l 0,0 c 0.16,0.711 0.67,0.604 1.192,0.498 l 0,0 c 0.53,-0.109 1.071,-0.219 1.268,0.538 l 0,0 c 1.018,3.921 1.895,7.666 0.749,9.206 l 0,0 c -0.292,0.393 -0.865,0.571 -1.664,0.571 l 0,0 c -2.414,0 -6.885,-1.63 -11.864,-3.904"
+ style="fill:url(#linearGradient4187);stroke:none"
+ id="path4193"
+ inkscape:connector-curvature="0" /></g></g></g></g><g
+ id="g4195"><g
+ id="g4197"
+ clip-path="url(#clipPath4199)"><g
+ id="g4203"
+ transform="translate(190.7241,261.791)"><path
+ d="m 0,0 13.065,0 0,-5.588 -5.924,0 0,-2.017 5.2,0 0,-5.072 -5.2,0 0,-6.83 -7.141,0 L 0,0 Z"
+ style="fill:#f7931d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4205"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4207"
+ transform="translate(214.5513,251.8828)"><path
+ d="m 0,0 c 0,-2.226 1.501,-3.752 3.648,-3.752 2.147,0 3.648,1.526 3.648,3.752 0,2.069 -1.397,3.803 -3.648,3.803 C 1.397,3.803 0,2.069 0,0 m 14.437,0.104 c 0,-6.261 -4.838,-10.013 -10.789,-10.013 -5.95,0 -10.789,3.752 -10.789,10.013 0,6.131 4.865,10.115 10.789,10.115 5.925,0 10.789,-3.984 10.789,-10.115"
+ style="fill:#f7931d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4209"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4211"
+ transform="translate(251.9355,261.791)"><path
+ d="m 0,0 -6.105,-9.132 6.959,-10.375 -8.072,0 -2.225,4.14 c -0.207,0.388 -0.336,0.776 -0.414,1.189 l -0.207,0 c -0.129,-0.465 -0.311,-0.905 -0.543,-1.344 l -2.174,-3.985 -8.072,0 6.831,10.375 -6.132,9.132 7.994,0 1.449,-2.768 c 0.285,-0.517 0.44,-1.06 0.595,-1.604 l 0.156,0 c 0.129,0.569 0.362,1.087 0.621,1.604 L -7.994,0 0,0 Z"
+ style="fill:#f7931d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4213"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4215"
+ transform="translate(262.3101,261.791)"><path
+ d="m 0,0 2.173,-5.097 0.207,0 c 0.207,0.621 0.388,1.243 0.647,1.837 L 4.424,0 l 7.969,0 -6.676,-11.099 0,-8.408 -6.933,0 0,8.408 L -8.021,0 0,0 Z"
+ style="fill:#f7931d;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4217"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4219"
+ transform="translate(196.2456,236.8574)"><path
+ d="m 0,0 -2.792,0 0,-2.57 2.803,0 c 0.543,0 0.984,0.105 1.323,0.317 0.369,0.227 0.554,0.535 0.554,0.928 C 1.888,-0.441 1.258,0 0,0 M 1.892,-3.091 C 1.333,-3.4 0.64,-3.555 -0.19,-3.555 l -2.602,0 0,-3.133 -1.086,0 0,7.672 3.813,0 c 2.036,0 3.054,-0.765 3.054,-2.297 0,-0.786 -0.365,-1.377 -1.097,-1.778"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4221"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4223"
+ transform="translate(196.2456,236.8574)"><path
+ d="m 0,0 -2.792,0 0,-2.57 2.803,0 c 0.543,0 0.984,0.105 1.323,0.317 0.369,0.227 0.554,0.535 0.554,0.928 C 1.888,-0.441 1.258,0 0,0 Z M 1.892,-3.091 C 1.333,-3.4 0.64,-3.555 -0.19,-3.555 l -2.602,0 0,-3.133 -1.086,0 0,7.672 3.813,0 c 2.036,0 3.054,-0.765 3.054,-2.297 0,-0.786 -0.365,-1.377 -1.097,-1.778 z"
+ style="fill:none;stroke:#535454;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4225"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4227"
+ transform="translate(213.5576,236.8574)"><path
+ d="m 0,0 -2.888,0 0,-2.477 2.707,0 c 0.604,0 1.083,0.102 1.438,0.306 0.393,0.226 0.589,0.558 0.589,0.995 C 1.846,-0.392 1.23,0 0,0 m 1.935,-2.942 c 0.586,-0.37 0.88,-0.928 0.88,-1.675 l 0,-2.071 -1.086,0 0,2.036 c 0,0.784 -0.619,1.175 -1.856,1.175 l -2.761,0 0,-3.211 -1.086,0 0,7.672 3.951,0 c 1.97,0 2.955,-0.709 2.955,-2.127 0,-0.807 -0.332,-1.407 -0.997,-1.799"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4229"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4231"
+ transform="translate(213.5576,236.8574)"><path
+ d="m 0,0 -2.888,0 0,-2.477 2.707,0 c 0.604,0 1.083,0.102 1.438,0.306 0.393,0.226 0.589,0.558 0.589,0.995 C 1.846,-0.392 1.23,0 0,0 Z m 1.935,-2.942 c 0.586,-0.37 0.88,-0.928 0.88,-1.675 l 0,-2.071 -1.086,0 0,2.036 c 0,0.784 -0.619,1.175 -1.856,1.175 l -2.761,0 0,-3.211 -1.086,0 0,7.672 3.951,0 c 1.97,0 2.955,-0.709 2.955,-2.127 0,-0.807 -0.332,-1.407 -0.997,-1.799 z"
+ style="fill:none;stroke:#535454;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4233"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4235"
+ transform="translate(233.5015,236.0957)"><path
+ d="m 0,0 c -0.565,0.487 -1.365,0.73 -2.397,0.73 -1.034,0 -1.829,-0.245 -2.387,-0.735 -0.558,-0.491 -0.837,-1.188 -0.837,-2.095 0,-0.905 0.283,-1.618 0.849,-2.138 0.565,-0.521 1.356,-0.782 2.375,-0.782 1.032,0 1.832,0.259 2.397,0.776 0.565,0.516 0.848,1.231 0.848,2.144 0,0.913 -0.283,1.614 -0.848,2.1 m 0.763,-4.951 c -0.781,-0.702 -1.835,-1.053 -3.163,-1.053 -1.335,0 -2.389,0.349 -3.163,1.047 -0.772,0.698 -1.159,1.65 -1.159,2.857 0,1.208 0.396,2.151 1.188,2.83 0.762,0.657 1.806,0.985 3.134,0.985 1.328,0 2.377,-0.328 3.146,-0.985 0.792,-0.679 1.188,-1.622 1.188,-2.83 0,-1.199 -0.391,-2.15 -1.171,-2.851"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4237"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4239"
+ transform="translate(233.5015,236.0957)"><path
+ d="m 0,0 c -0.565,0.487 -1.365,0.73 -2.397,0.73 -1.034,0 -1.829,-0.245 -2.387,-0.735 -0.558,-0.491 -0.837,-1.188 -0.837,-2.095 0,-0.905 0.283,-1.618 0.849,-2.138 0.565,-0.521 1.356,-0.782 2.375,-0.782 1.032,0 1.832,0.259 2.397,0.776 0.565,0.516 0.848,1.231 0.848,2.144 0,0.913 -0.283,1.614 -0.848,2.1 z m 0.763,-4.951 c -0.781,-0.702 -1.835,-1.053 -3.163,-1.053 -1.335,0 -2.389,0.349 -3.163,1.047 -0.772,0.698 -1.159,1.65 -1.159,2.857 0,1.208 0.396,2.151 1.188,2.83 0.762,0.657 1.806,0.985 3.134,0.985 1.328,0 2.377,-0.328 3.146,-0.985 0.792,-0.679 1.188,-1.622 1.188,-2.83 0,-1.199 -0.391,-2.15 -1.171,-2.851 z"
+ style="fill:none;stroke:#535454;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4241"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4243"
+ transform="translate(251.9077,230.1699)"><path
+ d="M 0,0 -2.891,3.174 -5.832,0 l -1.417,0 3.63,3.93 -3.456,3.742 1.434,0 2.742,-2.978 2.759,2.978 1.416,0 L -2.175,3.936 1.399,0 0,0 Z"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4245"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4247"
+ transform="translate(251.9077,230.1699)"><path
+ d="M 0,0 -2.891,3.174 -5.832,0 l -1.417,0 3.63,3.93 -3.456,3.742 1.434,0 2.742,-2.978 2.759,2.978 1.416,0 L -2.175,3.936 1.399,0 0,0 Z"
+ style="fill:none;stroke:#535454;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4249"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4251"
+ transform="translate(266.6904,233.792)"><path
+ d="m 0,0 0,-3.622 -1.086,0 0,3.621 -3.977,4.051 1.446,0 3.062,-3.108 3.073,3.108 1.429,0 L 0,0 Z"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4253"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4255"
+ transform="translate(266.6904,233.792)"><path
+ d="m 0,0 0,-3.622 -1.086,0 0,3.621 -3.977,4.051 1.446,0 3.062,-3.108 3.073,3.108 1.429,0 L 0,0 Z"
+ style="fill:none;stroke:#535454;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path4257"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4259"
+ transform="translate(250.4868,321.0234)"><path
+ d="m 0,0 c -1.953,-3.398 -2.12,-7.398 -9.162,-12.051 -1.332,-0.88 -2.181,-1.787 -2.848,-3.422 -3.239,-7.934 2.034,-25.066 2.768,-24.956 1.152,0.325 -5.615,14.689 -1.916,21.923 0.592,1.157 1.516,2.073 2.438,2.968 0.984,0.957 1.551,1.517 2.647,2.326 1.162,0.856 2.677,2.038 4.037,1.981 -0.91,0.038 -2.517,0.43 -3.432,0.469 0.717,1.218 2.645,3.181 3.182,4.031 1.328,2.1 1.92,4.192 2.286,6.731"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4261"
+ inkscape:connector-curvature="0" /></g><g
+ id="g4263"
+ transform="translate(245.6221,311.167)"><path
+ d="M 0,0 C 0.858,0.275 1.874,0.881 2.632,1.389 3.313,1.846 4.006,2.344 4.25,3.106 3.887,1.972 2.931,0.588 1.994,-0.055 1.517,-0.382 1.029,-0.701 0.617,-1.111 0.216,-1.509 -0.117,-1.482 -0.718,-1.59 -0.766,-1.337 -0.75,-1.075 -0.75,-0.813"
+ style="fill:#535454;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path4265"
+ inkscape:connector-curvature="0" /></g></g></g></g></svg> \ 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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="import">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ i.fa, i.fa:hover { color: inherit; font-size: inherit; }
+ i.fa.fa-eye, i.fa.fa-eye:hover {
+ font-size: 1.5em;
+ vertical-align: middle;
+ }
+ label {
+ display: inline-block;
+ }
+ textarea {
+ height: 10em;
+ }
+ div.tooltip hr {
+ color: white;
+ }
+ div.tooltip h3 {
+ margin: 0 0 .2em 0;
+ }
+ .align-right {
+ text-align: right;
+ }
+ .no-margin {
+ margin: 0;
+ }
+ #completeFormatTooltip {
+ width: 40rem;
+ }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="importProxyList"></div>
+
+ <!-- spinner -->
+ <div class="spinner on"><i class="fa fa-refresh fa-spin"></i></div>
+
+ <!-- main -->
+ <div class="prime">
+ <div class="tooltip">
+ <span data-i18n="pasteList"></span> <span data-i18n="formats"></span> <span data-i18n="simple"></span>
+ <i class="fa fa-info-circle"></i>
+ <div class="tooltiptext bottom table">
+ <div class="tooltiptable">
+ <p class="no-margin" data-i18n="simpleFormat"></p>
+ <p class="monospace"><span data-i18n="ipPort"></span><br><span data-i18n="ipPort"></span><br><span data-i18n="ipPortUsernamePassword"></span></p>
+ </div>
+ <hr>
+ <div class="tooltiptablefooter">
+ <h3 data-i18n="examples"></h3>
+ 78.205.12.1:6001<br>12.999.51.81:3128<br>foobar.com:12001<br>foobar.com:3128:kyleReese:hunterKiller
+ <p><a data-i18n="help" target="_blank" href="https://github.com/foxyproxy/firefox-extension/wiki/Import-Proxy-List"></a></p>
+ </div>
+ </div>
+ </div>
+ /
+ <div class="tooltip">
+ <span data-i18n="complete"></span>
+ <i class="fa fa-info-circle"></i>
+ <div id="completeFormatTooltip" class="tooltiptext bottom table">
+ <div class="tooltiptable">
+ <p data-i18n="completeFormat"></p>
+ <h3 data-i18n="examples"></h3>
+ <p class="monospace">* proxy://78.205.12.1:666<br>
+ * proxy://johnConnor:hunterKiller@192.168.100.9:5192?color=663300<br>
+ * socks://12.999.51.81?title=China&cc=CN&country=中国&proxyDns=false<br>
+ * socks://12.999.51.81:331?patternExcludesIntranet=false<br>
+ * https://192.168.100.9:5192?patternIncludesAll=false<br>
+ * ssl://kyleReese:passw0rd@78.205.12.1:21?color=ff00bc&title=work%20proxy</p>
+ </div>
+ <hr>
+ <div class="tooltiptablefooter">
+ <a data-i18n="help" target="_blank" href="https://github.com/foxyproxy/firefox-extension/wiki/Import-Proxy-List"></a>
+ </div>
+ </div>
+ </div>
+ <br><br>
+
+ <textarea id="proxyList"></textarea>
+
+ <label data-i18n="overwriteProxies"></label>
+ <input id="overwrite" type="checkbox" class="switch"><label for="overwrite"></label>
+ <div class="tooltip">
+ <i class="fa fa-info-circle"></i>
+ <div class="tooltiptext center bottom">
+ <div class="tooltiptable">
+ <span data-i18n="overwritProxiesHelp1"></span>
+ </div>
+ <br>
+ <div class="tooltiptable">
+ <span data-i18n="overwritProxiesHelp2"></span>
+ </div>
+ </div>
+ </div>
+ <br><br>
+ <button id="import" type="button" class="button" data-i18n="import"><i class="fa fa-download"></i> </button>
+
+ <div class="align-right">
+ <button type="button" class="button" data-i18n="back">&#x25c1; </button>
+ </div>
+ </div>
+
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/import-proxy-list.js"></script>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="import">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ i.fa, i.fa:hover { color: inherit; font-size: inherit; }
+ i.fa.fa-eye, i.fa.fa-eye:hover {
+ font-size: 1.5em;
+ vertical-align: middle;
+ }
+ button.plain {
+ vertical-align: text-bottom;
+ }
+ label {
+ display: inline-block;
+ min-width: 5em;
+ }
+ input[type="text"], input[type="password"] {
+ display: inline-block;
+ width: 10em;
+ height: auto;
+ padding: 0.1em
+ }
+ input#username {
+ margin-right: 1em;
+ }
+ fieldset {
+ margin-bottom: 1.5em;
+ }
+
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="import"></div>
+
+ <!-- spinner -->
+ <div class="spinner on"><i class="fa fa-refresh fa-spin"></i></div>
+
+ <!-- main -->
+ <div class="prime">
+
+ <fieldset>
+ <legend>Import VPN/Proxies from FoxyProxy Purchase</legend>
+ <p>If you have a paid account with <a href="https://getfoxyproxy.org/order/" target="_blank">FoxyProxy</a>, you can import your proxies here.</p>
+
+ <label>Username</label>
+ <input id="username" type="text" spellcheck="false" placeholder="username">
+
+ <label>Password</label>
+ <input id="password" type="password" spellcheck="false" placeholder="*****">
+ <button type="button" class="plain" data-i18n="togglePW|title"><i class="fa fa-eye"></i></button>
+ <br>
+ <button id="importFP" type="button" class="button" data-i18n="import"><i class="fa fa-download"></i> </button>
+ </fieldset>
+
+
+ <fieldset>
+ <legend>Import Settings from FoxyProxy 6.0+</legend>
+
+ <p>FoxyProxy can use <a href="https://support.mozilla.org/products/firefox/sync" target="_blank">Firefox Sync</a> to synchronize settings across different installations of Firefox. But if you don't use Firefox Sync or want to share your settings with friends,<button type="button" class="button small" data-i18n="export"><i class="fa fa-upload"></i> </button> FoxyProxy settings. Then use this page to import those settings. By default, the file is called <i><span data-i18n="extensionName"></span>_YYYY-MM-DD.json</i>.<br><br>
+ <input type="file" id="importJson" accept=".json">
+ <label for="importJson" class="button" data-i18n="import"><i class="fa fa-download"></i> </label>
+ </fieldset>
+
+ <fieldset>
+ <legend>Import Settings from FoxyProxy 4.x and earlier</legend>
+
+ <p>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.</p>
+
+ <p>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 <i>foxyproxy.xml</i> below. You can find it in the <a href="https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data#w_how-do-i-find-my-profile" target="_blank">Firefox profile directory</a>.<br><br>
+ <input type="file" id="importXml" accept=".xml">
+ <label for="importXml" class="button" data-i18n="import"><i class="fa fa-download"></i> </label>
+ </p>
+
+ <p style="text-align: center;"><a target="_blank" href="/images/legacy-version.png"><img src="/images/legacy-version.png" style="width: 300px;"></a><br>
+ <i>(click image for larger picture)</i></p>
+ </fieldset>
+
+
+
+ <div style="text-align: right;">
+ <button type="button" class="button" data-i18n="back">&#x25c1; </button>
+ </div>
+
+ </div>
+
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/import.js"></script>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="log">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ tbody tr:hover a { color: #a0522d; }
+ tbody td { padding: 0em 0.2em; overflow-x: auto; text-overflow: ellipsis; white-space: nowrap; scrollbar-color: #ddd #f5f5f5; scrollbar-width: thin; }
+ td:nth-child(1) { max-width: 20em; }
+ td:nth-child(2) { max-width: 5em; }
+ td:nth-child(3) { width: 2em; border-radius: 5px; border: 2px solid #fff; }
+ td:nth-child(4) { max-width: 8em; }
+ td:nth-child(5) { max-width: 8em; }
+ td:nth-child(6) { max-width: 14em; }
+ .prime.warning { font-size: 0.9em; padding: 0.5em;}
+ tbody { counter-reset: n; }
+ tbody tr td:first-child::before {
+ display: inline-block;
+ color: #aaa;
+ min-width: 1.5em;
+ text-align: right;
+ vertical-align: middle;
+ margin-right: 0.4em;
+ pointer-events: none;
+ counter-increment: n;
+ content: counter(n);
+ font-size: 0.7em;
+ }
+
+ label[data-i18n] {
+ margin: 0 0.5em 0 1em;
+ }
+
+ input[type="text"] {
+ display: inline-block;
+ width: 5em;
+ padding: 0.2em;
+ height: auto;
+ }
+ i.fa, i.fa:hover { color: inherit; font-size: inherit; }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="log"></div>
+
+
+ <!-- spinner --> <!-- options, patterns, log -->
+ <div class="spinner on"><i class="fa fa-refresh fa-spin"></i></div>
+
+
+ <!-- main -->
+ <div class="prime">
+
+ <div style="text-align: right;">
+
+ <label data-i18n="log"></label>
+ <input type="checkbox" class="switch" id="onOff"><label for="onOff"></label>
+
+ <label data-i18n="logSize"></label>
+ <input id="logSize" type="text">
+
+ </div>
+
+ <div class="prime warning"><strong>Failed URLs display here, too!</strong><br>
+ A row in this log does not mean the URL successfully loaded. The log shows only how <strong>attempts</strong> 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+.
+ </div>
+
+ <h3 data-i18n="matchedURLs"></h3>
+ <div class="scroll">
+ <table>
+ <thead>
+ <tr>
+ <th data-i18n="url"></th><th data-i18n="proxyTitle"></th><th data-i18n="color"></th>
+ <th data-i18n="proxyAddress"></th><th data-i18n="matchPattern"></th><th data-i18n="whiteBlack"></th>
+ <th data-i18n="timestamp"></th>
+ </tr>
+ <!-- template -->
+ <tr class="matchedtemplate">
+ <td><a href="" target="_blank"></a></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ </tr>
+ </thead>
+
+ <tbody></tbody>
+ </table>
+ </div>
+
+ <h3 data-i18n="unmatchedURLs"></h3>
+ <div class="scroll">
+ <table>
+ <thead>
+ <tr>
+ <th data-i18n="url"></th>
+ <th data-i18n="timestamp"></th>
+ </tr>
+ <!-- template -->
+ <tr class="unmatchedtemplate">
+ <td><a href="" target="_blank"></a></td>
+ <td></td>
+ </tr>
+ </thead>
+
+ <tbody></tbody>
+ </table>
+ </div>
+
+ <div style="margin-top: 1em; text-align: right;">
+ <button type="button" data-i18n="back">&#x25c1; </button>
+ <button type="button" data-i18n="refresh"><i class="fa fa-refresh"></i> </button>
+ <button type="button" class="alert" data-i18n="clear"><i class="fa fa-trash"></i> </button>
+ </div>
+
+ </div>
+
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/log.js"></script>
+ </body>
+</html>
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",
+ "<all_urls>"
+ ],
+
+ "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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="options">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ nav { width: 14em; margin-right: 0.5em; }
+ nav i.fa { margin-right: 0.4em; font-size: 1.6em; }
+ nav a { display: block; padding: 0.2em 0.5em; border-radius: 0 50px 50px 0; cursor: pointer; }
+ nav a:hover { background-color: #ffebcd; }
+ button i.fa { font-size: 1.8em; color: #ccc; }
+ button i.fa:hover { color: #f90; }
+ button[data-i18n="help|title"] { margin-right: 0.5em; }
+ i.fa-lock { color: #ccc; font-size: 1.3em; vertical-align: middle; margin-right: 0.3em; }
+ i.fa.fa-lock.on, i.fa-lock.on:hover { color: #080; }
+
+ .rightColumn { padding: 1em; flex-grow: 1; border: 1px solid #ccc; border-radius: 5px; }
+ .pxy { opacity: 1; transition: all 0.5s ease-in-out; border: 1px solid #ccc; border-radius: 5px;
+ margin: 0 0 0.2em 0; padding: 0.2em 0.4em; }
+ .pxy.on { box-shadow: 0px 2px 4px rgba(0,0,0,0.6); }
+ .pxy:first-of-type button.up,
+ .pxy:last-of-type button.down { visibility: hidden; }
+ #accounts { max-height: 60vh; }
+ #accounts div:last-of-type { margin: 0; }
+
+ .title, .address { display: inline-block; vertical-align: middle; }
+ .title { width: 6em; margin: 0 0.5em; }
+ .address { max-width: 10em; }
+ #accounts button.small { margin-left: 0.2em; }
+
+ .scroll { max-height: 40vh; padding-right: 0.2em; }
+
+ .color{
+ width: 2em;
+ display: inline-block;
+ border-radius: 5px;
+ border: 2px solid #fff;
+ box-shadow: 0px 2px 3px #777;
+ font-size: 0.8em;
+ width: 3em;
+ }
+ </style>
+
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="optionsPage"></div>
+
+
+ <!-- spinner -->
+ <div class="spinner on"><i class="fa fa-refresh fa-spin"></i></div>
+
+ <!-- error -->
+ <div id="error" class="prime hide"><h5 data-i18n="erroNoSettings"></h5></div>
+
+
+ <!-- main -->
+ <div class="prime">
+ <div style="display: flex;">
+
+ <nav>
+ <a data-i18n="add"><i class="fa fa-plus-circle"></i></a>
+ <a data-i18n="import"><i class="fa fa-download"></i></a>
+ <a data-i18n="importProxyList""><i class="fa fa-list"></i></a>
+ <a data-i18n="export"><i class="fa fa-upload"></i></a>
+ <a data-i18n="deleteAll"><i class="fa fa-trash"></i></a>
+ <a data-i18n="deleteBrowserData"><i class="fa fa-eraser" style="margin-right: 0.1em;"></i></a>
+ <a href="https://getfoxyproxy.org/geoip/" target="_blank" data-i18n="myIP"><i class="fa fa-globe"></i></a>
+ <a data-i18n="log"><i class="fa fa-file-text"></i></a>
+ <a data-i18n="about"><i class="fa fa-info-circle"></i></a>
+ </nav>
+
+ <div id="rightColumn" class="rightColumn">
+
+ <!-- select & sync -->
+ <div id="selectAndSync" class="flex">
+ <select id="mode">
+ <option value="patterns" style="color: #f90;" data-i18n="modePatterns"></option>
+ <option value="disabled" style="color: red;" data-i18n="modeDisabled"></option>
+ </select>
+
+ <div style="text-align: right;">
+ <span data-i18n="syncSettings"></span>
+ <button type="button" class="plain" data-i18n="help|title"><i class="fa fa-question-circle"></i></button>
+ <input type="checkbox" class="switch" id="syncOnOff"><label for="syncOnOff"></label>
+ </div>
+ </div>
+
+ <!-- proxy list -->
+ <div id="accounts" class="scroll"></div>
+ <div id="help" style="text-align: center;margin: 15% auto"> <!-- try to center -->
+ <span data-i18n="noProxies"></span>
+ Please click <a data-i18n="add" href="/proxy.html"><i class="fa fa-plus-circle"></i> </a> to start.
+ </div>
+
+ <!-- template -->
+ <div class="pxy flex template">
+ <div>
+ <span class="color">&nbsp;</span>
+ <span class="title ellipsis"></span>
+ <span class="address ellipsis"></span>
+ <span class="flag hide"></span>
+
+ </div>
+ <div style="text-align: right;">
+ <i class="fa fa-lock"></i>
+ <input type="checkbox" name="onOff" class="switch" id=""><label for=""></label>
+ <button type="button" class="small" data-i18n="edit"></button>
+ <button type="button" class="small" data-i18n="patterns"></button>
+ <button type="button" class="plain" data-i18n="delete|title"><i class="fa fa-trash"></i></button>
+ <button type="button" class="plain up" data-i18n="up|title"><i class="fa fa-caret-up"></i></button>
+ <button type="button" class="plain down" data-i18n="down|title"><i class="fa fa-sort-desc"></i></button>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+ <!-- popup -->
+ <div class="popup">
+ <div>
+ <h3></h3>
+ <div></div>
+ <div style="text-align: right;">
+ <button type="button" class="alert" data-i18n="cancel"></button>&nbsp;
+ <button type="submit" data-i18n="ok"></button>
+ </div>
+ </div>
+ </div>
+
+
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/options.js"></script>
+ </body>
+</html> \ 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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>FoxyProxy Pattern Help</title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ td { font-size: 0.9em; }
+ td:not(:first-of-type) { font-family: monospace; }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header">Pattern Help</div>
+
+ <!-- main -->
+ <div class="prime">
+
+ <div style="text-align: right;">
+
+ <div class="tooltip">
+ <span data-i18n="patternCheatSheet" class="fp-orange"></span>
+ <i class="fa fa-info-circle"></i>
+ <div class="tooltiptext bottom table">
+ <div class="tooltiptable">
+ <div class="monospace">*</div>
+ <div>all domains</div>
+ <div class="monospace">*.bbc.co.uk</div>
+ <div>exact domain and all subdomains</div>
+ <div class="monospace">**.bbc.co.uk</div>
+ <div>subdomains only (not bbc.co.uk)</div>
+ <div class="monospace">bbc.co.uk</div>
+ <div>exact domain only</div>
+ </div>
+ <hr>
+ <div class="tooltiptablefooter">
+ <div>Black patterns take precedence over white patterns. For example, a black pattern of <span class="monospace">*</span> means nothing will match, regardless of any white patterns.</div>
+ </div>
+ </div>
+ </div>
+ |
+ <a href="/pattern-tester.html" target="_blank"><span data-i18n="patternTester"></span> <i class="fa fa-flask"></i></a>
+ </div>
+
+ <p><h3 style="display: inline-block;">TL;DR</h3>: <a href="#examples">show me the examples</a></p>
+
+ <h3>What are they?</h3>
+ <p>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 <strong class="prime tiny warning">unless a black pattern also matches!</strong> 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 <strong>not</strong> used to load that URL.</p>
+ <div class="prime small warning">Patterns are <strong>ignored</strong> unless FoxyProxy is set to <i>Use Enabled Proxies By Patterns and Priority</i>.</div>
+
+ <h3>Ordering</h3>
+ <p>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 <i>Default</i> proxy that acted as a catch-all to matches all URLs. You may still have <i>Default</i> in FoxyProxy if you upgraded from a previous version.
+ <p>You can re-order the proxies using the arrow buttons as you like, but <strong>the white/black patterns for the top-most (first) proxy are checked first.</strong></p>
+ <p>If a black pattern matches, the proxy is not used for that URL <i>even if a white pattern also matches</i>. 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.</p>
+ <p>The order of white patterns and black patterns within a proxy do not matter.</p>
+
+ <h3>Wildcards</h3>
+ <div class="prime alert">
+ Because of <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1337001">Firefox limitations</a>, only URL domains, subdomains, and ports are recognized in patterns. Do not use paths or query parameters in patterns. Example: <strong>*.foxyproxy.com:30053</strong> is OK but not <strong>*.foxyproxy.com:30053/help/*</strong>
+ </div>
+
+ <h4 id="examples">Examples</h4>
+ <table>
+ <thead><tr><th>Pattern</th><th>Example Matches</th><th>Example Non-Matches</th></thead>
+ <tbody>
+ <tr>
+ <td><strong>*.foxyproxy.com</strong><br>Match URLs at foxyproxy.com and all subdomains</td>
+ <td>foxyproxy.com/order.html<br>help.foxyproxy.com/index.html<br>foo.bar.foxyproxy.com<br>twostep.foxyproxy.com</td>
+ <td>mozilla.com</td>
+ </tr>
+ <tr>
+ <td><strong>.foxyproxy.com</strong><br>Match URLs at foxyproxy.com and all subdomains<br>(same as pattern above)</td>
+ <td>foxyproxy.com/order.html<br>help.foxyproxy.com/index.html<br>foo.bar.foxyproxy.com<br>twostep.foxyproxy.com</td>
+ <td>mozilla.com</td>
+ </tr>
+ <tr>
+ <td><strong>**.foxyproxy.com</strong><br>Match URLs <strong>only at subdomains</strong> of foxyproxy.com</td>
+ <td>help.foxyproxy.com<br>help.foxyproxy.com/index.html<br>foo.bar.foxyproxy.com</td>
+ <td>foxyproxy.com</td>
+ </tr>
+ <tr>
+ <td><strong>foxyproxy.com</strong><br>Match all URLs at foxyproxy.com but not subdomains</td>
+ <td>foxyproxy.com<br>foxyproxy.com/index.html</td>
+ <td>help.foxyproxy.com</td>
+ </tr>
+ <tr>
+ <td><strong>*foo*</strong><br>Match all URLs with a domain containing the letters foo</td>
+ <td>foo.com<br>foodle.com<br>one.befoo.org</td>
+ <td>bar.com</td>
+ </tr>
+ <tr>
+ <td><strong>*foo*.com</strong></td>
+ <td>foo.com<br>foodle.com<br>food.com</td>
+ <td>one.befoo.org<br>food.org</td>
+ </tr>
+ <tr>
+ <td><strong>g?ogle.*</strong><br>? matches any single character</td>
+ <td>google.com<br>grogle.org<br>google.com/maps</td>
+ <td>goog.com</td>
+ </tr>
+ <tr>
+ <td><strong>.catsinsinks.com:8080</strong><br>Port matching!</td>
+ <td>catsinsinks.com:8080<br>www.catsinsinks.com:8080<br>www.catsinsinks.com:8080/privacy</td>
+ <td>catinsinks.net</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h4>Notes</h4>
+ <p>If a wildcard pattern begins with <strong>.</strong> or <strong>*.</strong> then it matches the main domain and all subdomains in a URL.</p>
+ <p>To match <strong>only</strong> subdomains, use <strong>**</strong> instead of <strong>*</strong> in the beginning. Example: <strong>**.foxyproxy.com</strong> will match <strong>help.foxyproxy.com</strong> but will not match <strong>foxyproxy.com</strong>.</p>
+
+ </div>
+ <script src="scripts/pattern-tester.js"></script>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="patternTester">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ h3 { margin-bottom: 0; }
+ .prime > h3:first-of-type { margin-top: 0; }
+ #result { margin: 1em 0 0; }
+ label { display: inline-block; }
+ label + span { color: #f30; font-style: italic; margin-left: 0.5em; font-size: 0.9em; }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="patternTester"></div>
+
+ <!-- main -->
+ <div class="prime">
+
+ <div style="text-align: right;">
+
+ <div class="tooltip">
+ <span data-i18n="patternCheatSheet" class="fp-orange"></span>
+ <i class="fa fa-info-circle"></i>
+ <div class="tooltiptext bottom table">
+ <div class="tooltiptable">
+ <div class="monospace">*</div>
+ <div>all domains</div>
+ <div class="monospace">*.bbc.co.uk</div>
+ <div>exact domain and all subdomains</div>
+ <div class="monospace">**.bbc.co.uk</div>
+ <div>subdomains only (not bbc.co.uk)</div>
+ <div class="monospace">bbc.co.uk</div>
+ <div>exact domain only</div>
+ </div>
+ <hr>
+ <div class="tooltiptablefooter">
+ <div>Black patterns take precedence over white patterns. For example, a black pattern of <span class="monospace">*</span> means nothing will match, regardless of any white patterns.</div>
+ </div>
+ </div>
+ </div>
+ |
+ <a href="/pattern-help.html" target="_blank"><span data-i18n="patternHelp"></span> <i class="fa fa-question-circle"></i></a>
+ </div>
+
+ <h3>Step 1</h3>
+ <label data-i18n="enterUrl"></label><span data-i18n="enterUrlNote"></span>
+ <input id="url" type="url" value="https://getfoxyproxy.org" placeholder="https://getfoxyproxy.org">
+
+
+ <h3>Step 2</h3>
+ <div class="flex">
+ <div style="flex: 3;">
+ <label data-i18n="patternDetail"></label><span data-i18n="patternNote"></span>
+ <input id="pattern" type="text" spellcheck="false">
+ </div>
+
+ <div style="margin-left: 1em;">
+ <label data-i18n="type"></label>
+ <select id="type">
+ <option value="1">Wildcard</option>
+ <option value="2">Reg Exp</option>
+ </select>
+ </div>
+
+ <div style="margin-left: 1em;">
+ <label data-i18n="protocol"></label>
+ <select id="protocols">
+ <option value="1">all</option>
+ <option value="2">http</option>
+ <option value="4">https</option>
+ </select>
+ </div>
+ </div>
+
+
+ <h3>Step 3</h3>
+ <p id="result" class="prime small success hide"></p><br>
+
+ <label data-i18n="clickTest"></label>
+ <div style="text-align: right;"><button type="button" data-i18n="test"></button></div>
+ </div>
+
+ <script src="scripts/common.js"></script>
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/pattern-tester.js"></script>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="patterns">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ td i.fa { font-size: 1.5em; color: #ccc; }
+ td i.fa:hover { font-size: 1.5em; color: #f90; }
+ tr { opacity: 1; transition: all 0.5s ease-in-out; }
+ td { padding: 0.2em 0.5em; }
+ td:nth-of-type(1), td:nth-of-type(2) { min-width: 10em; max-width: 15em; padding: 0 0.5em; }
+ td:nth-of-type(3), td:nth-of-type(4), td:nth-of-type(5) { width: 5em; text-align: center; }
+ td:nth-of-type(6) { width: 7em; }
+
+ input[type="text"], input[type="text"]:focus {
+ background-color: transparent;
+ box-shadow: none;
+ height: auto;
+ margin: 0;
+ padding: 0;
+ border: 1px solid transparent;
+ }
+ input[type="text"]:focus {
+ background-color: #f8f8ff;
+ }
+ input:invalid, input:invalid:focus {
+ background-color: #fff;
+ border-color: #f90;
+ }
+ select {
+ background: #f5fffa;
+ font-size: 0.9em;
+ height: auto;
+ margin: 0;
+ padding: 0.1em;
+ }
+ #result { margin: 1em 0 0; }
+ label[for="file"] { margin: 0; }
+ #links { float: right; }
+ hr { color: white; }
+ .addbuttoncontainer {
+ margin: 1em 0 2em;
+ text-align: center;
+ font-size: 0.9em;
+ }
+ .addbutton {
+ margin-left: 1em;
+ vertical-align: initial;
+ }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header" data-i18n="editPatterns"></div>
+
+ <!-- spinner --> <!-- options, patterns, log -->
+ <div class="spinner on"><i class="fa fa-refresh fa-spin"></i></div>
+
+ <!-- error -->
+ <div id="error" class="prime hide">
+ <h3 data-i18n="errorWas"></h3>
+ <button type="button" data-i18n="back">&#x25c1; </button>
+ </div>
+
+ <!-- main -->
+ <div class="prime main">
+
+ <div class="flex">
+ <div class="prime warning small" style="flex: 1.5">
+ <span data-i18n="activeNote"></span> <strong><span data-i18n="modePatterns"></span></strong>
+ </div>
+ <div style="text-align: right;">
+ <div class="tooltip">
+ <span data-i18n="patternCheatSheet" class="fp-orange"></span>
+ <i class="fa fa-info-circle"></i>
+ <div class="tooltiptext bottom table">
+ <div class="tooltiptable">
+ <div class="monospace">*</div>
+ <div>all domains</div>
+ <div class="monospace">*.bbc.co.uk</div>
+ <div>exact domain and all subdomains</div>
+ <div class="monospace">**.bbc.co.uk</div>
+ <div>subdomains only (not bbc.co.uk)</div>
+ <div class="monospace">bbc.co.uk</div>
+ <div>exact domain only</div>
+ </div>
+ <hr>
+ <div class="tooltiptablefooter">
+ <div>Black patterns take precedence over white patterns. For example, a black pattern of <span class="monospace">*</span> means nothing will match, regardless of any white patterns.</div>
+ </div>
+ </div>
+ </div>
+ |
+ <a href="/pattern-tester.html" target="_blank"><span data-i18n="patternTester"></span> <i class="fa fa-flask"></i></a>
+ |
+ <a href="/pattern-help.html" target="_blank">
+ <span data-i18n="patternHelp"></span>
+ </a>
+ </div>
+ </div>
+
+
+ <h3 data-i18n="whitePatterns"></h3>
+ <!-- table template -->
+ <div class="scroll" style="margin-bottom: 2em;">
+ <table style="width: 100%;">
+ <thead>
+ <tr>
+ <th data-i18n="name"></th><th data-i18n="pattern"></th><th data-i18n="type"></th>
+ <th>HTTP/s</th><th data-i18n="active"></th><th></th>
+ </tr>
+ <!-- template -->
+ <tr class="template">
+ <td><input type="text" spellcheck="false"></td>
+ <td><input type="text" spellcheck="false" required></td>
+ <td>
+ <select name="type">
+ <option value="1">Wildcard</option>
+ <option value="2">Reg Exp</option>
+ </select>
+ </td>
+ <td>
+ <select name="protocol">
+ <option value="1">all</option>
+ <option value="2">http</option>
+ <option value="4">https</option>
+ </select>
+ </td>
+ <td>
+ <input type="checkbox" class="switch" id=""><label for=""></label>
+ </td>
+ <td style="text-align: right;">
+ <button type="button" class="plain hide" data-i18n="imported|title"><i class="fa fa-download"></i></button>
+ <button type="button" class="plain" data-i18n="patternTester|title"><i class="fa fa-flask"></i></button>
+ <!-- <button type="button" class="plain" data-i18n="edit|title"><i class="fa fa-pencil"></i></button> -->
+ <button type="button" class="plain" data-i18n="delete|title"><i class="fa fa-trash"></i></button>
+ </td>
+ </tr>
+
+
+ </thead>
+
+ <tbody></tbody>
+ </table>
+ </div>
+
+ <div class="addbuttoncontainer">
+ <span data-i18n="addWhitelist"></span>
+ <div class="tooltip"><i class="fa fa-info-circle"></i><span class="tooltiptext center" data-i18n="addWhitelistTip"></span></div>
+ <button type="button" class="small addbutton" data-i18n="add" data-white></button>
+ </div>
+
+ <h3 data-i18n="blackPatterns"></h3>
+ <div class="scroll">
+
+ <table style="width: 100%;">
+ <thead>
+ <tr>
+ <th data-i18n="name"></th><th data-i18n="pattern"></th><th data-i18n="type"></th>
+ <th>HTTP/s</th><th data-i18n="active"></th><th></th>
+ </tr>
+ </thead>
+
+ <tbody></tbody>
+ </table>
+ </div>
+
+
+ <div class="addbuttoncontainer">
+ <span data-i18n="addBlacklist"></span>
+ <div class="tooltip"><i class="fa fa-info-circle"></i><span class="tooltiptext center" data-i18n="addBlacklistTip"></span></div>
+ <button type="button" class="small addbutton" data-i18n="add" data-black></button>
+ </div>
+
+ <div class="flex">
+ <div>
+ <input type="file" id="file" accept=".json">
+ <label for="file" class="button" data-i18n="importPatterns"></label>
+ <button type="button" data-i18n="exportPatterns"></button>
+ </div>
+ <div style="text-align: right;">
+ <button type="button" class="alert" data-i18n="cancel"></button>
+ <button type="button" data-i18n="newWhite"></button>
+ <button type="button" data-i18n="newBlack"></button>
+ <button type="submit" data-i18n="save"></button>
+ </div>
+ </div>
+
+ <p id="result" class="prime small success hide"></p>
+
+ </div>
+
+
+ <script src="scripts/common.js"></script>
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/patterns.js"></script>
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ body { padding: 0.4em; animation: none; min-width: 25em; }
+ ul.scroll { margin: 0; padding: 0; max-height: 20em; }
+ li { cursor: pointer; border-radius: 10px; padding-left: 0.2em; }
+ li:hover { background-color: #ffebcd; }
+ li span { display: inline-block; vertical-align: text-bottom; }
+
+ li span:nth-of-type(1) { width: 10em; margin-right: 1em; }
+ li span:nth-of-type(2) { color: #aaa; font-style: italic; font-size: 0.9em;}
+ li:first-of-type { color: #f80; }
+ #disabled { color: red; }
+
+ li::before {
+ content: '\2714';
+ width: 1em;
+ display: inline-block;
+ vertical-align: text-bottom;
+ margin-right: 0.2em;
+ color: transparent;
+ font-weight: bold;
+ }
+ li.on::before {
+ color: inherit;
+ }
+ button:first-of-type { margin-left: 0; }
+ </style>
+ </head>
+ <body>
+
+ <!-- header -->
+ <div class="prime header browserPopup">FoxyProxy</div>
+
+ <!-- error -->
+ <div id="error" class="prime hide"><h5 data-i18n="errorWas"></h5></div>
+
+
+ <!-- main -->
+ <div class="prime" style="margin: 0;">
+
+
+ <ul id="scroll" class="scroll">
+ <li id="patterns" data-i18n="modePatterns"></li>
+
+ <!-- template -->
+ <li class="template ellipsis"><span class="ellipsis"></span><span class="ellipsis"></span></li>
+
+ <li id="disabled" data-i18n="modeDisabled"></li>
+ </ul>
+ <div style="margin-top: 1em; text-align: center;">
+ <button type="button" data-i18n="options"></button>
+ <button type="button" data-i18n="myIP"></button>
+ <button type="button" data-i18n="log"></button>
+ </div>
+ </div>
+
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/popup.js"></script>
+ </body>
+</html> \ 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 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title data-i18n="add">FoxyProxy </title>
+ <link rel="icon" type="image/png" href="/images/icon.svg">
+ <link rel="stylesheet" href="styles/fontawesome-4.6.3.css">
+ <link rel="stylesheet" href="styles/app.css">
+ <style>
+ .main > div:nth-child(1) { flex: 1; margin-right: 1em; }
+ .main > div:nth-child(2) { flex: 1; }
+ input[type], select { margin-top: 0.3em; }
+
+ .onOff > label:first-of-type { flex: 0; flex-grow: 1; margin: 0 1em 0.5em 0; }
+ .onOff > div { flex: 0; }
+ .na { margin-top: 1.5em; }
+ .onOff, .pac, .na { display: none; }
+ input[name="nav"] { display: none; }
+
+ #http:checked ~ div .onOff:not(.dns),
+ #https:checked ~ div .onOff:not(.dns),
+ #socks4:checked ~ div .onOff:not(.dns) {
+ display: flex;
+ }
+ #socks5:checked ~ div .onOff { display: flex; }
+
+ #pac:checked ~ div .pac,
+ #wpad:checked ~ div .pac {
+ display: block;
+ }
+ #pac:checked ~ div div.na,
+ #wpad:checked ~ div div.na,
+ #system:checked ~ div div.na {
+ display: block;
+ }
+
+ #pac:checked ~ div .proxyData,
+ #wpad:checked ~ div .proxyData,
+ #system:checked ~ div .proxyData,
+ #direct:checked ~ div div.proxyData,
+ #pac:checked ~ div button:not(:first-of-type),
+ #wpad:checked ~ div button:not(:first-of-type),
+ #system:checked ~ div button:not(:first-of-type) {
+ display: none;
+ }
+
+ #http:checked ~ div .patternShortcuts,
+ #https:checked ~ div .patternShortcuts,
+ #socks4:checked ~ div .patternShortcuts,
+ #socks5:checked ~ div .patternShortcuts,
+ #direct:checked ~ div .patternShortcuts {
+ display: flex;
+ }
+
+ #pac:checked ~ div .patternShortcutsContainer,
+ #wpad:checked ~ div .patternShortcutsContainer,
+ #system:checked ~ div .patternShortcutsContainer {
+ display: none;
+ }
+
+ #direct:checked ~ div .patternShortcutsContainer {
+ display: block;
+ }
+ </style>
+ </head>
+ <body>
+
+ <!-- Navigation -->
+ <input type="radio" name="nav" id="http">
+ <input type="radio" name="nav" id="https" checked>
+ <input type="radio" name="nav" id="socks5">
+ <input type="radio" name="nav" id="socks4">
+ <input type="radio" name="nav" id="direct">
+ <input type="radio" name="nav" id="pac">
+ <input type="radio" name="nav" id="wpad">
+ <input type="radio" name="nav" id="system">
+
+ <!-- header -->
+ <div class="prime header" data-i18n="options"></div>
+
+
+ <!-- main -->
+ <div class="prime">
+ <div class="main flex">
+
+ <!-- left column -->
+ <div>
+ <div>
+ <label data-i18n="title"></label>
+ <input id="proxyTitle" type="text" spellcheck="false" placeholder="title">
+
+ <label data-i18n="color"></label>
+ <input id="colorChooser" type="text" style="text-align: center;">
+ </div>
+
+ <!-- SOCKS5 -->
+ <div class="flex onOff dns">
+ <label>Send DNS through SOCKS5 proxy</label>
+ <div>
+ <input type="checkbox" class="switch" id="proxyDNS" checked><label for="proxyDNS"></label>
+ </div>
+ </div>
+
+ <div class="patternShortcutsContainer notForBasic notForEdit">
+ <label data-i18n="patternShortcuts" class="notForBasic notForEdit"></label>
+ <div class="prime warning">
+ <div class="patternShortcuts flex onOff">
+ <label data-i18n="active"></label>
+ <div>
+ <input type="checkbox" class="switch" id="proxyActive" checked><label for="proxyActive"></label>
+ </div>
+ </div>
+
+ <div class="patternShortcuts onOff">
+ <label><span data-i18n="addWhitelist"></span>
+ <div class="tooltip"><i class="fa fa-info-circle"></i><span class="tooltiptext" data-i18n="addWhitelistTip"></span></div></label>
+ <div>
+ <input type="checkbox" class="switch" id="whiteAll" checked><label for="whiteAll"></label>
+ </div>
+ </div>
+
+ <div class="patternShortcuts onOff">
+ <label><span data-i18n="noLocal"></span>
+ <div class="tooltip"><i class="fa fa-info-circle"></i><span class="tooltiptext" data-i18n="addBlacklistTip"></span></div></label>
+
+ <div>
+ <input type="checkbox" class="switch" id="blackAll"><label for="blackAll"></label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- right column -->
+ <div>
+ <div class="rightData">
+ <label data-i18n="proxyType"></label>
+ <select id="proxyType">
+ <option value="1" selected>HTTP</option>
+ <option value="2">HTTPS/SSL</option>
+ <option value="3">SOCKS5</option>
+ <option value="4">SOCKS4</option>
+ <option value="6">PAC URL</option>
+ <option value="7">WPAD</option>
+ <option value="8">System (use system settings)</option>
+ <option value="5">Direct (no proxy)</option>
+ </select>
+
+
+ <div class="proxyData">
+ <label data-i18n="ip"></label> <i class="fa fa-star"></i>
+ <input id="proxyAddress" type="text" spellcheck="false" placeholder="111.111.111.111, www.example.com">
+
+ <label data-i18n="port"></label> <i class="fa fa-star"></i>
+ <input id="proxyPort" type="text" placeholder="3128">
+
+ <label data-i18n="username"></label>
+ <input id="proxyUsername" type="text" spellcheck="false" placeholder="username">
+
+ <label data-i18n="password"></label>
+ <button type="button" class="plain" data-i18n="togglePW|title"><i class="fa fa-eye"></i></button>
+ <input id="proxyPassword" type="password" spellcheck="false" placeholder="*****">
+ </div>
+ </div>
+
+ <!-- PAC/WPAD -->
+ <div class="pac">
+ <label>URL</label> <i class="fa fa-star"></i>
+ <input id="pacURL" type="text" placeholder="PAC URL">
+ </div>
+
+
+ <div class="prime alert small na">
+ <h3>Not supported</h3>
+ <p>Due to Firefox limitation, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1396485" target="_blank">PAC</a>, <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1319631" target="_blank">WPAD</a>, and <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1319630" target="_blank">System Settings</a> are currently not supported, but they are due to be added.</p>
+ </div>
+ </div>
+ </div>
+
+ <div style="text-align: right;">
+ <button type="button" class="alert" data-i18n="cancel"></button>
+ <button type="button" data-i18n="saveAdd"></button>
+ <button type="button" class="notForBasic" data-i18n="saveEditPattern"></button>
+ <button type="submit" data-i18n="save"></button>
+ </div>
+
+ </div>
+
+ <script src="scripts/jscolor-2.0.5.js"></script><!-- only used here -->
+ <script src="scripts/utils.js"></script>
+ <script src="scripts/proxy.js"></script>
+ </body>
+</html> \ 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: ["<all_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: ["<all_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 <foxyproxy ....>)');
+ 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(<targetElement> [, <options>])
+ //
+
+ 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 <input> 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 <option> elements except patterns and disabled
+ [...mode.children].forEach(item => !['patterns', 'disabled'].includes(item.value) && item.remove());
+
+ // ----- templates & containers
+ const docfrag = document.createDocumentFragment();
+ const docfrag2 = document.createDocumentFragment();
+ const temp = document.querySelector('.template');
+
+ // --- working directly with DB format
+
+ // add default lastresort if not there
+ //pref[LASTRESORT] || (pref[LASTRESORT] = DEFAULT_PROXY_SETTING);
+
+ const prefKeys = Object.keys(pref).filter(item => !NON_PROXY_KEYS.includes(item)); // not for these
+
+ prefKeys.sort((a, b) => pref[a].index - pref[b].index); // sort by index
+ if (prefKeys[0]) {
+ minIndex = pref[prefKeys[0]].index; // the first index after sort (if any)
+ }
+
+ pref.mode = pref.mode || 'disabled'; // defaults to disabled
+ prefKeys.forEach(id => {
+ const item = pref[id];
+
+ const div = temp.cloneNode(true);
+ const node = [...div.children[0].children, ...div.children[1].children];
+ div.classList.remove('template');
+ //id === LASTRESORT && div.children[1].classList.add('default');
+
+ div.id = id;
+ node[0].style.backgroundColor = item.color;
+ node[1].textContent = Utils.getProxyTitle(item);
+ node[2].textContent = item.address; // ellipsis is handled by CSS
+ if (item.cc) {
+ node[3].classList.remove('hide');
+ node[3].textContent = getFlag(item.cc);
+ node[3].title = item.country;
+ }
+ item.username && item.password && node[4].classList.add('on');
+ node[5].id = id + '-onoff';
+ node[5].checked = item.active;
+ node[6].setAttribute('for', node[5].id);
+
+ FOXYPROXY_BASIC && (node[8].style.display = 'none');
+
+ // setting div colors
+ switch (true) {
+
+ case Utils.isUnsupportedType(item.type):
+ div.classList.add('unsupported');
+ break;
+
+ case pref.mode === 'patterns':
+ case pref.mode === 'random':
+ case pref.mode === 'roundrobin':
+ div.classList.add(item.active ? 'success' : 'secondary');
+ break;
+
+ case pref.mode === 'disabled':
+ div.classList.add('secondary');
+ break;
+
+ default:
+ div.classList.add(pref.mode == id ? 'success' : 'secondary');
+ }
+
+ docfrag.appendChild(div);
+
+ // add to select
+ const opt = new Option(node[1].textContent, id);
+ opt.style.color = item.color;
+ docfrag2.appendChild(opt);
+ });
+
+ docfrag.hasChildNodes() && accounts.appendChild(docfrag);
+ docfrag2.hasChildNodes() && mode.appendChild(docfrag2, mode.lastElementChild);
+
+ if (FOXYPROXY_BASIC) {
+ mode.children[0].classList.add('hide'); // hide by pattern option
+ pref.mode === 'patterns' && (pref.mode = 'disabled');
+ }
+
+ const opt = mode.querySelector(`option[value="${pref.mode}"]`);
+ if (opt) {
+ opt.selected = true;
+ mode.style.color = opt.style.color;
+ }
+
+ // add Listeners
+ document.querySelectorAll('button').forEach(item => item.addEventListener('click', processButton));
+
+ document.querySelectorAll('input[name="onOff"]').forEach(item => item.addEventListener('change', function() {
+ const id = this.parentNode.parentNode.id;
+ storageArea.get(id, result => {
+ result[id].active = this.checked;
+ storageArea.set(result);
+ });
+ }));
+
+ doWeHaveProxiesDefined();
+ hideSpinner();
+}
+
+function doWeHaveProxiesDefined() {
+ if (!accounts.hasChildNodes()) {
+ document.querySelector('#help').style.display = 'block';
+ document.querySelector('#rightColumn').classList.add('secondary');
+ document.querySelector('#mode').style.display = 'none';
+ }
+ else {
+ document.querySelector('#help').style.display = 'none';
+ document.querySelector('#rightColumn').classList.remove('warning');
+ document.querySelector('#mode').style.display = 'flex';
+ }
+}
+
+function getFlag(cc) {
+
+ cc = /^[A-Z]{2}$/i.test(cc) && cc.toUpperCase();
+ return cc && String.fromCodePoint(...[...cc].map(c => c.charCodeAt() + 127397));
+}
+
+function processButton() {
+
+ const parent = this.parentNode.parentNode;
+ const id = parent.id;
+
+ switch (this.dataset.i18n) {
+
+ case 'help|title':
+ popupMain.children[0].textContent = chrome.i18n.getMessage('syncSettings');
+ popupMain.children[1].textContent = chrome.i18n.getMessage('syncSettingsHelp');
+ popupMain.children[2].children[0].style.visibility = 'hidden';
+ popupMain.children[2].children[1].addEventListener('click', closePopup);
+ showPopup();
+ break;
+
+ case 'edit':
+ localStorage.setItem('id', id);
+ location.href = '/proxy.html';
+ break;
+
+ case 'patterns':
+ localStorage.setItem('id', id);
+ location.href = '/patterns.html';
+ break;
+
+ case 'delete|title':
+ if (confirm(chrome.i18n.getMessage('confirmDelete'))) {
+ parent.style.opacity = 0;
+ setTimeout(() => { parent.remove(); doWeHaveProxiesDefined();}, 600); // remove row
+ storageArea.remove(id);
+ }
+ break;
+
+ case 'up|title':
+ case 'down|title':
+ const target = this.dataset.i18n === 'up|title' ? parent.previousElementSibling : parent.nextElementSibling;
+ const insert = this.dataset.i18n === 'up|title' ? target : target.nextElementSibling;
+ parent.parentNode.insertBefore(parent, insert);
+ parent.classList.add('on');
+ setTimeout(() => { parent.classList.remove('on'); }, 600);
+ storageArea.get(null, result => {
+ // re-index
+ //[...accounts.children].forEach((item, index) => item.id !== LASTRESORT && (result[item.id].index = index));
+ [...accounts.children].forEach((item, index) => result[item.id].index = index);
+ minIndex = 0; // minimum index is always 0 now
+ storageArea.set(result);
+ });
+ break;
+ }
+}
+
+function showPopup() {
+
+ popup.style.display = 'flex';
+ window.getComputedStyle(popup).opacity;
+ window.getComputedStyle(popup.children[0]).transform;
+ popup.classList.add('on');
+}
+
+function closePopup() {
+
+ popup.classList.remove('on');
+ setTimeout(() => {
+ popup.style.display = 'none';
+ // reset
+ popupMain.children[0].textContent = '';
+ popupMain.children[1].textContent = '';
+ popupMain.children[2].children[0].style.visibility = 'visible';
+ popupMain.replaceChild(popupMain.children[2].cloneNode(true), popupMain.children[2]); // cloning to remove listeners
+ }, 600);
+}
diff --git a/src/scripts/pattern-help.js b/src/scripts/pattern-help.js
new file mode 100644
index 0000000..c5d7be1
--- /dev/null
+++ b/src/scripts/pattern-help.js
@@ -0,0 +1,9 @@
+'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 -----------------
diff --git a/src/scripts/pattern-tester.js b/src/scripts/pattern-tester.js
new file mode 100644
index 0000000..83e0a26
--- /dev/null
+++ b/src/scripts/pattern-tester.js
@@ -0,0 +1,77 @@
+'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 url = document.querySelector('#url');
+const pattern = document.querySelector('#pattern');
+const type = document.querySelector('#type');
+const protocols = document.querySelector('#protocols');
+const result = document.querySelector('#result');
+
+
+document.querySelector('button[data-i18n="test"]').addEventListener('click', testPattern);
+
+
+// ----- check for Edit
+const pat = localStorage.getItem('pattern');
+if (pat) {
+
+ pattern.value = pat;
+ type.value = localStorage.getItem('type');
+ protocols.value = localStorage.getItem('protocols');
+
+ localStorage.removeItem('pattern');
+ localStorage.removeItem('type');
+ localStorage.removeItem('protocols');
+}
+
+
+function testPattern() {
+
+ // --- reset
+ url.classList.remove('invalid');
+ pattern.classList.remove('invalid');
+ result.classList.add('hide');
+ result.classList.remove('alert');
+
+ // --- trim text values
+ [url, pattern].forEach(item => item.value = item.value.trim());
+
+ // --- URL check
+ let parsedURL;
+ try { parsedURL = new URL(url.value); }
+ catch (e) {
+ url.classList.add('invalid');
+ showResult(e.message, true);
+ return;
+ }
+
+ // --- protocol check
+ const protocolSet = { // converting to meaningful terms
+ '1': ['http:', 'https:'],
+ '2': ['http:'],
+ '4': ['https:']
+ };
+
+ if (!protocolSet[protocols.value].includes(parsedURL.protocol)) {
+ showResult(chrome.i18n.getMessage('errorProtocol'), true);
+ return;
+ }
+
+
+ // --- pattern check
+ const regex = checkPattern(pattern, type);
+ if (!regex) { return; }
+
+ // --- pattern on URL check (pattern is valid)
+ regex.test(parsedURL.host) ? showResult(chrome.i18n.getMessage('patternMatch')) :
+ showResult(chrome.i18n.getMessage('patternNotMatch'), true);
+
+}
diff --git a/src/scripts/patterns.js b/src/scripts/patterns.js
new file mode 100644
index 0000000..a276a20
--- /dev/null
+++ b/src/scripts/patterns.js
@@ -0,0 +1,283 @@
+'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) {
+ history.back(); // We either came from /proxy.html or /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 ------------------------------
+
+// ----- global
+let proxy = {};
+const header = document.querySelector('.header');
+const tbody = document.querySelectorAll('tbody'); // there are 2
+const template = document.querySelector('tr.template');
+const docfrag = document.createDocumentFragment();
+
+const defaultPattern = {
+ title: '',
+ active: true,
+ pattern: '',
+ type: 1, // PATTERN_TYPE_WILDCARD,
+ protocols: 1 // PROTOCOL_ALL
+};
+
+
+const protocolSet = { // converting to meaningful terms
+ 1: 'All',
+ 2: 'HTTP',
+ 4: 'HTTPS'
+};
+
+const patternTypeSet = {
+ 1: 'wildcard',
+ 2: 'Reg Exp'
+}
+
+// ----- check for Edit
+const id = localStorage.getItem('id');
+const sync = localStorage.getItem('sync') === 'true';
+const storageArea = !sync ? chrome.storage.local : chrome.storage.sync;
+if (id) { // This is an edit operation
+
+ storageArea.get(id, result => {
+
+ if (!Object.keys(result).length) {
+/*
+ if (id === LASTRESORT) { // error prevention
+ proxy = DEFAULT_PROXY_SETTING;
+ processOptions();
+ return;
+ }*/
+ console.error('Unable to edit saved proxy (could not get existing settings)')
+ return;
+ }
+
+ proxy = result[id];
+ if (proxy.title) { header.textContent = chrome.i18n.getMessage('editPatternsFor', proxy.title); }
+ processOptions();
+ hideSpinner();
+ })
+}
+/*
+else {
+ // Error, shouldn't ever get here
+ hideSpinner();
+ document.querySelector('#error').classList.remove('hide');
+ document.querySelector('.main').classList.add('hide');
+ console.error("2: Unable to read saved proxy proxy (could not get existing settings)");
+}*/
+
+// --- processing all buttons
+document.querySelectorAll('button').forEach(item => item.addEventListener('click', process));
+
+function process() {
+
+ switch (this.dataset.i18n) {
+
+ case 'back': // error
+ case 'cancel':
+ location.href = '/options.html';
+ break;
+
+ case 'exportPatterns': exportPatterns(); break;
+
+ case 'newWhite':
+ addNew(tbody[0], 'whitePatterns');
+ break;
+
+ case 'newBlack':
+ addNew(tbody[1], 'blackPatterns');
+ break;
+
+ case 'save':
+ checkOptions();
+ break;
+
+ case 'add':
+ if (typeof(this.dataset.black) !== 'undefined') {
+ proxy.blackPatterns.push(...blacklistSet);
+ processOptions();
+ }
+ else {
+ proxy.whitePatterns.push(PATTERN_ALL_WHITE);
+ processOptions();
+ }
+ break;
+ }
+}
+
+function processOptions() {
+
+ // clearing the content
+ tbody[0].textContent = '';
+ tbody[1].textContent = '';
+
+ proxy.whitePatterns.forEach((item, index) => docfrag.appendChild(makeRow(item, index, 'whitePatterns')));
+ docfrag.hasChildNodes() && tbody[0].appendChild(docfrag);
+
+ proxy.blackPatterns.forEach((item, index) => docfrag.appendChild(makeRow(item, index, 'blackPatterns')));
+ docfrag.hasChildNodes() && tbody[1].appendChild(docfrag);
+
+}
+
+function makeRow(pat, index, bw) {
+
+ const tr = template.cloneNode(true);
+ tr.classList.remove('template');
+ tr.classList.add(pat.active ? 'success' : 'secondary');
+ tr.dataset.idx = index;
+ tr.dataset.bw = bw; // black/white
+ const td = tr.children;
+
+
+ td[0].children[0].value = pat.title;
+ td[1].children[0].value = pat.pattern;
+ td[2].children[0].value = pat.type;
+ td[3].children[0].value = pat.protocols;
+ td[4].children[0].checked = pat.active;
+ td[4].children[0].id = bw + index;
+ td[4].children[1].setAttribute('for', td[4].children[0].id);
+
+ pat.importedPattern && td[5].children[0].classList.remove('hide');
+
+ // add Listeners();
+ [...td[5].children].forEach(item => item.addEventListener('click', processEdit));
+
+ return tr;
+}
+
+function addNew(parent, bw) {
+
+ const tr = makeRow(defaultPattern, parent.children.length, bw);
+ parent.appendChild(tr);
+ tr.children[1].children[0].focus();
+}
+
+function processEdit() {
+
+ const parent = this.parentNode.parentNode;
+ const idx = parent.dataset.idx *1;
+ const patternsArray = proxy[parent.dataset.bw]; // whitePatterns | blackPatterns
+
+ switch (this.dataset.i18n) {
+
+ case 'imported|title':
+ alert(chrome.i18n.getMessage('importedPattern') + ' \n\n' + patternsArray[idx].importedPattern);
+ break;
+
+ case 'patternTester|title':
+ const pat = patternsArray[idx];
+ if (pat) {
+ localStorage.setItem('pattern', pat.pattern);
+ localStorage.setItem('type', pat.type);
+ localStorage.setItem('protocols', pat.protocols);
+ }
+ chrome.tabs.create({url: '/pattern-tester.html'});
+ break;
+
+ case 'delete|title':
+ parent.style.opacity = 0;
+ setTimeout(() => { parent.remove(); }, 300); // remove row
+ break;
+ }
+}
+
+
+function checkOptions() {
+
+ const pxy = {
+ whitePatterns: [],
+ blackPatterns: []
+ };
+
+ // use for loop to be able to return early on error
+ for (const item of document.querySelectorAll('tr[data-idx]')) {
+
+ const td = item.children;
+
+ // --- trim text values
+ [td[0].children[0], td[1].children[0]].forEach(item => item.value = item.value.trim());
+
+ // test pattern
+ const regex = testPattern(td[1].children[0], td[2].children[0]);
+ if (!regex) { return; }
+
+ const bw = item.dataset.bw;
+ pxy[bw].push({
+ title: td[0].children[0].value,
+ pattern: td[1].children[0].value,
+ type: td[2].children[0].value *1,
+ protocols: td[3].children[0].value *1,
+ active: td[4].children[0].checked
+ });
+ }
+
+ // all patterns passed
+ proxy.whitePatterns = pxy.whitePatterns;
+ proxy.blackPatterns = pxy.blackPatterns;
+ storageArea.set({[id]: proxy}, () => location.href = '/options.html');
+}
+
+
+function testPattern(pattern, type) {
+
+ // --- reset
+ pattern.classList.remove('invalid');
+ result.classList.add('hide');
+ result.classList.remove('alert');
+
+ // --- pattern check
+ return checkPattern(pattern, type);
+}
+
+
+
+function exportPatterns() {
+
+ const tmpObject = {whitePatterns: proxy.whitePatterns, blackPatterns: proxy.blackPatterns};
+ const blob = new Blob([JSON.stringify(tmpObject, null, 2)], {type : 'text/plain'});
+ const filename = 'foxyproxy' + (proxy.title ? '-' + proxy.title : '') + '-patterns' + '_' + new Date().toISOString().substring(0, 10) + '.json';
+ chrome.downloads.download({
+ url: URL.createObjectURL(blob),
+ filename,
+ saveAs: true,
+ conflictAction: 'uniquify'
+ }, () => console.log('Export/download finished')); // wait for it to complete before returning
+}
+
+
+document.getElementById('file').addEventListener('change', processFileSelect);
+function processFileSelect(e) {
+
+ const file = e.target.files[0];
+
+ Utils.importFile(file, ['application/json'], 1024*1024*5, 'json', imported => {
+ proxy.whitePatterns = imported.whitePatterns;
+ proxy.blackPatterns = imported.blackPatterns;
+ processOptions();
+ Utils.notify(chrome.i18n.getMessage('importBW', [proxy.whitePatterns.length, proxy.blackPatterns.length]));
+ });
+}
diff --git a/src/scripts/popup.js b/src/scripts/popup.js
new file mode 100644
index 0000000..120e5ed
--- /dev/null
+++ b/src/scripts/popup.js
@@ -0,0 +1,112 @@
+'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 -----------------
+
+// ----------------- User Preference -----------------------
+let storageArea;
+chrome.storage.local.get(null, result => {
+ storageArea = result.sync ? chrome.storage.sync : chrome.storage.local;
+ result.sync ? chrome.storage.sync.get(null, processOptions) : processOptions(result);
+});
+// ----------------- /User Preference ----------------------
+
+function processOptions(pref) {
+
+ // ----- templates & containers
+ const docfrag = document.createDocumentFragment();
+ const temp = document.querySelector('li.template');
+
+ // add default lastresort if not there
+ //pref[LASTRESORT] || (pref[LASTRESORT] = DEFAULT_PROXY_SETTING);
+
+ const prefKeys = Object.keys(pref).filter(item => !NON_PROXY_KEYS.includes(item)); // not for these
+
+ prefKeys.sort((a, b) => pref[a].index - pref[b].index); // sort by index
+
+ pref.mode = pref.mode || 'disabled'; // defaults to disabled
+ let hasProxySettings = false;
+ prefKeys.forEach(id => {
+
+ const item = pref[id];
+
+ if (!Utils.isUnsupportedType(item.type)) { // if supported
+
+ const li = temp.cloneNode(true);
+ li.classList.remove('template');
+ li.id = id;
+ li.style.color = item.color;
+ li.children[0].textContent = Utils.getProxyTitle(item);
+ li.children[1].textContent = '(' + chrome.i18n.getMessage('forAll') + ')';
+
+ docfrag.appendChild(li);
+ hasProxySettings = true;
+ }
+ });
+
+ docfrag.hasChildNodes() && temp.parentNode.appendChild(docfrag, temp.nextElementSibling);
+
+ if (FOXYPROXY_BASIC) {
+ temp.parentNode.children[0].classList.add('hide'); // hide by pattern option
+ pref.mode === 'patterns' && (pref.mode = 'disabled');
+ }
+
+ // hide the selections if there are no proxy settings defined
+ document.getElementById('scroll').style.display = hasProxySettings ? 'block' : 'none';
+
+ const node = document.getElementById(pref.mode); // querySelector error with selectors starting with number
+ node.classList.add('on');
+
+ // add Listeners
+ document.querySelectorAll('li, button').forEach(item => item.addEventListener('click', process));
+}
+
+function process() {
+
+ let tabs;
+ switch (this.dataset.i18n) {
+
+ case 'myIP':
+ chrome.tabs.create({url: 'https://getfoxyproxy.org/geoip/'}); // no need to wait for it
+ window.close();
+ break;
+
+ case 'log':
+ const url = chrome.runtime.getURL('log.html');
+ chrome.tabs.query({url}, tabs => { // find a log tab
+ tabs[0] ? chrome.tabs.update(tabs[0].id, {active: true}) : chrome.tabs.create({url}); // active existing tab OR open new tab
+ window.close();
+ });
+ break;
+
+ case 'options':
+ chrome.tabs.query({url: chrome.runtime.getURL('') + '*'}, tabs => {
+ if (!tabs[0]) {
+ chrome.runtime.openOptionsPage();
+ window.close();
+ return;
+ }
+ const tab = tabs.find(item => /(proxy|options|patterns)\.html/.test(item.url)); // find a option tab
+ tab ? chrome.tabs.update(tab.id, {active: true}) : chrome.tabs.update(tabs[0].id, {active: true, url: '/options.html'});
+ window.close();
+ });
+ break;
+
+ default:
+ // reset the old one
+ const old = document.querySelector('.on');
+ old && old.classList.remove('on');
+ this.classList.add('on');
+
+
+ storageArea.set({mode: this.id}); // keep it open for more action
+ // popup & options are the only place that can set mode
+ // sending message to option && bg, if it is open
+ chrome.runtime.sendMessage({mode: this.id});
+ }
+}
diff --git a/src/scripts/proxy.js b/src/scripts/proxy.js
new file mode 100644
index 0000000..16394b7
--- /dev/null
+++ b/src/scripts/proxy.js
@@ -0,0 +1,228 @@
+'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();
+ }
+});
+
+// ----- global
+let proxy = {}, proxiesAdded = 0;
+const color = new jscolor('colorChooser', {uppercase: false, hash: true});
+color.fromString(DEFAULT_COLOR); // starting from default color
+
+const header = document.querySelector('.header'); // dynamic header
+setHeader();
+
+// ----- check for Edit
+let id = localStorage.getItem('id');
+const sync = localStorage.getItem('sync') === 'true';
+const storageArea = !sync ? chrome.storage.local : chrome.storage.sync;
+if (id) { // This is an edit operation
+
+ storageArea.get(id, result => {
+
+ if (!Object.keys(result).length) {
+/*
+ if (id === LASTRESORT) { // error prevention
+ proxy = DEFAULT_PROXY_SETTING;
+ processOptions();
+ return;
+ }*/
+ console.error('Unable to edit saved proxy (could not get existing settings)')
+ return;
+ }
+ proxy = result[id];
+ processOptions();
+ });
+}
+
+
+// --- show & hide element using CSS
+const nav = [...document.querySelectorAll('input[name="nav"]')];
+//nav[0].checked = true;
+
+const proxyType = document.querySelector('#proxyType');
+proxyType.addEventListener('change', function() { nav[this.value -1].checked = true; });
+
+const proxyTitle = document.querySelector('#proxyTitle');
+proxyTitle.focus();
+
+const proxyAddress = document.querySelector('#proxyAddress');
+const proxyPort = document.querySelector('#proxyPort');
+const proxyUsername = document.querySelector('#proxyUsername');
+const proxyPassword = document.querySelector('#proxyPassword');
+const proxyActive = document.querySelector('#proxyActive');
+const proxyDNS = document.querySelector('#proxyDNS');
+const pacURL = document.querySelector('#pacURL');
+
+// --- remove nodes completely for FP Basic
+FOXYPROXY_BASIC && document.querySelectorAll('.notForBasic').forEach(item => item.remove());
+
+// --- remove pattern shortcuts if this is an edit operation
+id && document.querySelectorAll('.notForEdit').forEach(item => item.remove());
+
+// --- add Listeners
+document.querySelectorAll('button').forEach(item => item.addEventListener('click', process));
+function process() {
+
+ switch (this.dataset.i18n) {
+
+ case 'cancel':
+ close();
+ break;
+
+ case 'saveAdd':
+ if (!validateInput()) { return; }
+ storageArea.set(makeProxy(), resetOptions);
+ break;
+
+ case 'saveEditPattern':
+ if (!validateInput()) { return; }
+ storageArea.set(makeProxy(), () => {
+ localStorage.setItem('id', id); // in case new proxy was added
+ proxyPassword.value = ''; // prevent Firefox's save password prompt
+ location.href = '/patterns.html';
+ });
+ break;
+
+ case 'save':
+ if (!validateInput()) { return; }
+ storageArea.set(makeProxy(), () => {
+ proxyPassword.value = ''; // prevent Firefox's save password promp
+ location.href = '/options.html'
+ });
+ break;
+
+ case 'togglePW|title':
+ const inp = this.nextElementSibling;
+ inp.type = inp.type === 'password' ? 'text' : 'password';
+ break;
+ }
+}
+
+function setHeader(proxy) {
+
+ if (proxy) {
+ document.title = 'FoxyProxy ' + chrome.i18n.getMessage('editProxy', '');
+ header.textContent = chrome.i18n.getMessage('editProxy', proxy.title || `${proxy.address}:${proxy.port}`);
+ return;
+ }
+ document.title = 'FoxyProxy ' + chrome.i18n.getMessage('addProxy');
+ header.textContent = chrome.i18n.getMessage('addProxy');
+}
+
+
+function processOptions() {
+
+ setHeader(proxy);
+
+ // select
+ proxyType.value = proxy.type;
+ nav[proxyType.value -1].checked = true;
+
+ // checkbox
+ proxyActive.checked = proxy.active;
+ proxyDNS.checked = proxy.proxyDNS || false;
+
+ // color
+ color.fromString(proxy.color || DEFAULT_COLOR);
+
+ // input
+ proxyTitle.value = proxy.title || '';
+ proxyAddress.value = proxy.address || '';
+ proxyPort.value = proxy.port || '';
+ proxyUsername.value = proxy.username || '';
+ proxyPassword.value = proxy.password || '';
+ pacURL.value = proxy.pacURL || '';
+}
+
+function makeProxy() {
+
+ proxy.type = proxyType.value *1;
+ proxy.color = document.querySelector('#colorChooser').value;
+ proxy.title = proxyTitle.value;
+ proxy.active = proxyActive.checked;
+
+ if (proxy.type !== PROXY_TYPE_NONE) {
+
+ proxy.address = proxyAddress.value;
+ proxy.port = proxyPort.value *1;
+ proxy.proxyDNS = proxy.type === PROXY_TYPE_SOCKS5 && proxyDNS.checked;
+ // already trimmed in validateInput()
+ proxy.username = proxyUsername.value; // if it had u/p and then deletd it, it must be reflected
+ proxy.password = proxyPassword.value;
+ }
+ if (FOXYPROXY_BASIC) {
+ proxy.whitePatterns = proxy.blackPatterns = [];
+ }
+ else {
+ proxy.whitePatterns = proxy.whitePatterns || (document.querySelector('#whiteAll').checked ? [PATTERN_ALL_WHITE] : []);
+ proxy.blackPatterns = proxy.blackPatterns || (document.querySelector('#blackAll').checked ? blacklistSet : []);
+ }
+ proxy.pacURL = proxy.pacURL || pacURL.value; // imported foxyproxy.xml
+
+ if (!id) { // global
+ // This is an add operation since id does not exist. If this is an edit op, then id is already set.
+ // Get the nextIndex given to us by options.js and subtract by the number of proxies we've added
+ // while this window has been open. This ensures this proxy setting is first in list of all proxy settings.
+ proxy.index = (localStorage.getItem('nextIndex')) - (++proxiesAdded);
+ id = Utils.getUniqueId();
+ }
+ // else proxy.index is already set for edit operations
+ return {[id]: proxy};
+}
+
+function validateInput() {
+
+ document.querySelectorAll('input[type="text"]').forEach(item => item.value = item.value.trim());
+
+ if (proxyType.value *1 === PROXY_TYPE_NONE) { return true; }
+
+ // let's handle here, #proxyPort will be checked later separately
+ // escape all inputs
+ [proxyTitle, proxyAddress].forEach(item => item.value = Utils.stripBadChars(item.value));
+
+ // checking proxyAddress
+ proxyAddress.classList.remove('invalid'); // reset
+ if (!proxyAddress.value) {
+ proxyAddress.classList.add('invalid');
+ return false;
+ }
+
+ // checking proxyPort
+ proxyPort.classList.remove('invalid'); // reset
+ if (!proxyPort.value *1) { // check to see if it is a digit and not 0
+ proxyPort.classList.add('invalid');
+ return false;
+ }
+
+ return true;
+}
+
+
+function resetOptions() {
+
+ localStorage.removeItem('id');
+ id = null;
+
+ // to help entering sets quickly, some fields are kept
+ [proxyTitle, proxyAddress].forEach(item => item.value = '');
+ color.fromString(DEFAULT_COLOR);
+
+ setHeader();
+ proxyTitle.focus();
+}
+
+function close() {
+ proxyPassword.value = ''; /* prevent Firefox's save password prompt */
+ location.href = '/options.html';
+} \ No newline at end of file
diff --git a/src/scripts/utils.js b/src/scripts/utils.js
new file mode 100644
index 0000000..e456d23
--- /dev/null
+++ b/src/scripts/utils.js
@@ -0,0 +1,324 @@
+'use strict';
+
+// ----------------- Constants -----------------------------
+const FOXYPROXY_BASIC = false;
+
+// Bit-wise flags so we can add/remove these independently. We may add more later so PROTOCOL_ALL is future-proof.
+const PROTOCOL_ALL = 1; // in case other protocols besides http and https are supported later
+const PROTOCOL_HTTP = 2;
+const PROTOCOL_HTTPS = 4;
+
+
+// import | pac
+const PROXY_TYPE_HTTP = 1;
+const PROXY_TYPE_HTTPS = 2;
+const PROXY_TYPE_SOCKS5 = 3;
+const PROXY_TYPE_SOCKS4 = 4;
+const PROXY_TYPE_NONE = 5; // DIRECT
+const PROXY_TYPE_PAC = 6;
+const PROXY_TYPE_WPAD = 7;
+const PROXY_TYPE_SYSTEM = 8;
+const PROXY_TYPE_PASS = 9;
+
+
+const PATTERN_TYPE_WILDCARD = 1;
+const PATTERN_TYPE_REGEXP = 2;
+
+// Storage keys that are not proxy settings
+const NON_PROXY_KEYS = ['mode', 'logging', 'sync', 'browserVersion', 'foxyProxyVersion', 'foxyProxyEdition', 'nextIndex'];
+
+// bg | import | proxy | utils
+const PATTERN_ALL_WHITE = {
+ title: 'all URLs',
+ active: true,
+ pattern: '*',
+ type: 1, // PATTERN_TYPE_WILDCARD,
+ protocols: 1 // PROTOCOL_ALL
+};
+
+const DEFAULT_COLOR = '#66cc66'; // default proxy color
+
+// patterns | proxy
+// the local-internal blacklist, always used as a set
+const blacklistSet = [
+ {
+ title: "local hostnames (usually no dots in the name). Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.",
+ pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?(?:localhost|127\\.\\d+\\.\\d+\\.\\d+)(?::\\d+)?(?:/.*)?$",
+ },
+ {
+ title: "local subnets (IANA reserved address space). Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.",
+ pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?(?:192\\.168\\.\\d+\\.\\d+|10\\.\\d+\\.\\d+\\.\\d+|172\\.(?:1[6789]|2[0-9]|3[01])\\.\\d+\\.\\d+)(?::\\d+)?(?:/.*)?$",
+ },
+ {
+ title: "localhost - matches the local host optionally prefixed by a user:password authentication string and optionally suffixed by a port number. The entire local subnet (127.0.0.0/8) matches. Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.",
+ pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?[\\w-]+(?::\\d+)?(?:/.*)?$"
+ }
+].map (item => {
+ item.active = true;
+ item.type = 2; // PATTERN_TYPE_REGEXP,
+ item.protocols = 1; // PROTOCOL_ALL
+ return item;
+});
+
+// ----------------- Utils ---------------------------------
+class Utils {
+
+ static notify(message, title = 'FoxyProxy') {
+ // the id is not used anywhere and can be omitted, it is only useful if you want to manually close the notification early
+ chrome.notifications.create('foxyproxy', {
+ type: 'basic',
+ iconUrl: '/images/icon.svg',
+ title,
+ message
+ });
+ }
+
+ // options | popup
+ static isUnsupportedType(type) {
+ //return type === PROXY_TYPE_PAC || type === PROXY_TYPE_WPAD || type === PROXY_TYPE_SYSTEM || type === PROXY_TYPE_PASS;
+ return [PROXY_TYPE_PAC, PROXY_TYPE_WPAD, PROXY_TYPE_SYSTEM, PROXY_TYPE_PASS].includes(type);
+ }
+
+ // bg | pattern-tester | validate-pattern
+ static wildcardToRegExp(pat) {
+
+ let start = 0, end = pat.length, matchOptionalSubdomains = false;
+
+ if (pat[0] === '.') { pat = '*' + pat; }
+
+ if (pat.startsWith('**')) {
+ // Strip asterisks from front and back
+ while (pat[start] === '*' && start < end) start++;
+ while (pat[end - 1] === '*' && start < end) end--;
+ // If there's only an asterisk left, match everything
+ if (end - start == 1 && pat[start] == '*') return '';
+ }
+ else if (pat.startsWith('*.')) { matchOptionalSubdomains = true; }
+
+ let regExpStr = pat.substring(start, end+1)
+ // $& replaces with the string found, but with that string escaped
+ .replace(/[$.+()^{}\]\[|]/g, '\\$&')
+ .replace(/\*/g, '.*')
+ .replace(/\?/g, '.');
+
+ if (matchOptionalSubdomains) {
+ // Non-capturing group that matches:
+ // any group of non-whitespace characters following by an optional . repeated zero or more times
+ regExpStr = '(?:\\S+\\.)*' + regExpStr.substring(4);
+ }
+
+ // Leading or ending double-asterisks mean exact starting and ending positions
+ if (start === 0) { regExpStr = '^' + regExpStr; }
+ if (end === pat.length) { regExpStr += '$'; }
+ return regExpStr;
+ }
+
+ // Prep the patternObject for matching: convert wildcards to regexp,
+ // store the originalPattern which the user entered so we can display if needed, etc.
+ // Return null if patternObject is inactive or there is an error.
+ static processPatternObject(patternObject) {
+ if (patternObject.active) {
+ // Store the original pattern so if this pattern matches something,
+ // we can display whatever the user entered ("original") in the log.
+ patternObject.originalPattern = patternObject.pattern;
+ if (patternObject.type === PATTERN_TYPE_WILDCARD) {
+ patternObject.pattern = Utils.wildcardToRegExp(patternObject.pattern);
+ }
+ try {
+ // Convert to real RegExp, not just a string. Validate. If invalid, notify user.
+ patternObject.pattern = new RegExp(patternObject.pattern, 'i');
+ return patternObject;
+ }
+ catch(e) {
+ console.error(`Error creating regexp for pattern: ${patternObject.pattern}`, e);
+ Utils.notify(`Error creating regular expression for pattern ${regExpStr}`);
+ }
+ }
+ return null;
+ }
+
+ // import | pattern
+ static importFile(file, mimeTypeArr, maxSizeBytes, jsonOrXml, callback) {
+
+ if (!file) {
+ alert('There was an error');
+ return;
+ }
+
+ // Check MIME type // Ch65 no filetype for JSON
+ if (!mimeTypeArr.includes(file.type)) {
+ alert('Unsupported file format');
+ return;
+ }
+
+ if (file.size > maxSizeBytes) {
+ alert('Filesize is too large');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ if (reader.error) {
+ alert('Error reading file.');
+ return;
+ }
+
+ let settings;
+ try {
+ if (jsonOrXml === 'json') { settings = JSON.parse(reader.result); }
+ else if (jsonOrXml === 'xml') {
+ settings = new DOMParser().parseFromString(reader.result, 'text/xml');
+ if (settings.documentElement.nodeName === 'parsererror') { throw new Error(); }
+ }
+ }
+ catch(e) {
+ console.log(e);
+ alert("Error parsing file. Please remove sensitive data from the file, and then email it to support@getfoxyproxy.org so we can fix bugs in our parser.");
+ return;
+ }
+ if (settings && confirm('This will overwite existing proxy settings. Are you sure?')) { callback(settings); }
+ else { callback(); }
+
+ };
+ reader.onerror = () => { alert('Error reading file'); };
+ reader.readAsText(file);
+ }
+
+ // import | options
+ static exportFile() {
+
+ chrome.storage.local.get(null, result => {
+ browser.runtime.getBrowserInfo().then((bi) => {
+ !result.sync ? Utils.saveAs(result, bi.version) : chrome.storage.sync.get(null, result => {
+ Utils.saveAs(result, bi.version, true);
+ });
+ });
+ });
+ }
+ // exportFile helper
+ static saveAs(data, browserVersion, sync) {
+
+ const settings = data; //Utils.prepareForSettings(data);
+ // Browser version and extension version. These are used for debugging.
+ settings.browserVersion = browserVersion;
+ settings.foxyProxyVersion = chrome.runtime.getManifest().version;
+ settings.foxyProxyEdition = FOXYPROXY_BASIC ? 'basic' : 'standard';
+ settings.sync = sync;
+ const blob = new Blob([JSON.stringify(settings, null, 2)], {type : 'text/plain;charset=utf-8'});
+ const filename = chrome.i18n.getMessage('extensionName') + '_' + new Date().toISOString().substring(0, 10) + '.json';
+ chrome.downloads.download({
+ url: URL.createObjectURL(blob),
+ filename,
+ saveAs: true,
+ conflictAction: 'uniquify'
+ });
+ }
+
+ static updateIcon(iconPath, color, title, titleIsKey, badgeText, badgeTextIsKey) {
+ chrome.browserAction.setIcon({path: iconPath});
+ if (color) {
+ chrome.browserAction.setBadgeBackgroundColor({color: color});
+ }
+ else {
+ // TODO: confirm this is OK to do
+ chrome.browserAction.setBadgeBackgroundColor({color: null});
+ }
+ if (title) {
+ chrome.browserAction.setTitle({title: 'FoxyProxy: ' + (titleIsKey ? chrome.i18n.getMessage(title) : title)});
+ }
+ else {
+ chrome.browserAction.setTitle({title: ''});
+ }
+ if (badgeText) {
+ chrome.browserAction.setBadgeText({text: badgeTextIsKey ? chrome.i18n.getMessage(badgeText) : badgeText});
+ }
+ else {
+ chrome.browserAction.setBadgeText({text: ''});
+ }
+ }
+
+ static getProxyTitle(proxySetting) {
+ if (proxySetting.title) {
+ return proxySetting.title;
+ }
+ else if (proxySetting.type === PROXY_TYPE_NONE) {
+ return 'Direct (no proxy)';
+ }
+ else {
+ return `${proxySetting.address}:${proxySetting.port}`;
+ }
+ }
+
+/*
+ // utils only used for export, will be removed as DB format export is adapted
+ static prepareForSettings(settings = {}) {
+
+ //if (settings && !settings.mode) { }// 5.0 settings
+
+ let lastResortFound = false;
+ const prefKeys = Object.keys(settings);
+
+ const def = {
+ 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: []
+ };
+
+ // base format
+ const ret = {
+ mode: 'disabled',
+ proxySettings: [],
+ logging: {
+ size: 500,
+ active: true
+ }
+ };
+
+ if (!prefKeys.length) { // settings is {}
+ ret.proxySettings = [def];
+ return ret;
+ }
+
+ prefKeys.forEach(key => {
+
+ switch (key) {
+
+ case 'mode':
+ case 'logging':
+ ret[key] = settings[key];
+ break;
+
+ case 'sync': break; // do nothing
+
+ default:
+ const temp = settings[key];
+ temp.id = key; // Copy the id into the object
+ temp.id === LASTRESORT && (lastResortFound = true);
+ ret.proxySettings.push(temp);
+ }
+ });
+
+ ret.proxySettings.sort((a, b) => a.index - b.index);
+ ret.proxySettings.forEach(item => delete item.index); // Re-calculated when/if this object is written to disk again (user may move proxySetting up/down)
+
+ !lastResortFound && ret.proxySettings.push(def); // add default lastresort
+
+ return ret;
+ }
+*/
+
+ static getUniqueId() {
+ // We don't need cryptographically secure UUIDs, just something unique
+ return Math.random().toString(36).substring(7) + new Date().getTime();
+ }
+
+ static stripBadChars(str) {
+ return str ? str.replace(/[&<>"']+/g, '') : null;
+ }
+
+}
diff --git a/src/styles/app.css b/src/styles/app.css
new file mode 100644
index 0000000..57a87b7
--- /dev/null
+++ b/src/styles/app.css
@@ -0,0 +1,629 @@
+body {
+ background: #630;
+ color: #333;
+ font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
+ margin: 0;
+ padding: 1em;
+ animation: fadein 0.3s ease-in-out;
+}
+
+body::after {
+ content: '';
+ background-image: url('/images/logo.svg');
+ background-repeat: no-repeat;
+ background-size: contain;
+ position: absolute;
+ bottom: 0.5em;
+ right: 0.5em;
+ display: inline-block;
+ width: 128px;
+ height: 128px;
+ opacity: 0.3;
+ z-index: -1;
+}
+
+@keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+* {
+ box-sizing: border-box;
+}
+
+.flex {
+ display: flex;
+}
+
+.flex > * {
+ flex: 1;
+}
+
+img {
+ vertical-align: text-bottom;
+}
+
+
+a {
+ color: #f90;
+ text-decoration: none;
+ transition: all 0.5s ease-in-out;
+}
+
+a:hover {
+ color: #c60;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #c60;
+}
+
+fieldset {
+ border-radius: 10px;
+ margin-bottom: 1em;
+ border: 1px solid #ccc;
+}
+
+legend {
+ color: #c60;
+ font-size: 1.1em;
+}
+
+table {
+ border-spacing: 0;
+ width: 100%;
+}
+
+th {
+ background-color: #eee;
+ padding: 0.4em;
+ text-align: left;
+ border-bottom: 1px solid #fff;
+}
+
+tr{
+ background-color: #f8f8f8;
+}
+
+tbody tr:nth-child(even) {
+ background-color: #f5fffa;
+}
+tbody tr:hover {
+ background-color: #cae1d3;
+}
+
+td {
+ padding: 0.2em;
+}
+
+.ellipsis {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+
+
+
+button, .button {
+ background: #f90;
+ border-radius: 3px;
+ border: 0;
+ color: #fff;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 0.9em;
+ font-weight: bold;
+ margin: 0 0 0 0.5em;
+ min-width: 6em;
+ padding: 0.4em 1em;
+ text-align: center;
+ transition: all 0.3s ease-out;
+ vertical-align: middle;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+}
+
+button:not(.plain):hover, .button:hover {
+ background: #e37f08;
+ box-shadow: 0px 1px 5px rgba(0,0,0,0.3);
+}
+
+button.small {
+ font-size: 0.8em;
+ padding: 0.3em 0.5em;
+ min-width: 2em;
+}
+
+button.alert {
+ background: #ec5840;
+}
+button.alert:hover {
+ background: #dc143c;
+}
+
+button.plain {
+ background: transparent;
+ padding: 0;
+ margin: 0;
+ min-width: 1em;
+}
+
+button[type="submit"] {
+ background-color: #4682b4;
+}
+
+button[type="submit"]:hover {
+ background-color: #05a;
+}
+
+
+
+label {
+/* font-size: 0.9em;*/
+}
+
+
+input::placeholder {
+ font-style: italic;
+}
+
+input[type="file"] {
+ display: none;
+}
+
+[type="text"], [type="password"], [type="url"], textarea {
+ background-color: #fefefe;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1);
+ color: #999;
+ display: inline-block;
+ font-size: 1em;
+ margin: 0 0 1em;
+ padding: 0.5em;
+ transition: all 0.5s;
+ width: 100%;
+}
+
+[type="text"]:focus, [type="password"]:focus, [type="url"]:focus, textarea:focus {
+ border: 1px solid #999;
+ box-shadow: 0 0 5px #cacaca;
+}
+
+
+
+select {
+ background-color: #f8f8ff;
+ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="24" viewBox="0 0 32 24" fill="rgb(255, 153, 0)"><polygon points="0,0 32,0 16,24"></polygon></svg>');
+ background-position: right 0.5em center;
+ background-repeat: no-repeat;
+ background-size: 14px;
+ border-radius: 5px;
+ border: 1px solid #ccc;
+ color: #999;
+ font-size: 1em;
+ margin: 0 0 1em;
+ padding: 0.5em;
+ width: 100%;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+}
+
+select:disabled {
+ background-color: #e6e6e6;
+ cursor: default;
+}
+
+select[multiple] {
+ height: auto;
+}
+
+/* ----- switch ----- */
+input.switch {
+ display: none;
+}
+
+input.switch + label {
+ background: #ddd;
+ border-radius: 5px;
+ box-shadow: 0px 1px 3px rgba(0,0,0,0.5);
+ cursor: pointer;
+ display: inline-block;
+ height: 1.5em;
+ margin-right: 0.5em;
+ position: relative;
+ text-align: left;
+ transition: all 0.5s ease-in-out;
+ vertical-align: middle;
+ width: 3.5em;
+ font-size: 0.9em;
+}
+
+input.switch + label::before {
+ background: #ccc;
+ border-radius: 5px;
+ box-shadow:inset 0px 1px 3px rgba(0,0,0,0.5);
+ content: '';
+ display: block;
+ height: 1.1em;
+ left: 0.2em;
+ position: absolute;
+ top: 0.2em;
+ transition: all 0.5s ease-in-out;
+ width: 1.1em;
+}
+
+input.switch:checked + label {
+ background: #f90;
+}
+
+input.switch:checked + label::before {
+ left: 2.2em;
+ background: #fff;
+}
+
+input.switch + label::after {
+ display: inline-block;
+ content: 'Off';
+ color: #fff;
+ margin-left: 1.8em;
+ transition: all 0.5s ease-in-out;
+ font-weight: bold;
+ vertical-align: middle;
+}
+
+input.switch:checked + label::after {
+ content: 'On';
+ margin-left: 0.2em;
+}
+/* ----- /switch ----- */
+
+.template {
+ display: none !important;
+}
+
+.invalid {
+ background-color: rgba(236, 88, 64, 0.1);
+ border-color: #ec5840;
+}
+
+.hide {
+ display: none;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.prime {
+ margin: 0 0 1em 0;
+ padding: 1em;
+ border-radius: 5px;
+ background-color: white;
+}
+
+.prime.alert {
+ background-color: #fce6e2;
+ border: 1px solid #ccc;
+}
+
+.prime.warning {
+ background-color: #fff3d9;
+ border: 1px solid #ccc;
+}
+
+.prime.small {
+ padding: 0.5em;
+ border: 1px solid #ccc;
+}
+
+.prime.tiny {
+ margin: 0;
+ padding: 0.1em;
+ font-size: 0.8em;
+ border: 1px solid #ccc;
+}
+
+
+
+.fp-orange, .orangehover:hover {
+ color: #f90;
+}
+
+
+.flag {
+ font-size: 1.5em;
+ cursor: help;
+ width: 1em;
+ display: inline-block;
+ margin-left: 0.5em;
+}
+
+.success {
+ background-color: #e1faea;
+}
+
+.success:hover {
+ background-color: #cae1d3;
+}
+
+.secondary {
+ background-color: #ebebeb;
+}
+
+.secondary:hover {
+ background: #d4d4d4;
+}
+
+.unsupported {
+ background-color: #fce6e2;
+ border: 1px solid #ccc;
+}
+
+.unsupported:hover {
+ background-color: #e1ceca;
+ color: #fff;
+}
+
+
+.header {
+ color: #c60;
+ font-size: 1.5em;
+ font-weight: bold;
+ padding: 0.5em;
+ margin: 0 0 0.5rem 0;
+}
+
+.header::before {
+ content: '';
+ background-image: url('/images/icon.svg');
+ background-repeat: no-repeat;
+ background-size: contain;
+ vertical-align: middle;
+ margin-right: 0.5em;
+ display: inline-block;
+ width: 48px;
+ height: 48px;
+}
+
+.header.browserPopup {
+ font-size: 1.4em;
+ padding: 0.4em;
+}
+
+.header.browserPopup::before {
+ width: 36px;
+ height: 36px;
+}
+
+.scroll {
+ max-height: 50vh;
+ overflow-y: auto;
+ scrollbar-color: #ddd #f5f5f5;
+ scrollbar-width: thin;
+}
+
+
+
+.spinner {
+ align-items: center;
+ background-color: rgba(255, 255, 255, 0.8);
+ color: #f90;
+ display: flex;
+ font-size: 14em;
+ height: 100%;
+ justify-content: center;
+ left: 0;
+ margin: 0;
+ opacity: 0;
+ position: fixed;
+ top: 0;
+ transition: all 0.5s ease-in-out;
+ width: 100%;
+ z-index: 10000;
+}
+
+.spinner.on {
+ opacity: 0.5;
+}
+
+
+
+
+
+
+/* ----- Popup ----- */
+.popup {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.8);
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ z-index: 10000;
+ transition: all 0.5s ease-in-out;
+}
+
+.popup.on {
+ opacity: 1;
+}
+.popup.on div:first-child {
+ transform: scale(1);
+}
+
+.popup div:first-child {
+ display: inline-block;
+ background-color: #fff;
+ border-radius: 10px;
+ box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.2), 0 10px 15px 0 rgba(0, 0, 0, 0.3);
+ transform: scale(0.8);
+ transition: all 0.5s ease-in-out;
+ width: 50vw;
+}
+
+.popup div div {
+ margin: 1em;
+}
+
+.popup h3 {
+ color: #fff;
+ background-color: #630;
+ margin: 0 0 1em;
+ padding: 0.5em;
+ border-radius: 10px 10px 0 0 ;
+}
+
+.popup h3::before {
+ content: '';
+ background-image: url('/images/icon.svg');
+ background-size: contain;
+ vertical-align: middle;
+ margin-right: 0.5em;
+ display: inline-block;
+ width: 36px;
+ height: 36px;
+}
+
+.popup h4 {
+ margin-bottom: 0;
+}
+
+.popup p {
+ margin: 0;
+}
+
+/* ----- /Popup ----- */
+
+
+/* ----- fa ----- */
+i.fa {
+ color: #f90;
+ display: inline-block;
+ text-align: center;
+ transition: all 0.5s ease-in-out;
+}
+i.fa:hover {
+
+}
+
+a i.fa {
+ color: inherit;
+}
+
+i.fa.fa-eye {
+ font-size: 1.2em;
+}
+
+input[type="password"] + button i.fa.fa-eye {
+ color: #f90;
+}
+input[type="text"] + button i.fa.fa-eye {
+ color: #ccc;
+}
+
+
+
+/* ----- tooltip ----- */
+.tooltip {
+ position: relative;
+ cursor: help;
+ display: inline-block;
+}
+
+.tooltip .tooltiptext {
+ visibility: hidden;
+ width: 20em;
+ background-color: #630;
+ color: white;
+ text-align: center;
+ border-radius: 3px;
+ padding: .8em;
+ position: absolute;
+ z-index: 1;
+ bottom: 125%;
+ left: 10%;
+ margin-left: -2em;
+ opacity: 0;
+ transition: all 0.5s ease-in-out;
+ font-weight: normal;
+ box-shadow: 0px 1px 5px rgba(0,0,0,0.5);
+ font-size: 0.9rem;
+}
+
+.tooltip .tooltiptext::after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ left: 1em;
+ border-width: 5px;
+ border-style: solid;
+ border-color: #555 transparent transparent transparent;
+}
+
+.tooltip:hover .tooltiptext {
+ visibility: visible;
+ opacity: 1;
+}
+
+.tooltiptext.center {
+ margin-left: -10em;
+}
+
+.tooltiptext.center::after {
+ left: 50%;
+}
+
+.tooltiptext.bottom {
+ top: 125%;
+ bottom: auto;
+}
+
+.tooltiptext.bottom::after {
+ bottom: 100%;
+ top: auto;
+ left: 75%;
+ border-color: transparent transparent #555 transparent;
+}
+
+.tooltip i.fa {
+ font-size: 1.2em;
+ color: black;
+}
+
+.tooltip i.fa:hover {
+ color: #f90;
+}
+
+.tooltiptable {
+ /* https://stackoverflow.com/questions/20626685/better-way-to-set-distance-between-flexbox-items */
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ text-align: left;
+}
+
+.tooltiptable > div {
+ /*
+ 1/ - 2 columns per row
+ .5em - spacing between rows
+ */
+ box-sizing: border-box;
+ margin-bottom: .5em;
+ width: calc(1/2*100% - (1 - 1/2)*10px);
+}
+
+.tooltiptablefooter {
+ margin-top: 1em;
+ text-align: justify;
+}
+
+.monospace {
+ font-family: monospace;
+}
diff --git a/src/styles/images/animated-overlay.gif b/src/styles/images/animated-overlay.gif
new file mode 100644
index 0000000..d441f75
--- /dev/null
+++ b/src/styles/images/animated-overlay.gif
Binary files differ
diff --git a/src/styles/images/ui-bg_flat_0_888888_40x100.png b/src/styles/images/ui-bg_flat_0_888888_40x100.png
new file mode 100755
index 0000000..7cd111a
--- /dev/null
+++ b/src/styles/images/ui-bg_flat_0_888888_40x100.png
Binary files differ
diff --git a/src/styles/images/ui-bg_flat_0_aaaaaa_40x100.png b/src/styles/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000..f5d7830
--- /dev/null
+++ b/src/styles/images/ui-bg_flat_0_aaaaaa_40x100.png
Binary files differ
diff --git a/src/styles/images/ui-bg_flat_75_ffffff_40x100.png b/src/styles/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000..43b8cf3
--- /dev/null
+++ b/src/styles/images/ui-bg_flat_75_ffffff_40x100.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_25_e1f0f5_1x400.png b/src/styles/images/ui-bg_glass_25_e1f0f5_1x400.png
new file mode 100755
index 0000000..42f8788
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_25_e1f0f5_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_55_444444_1x400.png b/src/styles/images/ui-bg_glass_55_444444_1x400.png
new file mode 100755
index 0000000..b01b129
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_55_444444_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_55_fbf9ee_1x400.png b/src/styles/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 0000000..5f3d3f5
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_55_fbf9ee_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_65_ffffff_1x400.png b/src/styles/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..a673c51
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_65_ffffff_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_75_dadada_1x400.png b/src/styles/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 0000000..3267aac
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_75_dadada_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_75_e6e6e6_1x400.png b/src/styles/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 0000000..bb70394
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_75_e6e6e6_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_glass_95_fef1ec_1x400.png b/src/styles/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 0000000..9fd60c8
--- /dev/null
+++ b/src/styles/images/ui-bg_glass_95_fef1ec_1x400.png
Binary files differ
diff --git a/src/styles/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/src/styles/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 0000000..03443d9
--- /dev/null
+++ b/src/styles/images/ui-bg_highlight-soft_75_cccccc_1x100.png
Binary files differ
diff --git a/src/styles/images/ui-bg_inset-soft_95_fef1ec_1x100.png b/src/styles/images/ui-bg_inset-soft_95_fef1ec_1x100.png
new file mode 100755
index 0000000..0e05810
--- /dev/null
+++ b/src/styles/images/ui-bg_inset-soft_95_fef1ec_1x100.png
Binary files differ
diff --git a/src/styles/images/ui-icons_222222_256x240.png b/src/styles/images/ui-icons_222222_256x240.png
new file mode 100644
index 0000000..8a188cb
--- /dev/null
+++ b/src/styles/images/ui-icons_222222_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_2e83ff_256x240.png b/src/styles/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 0000000..4a50965
--- /dev/null
+++ b/src/styles/images/ui-icons_2e83ff_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_309bbf_256x240.png b/src/styles/images/ui-icons_309bbf_256x240.png
new file mode 100755
index 0000000..d6f62ed
--- /dev/null
+++ b/src/styles/images/ui-icons_309bbf_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_444444_256x240.png b/src/styles/images/ui-icons_444444_256x240.png
new file mode 100644
index 0000000..c2daae1
--- /dev/null
+++ b/src/styles/images/ui-icons_444444_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_454545_256x240.png b/src/styles/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000..c102dd8
--- /dev/null
+++ b/src/styles/images/ui-icons_454545_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_555555_256x240.png b/src/styles/images/ui-icons_555555_256x240.png
new file mode 100644
index 0000000..4784928
--- /dev/null
+++ b/src/styles/images/ui-icons_555555_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_777620_256x240.png b/src/styles/images/ui-icons_777620_256x240.png
new file mode 100644
index 0000000..d2f58d2
--- /dev/null
+++ b/src/styles/images/ui-icons_777620_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_777777_256x240.png b/src/styles/images/ui-icons_777777_256x240.png
new file mode 100644
index 0000000..1d53258
--- /dev/null
+++ b/src/styles/images/ui-icons_777777_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_888888_256x240.png b/src/styles/images/ui-icons_888888_256x240.png
new file mode 100644
index 0000000..43630ad
--- /dev/null
+++ b/src/styles/images/ui-icons_888888_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_bf3030_256x240.png b/src/styles/images/ui-icons_bf3030_256x240.png
new file mode 100755
index 0000000..c6cfba0
--- /dev/null
+++ b/src/styles/images/ui-icons_bf3030_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_cc0000_256x240.png b/src/styles/images/ui-icons_cc0000_256x240.png
new file mode 100644
index 0000000..2825f20
--- /dev/null
+++ b/src/styles/images/ui-icons_cc0000_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_cd0a0a_256x240.png b/src/styles/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 0000000..72e3b43
--- /dev/null
+++ b/src/styles/images/ui-icons_cd0a0a_256x240.png
Binary files differ
diff --git a/src/styles/images/ui-icons_ffffff_256x240.png b/src/styles/images/ui-icons_ffffff_256x240.png
new file mode 100755
index 0000000..42f8f99
--- /dev/null
+++ b/src/styles/images/ui-icons_ffffff_256x240.png
Binary files differ