Jelajahi Sumber

Merge pull request #19 from gildas-lormeau/master

upd
solokot 5 tahun lalu
induk
melakukan
81005e95bf

+ 12 - 12
README.MD

@@ -78,7 +78,7 @@ See https://addons.mozilla.org/firefox/addon/single-file/versions/
   - The last saved path cannot be remembered by default. To circumvent this limitation, disable the option "Misc > save pages in background".
   - The following characters are replaced with `_` in file names: `~`, `+`, `\`, `?`, `%`, `*`, `:`, `|`, `"`, `<`, `>`  
 - Chromium-based browsers:
-  - You must enable the option "Allow access to file URLs" in the extension page to display the infobar when viewing a saved page, or to save a page stored on the filesystem.
+  - You must enable the option "Allow access to file URLs" in the extension page to display the infobar when viewing a saved page, to save or to annotate a page stored on the filesystem.
   - If the file name of a saved page looks like "56833935-156b-4d8c-a00f-19599c6513d3.html", disable the option "Misc > save pages in background". Reinstalling the browser may also fix this issue. You can find more info about this bug [here](https://bugs.chromium.org/p/chromium/issues/detail?id=892133). This bug has been closed by Google as "WontFix".
   - Disabling the option "File name > open the "Save as" dialog to confirm the file name" will work if and only if the option "Ask where to save each file before downloading" is disabled in chrome://settings/downloads.
 - Firefox:
@@ -111,17 +111,17 @@ SingleFileZ is a fork of SingleFile that allows you to save a webpage as a self-
 More info here: https://github.com/gildas-lormeau/SingleFileZ
 
 ## File format comparison
-|   	                                                                           | HTML (SingleFile)  | HTML (SingleFileZ) | MAFF  | MHTML | Webarchive (Safari) | HTML+folder |
-| ---                                	                                           |       :---:        |       :---:        | :---: | :---: |         :---:       |    :---:    |
-| Pages are saved as a single file                                               | ✓ 	                | ✓ 	               | ✓     | ✓     | ✓                   |             | 
-| HTML and styles are minified                                                   | ✓                  | ✓ 	               |       |       |                     |   	        |
-| Unused HTML and styles are removed from files                                  | ✓                  | ✓ 	               |       |       |                     |   	        |
-| Binary resources are not encoded in base 64                                    |                    | ✓ 	               | ✓     |       | ✓                   | ✓ 	        |
-| Files are compressed                                                           |                    | ✓ 	               | ✓     |       |                     |   	        |
-| Files can be viewed without installing any extension                           | ✓                  | ✓*                 |       | ✓**   | ✓***                | ✓          |
-| Files can be viewed without running JavaScript                                 | ✓                  |   	               | ✓     | ✓     | ✓                   | ✓ 	        |
-| Files can be unzipped to extract resources and view pages                      |                    | ✓ 	               | ✓     |       |                     | n/a        |
-| Files contains the text of the page (plain or formatted) which can be indexed  | ✓ 	                | ✓****              |       | ✓     | ✓ 	                 | ✓ 	        |
+|   	                                                                        | HTML (SingleFile)  | HTML (SingleFileZ) | MAFF  | MHTML | Webarchive (Safari) | HTML+folder |
+| ---                                	                                        |       :---:        |       :---:        | :---: | :---: |         :---:       |    :---:    |
+| Pages are saved as a single file                                              | ✓ 	             | ✓ 	          | ✓     | ✓     | ✓                   |             | 
+| HTML and styles are minified                                                  | ✓                  | ✓ 	          |       |       |                     |   	      |
+| Unused HTML and styles are removed from files                                 | ✓                  | ✓ 	          |       |       |                     |   	      |
+| Binary resources are not encoded in base 64                                   |                    | ✓ 	          | ✓     |       | ✓                   | ✓ 	      |
+| Files are compressed                                                          |                    | ✓ 	          | ✓     |       |                     |   	      |
+| Files can be viewed without installing any extension                          | ✓                  | ✓*                 |       | ✓**   | ✓***                | ✓           |
+| Files can be viewed without running JavaScript                                | ✓                  |   	          | ✓     | ✓     | ✓                   | ✓ 	      |
+| Files can be unzipped to extract resources and view pages                     |                    | ✓ 	          | ✓     |       |                     | n/a         |
+| Files contains the text of the page (plain or formatted) which can be indexed | ✓ 	             | ✓****              |       | ✓     | ✓ 	                | ✓ 	      |
 
 |       | Notes                                                                                                            |
 | :---: | ---                                                                                                              |

+ 18 - 2
_locales/de/messages.json

@@ -295,6 +295,14 @@
 		"message": "Audioquellen entfernen",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Datei-Ziel",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Automatische Speicherung",
 		"description": "Options sub-title: 'Auto-save'"
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "In die Zwischenablage speichern",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "Existenzberechtigung hinzufügen",
@@ -469,7 +481,7 @@
 	},
 	"optionSaveToGDrive": {
 		"message": "In Google Drive speichern",
-		"description": "Options page label: 'save to Google Drive'"
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "Die Seite eines neu angelegten Bookmark speichern",
@@ -479,6 +491,10 @@
 		"message": "Das neue Bookmark mit der gespeicherten Seite verlinken",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "Ignorierte Ordner",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "Hilfe",
 		"description": "Options help link"

+ 20 - 4
_locales/en/messages.json

@@ -295,6 +295,14 @@
 		"message": "remove audio sources",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Auto-save",
 		"description": "Options sub-title: 'Auto-save'"
@@ -456,8 +464,12 @@
 		"description": "Options page label: 'save raw page'"
 	},
 	"optionSaveToClipboard": {
-		"message": "save to clipboard",
-		"description": "Options page label: 'save to clipboard'"
+		"message": "copy to clipboard",
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "add proof of existence",
@@ -468,8 +480,8 @@
 		"description": "Popup text displayed wen enabling the option 'add proof of existence'"
 	},
 	"optionSaveToGDrive": {
-		"message": "save to Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"message": "upload to Google Drive",
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "save the page of a newly created bookmark",
@@ -479,6 +491,10 @@
 		"message": "link the new bookmark to the saved page",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignored folders",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "help",
 		"description": "Options help link"

+ 19 - 3
_locales/es/messages.json

@@ -295,6 +295,14 @@
 		"message": "eliminar fuentes de audio",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destino",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Marcadores",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Auto-guardado",
 		"description": "Options sub-title: 'Auto-save'"
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "guardar en el portapapeles",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "añadir prueba de existencia",
@@ -468,8 +480,8 @@
 		"description": "Popup text displayed wen enabling the option 'add proof of existence'"
 	},
 	"optionSaveToGDrive": {
-		"message": "save en Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"message": "subir a Google Drive",
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "guardar la página de un marcador recién creado",
@@ -479,6 +491,10 @@
 		"message": "enlazar el nuevo marcador a la página guardada",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "carpetas ignoradas",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "ayuda",
 		"description": "Options help link"

+ 20 - 4
_locales/fr/messages.json

@@ -295,6 +295,14 @@
 		"message": "supprimer les sources audio",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Signets",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Auto-sauvegarde",
 		"description": "Options sub-title: 'Auto-save'"
@@ -456,8 +464,12 @@
 		"description": "Options page label: 'save raw page'"
 	},
 	"optionSaveToClipboard": {
-		"message": "sauvegarder dans le presse-papiers",
-		"description": "Options page label: 'save to clipboard'"
+		"message": "copier dans le presse-papiers",
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "enregistrer dans le système de fichiers",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "ajouter une preuve d'existence",
@@ -468,8 +480,8 @@
 		"description": "Popup text displayed wen enabling the option 'add proof of existence'"
 	},
 	"optionSaveToGDrive": {
-		"message": "sauvegarder dans Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"message": "téléverser sur Google Drive",
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "sauvegarder la page d'un signet nouvellement créé",
@@ -479,6 +491,10 @@
 		"message": "lier le nouveau signet à la page sauvegardée",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "dossiers ignorés",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "aide (anglais)",
 		"description": "Options help link"

+ 27 - 11
_locales/ja/messages.json

@@ -3,14 +3,14 @@
 		"message": "完全なページを単一の HTML ファイルに保存する",
 		"description": "Description of the extension."
 	},
-    "commandSaveSelectedTabs": {
-        "message": "Save the selected tabs or their selected contents",
-        "description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
-    },
-    "commandSaveAllTabs": {
-        "message": "すべてのタブを保存",
-        "description": "Command (Ctrl+Shift+U): 'Save all tabs'"
-    },
+	"commandSaveSelectedTabs": {
+		"message": "Save the selected tabs or their selected contents",
+		"description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
+	},
+	"commandSaveAllTabs": {
+		"message": "すべてのタブを保存",
+		"description": "Command (Ctrl+Shift+U): 'Save all tabs'"
+	},
 	"menuSavePage": {
 		"message": "SingleFile でページを保存",
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -295,6 +295,14 @@
 		"message": "オーディオソースを削除する",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "自動保存",
 		"description": "Options sub-title: 'Auto-save'"
@@ -382,7 +390,7 @@
 	"optionAutoSaveExternalSave": {
 		"message": "save the page with SingleFile Companion",
 		"description": "Options page label: 'save the page with SingleFile Companion'"
-	},	
+	},
 	"optionsEditorSubTitle": {
 		"message": "注釈エディタ",
 		"description": "Options sub-title: 'Annotation editor'"
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "クリップボードに保存する",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "ブロックチェーン証明を追加する",
@@ -469,7 +481,7 @@
 	},
 	"optionSaveToGDrive": {
 		"message": "Google Drive に保存",
-		"description": "Options page label: 'save to Google Drive'"
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "新しく作成したブックマークのページを保存する",
@@ -479,6 +491,10 @@
 		"message": "新しいブックマークを保存したページにリンクさせることを可能にする",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignored folders",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "ヘルプ",
 		"description": "Options help link"

+ 29 - 13
_locales/pl/messages.json

@@ -3,14 +3,14 @@
 		"message": "Zapisuj kompletną stronę w pojedynczym pliku HTML.",
 		"description": "Description of the extension."
 	},
-    "commandSaveSelectedTabs": {
-        "message": "Zapisz wybrane karty lub ich wybraną zawartość",
-        "description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
-    },
-    "commandSaveAllTabs": {
-        "message": "Zapisz wszystkie karty",
-        "description": "Command (Ctrl+Shift+U): 'Save all tabs'"
-    },
+	"commandSaveSelectedTabs": {
+		"message": "Zapisz wybrane karty lub ich wybraną zawartość",
+		"description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
+	},
+	"commandSaveAllTabs": {
+		"message": "Zapisz wszystkie karty",
+		"description": "Command (Ctrl+Shift+U): 'Save all tabs'"
+	},
 	"menuSavePage": {
 		"message": "Zapisz stronę z SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -295,6 +295,14 @@
 		"message": "usuwaj źródła audio",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Cel",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Zakładki",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Automatyczny zapis",
 		"description": "Options sub-title: 'Auto-save'"
@@ -382,7 +390,7 @@
 	"optionAutoSaveExternalSave": {
 		"message": "zapisuj stronę z SingleFile Companion",
 		"description": "Options page label: 'save the page with SingleFile Companion'"
-	},	
+	},
 	"optionsEditorSubTitle": {
 		"message": "Edytor adnotacji",
 		"description": "Options sub-title: 'Annotation editor'"
@@ -456,8 +464,12 @@
 		"description": "Options page label: 'save raw page'"
 	},
 	"optionSaveToClipboard": {
-		"message": "zapisuj do schowka",
-		"description": "Options page label: 'save to clipboard'"
+		"message": "kopiuj do schowka",
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "zapisuj do systemu plików",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "dodawaj dowód istnienia",
@@ -468,8 +480,8 @@
 		"description": "Popup text displayed wen enabling the option 'add proof of existence'"
 	},
 	"optionSaveToGDrive": {
-		"message": "zapisuj na Dysku Google",
-		"description": "Options page label: 'save to Google Drive'"
+		"message": "przesyłaj na Dysk Google",
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "zapisuj stronę nowo utworzonej zakładki",
@@ -479,6 +491,10 @@
 		"message": "łącz nową zakładkę z zapisaną stroną",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignorowane foldery",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "pomoc (w języku angielskim)",
 		"description": "Options help link"

+ 19 - 3
_locales/ru/messages.json

@@ -295,6 +295,14 @@
 		"message": "удалить источники аудио",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Автосохранение",
 		"description": "Options sub-title: 'Auto-save'"
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "сохранить в буфер обмена",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "добавить доказательство существования",
@@ -469,7 +481,7 @@
 	},
 	"optionSaveToGDrive": {
 		"message": "сохранить на Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "сохранить страницу недавно созданной закладки",
@@ -479,6 +491,10 @@
 		"message": "связать новую закладку с сохранённой страницей",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignored folders",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "помощь",
 		"description": "Options help link"
@@ -703,4 +719,4 @@
 		"message": "Отмена",
 		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
-}
+}

