Browse Source

added monitoring local file modification, redesigned code structure

Signed-off-by: Gerald <[email protected]>
Gerald 10 years ago
parent
commit
f39df9f51d

+ 2 - 0
.gitignore

@@ -1 +1,3 @@
 dist/
+node_modules/
+*.nex

+ 36 - 0
gulpfile.js

@@ -0,0 +1,36 @@
+#!node
+var gulp = require('gulp');
+var inject = require('gulp-inject');
+var less = require('gulp-less');
+var minifyCss = require('gulp-minify-css');
+
+gulp.task('inject-html', function() {
+	return gulp.src('src/*.html')
+		.pipe(inject(
+			gulp.src('src/style.less')
+				.pipe(less())
+				.pipe(minifyCss())
+				.pipe(gulp.dest('dist/')),
+			{
+				name: 'common',
+				ignorePath: '/dist/',
+				addRootSlash: false,
+			}
+		))
+		.pipe(gulp.dest('dist/'));
+});
+
+gulp.task('copy-files',function(){
+	return gulp.src([
+		'src/*.json',
+		'src/*.js',
+		'src/_locales/**/*',
+		'src/images/*',
+		'src/lib/**/*',
+		'!src/lib/*',
+		'src/mylib/**/*',
+	], {base:'src'})
+		.pipe(gulp.dest('dist/'));
+});
+
+gulp.task('default', ['inject-html', 'copy-files']);

+ 21 - 0
package.json

@@ -0,0 +1,21 @@
+{
+  "name": "Violentmonkey",
+  "version": "1.0.0",
+  "description": "Violentmonkey",
+  "devDependencies": {
+    "gulp": "^3.8.11",
+    "gulp-inject": "^1.2.0",
+    "gulp-less": "^3.0.2",
+    "gulp-minify-css": "^1.0.0"
+  },
+  "author": "Gerald <[email protected]>",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/gera2ld/Violentmonkey.git"
+  },
+  "bugs": {
+    "url": "https://github.com/gera2ld/Violentmonkey/issues"
+  },
+  "homepage": "https://github.com/gera2ld/Violentmonkey",
+  "license": "ISC"
+}

+ 33 - 21
src/_locales/cs/messages.json

@@ -59,10 +59,18 @@
 		"description": "Shown in the title of the confirm page while trying to install a script.",
 		"message": "Instaluji skript"
 	},
