Bladeren bron

Basic implementation of profiles

Gildas 7 jaren geleden
bovenliggende
commit
3c9c9890b8

+ 32 - 0
_locales/en/messages.json

@@ -7,6 +7,10 @@
         "message": "Save page with SingleFile",
         "description": "Menu entry: 'Save page with SingleFile'"
     },
+    "menuSelectProfile": {
+        "message": "Select profile",
+        "description": "Menu entry: 'Select profile'"
+    },
     "menuSaveSelection": {
         "message": "Save selection",
         "description": "Menu entry: 'Save selection'"
@@ -267,6 +271,10 @@
         "message": "Reset all options to their default values",
         "description": "Options 'Reset' button tooltip"
     },
+    "optionsResetConfirm": {
+        "message": "Confirm the reset of all options",
+        "description": "Popup text 'Confirm the reset of all options' in the options page"
+    },
     "logPanelDeferredImages": {
         "message": "Deferred images",
         "description": "Label 'Deferred images' in the log panel"
@@ -282,5 +290,29 @@
     "logPanelWidth": {
         "message": "120",
         "description": "Width of the log panel in pixels, it should be adjusted for the longest label beginning with 'log' (e.g. 'logPanelDeferredImages')"
+    },
+    "profileAddButtonTooltip": {
+        "message": "Add a new profile",
+        "description": "Tooltip 'Add a new profile' in the options page"
+    },
+    "profileRenameButtonTooltip": {
+        "message": "Rename the profile",
+        "description": "Tooltip 'Rename the profile' in the options page"
+    },
+    "profileDeleteButtonTooltip": {
+        "message": "Delete the profile",
+        "description": "Tooltip 'Delete the profile' in the options page"
+    },
+    "profileAddPrompt": {
+        "message": "Enter a name for this new profile",
+        "description": "Popup text 'Enter a name for this new profile' in the options page"
+    },
+    "profileDeleteConfirm": {
+        "message": "Confirm deletion of the selected profile",
+        "description": "Popup text 'Confirm deletion of the selected profile' in the options page"
+    },
+    "profileRenamePrompt": {
+        "message": "Enter a new name for the selected profile",
+        "description": "Popup text 'Enter a new name for the selected profile' in the options page"
     }
 }

+ 32 - 0
_locales/fr/messages.json

@@ -7,6 +7,10 @@
         "message": "Sauver la page avec SingleFile",
         "description": "Menu entry: 'Save page with SingleFile'"
     },
+    "menuSelectProfile": {
+        "message": "Sélectionner le profil",
+        "description": "Menu entry: 'Select profile'"
+    },
     "menuSaveSelection": {
         "message": "Sauver la sélection",
         "description": "Menu entry: 'Save selection'"
@@ -267,6 +271,10 @@
         "message": "Remet toutes les options à leur valeur par défaut",
         "description": "Options 'Reset' button tooltip"
     },
+    "optionsResetConfirm": {
+        "message": "Confirmez la remise à zéro de toutes les options",
+        "description": "Popup text 'Confirm the reset of all options' in the options page"
+    },
     "logPanelDeferredImages": {
         "message": "Images différées",
         "description": "Label 'Deferred images' in the log panel"
@@ -282,5 +290,29 @@
     "logPanelWidth": {
         "message": "130",
         "description": "Width of the log panel in pixels, it should be adjusted for the longest label beginning with 'log' (e.g. 'logPanelDeferredImages')"
+    },
+    "profileAddButtonTooltip": {
+        "message": "Ajouter un nouveau profil",
+        "description": "Tooltip 'Add a new profile' in the options page"
+    },
+    "profileRenameButtonTooltip": {
+        "message": "Renommer le profil",
+        "description": "Tooltip 'Rename the profile' in the options page"
+    },
+    "profileDeleteButtonTooltip": {
+        "message": "Supprimer le profil",
+        "description": "Tooltip 'Delete the profile' in the options page"
+    },
+    "profileAddPrompt": {
+        "message": "Saisissez un nom pour ce nouveau profil",
+        "description": "Popup text 'Enter a name for this new profile' in the options page"
+    },
+    "profileDeleteConfirm": {
+        "message": "Confirmez la suppression du profil selectionné",
+        "description": "Popup text 'Confirm deletion of the selected profile' in the options page"
+    },
+    "profileRenamePrompt": {
+        "message": "Saisissez un nouveau nom pour le profil sélectionné",
+        "description": "Popup text 'Enter a new name for the selected profile' in the options page"
     }
 }

+ 32 - 0
_locales/ja/messages.json

@@ -7,6 +7,10 @@
         "message": "SingleFile でページを保存",
         "description": "メニュー項目: 'SingleFile でページを保存'"
     },
+    "menuSelectProfile": {
+        "message": "Select profile",
+        "description": "Menu entry: 'Select profile'"
+    },
     "menuSaveSelection": {
         "message": "選択を保存",
         "description": "メニュー項目: '選択を保存'"
@@ -267,6 +271,10 @@
         "message": "すべてのオプションを規定値にリセットする",
         "description": "オプション 'リセット'ボタンのツールチップ"
     },