+ 28 - 12
_locales/uk/messages.json

@@ -3,14 +3,14 @@
 		"message": "Зберегти всю сторінку в один HTML-файл",
 		"description": "Description of the extension."
 	},
-    "commandSaveSelectedTabs": {
-        "message": "Save the selected tabs or their selected contents",
-        "description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
-    },
-    "commandSaveAllTabs": {
-        "message": "Зберегти всі вкладки",
-        "description": "Command (Ctrl+Shift+U): 'Save all tabs'"
-    },
+	"commandSaveSelectedTabs": {
+		"message": "Save the selected tabs or their selected contents",
+		"description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
+	},
+	"commandSaveAllTabs": {
+		"message": "Зберегти всі вкладки",
+		"description": "Command (Ctrl+Shift+U): 'Save all tabs'"
+	},
 	"menuSavePage": {
 		"message": "Зберегти сторінку з допомогою SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -295,6 +295,14 @@
 		"message": "видалити джерела аудіо",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "Автозбереження",
 		"description": "Options sub-title: 'Auto-save'"
@@ -382,7 +390,7 @@
 	"optionAutoSaveExternalSave": {
 		"message": "save the page with SingleFile Companion",
 		"description": "Options page label: 'save the page with SingleFile Companion'"
-	},	
+	},
 	"optionsEditorSubTitle": {
 		"message": "Annotation editor",
 		"description": "Options sub-title: 'Annotation editor'"
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "зберегти в буфер обміну",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "add proof of existence",
@@ -468,8 +480,8 @@
 		"description": "Popup text displayed wen enabling the option 'add proof of existence'"
 	},
 	"optionSaveToGDrive": {
-		"message": "save to Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"message": "upload to Google Drive",
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "save the page of a newly created bookmark",
@@ -479,6 +491,10 @@
 		"message": "link the new bookmark to the saved page",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignored folders",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "допомога",
 		"description": "Options help link"

+ 24 - 8
_locales/zh_CN/messages.json

@@ -295,6 +295,14 @@
 		"message": "移除音频资源",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "自动保存",
 		"description": "Options sub-title: 'Auto-save'"
@@ -448,7 +456,7 @@
 		"description": "Options page label: 'maximum size (MB)'"
 	},
 	"optionPassReferrerOnError": {
-		"message": "pass \"Referer\" header after a cross-origin request error",
+		"message": "跨域请求失败时添加来源地址头信息(Referer)",
 		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
 	},
 	"optionSaveRawPage": {
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "保存到剪切板",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "添加证明文件存在的指纹",
@@ -469,7 +481,7 @@
 	},
 	"optionSaveToGDrive": {
 		"message": "保存到 Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "将该页面保存到一个新创建的书签",
@@ -479,6 +491,10 @@
 		"message": "将新书签的链接指向已保存页面",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignored folders",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "帮助",
 		"description": "Options help link"
@@ -548,7 +564,7 @@
 		"description": "Label 'Disabled' of the disabled profile name in the options page"
 	},
 	"profileAddButtonTooltip": {
-		"message": "添加一个新的配置文件",
+		"message": "新增配置文件",
 		"description": "Tooltip 'Add a new profile' in the options page"
 	},
 	"profileRenameButtonTooltip": {
@@ -560,15 +576,15 @@
 		"description": "Tooltip 'Delete the profile' in the options page"
 	},
 	"profileAddPrompt": {
-		"message": "请输入这个新的配置文件名",
+		"message": "新增配置文件",
 		"description": "Popup text 'Enter a name for this new profile' in the options page"
 	},
 	"profileDeleteConfirm": {
-		"message": "请确认是否移除所选配置文件",
+		"message": "确认删除所选配置文件?",
 		"description": "Popup text 'Confirm the deletion of the selected profile' in the options page"
 	},
 	"profileRenamePrompt": {
-		"message": "请为所选配置文件输入新的名称",
+		"message": "重命名配置文件",
 		"description": "Popup text 'Enter a new name for the selected profile' in the options page"
 	},
 	"editorAddYellowNote": {
@@ -703,4 +719,4 @@
 		"message": "取消",
 		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
-}
+}

+ 24 - 8
_locales/zh_TW/messages.json

@@ -295,6 +295,14 @@
 		"message": "移除音頻資源",
 		"description": "Options page label: 'remove audio sources'"
 	},
+	"optionsDestionationSubTitle": {
+		"message": "Destination",
+		"description": "Options sub-title: 'Destination'"
+	},
+	"optionsBookmarkSubTitle": {
+		"message": "Bookmarks",
+		"description": "Options sub-title: 'Bookmarks'"
+	},
 	"optionsAutoSaveSubTitle": {
 		"message": "自動保存",
 		"description": "Options sub-title: 'Auto-save'"
@@ -448,7 +456,7 @@
 		"description": "Options page label: 'maximum size (MB)'"
 	},
 	"optionPassReferrerOnError": {
-		"message": "pass \"Referer\" header after a cross-origin request error",
+		"message": "跨域請求失敗時添加來源地址頭信息(Referer)",
 		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
 	},
 	"optionSaveRawPage": {
@@ -457,7 +465,11 @@
 	},
 	"optionSaveToClipboard": {
 		"message": "保存到剪切板",
-		"description": "Options page label: 'save to clipboard'"
+		"description": "Options page label: 'copy to clipboard'"
+	},
+	"optionSaveToFilesystem": {
+		"message": "save to filesystem",
+		"description": "Options page label: 'save to filesystem'"
 	},
 	"optionAddProof": {
 		"message": "添加證明文件存在的指紋",
@@ -469,7 +481,7 @@
 	},
 	"optionSaveToGDrive": {
 		"message": "保存到 Google Drive",
-		"description": "Options page label: 'save to Google Drive'"
+		"description": "Options page label: 'upload to Google Drive'"
 	},
 	"optionSaveCreatedBookmarks": {
 		"message": "將該頁面保存到一個新創建的書籤",
@@ -479,6 +491,10 @@
 		"message": "將新書籤的鏈接指向已保存頁面",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
+	"optionIgnoredBookmarkFolders": {
+		"message": "ignored folders",
+		"description": "Options page label: 'ignored folders'"
+	},
 	"optionsHelpLink": {
 		"message": "幫助",
 		"description": "Options help link"
@@ -548,7 +564,7 @@
 		"description": "Label 'Disabled' of the disabled profile name in the options page"
 	},
 	"profileAddButtonTooltip": {
-		"message": "添加一個新的配置文件",
+		"message": "新增配置文件",
 		"description": "Tooltip 'Add a new profile' in the options page"
 	},
 	"profileRenameButtonTooltip": {
@@ -560,15 +576,15 @@
 		"description": "Tooltip 'Delete the profile' in the options page"
 	},
 	"profileAddPrompt": {
-		"message": "請輸入這個新的配置文件名",
+		"message": "新增配置文件",
 		"description": "Popup text 'Enter a name for this new profile' in the options page"
 	},
 	"profileDeleteConfirm": {
-		"message": "請確認是否移除所選配置文件",
+		"message": "確認刪除所選配置文件?",
 		"description": "Popup text 'Confirm the deletion of the selected profile' in the options page"
 	},
 	"profileRenamePrompt": {
-		"message": "請為所選配置文件輸入新的名稱",
+		"message": "重命名配置文件",
 		"description": "Popup text 'Enter a new name for the selected profile' in the options page"
 	},
 	"editorAddYellowNote": {
@@ -703,4 +719,4 @@
 		"message": "取消",
 		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
-}
+}

+ 12 - 0
cli/Dockerfile

@@ -0,0 +1,12 @@
+FROM zenika/alpine-chrome:with-node
+
+RUN npm install --production gildas-lormeau/SingleFile#master
+
+WORKDIR /usr/src/app/node_modules/single-file/cli
+
+ENTRYPOINT [ \
+    "./single-file", \
+    "--browser-executable-path", "/usr/bin/chromium-browser", \
+    "--output-directory", "/out", \
+    "--browser-args", "[\"--no-sandbox\"]", \
+    "--dump-content" ]

+ 34 - 17
cli/README.MD

@@ -3,8 +3,41 @@
 ## Introduction
 
 SingleFile can be launched from the command line by running it into a (headless) browser. It runs through Node.js as a standalone script injected into the web page instead of being embedded into a WebExtension. To connect to the browser, it can use [Puppeteer](https://github.com/GoogleChrome/puppeteer) or [Selenium WebDriver](https://www.npmjs.com/package/selenium-webdriver). Alternatively, it can also emulate a browser with JavaScript disabled by using [jsdom](https://github.com/jsdom/jsdom).
+  
+## Installation with Docker
+
+- Installation from Docker Hub
+
+  `docker pull capsulecode/singlefile`
+  
+  `docker tag capsulecode/singlefile singlefile`
+  
+- Manual installation
+
+  `git clone --depth 1 --recursive https://github.com/gildas-lormeau/SingleFile.git`
+  
+  `cd SingleFile/cli`
+
+  `docker build --no-cache -t singlefile .`
+
+- Run
+
+  `docker run singlefile "https://www.wikipedia.org"`
+  
+- Run and redirect the result into a file
+
+  `docker run singlefile "https://www.wikipedia.org" > wikipedia.html`
+
+- Run and mount a volume to get the saved file in the current directory
+
+  `docker run -v %cd%:/out singlefile "https://www.wikipedia.org" wikipedia.html` (Windows)
 
-## Install
+  `docker run -v $(pwd):/out singlefile "https://www.wikipedia.org" wikipedia.html` (Linux/UNIX)
+
+
+- An alternative docker file can be found here https://github.com/screenbreak/SingleFile-dockerized. It allows you to save pages from the command line interface or through an HTTP server.
+
+## Manual installation
 
 - Make sure Chrome or Firefox is installed and the executable can be found through the `PATH` environment variable. Otherwise you will need to set the `--browser-executable-path` option to help SingleFile locating it. As an alternative to Chrome and Firefox, you can use jsdom by setting the `--back-end` option to `jsdom`.
 
@@ -83,22 +116,6 @@ SingleFile can be launched from the command line by running it into a (headless)
  - If the error message `UnhandledPromiseRejectionWarning: Error: Browser is not downloaded. Run "npm install" or "yarn install" at ChromeLauncher.launch` is displayed, it probably means that `single-file` was not able to find the executable of the browser. Using the option `--browser-executable-path` to pass to `single-file` the complete path of the executable fixes this issue.
  
   - If saving a page takes an unusually long time, this may be due to a timeout error that was automatically recovered. Setting `--browser-wait-until` to a lower value (e.g. `networkidle0` or `load` instead of `networkidle2`) fixes this issue.
-  
-## Docker
-
-- Build
-
-  `docker build -t singlefile .`
-  
-- Run
-
-  `docker run singlefile "https://www.wikipedia.org"`
-  
-- Run and pipe the result into a file
-
-  `docker run singlefile "https://www.wikipedia.org" > wikipedia.html`
-
-- An alternative docker file can be found here https://github.com/screenbreak/SingleFile-dockerized. It allows you to save pages from the command line interface or through an HTTP server.
 
 ## License
 

+ 8 - 4
cli/args.js

@@ -47,6 +47,7 @@ const args = require("yargs")
 		"compress-HTML": true,
 		"dump-content": false,
 		"filename-template": "{page-title} ({date-iso} {time-locale}).html",
+		"filename-conflict-action": "uniquify",
 		"filename-replacement-character": "_",
 		"group-duplicate-images": true,
 		"http-header": [],
@@ -78,7 +79,8 @@ const args = require("yargs")
 		"crawl-max-depth": 1,
 		"crawl-external-links-max-depth": 1,
 		"crawl-replace-urls": false,
-		"crawl-rewrite-rules": []
+		"crawl-rewrite-rules": [],
+		"output-directory": ""
 	})
 	.options("back-end", { description: "Back-end to use" })
 	.choices("back-end", ["jsdom", "puppeteer", "webdriver-chromium", "webdriver-gecko", "puppeteer-firefox", "playwright-firefox", "playwright-chromium"])