-	"optionClose": {
+	"buttonInstallOptions": {
+		"description": "Button to show options of installation confirm page.",
+		"message": "Options"
+	},
+	"installOptionClose": {
 		"description": "Option to close confirm window after installation.",
 		"message": "Close po instalaci"
 	},
+	"installOptionTrack": {
+		"description": "Option to track the loading local file before window is closed.",
+		"message": "Track local file before this window is closed"
+	},
 	"buttonConfirmInstallation": {
 		"description": "Button to confirm installation of a script.",
 		"message": "Potvrdit instalaci"
@@ -71,10 +79,6 @@
 		"description": "Button to close window.",
 		"message": "Zavřít"
 	},
-	"msgScriptURL": {
-		"description": "URL of the script to be installed on confirm page.",
-		"message": "Skript URL: $1"
-	},
 	"msgErrorLoadingData": {
 		"description": "Message shown on confirm page when the script to be installed cannot be loaded.",
 		"message": "Chyba při načítání skript."
@@ -83,10 +87,6 @@
 		"description": "Message shown in the confirm page when a javascript file to be installed is loaded.",
 		"message": "Skript načten."
 	},
-	"msgLoadingData": {
-		"description": "Message shown on confirm page when the script to be installed is loading.",
-		"message": "Nahrávám skript..."
-	},
 	"msgErrorLoadingDependency": {
 		"description": "Message shown when not all requirements are loaded successfully.",
 		"message": "Chyba při načítání požadavky."
@@ -95,6 +95,14 @@
 		"description": "Message shown on confirm page when the requirements are being downloaded.",
 		"message": "Vkládání požadavky... ($1/$2)"
 	},
+	"msgScriptURL": {
+		"description": "URL of the script to be installed on confirm page.",
+		"message": "Skript URL: $1"
+	},
+	"msgLoadingData": {
+		"description": "Message shown on confirm page when the script to be installed is loading.",
+		"message": "Nahrávám skript..."
+	},
 	"sideMenuInstalled": {
 		"description": "Side menu: Installed scripts",
 		"message": "Nainstalované skripty"
@@ -295,6 +303,10 @@
 		"description": "Button to save modifications of a script and then close the editing page.",
 		"message": "Uložit a Zavřít"
 	},
+	"labelNoScripts": {
+		"description": "Message shown when no script is installed.",
+		"message": "Oops, you haven't got any script yet."
+	},
 	"hintSupportPage": {
 		"description": "Hint for support page.",
 		"message": "Support page"
@@ -319,30 +331,30 @@
 		"description": "Check a script for updates.",
 		"message": "Zkontrolovat aktualizace"
 	},
-	"confirmNotSaved": {
-		"description": "Confirm message shown when there are unsaved script modifications.",
-		"message": "Modifikace není uložena! Klikni na OK pro zbavení se jí, nebo na zrušit pro zůstání zde."
-	},
 	"msgImported": {
 		"description": "Message shown after import. There is an argument referring to the count of scripts imported.",
 		"message": "$1 vec(i) jsou importovány."
 	},
-	"buttonVacuuming": {
-		"description": "Message shown when data vacuum is in progress.",
-		"message": "Čištení dat..."
-	},
-	"hintVacuum": {
-		"description": "Hint for vacuuming data.",
-		"message": "Zbavit se nadbytečného a zkusit znovu načíst chybějící zdroje v cache."
-	},
 	"hintUseDownloadURL": {
 		"description": "Shown as a place holder for @updateURL when it is not assigned",
 		"message": "Use @downloadURL"
 	},
+	"confirmNotSaved": {
+		"description": "Confirm message shown when there are unsaved script modifications.",
+		"message": "Modifikace není uložena! Klikni na OK pro zbavení se jí, nebo na zrušit pro zůstání zde."
+	},
+	"buttonVacuuming": {
+		"description": "Message shown when data vacuum is in progress.",
+		"message": "Čištení dat..."
+	},
 	"buttonVacuumed": {
 		"description": "Message shown when data is vacuumed.",
 		"message": "Data vyprázdněna"
 	},
+	"hintVacuum": {
+		"description": "Hint for vacuuming data.",
+		"message": "Zbavit se nadbytečného a zkusit znovu načíst chybějící zdroje v cache."
+	},
 	"menuManageScripts": {
 		"description": "Menu item to manage scripts, or to open the options page of the extension.",
 		"message": "Řízení skriptů"

+ 33 - 21
src/_locales/de/messages.json

@@ -59,10 +59,18 @@
 		"description": "Shown in the title of the confirm page while trying to install a script.",
 		"message": "Skript installieren"
 	},
-	"optionClose": {
+	"buttonInstallOptions": {
+		"description": "Button to show options of installation confirm page.",
+		"message": "Options"
+	},
+	"installOptionClose": {
 		"description": "Option to close confirm window after installation.",
 		"message": "Nach der Installation schließen"
 	},
+	"installOptionTrack": {
+		"description": "Option to track the loading local file before window is closed.",
+		"message": "Track local file before this window is closed"
+	},
 	"buttonConfirmInstallation": {
 		"description": "Button to confirm installation of a script.",
 		"message": "Installation bestätigen"
@@ -71,10 +79,6 @@
 		"description": "Button to close window.",
 		"message": "Schließen"
 	},
-	"msgScriptURL": {
-		"description": "URL of the script to be installed on confirm page.",
-		"message": "Skript URL: $1"
-	},
 	"msgErrorLoadingData": {
 		"description": "Message shown on confirm page when the script to be installed cannot be loaded.",
 		"message": "Fehler beim Laden der Skriptdaten."
@@ -83,10 +87,6 @@
 		"description": "Message shown in the confirm page when a javascript file to be installed is loaded.",
 		"message": "Skriptdaten geladen"
 	},
-	"msgLoadingData": {
-		"description": "Message shown on confirm page when the script to be installed is loading.",
-		"message": "Skriptdaten werden geladen..."
-	},
 	"msgErrorLoadingDependency": {
 		"description": "Message shown when not all requirements are loaded successfully.",
 		"message": "Fehler beim Laden der Abhängigkeiten."
@@ -95,6 +95,14 @@
 		"description": "Message shown on confirm page when the requirements are being downloaded.",
 		"message": "Lade Abhängigkeiten... ($1/$2)"
 	},
+	"msgScriptURL": {
+		"description": "URL of the script to be installed on confirm page.",
+		"message": "Skript URL: $1"
+	},
+	"msgLoadingData": {
+		"description": "Message shown on confirm page when the script to be installed is loading.",
+		"message": "Skriptdaten werden geladen..."
+	},
 	"sideMenuInstalled": {
 		"description": "Side menu: Installed scripts",
 		"message": "Installierte Skripte"
@@ -295,6 +303,10 @@
 		"description": "Button to save modifications of a script and then close the editing page.",
 		"message": "Speichern & Schließen"
 	},
+	"labelNoScripts": {
+		"description": "Message shown when no script is installed.",
+		"message": "Oops, you haven't got any script yet."
+	},
 	"hintSupportPage": {
 		"description": "Hint for support page.",
 		"message": "Support"
@@ -319,30 +331,30 @@
 		"description": "Check a script for updates.",
 		"message": "Auf Aktualisierung prüfen"
 	},
-	"confirmNotSaved": {
-		"description": "Confirm message shown when there are unsaved script modifications.",
-		"message": "Die Änderungen wurden nicht gespreichert!Zum Verwerfen OK drücken oder Abbrechen um fortzufahren."
-	},
 	"msgImported": {
 		"description": "Message shown after import. There is an argument referring to the count of scripts imported.",
 		"message": "$1 Skript(e) wurden importiert."
 	},
-	"buttonVacuuming": {
-		"description": "Message shown when data vacuum is in progress.",
-		"message": "Säubere Daten..."
-	},
-	"hintVacuum": {
-		"description": "Hint for vacuuming data.",
-		"message": "Redundazen verwerfen und fehlenden Ressourcen aus dem Cache neuladen."
-	},
 	"hintUseDownloadURL": {
 		"description": "Shown as a place holder for @updateURL when it is not assigned",
 		"message": "Verwende @downloadURL"
 	},
+	"confirmNotSaved": {
+		"description": "Confirm message shown when there are unsaved script modifications.",
+		"message": "Die Änderungen wurden nicht gespreichert!Zum Verwerfen OK drücken oder Abbrechen um fortzufahren."
+	},
+	"buttonVacuuming": {
+		"description": "Message shown when data vacuum is in progress.",
+		"message": "Säubere Daten..."
+	},
 	"buttonVacuumed": {
 		"description": "Message shown when data is vacuumed.",
 		"message": "Daten gesäubert"
 	},
+	"hintVacuum": {
+		"description": "Hint for vacuuming data.",
+		"message": "Redundazen verwerfen und fehlenden Ressourcen aus dem Cache neuladen."
+	},
 	"menuManageScripts": {
 		"description": "Menu item to manage scripts, or to open the options page of the extension.",
 		"message": "Skripte verwalten"

+ 33 - 21
src/_locales/en/messages.json

@@ -59,10 +59,18 @@
 		"description": "Shown in the title of the confirm page while trying to install a script.",
 		"message": "Installing script"
 	},
-	"optionClose": {
+	"buttonInstallOptions": {
+		"description": "Button to show options of installation confirm page.",
+		"message": "Options"
+	},
+	"installOptionClose": {
 		"description": "Option to close confirm window after installation.",
 		"message": "Close after installation"
 	},
+	"installOptionTrack": {
+		"description": "Option to track the loading local file before window is closed.",
+		"message": "Track local file before this window is closed"
+	},
 	"buttonConfirmInstallation": {
 		"description": "Button to confirm installation of a script.",
 		"message": "Confirm installation"
@@ -71,10 +79,6 @@
 		"description": "Button to close window.",
 		"message": "Close"
 	},
-	"msgScriptURL": {
-		"description": "URL of the script to be installed on confirm page.",
-		"message": "Script URL: $1"
-	},
 	"msgErrorLoadingData": {
 		"description": "Message shown on confirm page when the script to be installed cannot be loaded.",
 		"message": "Error loading script data."
@@ -83,10 +87,6 @@
 		"description": "Message shown in the confirm page when a javascript file to be installed is loaded.",
 		"message": "Script data loaded."
 	},
-	"msgLoadingData": {
-		"description": "Message shown on confirm page when the script to be installed is loading.",
-		"message": "Loading script data..."
-	},
 	"msgErrorLoadingDependency": {
 		"description": "Message shown when not all requirements are loaded successfully.",
 		"message": "Error loading dependencies."
@@ -95,6 +95,14 @@
 		"description": "Message shown on confirm page when the requirements are being downloaded.",
 		"message": "Loading dependencies... ($1/$2)"
 	},
+	"msgScriptURL": {
+		"description": "URL of the script to be installed on confirm page.",
+		"message": "Script URL: $1"
+	},
+	"msgLoadingData": {
+		"description": "Message shown on confirm page when the script to be installed is loading.",
+		"message": "Loading script data..."
+	},
 	"sideMenuInstalled": {
 		"description": "Side menu: Installed scripts",
 		"message": "Installed scripts"
@@ -295,6 +303,10 @@
 		"description": "Button to save modifications of a script and then close the editing page.",
 		"message": "Save & Close"
 	},
+	"labelNoScripts": {
+		"description": "Message shown when no script is installed.",
+		"message": "Oops, you haven't got any script yet."
+	},
 	"hintSupportPage": {
 		"description": "Hint for support page.",
 		"message": "Support page"
@@ -319,30 +331,30 @@
 		"description": "Check a script for updates.",
 		"message": "Check for updates"
 	},
-	"confirmNotSaved": {
-		"description": "Confirm message shown when there are unsaved script modifications.",
-		"message": "Modifications are not saved!\nClick OK to discard them or cancel to stay."
-	},
 	"msgImported": {
 		"description": "Message shown after import. There is an argument referring to the count of scripts imported.",
 		"message": "$1 item(s) are imported."
 	},
-	"buttonVacuuming": {
-		"description": "Message shown when data vacuum is in progress.",
-		"message": "Vacuuming data..."
-	},
-	"hintVacuum": {
-		"description": "Hint for vacuuming data.",
-		"message": "Discard the redundancy and try to reload the missing resources in cache."
-	},
 	"hintUseDownloadURL": {
 		"description": "Shown as a place holder for @updateURL when it is not assigned",
 		"message": "Use @downloadURL"
 	},
+	"confirmNotSaved": {
+		"description": "Confirm message shown when there are unsaved script modifications.",
+		"message": "Modifications are not saved!\nClick OK to discard them or cancel to stay."
+	},
+	"buttonVacuuming": {
+		"description": "Message shown when data vacuum is in progress.",
+		"message": "Vacuuming data..."
+	},
 	"buttonVacuumed": {
 		"description": "Message shown when data is vacuumed.",
 		"message": "Data vacuumed"
 	},
+	"hintVacuum": {
+		"description": "Hint for vacuuming data.",
+		"message": "Discard the redundancy and try to reload the missing resources in cache."
+	},
 	"menuManageScripts": {
 		"description": "Menu item to manage scripts, or to open the options page of the extension.",
 		"message": "Manage scripts"

+ 33 - 21
src/_locales/pl/messages.json

@@ -59,10 +59,18 @@
 		"description": "Shown in the title of the confirm page while trying to install a script.",
 		"message": "Instalator skryptu"
 	},
-	"optionClose": {
+	"buttonInstallOptions": {
+		"description": "Button to show options of installation confirm page.",
+		"message": "Options"
+	},
+	"installOptionClose": {
 		"description": "Option to close confirm window after installation.",
 		"message": "Zamknij instalator tego skryptu po zakończeniu instalacji skryptu"
 	},
+	"installOptionTrack": {
+		"description": "Option to track the loading local file before window is closed.",
+		"message": "Track local file before this window is closed"
+	},
 	"buttonConfirmInstallation": {
 		"description": "Button to confirm installation of a script.",
 		"message": "Potwierdź instalację skryptu"
@@ -71,10 +79,6 @@
 		"description": "Button to close window.",
 		"message": "Zamknij edytor/instalator"
 	},
-	"msgScriptURL": {
-		"description": "URL of the script to be installed on confirm page.",
-		"message": "Adres URL skryptu: $1"
-	},
 	"msgErrorLoadingData": {
 		"description": "Message shown on confirm page when the script to be installed cannot be loaded.",
 		"message": "Błąd wczytywania skryptu."
@@ -83,10 +87,6 @@
 		"description": "Message shown in the confirm page when a javascript file to be installed is loaded.",
 		"message": "Skrypt wczytany."
 	},
-	"msgLoadingData": {
-		"description": "Message shown on confirm page when the script to be installed is loading.",
-		"message": "Wczytywanie skryptu..."
-	},
 	"msgErrorLoadingDependency": {
 		"description": "Message shown when not all requirements are loaded successfully.",
 		"message": "Błąd podczas wczytywania wymagań."
@@ -95,6 +95,14 @@
 		"description": "Message shown on confirm page when the requirements are being downloaded.",
 		"message": "Wczytywanie wymagań... ($1/$2)"
 	},
+	"msgScriptURL": {
+		"description": "URL of the script to be installed on confirm page.",
+		"message": "Adres URL skryptu: $1"
+	},
+	"msgLoadingData": {
+		"description": "Message shown on confirm page when the script to be installed is loading.",
+		"message": "Wczytywanie skryptu..."
+	},
 	"sideMenuInstalled": {
 		"description": "Side menu: Installed scripts",
 		"message": "Zainstalowane skrypty"
@@ -295,6 +303,10 @@
 		"description": "Button to save modifications of a script and then close the editing page.",
 		"message": "Zapisz skrypt i zamknij edytor"
 	},
+	"labelNoScripts": {
+		"description": "Message shown when no script is installed.",
+		"message": "Oops, you haven't got any script yet."
+	},
 	"hintSupportPage": {
 		"description": "Hint for support page.",
 		"message": "Strona wsparcia"
@@ -319,30 +331,30 @@
 		"description": "Check a script for updates.",
 		"message": "Sprawdź aktualizację/e skryptu"
 	},
-	"confirmNotSaved": {
-		"description": "Confirm message shown when there are unsaved script modifications.",
-		"message": "Modyfikacje w tym skrypcie nie zostały zapisane! Kliknij \"OK\", aby wyjść lub \"Anuluj\", aby pozostać."
-	},
 	"msgImported": {
 		"description": "Message shown after import. There is an argument referring to the count of scripts imported.",
 		"message": "$1 pozycja/e/i zaimportowana/e/ych."
 	},
-	"buttonVacuuming": {
-		"description": "Message shown when data vacuum is in progress.",
-		"message": "Czyszczenie danych..."
-	},
-	"hintVacuum": {
-		"description": "Hint for vacuuming data.",
-		"message": "Usuń nadmiarowość i spróbuj przeładować brakujące dane w pamięci podręcznej."
-	},
 	"hintUseDownloadURL": {
 		"description": "Shown as a place holder for @updateURL when it is not assigned",
 		"message": "Use @downloadURL"
 	},
+	"confirmNotSaved": {
+		"description": "Confirm message shown when there are unsaved script modifications.",
+		"message": "Modyfikacje w tym skrypcie nie zostały zapisane! Kliknij \"OK\", aby wyjść lub \"Anuluj\", aby pozostać."
+	},
+	"buttonVacuuming": {
+		"description": "Message shown when data vacuum is in progress.",
+		"message": "Czyszczenie danych..."
+	},
 	"buttonVacuumed": {
 		"description": "Message shown when data is vacuumed.",
 		"message": "Dane wyczyszczone"
 	},
+	"hintVacuum": {
+		"description": "Hint for vacuuming data.",
+		"message": "Usuń nadmiarowość i spróbuj przeładować brakujące dane w pamięci podręcznej."
+	},
 	"menuManageScripts": {
 		"description": "Menu item to manage scripts, or to open the options page of the extension.",
 		"message": "Zarządzaj skryptami"

+ 33 - 21
src/_locales/ru/messages.json

@@ -59,10 +59,18 @@
 		"description": "Shown in the title of the confirm page while trying to install a script.",
 		"message": "Установка скрипта"
 	},
-	"optionClose": {
+	"buttonInstallOptions": {
+		"description": "Button to show options of installation confirm page.",
+		"message": "Options"
+	},
+	"installOptionClose": {
 		"description": "Option to close confirm window after installation.",
 		"message": "Закрыть после установки"
 	},
+	"installOptionTrack": {
+		"description": "Option to track the loading local file before window is closed.",
+		"message": "Track local file before this window is closed"
+	},
 	"buttonConfirmInstallation": {
 		"description": "Button to confirm installation of a script.",
 		"message": "Установить"
@@ -71,10 +79,6 @@
 		"description": "Button to close window.",
 		"message": "Закрыть"
 	},
-	"msgScriptURL": {
-		"description": "URL of the script to be installed on confirm page.",
-		"message": "URL-адрес скрипта: $1"
-	},
 	"msgErrorLoadingData": {
 		"description": "Message shown on confirm page when the script to be installed cannot be loaded.",
 		"message": "Ошибка загрузки скрипта."
@@ -83,10 +87,6 @@
 		"description": "Message shown in the confirm page when a javascript file to be installed is loaded.",
 		"message": "Скрипт загружен."
 	},
-	"msgLoadingData": {
-		"description": "Message shown on confirm page when the script to be installed is loading.",
-		"message": "Загружается скрипт..."
-	},
 	"msgErrorLoadingDependency": {
 		"description": "Message shown when not all requirements are loaded successfully.",
 		"message": "Ошибка загрузки требований."
@@ -95,6 +95,14 @@
 		"description": "Message shown on confirm page when the requirements are being downloaded.",
 		"message": "Загрузка требований... ($1/$2)"
 	},
+	"msgScriptURL": {
+		"description": "URL of the script to be installed on confirm page.",
+		"message": "URL-адрес скрипта: $1"
+	},
+	"msgLoadingData": {
+		"description": "Message shown on confirm page when the script to be installed is loading.",
+		"message": "Загружается скрипт..."
+	},
 	"sideMenuInstalled": {
 		"description": "Side menu: Installed scripts",
 		"message": "Установленные скрипты"
@@ -295,6 +303,10 @@
 		"description": "Button to save modifications of a script and then close the editing page.",
 		"message": "Сохранить и закрыть"
 	},
+	"labelNoScripts": {
+		"description": "Message shown when no script is installed.",
+		"message": "Oops, you haven't got any script yet."
+	},
 	"hintSupportPage": {
 		"description": "Hint for support page.",
 		"message": "Support page"
@@ -319,30 +331,30 @@
 		"description": "Check a script for updates.",
 		"message": "Проверить обновления"
 	},
-	"confirmNotSaved": {
-		"description": "Confirm message shown when there are unsaved script modifications.",
-		"message": "Изменения не сохранены!\nНажмите OK, чтобы выйти или Отмена, чтобы вернуться."
-	},
 	"msgImported": {
 		"description": "Message shown after import. There is an argument referring to the count of scripts imported.",
 		"message": "$1 скрипт(ов) импортировано."
 	},
-	"buttonVacuuming": {
-		"description": "Message shown when data vacuum is in progress.",
-		"message": "Очистка данных кэша..."
-	},
-	"hintVacuum": {
-		"description": "Hint for vacuuming data.",
-		"message": "Сбросить избыточность кэша и попробовать подгрузить недостающие ресурсы"
-	},
 	"hintUseDownloadURL": {
 		"description": "Shown as a place holder for @updateURL when it is not assigned",
 		"message": "Use @downloadURL"
 	},
+	"confirmNotSaved": {
+		"description": "Confirm message shown when there are unsaved script modifications.",
+		"message": "Изменения не сохранены!\nНажмите OK, чтобы выйти или Отмена, чтобы вернуться."
+	},
+	"buttonVacuuming": {
+		"description": "Message shown when data vacuum is in progress.",
+		"message": "Очистка данных кэша..."
+	},
 	"buttonVacuumed": {
 		"description": "Message shown when data is vacuumed.",
 		"message": "Сбросить кэш"
 	},
+	"hintVacuum": {
+		"description": "Hint for vacuuming data.",
+		"message": "Сбросить избыточность кэша и попробовать подгрузить недостающие ресурсы"
+	},
 	"menuManageScripts": {
 		"description": "Menu item to manage scripts, or to open the options page of the extension.",
 		"message": "Управление скриптами"

+ 35 - 23
src/_locales/zh/messages.json

@@ -59,10 +59,18 @@
 		"description": "Shown in the title of the confirm page while trying to install a script.",
 		"message": "安装脚本"
 	},
-	"optionClose": {
+	"buttonInstallOptions": {
+		"description": "Button to show options of installation confirm page.",
+		"message": "选项"
+	},
+	"installOptionClose": {
 		"description": "Option to close confirm window after installation.",
 		"message": "安装完成后关闭"
 	},
+	"installOptionTrack": {
+		"description": "Option to track the loading local file before window is closed.",
+		"message": "本窗口关闭前跟踪打开的本地文件同步更新"
+	},
 	"buttonConfirmInstallation": {
 		"description": "Button to confirm installation of a script.",
 		"message": "确认安装"
@@ -71,10 +79,6 @@
 		"description": "Button to close window.",
 		"message": "关闭"
 	},
-	"msgScriptURL": {
-		"description": "URL of the script to be installed on confirm page.",
-		"message": "脚本URL:$1"
-	},
 	"msgErrorLoadingData": {
 		"description": "Message shown on confirm page when the script to be installed cannot be loaded.",
 		"message": "加载脚本数据发生错误。"
@@ -83,10 +87,6 @@
 		"description": "Message shown in the confirm page when a javascript file to be installed is loaded.",
 		"message": "脚本数据已加载。"
 	},
-	"msgLoadingData": {
-		"description": "Message shown on confirm page when the script to be installed is loading.",
-		"message": "正在加载脚本数据..."
-	},
 	"msgErrorLoadingDependency": {
 		"description": "Message shown when not all requirements are loaded successfully.",
 		"message": "加载脚本依赖发生错误。"
@@ -95,6 +95,14 @@
 		"description": "Message shown on confirm page when the requirements are being downloaded.",
 		"message": "正在加载脚本依赖...($1/$2)"
 	},
+	"msgScriptURL": {
+		"description": "URL of the script to be installed on confirm page.",
+		"message": "脚本URL:$1"
+	},
+	"msgLoadingData": {
+		"description": "Message shown on confirm page when the script to be installed is loading.",
+		"message": "正在加载脚本数据..."
+	},
 	"sideMenuInstalled": {
 		"description": "Side menu: Installed scripts",
 		"message": "已安装的脚本"
@@ -149,7 +157,7 @@
 	},
 	"buttonImportData": {
 		"description": "Button to choose a file for data import.",
-		"message": "导入自zip文件"
+		"message": "从zip文件导入"
 	},
 	"buttonVacuum": {
 		"description": "Button to vacuum extension data.",
@@ -185,7 +193,7 @@
 	},
 	"anchorSupportPage": {
 		"description": "Link to the support page of Violentmonkey.",
-		"message": "<a href=http://geraldl.net/proj/vm-nex target=_blank>暴力猴</a>"
+		"message": "<a href=http://gerald.top/code/vm-nex target=_blank>暴力猴</a>"
 	},
 	"labelDonate": {
 		"description": "Label of link to donate page.",
@@ -295,6 +303,10 @@
 		"description": "Button to save modifications of a script and then close the editing page.",
 		"message": "保存并关闭"
 	},
+	"labelNoScripts": {
+		"description": "Message shown when no script is installed.",
+		"message": "你居然没有脚本!<br>快<a href=http://gerald.top/tag/userjs target=_blank>点此</a>查看<a href=http://gerald.top target=_blank style='color:orange'>Gerald</a>的脚本吧~"
+	},
 	"hintSupportPage": {
 		"description": "Hint for support page.",
 		"message": "支持页面"
@@ -319,30 +331,30 @@
 		"description": "Check a script for updates.",
 		"message": "查找更新"
 	},
-	"confirmNotSaved": {
-		"description": "Confirm message shown when there are unsaved script modifications.",
-		"message": "修改尚未保存!\n点击确定放弃修改或点击取消停留此页面。"
-	},
 	"msgImported": {
 		"description": "Message shown after import. There is an argument referring to the count of scripts imported.",
 		"message": "已导入$1个脚本。"
 	},
-	"buttonVacuuming": {
-		"description": "Message shown when data vacuum is in progress.",
-		"message": "正在整理..."
-	},
-	"hintVacuum": {
-		"description": "Hint for vacuuming data.",
-		"message": "丢弃多余的数据,并尝试重新获取缺失的资源。"
-	},
 	"hintUseDownloadURL": {
 		"description": "Shown as a place holder for @updateURL when it is not assigned",
 		"message": "使用@downloadURL"
 	},
+	"confirmNotSaved": {
+		"description": "Confirm message shown when there are unsaved script modifications.",
+		"message": "修改尚未保存!\n点击确定放弃修改或点击取消停留此页面。"
+	},
+	"buttonVacuuming": {
+		"description": "Message shown when data vacuum is in progress.",
+		"message": "正在整理..."
+	},
 	"buttonVacuumed": {
 		"description": "Message shown when data is vacuumed.",
 		"message": "数据已整理"
 	},
+	"hintVacuum": {
+		"description": "Hint for vacuuming data.",
+		"message": "丢弃多余的数据,并尝试重新获取缺失的资源。"
+	},
 	"menuManageScripts": {
 		"description": "Menu item to manage scripts, or to open the options page of the extension.",
 		"message": "管理脚本"

+ 10 - 1
src/background.html

@@ -1 +1,10 @@
-<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" /><script src="common.js"></script><script src="background.js"></script><title>ViolentMonkey</title></head><body></body></html>
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<script src="common.js"></script>
+		<script src="background.js"></script>
+		<title>ViolentMonkey</title>
+	</head>
+	<body></body>
+</html>

+ 83 - 86
src/background.js

@@ -1,3 +1,4 @@
+'use strict';
 function getUniqId() {
 	return Date.now().toString(36)+Math.random().toString(36).slice(2,6);
 }
@@ -134,11 +135,11 @@ function vacuum(o,src,callback) {
 			var i;
 			for(i in rq) if(rq[i]==1) fetchRequire(i);
 			for(i in cc) if(cc[i]==1) fetchCache(i);
-			chrome.tabs.sendMessage(src.tab.id,{cmd:'Vacuumed'});
+			callback();
 		}
 	}
 	init();
-	if(callback) callback();
+	return true;
 }
 function move(data,src,callback){
 	var o=db.transaction('scripts','readwrite').objectStore('scripts');
@@ -156,7 +157,8 @@ function move(data,src,callback){
 			var p=e.target.result,v;
 			if(p) {
 				data.offset--;
-				v=p.value;v.position=x;o.put(v);x=p.key;
+				v=p.value;v.position=x;x=p.key;
+				p.update(v);
 				if(data.offset) p.continue();
 				else {r.position=x;o.put(r);}
 			}
@@ -309,9 +311,9 @@ function getInjected(url,src,callback) {	// for injected
 		callback(data);
 		if(n&&src.url==src.tab.url) chrome.tabs.sendMessage(src.tab.id,{cmd:'GetBadge'});
 	}
-	var data={scripts:[],values:{},require:{},injectMode:settings.injectMode},
+	var data={scripts:[],values:{},require:{},injectMode:getOption('injectMode')},
 			cache={},values=[],n=0;
-	if(data.isApplied=settings.isApplied) getScripts(); else finish();
+	if(data.isApplied=getOption('isApplied')) getScripts(); else finish();
 	return true;
 }
 function fetchURL(url, cb, type, headers) {
@@ -329,23 +331,26 @@ function saveCache(url,data,callback) {
 	o.put({uri:url,data:data}).onsuccess=callback;
 	return true;
 }
-function fetchCache(url) {
-	if(u_cache[url]) return;
-	u_cache[url]=1;
-	fetchURL(url, function() {
-		if (this.status!=200) return;
-		//saveCache(url,this.response,function(){delete u_cache[url];});
+function fetchCache(url, check) {
+	function saveResult(blob) {
 		var r=new FileReader();
 		r.onload=function(e){
 			saveCache(url,window.btoa(r.result),function(){delete u_cache[url];});
 		};
-		r.readAsBinaryString(this.response);
-	}, 'blob');
+		r.readAsBinaryString(blob);
+	}
+	if(!u_cache[url]) {
+		u_cache[url]=1;
+		fetchURL(url, function() {
+			if (this.status!=200) return;
+			if(check) check(this.response,saveResult);
+			else saveResult(this.response);
+		}, 'blob');
+	}
 }
 function saveRequire(url,data,callback) {
 	var o=db.transaction('require','readwrite').objectStore('require');
 	o.put({uri:url,code:data}).onsuccess=callback;
-	return true;
 }
 function fetchRequire(url) {
 	if(u_require[url]) return;
@@ -354,9 +359,20 @@ function fetchRequire(url) {
 		if(this.status==200) saveRequire(url,this.responseText,function(){delete u_require[url];});
 	});
 }
-function updateItem(r){
+/**
+ * broadcast script change
+ * {
+ *   id: required if script is not given
+ *   script: optional
+ *   message: optional
+ *   code: optional
+ *     0 for updated, 1 for installed,
+ *     others for message: 3 for updating
+ * }
+ */
+function updateItem(res){
 	if(port) try{
-		port.postMessage(r);
+		port.postMessage(res);
 	}catch(e){
 		port=null;
 		console.log(e);
@@ -384,23 +400,23 @@ function queryScript(id,meta,callback){
 	return true;
 }
 function parseScript(o,src,callback) {
-	var i,r={status:0,message:'message' in o?o.message:_('msgUpdated')};
+	var i,r={code:0,message:'message' in o?o.message:_('msgUpdated')};
 	function finish(){
-		if(src) chrome.tabs.sendMessage(src.tab.id,{cmd:'ShowMessage',data:r});
 		updateItem(r);
+		callback(r);
 	}
 	if(o.status&&o.status!=200||o.code=='') {	// net error
-		r.status=-1;r.message=_('msgErrorFetchingScript');finish();
+		r.code=-1;r.message=_('msgErrorFetchingScript');finish();
 	} else {	// store script
 		var meta=parseMeta(o.code);
 		queryScript(o.id,meta,function(c){
-			if(!c.id){r.status=1;r.message=_('msgInstalled');}
+			if(!c.id){r.code=1;r.message=_('msgInstalled');}
 			if(o.more) for(i in o.more) if(i in c) c[i]=o.more[i];	// for import and user edit
 			c.meta=meta;c.code=o.code;c.uri=getNameURI(c);
 			if(o.from&&!c.meta.homepageURL&&!c.custom.homepageURL&&!/^(file|data):/.test(o.from)) c.custom.homepageURL=o.from;
 			if(o.url&&!/^(file|data):/.test(o.url)) c.custom.lastInstallURL=o.url;
 			saveScript(c,src).onsuccess=function(e){
-				r.id=c.id=e.target.result;r.obj=getMeta(c);finish();
+				r.id=c.id=e.target.result;r.script=getMeta(c);finish();
 				if(!meta.grant.length)
 					notify(_('Warning'),{
 						body:_('msgWarnGrant',[meta.name||_('labelNoName')]),
@@ -415,13 +431,28 @@ function parseScript(o,src,callback) {
 			var c=o.require&&o.require[u];
 			if(c) saveRequire(u,c); else fetchRequire(u);
 		});
-		for(d in meta.resources) {	// @resource
+		for(var d in meta.resources) {	// @resource
 			var u=meta.resources[d],c=o.resources&&o.resources[u];
 			if(c) saveCache(u,c); else fetchCache(u);
 		}
-		if(isRemote(meta.icon)) fetchCache(meta.icon);	// @icon
+		// @icon
+		if(isRemote(meta.icon)) fetchCache(meta.icon,function(blob,cb){
+			var free=function(){
+				URL.revokeObjectURL(url);
+			};
+			var url=URL.createObjectURL(blob);
+			var image=new Image;
+			image.onload=function(){
+				free();
+				cb(blob);
+			};
+			image.onerror=function(){
+				free();
+			};
+			image.src=url;
+		});
 	}
-	if(callback) callback();
+	return true;
 }
 function canUpdate(o,n){
   o=(o||'').split('.');
@@ -446,38 +477,6 @@ function setValue(data,src,callback){
 	o.put({uri:data.uri,values:data.values});
 	if(callback) callback();	// it seems that CALLBACK does not work with READWRITE transaction
 }
-function getOption(k,src,callback){
-	var v=localStorage.getItem(k)||'',r=true;
-	try{
-		v=JSON.parse(v);
-		settings[k]=v;
-	}catch(e){
-		v=null;
-		r=false;
-	}
-	if(callback) callback(v);
-}
-function setOption(o,src,callback){
-	if(!o.check||(o.key in settings)) {
-		localStorage.setItem(o.key,JSON.stringify(o.value));
-		settings[o.key]=o.value;
-	}
-	if(callback) callback(o.value);
-}
-function initSettings(){
-	function init(k,v){
-		getOption(k,null,function(v){
-			if(v===null) setOption({key:k,value:v});
-		});
-	}
-	init('isApplied',true);
-	init('autoUpdate',true);
-	init('lastUpdate',0);
-	init('withData',true);
-	init('closeAfterInstall',false);
-	init('dataVer',0);
-	init('injectMode',0);
-}
 function updateMeta(d,src,callback) {
 	var o=db.transaction('scripts','readwrite').objectStore('scripts');
 	o.get(d.id).onsuccess=function(e){
@@ -485,7 +484,7 @@ function updateMeta(d,src,callback) {
 		if(!r) return;
 		for(i in d) if(i in r) r[i]=d[i];
 		o.put(r).onsuccess=function(e){	// store script without another transaction
-			updateItem({id:d.id,obj:getMeta(r),status:0});
+			updateItem({id:r.id,script:getMeta(r),code:0});
 		};
 	};
 	if(callback) callback();
@@ -494,7 +493,7 @@ var _update={};
 function checkUpdateO(o) {
 	if(_update[o.id]) return;_update[o.id]=1;
 	function finish(){delete _update[o.id];}
-  var r={id:o.id,updating:1,status:2};
+  var r={id:o.id,code:3};
   function update() {
     if(du) {
       r.message=_('msgUpdating');
@@ -519,7 +518,7 @@ function checkUpdateO(o) {
         if(canUpdate(o.meta.version,m.version)) return update();
         r.message=_('msgNoUpdate');
       } catch(e){}
-      delete r.updating;
+			r.code = 2;
       updateItem(r);finish();
     },null,{Accept:'text/x-userscript-meta'});
   } else finish();
@@ -550,8 +549,8 @@ function checkUpdateAll(e,src,callback) {
 var checking=false;
 function autoCheck() {
   function check() {
-		if(settings.autoUpdate) {
-			if(Date.now()-settings.lastUpdate>=864e5) checkUpdateAll();
+		if(getOption('autoUpdate')) {
+			if(Date.now()-getOption('lastUpdate')>=864e5) checkUpdateAll();
 			setTimeout(check,36e5);
 		} else checking=false;
   }
@@ -582,7 +581,7 @@ function getData(d,src,callback) {
 			});
 		}
 	}
-	var data={settings:settings,scripts:[]},cache={};
+	var data={scripts:[]},cache={};
 	getScripts();
 	return true;
 }
@@ -617,7 +616,7 @@ function exportZip(z,src,callback){
 		} else finish();
 	}
 	function finish(){callback(d);}
-	var d={scripts:[],settings:settings},values=[];
+	var d={scripts:[]},values=[];
 	getScripts();
 	return true;
 }
@@ -704,27 +703,15 @@ chrome.runtime.onConnect.addListener(function(p){
 	port=p;
 	p.onDisconnect.addListener(function(){port=null;});
 });
-var settings={};
-initSettings();
 initDb(function(){
-	var dataVer=1;
-	getOption('dataVer',null,function(ver){
-		pos=null;
+	!function(){
+		pos=0;
 		var o=db.transaction('scripts','readwrite').objectStore('scripts');
 		o.index('position').openCursor(null,'prev').onsuccess=function(e){
-			var r=e.target.result;
-			if(pos===null) pos=r?r.key:0;
-			if(ver<dataVer) {
-				if(r) {
-					r.value.meta=parseMeta(r.value.code);
-					o.put(r.value).onsuccess=function(){r.continue();};
-				} else {
-					console.log('Data upgraded.');
-					setOption({key:'dataVer',value:dataVer});
-				}
-			}
+			var r=e.target.result,v;
+			if(r&&pos<r.key) pos=r.key;
 		};
-	});
+	}();
 	chrome.runtime.onMessage.addListener(function(req,src,callback) {
 		var maps={
 			NewScript:function(o,src,callback){callback(newScript());},
@@ -733,11 +720,11 @@ initDb(function(){
 			GetInjected: getInjected,
 			CheckUpdate: checkUpdate,
 			CheckUpdateAll: checkUpdateAll,
-			SaveScript: saveScript,
+			//SaveScript: saveScript,
 			UpdateMeta: updateMeta,
 			SetValue: setValue,
-			GetOption: getOption,
-			SetOption: setOption,
+			//GetOption: getOption,
+			//SetOption: setOption,
 			ExportZip: exportZip,
 			ParseScript: parseScript,
 			GetScript: getScript,	// for user edit
@@ -751,18 +738,26 @@ initDb(function(){
 			HttpRequest: httpRequest,
 			AbortRequest: abortRequest,
 		},f=maps[req.cmd];
-		if(f) return f(req.data,src,callback);
+		if(f) return f(req.data,src,function(){
+			// if callback function is not given in content page, callback will fail
+			try {
+				callback.apply(null,arguments);
+			} catch(e) {}
+		});
 	});
-	chrome.browserAction.setIcon({path:'images/icon19'+(settings.isApplied?'':'w')+'.png'});
+	chrome.browserAction.setIcon({path:'images/icon19'+(getOption('isApplied')?'':'w')+'.png'});
 	setTimeout(autoCheck,2e4);
 });
+
+// Confirm page
 chrome.webRequest.onBeforeRequest.addListener(function(o){
 	if(/\.user\.js([\?#]|$)/.test(o.url)) {
 		var x=new XMLHttpRequest();
 		x.open('GET',o.url,false);
 		x.send();
 		if((!x.status||x.status==200)&&!/^\s*</.test(x.responseText)) {
-			if(o.tabId<0) chrome.tabs.create({url:chrome.extension.getURL('/confirm.html')+'?url='+encodeURIComponent(o.url)});
+			if(o.tabId<0)
+				chrome.tabs.create({url:chrome.extension.getURL('/confirm.html')+'?url='+encodeURIComponent(o.url)});
 			else chrome.tabs.get(o.tabId,function(t){
 				chrome.tabs.create({url:chrome.extension.getURL('/confirm.html')+'?url='+encodeURIComponent(o.url)+'&from='+encodeURIComponent(t.url)});
 			});
@@ -772,6 +767,7 @@ chrome.webRequest.onBeforeRequest.addListener(function(o){
 },{
 	urls:['<all_urls>'],types:['main_frame']
 },['blocking']);
+
 // Modifications on headers
 chrome.webRequest.onBeforeSendHeaders.addListener(function(details) {
 	var headers=details.requestHeaders,new_headers=[],vm_headers={},v,i;
@@ -794,6 +790,7 @@ chrome.webRequest.onBeforeSendHeaders.addListener(function(details) {
 },{
 	urls:['<all_urls>'],types: ['xmlhttprequest'],
 },["blocking", "requestHeaders"]);
+
 // Watch URL redirects
 chrome.webRequest.onBeforeRedirect.addListener(function(details) {
 	var v=request_id_map[details.requestId],reqo;

+ 72 - 18
src/common.js

@@ -1,26 +1,80 @@
+'use strict';
+
+var _ = chrome.i18n.getMessage;
+var $ = document.querySelector.bind(document);
+var $$ = document.querySelectorAll.bind(document);
+var stopPropagation = function(e) {e.stopPropagation();};
+var defaults = {
+	isApplied: true,
+	autoUpdate: true,
+	lastUpdate: 0,
+	exportValues: true,
+	closeAfterInstall: false,
+	trackLocalFile: false,
+	injectMode: 0,
+};
+
+function getOption(key, def) {
+	var value = localStorage.getItem(key), obj;
+	if(value) try {
+		obj = JSON.parse(value);
+	} catch(e) {
+		obj = def;
+	} else obj = def;
+	if(typeof obj === 'undefined')
+		obj = defaults[key];
+	return obj;
+}
+
+function setOption(key, value) {
+	if(key in defaults)
+		localStorage.setItem(key, JSON.stringify(value));
+}
+
+function getAllOptions() {
+	var options = {};
+	for(var i in defaults) options[i] = getOption(i);
+	return options;
+}
+
+/*
 function format() {
-  var a = arguments;
-  if (a[0]) return a[0].replace(/\$(?:\{(\d+)\}|(\d+))/g, function(v, g1, g2) {
-    g1 = a[g1 || g2];
-    if (g1 == undefined) g1 = v;
-    return g1;
+  var args = arguments;
+  if (args[0]) return args[0].replace(/\$(?:\{(\d+)\}|(\d+))/g, function(value, group1, group2) {
+		var index = typeof group1 != 'undefined' ? group1 : group2;
+		return index >= args.length ? value : (args[index] || '');
   });
 }
+*/
+
+function safeHTML(html) {
+	return html.replace(/[&<]/g, function(m) {
+		return {
+			'&': '&amp;',
+			'<': '&lt;',
+		}[m];
+	});
+}
+
 function initI18n(callback){
-	window.addEventListener('DOMContentLoaded',function(){
-		var nodes=document.querySelectorAll('*[data-i18n]'),i,t;
-		for(i=0;i<nodes.length;i++) nodes[i].innerHTML=_(nodes[i].getAttribute('data-i18n'));
+	window.addEventListener('DOMContentLoaded', function() {
+		Array.prototype.forEach.call($$('*[data-i18n]'), function(node) {
+			node.innerHTML = _(node.getAttribute('data-i18n'));
+		});
 		if(callback) callback();
-	},true);
+	}, false);
 }
-function getLocaleString(dict,key){
-	var lang=navigator.languages,i,lkey;
-	for(i=0;i<lang.length;i++) {
-		lkey=key+':'+lang[i];
-		if(lkey in dict) {
-			key=lkey;break;
+
+/**
+ * Get locale attributes such as @name:zh-CN
+ */
+function getLocaleString(dict, key){
+	navigator.languages.some(function(lang) {
+		var keylang = key + ':' + lang;
+		if(keylang in dict) {
+			key = keylang;
+			return true;
 		}
-	}
-	return dict[key]||'';
+	});
+	return dict[key] || '';
 }
-var _=chrome.i18n.getMessage,$=document.querySelector.bind(document);

+ 10 - 4
src/confirm.html

@@ -3,8 +3,10 @@
 	<head>
 		<meta charset="utf-8" />
 		<link rel="shortcut icon" type="image/png" href="images/icon16.png" />
-		<link rel="stylesheet" type="text/css" href="style.min.css" />
-		<script type="text/javascript" src="load.js"></script>
+		<!-- common:css -->
+		<link rel="stylesheet/less" type="text/css" href="style.less">
+		<script src="lib/less.min.js"></script>
+		<!-- endinject -->
 		<title data-i18n=extName></title>
 	</head>
 	<body id=confirm>
@@ -12,7 +14,11 @@
 			<div class=header>
 				<h1><span data-i18n=labelInstall></span> - <span data-i18n=extName></span></h1>
 				<div class=buttons>
-					<input type=checkbox id=cClose><label for=cClose data-i18n=optionClose></label>
+					<button id=bOptions data-i18n=buttonInstallOptions></button>
+					<div class="options">
+						<label><input type=checkbox id=cbClose><span data-i18n=installOptionClose></span></label>
+						<label><input type=checkbox id=cbTrack><span data-i18n=installOptionTrack></span></label>
+					</div>
 					<button id=bInstall disabled=disabled data-i18n=buttonConfirmInstallation></button>
 					<button id=bClose data-i18n=buttonClose></button>
 				</div>
@@ -20,7 +26,7 @@
 				<div id=msg class=ellipsis></div>
 			</div>
 			<div class=body>
-				<div id=eCode></div>
+				<div class=code></div>
 			</div>
 		</div>
 	</body>

+ 201 - 92
src/confirm.js

@@ -1,94 +1,203 @@
-var M=$('#msg'),I=$('#bInstall'),U=$('#url'),B=$('#bClose'),C=$('#cClose'),data={},T;
-function showMsg(m,t){M.innerHTML=m;M.setAttribute('title',t||m);}
-B.onclick=function(){window.close();};
-C.onchange=function(){
-	chrome.runtime.sendMessage({cmd:'SetOption',data:{key:'closeAfterInstall',value:C.checked}});
-};
-I.onclick=function(){
-	chrome.runtime.sendMessage({
-		cmd:'ParseScript',
-		data:{
-			url:data.url,
-			from:data.from,
-			code:T.getValue(),
-			require:data.require,
-			resources:data.resources,
-		},
-	});
-	I.disabled=true;
-};
-chrome.runtime.onMessage.addListener(function(req,src,callback) {
-	var maps={
-		ShowMessage: function(o){
-			showMsg(o.message);
-			if(callback) callback();
-			if(o.status>=0&&C.checked) window.close();
-		},
-	},f=maps[req.cmd];
-	if(f) f(req.data,src,callback);
-	return true;
-});
-chrome.runtime.sendMessage({cmd:'GetOption',data:'closeAfterInstall'},function(o){C.checked=!!o;});
-initEditor(function(o){
-	T=o;o=location.search.slice(1);
-	o.split('&').forEach(function(i){
-		i.replace(/^([^=]*)=(.*)$/,function(r,g1,g2){data[g1]=decodeURIComponent(g2);});
-	});
-	U.innerHTML=_('msgScriptURL',[data.url||'-']);
-	function error(){showMsg(_('msgErrorLoadingData'));}
-	function loaded(){showMsg(_('msgLoadedData'));I.disabled=false;}
-	if(data.url) {
-		U.setAttribute('title',data.url);
-		showMsg(_('msgLoadingData'));
-		var x=new XMLHttpRequest();
-		x.open('GET',data.url,true);
-		x.onloadend=function(){
-			if((!this.status||this.status==200)&&this.responseText) {
-				T.setValueAndFocus(this.responseText);
-				chrome.runtime.sendMessage({cmd:'ParseMeta',data:this.responseText},function(o){
-					function next() {
-						i++;
-						if(i>=l) {
-							if(err.length) showMsg(_('msgErrorLoadingDependency'),err.join('\n'));
-							else loaded();
-						} else showMsg(_('msgLoadingDependency',[i,l]));
-					}
-					function loadDependency(d,r,b) {
-						r.forEach(function(u){
-							var x=new XMLHttpRequest();
-							x.open('GET',u,true);
-							if(b) x.responseType='blob';
-							x.onloadend=function(){
-								if(this.status==200) {
-									if(b) {
-										var r=new FileReader();
-										r.onload=function(e){
-											d[u]=window.btoa(r.result);
-											next();
-										};
-										r.readAsBinaryString(this.response);
-										return;
-									} else d[u]=this.responseText;
-								} else err.push(u);
-								next();
-							};
-							x.send();
-						});
-					}
-					var i=0,l,err=[],u=[];
-					for(l in o.resources) u.push(o.resources[l]);
-					l=o.require.length+u.length;
-					if(l) {
-						showMsg(_('msgLoadingDependency',[i,l]));
-						data.require={};
-						loadDependency(data.require,o.require);
-						data.resources={};
-						loadDependency(data.resources,u,true);
-					} else loaded();
-				});
-			} else error();
+'use strict';
+!function() {
+	var delay = 2000;
+	var data = {};
+	var lbUrl = $('#url');
+	var lbMsg = $('#msg');
+	var cbClose = $('#cbClose');
+	var cbTrack = $('#cbTrack');
+	var btInstall = $('#bInstall');
+	var options = $('.options');
+	var timer, editor;
+
+	function showMessage(message, title) {
+		lbMsg.innerHTML = message;
+		lbMsg.setAttribute('title', title || message);
+	}
+
+	function zfill(num, length) {
+		num = num.toString();
+		while(num.length < length) num = '0' + num;
+		return num;
+	}
+
+	function getTimeString() {
+		var now = new Date();
+		return zfill(now.getHours(), 2) + ':' +
+			zfill(now.getMinutes(), 2) + ':' +
+			zfill(now.getSeconds(), 2);
+	}
+
+	function getScriptFile(cb) {
+		var x = new XMLHttpRequest();
+		x.open('GET', data.url, true);
+		x.onloadend = function() {
+			var x = this;
+			if(!x.status) data.local = true;
+			if(x.status && x.status != 200 || !x.responseText)
+				showMessage(_('msgErrorLoadingData'));
+			else
+				cb(x.responseText);
 		};
 		x.send();
-	} else error();
-},{exit:B.onclick,readonly:true});
-initI18n();
+	}
+
+	function loadDependency(dict, urls, isBlob, cb) {
+		urls.forEach(function(url){
+			if(dict[url]) return cb();
+			var x = new XMLHttpRequest();
+			x.open('GET', url, true);
+			if(isBlob) x.responseType = 'blob';
+			x.onloadend = function(){
+				var x = this;
+				if(!x.status || x.status == 200) {
+					if(isBlob) {
+						var r = new FileReader();
+						r.onload = function(e) {
+							dict[url] = window.btoa(x.result);
+							cb();
+						};
+						r.readAsBinaryString(x.response);
+					} else {
+						dict[url] = x.responseText;
+						cb();
+					}
+				} else cb(url);
+			};
+			x.send();
+		});
+	}
+
+	function parseMeta(body, cb) {
+		function finish(){
+			showMessage(_('msgLoadedData'));
+			btInstall.disabled = false;
+			data.depStatus = 1;
+			if(cb) cb();
+		}
+		function finishOne(errUrl) {
+			if(errUrl) err.push(errUrl);
+			i ++;
+			if(i >= length) {
+				if(err.length) {
+					showMessage(_('msgErrorLoadingDependency'), err.join('\n'));
+				} else finish();
+			} else showMessage(_('msgLoadingDependency', [i, length]));
+		}
+		var urls = [];
+		var err = [];
+		var i;
+		var length;
+		data.depStatus=0;
+		chrome.runtime.sendMessage({cmd: 'ParseMeta', data: body}, function(script) {
+			for(i in script.resources) urls.push(script.resources[i]);
+			length = script.require.length + urls.length;
+			if(length) {
+				showMessage(_('msgLoadingDependency', [i = 0, length]));
+				loadDependency(data.require = {}, script.require, false, finishOne);
+				loadDependency(data.resources = {}, urls, true, finishOne);
+			} else finish();
+		});
+	}
+
+	function updateClose(){
+		if(cbTrack.disabled = cbClose.checked) {
+			cbTrack.checked = false;
+			updateLocal();
+		}
+		setOption('closeAfterInstall', cbClose.checked);
+	}
+
+	function updateLocal(){
+		setOption('trackLocalFile', cbTrack.checked);
+		if(cbTrack.checked && data.depStatus && data.local)
+			btInstall.disabled=false;
+	}
+
+	function trackLocalFile(){
+		function check(){
+			timer=null;
+			getScriptFile(function(body) {
+				var oldbody = editor.getValue();
+				editor.setValue(body);
+				body = editor.getValue();
+				if(oldbody != body)
+					parseMeta(body, function(){
+						if(cbTrack.checked) install();
+					});
+				else if(cbTrack.checked)
+					timer = setTimeout(check, delay);
+			});
+		}
+		if(!timer) timer = setTimeout(check, delay);
+	}
+
+	function install(){
+		btInstall.disabled=true;
+		chrome.runtime.sendMessage({
+			cmd:'ParseScript',
+			data:{
+				url: data.url,
+				from: data.from,
+				code: editor.getValue(),
+				require: data.require,
+				resources: data.resources,
+			},
+		}, function(res){
+			showMessage(res.message + '[' + getTimeString() + ']');
+			if(res.code >= 0) {
+				if(cbClose.checked) close();
+				else if(data.local && cbTrack.checked) trackLocalFile();
+			}
+		});
+	}
+
+	function close() {
+		window.close();
+	}
+
+	function bindEvents() {
+		cbClose.checked = getOption('closeAfterInstall');
+		updateClose();
+		cbClose.addEventListener('change', updateClose, false);
+		cbTrack.checked = getOption('trackLocalFile');
+		cbTrack.addEventListener('change', updateLocal, false);
+		$('#bClose').addEventListener('click', close, false);
+		btInstall.addEventListener('click', install, false);
+		var hideOptions = function(){
+			options.style.display = '';
+			document.removeEventListener('mousedown', hideOptions, false);
+		};
+		$('#bOptions').addEventListener('click', function(e){
+			options.style.right = this.parentNode.offsetWidth - this.offsetWidth - this.offsetLeft + 'px';
+			options.style.display = 'block';
+			document.addEventListener('mousedown', hideOptions, false);
+		}, false);
+		options.addEventListener('mousedown', stopPropagation, false);
+	}
+
+	initEditor({
+		container: $('.code'),
+		callback: function(_editor) {
+			editor = _editor;
+			location.search.slice(1).split('&').forEach(function(part) {
+				part.replace(/^([^=]*)=(.*)$/, function(value, group1, group2) {
+					data[group1] = decodeURIComponent(group2);
+				});
+			});
+			lbUrl.innerHTML = _('msgScriptURL', [data.url || '-']);
+			if(data.url) {
+				lbUrl.setAttribute('title', data.url);
+				showMessage(_('msgLoadingData'));
+				getScriptFile(function(body) {
+					editor.setValueAndFocus(body);
+					parseMeta(body);
+				});
+			}
+		},
+		onexit: close,
+		readonly: true,
+	});
+	bindEvents();
+	initI18n();
+}();

+ 87 - 45
src/editor.js

@@ -1,53 +1,95 @@
-function initEditor(callback,data){
-	data=data||{};
+'use strict';
+
+function addScript(data, callback) {
+	function add(data) {
+		var s = document.createElement('script');
+		s.async = false;
+		if(data.innerHTML) s.innerHTML = data.innerHTML;
+		else if(data.src) s.src = data.src;
+		s.onload = finish;
+		document.body.appendChild(s);
+	}
+	function finish() {
+		if(! -- count) callback();
+	}
+	if(!data.forEach) data = [data];
+	var count = data.length;
+	data.forEach(add);
+}
+
+function addCSS(data){
+	function add(data){
+		var s;
+		if(data.html) {
+			s = document.createElement('style');
+			s.innerHTML = data.html;
+		} else if(data.href) {
+			s = document.createElement('link');
+			s.rel = 'stylesheet';
+			s.type = 'text/css';
+			s.href = data.href;
+		}
+		if(s) document.head.appendChild(s);
+	}
+	if(!data.forEach) data = [data];
+	data.forEach(add);
+}
+
+function initEditor(options){
+	options = options || {};
 	addCSS([
-		{href:'lib/CodeMirror/lib/codemirror.css'},
-		{href:'mylib/CodeMirror/fold.css'},
-		{href:'mylib/CodeMirror/search.css'},
+		{href: 'lib/CodeMirror/lib/codemirror.css'},
+		{href: 'mylib/CodeMirror/fold.css'},
+		{href: 'mylib/CodeMirror/search.css'},
 	]);
 	addScript([
-		{src:'lib/CodeMirror/lib/codemirror.js'},
-		{src:'lib/CodeMirror/mode/javascript/javascript.js'},
-		{src:'lib/CodeMirror/addon/comment/continuecomment.js'},
-		{src:'lib/CodeMirror/addon/edit/matchbrackets.js'},
-		{src:'lib/CodeMirror/addon/edit/closebrackets.js'},
-		{src:'lib/CodeMirror/addon/fold/foldcode.js'},
-		{src:'lib/CodeMirror/addon/fold/foldgutter.js'},
-		{src:'lib/CodeMirror/addon/fold/brace-fold.js'},
-		{src:'lib/CodeMirror/addon/fold/comment-fold.js'},
-		{src:'lib/CodeMirror/addon/search/match-highlighter.js'},
-		{src:'lib/CodeMirror/addon/search/searchcursor.js'},
-		{src:'lib/CodeMirror/addon/selection/active-line.js'},
-		{src:'mylib/CodeMirror/search.js'},
-	],function(){
-		CodeMirror.keyMap.vm={'fallthrough':'default'};
-		if(data.save) {
-			CodeMirror.keyMap.vm['Ctrl-S']='save';
-			CodeMirror.commands.save=data.save;
+		{src: 'lib/CodeMirror/lib/codemirror.js'},
+		{src: 'lib/CodeMirror/mode/javascript/javascript.js'},
+		{src: 'lib/CodeMirror/addon/comment/continuecomment.js'},
+		{src: 'lib/CodeMirror/addon/edit/matchbrackets.js'},
+		{src: 'lib/CodeMirror/addon/edit/closebrackets.js'},
+		{src: 'lib/CodeMirror/addon/fold/foldcode.js'},
+		{src: 'lib/CodeMirror/addon/fold/foldgutter.js'},
+		{src: 'lib/CodeMirror/addon/fold/brace-fold.js'},
+		{src: 'lib/CodeMirror/addon/fold/comment-fold.js'},
+		{src: 'lib/CodeMirror/addon/search/match-highlighter.js'},
+		{src: 'lib/CodeMirror/addon/search/searchcursor.js'},
+		{src: 'lib/CodeMirror/addon/selection/active-line.js'},
+		{src: 'mylib/CodeMirror/search.js'},
+	], function(){
+		CodeMirror.keyMap.vm = {'fallthrough': 'default'};
+		if(options.onsave) {
+			CodeMirror.keyMap.vm['Ctrl-S'] = 'save';
+			CodeMirror.commands.save = options.onsave;
 		}
-		if(data.exit) {
-			CodeMirror.keyMap.vm['Esc']='exit';
-			CodeMirror.commands.exit=data.exit;
+		if(options.onexit) {
+			CodeMirror.keyMap.vm['Esc'] = 'exit';
+			CodeMirror.commands.exit = options.onexit;
 		}
-		var T=CodeMirror(document.getElementById('eCode'),{
-			continueComments:true,
-			matchBrackets:true,
-			autoCloseBrackets:true,
-			highlightSelectionMatches:true,
-			lineNumbers:true,
-			mode:'javascript',
-			lineWrapping:true,
-			indentUnit:4,
-			indentWithTabs:true,
-			keyMap:'vm',
-			styleActiveLine:true,
-			foldGutter:true,
-			gutters:['CodeMirror-linenumbers','CodeMirror-foldgutter'],
+		var editor = CodeMirror(options.container, {
+			continueComments: true,
+			matchBrackets: true,
+			autoCloseBrackets: true,
+			highlightSelectionMatches: true,
+			lineNumbers: true,
+			mode: 'javascript',
+			lineWrapping: true,
+			indentUnit: 4,
+			indentWithTabs: true,
+			keyMap: 'vm',
+			styleActiveLine: true,
+			foldGutter: true,
+			gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
 		});
-		T.clearHistory=function(){T.getDoc().clearHistory();};
-		T.setValueAndFocus=function(v){T.setValue(v);T.focus();};
-		if(data.onchange) T.on('change',data.onchange);
-		if(data.readonly) T.setOption('readOnly',data.readonly);
-		callback(T);
+		editor.clearHistory = function() {
+			this.getDoc().clearHistory();
+		};
+		editor.setValueAndFocus = function(value) {
+			this.setValue(value);
+			this.focus();
+		};
+		if(options.onchange) editor.on('change', options.onchange);
+		if(options.readonly) editor.setOption('readOnly', options.readonly);
+		if(options.callback) options.callback(editor);
 	});
 }

+ 3 - 3
src/injected.js

@@ -42,7 +42,7 @@ chrome.runtime.onMessage.addListener(function(req,src) {
 function getPopup(){
 	// XXX: only scripts run in top level window are counted
 	if(top===window)
-		chrome.runtime.sendMessage({cmd:'SetPopup',data:[menu,ids]});
+		chrome.runtime.sendMessage({cmd:'SetPopup',data:{ids:ids,menus:menus}});
 }
 function getBadge(){
 	// XXX: only scripts run in top level window are counted
@@ -408,7 +408,7 @@ var comm={
 		});
 		run(start);comm.checkLoad();
 	},
-},menu=[],ids=[],total=0;
+},menus=[],ids=[],total=0;
 function injectScript(o){
 	var f=function(u,did,func){
 		Object.defineProperty(window,'VM_'+u,{
@@ -427,7 +427,7 @@ function handleC(e){
 			chrome.runtime.sendMessage({cmd:'SetValue',data:o});
 		},
 		RegisterMenu:function(o){
-			if(window.top===window) menu.push(o);
+			if(window.top===window) menus.push(o);
 		},
 		GetRequestId:getRequestId,
 		HttpRequest:httpRequest,

File diff suppressed because it is too large
+ 12 - 0
src/lib/less.min.js


+ 0 - 55
src/load.js

@@ -1,55 +0,0 @@
-function addScript(data,callback){
-	function add(data){
-		var s=document.createElement('script');s.async=false;
-		if(data.innerHTML) s.innerHTML=data.innerHTML;
-		else if(data.src) s.src=data.src;
-		s.onload=finish;
-		document.body.appendChild(s);
-	}
-	function finish(){
-		if(!--count) callback();
-	}
-	if(!data.forEach) data=[data];
-	var count=data.length;
-	data.forEach(add);
-}
-function addCSS(data){
-	function add(data){
-		var s=null;
-		if(data.innerHTML) {
-			s=document.createElement('style');
-			s.innerHTML=data.innerHTML;
-		} else if(data.href) {
-			s=document.createElement('link');
-			s.rel='stylesheet';
-			s.type='text/css';
-			s.href=data.href;
-		}
-		if(s) document.head.appendChild(s);
-	}
-	if(!data.forEach) data=[data];
-	data.forEach(add);
-}
-var loadScript=(function(){
-	function getFile(filename,onerror){
-		var x=new XMLHttpRequest();
-		x.open('GET',filename,true);
-		x.onload=function(){addScript({innerHTML:this.responseText});};
-		x.onerror=onerror;
-		x.send();
-	}
-	return function(filename,prefixes){
-		function loop(){
-			if(prefixes&&prefixes.length) {
-				var p=prefixes.shift();
-				getFile(p+filename,loop);
-			}
-		}
-		getFile(filename,loop);
-	};
-})();
-window.addEventListener('DOMContentLoaded',function(){
-	Array.prototype.forEach.call(document.querySelectorAll('link[rel=compatible-script]'),function(i){
-		loadScript(i.getAttribute('href'));
-	});
-},false);

+ 4 - 5
src/manifest.json

@@ -1,13 +1,13 @@
 {
 	"name": "Violent monkey",
-	"version" : "2.1.7",
+	"version" : "2.1.9.2",
 	"manifest_version" : 2,
 	"description" : "__MSG_extDescription__",
 	"developer": {
 		"name" : "Gerald",
-		"url" : "http://geraldl.net"
+		"url" : "http://gerald.top"
 	},
-	"homepage_url":"http://geraldl.net/proj/vm?from=nex",
+	"homepage_url":"http://gerald.top/code/vm?from=nex",
 	"icons" : {
 		"16":"images/icon16.png",
 		"48":"images/icon48.png",
@@ -34,7 +34,6 @@
 		"tabs",
 		"<all_urls>",
 		"webRequest",
-		"webRequestBlocking",
-		"notifications"
+		"webRequestBlocking"
 	]
 }

+ 44 - 30
src/options.html

@@ -4,9 +4,11 @@
 		<meta charset="utf-8" />
 		<link rel="shortcut icon" type="image/png" href="images/icon16.png">
 		<link rel="stylesheet" type="text/css" href="lib/font-awesome/font-awesome.min.css">
-		<link rel="stylesheet" type="text/css" href="style.min.css">
+		<!-- common:css -->
+		<link rel="stylesheet/less" type="text/css" href="style.less">
+		<script src="lib/less.min.js"></script>
+		<!-- endinject -->
 		<script src="lib/zip.js/zip.js"></script>
-		<script src="load.js"></script>
 		<title data-i18n=extName></title>
 	</head>
 	<body id=options>
@@ -17,9 +19,9 @@
 				<p>2013-2015</p>
 				<hr>
 				<div class=sidemenu>
-					<a id=smInstalled href=#Installed data-i18n=sideMenuInstalled></a>
-					<a id=smSettings href=#Settings data-i18n=sideMenuSettings></a>
-					<a id=smAbout href=#About data-i18n=sideMenuAbout></a>
+					<a href=#Installed data-i18n=sideMenuInstalled></a>
+					<a href=#Settings data-i18n=sideMenuSettings></a>
+					<a href=#About data-i18n=sideMenuAbout></a>
 				</div>
 			</div>
 			<div class=content>
@@ -31,9 +33,8 @@
 							<a href=https://greasyfork.org/scripts target=_blank data-i18n=anchorGetMoreScripts></a>
 						</div>
 					</div>
-					<div id=sList>
-						<span id=message data-i18n=msgLoading></span>
-					</div>
+					<div class="mask"><div data-i18n=msgLoading></div></div>
+					<div id=sList></div>
 				</div>
 				<div id=tabSettings class=hide>
 					<h1 data-i18n=labelSettings></h1>
@@ -53,16 +54,16 @@
 					<fieldset class=title>
 						<legend data-i18n=labelDataExport></legend>
 						<b data-i18n=labelScriptsToExport></b>
-						<label><input type=checkbox id=cWithData><span data-i18n=labelExportScriptData></span></label>
+						<label><input type=checkbox id=cbValues><span data-i18n=labelExportScriptData></span></label>
 						<select id=xList multiple></select>
 						<button id=bSelect data-i18n=buttonAllNone></button>
-						<a id=xHelper></a><button id=bExport data-i18n=buttonExportData></button>
+						<button id=bExport data-i18n=buttonExportData></button>
 					</fieldset>
 				</div>
 				<div id=tabAbout class=hide>
 					<h1 data-i18n=labelAbout></h1>
 					<p data-i18n=extDescription></p>
-					<p><label data-i18n=labelRelated></label><span data-i18n=anchorSupportPage></span> | <a href=http://geraldl.net/donate target=_blank data-i18n=labelDonate></a> | <a href=https://github.com/gera2ld/Violentmonkey/issues target=_blank data-i18n=labelFeedback></a></p>
+					<p><label data-i18n=labelRelated></label><span data-i18n=anchorSupportPage></span> | <a href=http://gerald.top/donate target=_blank data-i18n=labelDonate></a> | <a href=https://github.com/gera2ld/Violentmonkey/issues target=_blank data-i18n=labelFeedback></a></p>
 					<p><label data-i18n=labelAuthor></label><span data-i18n=anchorAuthor></span></p>
 					<p><label data-i18n=labelTranslator></label><span data-i18n=anchorTranslator></span></p>
 					<p><label data-i18n=labelCurrentLang></label><span id=currentLang></span> | <a href=http://cotrans.geraldl.net target=_blank>Help with translation<i class="fa fa-language"></i></a></p>
@@ -72,16 +73,20 @@
 		<div id=wndEditor class="frame hide">
 			<div class=header>
 				<h2 data-i18n=labelScriptEditor></h2>
-				<div class=buttons><button id=bCustom><i id=sCustom class="fa fa-caret-down"></i> <span data-i18n=buttonCustomMeta></span></button></div>
+				<div class=buttons>
+					<button class=btCustom data-i18n=buttonCustomMeta></button>
+				</div>
 			</div>
 			<div class=body>
-				<div id=eCode></div>
-				<div id=eMeta class=hide>
+				<div class="code"></div>
+				<div class="meta hide">
 					<table>
 						<tr>
-							<td title="@name" data-i18n=labelName></td><td class=expand><input type=text id=mName></td>
-							<td title="@run-at" data-i18n=labelRunAt></td><td>
-								<select id=mRunAt>
+							<td title="@name" data-i18n=labelName></td>
+							<td class=expand><input type=text class=name></td>
+							<td title="@run-at" data-i18n=labelRunAt></td>
+							<td>
+								<select class=run-at>
 									<option value=default data-i18n=labelRunAtDefault></option>
 									<option value=start>document-start</option>
 									<option value=idle>document-idle</option>
@@ -89,46 +94,55 @@
 								</select>
 							</td>
 						</tr>
-						<tr title="@homepageURL"><td data-i18n=labelHomepageURL></td><td colspan=3 class=expand><input type=text id=mHomepageURL></td></tr>
+						<tr title="@homepageURL">
+							<td data-i18n=labelHomepageURL></td>
+							<td colspan=3 class=expand><input type=text class=homepage></td>
+						</tr>
 					</table>
 					<table>
-						<tr title="@updateURL"><td data-i18n=labelUpdateURL></td><td class=expand><input type=text id=mUpdateURL></td></tr>
-						<tr title="@downloadURL"><td data-i18n=labelDownloadURL></td><td class=expand><input type=text id=mDownloadURL></td></tr>
+						<tr title="@updateURL">
+							<td data-i18n=labelUpdateURL></td>
+							<td class=expand><input type=text class=updateurl></td>
+						</tr>
+						<tr title="@downloadURL">
+							<td data-i18n=labelDownloadURL></td>
+							<td class=expand><input type=text class=downloadurl></td>
+						</tr>
 					</table>
 					<fieldset title="@include">
 						<legend>
 							<span data-i18n=labelInclude></span>
-							<label><input type=checkbox id=cInclude><span data-i18n=labelKeepInclude></span></label>
+							<label><input type=checkbox class=keep-include><span data-i18n=labelKeepInclude></span></label>
 						</legend>
 						<div data-i18n=labelCustomInclude></div>
-						<textarea id=mInclude></textarea>
+						<textarea class=include></textarea>
 					</fieldset>
 					<fieldset title="@match">
 						<legend>
 							<span data-i18n=labelMatch></span>
-							<label><input type=checkbox id=cMatch><span data-i18n=labelKeepMatch></span></label>
+							<label><input type=checkbox class=keep-match><span data-i18n=labelKeepMatch></span></label>
 						</legend>
 						<div data-i18n=labelCustomMatch></div>
-						<textarea id=mMatch></textarea>
+						<textarea class=match></textarea>
 					</fieldset>
 					<fieldset title="@exclude">
 						<legend>
 							<span data-i18n=labelExclude></span>
-							<label><input type=checkbox id=cExclude><span data-i18n=labelKeepExclude></span></label>
+							<label><input type=checkbox class=keep-exclude><span data-i18n=labelKeepExclude></span></label>
 						</legend>
 						<div data-i18n=labelCustomExclude></div>
-						<textarea id=mExclude></textarea>
+						<textarea class=exclude></textarea>
 					</fieldset>
 				</div>
 			</div>
 			<div class=footer>
 				<div class=options>
-					<label><input type=checkbox id=eUpdate><span data-i18n=labelAllowUpdate></span></label>
+					<label><input type=checkbox class="update"><span data-i18n=labelAllowUpdate></span></label>
 				</div>
 				<div class=right>
-					<button id=eSave data-i18n=buttonSave></button>
-					<button id=eSaveClose data-i18n=buttonSaveClose></button>
-					<button id=eClose data-i18n=buttonClose></button>
+					<button class="save" data-i18n=buttonSave></button>
+					<button class="savenclose" data-i18n=buttonSaveClose></button>
+					<button class="close" data-i18n=buttonClose></button>
 				</div>
 			</div>
 		</div>

+ 683 - 387
src/options.js

@@ -1,420 +1,716 @@
-var L=$('#sList'),cur=null,C=$('.content');
-zip.workerScriptsPath='lib/zip.js/';
-initI18n();
-function split(t){return t.replace(/^\s+|\s+$/g,'').split(/\s*\n\s*/).filter(function(e){return e;});}
-function getName(d,n,def){
-	d.title=n||'';
-	d.innerHTML=n?n.replace(/&/g,'&amp;').replace(/</g,'&lt;'):(def||'<em>'+_('labelNoName')+'</em>');
-}
-$('#currentLang').innerHTML=navigator.language;
-
-// Main options
-function allowUpdate(n){
-	return n.update&&(
-		n.custom.updateURL||n.meta.updateURL
-		||n.custom.downloadURL||n.meta.downloadURL||n.custom.lastInstallURL
-	);
-}
-function setIcon(n,d){
-	d.src=cache[n.meta.icon]||n.meta.icon||'images/icon48.png';
+'use strict';
+zip.workerScriptsPath = 'lib/zip.js/';
+
+function setTitle(node, title, def) {
+	node.title = title || '';
+	node.innerHTML = title ?  safeHTML(title) :
+		(def || '<em>' + _('labelNoName') + '</em>');
 }
-function getAuthor(a,n){
-	var m=n.match(/^(.*?)\s<(\S*?@\S*?)>$/),t=_('labelAuthor');
-	if(m) a.innerHTML=t+'<a href=mailto:'+m[2]+'>'+m[1]+'</a>';
-	else {
-		if(n) n=t+n;a.innerText=n;
+
+var scriptList = function() {
+	var parent = $('#sList');
+	/**
+	 * list = [
+	 *   {
+	 *     node: DOM
+	 *     script: Object
+	 *   },
+	 *   ...
+	 * ]
+	 */
+	var list = [];
+	var dict = {};
+	var cache = {};
+	var mask = $('.mask');
+	var commands = {
+		edit: function(obj) {
+			chrome.runtime.sendMessage({
+				cmd: 'GetScript',
+				data: obj.data.script.id,
+			}, Editor.editScript);
+		},
+		enable: function(obj) {
+			var script = obj.data.script;
+			chrome.runtime.sendMessage({
+				cmd: 'UpdateMeta',
+				data: {
+					id: script.id,
+					enabled: script.enabled ? 0 : 1,
+				},
+			});
+		},
+		remove: function(obj) {
+			var script = obj.data.script;
+			chrome.runtime.sendMessage({
+				cmd: 'RemoveScript',
+				data: script.id,
+			}, function() {
+				list.splice(obj.index, 1);
+				delete dict[script.id];
+				parent.removeChild(obj.data.node);
+				if(!list.length) showEmptyHint();
+			});
+		},
+		update: function(obj) {
+			chrome.runtime.sendMessage({
+				cmd: 'CheckUpdate',
+				data: obj.data.script.id,
+			});
+		},
+	};
+
+	function showEmptyHint() {
+		mask.classList.remove('hide');
+		mask.style.opacity = 1;
+		mask.style.zIndex = 9;
+		mask.innerHTML = '<div>' + _('labelNoScripts') + '</div>';
+		parent.style.opacity = '';
 	}
-}
-function modifyItem(r){
-	var o=map[r.id],d=o.div,n=o.obj;
-	if(r.message) d.querySelector('.message').innerHTML=r.message;
-	d.className=n.enabled?'':'disabled';
-	var a=d.querySelector('.update');
-	if(a) a.disabled=r.updating;
-	a=d.querySelector('.name');
-	getName(a,n.custom.name||getLocaleString(n.meta,'name'));
-	if(o=n.custom.homepageURL||n.meta.homepageURL||n.meta.homepage) a.href=o;	// compatible with @homepage
-	if(o=n.meta.supportURL) {
-		a=d.querySelector('.support');a.classList.remove('hide');
-		a.href=o;a.title=_('hintSupportPage');
+
+	function hideMask() {
+		parent.style.opacity = 1;
+		mask.style.opacity = 0;
+		setTimeout(function() {
+			mask.classList.add('hide');
+		}, 1000);
 	}
-	getAuthor(d.querySelector('.author'),n.meta.author||'');
-	a=d.querySelector('.descrip');
-	getName(a,getLocaleString(n.meta,'description'),'&nbsp;');
-	setIcon(n,d.querySelector('.icon'));
-	a=d.querySelector('.enable');
-	a.innerHTML=n.enabled?_('buttonDisable'):_('buttonEnable');
-}
-function loadItem(o,r){
-	var d=o.div,n=o.obj;if(!r) r={id:n.id};
-	d.innerHTML='<img class=icon>'
-	+'<div class=panelH>'
-		+'<a class="name ellipsis" target=_blank></a>'
-		+'<a class="support hide" target=_blank><i class="fa fa-question-circle"></i></a>'
-		+'<span class=version>'+(n.meta.version?'v'+n.meta.version:'')+'</span>'
-		+'<span class=author></span>'
-	+'</div>'
-	+'<div class=panelT>'
-		+'<i class="fa fa-arrows move" data="move"></i>'
-	+'</div>'
-	+'<p class="descrip ellipsis"></p>'
-	+'<div class=panelB>'
-		+'<button data=edit>'+_('buttonEdit')+'</button> '
-		+'<button data=enable class=enable></button> '
-		+'<button data=remove>'+_('buttonRemove')+'</button> '
-		+(allowUpdate(n)?'<button data=update class=update>'+_('buttonUpdate')+'</button> ':'')
-		+'<span class=message></span>'
-	+'</div>';
-	modifyItem(r);
-}
-function addItem(o){
-	o.div=document.createElement('div');
-	loadItem(o);
-	L.appendChild(o.div);
-}
-(function(){
-	function getSource(e){
-		var o=e.target,p,i;
-		for(p=o;p&&p.parentNode!=L;p=p.parentNode);
-		i=Array.prototype.indexOf.call(L.childNodes,p);
-		return [i,p,o];
+
+	function setData(data) {
+		list = [];
+		dict = {};
+		cache = data.cache || {};
+		if(data.scripts.length) {
+			hideMask();
+			parent.innerHTML = '';
+			data.scripts.forEach(addScript);
+		} else
+			showEmptyHint();
+		Transporter.initList();
 	}
-	function moveItem(e){
-		var m=getSource(e);if(m[0]<0) return;
-		if(m[0]>=0&&m[0]!=t) {
-			e=m;m=e[1];if(e[0]>t) m=m.nextSibling;
-			L.insertBefore(o[1],m);
-			t=e[0];
-		}
+
+	function findNode(target) {
+		for(var node = target; node && node.parentNode != parent; node = node.parentNode);
+		var index = Array.prototype.indexOf.call(parent.childNodes, node);
+		return index >= 0 && {
+			index: index,
+			data: node,
+		};
 	}
-	function movedItem(e){
-		if(!moving) return;moving=false;
-		o[1].classList.remove('moving');
-		L.onmousemove=L.onmouseup=null;L.onmousedown=startMove;
-		if(o[0]!=t) {
-			chrome.runtime.sendMessage({cmd:'Move',data:{id:ids[o[0]],offset:t-o[0]}});
-			var s=t>o[0]?1:-1,i=o[0],x=ids[i];
-			for(;i!=t;i+=s) ids[i]=ids[i+s];
-			ids[t]=x;
-		}
+
+	function findItem(target) {
+		var data = findNode(target);
+		if(data) data.data = list[data.index];
+		return data;
+	}
+
+	function allowUpdate(script){
+		return script.update && (
+			script.custom.updateURL ||
+			script.meta.updateURL ||
+			script.custom.downloadURL ||
+			script.meta.downloadURL ||
+			script.custom.lastInstallURL
+		);
 	}
-	function startMove(e){
-		o=getSource(e);t=o[0];
-		if(o[2].getAttribute('data')=='move') {
-			if(moving) return;moving=true;
-			e.preventDefault();
-			o[1].classList.add('moving');
-			L.onmousedown=null;
-			L.onmousemove=moveItem;
-			L.onmouseup=movedItem;
+
+	function setAuthor(node, author) {
+		var matches = author.match(/^(.*?)\s<(\S*?@\S*?)>$/);
+		var label = _('labelAuthor');
+		if(matches)
+			node.innerHTML = label + '<a href=mailto:' + matches[2] + '>' + matches[1] + '</a>';
+		else
+			node.innerText = author ? label + author : '';
+	}
+
+	var images = {};
+	function loadImage(node, src) {
+		if(src in images) {
+			var data = images[src];
+			if(Array.isArray(data)) data.push(node);
+			else if(data) node.src = src;
+		} else {
+			var nodes = images[src] = [node];	// loading
+			var img = new Image;
+			img.src = src;
+			img.onload = function() {
+				images[src] = true;	// loaded
+				nodes.forEach(function(node) {
+					node.src = src;
+				});
+			};
+			img.onerror = function() {
+				images[src] = false;	// error
+			};
 		}
 	}
-	var maps={
-		edit:function(i){
-			E.cur=map[ids[i]];
-			chrome.runtime.sendMessage({cmd:'GetScript',data:ids[i]},gotScript);
-		},
-		enable:function(i,p,o){
-			var e=map[ids[i]].obj;
-			chrome.runtime.sendMessage({cmd:'UpdateMeta',data:{id:e.id,enabled:!e.enabled?1:0}});
-		},
-		remove:function(i,p){
-			chrome.runtime.sendMessage({cmd:'RemoveScript',data:ids[i]});
-			delete map[ids.splice(i,1)[0]];
-			L.removeChild(p);
-		},
-		update:function(i){
-			chrome.runtime.sendMessage({cmd:'CheckUpdate',data:ids[i]});
+
+	function updateNode(res) {
+		var data = dict[res.id];
+		if(!data) return;
+		var node = data.node;
+		var script = data.script;
+		if(res.message)
+			node.querySelector('.message').innerHTML = res.message;
+		node.classList[script.enabled ? 'remove' : 'add']('disabled');
+		var update = node.querySelector('.update');
+		if(update) update.disabled = res.code == 3;
+		var name = node.querySelector('.name');
+		setTitle(name, script.custom.name || getLocaleString(script.meta, 'name'));
+		var home = script.custom.homepageURL ||
+			script.meta.homepageURL ||
+			script.meta.homepage;
+		if(home) name.href = home;
+		var supportURL = script.meta.supportURL;
+		if(supportURL) {
+			var support = node.querySelector('.support');
+			support.classList.remove('hide');
+			support.href = supportURL;
+			support.title = _('hintSupportPage');
 		}
-	},o,t,moving=false;
-	L.onmousedown=startMove;
-	L.onclick=function(e){
-		var o=getSource(e),d=o[2].getAttribute('data'),f=maps[d];
-		if(f) {
-			e.preventDefault();
-			f.apply(this,o);
+		setAuthor(node.querySelector('.author'), script.meta.author || '');
+		setTitle(node.querySelector('.descrip'), getLocaleString(script.meta, 'description'));
+		var image = node.querySelector('.icon');
+		var src;
+		if(script.meta.icon) {
+			src = cache[script.meta.icon];
+			if(!src) loadImage(image, script.meta.icon);
 		}
-	};
-})();
-$('#bNew').onclick=function(){chrome.runtime.sendMessage({cmd:'NewScript'},function(o){
-	E.cur=null;gotScript(o);
-});};
-$('#bUpdate').onclick=function(){chrome.runtime.sendMessage({cmd:'CheckUpdateAll'});};
-function switchTab(e){
-	var h,o;
-	if(e) {
-		e=e.target;h=e.getAttribute('href').substr(1);
-	} else {
-		h=location.hash||'#Installed';
-		h=h.substr(1);
-		e=$('#sm'+h);
+		image.src = src || 'images/icon48.png';
+		var enable = node.querySelector('.enable');
+		enable.innerHTML = script.enabled ? _('buttonDisable') : _('buttonEnable');
 	}
-	o=C.querySelector('#tab'+h);
-	if(!o) return switchTab({target:$('#smInstalled')});
-	if(cur) {
-		cur.menu.classList.remove('selected');
-		cur.tab.classList.add('hide');
+
+	function initNode(data, res) {
+		var node = data.node;
+		var script = data.script;
+		node.innerHTML = 
+			'<img class=icon>' +
+			'<div class=panelH>' +
+				'<a class="name ellipsis" target=_blank></a>' +
+				'<a class="support hide" target=_blank><i class="fa fa-question-circle"></i></a>' +
+				'<span class=version>' +
+					(script.meta.version ? 'v' + script.meta.version : '') +
+				'</span>' +
+				'<span class=author></span>' +
+			'</div>' +
+			'<div class=panelT>' +
+				'<i class="fa fa-arrows move" data="move"></i>' +
+			'</div>' +
+			'<p class="descrip ellipsis"></p>' +
+			'<div class=panelB>' +
+				'<button data=edit>' + _('buttonEdit') + '</button> ' +
+				'<button data=enable class=enable></button> ' +
+				'<button data=remove>' + _('buttonRemove') + '</button> ' +
+				(allowUpdate(script) ? '<button data=update class=update>' + _('buttonUpdate') + '</button> ' : '') +
+				'<span class=message></span>' +
+			'</div>'
+		;
+		updateNode(res || {id: script.id});
 	}
-	cur={menu:e,tab:o};
-	e.classList.add('selected');
-	o.classList.remove('hide');
-	switch(h) {	// init
-		case 'Settings':xLoad();break;
+
+	function addScript(script) {
+		var node = document.createElement('div');
+		var data = {
+			script: script,
+			node: node,
+		};
+		dict[script.id] = data;
+		list.push(data);
+		node.className = 'item';
+		initNode(data);
+		parent.appendChild(data.node);
+		hideMask();
 	}
-}
-window.addEventListener('popstate',function(){switchTab()},false);
-$('.sidemenu').onclick=switchTab;
-function confirmCancel(dirty){
-	return !dirty||confirm(_('confirmNotSaved'));
-}
 
-// Advanced
-var H=$('#iImport'),V=$('#bVacuum');
-$('#cUpdate').onchange=function(){chrome.runtime.sendMessage({cmd:'AutoUpdate',data:this.checked});};
-$('#sInjectMode').onchange=function(){chrome.runtime.sendMessage({cmd:'SetOption',data:{key:'injectMode',value:this.value}});};
-H.onchange=function(e){
-	zip.createReader(new zip.BlobReader(e.target.files[0]),function(r){
-		r.getEntries(function(e){
-			function getFiles(i){
-				while(i=e.shift()) if(/\.user\.js$/.test(i.filename))
-					return i.getData(writer,function(t){
-						var c={code:t};
-						if(vm.scripts&&(v=vm.scripts[i.filename.slice(0,-8)])) {
-							delete v.id;c.more=v;
+	function updateItem(res) {
+		switch(res.code) {
+			case 0:	// script updated
+				var data = dict[res.id];
+				if(data && res.script) {
+					data.script = res.script;
+					initNode(data, res);
+				}
+				break;
+			case 1:	// script installed
+				addScript(res.script);
+				break;
+			default:	// message
+				updateNode(res);
+		}
+	}
+
+	function bindEvents() {
+		var index;
+		var current;
+		var moving = false;
+		var mousemove = function(e) {
+			var nodeData = findNode(e.target);
+			if(nodeData && nodeData.index !== index) {
+				var node = nodeData.data;
+				if(nodeData.index > index)
+					node = node.nextSibling;
+				parent.insertBefore(current.data.node, node);
+				index = nodeData.index;
+			}
+		};
+		var mouseup = function(e) {
+			if(!moving) return;
+			moving = false;
+			current.data.node.classList.remove('moving');
+			parent.removeEventListener('mousemove', mousemove, false);
+			parent.removeEventListener('mouseup', mouseup, false);
+			if(index != current.index) {
+				chrome.runtime.sendMessage({
+					cmd:'Move',
+					data:{
+						id: current.data.script.id,
+						offset: index - current.index,
+					},
+				});
+				var i = Math.min(index, current.index);
+				var j = Math.max(index, current.index);
+				var seq = [
+					list.slice(0, i),
+					list.slice(i+1, j+1),
+					list.slice(j+1),
+				];
+				if(i === index)
+					seq[1].unshift(seq[1].pop());
+				else
+					seq[1].push(seq[1].shift());
+				list = [];
+				seq.forEach(function(seq) {
+					list = list.concat(seq);
+				});
+			}
+		};
+		parent.addEventListener('mousedown', function(e) {
+			var data = e.target.getAttribute('data');
+			if(data == 'move') {
+				e.preventDefault();
+				if(moving) return;
+				moving = true;
+				parent.addEventListener('mousemove', mousemove, false);
+				parent.addEventListener('mouseup', mouseup, false);
+				current = findItem(e.target);
+				current.data.node.classList.add('moving');
+				index = current.index;
+			}
+		}, false);
+		parent.addEventListener('click', function(e) {
+			var data = e.target.getAttribute('data');
+			if(data) {
+				var obj = findItem(e.target);
+				if(obj) {
+					var func = commands[data];
+					if(func) func(obj);
+				}
+			}
+		}, false);
+	}
+	bindEvents();
+
+	return {
+		forEach: function(cb) {
+			list.forEach(cb);
+		},
+		setData: setData,
+		updateItem: updateItem,
+	};
+}();
+
+var Transporter = function() {
+	var helper = $('#iImport');
+	var cbValues = $('#cbValues');
+	var btExport = $('#bExport');
+	var xList = $('#xList');
+
+	function getFiles(entries) {
+		function getFile() {
+			var entry = entries.shift();
+			if(entry) {
+				if(userjs.test(entry.filename)) entry.getData(writer, function(text) {
+					var script = {code: text};
+					if(vm.scripts) {
+						var more = vm.scripts[entry.filename.slice(0, -8)];
+						if(more) {
+							delete more.id;
+							script.more = more;
 						}
-						chrome.runtime.sendMessage({cmd:'ParseScript',data:c});
-						count++;
-						getFiles();
+					}
+					chrome.runtime.sendMessage({cmd: 'ParseScript', data: script}, function() {
+						count ++;
+						getFile();
 					});
-				alert(_('msgImported',[count]));
+				}); else getFile();
+			} else {
+				alert(_('msgImported', [count]));
 				location.reload();
 			}
-			var i,vm={},writer=new zip.TextWriter(),count=0;
-			for(i=0;i<e.length;i++) if(e[i].filename=='ViolentMonkey') break;
-			if(i<e.length) e.splice(i,1)[0].getData(writer,function(t){
-				try{
-					vm=JSON.parse(t);
-				}catch(e){
-					vm={};
-					console.log('Error parsing ViolentMonkey configuration.');
-				}
-				if(vm.values) for(z in vm.values) chrome.runtime.sendMessage({cmd:'SetValue',data:{uri:z,values:vm.values[z]}});
-				if(vm.settings) for(z in vm.settings) chrome.runtime.sendMessage({cmd:'SetOption',data:{key:z,value:vm.settings[z],check:true}});
-				getFiles();
-			}); else getFiles();
-		});
-	},function(e){console.log(e);});
-};
-$('#bImport').onclick=function(){
-	var e=document.createEvent('MouseEvent');
-	e.initMouseEvent('click',true,true,window,0,0,0,0,0,false,false,false,false,0,null);
-	H.dispatchEvent(e);
-};
-V.onclick=function(){
-	this.disabled=true;
-	this.innerHTML=_('buttonVacuuming');
-	chrome.runtime.sendMessage({cmd:'Vacuum'});
-};
-V.title=_('hintVacuum');
-
-// Export
-var xL=$('#xList'),xE=$('#bExport'),xD=$('#cWithData');
-function xLoad() {
-	xL.innerHTML='';xE.disabled=false;
-	ids.forEach(function(i){
-		var d=document.createElement('option'),n=map[i].obj;
-		d.className='ellipsis';d.selected=true;
-		getName(d,n.custom.name||getLocaleString(n.meta,'name'));
-		xL.appendChild(d);
-	});
-}
-xD.onchange=function(){chrome.runtime.sendMessage({cmd:'SetOption',data:{key:'withData',value:this.checked}});};
-$('#bSelect').onclick=function(){
-	var c=xL.childNodes,v,i;
-	for(i=0;i<c.length;i++) if(!c[i].selected) break;
-	v=i<c.length;for(i=0;i<c.length;i++) c[i].selected=v;
-};
-function exported(o){
-	function addFiles(){
-		adding=true;
-		if(!writer) {	// create writer
-			zip.createWriter(new zip.BlobWriter(),function(w){writer=w;addFiles();});
-			return;
 		}
-		var i=files.shift();
-		if(i) {
-			if(i.name) {	// add file
-				writer.add(i.name,new zip.TextReader(i.content),addFiles);
-				return;
-			} else	// finished
-				writer.close(function(b){
-					var u=URL.createObjectURL(b),e=document.createEvent('MouseEvent');
-					e.initMouseEvent('click',true,true,window,0,0,0,0,0,false,false,false,false,0,null);
-					xH.href=u;
-					xH.download='scripts.zip';
-					xH.dispatchEvent(e);
-					writer=null;
-					URL.revokeObjectURL(u);
+		function getVMConfig(text) {
+			try {
+				vm = JSON.parse(text);
+			} catch(e) {
+				console.log('Error parsing ViolentMonkey configuration.');
+			}
+			var i;
+			if(vm.values)
+				for(i in vm.values) chrome.runtime.sendMessage({
+					cmd: 'SetValue',
+					data: {
+						uri: i,
+						values: vm.values[i],
+					},
 				});
+			if(vm.settings)
+				for(i in vm.settings) setOption(i, vm.settings[i]);
+			getFile();
 		}
-		adding=false;
+		var userjs = /\.user\.js$/i;
+		var writer = new zip.TextWriter();
+		var vm = {}, count = 0;
+		if(!entries.some(function(entry, i) {
+			if(entry.filename == 'ViolentMonkey') {
+				entries.splice(i, 1)[0].getData(writer, getVMConfig);
+				return true;
+			}
+		})) getFile();
 	}
-	function addFile(o){
-		files.push(o);
-		if(!adding) addFiles();
+
+	function importData(e) {
+		zip.createReader(new zip.BlobReader(e.target.files[0]), function(r){
+			r.getEntries(getFiles);
+		}, function(e){console.log(e);});
 	}
-	var writer=null,files=[],adding=false,xH=$('#xHelper'),
-			n,_n,names={},vm={scripts:{},settings:o.settings};
-	if(xD.checked) vm.values={};
-	o.scripts.forEach(function(c){
-		var j=0;
-		n=_n=c.custom.name||c.meta.name||'Noname';
-		while(names[n]) n=_n+'_'+(++j);names[n]=1;
-		addFile({name:n+'.user.js',content:c.code});
-		vm.scripts[n]={id:c.id,custom:c.custom,enabled:c.enabled,update:c.update};
-		if(xD.checked&&(n=o.values[c.uri])) vm.values[c.uri]=n;
-	});
-	addFile({name:'ViolentMonkey',content:JSON.stringify(vm)});
-	addFile({});	// finish adding files
-}
-xE.onclick=function(e){
-	e.preventDefault();
-	this.disabled=true;
-	var i,c=[];
-	for(i=0;i<ids.length;i++)
-		if(xL.childNodes[i].selected) c.push(ids[i]);
-	chrome.runtime.sendMessage({cmd:'ExportZip',data:{values:xD.checked,data:c}},exported);
-};
+
+	function exported(data) {
+		function addFiles(){
+			var file = files.shift();
+			if(file)
+				writer.add(file.name, new zip.TextReader(file.content), addFiles);
+			else
+				writer.close(function(blob){
+					var url = URL.createObjectURL(blob);
+					var helper = document.createElement('a');
+					var e = document.createEvent('MouseEvent');
+					e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+					helper.href = url;
+					helper.download = 'scripts.zip';
+					helper.dispatchEvent(e);
+					writer = null;
+					URL.revokeObjectURL(url);
+				});
+		}
+		var files = [];
+		var names = {};
+		var writer;
+		var vm = {scripts: {}, settings: getAllOptions()};
+		if(cbValues.checked) vm.values={};
+		data.scripts.forEach(function(script) {
+			var name = script.custom.name || script.meta.name || 'Noname';
+			if(names[name]) name += '_' + (++ names[name]);
+			else names[name] = 1;
+			files.push({name: name + '.user.js', content: script.code});
+			vm.scripts[name] = {
+				id: script.id,
+				custom: script.custom,
+				enabled: script.enabled,
+				update: script.update,
+			};
+			if(cbValues.checked) {
+				var values = data.values[script.uri];
+				if(values) vm.values[script.uri] = values;
+			}
+		});
+		files.push({name: 'ViolentMonkey', content: JSON.stringify(vm)});
+		zip.createWriter(new zip.BlobWriter(), function(_writer) {
+			writer = _writer;
+			addFiles();
+		});
+	}
+
+	function bindEvents() {
+		$('#bImport').addEventListener('click', function(e) {
+			var e = document.createEvent('MouseEvent');
+			e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+			helper.dispatchEvent(e);
+		}, false);
+		helper.addEventListener('change', importData, false);
+		btExport.addEventListener('click', function(e) {
+			this.disabled=true;
+			var ids = [];
+			scriptList.forEach(function(data, i) {
+				if(xList.childNodes[i].selected) ids.push(data.script.id);
+			});
+			chrome.runtime.sendMessage({
+				cmd: 'ExportZip',
+				data: {
+					values: cbValues.checked,
+					data: ids,
+				},
+			}, exported);
+		}, false);
+		$('#bSelect').addEventListener('click', function(e) {
+			var nodes = xList.childNodes;
+			var state = !Array.prototype.some.call(nodes, function(node) {
+				return node.selected;
+			});
+			Array.prototype.forEach.call(nodes, function(node) {
+				node.selected = state;
+			});
+		}, false);
+	}
+
+	cbValues.checked = getOption('exportValues');
+	cbValues.addEventListener('change', function(e) {
+		setOption('exportValues', this.checked);
+	}, false);
+	bindEvents();
+
+	return {
+		initList: function() {
+			xList.innerHTML = '';
+			scriptList.forEach(function(data) {
+				var option = document.createElement('option');
+				var script = data.script;
+				option.className = 'ellipsis';
+				option.selected = true;
+				setTitle(option, script.custom.name || getLocaleString(script.meta, 'name'));
+				xList.appendChild(option);
+			});
+			btExport.disabled = false;
+		},
+	};
+}();
 
 // Script Editor
-var E=$('#wndEditor'),U=$('#eUpdate'),M=$('#eMeta'),
-		mN=$('#mName'),mH=$('#mHomepageURL'),mR=$('#mRunAt'),
-		mU=$('#mUpdateURL'),mD=$('#mDownloadURL'),
-    mI=$('#mInclude'),mE=$('#mExclude'),mM=$('#mMatch'),
-    cI=$('#cInclude'),cE=$('#cExclude'),cM=$('#cMatch'),
-		eS=$('#eSave'),eSC=$('#eSaveClose'),T,sC=$('#sCustom');
-function markClean(){
-	eS.disabled=eSC.disabled=true;
-}
-function mReset(){
-	M.classList.add('hide');
-	var e=[],c=E.scr.custom,m=E.scr.meta;
-	M.dirty=false;
-	mN.value=c.name||'';
-	mN.placeholder=m.name||'';
-	mH.value=c.homepageURL||'';
-	mH.placeholder=m.homepageURL||'';
-	mU.value=c.updateURL||'';
-	mU.placeholder=m.updateURL||_('hintUseDownloadURL');
-	mD.value=c.downloadURL||'';
-	mD.placeholder=m.downloadURL||c.lastInstallURL||'';
-	switch(c['run-at']){
-		case 'document-start':mR.value='start';break;
-		case 'document-idle':mR.value='idle';break;
-		case 'document-end':mR.value='end';break;
-		default:mR.value='default';
+var Editor = function() {
+	var parent = $('#wndEditor');
+	var meta = parent.querySelector('.meta');
+	var btSave = parent.querySelector('.save');
+	var btSaveClose = parent.querySelector('.savenclose');
+	var btClose = parent.querySelector('.close');
+	var cbUpdate = parent.querySelector('.update');
+	var pCustom = {
+		name: meta.querySelector('.name'),
+		runAt: meta.querySelector('.run-at'),
+		homepage: meta.querySelector('.homepage'),
+		updateURL: meta.querySelector('.updateurl'),
+		downloadURL: meta.querySelector('.downloadurl'),
+		keepInclude: meta.querySelector('.keep-include'),
+		include: meta.querySelector('.include'),
+		keepMatch: meta.querySelector('.keep-match'),
+		match: meta.querySelector('.match'),
+		keepExclude: meta.querySelector('.keep-exclude'),
+		exclude: meta.querySelector('.exclude'),
+	};
+	var modified = false;
+	var metaModified = false;
+	var script = null;
+	var editor;
+
+	function markClean() {
+		modified = false;
+		metaModified = false;
+		btSave.disabled = btSaveClose.disabled = true;
 	}
-	cI.checked=c._include!=false;
-	mI.value=(c.include||e).join('\n');
-	cM.checked=c._match!=false;
-	mM.value=(c.match||e).join('\n');
-	cE.checked=c._exclude!=false;
-	mE.value=(c.exclude||e).join('\n');
-}
-function gotScript(o){
-	E.classList.remove('hide');
-	E.scr=o;U.checked=o.update;
-	T.setValueAndFocus(o.code);
-	T.clearHistory();markClean();mReset();
-}
-function eSave(){
-	if(M.dirty) {
-		var c=E.scr.custom;
-		c.name=mN.value;
-		c.homepageURL=mH.value;
-		c.updateURL=mU.value;
-		c.downloadURL=mD.value;
-		switch(mR.value){
-			case 'start':c['run-at']='document-start';break;
-			case 'idle':c['run-at']='document-idle';break;
-			case 'end':c['run-at']='document-end';break;
-			default:delete c['run-at'];
+
+	function markDirty(includeMeta) {
+		if(includeMeta) metaModified = true;
+		modified = true;
+		btSave.disabled = btSaveClose.disabled = false;
+	}
+
+	function bindEvents() {
+		var hideMeta = function(e) {
+			meta.classList.add('hide');
+			document.removeEventListener('mousedown', hideMeta, false);
+		};
+		meta.addEventListener('mousedown', stopPropagation, false);
+		parent.querySelector('.btCustom').addEventListener('click', function(e) {
+			meta.classList.remove('hide');
+			document.addEventListener('mousedown', hideMeta, false);
+		}, false);
+		btSave.addEventListener('click', save, false);
+		btSaveClose.addEventListener('click', function(e) {
+			save();
+			close();
+		}, false);
+		btClose.addEventListener('click', close, false);
+		cbUpdate.addEventListener('change', function(e) {
+			markDirty();
+		}, false);
+		meta.addEventListener('change', markDirty, false);
+		meta.addEventListener('dblclick', function(e) {
+			var target = e.target;
+			var placeholder = target.placeholder;
+			if(!target.value && placeholder)
+				target.value = placeholder;
+		}, false);
+	}
+
+	function editScript(_script) {
+		parent.classList.remove('hide');
+		script = _script;
+		cbUpdate.checked = script.update;
+		editor.setValueAndFocus(script.code);
+		editor.clearHistory();
+		markClean();
+		meta.classList.add('hide');
+		pCustom.name.value = script.custom.name || '';
+		pCustom.name.placeholder = script.meta.name || '';
+		pCustom.homepage.value = script.custom.homepageURL || '';
+		pCustom.homepage.placeholder = script.meta.homepageURL || '';
+		pCustom.updateURL.value = script.custom.updateURL || '';
+		pCustom.updateURL.placeholder = script.meta.updateURL || _('hintUseDownloadURL');
+		pCustom.downloadURL.value = script.custom.downloadURL || '';
+		pCustom.downloadURL.placeholder = script.meta.downloadURL || script.custom.lastInstallURL || '';
+		pCustom.runAt.value = {
+			'document-start': 'start',
+			'document-idle': 'idle',
+			'document-end': 'end',
+		}[script.custom['run-at']] || 'default';
+		pCustom.keepInclude.checked = script.custom._include != false;
+		pCustom.keepMatch.checked = script.custom._match != false;
+		pCustom.keepExclude.checked = script.custom._exclude != false;
+		pCustom.include.value = (script.custom.include || []).join('\n');
+		pCustom.match.value = (script.custom.match || []).join('\n');
+		pCustom.exclude.value = (script.custom.exclude || []).join('\n');
+	}
+
+	function split(str) {
+		var empty = /^\s*$/;
+		return str.split(/\s*\n\s*/)
+			.filter(function(e){return empty.test(e);});
+	}
+
+	function save() {
+		if(metaModified) {
+			script.custom.name = pCustom.name.value;
+			script.custom.homepageURL = pCustom.homepage.value;
+			script.custom.updateURL = pCustom.updateURL.value;
+			script.custom.downloadURL = pCustom.downloadURL.value;
+			var runAt = {
+				'start': 'document-start',
+				'idle': 'document-idle',
+				'end': 'document-end',
+			}[pCustom.runAt.value];
+			if(runAt) script.custom['run-at'] = runAt;
+			else delete script.custom['run-at'];
+			script.custom._include = pCustom.keepInclude.checked;
+			script.custom._match = pCustom.keepMatch.checked;
+			script.custom._exclude = pCustom.keepExclude.checked;
+			script.custom.include = split(pCustom.include.value);
+			script.custom.match = split(pCustom.match.value);
+			script.custom.exclude = split(pCustom.exclude.value);
 		}
-		c._include=cI.checked;
-		c.include=split(mI.value);
-		c._match=cM.checked;
-		c.match=split(mM.value);
-		c._exclude=cE.checked;
-		c.exclude=split(mE.value);
+		chrome.runtime.sendMessage({
+			cmd:'ParseScript',
+			data:{
+				id: script.id,
+				code: editor.getValue(),
+				// suppress message
+				message: '',
+				more: {
+					custom: script.custom,
+					update: script.update = cbUpdate.checked,
+				},
+			},
+		});
+		markClean();
 	}
-	chrome.runtime.sendMessage({
-		cmd:'ParseScript',
-		data:{
-			id:E.scr.id,
-			code:T.getValue(),
-			message:'',
-			more:{
-				custom:E.scr.custom,
-				update:E.scr.update=U.checked
-			}
+
+	function close() {
+		if(!modified || confirm(_('confirmNotSaved'))) {
+			script = null;
+			parent.classList.add('hide');
 		}
+	}
+
+	initEditor({
+		callback: function(_editor) {
+			editor = _editor;
+		},
+		container: parent.querySelector('.code'),
+		onsave: save,
+		onexit: close,
+		onchange: function(e) {
+			markDirty();
+		},
 	});
-	markClean();
-}
-function mClose(){M.classList.add('hide');}
-function eClose(){E.classList.add('hide');E.scr=null;}
-U.onchange=E.markDirty=function(){eS.disabled=eSC.disabled=false;};
-M.markDirty=function(){M.dirty=true;E.markDirty();};
-[mN,mH,mR,mU,mD,mI,mM,mE,cI,cM,cE].forEach(function(i){i.onchange=M.markDirty;});
-$('#bCustom').onclick=function(){
-	var r=M.classList.toggle('hide');
-	sC.className='fa '+(r?'fa-caret-down':'fa-caret-up');
-};
-eS.onclick=eSave;
-eSC.onclick=function(){eSave();eClose();};
-E.close=$('#eClose').onclick=function(){if(confirmCancel(!eS.disabled)) eClose();};
-initEditor(function(o){T=o;},{save:eSave,exit:E.close,onchange:E.markDirty});
-// double click to fill with default value
-function mDefault(e){
-	e=e.target;
-	if(!e.value) e.value=e.placeholder;
-}
-[mN,mH,mU,mD,mI,mM,mE].forEach(function(i){i.ondblclick=mDefault;});
+	bindEvents();
+
+	return {
+		editScript: editScript,
+	};
+}();
 
 // Load at last
-var ids=[],map={},cache;
-function loadOptions(o){
-	L.innerHTML='';
-	cache=o.cache;
-	o.scripts.forEach(function(i){
-		ids.push(i.id);addItem(map[i.id]={obj:i});
-	});
-	$('#cUpdate').checked=o.settings.autoUpdate;
-	$('#sInjectMode').value=o.settings.injectMode;
-	xD.checked=o.settings.withData;
+var switchTab = function() {
+	var menus = $$('.sidemenu>a');
+	var tabs = $$('.content>div');
+	return function(e) {
+		var current;
+		Array.prototype.forEach.call(menus, function(menu) {
+			var href = menu.getAttribute('href');
+			if(href == location.hash) {
+				current = href;
+				menu.classList.add('selected');
+			} else
+				menu.classList.remove('selected');
+		});
+		if(!current) {
+			current = menus[0].getAttribute('href');
+			menus[0].classList.add('selected');
+		}
+		current = 'tab' + current.substr(1);
+		Array.prototype.forEach.call(tabs, function(tab) {
+			if(tab.id == current)
+				tab.classList.remove('hide');
+			else
+				tab.classList.add('hide');
+		});
+		if(current == 'tabSettings') Transporter.initList();
+	};
+}();
+
+!function() {
+	$('#cUpdate').addEventListener('change', function(e) {
+		chrome.runtime.sendMessage({
+			cmd: 'AutoUpdate',
+			data: this.checked,
+		});
+	}, false);
+	$('#sInjectMode').addEventListener('change', function(e) {
+		setOption('injectMode', this.value);
+	}, false);
+	var vacuum = $('#bVacuum');
+	vacuum.onclick=function(){
+		var self = this;
+		self.disabled = true;
+		self.innerHTML = _('buttonVacuuming');
+		chrome.runtime.sendMessage({cmd:'Vacuum'}, function() {
+			self.innerHTML = _('buttonVacuumed');
+		});
+	};
+	vacuum.title = _('hintVacuum');
+	$('#bNew').addEventListener('click', function(e) {
+		chrome.runtime.sendMessage({cmd:'NewScript'}, function(script) {
+			Editor.editScript(script);
+		});
+	}, false);
+	$('#bUpdate').addEventListener('click', function(e) {
+		chrome.runtime.sendMessage({cmd:'CheckUpdateAll'});
+	}, false);
+	$('.sidemenu').addEventListener('click', switchTab, false);
+	$('#currentLang').innerHTML = navigator.language;
+	$('#cUpdate').checked = getOption('autoUpdate');
+	$('#sInjectMode').value = getOption('injectMode');
+	window.addEventListener('popstate', switchTab, false);
+	chrome.runtime.sendMessage({cmd:'GetData'}, scriptList.setData);
+	var port = chrome.runtime.connect({name:'Options'});
+	port.onMessage.addListener(scriptList.updateItem);
+	initI18n();
 	switchTab();
-}
-switchTab();
-function updateItem(r){
-	if(!('id' in r)) return;
-	var m=map[r.id];
-	if(!m) map[r.id]=m={};
-	if(r.obj) m.obj=r.obj;
-	switch(r.status){
-		case 0:loadItem(m,r);break;
-		case 1:ids.push(r.id);addItem(m);break;
-		default:modifyItem(r);
-	}
-}
-chrome.runtime.sendMessage({cmd:'GetData'},loadOptions);
-chrome.runtime.onMessage.addListener(function(req,src){
-	var maps={
-		Vacuumed: function(){
-			for(var i=0;i<ids.length;i++) map[ids[i]].obj.position=i+1;
-			V.innerHTML=_('buttonVacuumed');
-		},
-	},f=maps[req.cmd];
-	if(f) f(req.data,src);
-});
-var port=chrome.runtime.connect({name:'Options'});
-port.onMessage.addListener(updateItem);
+}();

+ 6 - 3
src/popup.html

@@ -1,12 +1,15 @@
 <!DOCTYPE html>
-<html id=popup>
+<html>
 	<head>
 		<meta charset="utf-8" />
 		<link rel="stylesheet" type="text/css" href="lib/font-awesome/font-awesome.min.css">
-		<link rel="stylesheet" type="text/css" href="style.min.css">
+		<!-- common:css -->
+		<link rel="stylesheet/less" type="text/css" href="style.less">
+		<script src="lib/less.min.js"></script>
+		<!-- endinject -->
 		<title>Popup Menu - Violentmonkey</title>
 	</head>
-	<body>
+	<body id=popup>
 		<div id=main>
 			<div class="top menu ellipsis"></div>
 			<div class="bot menu ellipsis"></div>

+ 171 - 133
src/popup.js

@@ -1,144 +1,182 @@
-var P=$('#main'),C=$('#commands'),
-		pT=P.querySelector('.top'),pB=P.querySelector('.bot'),
-		cT=C.querySelector('.top'),cB=C.querySelector('.bot'),
-		tab=null,ia=null,scripts={},hr=null;
-function loadItem(d,c) {
-	d.data=c;
-	if(d.symbols) {
-		d.firstChild.className='fa '+d.symbols[c?1:0];
-		if(d.symbols.length>1) {
-			if(c) d.classList.remove('disabled');
-			else d.classList.add('disabled');
-		}
-	}
-}
-function addItem(h,c,b) {
-  var d=document.createElement('div');
-  d.innerHTML='<i></i> '+h;
-  if('title' in c) {
-    d.title=typeof c.title=='string'?c.title:h;
-    delete c.title;
-  }
-  c.holder.insertBefore(d,b);
-  for(h in c) d[h]=c[h];
-	if(d.symbols) loadItem(d,d.data);
-	return d;
+'use strict';
+
+function Menu(data) {
+	this.data = data;
+	var node = this.node = document.createElement('div');
+	if(data.ellipsis) node.classList.add('ellipsis');
+	node.innerHTML = '<i></i> ' + safeHTML(data.name);
+	if('title' in data)
+		node.title = typeof data.title == 'string' ? data.title : data.name;
+	this.bindEvents();
+	this.update(data.value);
+	data.parent.insertBefore(node, data.before);
 }
-function menuCommand(e) {
-	chrome.tabs.sendMessage(tab.id,{cmd:'Command',data:e.target.cmd});
-}
-function menuScript(s) {
-	if(s&&!scripts[s.id]) {
-		scripts[s.id]=s;
-		var n=s.custom.name||getLocaleString(s.meta,'name');
-		n=n?n.replace(/&/g,'&amp;').replace(/</g,'&lt;'):'<em>'+_('labelNoName')+'</em>';
-		addItem(n,{
-			holder: pB,
-			symbols: ['fa-times','fa-check'],
-			className: 'ellipsis',
-			title: s.meta.name,
-			onclick: function(e){
-				var d=!this.data;
-				chrome.runtime.sendMessage({cmd:'UpdateMeta',data:{id:s.id,enabled:d}});
-				loadItem(this,d);
+Menu.prototype = {
+	update: function(value) {
+		var node = this.node;
+		var data = this.data;
+		if(typeof value != 'undefined') data.value = value;
+		if(data.symbols) {
+			node.firstChild.className = 'fa ' + data.symbols[data.value ? 1 : 0];
+			if(data.symbols.length > 1) {
+				if(value) node.classList.remove('disabled');
+				else node.classList.add('disabled');
+			}
+		}
+	},
+	bindEvents: function() {
+		var events = this.data.events;
+		for(var i in events)
+			this.node.addEventListener(i, events[i].bind(this), false);
+	},
+};
+
+var Popup = function() {
+	var main = $('#main');
+	var commands = $('#commands');
+	var scripts = {};
+	var main_top = main.querySelector('.top');
+	var main_bot = main.querySelector('.bot');
+	var commands_top = commands.querySelector('.top');
+	var commands_bot = commands.querySelector('.bot');
+	var nodeIsApplied;
+	var tab, sep;
+
+	function initMenu(){
+		new Menu({
+			name: _('menuManageScripts'),
+			parent: main_top,
+			symbols: ['fa-hand-o-right'],
+			events: {
+				click: function(e){
+					var url = chrome.extension.getURL('/options.html');
+					chrome.tabs.query({currentWindow: true, url: url}, function(tabs) {
+						if(tabs[0]) chrome.tabs.update(tabs[0].id, {active: true});
+						else chrome.tabs.create({url: url});
+					});
+				},
 			},
-			data:s.enabled,
 		});
+		if(/^https?:\/\//i.test(tab.url))
+			new Menu({
+				name: _('menuFindScripts'),
+				parent: main_top,
+				symbols: ['fa-hand-o-right'],
+				events: {
+					click: function(){
+						var matches = tab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
+						chrome.tabs.create({url: 'https://greasyfork.org/scripts/search?q=' + matches[1]});
+					},
+				},
+			});
+		nodeIsApplied = new Menu({
+			name: _('menuScriptEnabled'),
+			parent: main_top,
+			symbols: ['fa-times','fa-check'],
+			events: {
+				click: function(e) {
+					var value = !this.data.value;
+					setOption('isApplied', value);
+					this.update(value);
+					chrome.browserAction.setIcon({
+						path: 'images/icon19' + (value ? '' : 'w') + '.png',
+					});
+				},
+			},
+			value: getOption('isApplied'),
+		}).node;
 	}
-}
-function initMenu(){
-  addItem(_('menuManageScripts'),{
-    holder: pT,
-    symbols: ['fa-hand-o-right'],
-    //title: true,
-    onclick: function(){
-			var u=chrome.extension.getURL('/options.html');
-			chrome.tabs.query({currentWindow:true,url:u},function(t) {
-				if(t[0]) chrome.tabs.update(t[0].id,{active:true});
-				else chrome.tabs.create({url:u});
+
+	function menuScript(script) {
+		if(script && !scripts[script.id]) {
+			scripts[script.id] = script;
+			var name = script.custom.name || getLocaleString(script.meta, 'name');
+			name = name ? safeHTML(name) : '<em>' + _('labelNoName') + '</em>';
+			new Menu({
+				name: name,
+				parent: main_bot,
+				symbols: ['fa-times','fa-check'],
+				title: script.meta.name,
+				events: {
+					click: function(e) {
+						var value = !this.data.value;
+						chrome.runtime.sendMessage({cmd: 'UpdateMeta', data: {id: script.id, enabled: value}});
+						this.update(value);
+					},
+				},
+				value: script.enabled,
 			});
 		}
-  });
-  if(/^https?:\/\//i.test(tab.url))
-		addItem(_('menuFindScripts'), {
-			holder: pT,
-			symbols: ['fa-hand-o-right'],
-			//title: true,
-			onclick: function(){
-				var h=tab.url.match(/:\/\/(?:www\.)?([^\/]*)/);
-				chrome.tabs.create({url:'https://greasyfork.org/scripts/search?q='+h[1]});
-			},
-		});
-  ia=addItem(_('menuScriptEnabled'), {
-    holder: pT,
-		symbols: ['fa-times','fa-check'],
-    //title: true,
-    onclick: function(e) {
-			var d=!this.data;
-      chrome.runtime.sendMessage({
-				cmd:'SetOption',
-				data:{key:'isApplied',value:d},
+	}
+
+	function setData(data) {
+		if(!data) return;
+		if(data.menus && data.menus.length) {
+			new Menu({
+				name: _('menuBack'),
+				parent: commands_top,
+				symbols: ['fa-arrow-left'],
+				events: {
+					click: function(e) {
+						commands.classList.add('hide');
+						main.classList.remove('hide');
+					},
+				},
 			});
-			loadItem(this,d);
-			chrome.browserAction.setIcon({
-				path:'images/icon19'+(this.data?'':'w')+'.png',
+			commands_top.appendChild(document.createElement('hr'));
+			data.menus.forEach(function(menu) {
+				new Menu({
+					name: menu[0],
+					parent: commands_bot,
+					symbols: ['fa-hand-o-right'],
+					events: {
+						click: function(e) {
+							chrome.tabs.sendMessage(tab.id, {cmd:'Command', data:this.data.name});
+						},
+					},
+				});
 			});
-    }
-  });
-	chrome.runtime.sendMessage({
-		cmd:'GetOption',data:'isApplied',
-	},function(o){loadItem(ia,o);});
-}
-function load(data) {
-  if(data&&data[0]&&data[0].length) {
-    addItem(_('menuBack'), {
-      holder: cT,
-      symbols: ['fa-arrow-left'],
-      //title: true,
-      onclick: function() {
-        C.classList.add('hide');
-        P.classList.remove('hide');
-      }
-    });
-    cT.appendChild(document.createElement('hr'));
-    data[0].forEach(function(i) {
-      addItem(i[0], {
-        holder: cB,
-				className: 'ellipsis',
-        symbols: ['fa-hand-o-right'],
-        //title: true,
-        onclick: menuCommand,
-        cmd: i[0]
-      });
-    });
-    addItem(_('menuCommands'),{
-      holder: pT,
-      symbols: ['fa-arrow-right'],
-      //title: true,
-      onclick: function() {
-        P.classList.add('hide');
-        C.classList.remove('hide');
-      }
-    },ia);
-  }
-  if(data&&data[1]&&data[1].length) {
-		var ids=[];
-		data[1].forEach(function(i){
-			if(!scripts[i]) ids.push(i);
-		});
-		if(ids.length) chrome.runtime.sendMessage({cmd:'GetMetas',data:ids},function(o){
-			if(!hr) pT.appendChild(hr=document.createElement('hr'));
-			o.forEach(menuScript);
-		});
+			new Menu({
+				name: _('menuCommands'),
+				parent: main_top,
+				symbols: ['fa-arrow-right'],
+				events: {
+					click: function(e) {
+						main.classList.add('hide');
+						commands.classList.remove('hide');
+					},
+				},
+				before: nodeIsApplied,
+			});
+		}
+		if(data.ids && data.ids.length) {
+			var ids=[];
+			data.ids.forEach(function(id){
+				if(!scripts[id]) ids.push(id);
+			});
+			if(ids.length)
+				chrome.runtime.sendMessage({cmd: 'GetMetas', data: ids}, function(scripts) {
+					if(!sep)
+						main_top.appendChild(sep = document.createElement('hr'));
+					scripts.forEach(menuScript);
+				});
+		}
 	}
-}
-chrome.runtime.onMessage.addListener(function(req,src,callback) {
+
+	chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
+		tab=tabs[0];
+		initMenu();
+		chrome.tabs.sendMessage(tab.id, {cmd: 'GetPopup'});
+	});
+
+	return {
+		setData: setData,
+	};
+}();
+
+chrome.runtime.onMessage.addListener(function(req, src, callback) {
 	var maps={
-		SetPopup: load,
-	},f=maps[req.cmd];
-	if(f) f(req.data,src,callback);
-	return true;
-});
-chrome.tabs.query({currentWindow:true,active:true},function(t) {
-	tab=t[0];initMenu();chrome.tabs.sendMessage(tab.id,{cmd:'GetPopup'});
+		SetPopup: Popup.setData,
+	}, f=maps[req.cmd];
+	if(f) f(req.data, src, callback);
 });

+ 77 - 31
src/style.less

@@ -1,4 +1,5 @@
 // common styles
+@menu-height: 30px;
 .fill(){
 	height: 100%;
 	margin: 0;
@@ -55,9 +56,7 @@ h1{
 
 // popup menu
 #popup{
-	body {
-		background: inherit;
-	}
+	background: white;
 	.fa{
 		width: 1em;
 	}
@@ -87,7 +86,6 @@ h1{
 }
 
 // options page
-@menu-height: 2em;
 a{
 	color: dodgerblue;
 }
@@ -108,12 +106,14 @@ label>*{
 		max-width: 900px;
 		height: 100%;
 		margin: 0 auto;
+		position: relative;
 	}
 	.sidebar{
-		position: fixed;
+		position: absolute;
 		background: #fafafa;
+		top: @menu-height;
+		left: 0;
 		width: 200px;
-		margin-top: 30px;
 		box-sizing: border-box;
 		border: 1px solid darkgray;
 		border-radius: 10px 0 0 10px;
@@ -135,7 +135,7 @@ label>*{
 		font-weight: bold;
 		line-height: 2.5em;
 		text-decoration: none;
-		&.selected,&:hover{
+		&.selected, &:hover{
 			color: black;
 		}
 	}
@@ -194,20 +194,45 @@ label>*{
 		height: @menu-height;
 		line-height: @menu-height;
 		padding: 0 .5em;
+		border-bottom: 1px solid darkgray;
 	}
 	.right{
 		float: right;
 	}
 }
+.mask {
+	color: gray;
+	position: absolute;
+	top: @menu-height + 2px;
+	bottom: 2px;
+	width: 100%;
+	font-size: 0;
+	text-align: center;
+	background: white;
+	transition: opacity 1s;
+	&>*, &::after {
+		display: inline-block;
+		vertical-align: middle;
+	}
+	&>* {
+		font-size: 2rem;
+	}
+	&::after {
+		content: '.';
+		height: 100%;
+		width: 0;
+	}
+}
 #sList{
+	opacity: 0;
+	transition: opacity 1s;
 	overflow-y: auto;
 	box-sizing: border-box;
-	border-top: 1px solid darkgray;
 	position: absolute;
 	top: @menu-height;
 	bottom: 0;
 	width: 100%;
-	&>div{
+	.item {
 		border-bottom: 1px dashed silver;
 		padding: 10px;
 		position: relative;
@@ -278,50 +303,54 @@ label>*{
 // Editor
 @editor-header-height: 5em;
 @editor-footer-height: 3em;
-#eCode,#eCode .CodeMirror{
+#wndEditor {
+	z-index: 10;
+}
+.code, .code .CodeMirror{
 	height: 100%;
 }
-#eMeta{
+.meta {
 	max-width: 450px;
 	max-height: 100%;
 	height: auto;
-	background: lightgray;
+	background: #ddd;
 	z-index: 10;
 	right: 0;
 	top: 0;
 	padding: 10px;
 	box-sizing: border-box;
 	overflow-y: auto;
-	box-shadow: -2px 4px 4px gray;
+	box-shadow: 0 0 3px black;
 	position: absolute;
 	table{
 		width: 100%;
-		td{
-			width: 1px;
-			white-space: nowrap;
-			&.expand{
-				width: auto;
-			}
+	}
+	td{
+		width: 1px;
+		white-space: nowrap;
+		&.expand{
+			width: auto;
 		}
 	}
 }
+
 // Settings
 #xList{
 	display: block;
 	width: 100%;
 	white-space: normal;
-	min-height: 100px;
+	height: 200px;
 	&>option{
 		display: inline-block;
 		width: 180px;
 		margin: 2px;
 		padding: 0 5px;
-		border: 1px dashed lightgray;
+		box-shadow: 0 0 1px black;
 	}
 }
 // About
 #currentLang {
-	color: orange;
+	color: green;
 	font-weight: bold;
 }
 #tabAbout {
@@ -333,18 +362,32 @@ label>*{
 // confirm page
 @confirm-header-height: 7em;
 #confirm{
-	.frame{
-		.header{
-			height: @confirm-header-height;
-			.buttons{
-				bottom: 3em;
-			}
+	.header{
+		height: @confirm-header-height;
+	}
+	.buttons{
+		bottom: 3em;
+		.options {
+			display: none;
+			position: absolute;
+			top: 110%;
+			box-shadow: 0 0 3px black;
+			border-radius: 3px;
+			padding: 10px;
+			background: #ddd;
+			z-index: 1000;
+			width: 200px;
 		}
-		.body{
-			top: @confirm-header-height;
-			bottom: 5px;
+		label {
+			display: block;
+			padding-left: 16px;
+			text-indent: -16px;
 		}
 	}
+	.body{
+		top: @confirm-header-height;
+		bottom: 5px;
+	}
 }
 #url{
 	float: left;
@@ -353,3 +396,6 @@ label>*{
 #msg{
 	text-align: right;
 }
+input[disabled]~span {
+	color: gray;
+}

File diff suppressed because it is too large
+ 0 - 0
src/style.min.css


Some files were not shown because too many files changed in this diff