+    "optionsResetConfirm": {
+        "message": "Confirm the reset of all options",
+        "description": "Popup text 'Confirm the reset of all options' in the options page"
+    },
     "logPanelDeferredImages": {
         "message": "遅延画像",
         "description": "Label 'Deferred images' in the log panel"
@@ -282,5 +290,29 @@
     "logPanelWidth": {
         "message": "110",
         "description": "Width of the log panel in pixels, it should be adjusted for the longest label beginning with 'log' (e.g. 'logPanelDeferredImages')"
+    },
+    "profileAddButtonTooltip": {
+        "message": "Add a new profile",
+        "description": "Tooltip 'Add a new profile' in the options page"
+    },
+    "profileRenameButtonTooltip": {
+        "message": "Rename the profile",
+        "description": "Tooltip 'Rename the profile' in the options page"
+    },
+    "profileDeleteButtonTooltip": {
+        "message": "Delete the profile",
+        "description": "Tooltip 'Delete the profile' in the options page"
+    },
+    "profileAddPrompt": {
+        "message": "Enter a name for this new profile",
+        "description": "Popup text 'Enter a name for this new profile' in the options page"
+    },
+    "profileDeleteConfirm": {
+        "message": "Confirm deletion of the selected profile",
+        "description": "Popup text 'Confirm deletion of the selected profile' in the options page"
+    },
+    "profileRenamePrompt": {
+        "message": "Enter a new name for the selected profile",
+        "description": "Popup text 'Enter a new name for the selected profile' in the options page"
     }
 }

+ 32 - 0
_locales/pl/messages.json

@@ -7,6 +7,10 @@
         "message": "Zapsz stronę z SingleFile",
         "description": "Menu entry: 'Save page with SingleFile'"
     },
+    "menuSelectProfile": {
+        "message": "Select profile",
+        "description": "Menu entry: 'Select profile'"
+    },
     "menuSaveSelection": {
         "message": "Zapisz wybór",
         "description": "Menu entry: 'Save selection'"
@@ -267,6 +271,10 @@
         "message": "Zresetuj wszystkie opcje do ich wartości domyślnych",
         "description": "Options 'Reset' button tooltip"
     },
+    "optionsResetConfirm": {
+        "message": "Confirm the reset of all options",
+        "description": "Popup text 'Confirm the reset of all options' in the options page"
+    },
     "logPanelDeferredImages": {
         "message": "Odroczone obrazy",
         "description": "Label 'Deferred images' in the log panel"
@@ -282,5 +290,29 @@
     "logPanelWidth": {
         "message": "130",
         "description": "Width of the log panel in pixels, it should be adjusted for the longest label beginning with 'log' (e.g. 'logPanelDeferredImages')"
+    },
+    "profileAddButtonTooltip": {
+        "message": "Add a new profile",
+        "description": "Tooltip 'Add a new profile' in the options page"
+    },
+    "profileRenameButtonTooltip": {
+        "message": "Rename the profile",
+        "description": "Tooltip 'Rename the profile' in the options page"
+    },
+    "profileDeleteButtonTooltip": {
+        "message": "Delete the profile",
+        "description": "Tooltip 'Delete the profile' in the options page"
+    },
+    "profileAddPrompt": {
+        "message": "Enter a name for this new profile",
+        "description": "Popup text 'Enter a name for this new profile' in the options page"
+    },
+    "profileDeleteConfirm": {
+        "message": "Confirm deletion of the selected profile",
+        "description": "Popup text 'Confirm deletion of the selected profile' in the options page"
+    },
+    "profileRenamePrompt": {
+        "message": "Enter a new name for the selected profile",
+        "description": "Popup text 'Enter a new name for the selected profile' in the options page"
     }
 }

+ 33 - 1
_locales/ru/messages.json

@@ -7,6 +7,10 @@
         "message": "Сохранить страницу с помощью SingleFile",
         "description": "Menu entry: 'Save page with SingleFile'"
     },
+    "menuSelectProfile": {
+        "message": "Select profile",
+        "description": "Menu entry: 'Select profile'"
+    },
     "menuSaveSelection": {
         "message": "Сохранить выделенное",
         "description": "Menu entry: 'Save selection'"
@@ -267,6 +271,10 @@
         "message": "Сбросить все параметры к значениям по умолчанию",
         "description": "Options 'Reset' button tooltip"
     },
+    "optionsResetConfirm": {
+        "message": "Confirm the reset of all options",
+        "description": "Popup text 'Confirm the reset of all options' in the options page"
+    },
     "logPanelDeferredImages": {
         "message": "Отложенные изображения",
         "description": "Label 'Deferred images' in the log panel"
@@ -282,5 +290,29 @@
     "logPanelWidth": {
         "message": "120",
         "description": "Width of the log panel in pixels, it should be adjusted for the longest label beginning with 'log' (e.g. 'logPanelDeferredImages')"
+    },
+    "profileAddButtonTooltip": {
+        "message": "Add a new profile",
+        "description": "Tooltip 'Add a new profile' in the options page"
+    },
+    "profileRenameButtonTooltip": {
+        "message": "Rename the profile",
+        "description": "Tooltip 'Rename the profile' in the options page"
+    },
+    "profileDeleteButtonTooltip": {
+        "message": "Delete the profile",
+        "description": "Tooltip 'Delete the profile' in the options page"
+    },
+    "profileAddPrompt": {
+        "message": "Enter a name for this new profile",
+        "description": "Popup text 'Enter a name for this new profile' in the options page"
+    },
+    "profileDeleteConfirm": {
+        "message": "Confirm deletion of the selected profile",
+        "description": "Popup text 'Confirm deletion of the selected profile' in the options page"
+    },
+    "profileRenamePrompt": {
+        "message": "Enter a new name for the selected profile",
+        "description": "Popup text 'Enter a new name for the selected profile' in the options page"
     }
-}
+}