@@ -138,6 +140,8 @@ const args = require("yargs")
 	.string("error-file")
 	.options("filename-template", { description: "Template used to generate the output filename (see help page of the extension for more info)" })
 	.string("filename-template")
+	.options("filename-conflict-action", { description: "Action when the filename is conflicting with existing one on the filesystem. The possible values are \"uniquify\" (default), \"overwrite\" and \"skip\"" })
+	.string("filename-conflict-action")
 	.options("filename-replacement-character", { description: "The character used for replacing invalid characters in filenames" })
 	.string("filename-replacement-character")
 	.string("filename-replacement-character")
@@ -193,10 +197,10 @@ const args = require("yargs")
 	.boolean("user-script-enabled")
 	.options("web-driver-executable-path", { description: "Path to Selenium WebDriver executable (webdriver-gecko, webdriver-chromium)" })
 	.string("web-driver-executable-path")
+	.options("output-directory", { description: "Path to where to save files, this path must exist." })
+	.string("output-directory")
 	.argv;
-if (args.dumpContent) {
-	args.filenameTemplate = "";
-}
+args.backgroundSave = true;
 args.compressCSS = args.compressCss;
 args.compressHTML = args.compressHtml;
 args.includeBOM = args.includeBom;

+ 0 - 13
cli/dockerfile

@@ -1,13 +0,0 @@
-FROM zenika/alpine-chrome:with-node
-
-RUN set -x \
-    git \
-    && git clone --depth 1 --recursive https://github.com/gildas-lormeau/SingleFile \
-    && cd SingleFile && npm install --production && cd cli && npm install --production \
-    && chmod +x single-file \
-    && echo
-    
-WORKDIR /usr/src/app/SingleFile/cli
-
-ENTRYPOINT ["./single-file", "--browser-executable-path", "/usr/bin/chromium-browser", "--dump-content", "--browser-args", "[\"--no-sandbox\"]"]
-CMD ["https://github.com/Zenika/alpine-chrome"]

+ 26 - 11
cli/single-file-cli-api.js

@@ -24,6 +24,7 @@
 /* global require, module, URL */
 
 const fs = require("fs");
+const path = require("path");
 const VALID_URL_TEST = /^(https?|file):\/\//;
 
 const STATE_PROCESSING = "processing";
