├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .gitmodules
├── CONTRIBUTING.md
├── COPYING.txt
├── ISSUE_TEMPLATE.md
├── Makefile
├── README.md
├── _locales
├── en
│ └── messages.json
├── ja
│ └── messages.json
└── zh_CN
│ └── messages.json
├── background
├── background.html
├── background.js
└── context-menu.js
├── common
├── commands.js
├── common.js
├── constants.js
└── permissions.js
├── eslint.config.mjs
├── extlib
└── .gitkeep
├── history.en.md
├── history.ja.md
├── licenses
└── MPL2.0.txt
├── manifest.json
├── options
├── init.js
├── options.css
└── options.html
├── package-lock.json
├── package.json
├── resources
├── Save.svg
├── blank.html
├── downloads-option.png
└── notify-features.html
└── screenshots
└── menu.png
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: prepare manifest.json with a revision number
13 | run: |
14 | cp manifest.json manifest.json.bak
15 | version=$(cat manifest.json.bak | jq -r ".version" | sed -r -e "s/$/.$(git log --oneline | wc -l)/")
16 | cat manifest.json.bak | jq ".version |= \"$version\"" > manifest.json
17 | - name: build xpi
18 | run: make
19 | - uses: actions/upload-artifact@v4
20 | with:
21 | name: save-selected-tabs-to-files.xpi
22 | path: save-selected-tabs-to-files.xpi
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.xpi
2 | extlib/*.js
3 |
4 | # node.js dependency
5 | node_modules/
6 |
7 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "submodules/webextensions-lib-configs"]
2 | path = submodules/webextensions-lib-configs
3 | url = https://github.com/piroor/webextensions-lib-configs.git
4 | [submodule "submodules/webextensions-lib-l10n"]
5 | path = submodules/webextensions-lib-l10n
6 | url = https://github.com/piroor/webextensions-lib-l10n.git
7 | [submodule "submodules/webextensions-lib-options"]
8 | path = submodules/webextensions-lib-options
9 | url = https://github.com/piroor/webextensions-lib-options.git
10 | [submodule "submodules/webextensions-lib-rich-confirm"]
11 | path = submodules/webextensions-lib-rich-confirm
12 | url = https://github.com/piroor/webextensions-lib-rich-confirm.git
13 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guideline
2 |
3 | If you are planning to open a new issue for a bug report or a feature request, or having additional information for an existing issue, or hoping to translate the language resource for your language, then please see this document before posting.
4 |
5 | This is possibly a generic guideline for contributing any Firefox addon project or a public OSS/free software project.
6 |
7 | ## Good, helpful bug reports including feature requests
8 |
9 | A good report is the fastest way to solve a problem.
10 | Even if the problem is very clear for you, possibly unclear for me.
11 | Unclear report can be left unfixed for long time.
12 | You'll see an example of [good report](https://github.com/piroor/treestyletab/issues/1134) and [another report with too less information](https://github.com/piroor/treestyletab/issues/1135).
13 |
14 | Here is a list of typical questions I asked to existing reports:
15 |
16 | * **Does the problem appear with the [latest develpment build](http://piro.sakura.ne.jp/xul/xpi/nightly/)?**
17 | Possibly, problems you met has been resolved already.
18 | On Firefox 48 and later, you'll have to use an [unbranded Firefox](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) (including Beta, Aurora, and Nightly) with a secret preference `xpinstall.signatures.required`=`false` (you can set it via `about:config`), to try unsigned development builds.
19 | * **Is the "problem" really introduced by this addon?**
20 | If you use this addon with others, please confirm which addon actually causes the problem you met.
21 | If the problem doesn't appear with no other addon, it can be introduced by others.
22 | See also the next.
23 | * **Does the problem appear with a clean profile?**
24 | You can start Firefox with temporary clean profile by a command line `-profile`, like: `"C:\Program Filex (x86)\Mozilla Firefox\firefox.exe" -no-remote -profile "%Temp%\FirefoxTemporaryProfile"`
25 | If the problem doesn't appear with a clean profile, please find complete reproduction steps out.
26 | * **Is the main topic single and clear?**
27 | Sometimes I got an issue including multiple topics, but such an issue is hard to be closed, then it often stays opened for long time and confuses me.
28 | If you have multiple topics, please report them as separate issues for each.
29 |
30 | For future requests, some more important information:
31 |
32 | * **Did you find other addon which provide the feature you are going to request?**
33 | If there is any other addon for the purpose, then this addon should become compatible to it instead of merging the feature into this addon itself.
34 | * **Please add `[feature request]` tag into the summary.**
35 | Sometimes a feature request can be misunderstood as a simple bug report.
36 |
37 | Then, please report the bug with these information:
38 |
39 | * **Detailed steps to reproduce the problem.** For example:
40 | 1. Prepare Firefox version XX with plain profile.
41 | 2. Install Tree Style Tab version XXXX.
42 | 3. Install another addon XXXX version XXXX from "http://....".
43 | 4. Click a button on the toolbar.
44 | 5. ...
45 | * **Expected result.**
46 | If you have any screenshot or screencast, it will help me more.
47 | * **Actual result.**
48 | If you have any screenshot or screencast, it will help me more.
49 | * **Platform information.**
50 | If the problem appear on your multiple platforms, please list them.
51 |
52 | If your issue is related to something complex conditions, figures or screenshots will help me a lot, instead of long descriptions.
53 |
54 | ## Please don't join to an existing discussion if your problem is different from the originally reported one
55 |
56 | Even if the result is quite similar, they may be different problem if the reproduction steps for yours are different from the one originally reported.
57 | Then, you should create a new issue for yours, instead of adding comment to the existing issue.
58 | Otherwise, I'll be confused if the original reporter said "the issue is a compatibility issue with another addon" but another reporter said "I saw this problem without any other addon".
59 |
60 | To avoid such a confusion, please post your report with detailed reproduction steps always.
61 |
62 | ## Feature requests can be tagged as "out of purpose"
63 |
64 | If there is any [readme page](./README.md), please see it before you post a new feature request.
65 | (Some my addon doesn't provide such an information, sorry.)
66 | Even if a requested feature is very useful, it is possibly rejected by the project policy.
67 |
68 | Instead, please tell me other addon which provide the feature and report a new issue as "compatibility issue with the addon, this addon should work together with it".
69 | I'm very positive to make my addons compatible to others.
70 |
71 | ## Translations, pull requests
72 |
73 | If you've fixed a problem you met by your hand, then please send a pull request to me.
74 |
75 | Translations also.
76 | Pull requests are easy to merge, than sending ZIP files.
77 | You'll do it without any local application - you can do it on the GitHub.
78 | For example, if you want to fix an existing typo in a locale, you just have to click the pencil button (with a tooltip "Edit this file") for a language resource file.
79 |
--------------------------------------------------------------------------------
/COPYING.txt:
--------------------------------------------------------------------------------
1 | Each file under this directory is licensed under MPL 2.0 by default, if the file includes no license information.
2 | All rights reserved.
3 |
4 | ----
5 |
6 | This project includes files licensed under different type licenses:
7 |
8 | * Mozilla Public License 2.0 (MPL 2.0)
9 | * MIT License
10 |
11 | Please note that these licenses are applied "per-file". In most cases each file includes a license header in itself. If a file has no license header due to some reasons (for example, image files), please see a file named "license.txt" in closest parent directory. Otherwise please treat the file is licensed under the default license declared at the top of this file.
12 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | ## Short description
7 |
8 | ## Steps to reproduce
9 |
10 | 1. Start Firefox with clean profile.
11 | 2. Install MTH.
12 | 3.
13 | 4.
14 |
15 |
25 |
26 | ## Expected result
27 |
28 |
29 | ## Actual result
30 |
31 |
32 | ## Environment
33 |
34 | * Platform (OS):
35 | * Version of Firefox:
36 | * Version (or revision) of Multiple Tab Handler:
37 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NPM_MOD_DIR := $(CURDIR)/node_modules
2 | NPM_BIN_DIR := $(NPM_MOD_DIR)/.bin
3 |
4 | .PHONY: xpi install_dependency install_hook lint format init_extlib update_extlib install_extlib
5 |
6 | all: xpi
7 |
8 | install_dependency:
9 | [ -e "$(NPM_BIN_DIR)/eslint" -a -e "$(NPM_BIN_DIR)/jsonlint-cli" ] || npm install
10 |
11 | install_hook:
12 | echo '#!/bin/sh\nmake lint' > "$(CURDIR)/.git/hooks/pre-commit" && chmod +x "$(CURDIR)/.git/hooks/pre-commit"
13 |
14 | lint: install_dependency
15 | "$(NPM_BIN_DIR)/eslint" . --report-unused-disable-directives
16 | find . -type d -name node_modules -prune -o -type f -name '*.json' -print | xargs "$(NPM_BIN_DIR)/jsonlint-cli"
17 |
18 | format: install_dependency
19 | "$(NPM_BIN_DIR)/eslint" . --ext=.js --report-unused-disable-directives --fix
20 |
21 | xpi: init_extlib install_extlib lint
22 | rm -f ./*.xpi
23 | zip -r -9 save-selected-tabs-to-files.xpi manifest.json common resources background panel options _locales extlib -x '*/.*' >/dev/null 2>/dev/null
24 |
25 | init_extlib:
26 | git submodule update --init
27 |
28 | update_extlib:
29 | git submodule foreach 'git checkout trunk || git checkout main || git checkout master && git pull'
30 |
31 | install_extlib:
32 | rm -f extlib/*.js
33 | cp submodules/webextensions-lib-configs/Configs.js extlib/; echo 'export default Configs;' >> extlib/Configs.js
34 | cp submodules/webextensions-lib-options/Options.js extlib/; echo 'export default Options;' >> extlib/Options.js
35 | cp submodules/webextensions-lib-l10n/l10n.js extlib/; echo 'export default l10n;' >> extlib/l10n.js
36 | cp submodules/webextensions-lib-rich-confirm/RichConfirm.js extlib/; echo 'export default RichConfirm;' >> extlib/RichConfirm.js
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Save Selected Tabs to Files
2 |
3 | 
4 |
5 | * [Signed package on AMO](https://addons.mozilla.org/firefox/addon/save-selected-tabs-to-files/)
6 | * [Development builds for each commit are available at "Artifacts" of the CI/CD action](https://github.com/piroor/save-selected-tabs-to-files/actions?query=workflow%3ACI%2FCD)
7 |
8 | ## Privacy Policy
9 |
10 | This software does not collect any privacy data automatically, but this includes ability to synchronize options across multiple devices automatically via Firefox Sync.
11 | Any data you input to options may be sent to Mozilla's Sync server, if you configure Firefox to activate Firefox Sync.
12 |
13 | このソフトウェアはいかなるプライバシー情報も自動的に収集しませんが、Firefox Syncを介して自動的に設定情報をデバイス間で同期する機能を含みます。
14 | Firefox Syncを有効化している場合、設定画面に入力されたデータは、Mozillaが運用するSyncサーバーに送信される場合があります。
15 |
--------------------------------------------------------------------------------
/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": { "message": "Save Selected Tabs to Files" },
3 | "extensionDescription": { "message": "Provides ability to save selected tabs to local files." },
4 |
5 | "saveTabsPrefix_defaultValue": { "message": "Saved Tabs/" },
6 |
7 | "context_saveTab_label": { "message": "Sa&ve Tab as File" },
8 | "context_saveTabs_label": { "message": "Sa&ve Tabs as Files" },
9 | "command_saveSelectedTabs": { "message": "Save Selected Tabs as Files" },
10 |
11 | "dialog_title": { "message": "Where to save?" },
12 | "dialog_inputDescription": { "message": "Input a folder name to save files within" },
13 | "dialog_save": { "message": "Create Folder and Save" },
14 | "dialog_cancel": { "message": "Cancel" },
15 |
16 |
17 | "startup_useDownloadDirOptionNote_title": { "message": "Important note before start using Save Selected Tabs to Files" },
18 | "startup_useDownloadDirOptionNote_message": { "message": "You should configure Firefox itself before using this addon. Click here to see more details." },
19 |
20 | "useDownloadDirOption_caption": { "message": "Important note before start using" },
21 | "useDownloadDirOption_note": { "message": "It is strongly recommended to configure Firefox itself to save downloaded files to a single folder: Firefox settings => \"General\" => \"Files and Applications\" => \"Downloads\" => choose \"Save files to\". If \"Always ask you where to save files\" is chosen, you'll see a \"save file as...\" common dialog for every files downloaded from tabs, and it will be too annoying." },
22 |
23 |
24 | "config_saveTabs_caption": { "message": "Save Tabs as Files" },
25 | "config_saveTabsPrefix_label_before": { "message": "Save tabs under " },
26 | "config_saveTabsPrefix_label_after": { "message": "\u200b" },
27 | "config_saveTabsPrefix_description": { "message": "*Path under the download folder. \"/\" will be recognized as a path delimiter. A `%input%` placeholder in the path produces a diaglog to input folder name when you try to save." },
28 | "config_maxFileNameLength_label_before": { "message": "Shorten file name longer than " },
29 | "config_maxFileNameLength_label_after": { "message": " characters" },
30 | "config_maxDownloads_label_before": { "message": "Run " },
31 | "config_maxDownloads_label_after": { "message": " downloads parallely at a time" },
32 |
33 | "config_allUrlsPermissionGranted_label": { "message": "Guess filename extension based on Content-Type of tabs" },
34 |
35 | "config_showContextCommandOnTab_label": { "message": "Show context menu item in the context menu on tabs" },
36 | "config_showContextCommandOnPage_label": { "message": "Show context menu item in the context menu on the content area" },
37 | "config_showContextCommandForSingleTab_label": { "message": "Show context menu item even if there is no multiselection" },
38 |
39 | "config_clearSelectionAfterCommandInvoked_label": { "message": "Clear selection after a command is invoked" },
40 |
41 | "config_shortcuts_caption": { "message": "Keyboard shortcuts" },
42 |
43 |
44 | "config_debug_caption": { "message": "Development" },
45 | "config_debug_label": { "message": "Debug mode" },
46 | "config_all_caption": { "message": "All Configs" }
47 | }
48 |
--------------------------------------------------------------------------------
/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": { "message": "選択したタブを保存" },
3 | "extensionDescription": { "message": "選択されたタブをローカルファイルとして保存する機能を提供します。" },
4 |
5 | "saveTabsPrefix_defaultValue": { "message": "保存されたタブ/" },
6 |
7 | "context_saveTab_label": { "message": "ファイルに保存(&V)" },
8 | "context_saveTabs_label": { "message": "ファイルに保存(&V)" },
9 | "command_saveSelectedTabs": { "message": "選択中のタブをファイルに保存" },
10 |
11 | "dialog_title": { "message": "どこに保存しますか?" },
12 | "dialog_inputDescription": { "message": "ファイルの保存先フォルダ名を入力してください" },
13 | "dialog_save": { "message": "フォルダを作成して保存" },
14 | "dialog_cancel": { "message": "キャンセル" },
15 |
16 |
17 | "startup_useDownloadDirOptionNote_title": { "message": "選択したタブを保存 の使用開始前の重要な注意事項" },
18 | "startup_useDownloadDirOptionNote_message": { "message": "このアドオンを使い始める前に、Firefoxを自分で正しく設定してください。ここをクリックすると詳細を確認できます。" },
19 |
20 | "useDownloadDirOption_caption": { "message": "使用開始前の重要な注意" },
21 | "useDownloadDirOption_note": { "message": "このアドオンを使用するときは、Firefoxの設定の「一般」→「ファイルとプログラム」→「ダウンロード」で「次のフォルダーに保存する」を選択しておくことを強くお勧めします。「ファイルごとに保存先を指定する」を選択していると、保存しようとしているタブの数だけファイルの保存先を尋ねるダイアログが表示され、非常に煩わしい結果になるので、ご注意ください。" },
22 |
23 |
24 | "config_saveTabs_caption": { "message": "タブのファイルへの保存" },
25 | "config_saveTabsPrefix_label_before": { "message": "タブを" },
26 | "config_saveTabsPrefix_label_after": { "message": "以下に保存する" },
27 | "config_saveTabsPrefix_description": { "message": "※ダウンロードフォルダ配下の保存先パスを指定できます。「/」はパスの区切り文字として解釈されます。パスの中にプレースホルダ「%input%」を含めると、保存時にフォルダ名を入力するダイアログが表示されます。" },
28 | "config_maxFileNameLength_label_before": { "message": "ファイル名の長さが " },
29 | "config_maxFileNameLength_label_after": { "message": "文字より長い場合は省略する" },
30 | "config_maxDownloads_label_before": { "message": "ダウンロードを" },
31 | "config_maxDownloads_label_after": { "message": "件まで並行で行う" },
32 |
33 | "config_allUrlsPermissionGranted_label": { "message": "タブの内容のContent-Typeに基づいてファイル名の拡張子を推定する" },
34 |
35 | "config_showContextCommandOnTab_label": { "message": "タブのコンテキストメニューにメニュー項目を表示する" },
36 | "config_showContextCommandOnPage_label": { "message": "コンテンツ領域のコンテキストメニューにメニュー項目を表示する" },
37 | "config_showContextCommandForSingleTab_label": { "message": "タブが複数選択されていない場合でもコンテキストメニューの項目を有効化する" },
38 |
39 | "config_clearSelectionAfterCommandInvoked_label": { "message": "コマンドの実行後にタブの選択を解除する" },
40 |
41 | "config_shortcuts_caption": { "message": "キーボードショートカット" },
42 |
43 | "config_debug_caption": { "message": "開発用" },
44 | "config_debug_label": { "message": "デバッグモード" },
45 | "config_all_caption": { "message": "すべての設定" }
46 | }
47 |
--------------------------------------------------------------------------------
/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "saveTabsPrefix_defaultValue": {
3 | "message": "已保存标签页/",
4 | "hash": "c4de7a7d09f7247035c1686067b893b5"
5 | },
6 | "context_saveTabs_label": {
7 | "message": "保存标签页到文件(&V)",
8 | "hash": "e4b61003932478b5800fe582aae631b3"
9 | },
10 | "command_saveSelectedTabs": {
11 | "message": "保存选中标签页到文件",
12 | "hash": "66da9e69ca5c42f620d31d523aae8f1f"
13 | },
14 | "config_saveTabsPrefix_label_before": {
15 | "message": "标签页保存到",
16 | "hash": "c5bfd74aed6217ca9d8a3c6b00e992f3"
17 | },
18 | "config_saveTabsPrefix_label_after": {
19 | "message": "\u200b",
20 | "hash": "aa7906338dbdd2af4993295aa9112e97"
21 | },
22 | "config_saveTabsPrefix_description": {
23 | "message": "*路径相对于“下载”文件夹。“/”将识别为路径分隔符。",
24 | "hash": "2ff802469cb2f1617681b50581ed2e94"
25 | },
26 | "config_shortcuts_caption": {
27 | "message": "键盘快捷键",
28 | "hash": "3e4c94412820831fbea1494aa4d5ecf4"
29 | },
30 | "config_debug_caption": {
31 | "message": "开发调试",
32 | "hash": "af84c3ce90181048c533886621f00ac2"
33 | },
34 | "config_debug_label": {
35 | "message": "调试模式",
36 | "hash": "09b9b4dc690b71222315f1191f9a1d1b"
37 | },
38 | "config_all_caption": {
39 | "message": "所有配置参数",
40 | "hash": "e0437bb7bd8d7ce28700078c6bb12418"
41 | },
42 |
43 | "__WET_LOCALE__": { "message": "zh-CN" }
44 | }
45 |
--------------------------------------------------------------------------------
/background/background.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
68 | `.trim(),
69 | onShown(container) {
70 | container.querySelector('[name="input"]').select();
71 | },
72 | buttons: [
73 | browser.i18n.getMessage('dialog_save'),
74 | browser.i18n.getMessage('dialog_cancel')
75 | ]
76 | });
77 | log(' result: ', result);
78 | if (result.buttonIndex != 0)
79 | return;
80 | input = result.values.input || '';
81 | prefix = `${prePart}${input}${postPart}`;
82 | }
83 | catch(error) {
84 | log('error: ', error);
85 | }
86 | log(' => ', { input, prefix });
87 | }
88 | prefix = `${prefix.replace(/\/$/, '')}/`;
89 | const alreadyUsedNames = new Set();
90 | await Promise.all(tabs.map(async tab => {
91 | tab.$fileName = await suggestUniqueFileNameForTab(tab, alreadyUsedNames)
92 | }));
93 |
94 | const downloadLoop = async () => {
95 | while (tabs.length > 0) {
96 | const tab = tabs.shift();
97 | try {
98 | await browser.downloads.download({
99 | url: tab.url,
100 | filename: `${prefix}${tab.$fileName}`
101 | });
102 | }
103 | catch(_error) {
104 | }
105 | }
106 | };
107 | const promisedTasks = [];
108 | for (let i = 0; i < configs.maxDownloads; i++) {
109 | promisedTasks.push(downloadLoop());
110 | }
111 | await Promise.all(promisedTasks);
112 | }
113 |
114 | async function suggestUniqueFileNameForTab(tab, alreadyUsedNames) {
115 | let name = await suggestFileNameForTab(tab);
116 | if (!alreadyUsedNames.has(name)) {
117 | alreadyUsedNames.add(name);
118 | log(`filename for tab ${tab.id}: `, name);
119 | return name;
120 | }
121 | const WITH_SUFFIX_MATCHER = /(-(\d+)(\.?[^\.]+))$/;
122 | let matched = name.match(WITH_SUFFIX_MATCHER);
123 | if (!matched) {
124 | name = name.replace(/(\.?[^\.]+)$/, '-0$1');
125 | matched = name.match(WITH_SUFFIX_MATCHER);
126 | }
127 | let count = parseInt(matched[2]);
128 | while (true) {
129 | count++;
130 | const newName = name.replace(matched[1], `-${count}${matched[3]}`);
131 | if (alreadyUsedNames.has(newName))
132 | continue;
133 | alreadyUsedNames.add(newName);
134 | log(`filename for tab ${tab.id}: `, newName);
135 | return newName;
136 | }
137 | }
138 |
139 | const kMAYBE_IMAGE_PATTERN = /\.(jpe?g|png|gif|bmp|svg)/i;
140 | const kMAYBE_RAW_FILE_PATTERN = /\.(te?xt|md)/i;
141 |
142 | async function suggestFileNameForTab(tab) {
143 | const fileNameMatch = tab.url
144 | .replace(/^\w+:\/\/[^\/]+\//, '') // remove origin part
145 | .replace(/#.*$/, '') // remove fragment
146 | .replace(/\?.*$/, '') // remove query
147 | .match(/([^\/]+\.([^\.\/]+))$/);
148 | log('suggestFileNameForTab ', tab.id, fileNameMatch);
149 | if (fileNameMatch &&
150 | (kMAYBE_IMAGE_PATTERN.test(fileNameMatch[1]) ||
151 | kMAYBE_RAW_FILE_PATTERN.test(fileNameMatch[1]))) {
152 | const parts = fileNameMatch[1].split('.');
153 | const baseName = parts.slice(0, parts.length - 1);
154 | return `${shorten(baseName)}.${parts[parts.length - 1]}`;
155 | }
156 |
157 | let suggestedExtension = '';
158 | if (!tab.discarded &&
159 | Permissions.isPermittedTab(tab) &&
160 | await Permissions.isGranted(Permissions.ALL_URLS)) {
161 | log(`getting content type of ${tab.id}`);
162 | try {
163 | let contentType = await browser.tabs.executeScript(tab.id, {
164 | code: `document.contentType`
165 | });
166 | if (Array.isArray(contentType))
167 | contentType = contentType[0];
168 | log(`contentType of ${tab.id}: `, contentType);
169 | if (/^(text\/html|application\/xhtml\+xml)/.test(contentType)) {
170 | suggestedExtension = '.html';
171 | }
172 | else if (/^text\//.test(contentType)) {
173 | suggestedExtension = '.txt';
174 | }
175 | else if (/^image\//.test(contentType)) {
176 | suggestedExtension = `.${contentType.replace(/^image\/|\+.+$/g, '')}`;
177 | }
178 | }
179 | catch(e) {
180 | log('Error! ', e);
181 | }
182 | }
183 | log('suggestedExtension: ', tab.id, suggestedExtension);
184 | const baseName = shorten(tab.title.replace(/[\/\\:*?"<>|]/g, '_'));
185 | const fileName = `${baseName}${suggestedExtension}`;
186 | log('finally suggested fileName: ', tab.id, fileName);
187 | return fileName;
188 | }
189 |
190 | function shorten(name) {
191 | if (configs.maxFileNameLength < 0 ||
192 | name.length < configs.maxFileNameLength)
193 | return name;
194 |
195 | return `${name.substring(0, configs.maxFileNameLength - 1)}…`;
196 | }
197 |
--------------------------------------------------------------------------------
/common/common.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | import Configs from '/extlib/Configs.js';
9 |
10 | export const configs = new Configs({
11 | showContextCommandOnTab: true,
12 | showContextCommandOnPage: false,
13 | showContextCommandForSingleTab: false,
14 | clearSelectionAfterCommandInvoked: false,
15 | saveTabsPrefix: browser.i18n.getMessage('saveTabsPrefix_defaultValue'),
16 | maxFileNameLength: 30,
17 | maxDownloads: 10,
18 |
19 | optionsExpandedGroups: [
20 | 'useDownloadDirOptionNote',
21 | ],
22 |
23 | useDownloadDirOptionNoteShown: false,
24 |
25 | notificationTimeout: 10 * 1000,
26 |
27 | debug: false
28 | }, {
29 | localKeys: `
30 | debug
31 | `.trim().split('\n').map(key => key.trim()).filter(key => key && key.indexOf('//') != 0)
32 | });
33 |
34 |
35 | export function log(message, ...args)
36 | {
37 | if (!configs || !configs.debug)
38 | return;
39 |
40 | const nest = (new Error()).stack.split('\n').length;
41 | let indent = '';
42 | for (let i = 0; i < nest; i++) {
43 | indent += ' ';
44 | }
45 | console.log(`savetab<${log.context}>: ${indent}${message}`, ...args);
46 | }
47 | log.context = '?';
48 |
49 | export async function wait(task = 0, timeout = 0) {
50 | if (typeof task != 'function') {
51 | timeout = task;
52 | task = null;
53 | }
54 | return new Promise((resolve, _reject) => {
55 | setTimeout(async () => {
56 | if (task)
57 | await task();
58 | resolve();
59 | }, timeout);
60 | });
61 | }
62 |
63 | export function handleMissingReceiverError(error) {
64 | if (!error ||
65 | !error.message ||
66 | error.message.indexOf('Could not establish connection. Receiving end does not exist.') == -1)
67 | throw error;
68 | // otherwise, this error is caused from missing receiver.
69 | // we just ignore it.
70 | }
71 |
72 | export async function notify({ icon, title, message, timeout, url } = {}) {
73 | const id = await browser.notifications.create({
74 | type: 'basic',
75 | iconUrl: icon || browser.extension.getURL(`resources/Save.svg`),
76 | title,
77 | message
78 | });
79 |
80 | let onClicked;
81 | let onClosed;
82 | return new Promise(async (resolve, _reject) => {
83 | let resolved = false;
84 |
85 | onClicked = notificationId => {
86 | if (notificationId != id)
87 | return;
88 | if (url) {
89 | browser.tabs.create({
90 | url
91 | });
92 | }
93 | resolved = true;
94 | resolve(true);
95 | };
96 | browser.notifications.onClicked.addListener(onClicked);
97 |
98 | onClosed = notificationId => {
99 | if (notificationId != id)
100 | return;
101 | if (!resolved) {
102 | resolved = true;
103 | resolve(false);
104 | }
105 | };
106 | browser.notifications.onClosed.addListener(onClosed);
107 |
108 | if (typeof timeout != 'number')
109 | timeout = configs.notificationTimeout;
110 | if (timeout >= 0) {
111 | await wait(timeout);
112 | }
113 | await browser.notifications.clear(id);
114 | if (!resolved)
115 | resolve(false);
116 | }).then(clicked => {
117 | browser.notifications.onClicked.removeListener(onClicked);
118 | onClicked = null;
119 | browser.notifications.onClosed.removeListener(onClosed);
120 | onClosed = null;
121 | return clicked;
122 | });
123 | }
124 |
125 |
126 | const RTL_LANGUAGES = new Set([
127 | 'ar',
128 | 'he',
129 | 'fa',
130 | 'ur',
131 | 'ps',
132 | 'sd',
133 | 'ckb',
134 | 'prs',
135 | 'rhg',
136 | ]);
137 |
138 | export function isRTL() {
139 | const lang = (
140 | navigator.language ||
141 | navigator.userLanguage ||
142 | //(new Intl.DateTimeFormat()).resolvedOptions().locale ||
143 | ''
144 | ).split('-')[0];
145 | return RTL_LANGUAGES.has(lang);
146 | }
147 |
--------------------------------------------------------------------------------
/common/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | export const kTST_ID = 'treestyletab@piro.sakura.ne.jp';
9 |
10 | export const kTSTAPI_REGISTER_SELF = 'register-self';
11 | export const kTSTAPI_UNREGISTER_SELF = 'unregister-self';
12 | export const kTSTAPI_PING = 'ping';
13 | export const kTSTAPI_NOTIFY_READY = 'ready';
14 | export const kTSTAPI_GET_TREE = 'get-tree';
15 | export const kTSTAPI_GET_TREE_STRUCTURE = 'get-tree-structure';
16 | export const kTSTAPI_CONTEXT_MENU_SHOWN = 'fake-contextMenu-shown';
17 | export const kTSTAPI_CONTEXT_MENU_CREATE = 'fake-contextMenu-create';
18 | export const kTSTAPI_CONTEXT_MENU_UPDATE = 'fake-contextMenu-update';
19 | export const kTSTAPI_CONTEXT_MENU_REMOVE = 'fake-contextMenu-remove';
20 | export const kTSTAPI_CONTEXT_MENU_REMOVE_ALL = 'fake-contextMenu-remove-all';
21 | export const kTSTAPI_CONTEXT_MENU_CLICK = 'fake-contextMenu-click';
22 |
23 |
24 | export const kMTH_ID = 'multipletab@piro.sakura.ne.jp';
25 |
26 | export const kMTHAPI_READY = 'ready';
27 | export const kMTHAPI_GET_TAB_SELECTION = 'get-tab-selection';
28 | export const kMTHAPI_SET_TAB_SELECTION = 'set-tab-selection';
29 | export const kMTHAPI_CLEAR_TAB_SELECTION = 'clear-tab-selection';
30 | export const kMTHAPI_ADD_SELECTED_TAB_COMMAND = 'add-selected-tab-command';
31 | export const kMTHAPI_REMOVE_SELECTED_TAB_COMMAND = 'remove-selected-tab-command';
32 | export const kMTHAPI_REMOVE_ALL_SELECTED_TAB_COMMANDS = 'remove-all-selected-tab-commands';
33 | export const kMTHAPI_INVOKE_SELECTED_TAB_COMMAND = 'selected-tab-command';
34 |
--------------------------------------------------------------------------------
/common/permissions.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | import {
9 | configs
10 | } from './common.js';
11 |
12 | export const ALL_URLS = { origins: [''] };
13 |
14 | export function clearRequest() {
15 | configs.requestingPermissions = null;
16 | }
17 |
18 | export function isGranted(permissions) {
19 | try {
20 | return browser.permissions.contains(permissions);
21 | }
22 | catch(_e) {
23 | return Promise.reject(new Error('unsupported permission'));
24 | }
25 | }
26 |
27 | export function bindToCheckbox(permissions, checkbox, options = {}) {
28 | isGranted(permissions)
29 | .then(granted => {
30 | checkbox.checked = granted;
31 | })
32 | .catch(_error => {
33 | checkbox.setAttribute('readonly', true);
34 | checkbox.setAttribute('disabled', true);
35 | const label = checkbox.closest('label') || document.querySelector(`label[for=${checkbox.id}]`);
36 | if (label)
37 | label.setAttribute('disabled', true);
38 | });
39 |
40 | checkbox.addEventListener('change', _event => {
41 | checkbox.requestPermissions()
42 | });
43 |
44 | /*
45 | // These events are not available yet on Firefox...
46 | browser.permissions.onAdded.addListener(addedPermissions => {
47 | if (addedPermissions.permissions.indexOf('...') > -1)
48 | checkbox.checked = true;
49 | });
50 | browser.permissions.onRemoved.addListener(removedPermissions => {
51 | if (removedPermissions.permissions.indexOf('...') > -1)
52 | checkbox.checked = false;
53 | });
54 | */
55 |
56 | checkbox.requestPermissions = async () => {
57 | try {
58 | if (!checkbox.checked) {
59 | await browser.permissions.remove(permissions);
60 | if (options.onChanged)
61 | options.onChanged(false);
62 | return;
63 | }
64 |
65 | checkbox.checked = false;
66 | if (configs.requestingPermissionsNatively)
67 | return;
68 |
69 | let granted = await browser.permissions.request(permissions);
70 | if (granted === undefined)
71 | granted = await isGranted(permissions);
72 | else if (!granted)
73 | return;
74 |
75 | if (granted) {
76 | checkbox.checked = true;
77 | if (options.onChanged)
78 | options.onChanged(true);
79 | return;
80 | }
81 | }
82 | catch(error) {
83 | console.log(error);
84 | }
85 | checkbox.checked = false;
86 | };
87 | }
88 |
89 | export function isPermittedTab(tab) {
90 | if (tab.discarded)
91 | return false;
92 | return /^about:blank($|\?|#)/.test(tab.url) ||
93 | !/^(about|resource|chrome|file|view-source):/.test(tab.url);
94 | }
95 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import _import from "eslint-plugin-import";
3 |
4 | export default [{
5 | ignores: ["eslint.config.mjs", "extlib/*", "submodules/*", "!**/.eslintrc.js"],
6 | }, {
7 | plugins: {
8 | import: _import,
9 | },
10 |
11 | languageOptions: {
12 | globals: {
13 | ...globals.browser,
14 | ...globals.webextensions,
15 | },
16 |
17 | ecmaVersion: 2022,
18 | sourceType: "module",
19 | },
20 |
21 | settings: {
22 | "import/resolver": {
23 | "babel-module": {
24 | root: ["./"],
25 | },
26 | },
27 | },
28 |
29 | rules: {
30 | indent: ["warn", 2, {
31 | SwitchCase: 1,
32 | MemberExpression: 1,
33 |
34 | CallExpression: {
35 | arguments: "first",
36 | },
37 |
38 | VariableDeclarator: {
39 | var: 2,
40 | let: 2,
41 | const: 3,
42 | },
43 | }],
44 |
45 | quotes: ["warn", "single", {
46 | avoidEscape: true,
47 | allowTemplateLiterals: true,
48 | }],
49 |
50 |
51 | "no-const-assign": "error",
52 |
53 | "prefer-const": ["warn", {
54 | destructuring: "any",
55 | ignoreReadBeforeAssign: false,
56 | }],
57 |
58 | "no-var": "error",
59 |
60 | "no-unused-vars": ["warn", {
61 | vars: "all",
62 | args: "after-used",
63 | argsIgnorePattern: "^_",
64 | caughtErrors: "all",
65 | caughtErrorsIgnorePattern: "^_",
66 | }],
67 |
68 | "no-use-before-define": ["error", {
69 | functions: false,
70 | classes: true,
71 | }],
72 |
73 | "no-unused-expressions": "error",
74 | "no-unused-labels": "error",
75 |
76 | "no-undef": ["error", {
77 | typeof: true,
78 | }],
79 |
80 | "import/default": "error",
81 | "import/namespace": "error",
82 | "import/no-duplicates": "error",
83 | "import/export": "error",
84 | "import/extensions": ["error", "always"],
85 | "import/first": "error",
86 | "import/named": "error",
87 | "import/no-named-as-default": "error",
88 | "import/no-named-as-default-member": "error",
89 | "import/no-cycle": ["warn", {}],
90 | "import/no-self-import": "error",
91 |
92 | "import/no-unresolved": ["error", {
93 | caseSensitive: true,
94 | }],
95 |
96 | "import/no-useless-path-segments": "error",
97 | },
98 | }];
--------------------------------------------------------------------------------
/extlib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piroor/save-selected-tabs-to-files/c65d2f18608c4d3262ea0765bc536f3eada9595e/extlib/.gitkeep
--------------------------------------------------------------------------------
/history.en.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | - master/HEAD
4 | - 1.2.0 (2022.9.12)
5 | * Restrict paralel downloads max 10 tabs by default.
6 | - 1.1.6 (2021.8.16)
7 | * Shorten too long file name by default.
8 | * Show message correctly about a download config of Firefox in the options page.
9 | - 1.1.5 (2021.5.5)
10 | * Show a notification about a download config of Firefox at the initial startup.
11 | * Fix wrong behaviors of "All Configs" UI: apply imported configs to options UI immediately and treat decimal values as valid for some numeric options.
12 | - 1.1.4 (2020.7.29)
13 | * Better support for the Managed Storage.
14 | * Flexible width input field for the dialog to input the folder name.
15 | - 1.1.3 (2020.4.28)
16 | * Handle dismissed semi-modal dialogs correctly.
17 | * Optimize semi-modal dialogs a little.
18 | - 1.1.2 (2020.4.25)
19 | * Improve implementation of semi-modal dialogs. Now it is more stable, more similar to native dialogs, more friendly for dark color scheme, and don't appear in the "Recently Closed Windows" list.
20 | - 1.1.1 (2020.4.24)
21 | * Show popup windows correctly on Firefox ESR68. (regression on 1.1.0)
22 | - 1.1.0 (2020.4.22)
23 | * Show folder name prompt as a semi-modal popup window.
24 | - 1.0.8 (2020.3.6)
25 | * Show in-content confirmation dialog correctly on lately versions of Firefox.
26 | * Remove keyboard shorctut customization UI, because Firefox ESR68 has it.
27 | * Uninitialized options page is now invisible.
28 | - 1.0.7 (2019.5.24)
29 | * Follow to changes on Tree Style Tab 3.0.12 and Multiple Tab Handler 3.0.7.
30 | * Add ability to export and import all configurations except keyboard shortcuts. (Options => "Development" => "Debug mode" => "All Configs" => "Import/Export")
31 | - 1.0.6 (2019.1.3)
32 | * Add ability co control visibility of context menu items for each: tab context menu and page context menu.
33 | - 1.0.5 (2018.12.15)
34 | * Invoke command from Multiple Tab Handler correctly.
35 | - 1.0.4 (2018.11.3)
36 | * Improve compatibility with Multiple Tab Handler.
37 | - 1.0.3 (2018.10.31)
38 | * Improve compatibility with Tree Style Tab.
39 | * Update menu label when there is no more multiselected tab.
40 | - 1.0.2 (2018.10.30)
41 | * Don't execute command for all unselected tabs.
42 | - 1.0.1 (2018.10.30)
43 | * Make `` permission optional.
44 | * Run command from keyboard shortcut correctly.
45 | - 1.0 (2018.10.30)
46 | * Separated from [Multiple Tab Handler](https://addons.mozilla.org/firefox/addon/multiple-tab-handler/).
47 | * The "zh-CN" locale is added by yfdyh000. Thanks!
48 |
--------------------------------------------------------------------------------
/history.ja.md:
--------------------------------------------------------------------------------
1 | # 更新履歴
2 |
3 | - master/HEAD
4 | - 1.2.0 (2022.9.12)
5 | * 並列ダウンロード数を最大で10タブ分までに制限するようにした
6 | - 1.1.6 (2021.8.16)
7 | * 長すぎるファイル名を初期状態で短縮するように↓
8 | * 設定ページでFirefoxのダウンロード設定に関するメッセージを正しく表示するように修正
9 | - 1.1.5 (2021.5.5)
10 | * 初回起動時にFirefoxのダウンロード設定に関する注意を表示するようにした
11 | * 「すべての設定」のUIの不備を改善:インポートした設定をUIに即座に反映し、また、数値型の一部の設定項目で小数が不正な値として警告されてしまわないようにした
12 | - 1.1.4 (2020.7.29)
13 | * Managed Storageへの対応を改善
14 | * フォルダ名入力用ダイアログの入力欄をウィンドウに合わせて拡大するようにした
15 | - 1.1.3 (2020.4.28)
16 | * モーダル風ダイアログがクローズボックスで閉じられた時の挙動を改善
17 | * モーダル風ダイアログを開くのに要する時間を若干短縮した
18 | - 1.1.2 (2020.4.25)
19 | * モーダル風ダイアログの実装を改善した(安定性の向上、ネイティブのダイアログを模したボタンの配置、Darkモードへの対応、閉じられたダイアログが「最近閉じたウィンドウ」の一覧になるべく現れないようにした)
20 | - 1.1.1 (2020.4.24)
21 | * Firefox ESR68でポップアップウィンドウが表示されない問題を修正(1.1.0での後退バグ)
22 | - 1.1.0 (2020.4.22)
23 | * 保存先フォルダ名の入力をモーダル風ポップアップウィンドウで表示するようにした
24 | - 1.0.8 (2020.3.6)
25 | * 最近のバージョンのFirefoxにおいて、コンテンツ領域内に確認ダイアログが表示されなくなっていたのを修正
26 | * Firefox ESR68時点ですでに本体にUIがあるため、キーボードショートカット設定用のUIを削除した
27 | * 初期化前の設定画面を隠すようにした
28 | - 1.0.7 (2019.5.24)
29 | * ツリー型タブ 3.0.12 および マルチプルタブハンドラ 3.0.7 の仕様変更に追従
30 | * キーボードショートカットを除く全設定のインポートとエクスポートに対応(設定→開発用→デバッグモード→すべての設定→Import/Export)
31 | - 1.0.6 (2019.1.3)
32 | * タブのコンテキストメニューとWebページ上のコンテキストメニューのそれぞれについてメニュー項目の表示・非表示を制御できるようにした
33 | - 1.0.5 (2018.12.15)
34 | * マルチプルタブハンドラのAPI経由でのコマンド実行に失敗していたのを修正
35 | - 1.0.4 (2018.11.3)
36 | * マルチプルタブハンドラと併用時の互換性を向上
37 | - 1.0.3 (2018.10.31)
38 | * ツリー型タブと併用時の互換性を向上
39 | - 1.0.2 (2018.10.30)
40 | * 選択されていないすべてに対してコマンドを実行してしまう不具合を修正
41 | - 1.0.1 (2018.10.30)
42 | * ``の権限を必須でなくした
43 | * キーボードショートカットからコマンドを実行できない問題を修正
44 | - 1.0 (2018.10.30)
45 | * [マルチプルタブハンドラ](https://addons.mozilla.org/firefox/addon/multiple-tab-handler/)から機能を分割した
46 | * zh-CNロケール追加(by yfdyh000, thanks!)
47 |
--------------------------------------------------------------------------------
/licenses/MPL2.0.txt:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extensionName__",
4 | "version": "1.2.0",
5 | "author": "YUKI \"Piro\" Hiroshi",
6 | "description": "__MSG_extensionDescription__",
7 | "permissions": [
8 | "activeTab",
9 | "downloads",
10 | "menus",
11 | "notifications",
12 | "storage",
13 | "tabs"
14 | ],
15 | "optional_permissions": [
16 | ""
17 | ],
18 | "icons": {
19 | "16": "resources/Save.svg#default",
20 | "20": "resources/Save.svg#default",
21 | "24": "resources/Save.svg#default",
22 | "32": "resources/Save.svg#default"
23 | },
24 | "background": {
25 | "page": "background/background.html"
26 | },
27 | "commands": {
28 | "saveSelectedTabs": {
29 | "description": "__MSG_command_saveSelectedTabs__"
30 | }
31 | },
32 | "options_ui": {
33 | "page": "options/options.html",
34 | "browser_style": true
35 | },
36 | "default_locale": "en",
37 | "applications": {
38 | "gecko": {
39 | "id": "save-selected-tabs-to-files@piro.sakura.ne.jp",
40 | "strict_min_version": "67.0"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/options/init.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | import {
9 | log,
10 | configs,
11 | isRTL,
12 | } from '/common/common.js';
13 | import * as Permissions from '/common/permissions.js';
14 | import Options from '/extlib/Options.js';
15 | import '../extlib/l10n.js';
16 |
17 | log.context = 'Options';
18 | const options = new Options(configs);
19 |
20 | function onConfigChanged(key) {
21 | switch (key) {
22 | case 'debug':
23 | if (configs.debug)
24 | document.documentElement.classList.add('debugging');
25 | else
26 | document.documentElement.classList.remove('debugging');
27 | break;
28 | }
29 | }
30 |
31 | configs.$addObserver(onConfigChanged);
32 | window.addEventListener('DOMContentLoaded', async () => {
33 | document.documentElement.classList.toggle('rtl', isRTL());
34 | await configs.$loaded;
35 |
36 | const focusedItem = document.querySelector(':target');
37 | for (const fieldset of document.querySelectorAll('fieldset.collapsible')) {
38 | if (configs.optionsExpandedGroups.includes(fieldset.id) ||
39 | (focusedItem && fieldset.contains(focusedItem)))
40 | fieldset.classList.remove('collapsed');
41 | else
42 | fieldset.classList.add('collapsed');
43 |
44 | const onChangeCollapsed = () => {
45 | if (!fieldset.id)
46 | return;
47 | const otherExpandedSections = configs.optionsExpandedGroups.filter(id => id != fieldset.id);
48 | if (fieldset.classList.contains('collapsed'))
49 | configs.optionsExpandedGroups = otherExpandedSections;
50 | else
51 | configs.optionsExpandedGroups = otherExpandedSections.concat([fieldset.id]);
52 | };
53 |
54 | const legend = fieldset.querySelector(':scope > legend');
55 | legend.addEventListener('click', () => {
56 | fieldset.classList.toggle('collapsed');
57 | onChangeCollapsed();
58 | });
59 | legend.addEventListener('keydown', event => {
60 | if (event.key != 'Enter')
61 | return;
62 | fieldset.classList.toggle('collapsed');
63 | onChangeCollapsed();
64 | });
65 | }
66 |
67 | Permissions.bindToCheckbox(
68 | Permissions.ALL_URLS,
69 | document.querySelector('#allUrlsPermissionGranted')
70 | );
71 |
72 | options.buildUIForAllConfigs(document.querySelector('#debug-configs'));
73 | onConfigChanged('debug');
74 |
75 | document.documentElement.classList.add('initialized');
76 | }, { once: true });
77 |
78 |
--------------------------------------------------------------------------------
/options/options.css:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 |
7 | :root > * {
8 | transition: opacity 0.25s ease-out;
9 | }
10 | :root:not(.initialized) > * {
11 | opacity: 0;
12 | }
13 |
14 | :root.rtl {
15 | direction: rtl;
16 | }
17 |
18 | hr {
19 | border: 0 none;
20 | border-top: 1px solid;
21 | margin: 2em 0;
22 | width: 100%;
23 | }
24 |
25 | p, ul {
26 | margin: 0 0 0.5em 0;
27 | padding: 0;
28 | }
29 |
30 | ul,
31 | ul li {
32 | list-style: none;
33 | }
34 |
35 | p.sub {
36 | margin-left: 2em;
37 | }
38 |
39 | ul p.sub {
40 | margin-top: 0;
41 | margin-bottom: 0;
42 | }
43 |
44 | :root:not(.debugging) #debug-configs {
45 | max-height: 0;
46 | overflow: hidden;
47 | }
48 |
49 | :root:not(.debugging) #debug-configs * {
50 | -moz-user-focus: ignore;
51 | -moz-user-input: disabled;
52 | }
53 |
54 |
55 |
56 | fieldset.collapsible.collapsed > *:not(legend):not(div) /* "div" is for the container of "import" and "export" buttons */ {
57 | display: none;
58 | }
59 |
60 | fieldset.collapsible > legend::before {
61 | content: "▼";
62 | display: inline-block;
63 | font-size: 65%;
64 | margin-right: 0.5em;
65 | position: relative;
66 | transition: transform 0.2s ease;
67 | }
68 |
69 | fieldset.collapsible.collapsed > legend::before {
70 | transform: rotate(-90deg);
71 | }
72 |
73 |
74 |
75 | #useDownloadDirOptionNote {
76 | border: 0 none;
77 | margin: 0 0 2em;
78 | padding: 0;
79 | }
80 |
81 | #useDownloadDirOptionNote legend {
82 | font-size: large;
83 | font-weight: bold;
84 | }
85 |
86 | #useDownloadDirOptionNote img {
87 | display: block;
88 | margin: 1em auto 0;
89 | /*
90 | 664px is the value of `--section-width` defined in Firefox itself.
91 | https://searchfox.org/mozilla-central/rev/185ab5e4f4e01341e009cd4633d1275ffe4d4c8b/toolkit/mozapps/extensions/content/aboutaddons.css#8
92 | */
93 | max-width: calc(664px * 0.9 - 2em);
94 | box-shadow: 0.2em 0.2em 1em rgba(0, 0, 0, 0.45);
95 | }
96 |
--------------------------------------------------------------------------------
/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
25 |