+ 32 - 0
_locales/zh_CN/messages.json

@@ -7,6 +7,10 @@
         "message": "使用 SingleFile 保存页面",
         "description": "菜单项: '使用 SingleFile 保存页面'"
     },
+    "menuSelectProfile": {
+        "message": "Select profile",
+        "description": "Menu entry: 'Select profile'"
+    },
     "menuSaveSelection": {
         "message": "保存选中部分",
         "description": "菜单项: '保存选中部分'"
@@ -267,6 +271,10 @@
         "message": "重置所有选项为默认值",
         "description": "选项按钮'重置'的提示"
     },
+    "optionsResetConfirm": {
+        "message": "Confirm the reset of all options",
+        "description": "Popup text 'Confirm the reset of all options' in the options page"
+    },
     "logPanelDeferredImages": {
         "message": "延迟加载的图像",
         "description": "Label 'Deferred images' in the log panel"
@@ -283,6 +291,30 @@
         "message": "115",
         "description": "Width of the log panel in pixels, it should be adjusted for the longest label beginning with 'log' (e.g. 'logPanelDeferredImages')"
     },
+    "profileAddButtonTooltip": {
+        "message": "Add a new profile",
+        "description": "Tooltip 'Add a new profile' in the options page"
+    },
+    "profileRenameButtonTooltip": {
+        "message": "Rename the profile",
+        "description": "Tooltip 'Rename the profile' in the options page"
+    },
+    "profileDeleteButtonTooltip": {
+        "message": "Delete the profile",
+        "description": "Tooltip 'Delete the profile' in the options page"
+    },
+    "profileAddPrompt": {
+        "message": "Enter a name for this new profile",
+        "description": "Popup text 'Enter a name for this new profile' in the options page"
+    },
+    "profileDeleteConfirm": {
+        "message": "Confirm deletion of the selected profile",
+        "description": "Popup text 'Confirm deletion of the selected profile' in the options page"
+    },
+    "profileRenamePrompt": {
+        "message": "Enter a new name for the selected profile",
+        "description": "Popup text 'Enter a new name for the selected profile' in the options page"
+    },
     "__WET_LOCALE__": {
         "message": "zh-cn"
     }

+ 9 - 5
extension/core/bg/autosave.js

@@ -23,7 +23,8 @@
 singlefile.autosave = (() => {
 
 	browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
-		const [options, tabsData] = await Promise.all([singlefile.config.getDefaultConfig(), singlefile.tabsData.get()]);
+		const [config, tabsData] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get()]);
+		const options = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
 		if ((options.autoSaveLoad || options.autoSaveLoadOrUnload) && (tabsData.autoSaveAll || (tabsData.autoSaveUnpinned && !tab.pinned) || (tabsData[tab.id] && tabsData[tab.id].autoSave))) {
 			if (changeInfo.status == "complete") {
 				singlefile.ui.saveTab(tab, { autoSave: true });
@@ -63,7 +64,8 @@ singlefile.autosave = (() => {
 	};
 
 	async function saveContent(message, tabId, incognito) {
-		const options = await singlefile.config.getDefaultConfig();
+		const [config, tabsData] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get()]);
+		const options = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
 		options.content = message.content;
 		options.url = message.url;
 		options.framesData = message.framesData;
@@ -113,12 +115,13 @@ singlefile.autosave = (() => {
 			}
 			tabsData[tabId].autoSave = enabled;
 			await singlefile.tabsData.set(tabsData);
-			singlefile.ui.refresh(tabId, { autoSave: enabled });
+			singlefile.ui.refresh(tabId);
 		}
 	}
 
 	async function isAutoSaveEnabled(tabId) {
-		const [options, autoSaveEnabled] = await Promise.all([singlefile.config.getDefaultConfig(), enabled(tabId)]);
+		const [config, tabsData, autoSaveEnabled] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get(), enabled(tabId)]);
+		const options = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
 		return { autoSaveEnabled, options };
 	}
 
@@ -131,7 +134,8 @@ singlefile.autosave = (() => {
 		const tabs = await browser.tabs.query({});
 		return Promise.all(tabs.map(async tab => {
 			try {
-				const [autoSaveEnabled, options] = await Promise.all([enabled(tab.id), singlefile.config.getDefaultConfig()]);
+				const [config, tabsData, autoSaveEnabled] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get(), enabled(tab.id)]);
+				const options = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
 				await browser.tabs.sendMessage(tab.id, { autoSaveUnloadEnabled: true, autoSaveEnabled, options });
 			} catch (error) {
 				/* ignored */

+ 54 - 11
extension/core/bg/config.js

@@ -22,6 +22,8 @@
 
 singlefile.config = (() => {
 
+	const DEFAULT_PROFILE_NAME = "Default settings";
+
 	const DEFAULT_CONFIG = {
 		removeHiddenElements: true,
 		removeUnusedStyles: true,
@@ -71,7 +73,7 @@ singlefile.config = (() => {
 		if (!config.profiles) {
 			delete defaultConfig.tabsData;
 			applyUpgrade(defaultConfig);
-			const config = { profiles: {}, defaultProfile: "default" };
+			const config = { profiles: {}, defaultProfile: DEFAULT_PROFILE_NAME };
 			config.profiles[config.defaultProfile] = defaultConfig;
 			browser.storage.local.remove(Object.keys(DEFAULT_CONFIG));
 			return browser.storage.local.set(config);
@@ -181,26 +183,67 @@ singlefile.config = (() => {
 	}
 
 	return {
-		async set(profiles) {
-			await pendingUpgradePromise;
-			await browser.storage.local.set({ profiles });
+		DEFAULT_PROFILE_NAME,
+		async create(profileName) {
+			const config = await getConfig();
+			if (Object.keys(config.profiles).includes(profileName)) {
+				throw new Error("Duplicate profile name");
+			}
+			config.profiles[profileName] = DEFAULT_CONFIG;
+			await browser.storage.local.set({ profiles: config.profiles, defaultProfile: DEFAULT_PROFILE_NAME });
 		},
 		async get() {
 			return getConfig();
 		},
-		async getDefaultConfig() {
+		async update(profileName, data) {
 			const config = await getConfig();
-			return config.profiles[config.defaultProfile];
+			if (!Object.keys(config.profiles).includes(profileName)) {
+				throw new Error("Profile not found");
+			}
+			config.profiles[profileName] = data;
+			await browser.storage.local.set({ profiles: config.profiles });
 		},
-		async setDefaultConfig(defaultConfig) {
-			const config = await getConfig();
-			config[config.defaultProfile] = defaultConfig;
-			await this.set(config);
+		async rename(oldProfileName, profileName) {
+			const [config, tabsData] = await Promise.all([getConfig(), singlefile.tabsData.get()]);
+			if (!Object.keys(config.profiles).includes(oldProfileName)) {
+				throw new Error("Profile not found");
+			}
+			if (Object.keys(config.profiles).includes(profileName)) {
+				throw new Error("Duplicate profile name");
+			}
+			if (oldProfileName == DEFAULT_PROFILE_NAME) {
+				throw new Error("Default settings cannot be renamed");
+			}
+			if (tabsData.profileName == oldProfileName) {
+				tabsData.profileName = profileName;
+				await singlefile.tabsData.set(tabsData);
+			}
+			config.profiles[profileName] = config.profiles[oldProfileName];
+			delete config.profiles[oldProfileName];
+			await browser.storage.local.set({ profiles: config.profiles });
+		},
+		async delete(profileName) {
+			const [config, tabsData] = await Promise.all([getConfig(), singlefile.tabsData.get()]);
+			if (!Object.keys(config.profiles).includes(profileName)) {
+				throw new Error("Profile not found");
+			}
+			if (profileName == DEFAULT_PROFILE_NAME) {
+				throw new Error("Default settings cannot be deleted");
+			}
+			if (tabsData.profileName == profileName) {
+				delete tabsData.profileName;
+				await singlefile.tabsData.set(tabsData);
+			}
+			delete config.profiles[profileName];
+			await browser.storage.local.set({ profiles: config.profiles });
 		},
 		async reset() {
 			await pendingUpgradePromise;
+			const tabsData = await singlefile.tabsData.get();
+			delete tabsData.profileName;
+			await singlefile.tabsData.set(tabsData);
 			await browser.storage.local.remove(["profiles", "defaultProfile"]);
-			await browser.storage.local.set({ profiles: { default: DEFAULT_CONFIG }, defaultProfile: "default" });
+			await browser.storage.local.set({ profiles: { [DEFAULT_PROFILE_NAME]: DEFAULT_CONFIG }, defaultProfile: DEFAULT_PROFILE_NAME });
 		}
 	};
 

+ 6 - 4
extension/core/bg/core.js

@@ -27,13 +27,15 @@ singlefile.core = (() => {
 	return { saveTab, autoSaveTab, isAllowedURL };
 
 	async function saveTab(tab, options) {
-		const config = await singlefile.config.getDefaultConfig();
-		Object.keys(options).forEach(key => config[key] = options[key]);
-		return singlefile.runner.saveTab(tab, config);
+		const [config, tabsData] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get()]);
+		const mergedOptions = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
+		Object.keys(options).forEach(key => mergedOptions[key] = options[key]);
+		return singlefile.runner.saveTab(tab, mergedOptions);
 	}
 
 	async function autoSaveTab(tab) {
-		let options = await singlefile.config.getDefaultConfig();
+		const [config, tabsData] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get()]);
+		const options = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
 		if (singlefile.autosave.enabled(tab.id)) {
 			await browser.tabs.sendMessage(tab.id, { autoSavePage: true, options });
 		}

+ 2 - 2
extension/ui/bg/bg-ui.js

@@ -42,8 +42,8 @@ singlefile.ui = (() => {
 		isAllowedURL(url) {
 			return singlefile.core.isAllowedURL(url);
 		},
-		refresh(tabId, options) {
-			return Promise.all([singlefile.ui.menu.refresh(), singlefile.ui.button.refresh(tabId, options)]);
+		refresh(tabId) {
+			return Promise.all([singlefile.ui.menu.refresh(), singlefile.ui.button.refresh(tabId)]);
 		},
 		onProgress(tabId, index, maxIndex, options) {
 			singlefile.ui.button.onProgress(tabId, index, maxIndex, options);

+ 3 - 2
extension/ui/bg/ui-button.js

@@ -71,9 +71,10 @@ singlefile.ui.button = (() => {
 		onProgress,
 		onEnd,
 		onError,
-		refresh: async (tabId, options) => {
+		refresh: async tabId => {
 			if (tabId) {
-				await refresh(tabId, getProperties(options));
+				const tabsData = await singlefile.tabsData.get();
+				await refresh(tabId, getProperties({ autoSave: tabsData.autoSaveAll || tabsData.autoSaveUnpinned || (tabsData[tabId] && tabsData[tabId].autoSave) }));
 			}
 		}
 	};

+ 28 - 3
extension/ui/bg/ui-menu.js

@@ -24,6 +24,8 @@ singlefile.ui.menu = (() => {
 
 	const BROWSER_MENUS_API_SUPPORTED = browser.menus && browser.menus.onClicked && browser.menus.create && browser.menus.update && browser.menus.removeAll;
 	const MENU_ID_SAVE_PAGE = "save-page";
+	const MENU_ID_SELECT_PROFILE = "select-profile";
+	const MENU_ID_SELECT_PROFILE_PREFIX = "select-profile-";
 	const MENU_ID_SAVE_SELECTED = "save-selected";
 	const MENU_ID_SAVE_FRAME = "save-frame";
 	const MENU_ID_SAVE_SELECTED_TABS = "save-selected-tabs";
@@ -46,7 +48,8 @@ singlefile.ui.menu = (() => {
 	};
 
 	async function refresh() {
-		const options = await singlefile.config.getDefaultConfig();
+		const [config, tabsData] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get()]);
+		const options = config.profiles[tabsData.profileName || singlefile.config.DEFAULT_PROFILE_NAME];
 		if (BROWSER_MENUS_API_SUPPORTED) {
 			const pageContextsEnabled = ["page", "frame", "image", "link", "video", "audio"];
 			const defaultContextsDisabled = ["browser_action"];
@@ -101,6 +104,21 @@ singlefile.ui.menu = (() => {
 					type: "separator"
 				});
 			}
+			browser.menus.create({
+				id: MENU_ID_SELECT_PROFILE,
+				title: browser.i18n.getMessage("menuSelectProfile"),
+				contexts: defaultContexts,
+			});
+			Object.keys(config.profiles).forEach((profileName, profileIndex) => {
+				browser.menus.create({
+					id: MENU_ID_SELECT_PROFILE_PREFIX + profileIndex,
+					type: "radio",
+					contexts: defaultContexts,
+					title: profileName,
+					checked: tabsData.profileName ? tabsData.profileName == profileName : profileName == singlefile.config.DEFAULT_PROFILE_NAME,
+					parentId: MENU_ID_SELECT_PROFILE
+				});
+			});
 			browser.menus.create({
 				id: MENU_ID_AUTO_SAVE,
 				contexts: defaultContexts,
@@ -194,15 +212,22 @@ singlefile.ui.menu = (() => {
 					await singlefile.tabsData.set(tabsData);
 					refreshExternalComponents(tab.id, { autoSave: true });
 				}
+				if (event.menuItemId.startsWith(MENU_ID_SELECT_PROFILE_PREFIX)) {
+					const [config, tabsData] = await Promise.all([singlefile.config.get(), singlefile.tabsData.get()]);
+					const profileIndex = Number(event.menuItemId.split(MENU_ID_SELECT_PROFILE_PREFIX)[1]);
+					tabsData.profileName = Object.keys(config.profiles)[profileIndex];
+					await singlefile.tabsData.set(tabsData);
+					refreshExternalComponents(tab.id, { autoSave: tabsData.autoSaveAll || tabsData.autoSaveUnpinned || (tabsData[tab.id] && tabsData[tab.id].autoSave) });
+				}
 			});
 			const tabs = await browser.tabs.query({});
 			tabs.forEach(tab => refreshTab(tab));
 		}
 	}
 
-	async function refreshExternalComponents(tabId, tabData) {
+	async function refreshExternalComponents(tabId) {
 		await singlefile.autosave.refresh();
-		singlefile.ui.button.refresh(tabId, tabData);
+		singlefile.ui.button.refresh(tabId);
 	}
 
 	async function refreshTab(tab) {

+ 108 - 47
extension/ui/bg/ui-options.js

@@ -18,11 +18,16 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global browser, document */
+/* global browser, window, document */
 
 (async () => {
 
-	const bgPage = await browser.runtime.getBackgroundPage();
+	const CHROME_BROWSER_NAME = "Chrome";
+
+	const [bgPage, browserInfo] = await Promise.all([browser.runtime.getBackgroundPage(), browser.runtime.getBrowserInfo()]);
+	const singlefile = bgPage.singlefile;
+	const prompt = browserInfo.name == CHROME_BROWSER_NAME ? (message, defaultMessage) => bgPage.prompt(message, defaultMessage) : (message, defaultMessage) => window.prompt(message, defaultMessage);
+	const confirm = browserInfo.name == CHROME_BROWSER_NAME ? message => bgPage.confirm(message) : message => window.confirm(message);
 	const removeHiddenElementsLabel = document.getElementById("removeHiddenElementsLabel");
 	const removeUnusedStylesLabel = document.getElementById("removeUnusedStylesLabel");
 	const removeFramesLabel = document.getElementById("removeFramesLabel");
@@ -68,7 +73,11 @@
 	const infobarTemplateLabel = document.getElementById("infobarTemplateLabel");
 	const miscLabel = document.getElementById("miscLabel");
 	const helpLabel = document.getElementById("helpLabel");
+	const addProfileButton = document.getElementById("addProfileButton");
+	const deleteProfileButton = document.getElementById("deleteProfileButton");
+	const renameProfileButton = document.getElementById("renameProfileButton");
 	const resetButton = document.getElementById("resetButton");
+	const profileNamesInput = document.getElementById("profileNamesInput");
 	const removeHiddenElementsInput = document.getElementById("removeHiddenElementsInput");
 	const removeUnusedStylesInput = document.getElementById("removeUnusedStylesInput");
 	const removeFramesInput = document.getElementById("removeFramesInput");
@@ -103,10 +112,45 @@
 	const confirmInfobarInput = document.getElementById("confirmInfobarInput");
 	const expandAllButton = document.getElementById("expandAllButton");
 	let pendingSave = Promise.resolve();
+	addProfileButton.addEventListener("click", async () => {
+		const profileName = prompt(browser.i18n.getMessage("profileAddPrompt"));
+		if (profileName) {
+			try {
+				await singlefile.config.create(profileName);
+				await await Promise.all([refresh(profileName), singlefile.ui.menu.refresh()]);
+			} catch (error) {
+				// ignored
+			}
+		}
+	}, false);
+	deleteProfileButton.addEventListener("click", async () => {
+		if (confirm(browser.i18n.getMessage("profileDeleteConfirm"))) {
+			try {
+				await singlefile.config.delete(profileNamesInput.value);
+				profileNamesInput.value = null;
+				await Promise.all([refresh(), singlefile.ui.menu.refresh()]);
+			} catch (error) {
+				// ignored
+			}
+		}
+	}, false);
+	renameProfileButton.addEventListener("click", async () => {
+		const profileName = prompt(browser.i18n.getMessage("profileRenamePrompt"), profileNamesInput.value);
+		if (profileName) {
+			try {
+				await singlefile.config.rename(profileNamesInput.value, profileName);
+				await Promise.all([refresh(profileName), singlefile.ui.menu.refresh()]);
+			} catch (error) {
+				// ignored
+			}
+		}
+	}, false);
 	resetButton.addEventListener("click", async () => {
-		await bgPage.singlefile.config.reset();
-		await refresh();
-		await update();
+		if (confirm(browser.i18n.getMessage("optionsResetConfirm"))) {
+			await singlefile.config.reset();
+			await Promise.all([refresh(), singlefile.ui.menu.refresh()]);
+			await update();
+		}
 	}, false);
 	autoSaveUnloadInput.addEventListener("click", async () => {
 		if (!autoSaveLoadInput.checked && !autoSaveUnloadInput.checked) {
@@ -134,10 +178,15 @@
 		}
 		document.querySelectorAll("details").forEach(detailElement => detailElement.open = Boolean(expandAllButton.className));
 	}, false);
-	document.body.onchange = async () => {
-		await update();
+	document.body.onchange = async event => {
+		if (event.target != profileNamesInput) {
+			await update();
+		}
 		await refresh();
 	};
+	addProfileButton.title = browser.i18n.getMessage("profileAddButtonTooltip");
+	deleteProfileButton.title = browser.i18n.getMessage("profileDeleteButtonTooltip");
+	renameProfileButton.title = browser.i18n.getMessage("profileRenameButtonTooltip");
 	removeHiddenElementsLabel.textContent = browser.i18n.getMessage("optionRemoveHiddenElements");
 	removeUnusedStylesLabel.textContent = browser.i18n.getMessage("optionRemoveUnusedStyles");
 	removeFramesLabel.textContent = browser.i18n.getMessage("optionRemoveFrames");
@@ -188,50 +237,62 @@
 
 	refresh();
 
-	async function refresh() {
-		const options = await bgPage.singlefile.config.getDefaultConfig();
-		removeHiddenElementsInput.checked = options.removeHiddenElements;
-		removeUnusedStylesInput.checked = options.removeUnusedStyles;
-		removeFramesInput.checked = options.removeFrames;
-		removeImportsInput.checked = options.removeImports;
-		removeScriptsInput.checked = options.removeScripts;
-		saveRawPageInput.checked = options.saveRawPage;
-		compressHTMLInput.checked = options.compressHTML;
-		compressCSSInput.checked = options.compressCSS;
-		lazyLoadImagesInput.checked = options.lazyLoadImages;
-		maxLazyLoadImagesIdleTimeInput.value = options.maxLazyLoadImagesIdleTime;
-		maxLazyLoadImagesIdleTimeInput.disabled = !options.lazyLoadImages;
-		contextMenuEnabledInput.checked = options.contextMenuEnabled;
-		filenameTemplateInput.value = options.filenameTemplate;
-		shadowEnabledInput.checked = options.shadowEnabled;
-		maxResourceSizeEnabledInput.checked = options.maxResourceSizeEnabled;
-		maxResourceSizeInput.value = options.maxResourceSize;
-		maxResourceSizeInput.disabled = !options.maxResourceSizeEnabled;
-		confirmFilenameInput.checked = options.confirmFilename;
-		conflictActionInput.value = options.conflictAction;
-		removeAudioSrcInput.checked = options.removeAudioSrc;
-		removeVideoSrcInput.checked = options.removeVideoSrc;
-		displayInfobarInput.checked = options.displayInfobar;
-		displayStatsInput.checked = options.displayStats;
-		backgroundSaveInput.checked = options.backgroundSave;
-		autoSaveDelayInput.value = options.autoSaveDelay;
-		autoSaveDelayInput.disabled = !options.autoSaveLoadOrUnload && !options.autoSaveLoad;
-		autoSaveLoadInput.checked = !options.autoSaveLoadOrUnload && options.autoSaveLoad;
-		autoSaveLoadOrUnloadInput.checked = options.autoSaveLoadOrUnload;
-		autoSaveUnloadInput.checked = !options.autoSaveLoadOrUnload && options.autoSaveUnload;
-		autoSaveLoadInput.disabled = options.autoSaveLoadOrUnload;
-		autoSaveUnloadInput.disabled = options.autoSaveLoadOrUnload;
-		removeAlternativeFontsInput.checked = options.removeAlternativeFonts;
-		removeAlternativeImagesInput.checked = options.removeAlternativeImages;
-		groupDuplicateImagesInput.checked = options.groupDuplicateImages;
-		removeAlternativeMediasInput.checked = options.removeAlternativeMedias;
-		infobarTemplateInput.value = options.infobarTemplate;
-		confirmInfobarInput.checked = options.confirmInfobar;
+	async function refresh(profileName) {
+		const options = await bgPage.singlefile.config.get();
+		const selectedProfileName = profileName || profileNamesInput.value || options.defaultProfile;
+		profileNamesInput.childNodes.forEach(node => node.remove());
+		const profileNames = Object.keys(options.profiles);
+		profileNamesInput.options.length = 0;
+		profileNames.forEach(profileName => {
+			const optionElement = document.createElement("option");
+			optionElement.value = optionElement.textContent = profileName;
+			profileNamesInput.appendChild(optionElement);
+		});
+		profileNamesInput.value = selectedProfileName;
+		renameProfileButton.disabled = deleteProfileButton.disabled = profileNamesInput.value == singlefile.config.DEFAULT_PROFILE_NAME;
+		const profileOptions = options.profiles[selectedProfileName];
+		removeHiddenElementsInput.checked = profileOptions.removeHiddenElements;
+		removeUnusedStylesInput.checked = profileOptions.removeUnusedStyles;
+		removeFramesInput.checked = profileOptions.removeFrames;
+		removeImportsInput.checked = profileOptions.removeImports;
+		removeScriptsInput.checked = profileOptions.removeScripts;
+		saveRawPageInput.checked = profileOptions.saveRawPage;
+		compressHTMLInput.checked = profileOptions.compressHTML;
+		compressCSSInput.checked = profileOptions.compressCSS;
+		lazyLoadImagesInput.checked = profileOptions.lazyLoadImages;
+		maxLazyLoadImagesIdleTimeInput.value = profileOptions.maxLazyLoadImagesIdleTime;
+		maxLazyLoadImagesIdleTimeInput.disabled = !profileOptions.lazyLoadImages;
+		contextMenuEnabledInput.checked = profileOptions.contextMenuEnabled;
+		filenameTemplateInput.value = profileOptions.filenameTemplate;
+		shadowEnabledInput.checked = profileOptions.shadowEnabled;
+		maxResourceSizeEnabledInput.checked = profileOptions.maxResourceSizeEnabled;
+		maxResourceSizeInput.value = profileOptions.maxResourceSize;
+		maxResourceSizeInput.disabled = !profileOptions.maxResourceSizeEnabled;
+		confirmFilenameInput.checked = profileOptions.confirmFilename;
+		conflictActionInput.value = profileOptions.conflictAction;
+		removeAudioSrcInput.checked = profileOptions.removeAudioSrc;
+		removeVideoSrcInput.checked = profileOptions.removeVideoSrc;
+		displayInfobarInput.checked = profileOptions.displayInfobar;
+		displayStatsInput.checked = profileOptions.displayStats;
+		backgroundSaveInput.checked = profileOptions.backgroundSave;
+		autoSaveDelayInput.value = profileOptions.autoSaveDelay;
+		autoSaveDelayInput.disabled = !profileOptions.autoSaveLoadOrUnload && !profileOptions.autoSaveLoad;
+		autoSaveLoadInput.checked = !profileOptions.autoSaveLoadOrUnload && profileOptions.autoSaveLoad;
+		autoSaveLoadOrUnloadInput.checked = profileOptions.autoSaveLoadOrUnload;
+		autoSaveUnloadInput.checked = !profileOptions.autoSaveLoadOrUnload && profileOptions.autoSaveUnload;
+		autoSaveLoadInput.disabled = profileOptions.autoSaveLoadOrUnload;
+		autoSaveUnloadInput.disabled = profileOptions.autoSaveLoadOrUnload;
+		removeAlternativeFontsInput.checked = profileOptions.removeAlternativeFonts;
+		removeAlternativeImagesInput.checked = profileOptions.removeAlternativeImages;
+		groupDuplicateImagesInput.checked = profileOptions.groupDuplicateImages;
+		removeAlternativeMediasInput.checked = profileOptions.removeAlternativeMedias;
+		infobarTemplateInput.value = profileOptions.infobarTemplate;
+		confirmInfobarInput.checked = profileOptions.confirmInfobar;
 	}
 
 	async function update() {
 		await pendingSave;
-		pendingSave = bgPage.singlefile.config.setDefaultConfig({
+		pendingSave = bgPage.singlefile.config.update(profileNamesInput.value, {
 			removeHiddenElements: removeHiddenElementsInput.checked,
 			removeUnusedStyles: removeUnusedStylesInput.checked,
 			removeFrames: removeFramesInput.checked,

+ 47 - 5
extension/ui/pages/options.css

@@ -5,10 +5,6 @@ body {
 }
 
 button {
-    background-color: transparent;
-    border-color: rgb(191, 191, 191);
-    border-style: solid;
-    border-radius: 2px;
     padding-top: 5px;
     padding-bottom: 5px;
     padding-left: 15px;
@@ -16,6 +12,15 @@ button {
     border-width: 1px;
 }
 
+button,
+select {
+    background-color: transparent;
+    border-color: rgb(191, 191, 191);
+    border-style: solid;
+    border-radius: 2px;
+    border-width: 1px;
+}
+
 button:active {
     border-color: rgb(237, 237, 237);
 }
@@ -47,6 +52,41 @@ h3 {
     margin-top: 10px;
 }
 
+.profiles {
+    float: right;
+    margin-right: 12px;
+}
+
+.profiles button {
+    padding: 0;
+    margin: 0;
+    width: 22px;
+    text-align: center;
+}
+
+.profiles button,
+.profiles select {
+    height: 22px;
+    vertical-align: top;
+}
+
+.profiles button:disabled {
+    opacity: .25;
+}
+
+.profiles button>img {
+    width: 14px;
+    height: auto;
+    vertical-align: middle;
+    padding: 2px;
+}
+
+#profilesLabel {
+    font-weight: normal;
+    padding-top: 5px;
+    margin-right: 5px;
+}
+
 details {
     margin-left: 12px;
     margin-right: 12px;
@@ -116,7 +156,9 @@ a {
     transition: opacity 250ms;
     cursor: pointer;
     font-size: .9em;
-    margin-right: 1px;
+    position: relative;
+    top: -1px;
+    left: -3px;
 }
 
 #expandAllButton::after {

+ 9 - 1
extension/ui/pages/options.html

@@ -8,7 +8,15 @@
 </head>
 
 <body>
-	<h3><span id="expandAllButton"></span> <span id="titleLabel"></span></h3>
+	<h3>
+		<span id="expandAllButton"></span> <span id="titleLabel"></span>
+		<span class="profiles">
+			<select id="profileNamesInput"></select>
+			<button id="addProfileButton"><img src="../resources/button_new.png"></button>
+			<button id="deleteProfileButton"><img src="../resources/button_delete.png"></button>
+			<button id="renameProfileButton"><img src="../resources/button_edit.png"></button>
+		</span>
+	</h3>
 	<details>
 		<summary id="userInterfaceLabel"></summary>
 		<div class="option">

BIN
extension/ui/resources/button_delete.png


BIN
extension/ui/resources/button_edit.png


BIN
extension/ui/resources/button_new.png


+ 1 - 0
lib/browser-polyfill/chrome-browser-polyfill.js

@@ -152,6 +152,7 @@
 				})
 			},
 			runtime: {
+				getBrowserInfo: () => Promise.resolve({ name: isChrome ? "Chrome" : "Unknown" }),
 				onMessage: {
 					addListener: listener => nativeAPI.runtime.onMessage.addListener((message, sender, sendResponse) => {
 						const response = listener(message, sender);