@@ -201,15 +202,21 @@ function getHostURL(url) {
 
 async function capturePage(options) {
 	try {
+		let filename;
 		const pageData = await backend.getPageData(options);
 		if (options.output) {
-			fs.writeFileSync(getFilename(options.output), pageData.content);
+			filename = getFilename(options.output, options);
+		} else if (options.dumpContent) {
+			console.log(pageData.content); // eslint-disable-line no-console
 		} else {
-			if (options.filenameTemplate && pageData.filename) {
-				fs.writeFileSync(getFilename(pageData.filename), pageData.content);
-			} else {
-				console.log(pageData.content); // eslint-disable-line no-console
+			filename = getFilename(pageData.filename, options);
+		}
+		if (filename) {
+			const dirname = path.dirname(filename);
+			if (dirname) {
+				fs.mkdirSync(dirname, { recursive: true });
 			}
+			fs.writeFileSync(filename, pageData.content);
 		}
 		return pageData;
 	} catch (error) {
@@ -222,19 +229,27 @@ async function capturePage(options) {
 	}
 }
 
-function getFilename(filename, index = 1) {
-	let newFilename = filename;
-	if (index > 1) {
+function getFilename(filename, options, index = 1) {
+	let outputDirectory = options.outputDirectory;
+	if (outputDirectory && !outputDirectory.endsWith("/")) {
+		outputDirectory += "/";
+	}
+	let newFilename = outputDirectory + filename;
+	if (options.filenameConflictAction == "overwrite") {
+		return filename;
+	} else if (options.filenameConflictAction == "uniquify" && index > 1) {
 		const regExpMatchExtension = /(\.[^.]+)$/;
 		const matchExtension = newFilename.match(regExpMatchExtension);
 		if (matchExtension && matchExtension[1]) {
-			newFilename = newFilename.replace(regExpMatchExtension, " - " + index + matchExtension[1]);
+			newFilename = newFilename.replace(regExpMatchExtension, " (" + index + matchExtension[1]) + ")";
 		} else {
-			newFilename += " - " + index;
+			newFilename += " (" + index + ")";
 		}
 	}
 	if (fs.existsSync(newFilename)) {
-		return getFilename(filename, index + 1);
+		if (options.filenameConflictAction != "skip") {
+			return getFilename(filename, options, index + 1);
+		}
 	} else {
 		return newFilename;
 	}

+ 22 - 16
extension/core/bg/bookmarks.js

@@ -30,10 +30,16 @@ singlefile.extension.core.bg.bookmarks = (() => {
 		onMessage,
 		saveCreatedBookmarks: enable,
 		disable,
-		update: (id, changes) => browser.bookmarks.update(id, changes)
+		update: async (id, changes) => {
+			try {
+				await browser.bookmarks.update(id, changes);
+			} catch (error) {
+				// ignored
+			}
+		}
 	};
 
-	function onMessage(message) {
+	async function onMessage(message) {
 		if (message.method.endsWith(".saveCreatedBookmarks")) {
 			enable();
 			return {};
@@ -71,19 +77,21 @@ singlefile.extension.core.bg.bookmarks = (() => {
 		}
 	}
 
-	async function onCreated(id, bookmarkInfo) {
+	async function onCreated(bookmarkId, bookmarkInfo) {
 		const tabs = await singlefile.extension.core.bg.tabs.get({ lastFocusedWindow: true, active: true });
 		const options = await singlefile.extension.core.bg.config.getOptions(bookmarkInfo.url);
 		if (options.saveCreatedBookmarks) {
-			if (!(await findParentFolder(bookmarkInfo.parentId, options.ignoredBookmarkFolders))) {
+			const bookmarkFolders = await getParentFolders(bookmarkInfo.parentId);
+			const ignoredBookmark = bookmarkFolders.find(folder => options.ignoredBookmarkFolders.includes(folder));
+			if (!ignoredBookmark) {
 				if (tabs.length && tabs[0].url == bookmarkInfo.url) {
-					singlefile.extension.core.bg.business.saveTabs(tabs, { bookmarkId: bookmarkInfo.id });
+					singlefile.extension.core.bg.business.saveTabs(tabs, { bookmarkId, bookmarkFolders });
 				} else {
 					const tabs = await singlefile.extension.core.bg.tabs.get({});
 					if (tabs.length) {
 						const tab = tabs.find(tab => tab.url == bookmarkInfo.url);
 						if (tab) {
-							singlefile.extension.core.bg.business.saveTabs([tab], { bookmarkId: bookmarkInfo.id });
+							singlefile.extension.core.bg.business.saveTabs([tab], { bookmarkId, bookmarkFolders });
 						} else {
 							if (bookmarkInfo.url) {
 								if (bookmarkInfo.url == "about:blank") {
@@ -98,28 +106,26 @@ singlefile.extension.core.bg.bookmarks = (() => {
 			}
 		}
 
-		async function findParentFolder(id, folderNames) {
-			if (id && folderNames.length) {
+		async function getParentFolders(id, folderNames = []) {
+			if (id) {
 				const bookmarkNode = (await browser.bookmarks.get(id))[0];
-				if (bookmarkNode) {
-					return folderNames.includes(bookmarkNode.title) || findParentFolder(bookmarkNode.parentId, folderNames);
-				} else {
-					return false;
+				if (bookmarkNode && bookmarkNode.title) {
+					folderNames.unshift(bookmarkNode.title);
+					await getParentFolders(bookmarkNode.parentId, folderNames);
 				}
-			} else {
-				return false;
 			}
+			return folderNames;
 		}
 
 		function onChanged(id, changeInfo) {
-			if (id == bookmarkInfo.id && changeInfo.url) {
+			if (id == bookmarkId && changeInfo.url) {
 				browser.bookmarks.onChanged.removeListener(onChanged);
 				saveUrl(changeInfo.url);
 			}
 		}
 
 		function saveUrl(url) {
-			singlefile.extension.core.bg.business.saveUrls([url], { bookmarkId: bookmarkInfo.id });
+			singlefile.extension.core.bg.business.saveUrls([url], { bookmarkId });
 		}
 	}
 

+ 3 - 0
extension/core/bg/business.js

@@ -99,6 +99,9 @@ singlefile.extension.core.bg.business = (() => {
 			Object.keys(options).forEach(key => tabOptions[key] = options[key]);
 			tabOptions.autoClose = true;
 			tabOptions.extensionScriptFiles = extensionScriptFiles;
+			if (tabOptions.passReferrerOnError) {
+				await singlefile.extension.core.bg.requests.enableReferrerOnError();
+			}
 			addTask({
 				tab: { url },
 				status: TASK_PENDING_STATE,

+ 2 - 1
extension/core/bg/config.js

@@ -29,6 +29,7 @@ singlefile.extension.core.bg.config = (() => {
 	const DEFAULT_PROFILE_NAME = "__Default_Settings__";
 	const DISABLED_PROFILE_NAME = "__Disabled_Settings__";
 	const REGEXP_RULE_PREFIX = "regexp:";
+	const BACKGROUND_SAVE_DEFAULT = !/Mobile.*Firefox/.test(navigator.userAgent);
 
 	const DEFAULT_CONFIG = {
 		removeHiddenElements: true,
@@ -66,7 +67,7 @@ singlefile.extension.core.bg.config = (() => {
 		removeVideoSrc: true,
 		displayInfobar: true,
 		displayStats: false,
-		backgroundSave: true,
+		backgroundSave: BACKGROUND_SAVE_DEFAULT,
 		defaultEditorMode: "normal",
 		applySystemTheme: true,
 		autoSaveDelay: 1,

+ 4 - 3
extension/core/content/content-download.js

@@ -21,7 +21,7 @@
  *   Source.
  */
 
-/* global browser, document, URL, Blob, MouseEvent */
+/* global browser, document, URL, Blob, MouseEvent, setTimeout */
 
 this.singlefile.extension.core.content.download = this.singlefile.extension.core.content.download || (() => {
 
@@ -74,14 +74,14 @@ this.singlefile.extension.core.content.download = this.singlefile.extension.core
 			if (options.saveToClipboard) {
 				saveToClipboard(pageData);
 			} else {
-				downloadPageForeground(pageData);
+				await downloadPageForeground(pageData);
 			}
 			browser.runtime.sendMessage({ method: "ui.processEnd" });
 		}
 		await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId, hash: pageData.hash });
 	}
 
-	function downloadPageForeground(pageData) {
+	async function downloadPageForeground(pageData) {
 		if (pageData.filename && pageData.filename.length) {
 			const link = document.createElement("a");
 			link.download = pageData.filename;
@@ -89,6 +89,7 @@ this.singlefile.extension.core.content.download = this.singlefile.extension.core
 			link.dispatchEvent(new MouseEvent("click"));
 			URL.revokeObjectURL(link.href);
 		}
+		return new Promise(resolve => setTimeout(resolve, 1));
 	}
 
 	function saveToClipboard(page) {

+ 38 - 4
extension/lib/single-file/lazy/bg/lazy-timeout.js

@@ -27,21 +27,55 @@
 
 	"use strict";
 
+	const timeouts = new Map();
+
 	browser.runtime.onMessage.addListener((message, sender) => {
 		if (message.method == "singlefile.lazyTimeout.setTimeout") {
+			let tabTimeouts = timeouts.get(sender.tab.id);
+			if (tabTimeouts) {
+				const previousTimeoutId = tabTimeouts.get(message.type);
+				if (previousTimeoutId) {
+					clearTimeout(previousTimeoutId);
+				}
+			}
 			const timeoutId = setTimeout(async () => {
 				try {
-					await browser.tabs.sendMessage(sender.tab.id, { method: "singlefile.lazyTimeout.onTimeout", id: timeoutId });
+					const tabTimeouts = timeouts.get(sender.tab.id);
+					if (tabTimeouts) {
+						deleteTimeout(tabTimeouts, sender.tab.id, message.type);
+					}
+					await browser.tabs.sendMessage(sender.tab.id, { method: "singlefile.lazyTimeout.onTimeout", type: message.type });
 				} catch (error) {
 					// ignored
 				}
 			}, message.delay);
-			return Promise.resolve(timeoutId);
+			if (!tabTimeouts) {
+				tabTimeouts = new Map();
+				timeouts.set(sender.tab.id, tabTimeouts);
+			}
+			tabTimeouts.set(message.type, timeoutId);
+			return Promise.resolve({});
 		}
 		if (message.method == "singlefile.lazyTimeout.clearTimeout") {
-			clearTimeout(message.id);
-			return Promise.resolve({ id: message.id });
+			let tabTimeouts = timeouts.get(sender.tab.id);
+			if (tabTimeouts) {
+				const timeoutId = tabTimeouts.get(message.type);
+				if (timeoutId) {
+					clearTimeout(timeoutId);
+				}
+				deleteTimeout(tabTimeouts, sender.tab.id, message.type);
+			}
+			return Promise.resolve({});
 		}
 	});
 
+	browser.tabs.onRemoved.addListener(tabId => timeouts.delete(tabId));
+
+	function deleteTimeout(tabTimeouts, tabId, type) {
+		tabTimeouts.delete(type);
+		if (!tabTimeouts.size) {
+			timeouts.delete(tabId);
+		}
+	}
+
 })();

+ 11 - 9
extension/ui/bg/ui-editor.js

@@ -278,15 +278,17 @@ singlefile.extension.ui.bg.editor = (() => {
 				document.head.appendChild(linkElement);
 			}
 			tabData.docSaved = true;
-			const defaultEditorMode = tabData.options.defaultEditorMode;
-			if (defaultEditorMode == "edit") {
-				enableEditPage();
-			} else if (defaultEditorMode == "format" && !tabData.options.disableFormatPage) {
-				formatPage();
-			} else if (defaultEditorMode == "cut") {
-				enableCutInnerPage();
-			} else if (defaultEditorMode == "cut-external") {
-				enableCutOuterPage();
+			if (!message.reset) {
+				const defaultEditorMode = tabData.options.defaultEditorMode;
+				if (defaultEditorMode == "edit") {
+					enableEditPage();
+				} else if (defaultEditorMode == "format" && !tabData.options.disableFormatPage) {
+					formatPage();
+				} else if (defaultEditorMode == "cut") {
+					enableCutInnerPage();
+				} else if (defaultEditorMode == "cut-external") {
+					enableCutOuterPage();
+				}
 			}
 		}
 		if (message.method == "savePage") {

+ 44 - 28
extension/ui/bg/ui-options.js

@@ -36,6 +36,7 @@
 	const removeScriptsLabel = document.getElementById("removeScriptsLabel");
 	const saveRawPageLabel = document.getElementById("saveRawPageLabel");
 	const saveToClipboardLabel = document.getElementById("saveToClipboardLabel");
+	const saveToFilesystemLabel = document.getElementById("saveToFilesystemLabel");
 	const addProofLabel = document.getElementById("addProofLabel");
 	const saveToGDriveLabel = document.getElementById("saveToGDriveLabel");
 	const compressHTMLLabel = document.getElementById("compressHTMLLabel");
@@ -73,6 +74,7 @@
 	const saveCreatedBookmarksLabel = document.getElementById("saveCreatedBookmarksLabel");
 	const passReferrerOnErrorLabel = document.getElementById("passReferrerOnErrorLabel");
 	const replaceBookmarkURLLabel = document.getElementById("replaceBookmarkURLLabel");
+	const ignoredBookmarkFoldersLabel = document.getElementById("ignoredBookmarkFoldersLabel");
 	const titleLabel = document.getElementById("titleLabel");
 	const userInterfaceLabel = document.getElementById("userInterfaceLabel");
 	const filenameLabel = document.getElementById("filenameLabel");
@@ -81,6 +83,8 @@
 	const stylesheetsLabel = document.getElementById("stylesheetsLabel");
 	const fontsLabel = document.getElementById("fontsLabel");
 	const otherResourcesLabel = document.getElementById("otherResourcesLabel");
+	const destinationLabel = document.getElementById("destinationLabel");
+	const bookmarksLabel = document.getElementById("bookmarksLabel");
 	const autoSaveLabel = document.getElementById("autoSaveLabel");
 	const autoSettingsLabel = document.getElementById("autoSettingsLabel");
 	const autoSettingsUrlLabel = document.getElementById("autoSettingsUrlLabel");
@@ -120,6 +124,7 @@
 	const saveToClipboardInput = document.getElementById("saveToClipboardInput");
 	const addProofInput = document.getElementById("addProofInput");
 	const saveToGDriveInput = document.getElementById("saveToGDriveInput");
+	const saveToFilesystemInput = document.getElementById("saveToFilesystemInput");
 	const compressHTMLInput = document.getElementById("compressHTMLInput");
 	const compressCSSInput = document.getElementById("compressCSSInput");
 	const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
@@ -151,6 +156,7 @@
 	const saveCreatedBookmarksInput = document.getElementById("saveCreatedBookmarksInput");
 	const passReferrerOnErrorInput = document.getElementById("passReferrerOnErrorInput");
 	const replaceBookmarkURLInput = document.getElementById("replaceBookmarkURLInput");
+	const ignoredBookmarkFoldersInput = document.getElementById("ignoredBookmarkFoldersInput");
 	const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
 	const infobarTemplateInput = document.getElementById("infobarTemplateInput");
 	const includeInfobarInput = document.getElementById("includeInfobarInput");
@@ -450,6 +456,7 @@
 	removeScriptsLabel.textContent = browser.i18n.getMessage("optionRemoveScripts");
 	saveRawPageLabel.textContent = browser.i18n.getMessage("optionSaveRawPage");
 	saveToClipboardLabel.textContent = browser.i18n.getMessage("optionSaveToClipboard");
+	saveToFilesystemLabel.textContent = browser.i18n.getMessage("optionSaveToFilesystem");
 	addProofLabel.textContent = browser.i18n.getMessage("optionAddProof");
 	saveToGDriveLabel.textContent = browser.i18n.getMessage("optionSaveToGDrive");
 	compressHTMLLabel.textContent = browser.i18n.getMessage("optionCompressHTML");
@@ -487,6 +494,7 @@
 	saveCreatedBookmarksLabel.textContent = browser.i18n.getMessage("optionSaveCreatedBookmarks");
 	passReferrerOnErrorLabel.textContent = browser.i18n.getMessage("optionPassReferrerOnError");
 	replaceBookmarkURLLabel.textContent = browser.i18n.getMessage("optionReplaceBookmarkURL");
+	ignoredBookmarkFoldersLabel.textContent = browser.i18n.getMessage("optionIgnoredBookmarkFolders");
 	groupDuplicateImagesLabel.textContent = browser.i18n.getMessage("optionGroupDuplicateImages");
 	titleLabel.textContent = browser.i18n.getMessage("optionsTitle");
 	userInterfaceLabel.textContent = browser.i18n.getMessage("optionsUserInterfaceSubTitle");
@@ -496,6 +504,8 @@
 	stylesheetsLabel.textContent = browser.i18n.getMessage("optionsStylesheetsSubTitle");
 	fontsLabel.textContent = browser.i18n.getMessage("optionsFontsSubTitle");
 	otherResourcesLabel.textContent = browser.i18n.getMessage("optionsOtherResourcesSubTitle");
+	destinationLabel.textContent = browser.i18n.getMessage("optionsDestionationSubTitle");
+	bookmarksLabel.textContent = browser.i18n.getMessage("optionsBookmarkSubTitle");
 	autoSaveLabel.textContent = browser.i18n.getMessage("optionsAutoSaveSubTitle");
 	miscLabel.textContent = browser.i18n.getMessage("optionsMiscSubTitle");
 	helpLabel.textContent = browser.i18n.getMessage("optionsHelpLink");
@@ -634,46 +644,38 @@
 		profileNamesInput.value = selectedProfileName;
 		renameProfileButton.disabled = deleteProfileButton.disabled = profileNamesInput.value == DEFAULT_PROFILE_NAME;
 		const profileOptions = profiles[selectedProfileName];
-		removeHiddenElementsInput.checked = profileOptions.removeHiddenElements || profileOptions.saveRawPage;
-		removeHiddenElementsInput.disabled = profileOptions.saveRawPage;
+		removeHiddenElementsInput.checked = profileOptions.removeHiddenElements;
 		removeUnusedStylesInput.checked = profileOptions.removeUnusedStyles;
 		removeUnusedFontsInput.checked = profileOptions.removeUnusedFonts;
-		removeFramesInput.checked = profileOptions.removeFrames || profileOptions.saveRawPage;
-		removeFramesInput.disabled = profileOptions.saveRawPage;
+		removeFramesInput.checked = profileOptions.removeFrames;
 		removeImportsInput.checked = profileOptions.removeImports;
 		removeScriptsInput.checked = profileOptions.removeScripts;
 		saveRawPageInput.checked = profileOptions.saveRawPage;
-		saveToClipboardInput.checked = profileOptions.saveToClipboard && !profileOptions.saveToGDrive;
-		saveToClipboardInput.disabled = profileOptions.saveToGDrive;
+		saveToClipboardInput.checked = profileOptions.saveToClipboard;
 		addProofInput.checked = profileOptions.addProof;
-		saveToGDriveInput.checked = profileOptions.saveToGDrive && !profileOptions.saveToClipboard;
-		saveToGDriveInput.disabled = profileOptions.saveToClipboard;
+		saveToGDriveInput.checked = profileOptions.saveToGDrive;
+		saveToFilesystemInput.checked = !profileOptions.saveToGDrive && !saveToClipboardInput.checked;
 		compressHTMLInput.checked = profileOptions.compressHTML;
 		compressCSSInput.checked = profileOptions.compressCSS;
-		loadDeferredImagesInput.checked = profileOptions.loadDeferredImages && !profileOptions.saveRawPage;
-		loadDeferredImagesInput.disabled = profileOptions.saveRawPage;
+		loadDeferredImagesInput.checked = profileOptions.loadDeferredImages;
 		loadDeferredImagesMaxIdleTimeInput.value = profileOptions.loadDeferredImagesMaxIdleTime;
-		loadDeferredImagesKeepZoomLevelInput.checked = profileOptions.loadDeferredImagesKeepZoomLevel && !profileOptions.saveRawPage;
-		loadDeferredImagesKeepZoomLevelInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPape;
-		loadDeferredImagesMaxIdleTimeInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPage;
+		loadDeferredImagesKeepZoomLevelInput.checked = profileOptions.loadDeferredImagesKeepZoomLevel;
+		loadDeferredImagesKeepZoomLevelInput.disabled = !profileOptions.loadDeferredImages;
+		loadDeferredImagesMaxIdleTimeInput.disabled = !profileOptions.loadDeferredImages;
 		contextMenuEnabledInput.checked = profileOptions.contextMenuEnabled;
 		filenameTemplateInput.value = profileOptions.filenameTemplate;
 		filenameMaxLengthInput.value = profileOptions.filenameMaxLength;
-		filenameTemplateInput.disabled = profileOptions.saveToClipboard;
 		shadowEnabledInput.checked = profileOptions.shadowEnabled;
 		maxResourceSizeEnabledInput.checked = profileOptions.maxResourceSizeEnabled;
 		maxResourceSizeInput.value = profileOptions.maxResourceSize;
 		maxResourceSizeInput.disabled = !profileOptions.maxResourceSizeEnabled;
 		confirmFilenameInput.checked = profileOptions.confirmFilename;
-		confirmFilenameInput.disabled = profileOptions.saveToClipboard;
 		filenameConflictActionInput.value = profileOptions.filenameConflictAction;
-		filenameConflictActionInput.disabled = profileOptions.saveToClipboard || profileOptions.saveToGDrive;
 		removeAudioSrcInput.checked = profileOptions.removeAudioSrc;
 		removeVideoSrcInput.checked = profileOptions.removeVideoSrc;
 		displayInfobarInput.checked = profileOptions.displayInfobar;
 		displayStatsInput.checked = profileOptions.displayStats;
 		backgroundSaveInput.checked = profileOptions.backgroundSave;
-		backgroundSaveInput.disabled = profileOptions.saveToGDrive;
 		autoSaveDelayInput.value = profileOptions.autoSaveDelay;
 		autoSaveDelayInput.disabled = !profileOptions.autoSaveLoadOrUnload && !profileOptions.autoSaveLoad;
 		autoSaveLoadInput.checked = !profileOptions.autoSaveLoadOrUnload && profileOptions.autoSaveLoad;
@@ -693,8 +695,10 @@
 		removeAlternativeMediasInput.checked = profileOptions.removeAlternativeMedias;
 		saveCreatedBookmarksInput.checked = profileOptions.saveCreatedBookmarks;
 		passReferrerOnErrorInput.checked = profileOptions.passReferrerOnError;
-		replaceBookmarkURLInput.checked = profileOptions.saveCreatedBookmarks && profileOptions.backgroundSave && profileOptions.replaceBookmarkURL;
-		replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks || !profileOptions.backgroundSave || profileOptions.saveToClipboard || profileOptions.saveToGDrive;
+		replaceBookmarkURLInput.checked = profileOptions.replaceBookmarkURL;
+		replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks;
+		ignoredBookmarkFoldersInput.value = profileOptions.ignoredBookmarkFolders.map(folder => folder.replace(/,/g, "\\,")).join(","); // eslint-disable-line no-useless-escape
+		ignoredBookmarkFoldersInput.disabled = !profileOptions.saveCreatedBookmarks;
 		infobarTemplateInput.value = profileOptions.infobarTemplate;
 		includeInfobarInput.checked = profileOptions.includeInfobar;
 		confirmInfobarInput.checked = profileOptions.confirmInfobarContent;
@@ -704,12 +708,6 @@
 		defaultEditorModeInput.value = profileOptions.defaultEditorMode;
 		applySystemThemeInput.checked = profileOptions.applySystemTheme;
 		warnUnsavedPageInput.checked = profileOptions.warnUnsavedPage;
-		removeFramesInput.disabled = saveRawPageInput.checked;
-		removeFramesInput.checked = removeFramesInput.checked || saveRawPageInput.checked;
-		loadDeferredImagesInput.disabled = saveRawPageInput.checked;
-		if (saveRawPageInput.checked) {
-			loadDeferredImagesInput.checked = false;
-		}
 	}
 
 	function getProfileText(profileName) {
@@ -717,7 +715,11 @@
 	}
 
 	async function update() {
-		await pendingSave;
+		try {
+			await pendingSave;
+		} catch (error) {
+			// ignored
+		}
 		pendingSave = browser.runtime.sendMessage({
 			method: "config.updateProfile",
 			profileName: profileNamesInput.value,
@@ -763,6 +765,7 @@
 				saveCreatedBookmarks: saveCreatedBookmarksInput.checked,
 				passReferrerOnError: passReferrerOnErrorInput.checked,
 				replaceBookmarkURL: replaceBookmarkURLInput.checked,
+				ignoredBookmarkFolders: ignoredBookmarkFoldersInput.value.replace(/([^\\]),/g, "$1 ,").split(/[^\\],/).map(folder => folder.replace(/\\,/g, ",")),
 				groupDuplicateImages: groupDuplicateImagesInput.checked,
 				infobarTemplate: infobarTemplateInput.value,
 				includeInfobar: includeInfobarInput.checked,
@@ -775,7 +778,11 @@
 				warnUnsavedPage: warnUnsavedPageInput.checked
 			}
 		});
-		await pendingSave;
+		try {
+			await pendingSave;
+		} catch (error) {
+			// ignored
+		}
 	}
 
 	async function refreshExternalComponents() {
@@ -805,6 +812,7 @@
 					await disableOption();
 				}
 			} catch (error) {
+				saveCreatedBookmarksInput.checked = false;
 				await disableOption();
 			}
 		} else {
@@ -954,9 +962,16 @@
 		const items = doc.querySelectorAll("[data-options-label]");
 		items.forEach(itemElement => {
 			const optionLabel = document.getElementById(itemElement.dataset.optionsLabel);
+			const helpIconWrapper = document.createElement("span");
 			const helpIconContainer = document.createElement("span");
 			const helpIcon = document.createElement("img");
 			helpIcon.src = HELP_ICON_URL;
+			helpIconWrapper.className = "help-icon-wrapper";
+			const labelWords = optionLabel.textContent.split(/\s+/);
+			if (labelWords.length > 1) {
+				helpIconWrapper.textContent = labelWords.pop();
+				optionLabel.textContent = labelWords.join(" ") + " ";
+			}
 			helpIconContainer.className = "help-icon";
 			helpIconContainer.onclick = () => {
 				helpContent.hidden = !helpContent.hidden;
@@ -970,7 +985,8 @@
 				}
 			};
 			helpIconContainer.appendChild(helpIcon);
-			optionLabel.appendChild(helpIconContainer);
+			helpIconWrapper.appendChild(helpIconContainer);
+			optionLabel.appendChild(helpIconWrapper);
 			const helpContent = document.createElement("div");
 			helpContent.hidden = true;
 			helpContent.className = "help-content";

+ 8 - 5
extension/ui/content/content-ui-editor-web.js

@@ -932,11 +932,11 @@ table {
 			const file = event.dataTransfer.files[0];
 			event.preventDefault();
 			const content = new TextDecoder().decode(await file.arrayBuffer());
-			await init(content, file.name);
+			await init(content, { filename: file.name });
 		}
 	};
 
-	async function init(content, filename) {
+	async function init(content, { filename, reset } = {}) {
 		await initConstants();
 		const initScriptContentMatch = content.match(/<script data-template-shadow-root.*<\/script>/);
 		if (initScriptContentMatch && initScriptContentMatch[0]) {
@@ -981,6 +981,7 @@ table {
 				title: document.title,
 				icon: iconElement && iconElement.href,
 				filename,
+				reset,
 				formatPageEnabled: isProbablyReaderable(document)
 			}), "*");
 		}
@@ -1679,7 +1680,7 @@ table {
 	async function cancelFormatPage() {
 		if (previousContent) {
 			const contentEditable = document.body.contentEditable;
-			await init(previousContent);
+			await init(previousContent, { reset: true });
 			document.body.contentEditable = contentEditable;
 			onUpdate(false);
 			previousContent = null;
@@ -1843,8 +1844,10 @@ table {
 							shadowRoot.innerHTML = element.innerHTML;
 							element.remove();
 						} catch (error) {}						
-						processNode(shadowRoot);
-					}
+						if (shadowRoot) {
+							processNode(shadowRoot);
+						}
+					}					
 				})
 			};
 			const FORBIDDEN_TAG_NAMES = ${JSON.stringify(FORBIDDEN_TAG_NAMES)};

+ 112 - 88
extension/ui/pages/help.html

@@ -60,7 +60,7 @@
 					<li>Select "Save Selection" from the context menu without selecting any content first to display a
 						selector that will help you choose content by hovering over it with the mouse.</li>
 					<li>Right-click on the SingleFile button and select "Options" to open the options page.</li>
-					<li>Enable the option "Misc. &gt; save to Google Drive" to upload pages to Google Drive</li>
+					<li>Enable the option "Destination &gt; upload to Google Drive" to upload pages to Google Drive</li>
 					<li>You can use the customizable shortcut Ctrl+Shift+Y to save the current tab or the selected tabs.
 						Go to about:addons and select "Manage extension shortcuts" in the cogwheel menu to change it in
 						Firefox. Go to chrome://extensions/shortcuts to change it in Chrome..</li>
@@ -210,6 +210,11 @@
 							option to display the ℹ button at the top right of the page when viewing a saved page in a
 							browser where SingleFile is not installed.</p>
 					</li>
+					<li data-options-label="saveRawPageLabel"> <span class="option">Option: save raw page</span>
+						<p>Check this option to save the page without interpreting JavaScript. Checking this option may
+							alter the document.</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
 				</ul>
 				<p>Stylesheets</p>
 				<ul>
@@ -301,45 +306,24 @@
 						<p class="notice">It is recommended to <u>check</u> this option</p>
 					</li>
 				</ul>
-				<p>Auto-save</p>
+				<p>Destination</p>
 				<ul>
-					<li data-options-label="autoSaveLoadOrUnloadLabel"> <span class="option">Option: auto-save after
-							page load or on page unload</span>
-						<p>Check this option to auto-save pages after being loaded. If you browse to another page before
-							the page is fully loaded then the page will be saved just before being unloaded. With this
-							option active, you are guaranteed pages will always be saved. Some frame contents may be
-							missing (if you unchecked "remove frames") when pages are saved before being unloaded. </p>
-					</li>
-					<li data-options-label="autoSaveLoadLabel"> <span class="option">Option: auto-save after page
-							load</span>
-						<p>Check this option to auto-save pages after being loaded.</p>
-					</li>
-					<li data-options-label="autoSaveUnloadLabel"> <span class="option">Option: auto-save on page
-							unload</span>
-						<p>Check this option to auto-save pages before being unloaded instead of saving pages after
-							being loaded. With this option active, you are guaranteed pages will always be saved but
-							some frame contents may be missing (if you unchecked "remove frames"). </p>
-					</li>
-					<li data-options-label="autoSaveDelayLabel"> <span class="option">Option: auto-save waiting delay
-							after load (s)</span>
-						<p>Specify the delay in seconds to wait before saving a page when the "auto-save on page load or
-							on page unload" or "auto-save on page load" is checked. </p>
-					</li>
-					<li data-options-label="autoSaveRepeatLabel"> <span class="option">Option: auto-save
-							periodically</span>
-						<p>Check this option to auto-save pages periodically after load.</p>
+					<li data-options-label="saveToFilesystemLabel"> <span class="option">Option: save to
+							filesystem</span>
+						<p>Check this option to save the downloaded page on the filesystem of your computer.</p>
+						<p class="notice">It is recommended to <u>check</u> this option</p>
 					</li>
-					<li data-options-label="autoSaveRepeatDelayLabel"> <span class="option">Option: period (s)</span>
-						<p>Specify the delay in seconds to wait before each page saving when the "auto-save
-							periodically" option is checked. </p>
+					<li data-options-label="saveToClipboardLabel"> <span class="option">Option: copy to clipboard</span>
+						<p>Check this option to copy the page to the clipboard.</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
-					<li data-options-label="autoSaveExternalSaveLabel"> <span class="option">Option: save the page with
-							SingleFile Companion</span>
-						<p>Check this option to delegate the saving process to SingleFile Companion. It is a program
-							that runs outside the browser and can help to make the saving process more transparent. It
-							also allows the pages to be saved in another directory than the download directory. You can
-							find more info <a
-								href="https://github.com/gildas-lormeau/SingleFile/tree/master/companion">here</a></p>
+					<li data-options-label="saveToGDriveLabel"> <span class="option">Option: upload to Google
+							Drive</span>
+						<p>Check this option to save the page on Google Drive.</p>
+						<p>The permissions requested by SingleFile allow it to access only to the files and folders it
+							has created. When you uncheck this option, SingleFile revokes automatically its access to
+							your Google Drive account. </p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
 				</ul>
 				<p>Annotation editor</p>
@@ -385,23 +369,71 @@
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
 				</ul>
-				<p>Misc.</p>
+				<p>Bookmarks</p>
 				<ul>
-					<li data-options-label="backgroundSaveLabel"> <span class="option">Option: save pages in
-							background</span>
-						<p>Uncheck this option if you get invalid file names like
-							"37bec68b-446a-46a5-8642-19a89c231b46.html" or interrupted downloads when saving pages. You
-							can also uncheck this option if you want the "Save as" dialog to remember the last saved
-							path. Unchecking this option prevent using sub-directories in filename templates.</p>
-						<p class="notice">It is recommended to <u>check</u> this option</p>
+					<li data-options-label="saveCreatedBookmarksLabel"> <span class="option">Option: save the page of a
+							newly created bookmark</span>
+						<p>Check this option to save pages that you add into your bookmarks. Note that if the page is to
+							be saved is not already displayed in a tab, SingleFile will open temporarily a new tab to
+							save the page. </p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
-					<li data-options-label="displayStatsLabel"> <span class="option">Option: display stats in the
-							console after processing</span>
-						<p>Check this option to display stats about processing in the JavaScript developer tools of your
-							browser. Checking this option may increase the CPU consumption and the time needed to save a
-							page. </p>
+					<li data-options-label="replaceBookmarkURLLabel"> <span class="option">Option: link the new bookmark
+							to the saved page</span>
+						<p>Check this option to replace the URL of the page added into your bookmark with the file URI
+							of the saved page on your disk. </p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
+					<li data-options-label="ignoredBookmarkFoldersLabel"> <span class="option">Option: ignored
+							folders</span>
+						<p>Enter a list of bookmark folder names to ignore. The folder names must be separated with a
+							comma.
+						</p>
+					</li>
+				</ul>
+				<p>Auto-save</p>
+				<ul>
+					<li data-options-label="autoSaveLoadOrUnloadLabel"> <span class="option">Option: auto-save after
+							page load or on page unload</span>
+						<p>Check this option to auto-save pages after being loaded. If you browse to another page before
+							the page is fully loaded then the page will be saved just before being unloaded. With this
+							option active, you are guaranteed pages will always be saved. Some frame contents may be
+							missing (if you unchecked "remove frames") when pages are saved before being unloaded. </p>
+					</li>
+					<li data-options-label="autoSaveLoadLabel"> <span class="option">Option: auto-save after page
+							load</span>
+						<p>Check this option to auto-save pages after being loaded.</p>
+					</li>
+					<li data-options-label="autoSaveUnloadLabel"> <span class="option">Option: auto-save on page
+							unload</span>
+						<p>Check this option to auto-save pages before being unloaded instead of saving pages after
+							being loaded. With this option active, you are guaranteed pages will always be saved but
+							some frame contents may be missing (if you unchecked "remove frames"). </p>
+					</li>
+					<li data-options-label="autoSaveDelayLabel"> <span class="option">Option: auto-save waiting delay
+							after load (s)</span>
+						<p>Specify the delay in seconds to wait before saving a page when the "auto-save on page load or
+							on page unload" or "auto-save on page load" is checked. </p>
+					</li>
+					<li data-options-label="autoSaveRepeatLabel"> <span class="option">Option: auto-save
+							periodically</span>
+						<p>Check this option to auto-save pages periodically after load.</p>
+					</li>
+					<li data-options-label="autoSaveRepeatDelayLabel"> <span class="option">Option: period (s)</span>
+						<p>Specify the delay in seconds to wait before each page saving when the "auto-save
+							periodically" option is checked. </p>
+					</li>
+					<li data-options-label="autoSaveExternalSaveLabel"> <span class="option">Option: save the page with
+							SingleFile Companion</span>
+						<p>Check this option to delegate the saving process to SingleFile Companion. It is a program
+							that runs outside the browser and can help to make the saving process more transparent. It
+							also allows the pages to be saved in another directory than the download directory. You can
+							find more info <a
+								href="https://github.com/gildas-lormeau/SingleFile/tree/master/companion">here</a></p>
+					</li>
+				</ul>
+				<p>Misc.</p>
+				<ul>
 					<li data-options-label="setMaxResourceSizeLabel"> <span class="option">Option: set a maximum size
 							for embedded resources (MB)</span>
 						<p>Check this option to remove from the saved page embedded resources (i.e. images, stylesheets,
@@ -410,24 +442,14 @@
 					<li data-options-label="maxResourceSizeLabel"> <span class="option">Option: maximum size (MB)</span>
 						<p>Specify the maximum size of embedded resources in megabytes.</p>
 					</li>
-					<li data-options-label="passReferrerOnErrorLabel"> <span class="option">Option: pass \"Referer\" header on
+					<li data-options-label="passReferrerOnErrorLabel"> <span class="option">Option: pass \"Referer\"
+							header on
 							cross-origin errors</span>
 						<p>Check this option to pass the HTTP header "Referer" with the "origin" policy after an 401,
 							403, or 404 HTTP error when downloading a cross-origin resource. You should enable this
 							option if you cannot download resources blocked by a hotlink protection.</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
-					<li data-options-label="saveRawPageLabel"> <span class="option">Option: save raw page</span>
-						<p>Check this option to save the page without interpreting JavaScript. Checking this option may
-							alter the document, will force the options "remove frames", "remove hidden elements" to be
-							enabled and "save deferred images" to be disabled.</p>
-						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
-					</li>
-					<li data-options-label="saveToClipboardLabel"> <span class="option">Option: save to clipboard</span>
-						<p>Check this option to copy the page to the clipboard instead of downloading it on your
-							computer. Checking this option will force the "File name" options to be disabled.</p>
-						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
-					</li>
 					<li data-options-label="addProofLabel"> <span class="option">Option: add proof od existence</span>
 						<p>Check this option to create a worldwide proof of the existence of the page you want to save.
 						</p>
@@ -446,28 +468,19 @@
 						</ul>
 						<p> More information <a href="https://doc.woleet.io">doc.woleet.io</a> </p>
 					</li>
-					<li data-options-label="saveToGDriveLabel"> <span class="option">Option: save to Google Drive</span>
-						<p>Check this option to save the page on Google Drive instead of downloading it on your
-							computer. Checking this option will force some "File name" options to be disabled. However,
-							you can change the value of the "File name &gt; template" option to save files into
-							sub-folders, e.g. "<code>SingleFile/{page-title} ({date-iso} {time-locale}).html</code>" to
-							save pages in the folder named "SingleFile". </p>
-						<p> The permissions requested by SingleFile allow it to access only to the files and folders it
-							has created. When you uncheck this option, SingleFile revokes automatically its access to
-							your Google Drive account. </p>
-						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
-					</li>
-					<li data-options-label="saveCreatedBookmarksLabel"> <span class="option">Option: save the page of a
-							newly created bookmark</span>
-						<p> Check this option to save pages that you add into your bookmarks. Note that if the page is
-							to be saved is not already displayed in a tab, SingleFile will open temporarily a new tab to
-							save the page. </p>
-						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					<li data-options-label="backgroundSaveLabel"> <span class="option">Option: save pages in
+							background</span>
+						<p>Uncheck this option if you get invalid file names like
+							"37bec68b-446a-46a5-8642-19a89c231b46.html" or interrupted downloads when saving pages. You
+							can also uncheck this option if you want the "Save as" dialog to remember the last saved
+							path. Unchecking this option prevent using sub-directories in filename templates.</p>
+						<p class="notice">It is recommended to <u>check</u> this option</p>
 					</li>
-					<li data-options-label="replaceBookmarkURLLabel"> <span class="option">Option: link the new bookmark
-							to the saved page</span>
-						<p> Check this option to replace the URL of the page added into your bookmark with the file URI
-							of the saved page on your disk. </p>
+					<li data-options-label="displayStatsLabel"> <span class="option">Option: display stats in the
+							console after processing</span>
+						<p>Check this option to display stats about processing in the JavaScript developer tools of your
+							browser. Checking this option may increase the CPU consumption and the time needed to save a
+							page. </p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
 				</ul>
@@ -611,6 +624,12 @@
 						"http://example.com")</li>
 					<li><code>{url-referrer-flat}</code>: the URI of the page that "linked" to the page with slashed
 						replaced (e.g. "http_example.com")</li>
+					<li><code>{bookmark-pathname}</code>: the path name of the newly created bookmark (e.g. "My
+						Bookmarks/Last Month") when the option "Bookmarks &gt; save the page of a
+						newly created bookmark" is enabled</li>
+					<li><code>{bookmark-pathname-flat}</code>: the path name of the newly created bookmark with replaced
+						slashes (e.g. "My Bookmarks_Last Month") when the option "Bookmarks &gt; save the page of a
+						newly created bookmark" is enabled</li>
 					<li><code>{tab-id}</code>: the unique identifier of the tab (e.g. "326")</li>
 					<li><code>{tab-index}</code>: the index of the tab in the window (e.g. "1")</li>
 					<li><code>{digest-sha-256}</code>: the SHA-256 hash value of the entire page content (e.g.
@@ -629,7 +648,8 @@
 			</li>
 			<li><a id="known-issues">Known issues</a>
 				<ul>
-					<li>All browsers <ul>
+					<li>All browsers
+						<ul>
 							<li>For security reasons, you cannot save pages hosted on https://chrome.google.com,
 								https://addons.mozilla.org and some other Mozilla domains. When this happens, 🚫 is
 								displayed on top of the SingleFile icon.</li>
@@ -641,9 +661,11 @@
 								&lt;, &gt;</li>
 						</ul>
 					</li>
-					<li>Chrome/Opera <ul>
+					<li>Chromium-based browsers
+						<ul>
 							<li>You must enable the option "Allow access to file URLs" in the extension page to display
-								the infobar when viewing a saved page, or to save a page stored on the filesystem.</li>
+								the infobar when viewing a saved page, to save or to annotate a page stored on the
+								filesystem.</li>
 							<li>If the filename of a saved page looks like "56833935-156b-4d8c-a00f-19599c6513d3",
 								disable the option "Misc. &gt; Save pages in background". Reinstalling the browser may
 								also fix this issue.</li>
@@ -652,8 +674,10 @@
 								chrome://settings/downloads</li>
 						</ul>
 					</li>
-					<li>Firefox <ul>
-							<li>The "file name conflict resolution" option does not work if set to "prompt for a name".
+					<li>Firefox
+						<ul>
+							<li>The "File name &gt; file name conflict resolution" option does not work if set to
+								"prompt for a name".
 							</li>
 							<li>Sometimes, SingleFile is unable to save the contents of sandboxed iframes.</li>
 							<li>When processing a page from the filesystem, external resources (e.g. images,

+ 66 - 15
extension/ui/pages/options.css

@@ -1,7 +1,12 @@
+* {
+    box-sizing: content-box;
+}
+
 body {
     background-color: #fff;
     font-family: sans-serif;
     font-size: 12px;
+    min-width: 480px;
     max-width: 1024px;
     width: 100%;
     margin: 0;
@@ -76,9 +81,11 @@ input.large-input {
 }
 
 h3 {
+    display: flex;
     padding-left: 8px;
     padding-top: 10px;
     margin-top: 4px;
+    margin-right: 10px;
     min-height: 28px;
 }
 
@@ -102,11 +109,19 @@ h3 a {
 }
 
 .profiles {
-    float: right;
-    margin-right: 12px;
-    position: relative;
-    top: 3px;
-    max-width: calc(100% - 115px);
+    display: flex;
+    flex: 0 1 auto;
+    margin-left: 8px;
+    justify-content: flex-end;
+}
+
+.options-title {
+    flex: 1;
+    white-space: nowrap;
+}
+
+.help-icon-wrapper {
+    white-space: nowrap;
 }
 
 .help-icon {
@@ -165,23 +180,28 @@ h3 a {
     margin: 2px;
 }
 
+.rules-table-container * {
+    box-sizing: border-box;
+}
+
 .rules-table-container {
-    width: 100%;
+    width: calc(100% - 10px);
     border-spacing: 0;
     border-color: rgb(191, 191, 191);
     border-style: solid;
     border-width: 1px;
     margin-top: 24px;
+    margin-left: 6px;
 }
 
 .rules-table-container .tr {
     height: 26px;
     display: grid;
-    grid-template-columns: 1fr 1fr 1fr 58px;
+    grid-template-columns: 1fr 1fr 1fr 64px;
 }
 
 .rules-table-container.compact .tr {
-    grid-template-columns: 1fr 1fr 58px;
+    grid-template-columns: 1fr 1fr 64px;
 }
 
 .rules-table-container.compact .tr .rule-autosave-profile {
@@ -234,8 +254,9 @@ h3 a {
 .rules-table-container button {
     padding: 0;
     margin: 0;
-    width: 22px;
     text-align: center;
+    margin-left: 4px;
+    min-width: 22px;
 }
 
 .rules-table-container .thead .tr {
@@ -270,7 +291,8 @@ h3 a {
 }
 
 .profiles select {
-    max-width: calc(100% - 78px);
+    width: calc(100% - 86px);
+    text-align-last: right;
 }
 
 .rules-table-container select {
@@ -354,6 +376,7 @@ a {
 
 .option.vertical label {
     align-self: flex-start;
+    min-height: 24px;
 }
 
 .option.vertical input {
@@ -486,7 +509,7 @@ a {
     margin-top: 8px;
 }
 
-.maximized main {
+.maximized body>main {
     margin: 0px;
     border: solid 1px rgb(191, 191, 191);
     background-color: #fbfbfb;
@@ -497,8 +520,8 @@ html.maximized {
 }
 
 .maximized .profiles {
+    position: relative;
     top: -4px;
-    max-width: calc(100% - 90px);
 }
 
 .maximized #helpLabel {
@@ -507,7 +530,35 @@ html.maximized {
     padding-top: 0;
 }
 
-@media (max-width:400px) {
+@media (max-width:279px) {
+    h3 {
+        flex-direction: column;
+    }
+
+    .profiles, .maximized .profiles {
+        position: static;
+        margin-top: 8px;
+        margin-left: 16px;
+        max-width: 100%;
+        justify-content: flex-start;
+    }
+
+    .side-panel .profiles {
+        margin-top: 0;
+        margin-left: 0;
+        margin-right: 8px;
+        justify-content: inherit;
+    }
+
+    .profiles select {
+        text-align-last: auto;
+    }
+}
+
+@media (max-width:479px) {
+    body {
+        min-width: auto;
+    }
 
     body,
     input,
@@ -542,7 +593,7 @@ html.maximized {
     }
 
     .side-panel,
-    .side-panel main,
+    .side-panel body>main,
     .side-panel details>summary,
     .side-panel button,
     .side-panel select,
@@ -550,7 +601,7 @@ html.maximized {
         background-color: #38383d;
     }
 
-    .maximized main {
+    .maximized body>main {
         border-color: rgb(81, 81, 81);
     }
 

+ 58 - 44
extension/ui/pages/options.html

@@ -97,6 +97,10 @@
 				<label for="includeInfobarInput" id="includeInfobarLabel"></label>
 				<input type="checkbox" id="includeInfobarInput">
 			</div>
+			<div class="option">
+				<label for="saveRawPageInput" id="saveRawPageLabel"></label>
+				<input type="checkbox" id="saveRawPageInput">
+			</div>
 		</details>
 		<details>
 			<summary id="stylesheetsLabel"></summary>
@@ -163,34 +167,18 @@
 			</div>
 		</details>
 		<details>
-			<summary id="autoSaveLabel"></summary>
-			<div class="option">
-				<label for="autoSaveLoadOrUnloadInput" id="autoSaveLoadOrUnloadLabel"></label>
-				<input type="checkbox" id="autoSaveLoadOrUnloadInput">
-			</div>
+			<summary id="destinationLabel"></summary>
 			<div class="option">
-				<label for="autoSaveLoadInput" id="autoSaveLoadLabel"></label>
-				<input type="checkbox" id="autoSaveLoadInput">
+				<label for="saveToFilesystemInput" id="saveToFilesystemLabel"></label>
+				<input type="radio" id="saveToFilesystemInput" name="destinationInput">
 			</div>
 			<div class="option">
-				<label for="autoSaveUnloadInput" id="autoSaveUnloadLabel"></label>
-				<input type="checkbox" id="autoSaveUnloadInput">
-			</div>
-			<div class="option">
-				<label for="autoSaveDelayInput" id="autoSaveDelayLabel"></label>
-				<input type="number" id="autoSaveDelayInput" min="0">
-			</div>
-			<div class="option">
-				<label for="autoSaveRepeatInput" id="autoSaveRepeatLabel"></label>
-				<input type="checkbox" id="autoSaveRepeatInput">
-			</div>
-			<div class="option second-level">
-				<label for="autoSaveRepeatDelayInput" id="autoSaveRepeatDelayLabel"></label>
-				<input type="number" id="autoSaveRepeatDelayInput" min="1">
+				<label for="saveToClipboardInput" id="saveToClipboardLabel"></label>
+				<input type="radio" id="saveToClipboardInput" name="destinationInput">
 			</div>
 			<div class="option">
-				<label for="autoSaveExternalSaveInput" id="autoSaveExternalSaveLabel"></label>
-				<input type="checkbox" id="autoSaveExternalSaveInput">
+				<label for="saveToGDriveInput" id="saveToGDriveLabel"></label>
+				<input type="radio" id="saveToGDriveInput" name="destinationInput">
 			</div>
 		</details>
 		<details>
@@ -223,15 +211,53 @@
 			</div>
 		</details>
 		<details>
-			<summary id="miscLabel"></summary>
+			<summary id="bookmarksLabel"></summary>
 			<div class="option">
-				<label for="backgroundSaveInput" id="backgroundSaveLabel"></label>
-				<input type="checkbox" id="backgroundSaveInput">
+				<label for="saveCreatedBookmarksInput" id="saveCreatedBookmarksLabel"></label>
+				<input type="checkbox" id="saveCreatedBookmarksInput">
 			</div>
+			<div class="option second-level">
+				<label for="replaceBookmarkURLInput" id="replaceBookmarkURLLabel"></label>
+				<input type="checkbox" id="replaceBookmarkURLInput">
+			</div>
+			<div class="option second-level vertical">
+				<label for="ignoredBookmarkFoldersInput" id="ignoredBookmarkFoldersLabel"></label>
+				<input type="text" id="ignoredBookmarkFoldersInput">
+			</div>
+		</details>
+		<details>
+			<summary id="autoSaveLabel"></summary>
 			<div class="option">
-				<label for="displayStatsInput" id="displayStatsLabel"></label>
-				<input type="checkbox" id="displayStatsInput">
+				<label for="autoSaveLoadOrUnloadInput" id="autoSaveLoadOrUnloadLabel"></label>
+				<input type="checkbox" id="autoSaveLoadOrUnloadInput">
 			</div>
+			<div class="option">
+				<label for="autoSaveLoadInput" id="autoSaveLoadLabel"></label>
+				<input type="checkbox" id="autoSaveLoadInput">
+			</div>
+			<div class="option">
+				<label for="autoSaveUnloadInput" id="autoSaveUnloadLabel"></label>
+				<input type="checkbox" id="autoSaveUnloadInput">
+			</div>
+			<div class="option">
+				<label for="autoSaveDelayInput" id="autoSaveDelayLabel"></label>
+				<input type="number" id="autoSaveDelayInput" min="0">
+			</div>
+			<div class="option">
+				<label for="autoSaveRepeatInput" id="autoSaveRepeatLabel"></label>
+				<input type="checkbox" id="autoSaveRepeatInput">
+			</div>
+			<div class="option second-level">
+				<label for="autoSaveRepeatDelayInput" id="autoSaveRepeatDelayLabel"></label>
+				<input type="number" id="autoSaveRepeatDelayInput" min="1">
+			</div>
+			<div class="option">
+				<label for="autoSaveExternalSaveInput" id="autoSaveExternalSaveLabel"></label>
+				<input type="checkbox" id="autoSaveExternalSaveInput">
+			</div>
+		</details>
+		<details>
+			<summary id="miscLabel"></summary>
 			<div class="option">
 				<label for="maxResourceSizeEnabledInput" id="setMaxResourceSizeLabel"></label>
 				<input type="checkbox" id="maxResourceSizeEnabledInput">
@@ -244,29 +270,17 @@
 				<label for="passReferrerOnErrorInput" id="passReferrerOnErrorLabel"></label>
 				<input type="checkbox" id="passReferrerOnErrorInput">
 			</div>
-			<div class="option">
-				<label for="saveRawPageInput" id="saveRawPageLabel"></label>
-				<input type="checkbox" id="saveRawPageInput">
-			</div>
-			<div class="option">
-				<label for="saveToClipboardInput" id="saveToClipboardLabel"></label>
-				<input type="checkbox" id="saveToClipboardInput">
-			</div>
 			<div class="option">
 				<label for="addProofInput" id="addProofLabel"></label>
 				<input type="checkbox" id="addProofInput">
 			</div>
 			<div class="option">
-				<label for="saveToGDriveInput" id="saveToGDriveLabel"></label>
-				<input type="checkbox" id="saveToGDriveInput">
+				<label for="backgroundSaveInput" id="backgroundSaveLabel"></label>
+				<input type="checkbox" id="backgroundSaveInput">
 			</div>
 			<div class="option">
-				<label for="saveCreatedBookmarksInput" id="saveCreatedBookmarksLabel"></label>
-				<input type="checkbox" id="saveCreatedBookmarksInput">
-			</div>
-			<div class="option second-level">
-				<label for="replaceBookmarkURLInput" id="replaceBookmarkURLLabel"></label>
-				<input type="checkbox" id="replaceBookmarkURLInput">
+				<label for="displayStatsInput" id="displayStatsLabel"></label>
+				<input type="checkbox" id="displayStatsInput">
 			</div>
 		</details>
 		<details>

+ 60 - 37
lib/single-file/processors/lazy/content/content-lazy-loader.js

@@ -33,10 +33,24 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 	const MutationObserver = window.MutationObserver;
 	const addEventListener = (type, listener, options) => window.addEventListener(type, listener, options);
 	const removeEventListener = (type, listener, options) => window.removeEventListener(type, listener, options);
+	const timeouts = new Map();
+
+	if (browser && browser.runtime && browser.runtime.onMessage && browser.runtime.onMessage.addListener) {
+		browser.runtime.onMessage.addListener(message => {
+			if (message.method == "singlefile.lazyTimeout.onTimeout") {
+				const timeoutData = timeouts.get(message.type);
+				if (timeoutData) {
+					timeouts.delete(message.type);
+					timeoutData.callback();
+				}
+			}
+		});
+	}
 
 	return {
 		process: async options => {
 			if (document.documentElement) {
+				timeouts.clear();
 				const maxScrollY = Math.max(document.documentElement.scrollHeight - (document.documentElement.clientHeight * 1.5), 0);
 				const maxScrollX = Math.max(document.documentElement.scrollWidth - (document.documentElement.clientWidth * 1.5), 0);
 				if (window.scrollY <= maxScrollY && window.scrollX <= maxScrollX) {
@@ -55,7 +69,7 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 	function process(options) {
 		const frames = singlefile.lib.processors.hooks.content.frames;
 		return new Promise(async resolve => { // eslint-disable-line  no-async-promise-executor
-			let timeoutId, idleTimeoutId, maxTimeoutId, loadingImages;
+			let loadingImages;
 			const pendingImages = new Set();
 			const observer = new MutationObserver(async mutations => {
 				mutations = mutations.filter(mutation => mutation.type == ATTRIBUTES_MUTATION_TYPE);
@@ -71,20 +85,21 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 					});
 					if (updated.length) {
 						loadingImages = true;
-						maxTimeoutId = await deferForceLazyLoadEnd(timeoutId, idleTimeoutId, maxTimeoutId, observer, options, cleanupAndResolve);
+						await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
 						if (!pendingImages.size) {
-							timeoutId = await deferLazyLoadEnd(timeoutId, idleTimeoutId, observer, options, cleanupAndResolve);
+							await deferLazyLoadEnd(observer, options, cleanupAndResolve);
 						}
 					}
 				}
 			});
-			idleTimeoutId = await setAsyncTimeout(() => {
+			await setAsyncTimeout("idleTimeout", () => {
 				if (!loadingImages) {
-					clearAsyncTimeout(timeoutId);
-					lazyLoadEnd(idleTimeoutId, observer, options, cleanupAndResolve);
+					clearAsyncTimeout("loadTimeout");
+					clearAsyncTimeout("maxTimeout");
+					lazyLoadEnd(observer, options, cleanupAndResolve);
 				}
-			}, options.loadDeferredImagesMaxIdleTime * 1.2);
-			maxTimeoutId = await deferForceLazyLoadEnd(timeoutId, idleTimeoutId, maxTimeoutId, observer, options, cleanupAndResolve);
+			}, options.loadDeferredImagesMaxIdleTime * 2);
+			await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
 			observer.observe(document, { subtree: true, childList: true, attributes: true });
 			if (frames) {
 				addEventListener(frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
@@ -100,17 +115,19 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 
 			async function onImageLoadEvent(event) {
 				loadingImages = true;
-				maxTimeoutId = await deferForceLazyLoadEnd(timeoutId, idleTimeoutId, maxTimeoutId, observer, options, cleanupAndResolve);
+				await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+				await deferLazyLoadEnd(observer, options, cleanupAndResolve);
 				if (event.detail) {
 					pendingImages.add(event.detail);
 				}
 			}
 
 			async function onImageLoadedEvent(event) {
-				maxTimeoutId = await deferForceLazyLoadEnd(timeoutId, idleTimeoutId, maxTimeoutId, observer, options, cleanupAndResolve);
+				await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+				await deferLazyLoadEnd(observer, options, cleanupAndResolve);
 				pendingImages.delete(event.detail);
 				if (!pendingImages.size) {
-					timeoutId = await deferLazyLoadEnd(timeoutId, idleTimeoutId, observer, options, cleanupAndResolve);
+					await deferLazyLoadEnd(observer, options, cleanupAndResolve);
 				}
 			}
 
@@ -125,50 +142,56 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 		});
 	}
 
-	async function deferLazyLoadEnd(timeoutId, idleTimeoutId, observer, options, resolve) {
-		await clearAsyncTimeout(timeoutId);
-		return setAsyncTimeout(() => lazyLoadEnd(idleTimeoutId, observer, options, resolve), options.loadDeferredImagesMaxIdleTime);
+	async function deferLazyLoadEnd(observer, options, resolve) {
+		await setAsyncTimeout("loadTimeout", () => lazyLoadEnd(observer, options, resolve), options.loadDeferredImagesMaxIdleTime);
 	}
 
-	function deferForceLazyLoadEnd(timeoutId, idleTimeoutId, maxTimeoutId, observer, options, resolve) {
-		clearAsyncTimeout(maxTimeoutId);
-		return setAsyncTimeout(() => {
-			clearAsyncTimeout(timeoutId);
-			lazyLoadEnd(idleTimeoutId, observer, options, resolve);
+	async function deferForceLazyLoadEnd(observer, options, resolve) {
+		await setAsyncTimeout("maxTimeout", async () => {
+			await clearAsyncTimeout("loadTimeout");
+			await lazyLoadEnd(observer, options, resolve);
 		}, options.loadDeferredImagesMaxIdleTime * 10);
 	}
 
-	function lazyLoadEnd(idleTimeoutId, observer, options, resolve) {
-		clearAsyncTimeout(idleTimeoutId);
+	async function lazyLoadEnd(observer, options, resolve) {
+		await clearAsyncTimeout("idleTimeout");
 		if (singlefile.lib.processors.hooks.content.frames) {
 			singlefile.lib.processors.hooks.content.frames.loadDeferredImagesEnd(options);
 		}
-		setAsyncTimeout(resolve, options.loadDeferredImagesMaxIdleTime / 2);
+		await setAsyncTimeout("endTimeout", async () => {
+			await clearAsyncTimeout("maxTimeout");
+			resolve();
+		}, options.loadDeferredImagesMaxIdleTime / 2);
 		observer.disconnect();
 	}
 
-	async function setAsyncTimeout(callback, delay) {
+	async function setAsyncTimeout(type, callback, delay) {
 		if (browser && browser.runtime && browser.runtime.sendMessage) {
-			const timeoutId = await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.setTimeout", delay });
-			const timeoutCallback = message => {
-				if (message.method == "singlefile.lazyTimeout.onTimeout" && message.id == timeoutId) {
-					browser.runtime.onMessage.removeListener(timeoutCallback);
-					callback();
-					return Promise.resolve({});
-				}
-			};
-			browser.runtime.onMessage.addListener(timeoutCallback);
-			return timeoutId;
+			if (!timeouts.get(type) || !timeouts.get(type).pending) {
+				const timeoutData = { callback, pending: true };
+				timeouts.set(type, timeoutData);
+				await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.setTimeout", type, delay });
+				timeoutData.pending = false;
+			}
 		} else {
-			return window.setTimeout(callback, delay);
+			const timeoutId = timeouts.get(type);
+			if (timeoutId) {
+				window.clearTimeout(timeoutId);
+			}
+			timeouts.set(type, callback);
+			window.setTimeout(callback, delay);
 		}
 	}
 
-	async function clearAsyncTimeout(timeoutId) {
+	async function clearAsyncTimeout(type) {
 		if (browser && browser.runtime && browser.runtime.sendMessage) {
-			await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.clearTimeout", id: timeoutId });
+			await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.clearTimeout", type });
 		} else {
-			return window.clearTimeout(timeoutId);
+			const previousTimeoutId = timeouts.get(type);
+			timeouts.delete(type);
+			if (previousTimeoutId) {
+				window.clearTimeout(previousTimeoutId);
+			}
 		}
 	}
 

+ 14 - 7
lib/single-file/single-file-core.js

@@ -1481,7 +1481,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 			template = await evalTemplateVariable(template, "url-href", () => decode(url.href) || "No href", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
 			template = await evalTemplateVariable(template, "url-href-flat", () => decode(url.href) || "No href", false, options.filenameReplacementCharacter);
 			template = await evalTemplateVariable(template, "url-referrer", () => decode(options.referrer) || "No referrer", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-referrer-flat", () => decode(options.referrer) || "No referrer", dontReplaceSlash, options.filenameReplacementCharacter);
+			template = await evalTemplateVariable(template, "url-referrer-flat", () => decode(options.referrer) || "No referrer", false, options.filenameReplacementCharacter);
 			template = await evalTemplateVariable(template, "url-password", () => url.password || "No password", dontReplaceSlash, options.filenameReplacementCharacter);
 			template = await evalTemplateVariable(template, "url-pathname", () => decode(url.pathname).replace(/^\//, "").replace(/\/$/, "") || "No pathname", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
 			template = await evalTemplateVariable(template, "url-pathname-flat", () => decode(url.pathname) || "No pathname", false, options.filenameReplacementCharacter);
@@ -1502,6 +1502,9 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 				template = await evalTemplateVariable(template, "digest-sha-384", async () => util.digest("SHA-384", content), dontReplaceSlash, options.filenameReplacementCharacter);
 				template = await evalTemplateVariable(template, "digest-sha-512", async () => util.digest("SHA-512", content), dontReplaceSlash, options.filenameReplacementCharacter);
 			}
+			const bookmarkFolder = (options.bookmarkFolders && options.bookmarkFolders.join("/")) || "";
+			template = await evalTemplateVariable(template, "bookmark-pathname", () => bookmarkFolder, dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
+			template = await evalTemplateVariable(template, "bookmark-pathname-flat", () => bookmarkFolder, false, options.filenameReplacementCharacter);
 			return template.trim();
 
 			function decode(value) {
@@ -1716,12 +1719,16 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 
 		static async processStyle(declarations, baseURI, options, cssVariables, batchRequest) {
 			await Promise.all(declarations.map(async declaration => {
-				let children = declaration.value.children;
-				if (!children && declaration.value && declaration.value.type == "Raw") {
-					try {
-						children = cssTree.parse(declaration.value.value, { context: "value" }).children;
-					} catch (error) {
-						// ignored
+				let children;
+				if (declaration.value) {
+					if (declaration.value.children) {
+						children = declaration.value.children;
+					} else if (declaration.value.type == "Raw") {
+						try {
+							children = cssTree.parse(declaration.value.value, { context: "value" }).children;
+						} catch (error) {
+							// ignored
+						}
 					}
 				}
 				if (declaration.type == "Declaration" && children) {

+ 1 - 1
lib/single-file/single-file-helper.js

@@ -111,7 +111,7 @@ this.singlefile.lib.helper = this.singlefile.lib.helper || (() => {
 	}
 
 	function preProcessDoc(doc, win, options) {
-		doc.querySelectorAll("noscript").forEach(element => {
+		doc.querySelectorAll("noscript:not([" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "])").forEach(element => {
 			element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.textContent);
 			element.textContent = "";
 		});

File diff ditekan karena terlalu besar
+ 0 - 0
lib/single-file/vendor/css-tree.js


+ 1 - 1
manifest.json

@@ -8,7 +8,7 @@
 		"64": "extension/ui/resources/icon_64.png",
 		"128": "extension/ui/resources/icon_128.png"
 	},
-	"version": "1.18.45",
+	"version": "1.18.51",
 	"description": "__MSG_extensionDescription__",
 	"content_scripts": [
 		{

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "single-file",
-	"version": "0.1.44",
+	"version": "0.1.51",
 	"description": "SingleFile",
 	"author": "Gildas Lormeau",
 	"license": "AGPL-3.0-or-later",

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini