├── LICENSE
├── README.md
├── v2
├── chrome
│ ├── _locales
│ ├── background.js
│ ├── data
│ └── manifest.json
└── firefox
│ ├── _locales
│ ├── de
│ │ └── messages.json
│ ├── en
│ │ └── messages.json
│ ├── es
│ │ └── messages.json
│ ├── fr
│ │ └── messages.json
│ ├── it
│ │ └── messages.json
│ ├── ja
│ │ └── messages.json
│ ├── nl
│ │ └── messages.json
│ ├── pl
│ │ └── messages.json
│ ├── pt_BR
│ │ └── messages.json
│ ├── pt_PT
│ │ └── messages.json
│ ├── ru
│ │ └── messages.json
│ └── zh_CN
│ │ └── messages.json
│ ├── background.js
│ ├── data
│ ├── assets
│ │ └── bell.wav
│ ├── commander
│ │ ├── components
│ │ │ ├── directory-view.js
│ │ │ ├── directory-view
│ │ │ │ ├── list-view.js
│ │ │ │ └── path-view.js
│ │ │ ├── prompt-view.js
│ │ │ └── tools-view.js
│ │ ├── engine.js
│ │ ├── images
│ │ │ ├── directory-readonly.svg
│ │ │ ├── directory.svg
│ │ │ ├── error.svg
│ │ │ └── page.svg
│ │ ├── index.css
│ │ ├── index.html
│ │ └── index.js
│ └── icons
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 19.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ ├── 38.png
│ │ ├── 48.png
│ │ ├── 512.png
│ │ ├── 64.png
│ │ ├── dark
│ │ └── 128.png
│ │ ├── light
│ │ └── 128.png
│ │ └── svgs
│ │ ├── dark.svg
│ │ ├── default.svg
│ │ └── light.svg
│ └── manifest.json
└── v3
├── _locales
├── de
│ └── messages.json
├── en
│ └── messages.json
├── es
│ └── messages.json
├── fr
│ └── messages.json
├── it
│ └── messages.json
├── ja
│ └── messages.json
├── nl
│ └── messages.json
├── pl
│ └── messages.json
├── pt_BR
│ └── messages.json
├── pt_PT
│ └── messages.json
├── ru
│ └── messages.json
└── zh_CN
│ └── messages.json
├── data
├── assets
│ └── bell.wav
├── commander
│ ├── commands
│ │ ├── default.json
│ │ └── vim.json
│ ├── components
│ │ ├── directory-view.js
│ │ ├── directory-view
│ │ │ ├── list-view.js
│ │ │ ├── path-view-test
│ │ │ │ ├── index.css
│ │ │ │ ├── index.html
│ │ │ │ └── index.js
│ │ │ └── path-view.js
│ │ ├── notify-view.js
│ │ ├── prompt-view.js
│ │ └── tools-view.js
│ ├── engine.js
│ ├── images
│ │ ├── directory-readonly.svg
│ │ ├── directory.svg
│ │ ├── drop-after.svg
│ │ ├── drop-inside.svg
│ │ ├── error.svg
│ │ └── page.svg
│ ├── index.css
│ ├── index.html
│ └── index.js
└── icons
│ ├── 128.png
│ ├── 16.png
│ ├── 19.png
│ ├── 256.png
│ ├── 32.png
│ ├── 38.png
│ ├── 48.png
│ ├── 512.png
│ ├── 64.png
│ ├── dark
│ └── 128.png
│ ├── light
│ └── 128.png
│ └── svgs
│ ├── dark.svg
│ ├── default.svg
│ └── light.svg
├── manifest.json
└── worker.js
/LICENSE:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bookmarks Commander
2 | A dual-pane Norton Commander liked bookmarks manager that supports sorting, dark theme, search, and duplicate detection
3 |
4 | ### Preview
5 |
6 | [](https://www.youtube.com/watch?v=qoqJK3hEFMs)
7 |
8 | ### Web Components
9 |
10 | List View: https://webextension.org/custom-component/list-view/index.html
11 | Path View: https://webextension.org/custom-component/path-view/index.html
12 |
13 | ### Download Links
14 | * Homepage: https://add0n.com/bookmarks-commander.html
15 | * Usage Review: https://webextension.org/blog/2022/04/17/bookmarks-commander-extension.html
16 | * Chrome: https://chrome.google.com/webstore/detail/bookmarks-commander/knfpajocfeohpaipkfpdbfhgibajfmcf
17 | * Edge: https://microsoftedge.microsoft.com/addons/detail/kenlddohpphjdhgfejegaaplbfmcpcin
18 | * Firefox: https://addons.mozilla.org/firefox/addon/bookmarks-commander/
19 |
--------------------------------------------------------------------------------
/v2/chrome/_locales:
--------------------------------------------------------------------------------
1 | ../firefox/_locales
--------------------------------------------------------------------------------
/v2/chrome/background.js:
--------------------------------------------------------------------------------
1 | ../firefox/background.js
--------------------------------------------------------------------------------
/v2/chrome/data:
--------------------------------------------------------------------------------
1 | ../firefox/data
--------------------------------------------------------------------------------
/v2/chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "version": "0.2.8",
4 | "name": "Bookmarks Commander",
5 | "description": "__MSG_description__",
6 | "default_locale": "en",
7 | "offline_enabled": true,
8 | "permissions": [
9 | "bookmarks",
10 | "contextMenus",
11 | "notifications",
12 | "storage",
13 | "chrome://favicon/*"
14 | ],
15 | "homepage_url": "https://add0n.com/bookmarks-commander.html",
16 | "background": {
17 | "persistent": false,
18 | "scripts": [
19 | "background.js"
20 | ]
21 | },
22 | "icons": {
23 | "16": "data/icons/16.png",
24 | "19": "data/icons/19.png",
25 | "32": "data/icons/32.png",
26 | "38": "data/icons/38.png",
27 | "48": "data/icons/48.png",
28 | "64": "data/icons/64.png",
29 | "128": "data/icons/128.png",
30 | "256": "data/icons/256.png",
31 | "512": "data/icons/512.png"
32 | },
33 | "browser_action": {},
34 | "incognito": "split"
35 | }
36 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Ein Lesezeichen-Manager mit zwei Fenstern, der Sortierung, dunkles Thema, Suche und Erkennung von Duplikaten unterstützt"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "A dual-pane Norton Commander liked bookmarks manager that supports sorting, dark theme, search, and duplicate detection"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un gestor de marcadores de doble panel que admite la clasificación, el tema oscuro, la búsqueda y la detección de duplicados"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un gestionnaire de signets à double volet qui prend en charge le tri, les thèmes sombres, la recherche et la détection des doublons"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un gestore di segnalibri a doppio pannello che supporta l'ordinamento, il tema scuro, la ricerca e il rilevamento dei duplicati"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "デュアルペインのノートンコマンダーは、ソート、ダークテーマ、検索、重複検出をサポートしています。"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/nl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Een bladwijzerbeheerder met twee deelvensters die sorteren, een donker thema, zoeken en duplicaatdetectie ondersteunt"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/pl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Dwupanelowy menedżer zakładek Norton Commander z obsługą sortowania, ciemnego motywu, wyszukiwania i wykrywania duplikatów"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Um gerenciador de marcadores de dois painéis que suporta classificação, tema escuro, busca e detecção de duplicatas."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/pt_PT/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Um gestor de marcadores de dois painéis que suporta a classificação, tema escuro, pesquisa e detecção de duplicados"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Двухпанельный менеджер закладок, поддерживающий сортировку, темную тему, поиск и обнаружение дубликатов."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "一个双窗格的诺顿指挥官喜欢的书签管理器,支持排序,暗主题,搜索和重复检测。"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | chrome.browserAction.onClicked.addListener(() => {
4 | chrome.storage.local.get({
5 | 'mode': 'tab'
6 | }, async prefs => {
7 | // try to find an open instance
8 | try {
9 | await new Promise((resolve, reject) => {
10 | chrome.runtime.sendMessage({
11 | method: 'instance'
12 | }, r => r ? resolve() : reject(chrome.runtime.lastError));
13 | });
14 | }
15 | catch (e) {
16 | if (prefs.mode === 'tab') {
17 | chrome.tabs.create({
18 | url: 'data/commander/index.html'
19 | }, tab => chrome.storage.local.set({
20 | tab: tab.id
21 | }));
22 | }
23 | else if (prefs.mode === 'window') {
24 | chrome.windows.getCurrent(win => {
25 | chrome.storage.local.get({
26 | 'window.width': 750,
27 | 'window.height': 600,
28 | 'window.left': win.left + Math.round((win.width - 700) / 2),
29 | 'window.top': win.top + Math.round((win.height - 500) / 2)
30 | }, prefs => {
31 | chrome.windows.create({
32 | url: '/data/commander/index.html?mode=window',
33 | width: Math.max(400, prefs['window.width']),
34 | height: Math.max(300, prefs['window.height']),
35 | left: prefs['window.left'],
36 | top: prefs['window.top'],
37 | type: 'popup'
38 | });
39 | });
40 | });
41 | }
42 | }
43 | });
44 | });
45 |
46 | const icon = mode => chrome.browserAction.setIcon({
47 | path: {
48 | '16': 'data/icons/' + mode + '/128.png'
49 | }
50 | });
51 |
52 | {
53 | const startup = () => chrome.storage.local.get({
54 | 'mode': 'tab',
55 | 'popup.width': 800,
56 | 'popup.height': 600,
57 | 'custom-icon': ''
58 | }, prefs => {
59 | if (prefs['custom-icon']) {
60 | icon(prefs['custom-icon']);
61 | }
62 | chrome.contextMenus.create({
63 | id: 'mode-tab',
64 | title: 'Open in Tab',
65 | contexts: ['browser_action'],
66 | type: 'radio',
67 | checked: prefs.mode === 'tab'
68 | });
69 | chrome.contextMenus.create({
70 | id: 'mode-window',
71 | title: 'Open in Window',
72 | contexts: ['browser_action'],
73 | type: 'radio',
74 | checked: prefs.mode === 'window'
75 | });
76 | chrome.contextMenus.create({
77 | id: 'mode-popup',
78 | title: 'Open in Popup',
79 | contexts: ['browser_action'],
80 | type: 'radio',
81 | checked: prefs.mode === 'popup'
82 | });
83 | if (prefs.mode === 'popup') {
84 | chrome.browserAction.setPopup({
85 | popup: `data/commander/index.html?mode=popup&width=${prefs['popup.width']}&height=${prefs['popup.height']}`
86 | });
87 | }
88 | });
89 | chrome.runtime.onInstalled.addListener(startup);
90 | chrome.runtime.onStartup.addListener(startup);
91 | }
92 | chrome.contextMenus.onClicked.addListener(info => {
93 | if (info.menuItemId.startsWith('mode-')) {
94 | chrome.storage.local.set({
95 | mode: info.menuItemId.replace('mode-', '')
96 | });
97 | }
98 | });
99 |
100 | chrome.storage.onChanged.addListener(ps => {
101 | if (ps.mode) {
102 | chrome.storage.local.get({
103 | 'popup.width': 800,
104 | 'popup.height': 600
105 | }, prefs => {
106 | chrome.browserAction.setPopup({
107 | popup: ps.mode.newValue === 'popup' ?
108 | `data/commander/index.html?mode=popup&width=${prefs['popup.width']}&height=${prefs['popup.height']}` :
109 | ''
110 | });
111 | });
112 | }
113 | if (ps['custom-icon']) {
114 | icon(ps['custom-icon'].newValue);
115 | }
116 | });
117 |
118 | chrome.runtime.onMessage.addListener((request, sender) => {
119 | if (request.method === 'save-size') {
120 | chrome.storage.local.set(request.prefs);
121 | }
122 | else if (request.method === 'activate') {
123 | chrome.windows.update(sender.tab.windowId, {
124 | focused: true
125 | });
126 | chrome.tabs.update(sender.tab.id, {
127 | active: true
128 | });
129 | }
130 | });
131 |
132 | /* FAQs & Feedback */
133 | {
134 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome;
135 | if (navigator.webdriver !== true) {
136 | const page = getManifest().homepage_url;
137 | const {name, version} = getManifest();
138 | onInstalled.addListener(({reason, previousVersion}) => {
139 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({
140 | 'faqs': true,
141 | 'last-update': 0
142 | }, prefs => {
143 | if (reason === 'install' || (prefs.faqs && reason === 'update')) {
144 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45;
145 | if (doUpdate && previousVersion !== version) {
146 | tabs.query({active: true, currentWindow: true}, tbs => tabs.create({
147 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason,
148 | active: reason === 'install',
149 | ...(tbs && tbs.length && {index: tbs[0].index + 1})
150 | }));
151 | storage.local.set({'last-update': Date.now()});
152 | }
153 | }
154 | }));
155 | });
156 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version);
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/v2/firefox/data/assets/bell.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/assets/bell.wav
--------------------------------------------------------------------------------
/v2/firefox/data/commander/components/directory-view.js:
--------------------------------------------------------------------------------
1 | /* global engine */
2 | class DirectoryView extends HTMLElement {
3 | constructor() {
4 | super();
5 |
6 | this.history = [];
7 | const shadow = this.attachShadow({
8 | mode: 'open'
9 | });
10 | shadow.innerHTML = `
11 |
27 |
28 | -
29 |
30 |
31 | `;
32 | this.pathView = shadow.querySelector('path-view');
33 | this.listView = shadow.querySelector('list-view');
34 | this.CountElement = shadow.getElementById('count');
35 |
36 | // events
37 | const onsubmit = e => this.emit('directory-view:submit', e.detail);
38 | this.pathView.addEventListener('submit', onsubmit);
39 | this.listView.addEventListener('submit', onsubmit);
40 | this.listView.addEventListener('selection-changed', () => this.emit('directory-view:selection-changed'));
41 | this.listView.addEventListener('drop-request', e => this.emit('directory-view:drop-request', e.detail));
42 | this.listView.addEventListener('command', e => this.emit('directory-view:command', e.detail));
43 | // focus the list-view element
44 | this.addEventListener('click', () => {
45 | this.listView.focus();
46 | });
47 | }
48 | emit(name, detail) {
49 | return this.dispatchEvent(new CustomEvent(name, {
50 | bubbles: true,
51 | detail
52 | }));
53 | }
54 | async buildPathView(id, arr) {
55 | // store path only if it is needed
56 | if (!arr) {
57 | arr = await engine.bookmarks.hierarchy(id);
58 | this.emit('directory-view:path', {
59 | id,
60 | arr
61 | });
62 | }
63 | this.arr = arr;
64 | this.pathView.build(arr);
65 | }
66 | // if update, then selected elements are persistent
67 | async buildListView(id, update = false, selectedIDs = []) {
68 | const method = update ? 'update' : 'build';
69 | try {
70 | // add openerId to empty "duplicates" queries
71 | if (id.query && id.query === 'duplicates') {
72 | let openerId = this.id();
73 | if (/Firefox/.test(navigator.userAgent)) {
74 | if (typeof openerId !== 'string' || openerId.trim() === '') {
75 | openerId = engine.bookmarks.rootID;
76 | }
77 | }
78 | else if (isNaN(openerId)) { // Chrome
79 | openerId = engine.bookmarks.rootID;
80 | }
81 | id.query += ':' + openerId;
82 | }
83 | const nodes = await engine.bookmarks.children(id);
84 | this.count = this.CountElement.textContent = nodes.length;
85 | if (this.isSearch(id)) {
86 | const length = this.history.length;
87 | nodes.unshift({
88 | title: '[..]',
89 | id: length ? this.history[length - 1] : '',
90 | openerId: id,
91 | index: -1,
92 | readonly: true
93 | });
94 | }
95 | else if (this.isRoot(id) === false) {
96 | const parent = await engine.bookmarks.parent(id);
97 | nodes.unshift({
98 | title: '[..]',
99 | id: parent.parentId,
100 | openerId: id,
101 | index: -1,
102 | readonly: true
103 | });
104 | }
105 | const origin = this.isSearch(id) ? 'search' : (
106 | this.isRoot(id) ? 'root' : 'other'
107 | );
108 |
109 | if (method === 'build') {
110 | this.listView.build(nodes, undefined, selectedIDs, {origin});
111 | }
112 | else {
113 | this.listView.update(nodes);
114 | }
115 | this.listView.mode({
116 | path: this.isSearch(id)
117 | });
118 |
119 | this.history.push(id);
120 | }
121 | catch (e) {
122 | this.listView.build(undefined, e, undefined, {origin});
123 | console.warn(e);
124 | window.setTimeout(() => this.build(''), 2000);
125 | }
126 | }
127 | async build(id, arr, selectedIDs = []) {
128 | this.emit('directory-view:update-requested');
129 |
130 | id = id || engine.bookmarks.rootID;
131 | this.buildListView(id, false, selectedIDs);
132 | this.buildPathView(id, arr).then(() => {
133 | this.emit('directory-view:content-updated');
134 | });
135 | this._id = id;
136 | }
137 | style({
138 | name = 200,
139 | added = 90,
140 | modified = 90
141 | }) {
142 | this.listView.style.setProperty('--name-width', name + 'px');
143 | this.listView.style.setProperty('--added-width', added + 'px');
144 | this.listView.style.setProperty('--modified-width', modified + 'px');
145 | }
146 | update(id) {
147 | this.buildListView(id, true);
148 | }
149 | entries(...args) {
150 | return this.listView.entries(...args);
151 | }
152 | id() {
153 | return this._id;
154 | }
155 | list() {
156 | return this.arr;
157 | }
158 | isRoot(id) {
159 | return engine.bookmarks.isRoot(id || this.id());
160 | }
161 | isSearch(id) {
162 | return engine.bookmarks.isSearch(id || this.id());
163 | }
164 | navigate(direction = 'forward') {
165 | if (direction === 'first' || direction === 'last') {
166 | this.listView[direction]();
167 | }
168 | else {
169 | this.listView[direction === 'forward' ? 'next' : 'previous']();
170 | }
171 | }
172 | owner(name) {
173 | this.setAttribute('owner', name);
174 | this.listView.setAttribute('owner', name);
175 | this.pathView.setAttribute('owner', name);
176 | }
177 | static get observedAttributes() {
178 | return ['path'];
179 | }
180 | async attributeChangedCallback(name, oldValue, newValue) {
181 | if (name === 'path') {
182 | this.build(newValue);
183 | }
184 | }
185 | }
186 | window.customElements.define('directory-view', DirectoryView);
187 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/components/directory-view/list-view.js:
--------------------------------------------------------------------------------
1 | /* global engine */
2 |
3 | class ListView extends HTMLElement {
4 | constructor() {
5 | super();
6 | const shadow = this.attachShadow({
7 | mode: 'open'
8 | });
9 | shadow.innerHTML = `
10 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
148 |
149 |
150 |
151 |
152 |
Name
153 |
Path
154 |
Link
155 |
Added
156 |
Modified
157 |
158 |
159 | `;
160 |
161 | this.template = shadow.querySelector('template');
162 | this.content = shadow.getElementById('content');
163 |
164 | this.content.addEventListener('focus', () => this.classList.add('active'));
165 | this.content.addEventListener('blur', () => {
166 | const active = this.shadowRoot.activeElement;
167 | // if document is not focused, keep the active view
168 | if (active === null) {
169 | this.classList.remove('active');
170 | }
171 | });
172 |
173 | // context menu
174 | this.content.addEventListener('contextmenu', e => {
175 | const {target} = e;
176 | if (target.classList.contains('entry') && target.dataset.index !== '-1') {
177 | e.preventDefault();
178 | this.emit('selection-changed');
179 | // is this entry selected?
180 | if (target.dataset.selected === 'false') {
181 | this.select(target);
182 | }
183 |
184 | const m = this.shadowRoot.getElementById('menu');
185 | const directory = target.dataset.type === 'DIRECTORY';
186 | m.querySelector('[data-id="open-in-new-tab"]').classList[directory ? 'add' : 'remove']('disabled');
187 | m.querySelector('[data-id="open-in-new-window"]').classList[directory ? 'add' : 'remove']('disabled');
188 | m.querySelector('[data-id="copy-link"]').classList[directory ? 'add' : 'remove']('disabled');
189 | m.querySelector('[data-id="import-tree"]').classList[
190 | this?.extra?.origin === 'search' ? 'add' : 'remove'
191 | ]('disabled');
192 | m.querySelector('[data-id="open-folder"]').classList[
193 | this?.extra?.origin === 'other' && directory === false ? 'add' : 'remove'
194 | ]('disabled');
195 |
196 | m.style.left = (e.clientX - 10) + 'px';
197 | m.style.top = (e.clientY - 10) + 'px';
198 | m.classList.remove('hidden');
199 | m.focus();
200 | }
201 | });
202 | this.shadowRoot.getElementById('menu').onblur = e => e.target.classList.add('hidden');
203 |
204 | this.config = {
205 | remote: false
206 | };
207 |
208 | shadow.addEventListener('click', e => {
209 | const {target} = e;
210 | if (target.classList.contains('entry') && target.classList.contains('hr') === false) {
211 | // single-click => toggle selection
212 | if (e.detail === 1 || e.detail === 0) {
213 | if (e.ctrlKey === false && e.metaKey === false && e.shiftKey === false) {
214 | this.items().forEach(e => e.dataset.selected = false);
215 | }
216 | // multiple select
217 | if (e.shiftKey) {
218 | const e = this.content.querySelector('.entry[data-last-selected=true]');
219 | const es = [...this.content.querySelectorAll('.entry')];
220 | if (e) {
221 | const i = es.indexOf(e);
222 | const j = es.indexOf(target);
223 |
224 | for (let k = Math.min(i, j); k < Math.max(i, j); k += 1) {
225 | es[k].dataset.selected = true;
226 | }
227 | }
228 | }
229 | // select / deselect on meta
230 | if (e.ctrlKey || e.metaKey) {
231 | target.dataset.selected = target.dataset.selected !== 'true';
232 | }
233 | else {
234 | target.dataset.selected = true;
235 | }
236 | for (const e of [...this.content.querySelectorAll('.entry[data-last-selected=true]')]) {
237 | e.dataset.lastSelected = false;
238 | }
239 | target.dataset.lastSelected = true;
240 |
241 | // scroll (only when e.isTrusted === false)
242 | if (e.isTrusted === false) {
243 | this.scroll(target);
244 | }
245 | this.emit('selection-changed');
246 | }
247 | // double-click => submit selection
248 | else {
249 | const entries = [];
250 | if (target.dataset.selected === 'true') {
251 | entries.push(...this.entries(true));
252 | }
253 | else {
254 | const entry = Object.assign({}, target.node, target.dataset);
255 | if (entry.id.startsWith('{')) {
256 | entry.id = JSON.parse(entry.id);
257 | }
258 | entries.push(entry);
259 | }
260 | this.emit('submit', {
261 | shiftKey: e.shiftKey,
262 | ctrlKey: e.ctrlKey,
263 | metaKey: e.metaKey,
264 | entries
265 | });
266 | }
267 | }
268 | else if (target.dataset.id === 'open-in-new-tab') {
269 | shadow.dispatchEvent(new KeyboardEvent('keydown', {
270 | code: 'Enter',
271 | metaKey: true
272 | }));
273 | }
274 | else if (target.dataset.id === 'open-in-new-window') {
275 | shadow.dispatchEvent(new KeyboardEvent('keydown', {
276 | code: 'Enter',
277 | shiftKey: true
278 | }));
279 | }
280 | else if (target.dataset.id === 'open-folder') {
281 | this.emit('command', {
282 | command: 'open-folder'
283 | });
284 | }
285 | else if (target.dataset.id === 'open-folder-other-pane') {
286 | this.emit('command', {
287 | command: 'mirror',
288 | altKey: true
289 | });
290 | }
291 | else if (
292 | ['copy-link', 'copy-id', 'copy-title', 'copy-details'].indexOf(target.dataset.id) !== -1 ||
293 | ['import-tree', 'export-tree'].indexOf(target.dataset.id) !== -1 ||
294 | ['trash'].indexOf(target.dataset.id) !== -1
295 | ) {
296 | if (target.dataset.id === 'trash' && confirm('Are you sure?') === false) {
297 | return;
298 | }
299 | this.emit('command', {
300 | command: target.dataset.id,
301 | shiftKey: e.shiftKey
302 | });
303 | }
304 | });
305 | // to prevent conflict with command access
306 | shadow.addEventListener('keyup', e => {
307 | if (e.code.startsWith('Key') || e.code.startsWith('Digit')) {
308 | const d = this.content.querySelector(`.entry[data-selected=true] ~ .entry[data-key="${e.key}"]`);
309 | if (d) {
310 | d.click();
311 | }
312 | else {
313 | const d = this.content.querySelector(`.entry[data-key="${e.key}"]`);
314 | if (d) {
315 | d.click();
316 | }
317 | }
318 | }
319 | else if (
320 | e.code === 'Backspace' &&
321 | e.shiftKey === false && e.altKey === false && e.metaKey === false && e.ctrlKey === false
322 | ) {
323 | const d = this.content.querySelector('.entry[data-index="-1"]');
324 | if (d) {
325 | this.dbclick(d);
326 | }
327 | else {
328 | engine.notify('beep');
329 | }
330 | }
331 | });
332 | shadow.addEventListener('keydown', e => {
333 | const meta = e.metaKey || e.ctrlKey || e.shiftKey;
334 | if (e.code === 'Enter') {
335 | const entries = this.entries();
336 | if (entries.length) {
337 | this.emit('submit', {
338 | shiftKey: e.shiftKey,
339 | ctrlKey: e.ctrlKey,
340 | metaKey: e.metaKey,
341 | entries
342 | });
343 | }
344 | }
345 | // select all
346 | else if (e.code === 'KeyA' && meta) {
347 | const [e, ...es] = [...this.content.querySelectorAll('.entry[data-readonly=false]')];
348 | this.select(e);
349 | for (const e of es) {
350 | this.select(e, true);
351 | }
352 | }
353 | });
354 | // keyboard navigation
355 | shadow.addEventListener('keydown', e => {
356 | if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
357 | e.preventDefault();
358 | const meta = e.metaKey || e.ctrlKey || e.shiftKey;
359 | const reverse = (e.metaKey && e.shiftKey) || (e.ctrlKey && e.shiftKey);
360 | this[e.key === 'ArrowUp' ? 'previous' : 'next'](meta, reverse);
361 | }
362 | });
363 | // drag and drop
364 | shadow.addEventListener('drop', e => {
365 | e.preventDefault();
366 | const j = e.dataTransfer.getData('application/bc.bookmark');
367 | try {
368 | this.emit('drop-request', {
369 | ...JSON.parse(j),
370 | destination: e.target.getRootNode().host.getAttribute('owner')
371 | });
372 | }
373 | catch (e) {}
374 | });
375 | shadow.addEventListener('dragover', e => e.preventDefault());
376 | shadow.addEventListener('dragstart', e => {
377 | const ids = [];
378 | const types = [];
379 | const selected = [];
380 | if (e.target.dataset.selected === 'true') {
381 | const es = [...this.content.querySelectorAll('.entry[data-selected=true]')];
382 | ids.push(...es.map(e => e.dataset.id));
383 | types.push(...es.map(e => e.dataset.type));
384 | selected.push(...es.map(e => e.dataset.selected));
385 | }
386 | else {
387 | ids.push(e.target.dataset.id);
388 | types.push(e.target.dataset.type);
389 | selected.push(e.target.dataset.selected);
390 | }
391 |
392 | e.dataTransfer.setData('application/bc.bookmark', JSON.stringify({
393 | ids,
394 | types,
395 | selected,
396 | source: e.target.getRootNode().host.getAttribute('owner')
397 | }));
398 | e.dataTransfer.setData('text/uri-list', e.target.querySelector('[data-id="href"]').textContent);
399 | e.dataTransfer.setData('text/plain', e.target.querySelector('[data-id="name"]').textContent);
400 | });
401 | }
402 | query(q) {
403 | return this.content.querySelector(q);
404 | }
405 | select(e, metaKey = false) {
406 | const event = document.createEvent('MouseEvent');
407 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, metaKey, false, false, metaKey, 0, null);
408 | e.dispatchEvent(event);
409 | }
410 | first(metaKey = false) {
411 | const e = this.content.querySelector('.entry[data-index]');
412 | if (e) {
413 | this.select(e, metaKey);
414 | }
415 | }
416 | last(metaKey = false) {
417 | const e = this.content.querySelector('.entry[data-index]:last-child');
418 | if (e) {
419 | this.select(e, metaKey);
420 | }
421 | }
422 | previous(metaKey = false, reverse = false) {
423 | if (reverse) {
424 | const es = this.content.querySelectorAll('.entry[data-selected=true]');
425 | if (es.length > 1) {
426 | es[0].dataset.selected = false;
427 | }
428 | }
429 | else {
430 | const e = this.content.querySelector('.entry:not(.hr) + .entry[data-selected=true]');
431 | if (e) {
432 | this.select(e.previousElementSibling, metaKey);
433 | }
434 | }
435 | }
436 | next(metaKey = false, reverse = false) {
437 | if (reverse) {
438 | const es = this.content.querySelectorAll('.entry[data-selected=true]');
439 | if (es.length > 1) {
440 | es[es.length - 1].dataset.selected = false;
441 | }
442 | }
443 | else {
444 | const e = [...this.content.querySelectorAll('.entry[data-selected=true] + .entry')].pop();
445 | if (e) {
446 | this.select(e, metaKey);
447 | }
448 | }
449 | }
450 | items(selected = true) {
451 | if (selected) {
452 | return [...this.content.querySelectorAll('.entry[data-selected=true]')];
453 | }
454 | return [...this.content.querySelectorAll('.entry[data-index]:not([data-index="-1"])')];
455 | }
456 | entries(selected = true) {
457 | return this.items(selected).map(target => {
458 | const o = Object.assign({}, target.node, target.dataset);
459 | // id is from search
460 | if (target.dataset.id.startsWith('{')) {
461 | o.id = JSON.parse(target.dataset.id);
462 | }
463 | return o;
464 | });
465 | }
466 | emit(name, detail) {
467 | return this.dispatchEvent(new CustomEvent(name, {
468 | bubbles: true,
469 | detail
470 | }));
471 | }
472 | dbclick(e) {
473 | return e.dispatchEvent(new CustomEvent('click', {
474 | detail: 2,
475 | bubbles: true
476 | }));
477 | }
478 | favicon(href) {
479 | if (typeof InstallTrigger !== 'undefined') {
480 | if (this.config.remote) {
481 | return 'http://www.google.com/s2/favicons?domain_url=' + href;
482 | }
483 | else {
484 | return '/data/commander/images/page.svg';
485 | }
486 | }
487 | return 'chrome://favicon/' + href;
488 | }
489 | date(ms) {
490 | if (ms) {
491 | return (new Date(ms)).toLocaleDateString();
492 | }
493 | return '';
494 | }
495 | clean() {
496 | [...this.content.querySelectorAll('.entry:not(.hr)')].forEach(e => e.remove());
497 | }
498 | // ids of selected elements
499 | build(nodes, err, ids = [], extra) { // extra = {origin: ['root', 'search', 'extra']}
500 | this.clean();
501 | this.extra = extra;
502 |
503 | // remove unknown ids
504 | ids = ids.filter(id => nodes.some(n => n.id === id));
505 |
506 | const f = document.createDocumentFragment();
507 | if (err) {
508 | const clone = document.importNode(this.template.content, true);
509 | clone.querySelector('[data-id="name"]').textContent = err.message;
510 | clone.querySelector('div').dataset.type = 'ERROR';
511 | f.appendChild(clone);
512 | }
513 | else {
514 | for (const node of nodes) {
515 | const clone = document.importNode(this.template.content, true);
516 | clone.querySelector('[data-id="name"]').textContent = node.title;
517 | clone.querySelector('[data-id="href"]').textContent = node.url;
518 | clone.querySelector('[data-id="path"]').textContent = node.relativePath;
519 | clone.querySelector('[data-id="added"]').textContent = this.date(node.dateAdded);
520 | clone.querySelector('[data-id="modified"]').textContent = this.date(node.dateGroupModified);
521 | const type = node.url ? 'FILE' : 'DIRECTORY';
522 | const div = clone.querySelector('div');
523 | Object.assign(div.dataset, {
524 | key: node.title ? node.title[0].toLowerCase() : '',
525 | type,
526 | index: node.index,
527 | id: typeof node.id === 'string' ? node.id : JSON.stringify(node.id),
528 | readonly: node.readonly || false
529 | });
530 | div.title = `${node.title}
531 | ${node.url}
532 | ${node.relativePath}`;
533 |
534 | if (node.readonly !== true) {
535 | div.setAttribute('draggable', 'true');
536 | }
537 | div.node = node;
538 | div.dataset.selected = ids.length ? ids.indexOf(node.id) !== -1 : node === nodes[0];
539 | if (type === 'FILE') {
540 | clone.querySelector('[data-id="icon"]').style['background-image'] = `url(${this.favicon(node.url)})`;
541 | }
542 | f.appendChild(clone);
543 | }
544 | }
545 | this.content.appendChild(f);
546 | // scroll the first selected index into the view
547 | if (ids.length) {
548 | const e = this.content.querySelector(`[data-id="${ids[0]}"`);
549 | if (e) {
550 | this.scroll(e);
551 | }
552 | }
553 | }
554 | mode(o) {
555 | this.content.dataset.path = Boolean(o.path);
556 | }
557 | // refresh the list while keeping selections
558 | update(nodes, err) {
559 | const ids = [...this.content.querySelectorAll('[data-selected="true"]')]
560 | .map(e => e.dataset.id)
561 | // make sure ids are still present
562 | .filter(id => nodes.some(n => n.id === id));
563 | return this.build(nodes, err, ids);
564 | }
565 | // content is the only focusable element
566 | focus() {
567 | this.content.focus();
568 | }
569 | scroll(target) {
570 | const hr = this.content.querySelector('.hr').getBoundingClientRect();
571 | const bounding = target.getBoundingClientRect();
572 | // do we need scroll from top
573 | if (bounding.top < hr.top + hr.height) {
574 | target.scrollIntoView({
575 | block: 'start'
576 | });
577 | this.content.scrollTop -= bounding.height;
578 | }
579 | if (bounding.bottom > hr.top + this.content.clientHeight) {
580 | target.scrollIntoView({
581 | block: 'end'
582 | });
583 | }
584 | }
585 | connectedCallback() {
586 | const hr = this.content.querySelector('div.entry.hr');
587 | const entries = [...hr.querySelectorAll('div')];
588 | entries.forEach((entry, index) => {
589 | const drag = entry.querySelector('i');
590 | if (!drag) {
591 | return;
592 | }
593 | drag.onmousedown = () => {
594 | const resize = e => {
595 | const widths = entries.map(e => e.getBoundingClientRect().width);
596 | const total = widths.reduce((p, c) => c + p, 0);
597 |
598 | widths[index] -= e.movementX;
599 | if (widths[index] < 32) {
600 | return;
601 | }
602 | for (let j = index - 1; j >= 0; j -= 1) {
603 | if (widths[j] !== 0) {
604 | widths[j] += e.movementX;
605 | if (widths[j] < 32) {
606 | return;
607 | }
608 | break;
609 | }
610 | }
611 | this.shadowRoot.getElementById('styles').textContent = `
612 | #content[data-path=${this.content.dataset.path}] div.entry {
613 | grid-template-columns: ${widths.filter(w => w).map(w => (w / total * 100) + '%').join(' ')};
614 | }
615 | `;
616 | };
617 | document.addEventListener('mousemove', resize);
618 | document.onmouseup = () => {
619 | document.removeEventListener('mousemove', resize);
620 | };
621 | };
622 | });
623 | }
624 | }
625 | window.customElements.define('list-view', ListView);
626 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/components/directory-view/path-view.js:
--------------------------------------------------------------------------------
1 | class PathView extends HTMLElement {
2 | constructor() {
3 | super();
4 | const shadow = this.attachShadow({
5 | mode: 'open'
6 | });
7 | shadow.innerHTML = `
8 |
47 |
51 | `;
52 | this.content = shadow.getElementById('content');
53 | this.content.addEventListener('click', e => {
54 | const {target} = e;
55 | if (target.id && target !== this.content) {
56 | const id = target.id.startsWith('{') ? JSON.parse(target.id) : target.id;
57 | this.dispatchEvent(new CustomEvent('submit', {
58 | detail: {
59 | entries: [{
60 | id,
61 | type: 'DIRECTORY'
62 | }]
63 | }
64 | }));
65 | }
66 | });
67 | }
68 | build(map) {
69 | this.content.textContent = '';
70 | const f = document.createDocumentFragment();
71 | map.forEach(({title, id}, i) => {
72 | const label = document.createElement('label');
73 | const span = document.createElement('span');
74 | const input = document.createElement('input');
75 | input.type = 'radio';
76 | input.name = 'group';
77 | input.id = typeof id === 'string' ? id : JSON.stringify(id);
78 | input.checked = i === map.length - 1;
79 | f.appendChild(input);
80 | label.title = span.textContent = title || '';
81 | label.appendChild(span);
82 | f.appendChild(label);
83 | label.setAttribute('for', input.id);
84 | });
85 | this.content.appendChild(f);
86 | this.content.scrollLeft = this.content.scrollWidth;
87 | }
88 | }
89 | window.customElements.define('path-view', PathView);
90 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/components/prompt-view.js:
--------------------------------------------------------------------------------
1 | class PromptView extends HTMLElement {
2 | constructor() {
3 | super();
4 | const shadow = this.attachShadow({
5 | mode: 'open'
6 | });
7 | shadow.innerHTML = `
8 |
52 |
60 | `;
61 | this.events = {};
62 | }
63 | connectedCallback() {
64 | const input = this.shadowRoot.querySelector('input[type=text]');
65 | const next = value => {
66 | this.classList.add('hidden');
67 | for (const c of this.events.blur || []) {
68 | c();
69 | }
70 | this.resolve(value);
71 | };
72 |
73 | this.shadowRoot.addEventListener('submit', e => {
74 | e.preventDefault();
75 | next(input.value);
76 | });
77 | this.addEventListener('keyup', e => {
78 | if (e.code === 'Escape') {
79 | next('');
80 | }
81 | });
82 | this.shadowRoot.querySelector('input[type=button]').addEventListener('click', () => {
83 | next('');
84 | });
85 | this.addEventListener('click', () => {
86 | input.focus();
87 | });
88 |
89 | this.addEventListener('keypress', e => e.stopPropagation());
90 | this.addEventListener('keyup', e => e.stopPropagation());
91 | this.addEventListener('keydown', e => e.stopPropagation());
92 | }
93 | ask(message, value = '') {
94 | const input = this.shadowRoot.querySelector('input[type=text]');
95 | const span = this.shadowRoot.querySelector('span');
96 | span.textContent = message;
97 |
98 | this.classList.remove('hidden');
99 | input.value = value;
100 |
101 | window.setTimeout(() => {
102 | input.focus();
103 | input.select();
104 | });
105 |
106 | return new Promise(resolve => this.resolve = resolve);
107 | }
108 | on(method, callback) {
109 | this.events[method] = this.events[method] || [];
110 | this.events[method].push(callback);
111 | }
112 | }
113 | window.customElements.define('prompt-view', PromptView);
114 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/components/tools-view.js:
--------------------------------------------------------------------------------
1 | /* global engine */
2 | class ToolsView extends HTMLElement {
3 | constructor() {
4 | super();
5 | const shadow = this.attachShadow({
6 | mode: 'open'
7 | });
8 | this.shadow = shadow;
9 | shadow.innerHTML = `
10 |
85 |
86 |
87 | Copy
88 | Title (X )
89 | Link (C)
90 | I D
91 | Edit
92 | Title
93 | L ink
94 | JSON
95 | Imp ort
96 | Export (Y)
97 | New
98 | B ookmark
99 | D irectory
100 | Move
101 | Left (←)
102 | Right (→)
103 | Tools
104 | CMD (S )
105 | Ro ot
106 | M irror
107 | S ync
108 | Delete
109 | Search (F )
110 | Sort (J )
111 |
112 | `;
113 | this.shadow.addEventListener('click', e => {
114 | const command = e.target.dataset.command;
115 | if (command === 'commands') {
116 | this.command(new KeyboardEvent('keydown', {
117 | code: 'KeyS',
118 | ctrlKey: true
119 | }));
120 | }
121 | else if (command) {
122 | this.emit('tools-view:command', {
123 | command,
124 | shiftKey: e.shiftKey
125 | });
126 | }
127 | });
128 | }
129 | emit(name, detail) {
130 | return this.dispatchEvent(new CustomEvent(name, {
131 | bubbles: true,
132 | detail
133 | }));
134 | }
135 | validate(name) {
136 | if (name === 'open-folder') {
137 | name = 'mirror';
138 | }
139 | const d = this.shadow.querySelector(`[data-command="${name}"]`);
140 | if (d) {
141 | if (d.dataset.enabled === 'false') {
142 | return 0;
143 | }
144 | else {
145 | return 1;
146 | }
147 | }
148 | // commands without buttons
149 | else if (['first', 'last'].some(s => s === name)) {
150 | return 1;
151 | }
152 | return -1;
153 | }
154 | command(e, callback = () => {}) {
155 | const meta = e.ctrlKey || e.metaKey;
156 | let command = '';
157 | if (e.key === 'Home') {
158 | command = 'first';
159 | }
160 | else if (e.key === 'End') {
161 | command = 'last';
162 | }
163 | else if (e.code === 'KeyC' && meta) {
164 | command = 'copy-link';
165 | }
166 | else if (e.code === 'KeyP' && meta) {
167 | command = 'import-tree';
168 | }
169 | else if (e.code === 'KeyY' && meta) {
170 | command = 'export-tree';
171 | }
172 | else if (e.code === 'KeyX' && meta) {
173 | command = 'copy-title';
174 | }
175 | else if (e.code === 'KeyI' && meta) {
176 | command = 'copy-id';
177 | }
178 | else if (e.code === 'KeyE' && meta) {
179 | command = 'edit-title';
180 | }
181 | else if (e.code === 'KeyL' && meta) {
182 | command = 'edit-link';
183 | }
184 | else if (e.code === 'KeyB' && meta) {
185 | command = 'new-file';
186 | }
187 | else if (e.code === 'KeyD' && meta) {
188 | command = 'new-directory';
189 | }
190 | else if (e.code === 'ArrowLeft' && meta) {
191 | command = 'move-left';
192 | }
193 | else if (e.code === 'ArrowRight' && meta) {
194 | command = 'move-right';
195 | }
196 | else if ((e.code === 'Delete' || e.code === 'Backspace') && meta) {
197 | command = 'trash';
198 | }
199 | else if (e.code === 'KeyO' && meta) {
200 | command = 'root';
201 | }
202 | else if (e.code === 'KeyM' && e.altKey && e.shiftKey) {
203 | command = 'open-folder';
204 | }
205 | else if (e.code === 'KeyM' && e.altKey) {
206 | command = 'mirror';
207 | }
208 | else if (e.code === 'KeyM' && meta) {
209 | command = 'mirror';
210 | }
211 | else if (e.code === 'KeyF' && meta) {
212 | command = 'search';
213 | }
214 | else if (e.code === 'KeyJ' && (meta || e.altKey)) {
215 | command = 'sort';
216 | }
217 | else if (e.code === 'KeyS' && meta && e.shiftKey) {
218 | command = 'sync';
219 | }
220 | // command box
221 | if (e.code === 'KeyS' && meta && e.shiftKey === false) {
222 | engine.user.ask(`Enter a Command:
223 |
224 | icon=[default|light|dark]
225 | theme=[default|dark|light]
226 | font-size=[number]px
227 | font-family=[font-name]
228 | views=[1|2]
229 | column-widths=[name]px, [added]px, [modified]px`).then(command => {
230 | if (command.startsWith('icon=')) {
231 | const path = command.replace('icon=', '') || 'default';
232 | chrome.storage.local.set({
233 | 'custom-icon': path === 'default' ? '' : path
234 | });
235 | }
236 | else if (command.startsWith('theme=')) {
237 | const path = command.replace('theme=', '') || 'default';
238 | chrome.storage.local.set({
239 | 'theme': path === 'default' ? '' : path
240 | });
241 | }
242 | else if (command.startsWith('font-size=')) {
243 | const px = /font-size=(\d+)px/.exec(command);
244 | chrome.storage.local.set({
245 | 'font-size': px && px.length ? px[1] : ''
246 | });
247 | }
248 | else if (command.startsWith('font-family=')) {
249 | chrome.storage.local.set({
250 | 'font-family': command.replace('font-family=', '')
251 | });
252 | }
253 | else if (command.startsWith('views=')) {
254 | const views = Math.min(2, Math.max(1, Number(command.replace('views=', ''))));
255 | chrome.storage.local.set({
256 | views
257 | });
258 | }
259 | else if (command.startsWith('column-widths=')) {
260 | const widths = [...command.replace('column-widths=', '').split(/\s*,\s*/).map(s => parseInt(s))].slice(0, 3);
261 | widths[0] = widths[0] ? Math.min(1000, Math.max(32, widths[0])) : 200;
262 | widths[1] = widths[1] ? Math.min(1000, Math.max(32, widths[1])) : 90;
263 | widths[2] = widths[2] ? Math.min(1000, Math.max(32, widths[2])) : 90;
264 |
265 | chrome.storage.local.set({
266 | widths: {
267 | name: widths[0],
268 | added: widths[1],
269 | modified: widths[2]
270 | }
271 | });
272 | }
273 | });
274 | e.preventDefault();
275 | }
276 | if (command) {
277 | const code = this.validate(command);
278 | if (code === 0 || code === 1) {
279 | e.preventDefault();
280 | e.stopPropagation(); // to prevent other modules from running
281 | }
282 | if (code === 1) {
283 | callback(command, e);
284 | }
285 | if (code === 0) {
286 | engine.notify('beep');
287 | }
288 | }
289 | }
290 | state(command, enabled) {
291 | const e = this.shadow.querySelector(`[data-command="${command}"]`);
292 | if (e) {
293 | e.dataset.enabled = enabled;
294 | }
295 | }
296 | }
297 | window.customElements.define('tools-view', ToolsView);
298 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/engine.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const bookmarks = {
4 | rootID: typeof InstallTrigger !== 'undefined' ? 'root________' : '0',
5 | isRoot(id) {
6 | return id === '' || id === bookmarks.rootID;
7 | },
8 | isSearch(id) {
9 | return Boolean(id.query);
10 | },
11 | parent(id) {
12 | return new Promise((resolve, reject) => {
13 | chrome.bookmarks.get(id, arr => {
14 | const lastError = chrome.runtime.lastError;
15 | if (lastError) {
16 | reject(lastError);
17 | }
18 | else {
19 | resolve(arr[0]);
20 | }
21 | });
22 | });
23 | },
24 | async hierarchy(id) {
25 | const cache = [];
26 | if (bookmarks.isSearch(id)) {
27 | let title = 'Search: ' + id.query;
28 | if (id.query.startsWith('duplicates')) {
29 | const openerId = id.query.replace('duplicates:', '') || bookmarks.rootID;
30 | title = `Duplicates for "${openerId}"`;
31 | }
32 | cache.push({
33 | title,
34 | id
35 | });
36 | }
37 | else {
38 | while (this.isRoot(id) === false) {
39 | const node = await bookmarks.parent(id);
40 | id = node.parentId;
41 | cache.unshift(node);
42 | }
43 | cache.unshift({
44 | title: '/',
45 | id: bookmarks.rootID
46 | });
47 | }
48 |
49 | return cache;
50 | },
51 | children(id) {
52 | // duplicate finder
53 | if (id.query && id.query.startsWith('duplicates')) {
54 | let openerId = id.query.replace('duplicates:', '') || bookmarks.rootID;
55 | if (/Firefox/.test(navigator.userAgent)) {
56 | if (typeof openerId !== 'string' || openerId.trim() === '') {
57 | openerId = bookmarks.rootID;
58 | }
59 | }
60 | else if (isNaN(openerId)) { // Chrome
61 | openerId = bookmarks.rootID;
62 | }
63 | return new Promise(resolve => chrome.bookmarks.getSubTree(openerId, children => {
64 | const links = {};
65 | const swipe = (root, path = '.') => {
66 | for (const node of root.children) {
67 | if ('children' in node) {
68 | swipe(node, path + '/' + (node.title || ''));
69 | }
70 | else if (node.url) {
71 | links[node.url] = links[node.url] || [];
72 | node.relativePath = path.replace('.//', '/');
73 | links[node.url].push(node);
74 | }
75 | }
76 | };
77 | swipe({
78 | children
79 | });
80 | return resolve(Object.values(links).filter(nodes => nodes.length > 1).flat());
81 | }));
82 | }
83 | else if (id.query) {
84 | return new Promise(resolve => chrome.bookmarks.search({
85 | query: id.query
86 | }, async nodes => {
87 | for (const node of nodes) {
88 | const arr = await bookmarks.hierarchy(node.id);
89 | arr.shift();
90 | arr.pop();
91 | node.relativePath = ['', ...arr, ''].map(n => n.title).join('/');
92 | }
93 | resolve(nodes);
94 | }));
95 | }
96 | return new Promise((resolve, reject) => {
97 | chrome.bookmarks.getChildren(id, nodes => {
98 | const lastError = chrome.runtime.lastError;
99 | if (lastError) {
100 | reject(lastError);
101 | }
102 | else {
103 | // You cannot use this API to add or remove entries in the root folder.
104 | if (id === '' || id === bookmarks.rootID) {
105 | nodes.forEach(n => n.readonly = true);
106 | }
107 | resolve(nodes);
108 | }
109 | });
110 | });
111 | },
112 | tree(id) {
113 | return new Promise((resolve, reject) => {
114 | chrome.bookmarks.getSubTree(id, nodes => {
115 | const lastError = chrome.runtime.lastError;
116 | if (lastError) {
117 | reject(lastError);
118 | }
119 | else {
120 | resolve(nodes);
121 | }
122 | });
123 | });
124 | },
125 | update(id, o) {
126 | return new Promise((resolve, reject) => chrome.bookmarks.update(id, o, nodes => {
127 | const lastError = chrome.runtime.lastError;
128 | if (lastError) {
129 | reject(lastError);
130 | }
131 | else {
132 | resolve(nodes);
133 | }
134 | }));
135 | },
136 | move(id, o) {
137 | return new Promise((resolve, reject) => chrome.bookmarks.move(id, o, node => {
138 | const lastError = chrome.runtime.lastError;
139 | if (lastError) {
140 | reject(lastError);
141 | }
142 | else {
143 | resolve(node);
144 | }
145 | }));
146 | },
147 | create(o) {
148 | return new Promise((resolve, reject) => chrome.bookmarks.create(o, node => {
149 | const lastError = chrome.runtime.lastError;
150 | if (lastError) {
151 | reject(lastError);
152 | }
153 | else {
154 | resolve(node);
155 | }
156 | }));
157 | },
158 | remove(id, recursive = false) {
159 | return new Promise((resolve, reject) => chrome.bookmarks[recursive ? 'removeTree' : 'remove'](id, () => {
160 | const lastError = chrome.runtime.lastError;
161 | if (lastError) {
162 | reject(lastError);
163 | }
164 | else {
165 | resolve();
166 | }
167 | }));
168 | }
169 | };
170 |
171 | const tabs = {
172 | create(o) {
173 | return new Promise(resolve => chrome.tabs.create(o, resolve));
174 | },
175 | update(id, o) {
176 | return new Promise(resolve => chrome.tabs.update(id, o, resolve));
177 | },
178 | active() {
179 | return new Promise((resolve, reject) => chrome.tabs.query({
180 | active: true,
181 | windowType: 'normal'
182 | }, tabs => tabs.length ? resolve(tabs[0]) : reject(Error('no active tab'))));
183 | }
184 | };
185 |
186 | const windows = {
187 | create(o) {
188 | return new Promise(resolve => chrome.windows.create(o, resolve));
189 | }
190 | };
191 |
192 | const storage = {
193 | get(o) {
194 | return new Promise(resolve => chrome.storage.local.get(o, resolve));
195 | },
196 | set(o) {
197 | return new Promise(resolve => chrome.storage.local.set(o, resolve));
198 | },
199 | changed(callback) {
200 | chrome.storage.onChanged.addListener(callback);
201 | }
202 | };
203 |
204 | const ue = document.querySelector('prompt-view');
205 | const user = {
206 | ask(msg, value) {
207 | return ue.ask(msg, value);
208 | },
209 | on(name, callback) {
210 | ue.on(name, callback);
211 | }
212 | };
213 |
214 | window.engine = {
215 | bookmarks,
216 | tabs,
217 | windows,
218 | storage,
219 | user,
220 | notify(e) {
221 | if (e === 'beep') {
222 | return (new Audio('/data/assets/bell.wav')).play();
223 | }
224 | chrome.notifications.create({
225 | type: 'basic',
226 | iconUrl: '/data/icons/48.png',
227 | title: chrome.runtime.getManifest().name,
228 | message: e.message || e
229 | });
230 | },
231 | clipboard: {
232 | copy(str) {
233 | return navigator.clipboard.writeText(str).catch(() => new Promise(resolve => {
234 | document.oncopy = e => {
235 | e.clipboardData.setData('text/plain', str);
236 | e.preventDefault();
237 | resolve();
238 | };
239 | document.execCommand('Copy', false, null);
240 | }));
241 | },
242 | read() {
243 | return navigator.clipboard.readText();
244 | }
245 | },
246 | download(content, name, type) {
247 | const a = document.createElement('a');
248 | const b = new Blob([content], {
249 | type
250 | });
251 | a.href = URL.createObjectURL(b);
252 | a.download = name;
253 | a.click();
254 | setTimeout(() => URL.revokeObjectURL(a.href), 1000);
255 | }
256 | };
257 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/images/directory-readonly.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/images/directory.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/images/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/images/page.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color: #3e3e3e;
3 | --bg: #eee;
4 | --bg-light: #dadada;
5 | --bg-active: #fff;
6 | --bg-header: #f5f5f5;
7 | --bg-even-row: #f5f5f5;
8 | --bg-selected-row: #c0e7ff;
9 | --bg-path: #dadada;
10 | --bg-path-active: #fff;
11 | --bg-blur: rgba(0, 0, 0, 0.6);
12 | --disabled-color: #a0a0a0;
13 | --disabled-shadow: #fcffff;
14 | --border: #cacaca;
15 | --border-alt: #e8e3e9;
16 | --selection: #8a8c8d;
17 | }
18 | :root.dark {
19 | --color: #9c9c9c;
20 | --bg: #18191b;
21 | --bg-light: #35363a;
22 | --bg-even-row: rgba(255, 255, 255, 0.05);
23 | --bg-selected-row: #0f488e;
24 | --bg-header: #35363a;
25 | --bg-active: #000;
26 | --bg-path: #202124;
27 | --bg-path-active: #000;
28 | --bg-blur: rgba(255, 255, 255, 0.05);
29 | --disabled-color: #5b5b5b;
30 | --disabled-shadow: #393b42;
31 | --border: #4f5052;
32 | --border-alt: #4f5052;
33 | --selection: #000;
34 | }
35 |
36 | body {
37 | font-size: 13px;
38 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
39 | height: 100vh;
40 | margin: 0;
41 | display: flex;
42 | flex-direction: column;
43 | background-color: var(--bg, #eee);
44 | color: var(--color, #3e3e3e);
45 | }
46 | #directories {
47 | overflow: hidden;
48 | display: grid;
49 | grid-template-columns: 1fr 1fr;
50 | flex: 1;
51 | margin: 2px;
52 | }
53 | #directories label {
54 | display: flex;
55 | overflow: hidden;
56 | }
57 | #directories input[type=radio] {
58 | display: none;
59 | }
60 | directory-view {
61 | flex: 1;
62 | overflow: hidden;
63 | }
64 | label:not(:first-child) directory-view {
65 | margin-left: 2px;
66 | }
67 | @media screen and (max-width: 600px) {
68 | #directories {
69 | grid-template-columns: 1fr;
70 | }
71 | }
72 |
73 | prompt-view {
74 | position: fixed;
75 | top: 0;
76 | left: 0;
77 | width: 100%;
78 | height: 100%;
79 | background-color: var(--bg-blur, rgba(0, 0, 0, 0.6));
80 | }
81 |
82 | #toast {
83 | padding: 10px;
84 | position: fixed;
85 | top: 10px;
86 | right: 10px;
87 | width: 300px;
88 | background-color: var(--bg, rgba(0, 0, 0, 0.6));
89 | border: solid 1px var(--border);
90 | }
91 | #toast:empty {
92 | display: none;
93 | }
94 |
95 | .hidden {
96 | display: none;
97 | }
98 |
99 | body[data-views="1"] #directories {
100 | grid-template-columns: 1fr;
101 | }
102 | body[data-views="1"] label[data-id="right"] {
103 | display: none !important;
104 | }
105 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bookmarks Commander
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/v2/firefox/data/commander/index.js:
--------------------------------------------------------------------------------
1 | /* global engine */
2 | 'use strict';
3 |
4 | const args = new URLSearchParams(location.search);
5 | if (args.has('width')) {
6 | document.documentElement.style.width = args.get('width') + 'px';
7 | }
8 | if (args.has('height')) {
9 | document.documentElement.style.height = args.get('height') + 'px';
10 | }
11 |
12 | const title = {
13 | 'directory-view-1': '...',
14 | 'directory-view-2': '...'
15 | };
16 |
17 | const toast = msg => {
18 | clearTimeout(toast.id);
19 | toast.id = setTimeout(() => {
20 | document.getElementById('toast').textContent = '';
21 | }, 2000);
22 | document.getElementById('toast').textContent = msg;
23 | };
24 |
25 | /* events */
26 | // persist last visited paths and update title
27 | document.addEventListener('directory-view:path', e => {
28 | const {detail} = e;
29 | const id = e.target.getAttribute('id');
30 | title[id] = detail.arr[detail.arr.length - 1].title;
31 |
32 | document.title = '[L] ' + title['directory-view-1'] + ' [R] ' + title['directory-view-2'];
33 | engine.storage.set({
34 | [id]: detail.id
35 | });
36 | });
37 | /* history */
38 | document.addEventListener('directory-view:path', () => {
39 | const state = {
40 | 'directory-view-1': views.left.id(),
41 | 'directory-view-1-ids': views.left.entries().map(o => o.id),
42 | 'directory-view-2': views.right.id(),
43 | 'directory-view-2-ids': views.right.entries().map(o => o.id),
44 | 'active': views.active() === views.left ? 'left' : 'right'
45 | };
46 | // do not push state if history.state is equal to the current state => state is set by history.state
47 | if (history.state) {
48 | if (
49 | JSON.stringify(history.state['directory-view-1']) === JSON.stringify(state['directory-view-1']) &&
50 | JSON.stringify(history.state['directory-view-2']) === JSON.stringify(state['directory-view-2'])
51 | ) {
52 | return;
53 | }
54 | }
55 | clearTimeout(history.id);
56 | history.id = setTimeout(() => {
57 | history[history.state ? 'pushState' : 'replaceState'](state, document.title);
58 | }, 100);
59 | });
60 | window.addEventListener('popstate', e => {
61 | if (history.state && history.state.active) {
62 | start(e.state);
63 | }
64 | });
65 | history.busy = false;
66 |
67 | // user-action
68 | document.addEventListener('directory-view:submit', e => {
69 | const {detail} = e;
70 | detail.entries.forEach(o => {
71 | if (o.type === 'DIRECTORY' && detail.entries.length === 1) {
72 | const {id, openerId} = detail.entries[0];
73 | // prevent looping
74 | if (openerId && openerId.id) {
75 | if (openerId.id.id) {
76 | e.target.build(openerId.id, undefined, [openerId.id.id]);
77 | }
78 | else {
79 | e.target.build(openerId.id, undefined, []);
80 | }
81 | }
82 | else if (openerId) {
83 | e.target.build(id, undefined, [openerId]);
84 | }
85 | else {
86 | e.target.build(id);
87 | }
88 | }
89 | else if (o.type === 'FILE') {
90 | if (detail.metaKey || detail.ctrlKey) {
91 | engine.tabs.create({
92 | url: o.url,
93 | active: false
94 | });
95 | }
96 | else if (detail.shiftKey) {
97 | engine.windows.create({
98 | url: o.url
99 | });
100 | }
101 | else {
102 | if (args.get('mode') === 'window') {
103 | engine.tabs.active().then(tab => engine.tabs.update(tab.id, {
104 | url: o.url
105 | })).catch(() => engine.tabs.create({
106 | url: o.url
107 | })).finally(() => window.close());
108 | }
109 | else {
110 | engine.tabs.update(undefined, {
111 | url: o.url
112 | });
113 | }
114 | }
115 | }
116 | });
117 | });
118 | document.addEventListener('directory-view:drop-request', async e => {
119 | const {ids, types, selected, source, destination} = e.detail;
120 | // cannot move to the root directory
121 | if (views[destination].isRoot()) {
122 | toast('Cannot move to the root directory');
123 | return;
124 | }
125 | // cannot move to a search directory
126 | if (views[destination].isSearch()) {
127 | toast('Cannot move to a search view');
128 | return;
129 | }
130 | // cannot move a directory to a child directory
131 | if (types.some(type => type === 'DIRECTORY')) {
132 | const d = views[destination].list();
133 | // if any selected directory is in the path of destination directory, prevent moving
134 | if (ids.some(id => d.some(de => de.id === id))) {
135 | toast('Cannot move to a child directory');
136 | return;
137 | }
138 | }
139 |
140 | const s = source === 'right' ? views.right : views.left;
141 | const d = destination === 'right' ? views.right : views.left;
142 | if (selected.some(s => s === 'true')) {
143 | s.navigate('previous');
144 | }
145 | for (const id of ids) {
146 | await engine.bookmarks.move(id, {
147 | parentId: d.id(),
148 | index: Number(d.entries()[0].index) + 1
149 | }).catch(engine.notify);
150 | }
151 | // update both views
152 | views.update();
153 | });
154 | document.addEventListener('directory-view:selection-changed', e => {
155 | const input = e.target.parentElement.querySelector('input[type=radio]');
156 | if (input) {
157 | input.click();
158 | }
159 | views.changed();
160 |
161 | engine.storage.set({
162 | [e.target.getAttribute('id') + '-ids']: e.target.entries().map(o => o.id)
163 | });
164 | });
165 | document.addEventListener('directory-view:content-updated', () => {
166 | views.changed();
167 | });
168 | {
169 | const commit = e => {
170 | command(e.detail.command, {
171 | shiftKey: e.detail.shiftKey,
172 | altKey: e.detail.altKey,
173 | metaKey: e.detail.metaKey,
174 | ctrlKey: e.detail.ctrlKey
175 | });
176 | views.active().click();
177 | };
178 | document.addEventListener('directory-view:command', commit);
179 | document.addEventListener('tools-view:command', commit);
180 | }
181 |
182 | engine.user.on('blur', () => views.active().click());
183 |
184 | /* views */
185 | const views = {
186 | 'parent': document.getElementById('directories'),
187 | 'left': document.getElementById('directory-view-1'),
188 | 'right': document.getElementById('directory-view-2'),
189 | active(reverse = false) {
190 | let e = document.querySelector('input:checked + directory-view');
191 | if (reverse) {
192 | e = document.querySelector('input:not(:checked) + directory-view');
193 | }
194 | return e || views.left;
195 | },
196 | changed() {
197 | const active = views.active();
198 |
199 | const direction = active === views.left ? 'LEFT' : 'RIGHT';
200 | engine.storage.set({
201 | active: active === views.left ? 'left' : 'right'
202 | });
203 | const entries = active.entries();
204 |
205 | const readonly = entries.some(o => o.readonly === 'true');
206 | const directory = entries.some(o => o.type === 'DIRECTORY');
207 | const file = entries.some(o => o.type === 'FILE');
208 |
209 | // move-left or move-right
210 | if (readonly) {
211 | toolsView.state('move-left', false);
212 | toolsView.state('move-right', false);
213 | }
214 | else {
215 | /* move-left or move-right*/
216 | for (const moveTo of ['left', 'right']) {
217 | let move = direction !== moveTo.toUpperCase();
218 | // cannot move to the root directory
219 | if (move && views[moveTo].isRoot()) {
220 | move = false;
221 | }
222 | // cannot move to a search directory
223 | if (move && views[moveTo].isSearch()) {
224 | move = false;
225 | }
226 | // cannot move a directory to a child directory
227 | if (move && directory) {
228 | const d = views[moveTo].list();
229 | // if any selected directory is in the path of destination directory, prevent moving
230 | if (entries.some(e => d.some(de => de.id === e.id))) {
231 | move = false;
232 | }
233 | }
234 | toolsView.state('move-' + moveTo, move);
235 | }
236 | }
237 | // delete
238 | toolsView.state('trash', readonly === false);
239 | // sort
240 | toolsView.state('sort', active.isRoot() === false && active.count > 1 && active.isSearch() === false);
241 | // copy-link
242 | toolsView.state('copy-link', file);
243 | // edit-link
244 | toolsView.state('edit-link', readonly === false && file && entries.length === 1);
245 | // edit-title
246 | toolsView.state('edit-title', readonly === false && entries.length === 1);
247 | // new-file
248 | toolsView.state('new-file', active.isRoot() || active.isSearch() ? false : true);
249 | // new-directory (test; create directory on [..])
250 | toolsView.state('new-directory', active.isRoot() || active.isSearch() ? false : true);
251 | // import-tree;
252 | // allow on root; does not allow on back button; does not allow on search
253 | toolsView.state('import-tree', entries.length === 1 && active.isSearch() === false && (readonly === false || active.isRoot()));
254 | // sync
255 | if (
256 | views.left.isRoot() || views.left.isSearch() ||
257 | views.right.isRoot() || views.right.isSearch() ||
258 | views.left.id() === views.right.id()
259 | ) {
260 | toolsView.state('sync', false);
261 | }
262 | else {
263 | toolsView.state('sync', true);
264 | }
265 | },
266 | update() {
267 | views.left.update(views.left.id());
268 | views.right.update(views.right.id());
269 | }
270 | };
271 | views.left.owner('left');
272 | views.right.owner('right');
273 | const toolsView = document.getElementById('tools-view');
274 |
275 | /* restore, and active a pane after DOM content is loaded */
276 | const start = state => {
277 | document.getElementById('directory-view-1').build(
278 | state['directory-view-1'],
279 | undefined,
280 | state['directory-view-1-ids'] || []
281 | );
282 | document.getElementById('directory-view-2').build(
283 | state['directory-view-2'],
284 | undefined,
285 | state['directory-view-2-ids'] || []
286 | );
287 |
288 | views[state.active].click();
289 | };
290 | document.addEventListener('DOMContentLoaded', () => engine.storage.get({
291 | 'directory-view-1': '',
292 | 'directory-view-1-ids': [],
293 | 'directory-view-2': '',
294 | 'directory-view-2-ids': [],
295 | 'active': 'left'
296 | }).then(start));
297 |
298 | /* on command */
299 | const command = async (command, e) => {
300 | const view = views.active();
301 | if (view) {
302 | const entries = view.entries();
303 | // copy-details
304 | if (command === 'copy-details') {
305 | engine.clipboard.copy(entries.map(o => [o.title, o.url, o.id].filter(a => a).join('\n')).join('\n\n'));
306 | }
307 | // copy-title
308 | else if (command === 'copy-title') {
309 | engine.clipboard.copy(entries.map(o => o.title).join('\n'));
310 | }
311 | // copy-id
312 | else if (command === 'copy-id') {
313 | engine.clipboard.copy(entries.map(o => o.id).join('\n'));
314 | }
315 | // copy-link
316 | else if (command === 'copy-link') {
317 | engine.clipboard.copy(entries.map(o => o.url).filter(a => a).join('\n'));
318 | }
319 | // import-tree
320 | else if (command === 'import-tree') {
321 | engine.clipboard.read().then(JSON.parse).then(async nodes => {
322 | if (Array.isArray(nodes) === false) {
323 | throw Error('This is not a valid JSON array');
324 | }
325 | const entry = entries[0];
326 | const step = async (node, parentId) => {
327 | const o = {
328 | parentId
329 | };
330 | if (node.index) {
331 | o.index = Number(node.index) + 1;
332 | }
333 | o.title = node.title;
334 | if (!o.title) {
335 | throw Error('Bookmark needs title');
336 | }
337 | if (node.type === 'FILE') {
338 | o.url = node.url;
339 | if (!o.url) {
340 | throw Error('Bookmark needs URL');
341 | }
342 | }
343 | const n = await engine.bookmarks.create(o);
344 | if (node.type === 'DIRECTORY') {
345 | for (const e of node.children) {
346 | await step(e, n.id);
347 | }
348 | }
349 | };
350 | let msg = 'Insert the bookmark tree after this node?';
351 | if (entry.type === 'DIRECTORY') {
352 | msg = 'Insert the bookmark tree inside this node?';
353 | }
354 | if (window.confirm(msg)) {
355 | for (const node of nodes) {
356 | if (entry.type === 'FILE') {
357 | node.index = entry.index;
358 | }
359 | await step(node, entry.type === 'FILE' ? entry.parentId : entry.id).then(() => views.update());
360 | }
361 | }
362 | }).catch(engine.notify);
363 | }
364 | // export-tree
365 | else if (command === 'export-tree') {
366 | const items = [];
367 | for (const entry of entries) {
368 | await engine.bookmarks.tree(entry.id).then(nodes => {
369 | const step = (parent, node) => {
370 | parent.title = node.title;
371 | parent.type = node.url ? 'FILE' : 'DIRECTORY';
372 | if (parent.type === 'FILE') {
373 | parent.url = node.url;
374 | }
375 | else {
376 | parent.children = [];
377 | for (const n of (node.children || [])) {
378 | const p = {};
379 | parent.children.push(p);
380 | step(p, n);
381 | }
382 | }
383 | };
384 | const root = {};
385 | step(root, nodes[0]);
386 | items.push(root);
387 | });
388 | if (e.shiftKey) {
389 | engine.download(JSON.stringify(items, undefined, ' '), 'tree.json', 'application/json');
390 | }
391 | else {
392 | engine.clipboard.copy(JSON.stringify(items, undefined, ' '));
393 | }
394 | }
395 | }
396 | // edit-title
397 | else if (command === 'edit-title' || command === 'edit-link') {
398 | const entry = entries[0];
399 | let o = {};
400 | if (command === 'edit-title') {
401 | const title = await engine.user.ask('Edit Title', entry.title);
402 | o = {title};
403 | if (title === entry.title || title === '') {
404 | return;
405 | }
406 | }
407 | else {
408 | const url = await engine.user.ask('Edit Link', entry.url);
409 | o = {url};
410 | if (url === entry.url || url === '') {
411 | return;
412 | }
413 | }
414 | engine.bookmarks.update(entry.id, o).catch(engine.notify).finally(() => {
415 | // we need to update both views
416 | views.update();
417 | });
418 | }
419 | else if (command === 'move-left' || command === 'move-right') {
420 | const s = command === 'move-left' ? views.right : views.left;
421 | const d = command === 'move-right' ? views.right : views.left;
422 | const toBeMoved = s.entries();
423 | s.navigate('previous');
424 | for (const entry of toBeMoved) {
425 | await engine.bookmarks.move(entry.id, {
426 | parentId: d.id(),
427 | index: Number(d.entries()[0].index) + 1
428 | }).catch(engine.notify);
429 | }
430 | // update both views
431 | views.update();
432 | }
433 | else if (command === 'root') {
434 | if (e.shiftKey) {
435 | engine.storage.set({
436 | [view.getAttribute('id')]: ''
437 | }).then(() => {
438 | view.build(engine.bookmarks.rootID);
439 | });
440 | }
441 | else {
442 | engine.storage.set({
443 | 'directory-view-1': '',
444 | 'directory-view-2': ''
445 | }).then(() => location.reload());
446 | }
447 | }
448 | else if (command === 'new-file' || command === 'new-directory') {
449 | const entry = entries[0];
450 | const o = {
451 | parentId: view.id(),
452 | index: Number(entry.index) + 1
453 | };
454 | if (command === 'new-file') {
455 | const title = ((await engine.user.ask('Title of New Bookmark', entry.title)) || '').trim();
456 | if (!title) {
457 | return;
458 | }
459 | o.title = title;
460 | const url = ((await engine.user.ask('URL of New Bookmark', entry.url || 'https://www.example.com')) || '').trim();
461 | if (!url) {
462 | return;
463 | }
464 | o.url = url;
465 | }
466 | else {
467 | const title = ((await engine.user.ask('Title of New Directory', entry.title)) || '').trim();
468 | if (title) {
469 | o.title = title;
470 | }
471 | else {
472 | return;
473 | }
474 | }
475 | engine.bookmarks.create(o).then(() => {
476 | // update both views
477 | views.update();
478 | }, engine.notify);
479 | }
480 | else if (command === 'trash') {
481 | view.navigate('previous');
482 | for (const entry of entries) {
483 | await engine.bookmarks.remove(entry.id).catch(e => {
484 | if (entry.type === 'DIRECTORY') {
485 | if (window.confirm(`"${entry.title}" directory is not empty. Remove anyway?`)) {
486 | engine.bookmarks.remove(entry.id, true).catch(engine.notify);
487 | }
488 | }
489 | else {
490 | engine.notify(e);
491 | }
492 | });
493 | }
494 | views.update();
495 | }
496 | else if (command === 'mirror') {
497 | const next = (...args) => {
498 | views[view === views.left ? 'right' : 'left'].build(...args);
499 | };
500 | if (e.shiftKey) {
501 | const dir = entries.filter(o => o.type === 'DIRECTORY').shift();
502 | if (dir) {
503 | return next(dir.id);
504 | }
505 | }
506 | // on search pane, entries[0].id browse the directory while view.id() browse the search
507 | next(e.altKey ? entries[0].parentId : view.id(), undefined, entries.map(o => o.id));
508 | }
509 | else if (command === 'sync') {
510 | const el = views.left.entries(false).filter(o => o.type === 'FILE');
511 | const er = views.right.entries(false).filter(o => o.type === 'FILE');
512 |
513 | const combined = [...el, ...er].map(o => ({
514 | title: o.title,
515 | url: o.url
516 | })).map(o => JSON.stringify(o)).filter((s, i, l) => s && l.indexOf(s) === i).map(JSON.parse);
517 | // sync panes
518 | for (const [view, list] of [[views.left, el], [views.right, er]]) {
519 | const selected = [];
520 | for (const o of combined) {
521 | if (list.some(e => e.title === o.title && e.url === o.url) === false) {
522 | const b = {
523 | ...o,
524 | parentId: view.id()
525 | };
526 | const node = await engine.bookmarks.create(b);
527 | selected.push(node.id);
528 | }
529 | }
530 | if (selected.length) {
531 | view.build(view.id(), undefined, selected);
532 | }
533 | }
534 | }
535 | else if (command === 'open-folder') {
536 | const next = (...args) => {
537 | views[view === views.left ? 'left' : 'right'].build(...args);
538 | };
539 | next(entries[0].parentId, undefined, entries.map(o => o.id));
540 | }
541 | else if (command === 'search') {
542 | const id = view.id();
543 | let value = '';
544 | if (e.shiftKey) {
545 | value = 'duplicates';
546 | }
547 | else if (id.query) {
548 | value = id.query;
549 | }
550 | engine.user.ask(
551 | 'Search For:\n\nUse "duplicates" keyword to find duplicated bookmarks in the current tree)',
552 | value
553 | ).then(query => {
554 | if (query) {
555 | view.build({
556 | id,
557 | query
558 | });
559 | }
560 | });
561 | }
562 | else if (command === 'sort') {
563 | const entries = view.entries(false);
564 | // sort based on
565 | const rules = (e.altKey ? await engine.user.ask(
566 | 'Sort By (link, name, date):',
567 | 'name, link'
568 | ) : 'name').split(/\s*,\s*/).filter(a => a === 'link' || a === 'name' || a === 'date');
569 | if (rules.length === 0) {
570 | return;
571 | }
572 |
573 | const sort = list => {
574 | return list.sort((a, b) => {
575 | const compare = method => {
576 | if (method === 'name') {
577 | return ('' + a.title).localeCompare(b.title + '');
578 | }
579 | else if (method === 'link') {
580 | return ('' + a.url).localeCompare(b.url + '');
581 | }
582 | else if (method === 'date') {
583 | return a.dateAdded - b.dateAdded;
584 | }
585 | return 0;
586 | };
587 | let w = 0;
588 | for (const rule of rules) {
589 | w = compare(rule);
590 | if (w !== 0) {
591 | break;
592 | }
593 | }
594 | if (e.shiftKey) {
595 | return -1 * w;
596 | }
597 | return w;
598 | });
599 | };
600 | const directories = sort(entries.filter(e => e.readonly === 'false' && e.type === 'DIRECTORY'));
601 | const files = sort(entries.filter(e => e.readonly === 'false' && e.type === 'FILE'));
602 |
603 | let index = 0;
604 | for (const directory of directories) {
605 | await engine.bookmarks.move(directory.id, {
606 | parentId: view.id(),
607 | index
608 | }).then(() => index += 1).catch(engine.notify);
609 | }
610 | for (const file of files) {
611 | await engine.bookmarks.move(file.id, {
612 | parentId: view.id(),
613 | index
614 | }).then(() => index += 1).catch(engine.notify);
615 | }
616 | // update both views
617 | views.update();
618 | }
619 | else if (command === 'first' || command === 'last') {
620 | view.navigate(command);
621 | }
622 | }
623 | };
624 |
625 | /* keyboard */
626 | document.addEventListener('keydown', e => {
627 | // toggle between views by Tab
628 | if (e.key === 'Tab') {
629 | const view = views.active(true);
630 | e.preventDefault();
631 | if (view) {
632 | return view.click();
633 | }
634 | }
635 | // move to the left view with arrow key
636 | else if (e.code === 'ArrowLeft' && e.shiftKey === false && e.ctrlKey === false && e.metaKey === false) {
637 | views.left.click();
638 | }
639 | // move to the left view with arrow key
640 | else if (e.code === 'ArrowRight' && e.shiftKey === false && e.ctrlKey === false && e.metaKey === false) {
641 | views.right.click();
642 | }
643 | // toggle between views by Ctrl + Digit
644 | else if (e.code === 'Digit1' && (e.metaKey || e.ctrlKey)) {
645 | views.left.click();
646 | e.preventDefault();
647 | }
648 | else if (e.code === 'Digit2' && (e.metaKey || e.ctrlKey)) {
649 | views.right.click();
650 | e.preventDefault();
651 | }
652 | toolsView.command(e, command);
653 | });
654 | // on active view change
655 | views.parent.addEventListener('change', () => {
656 | views.changed();
657 | });
658 |
659 | // select view on tools empty space
660 | toolsView.addEventListener('click', () => views.active().click());
661 |
662 | // remember last state
663 | if (args.get('mode') === 'window') {
664 | const resize = () => {
665 | engine.storage.set({
666 | 'window.left': window.screenX,
667 | 'window.top': window.screenY,
668 | 'window.width': window.outerWidth,
669 | 'window.height': window.outerHeight
670 | });
671 | };
672 | window.addEventListener('resize', resize);
673 | window.addEventListener('beforeunload', resize);
674 | }
675 |
676 | // styling
677 | const styling = () => engine.storage.get({
678 | 'font-size': 13,
679 | 'font-family': 'Arial, "Helvetica Neue", Helvetica, sans-serif',
680 | 'user-styles': '',
681 | 'theme': 'default',
682 | 'views': 2,
683 | 'widths': {
684 | name: 100,
685 | added: 90,
686 | modified: 90
687 | }
688 | }).then(prefs => {
689 | if (prefs.theme === 'dark') {
690 | document.documentElement.classList.add('dark');
691 | }
692 | else if (prefs.theme === 'light') {
693 | document.documentElement.classList.remove('dark');
694 | }
695 | else {
696 | if (matchMedia('(prefers-color-scheme: dark)').matches) {
697 | document.documentElement.classList.add('dark');
698 | }
699 | else {
700 | document.documentElement.classList.remove('dark');
701 | }
702 | }
703 |
704 | document.body.dataset.views = prefs.views;
705 | document.getElementById('user-styles').textContent = `
706 | body {
707 | font-size: ${prefs['font-size']}px;
708 | font-family: ${prefs['font-family']};
709 | }
710 | ${prefs['user-styles']}
711 | `;
712 | views.left.style(prefs.widths);
713 | views.right.style(prefs.widths);
714 | });
715 | styling();
716 | engine.storage.changed(ps => {
717 | if (ps['font-size'] || ps['font-family'] || ps['user-styles'] || ps['views'] || ps['widths'] || ps['theme']) {
718 | styling();
719 | }
720 | });
721 |
722 | // messaging
723 | chrome.runtime.onMessage.addListener((request, sender, response) => {
724 | if (request.method === 'instance') {
725 | response(true);
726 | chrome.runtime.sendMessage({method: 'activate'});
727 | }
728 | });
729 |
--------------------------------------------------------------------------------
/v2/firefox/data/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/128.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/16.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/19.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/256.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/32.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/38.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/48.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/512.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/64.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/dark/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/dark/128.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/light/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v2/firefox/data/icons/light/128.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/svgs/dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
50 |
51 |
--------------------------------------------------------------------------------
/v2/firefox/data/icons/svgs/default.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
50 |
51 |
--------------------------------------------------------------------------------
/v2/firefox/data/icons/svgs/light.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
50 |
51 |
--------------------------------------------------------------------------------
/v2/firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "version": "0.2.8",
4 | "name": "Bookmarks Commander",
5 | "description": "__MSG_description__",
6 | "default_locale": "en",
7 | "permissions": [
8 | "bookmarks",
9 | "contextMenus",
10 | "notifications",
11 | "storage",
12 | "clipboardRead",
13 | "clipboardWrite"
14 | ],
15 | "homepage_url": "https://add0n.com/bookmarks-commander.html",
16 | "background": {
17 | "scripts": [
18 | "background.js"
19 | ]
20 | },
21 | "icons": {
22 | "16": "data/icons/16.png",
23 | "19": "data/icons/19.png",
24 | "32": "data/icons/32.png",
25 | "38": "data/icons/38.png",
26 | "48": "data/icons/48.png",
27 | "64": "data/icons/64.png",
28 | "128": "data/icons/128.png",
29 | "256": "data/icons/256.png",
30 | "512": "data/icons/512.png"
31 | },
32 | "browser_action": {
33 | "default_icon": "data/icons/svgs/default.svg",
34 | "theme_icons": [{
35 | "light": "data/icons/svgs/light.svg",
36 | "dark": "data/icons/svgs/dark.svg",
37 | "size": 19
38 | }]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/v3/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Ein Lesezeichen-Manager mit zwei Fenstern, der Sortierung, dunkles Thema, Suche und Erkennung von Duplikaten unterstützt"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "A dual-pane Norton Commander liked bookmarks manager that supports sorting, dark theme, search, and duplicate detection"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un gestor de marcadores de doble panel que admite la clasificación, el tema oscuro, la búsqueda y la detección de duplicados"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un gestionnaire de signets à double volet qui prend en charge le tri, les thèmes sombres, la recherche et la détection des doublons"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un gestore di segnalibri a doppio pannello che supporta l'ordinamento, il tema scuro, la ricerca e il rilevamento dei duplicati"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "デュアルペインのノートンコマンダーは、ソート、ダークテーマ、検索、重複検出をサポートしています。"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/nl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Een bladwijzerbeheerder met twee deelvensters die sorteren, een donker thema, zoeken en duplicaatdetectie ondersteunt"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/pl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Dwupanelowy menedżer zakładek Norton Commander z obsługą sortowania, ciemnego motywu, wyszukiwania i wykrywania duplikatów"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Um gerenciador de marcadores de dois painéis que suporta classificação, tema escuro, busca e detecção de duplicatas."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/pt_PT/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Um gestor de marcadores de dois painéis que suporta a classificação, tema escuro, pesquisa e detecção de duplicados"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Двухпанельный менеджер закладок, поддерживающий сортировку, темную тему, поиск и обнаружение дубликатов."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "一个双窗格的诺顿指挥官喜欢的书签管理器,支持排序,暗主题,搜索和重复检测。"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/data/assets/bell.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/assets/bell.wav
--------------------------------------------------------------------------------
/v3/data/commander/commands/default.json:
--------------------------------------------------------------------------------
1 | [{
2 | "name": "first",
3 | "shortcuts": [{
4 | "keys": ["Home"],
5 | "description": "Move selection to top"
6 | }]
7 | }, {
8 | "name": "last",
9 | "shortcuts": [{
10 | "keys": ["End"],
11 | "description": "Move selection to bottom"
12 | }]
13 | }, {
14 | "name": "copy-title",
15 | "shortcuts": [{
16 | "keys": ["Ctrl + KeyX", "Command + KeyX"],
17 | "description": "Copy bookmark's title to the clipboard"
18 | }]
19 | }, {
20 | "name": "copy-link",
21 | "shortcuts": [{
22 | "keys": ["Ctrl + KeyC", "Command + KeyC"],
23 | "description": "Copy bookmark's link to the clipboard"
24 | }]
25 | }, {
26 | "name": "copy-id",
27 | "shortcuts": [{
28 | "keys": ["Ctrl + KeyI", "Command + KeyI"],
29 | "description": "Copy bookmark's internal ID to the clipboard"
30 | }]
31 | }, {
32 | "name": "duplicate",
33 | "shortcuts": [{
34 | "keys": ["Ctrl + KeyU", "Command + KeyU"],
35 | "description": "Duplicate bookmark to the other pane"
36 | }]
37 | }, {
38 | "name": "edit-title",
39 | "shortcuts": [{
40 | "keys": ["Ctrl + KeyE", "Command + KeyE"],
41 | "description": "Change bookmark's title"
42 | }]
43 | }, {
44 | "name": "edit-link",
45 | "shortcuts": [{
46 | "keys": ["Ctrl + KeyL", "Command + KeyL"],
47 | "description": "Change bookmark's link"
48 | }]
49 | }, {
50 | "name": "import-tree",
51 | "shortcuts": [{
52 | "keys": ["Ctrl + KeyP", "Command + KeyP"],
53 | "description": "Paste the current bookmark tree inside a directory or next to the current bookmark"
54 | }]
55 | }, {
56 | "name": "export-tree",
57 | "shortcuts": [{
58 | "keys": ["Ctrl + KeyY", "Command + KeyY"],
59 | "description": "Copy the selected bookmarks to the clipboard"
60 | }, {
61 | "keys": ["Ctrl + Shift + KeyY", "Command + Shift + KeyY"],
62 | "description": ["Export selected bookmarks to a JSON file"]
63 | }]
64 | }, {
65 | "name": "new-file",
66 | "shortcuts": [{
67 | "keys": ["Ctrl + KeyB", "Command + KeyB"],
68 | "description": "Create a new bookmark"
69 | }]
70 | }, {
71 | "name": "new-directory",
72 | "shortcuts": [{
73 | "keys": ["Ctrl + KeyD", "Command + KeyD"],
74 | "description": "Create a new empty directory"
75 | }]
76 | }, {
77 | "name": "move-left",
78 | "shortcuts": [{
79 | "keys": ["Ctrl + ArrowLeft", "Command + ArrowLeft"],
80 | "description": "Move selected bookmarks to the left pane"
81 | }]
82 | }, {
83 | "name": "move-right",
84 | "shortcuts": [{
85 | "keys": ["Ctrl + ArrowRight", "Command + ArrowRight"],
86 | "description": "Move the selected bookmarks to the right pane"
87 | }]
88 | }, {
89 | "name": "move-top",
90 | "shortcuts": [{
91 | "keys": ["Alt + Home"],
92 | "description": "Move to the top of the list"
93 | }]
94 | }, {
95 | "name": "move-up",
96 | "shortcuts": [{
97 | "keys": ["Alt + ArrowUp"],
98 | "description": "Move one-level up"
99 | }]
100 | }, {
101 | "name": "move-down",
102 | "shortcuts": [{
103 | "keys": ["Alt + ArrowDown"],
104 | "description": "Move one-level down"
105 | }]
106 | }, {
107 | "name": "move-bottom",
108 | "shortcuts": [{
109 | "keys": ["Alt + End"],
110 | "description": "Move to the end of the list"
111 | }]
112 | }, {
113 | "name": "commands",
114 | "shortcuts": [{
115 | "keys": ["Ctrl + KeyS", "Command + KeyS"],
116 | "description": "Open commands box"
117 | }]
118 | }, {
119 | "name": "root",
120 | "shortcuts": [{
121 | "keys": ["Ctrl + KeyO", "Command + KeyO"],
122 | "description": "Reset both panes"
123 | }, {
124 | "keys": ["Ctrl + Shift + KeyO", "Command + Shift + KeyO"],
125 | "description": "Reset only the active pane"
126 | }]
127 | }, {
128 | "name": "mirror",
129 | "shortcuts": [{
130 | "keys": ["Alt + Shift + KeyM"],
131 | "description": "Open path folder",
132 | "name": "open-folder"
133 | }, {
134 | "keys": ["Ctrl + KeyM", "Command + KeyM"],
135 | "description": "Mirror the inactive pane"
136 | }, {
137 | "keys": ["Ctrl + Shift + KeyM", "Command + Shift + KeyM"],
138 | "description": "Navigate inactive pane into the first selected Dir"
139 | }, {
140 | "keys": ["Alt + KeyM"],
141 | "description": "Open path folder in opposite pane"
142 | }]
143 | }, {
144 | "name": "sync",
145 | "shortcuts": [{
146 | "keys": ["Ctrl + Shift + KeyS", "Command + Shift + KeyS"],
147 | "description": "Sync bookmarks (not directories) of two panes"
148 | }]
149 | }, {
150 | "name": "trash",
151 | "shortcuts": [{
152 | "keys": ["Ctrl + Delete", "Ctrl + Backspace", "Command + Delete", "Command + Backspace"],
153 | "description": "Delete the active bookmarks and directories"
154 | }]
155 | }, {
156 | "name": "sort",
157 | "shortcuts": [{
158 | "keys": ["Ctrl + KeyJ", "Command + KeyJ"],
159 | "description": "Sort A-Z"
160 | }, {
161 | "keys": ["Ctrl + Shift + KeyJ", "Command + Shift + KeyJ"],
162 | "description": "Sort Z-A"
163 | }, {
164 | "keys": ["Alt + KeyJ"],
165 | "description": "Custom Sorting (A-Z)"
166 | }, {
167 | "keys": ["Alt + Shift + KeyJ"],
168 | "description": "Custom Sorting (Z-A)"
169 | }]
170 | }, {
171 | "name": "shortcuts",
172 | "shortcuts": [{
173 | "keys": ["Ctrl + KeyH", "Command + KeyH"],
174 | "description": "View shortcuts"
175 | }]
176 | }, {
177 | "name": "search",
178 | "shortcuts": [{
179 | "keys": ["Ctrl + KeyF", "Command + KeyF"],
180 | "description": "Search inside the active directory"
181 | }, {
182 | "keys": ["Ctrl + Shift + KeyF", "Command + Shift + KeyF"],
183 | "description": "Search for duplicates inside the active directory"
184 | }, {
185 | "keys": ["Escape"],
186 | "description": "Focus active pane"
187 | }]
188 | }]
189 |
--------------------------------------------------------------------------------
/v3/data/commander/commands/vim.json:
--------------------------------------------------------------------------------
1 | [{
2 | "name": "select-previous",
3 | "shortcuts": [{
4 | "keys": ["KeyJ"],
5 | "description": "Select previous"
6 | }]
7 | }, {
8 | "name": "select-next",
9 | "shortcuts": [{
10 | "keys": ["KeyK"],
11 | "description": "Select next"
12 | }]
13 | }, {
14 | "name": "open-in-new-tab",
15 | "shortcuts": [{
16 | "keys": ["KeyO", "Shift + KeyO", "Ctrl + KeyO", "Command + KeyO"],
17 | "description": "Open in new tab"
18 | }]
19 | }]
20 |
--------------------------------------------------------------------------------
/v3/data/commander/components/directory-view.js:
--------------------------------------------------------------------------------
1 | /* global engine */
2 | class DirectoryView extends HTMLElement {
3 | constructor() {
4 | super();
5 |
6 | this.history = [];
7 | const shadow = this.attachShadow({
8 | mode: 'open'
9 | });
10 | shadow.innerHTML = `
11 |
28 |
29 | -
30 |
31 |
32 | `;
33 | this.listView = shadow.querySelector('list-view');
34 | this.CountElement = shadow.getElementById('count');
35 |
36 | // events
37 | const onsubmit = e => this.emit('directory-view:submit', e.detail);
38 | this.listView.addEventListener('submit', onsubmit);
39 | this.listView.addEventListener('selection-changed', () => this.emit('directory-view:selection-changed'));
40 | this.listView.addEventListener('drop-request', e => this.emit('directory-view:drop-request', e.detail));
41 | this.listView.addEventListener('command', e => this.emit('directory-view:command', e.detail));
42 |
43 | this.pathView = shadow.querySelector('path-view');
44 | this.pathView.addEventListener('change', e => onsubmit({
45 | detail: {
46 | entries: [{
47 | id: e.target.value.id,
48 | type: 'DIRECTORY'
49 | }]
50 | }
51 | }));
52 | // focus the list-view element
53 | this.addEventListener('click', () => {
54 | this.listView.focus();
55 | });
56 | }
57 | emit(name, detail) {
58 | return this.dispatchEvent(new CustomEvent(name, {
59 | bubbles: true,
60 | detail
61 | }));
62 | }
63 | async buildPathView(id, arr) {
64 | // store path only if it is needed
65 | if (!arr) {
66 | arr = await engine.bookmarks.hierarchy(id);
67 | this.emit('directory-view:path', {
68 | id,
69 | arr
70 | });
71 | }
72 | this.arr = arr;
73 | this.pathView.build(arr);
74 | }
75 | // if update, then selected elements are persistent
76 | async buildListView(id, update = false, selectedIDs = []) {
77 | const method = update ? 'update' : 'build';
78 | try {
79 | // add openerId to empty "duplicates" queries
80 | if (id.query && id.query === 'duplicates') {
81 | let openerId = this.id();
82 | if (/Firefox/.test(navigator.userAgent)) {
83 | if (typeof openerId !== 'string' || openerId.trim() === '') {
84 | openerId = engine.bookmarks.rootID;
85 | }
86 | }
87 | else if (isNaN(openerId)) { // Chrome
88 | openerId = engine.bookmarks.rootID;
89 | }
90 | id.query += ':' + openerId;
91 | }
92 | const nodes = await engine.bookmarks.children(id);
93 | this.count = this.CountElement.textContent = nodes.length;
94 | if (this.isSearch(id)) {
95 | const length = this.history.length;
96 | nodes.unshift({
97 | title: '[..]',
98 | id: length ? this.history[length - 1] : '',
99 | openerId: id,
100 | index: -1,
101 | readonly: true
102 | });
103 | }
104 | else if (this.isRoot(id) === false) {
105 | const parent = await engine.bookmarks.parent(id);
106 | nodes.unshift({
107 | title: '[..]',
108 | id: parent.parentId,
109 | openerId: id,
110 | index: -1,
111 | readonly: true
112 | });
113 | }
114 | const origin = this.isSearch(id) ? 'search' : (
115 | this.isRoot(id) ? 'root' : 'other'
116 | );
117 |
118 | if (method === 'build') {
119 | this.listView.build(nodes, undefined, selectedIDs, {origin});
120 | }
121 | else {
122 | this.listView.update(nodes);
123 | }
124 | this.listView.mode({
125 | path: this.isSearch(id)
126 | });
127 |
128 | this.history.push(id);
129 | }
130 | catch (e) {
131 | this.listView.build(undefined, e, undefined, {origin});
132 | console.warn(e);
133 | window.setTimeout(() => this.build(''), 2000);
134 | }
135 | }
136 | build(id, arr, selectedIDs = []) {
137 | this.emit('directory-view:update-requested');
138 |
139 | id = id || engine.bookmarks.rootID;
140 | Promise.all([
141 | this.buildListView(id, false, selectedIDs),
142 | this.buildPathView(id, arr)
143 | ]).then(() => {
144 | this.emit('directory-view:content-updated');
145 | });
146 | this._id = id;
147 | }
148 | style({
149 | name = 200,
150 | added = 90,
151 | modified = 90
152 | }) {
153 | this.listView.style.setProperty('--name-width', name + 'px');
154 | this.listView.style.setProperty('--added-width', added + 'px');
155 | this.listView.style.setProperty('--modified-width', modified + 'px');
156 | }
157 | update(id) {
158 | this.buildListView(id, true).then(() => {
159 | this.emit('directory-view:content-updated');
160 | });
161 | }
162 | entries(...args) {
163 | return this.listView.entries(...args);
164 | }
165 | id() {
166 | return this._id;
167 | }
168 | list() {
169 | return this.arr;
170 | }
171 | isRoot(id) {
172 | return engine.bookmarks.isRoot(id || this.id());
173 | }
174 | isSearch(id) {
175 | return engine.bookmarks.isSearch(id || this.id());
176 | }
177 | navigate(direction = 'forward') {
178 | if (direction === 'first' || direction === 'last') {
179 | this.listView[direction]();
180 | }
181 | else {
182 | this.listView[direction === 'forward' ? 'next' : 'previous']();
183 | }
184 | }
185 | simulate(e) {
186 | this.listView.simulate(e);
187 | }
188 | state(command, enabled) {
189 | this.listView.state(command, enabled);
190 | }
191 | owner(name) {
192 | this.setAttribute('owner', name);
193 | this.listView.setAttribute('owner', name);
194 | this.pathView.setAttribute('owner', name);
195 | }
196 | static get observedAttributes() {
197 | return ['path'];
198 | }
199 | attributeChangedCallback(name, oldValue, newValue) {
200 | if (name === 'path') {
201 | this.build(newValue);
202 | }
203 | }
204 | }
205 | window.customElements.define('directory-view', DirectoryView);
206 |
--------------------------------------------------------------------------------
/v3/data/commander/components/directory-view/path-view-test/index.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/commander/components/directory-view/path-view-test/index.css
--------------------------------------------------------------------------------
/v3/data/commander/components/directory-view/path-view-test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | one
11 |
12 |
13 |
14 | two
15 |
16 |
17 |
18 | three
19 |
20 | !!!
21 |
22 |
23 |
24 | four
25 |
26 | !!!
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/v3/data/commander/components/directory-view/path-view-test/index.js:
--------------------------------------------------------------------------------
1 | {
2 | const view = document.getElementById('one');
3 | view.build([
4 | {title: 'first'},
5 | {title: 'two'},
6 | {title: 'three'},
7 | {title: '1 / This is a Long Text', checked: true},
8 | {title: '2 / This is a Long Text', checked: true},
9 | {title: '3 / This is a Long Text'},
10 | {title: '4 / This is a Long Text'},
11 | {title: '5 / This is a Long Text'},
12 | {title: '6 / This is a Long Text'},
13 | {title: '7 / This is a Long Text'},
14 | {title: 'This is a Long Text'},
15 | {title: 'last'}
16 | ]);
17 | }
18 | {
19 | const view = document.getElementById('two');
20 | view.build();
21 | }
22 | {
23 | const view = document.getElementById('three');
24 | view.build([
25 | {title: 'item █'},
26 | {title: 'item ⧉'},
27 | {title: 'item ╬'},
28 | {title: 'item ⧅'}
29 | ]);
30 | }
31 | {
32 | const view = document.getElementById('four');
33 | view.build([
34 | {title: 'one', id: {
35 | a: 1,
36 | b: 2
37 | }},
38 | {title: 'two', id: 2},
39 | {title: 'three', id: 3}
40 | ]);
41 | const update = () => document.getElementById('selected').textContent = JSON.stringify(view.value);
42 | view.addEventListener('change', update);
43 | update();
44 | }
45 |
--------------------------------------------------------------------------------
/v3/data/commander/components/directory-view/path-view.js:
--------------------------------------------------------------------------------
1 | class PathView extends HTMLElement {
2 | constructor() {
3 | super();
4 | const shadow = this.attachShadow({
5 | mode: 'open'
6 | });
7 | shadow.innerHTML = `
8 |
64 |
68 | `;
69 | this.content = shadow.getElementById('content');
70 | this.entries = new Map();
71 | }
72 | connectedCallback() {
73 | this.content.addEventListener('change', e => {
74 | if (this.entries.has(e.target)) {
75 | this.dispatchEvent(new Event('change'));
76 | }
77 | });
78 | }
79 | build(map = [{title: 'empty'}]) {
80 | this.content.textContent = '';
81 | const f = document.createDocumentFragment();
82 |
83 | map.forEach(o => {
84 | const {title, id = 'pve-' + Math.random(), checked = false} = o;
85 | const label = document.createElement('label');
86 | const span = document.createElement('span');
87 | const input = document.createElement('input');
88 | input.type = 'radio';
89 | input.name = 'group';
90 | input.id = typeof id === 'string' ? id : JSON.stringify(id);
91 | input.checked = checked;
92 | f.appendChild(input);
93 | label.title = span.textContent = title || '';
94 | label.appendChild(span);
95 | f.appendChild(label);
96 | label.setAttribute('for', input.id);
97 |
98 | this.entries.set(input, o);
99 | });
100 |
101 | // check
102 | if (!f.querySelector('input:checked')) {
103 | f.querySelector('input:last-of-type').checked = true;
104 | }
105 |
106 | this.content.appendChild(f);
107 |
108 | this.content.scrollLeft = this.content.scrollWidth;
109 | }
110 | get value() {
111 | const e = this.content.querySelector('input:checked');
112 | console.log(e);
113 | return this.entries.get(e);
114 | }
115 | }
116 | window.customElements.define('path-view', PathView);
117 |
--------------------------------------------------------------------------------
/v3/data/commander/components/notify-view.js:
--------------------------------------------------------------------------------
1 | class NotifyView extends HTMLElement {
2 | constructor() {
3 | super();
4 | const shadow = this.attachShadow({
5 | mode: 'open'
6 | });
7 | shadow.innerHTML = `
8 |
24 |
25 |
26 |
27 | `;
28 | }
29 | notify(message, timeout = 5000) {
30 | this.timeout = setTimeout(() => {
31 | this.classList.add('hidden');
32 | }, timeout);
33 | this.shadowRoot.querySelector('span').textContent = message;
34 | this.classList.remove('hidden');
35 | }
36 | }
37 | window.customElements.define('notify-view', NotifyView);
38 |
--------------------------------------------------------------------------------
/v3/data/commander/components/prompt-view.js:
--------------------------------------------------------------------------------
1 | class PromptView extends HTMLElement {
2 | constructor() {
3 | super();
4 | const shadow = this.attachShadow({
5 | mode: 'open'
6 | });
7 | shadow.innerHTML = `
8 |
56 |
57 |
66 |
67 | `;
68 | this.events = {};
69 | }
70 | connectedCallback() {
71 | this.shadowRoot.querySelector('input[type=button]').addEventListener('click', () => {
72 | this.shadowRoot.querySelector('dialog').close();
73 | });
74 | this.addEventListener('keypress', e => e.stopPropagation());
75 | this.addEventListener('keyup', e => e.stopPropagation());
76 | this.addEventListener('keydown', e => e.stopPropagation());
77 | }
78 | ask(message, value = '', history = []) {
79 | const dialog = this.shadowRoot.querySelector('dialog');
80 | const form = this.shadowRoot.querySelector('form');
81 | const input = this.shadowRoot.querySelector('input[type=text]');
82 | const span = this.shadowRoot.querySelector('span');
83 | const list = this.shadowRoot.getElementById('list');
84 | span.textContent = message;
85 |
86 | input.value = value;
87 | list.textContent = '';
88 |
89 | for (const s of history) {
90 | const option = document.createElement('option');
91 | option.value = s;
92 | option.textContent = s;
93 | list.appendChild(option);
94 | }
95 |
96 | return new Promise(resolve => {
97 | const next = value => {
98 | dialog.close();
99 | for (const c of this.events.blur || []) {
100 | c();
101 | }
102 | setTimeout(() => resolve(value), 100);
103 | };
104 | form.onsubmit = e => {
105 | e.preventDefault();
106 | next(input.value);
107 | };
108 | dialog.onclose = () => next('');
109 | dialog.showModal();
110 |
111 | window.setTimeout(() => {
112 | input.focus();
113 | input.select();
114 | });
115 | });
116 | }
117 | on(method, callback) {
118 | this.events[method] = this.events[method] || [];
119 | this.events[method].push(callback);
120 | }
121 | }
122 | window.customElements.define('prompt-view', PromptView);
123 |
--------------------------------------------------------------------------------
/v3/data/commander/components/tools-view.js:
--------------------------------------------------------------------------------
1 | /* global engine */
2 | class ToolsView extends HTMLElement {
3 | constructor() {
4 | super();
5 | const shadow = this.attachShadow({
6 | mode: 'open'
7 | });
8 | this.shadow = shadow;
9 | shadow.innerHTML = `
10 |
109 |
110 |
111 | Copy
112 | Title (X )
113 | Link (C)
114 | I D
115 | Du p
116 | Edit
117 | Title
118 | L ink
119 | JSON
120 | Imp ort
121 | Export (Y)
122 | New
123 | B ookmark
124 | D irectory
125 | Focus
126 | Left (←)
127 | Right (→)
128 | Move
129 | Top
130 | Up
131 | Down
132 | Last
133 | Tools
134 | CMD (S )
135 | Ro ot
136 | M irror
137 | S ync
138 | Delete
139 | Sort (J )
140 | H elp
141 |
142 |
143 | `;
144 | this.commands = [];
145 |
146 | this.shadow.getElementById('search').addEventListener('keyup', e => {
147 | if (e.key === 'Enter') {
148 | this.emit('tools-view:command', {
149 | command: 'search',
150 | query: e.target.value
151 | });
152 | }
153 | });
154 | this.shadow.getElementById('search').addEventListener('keydown', e => {
155 | if (e.key === 'Escape') {
156 | this.emit('tools-view:blur');
157 | }
158 | });
159 | this.shadow.addEventListener('click', e => {
160 | const command = e.target?.dataset?.command;
161 | if (command === 'commands') {
162 | this.command(new KeyboardEvent('keydown', {
163 | code: 'KeyS',
164 | ctrlKey: true
165 | }));
166 | }
167 | else if (command) {
168 | this.emit('tools-view:command', {
169 | command,
170 | shiftKey: e.shiftKey
171 | });
172 | }
173 | // allow the search box to get focused
174 | else if (e.target.id === 'search') {
175 | e.stopPropagation();
176 | }
177 | });
178 | }
179 | emit(name, detail = {}) {
180 | return this.dispatchEvent(new CustomEvent(name, {
181 | bubbles: true,
182 | detail
183 | }));
184 | }
185 | validate(name) {
186 | if (name === 'ignore') {
187 | return -1;
188 | }
189 | // since the command does not exist, map it to sometime close to pass validation
190 | if (name === 'open-in-new-tab') {
191 | name = 'copy-link';
192 | }
193 | else if (name === 'open-folder') {
194 | name = 'mirror';
195 | }
196 | else if (name === 'select-previous' || name === 'select-next') {
197 | return 1;
198 | }
199 | const d = this.shadow.querySelector(`[data-command="${name}"]`);
200 | if (d) {
201 | if (d.dataset.enabled === 'false') {
202 | return 0;
203 | }
204 | else {
205 | return 1;
206 | }
207 | }
208 | // commands without buttons
209 | else if (['first', 'last'].some(s => s === name)) {
210 | return 1;
211 | }
212 | return -1;
213 | }
214 | command(e, callback = () => {}) {
215 | const parts = [];
216 | if (e.altKey) {
217 | parts.push('Alt');
218 | }
219 | if (e.ctrlKey) {
220 | parts.push('Ctrl');
221 | }
222 | else if (e.metaKey) {
223 | parts.push('Command');
224 | }
225 | if (e.shiftKey) {
226 | parts.push('Shift');
227 | }
228 | parts.push(e.code);
229 | const cmd = parts.join(' + ');
230 | let command = (() => {
231 | for (const {name, shortcuts} of this.commands) {
232 | for (const o of shortcuts) {
233 | for (const key of o.keys) {
234 | if (cmd === key) {
235 | return o.name || name;
236 | }
237 | }
238 | }
239 | }
240 | return '';
241 | })();
242 |
243 | const meta = e.ctrlKey || e.metaKey;
244 |
245 | if (e.code === 'KeyF' && meta) {
246 | command = 'ignore';
247 | const o = this.shadow.getElementById('search');
248 |
249 | if (e.shiftKey) {
250 | o.value = 'duplicates';
251 | }
252 | o.focus();
253 | o.select();
254 | }
255 | // command box
256 | if (e.code === 'KeyS' && meta && e.shiftKey === false) {
257 | engine.user.ask(`Enter a Command:
258 |
259 | icon=[default|light|dark]
260 | theme=[default|dark|light]
261 | font-size=[number]px
262 | font-family=[font-name]
263 | views=[1|2]
264 | column-widths=[name]px, [added]px, [modified]px
265 | ask-before-delete=[true|false]
266 | ask-before-directory-delete=[true|false]
267 | commands-mapping=[default|vim] (vim mapping is not ready)`, '', [
268 | 'icon=default',
269 | 'icon=light',
270 | 'icon=dark',
271 | 'theme=default',
272 | 'theme=dark',
273 | 'theme=light',
274 | 'font-family=',
275 | 'views=1',
276 | 'views=2',
277 | 'column-widths=',
278 | 'ask-before-delete=true',
279 | 'ask-before-delete=false',
280 | 'ask-before-directory-delete=true',
281 | 'ask-before-directory-delete=false',
282 | 'commands-mapping=default',
283 | 'commands-mapping=vim'
284 | ]).then(command => {
285 | if (command.startsWith('icon=')) {
286 | const path = command.replace('icon=', '') || 'default';
287 | engine.storage.set({
288 | 'custom-icon': path === 'default' ? '' : path
289 | });
290 | }
291 | else if (command.startsWith('theme=')) {
292 | const path = command.replace('theme=', '') || 'default';
293 | engine.storage.set({
294 | 'theme': path === 'default' ? '' : path
295 | });
296 | }
297 | else if (command.startsWith('font-size=')) {
298 | const px = /font-size=(\d+)px/.exec(command);
299 | engine.storage.set({
300 | 'font-size': px && px.length ? px[1] : ''
301 | });
302 | }
303 | else if (command.startsWith('font-family=')) {
304 | engine.storage.set({
305 | 'font-family': command.replace('font-family=', '')
306 | });
307 | }
308 | else if (command.startsWith('commands-mapping=')) {
309 | engine.storage.set({
310 | 'commands-mapping': command.replace('commands-mapping=', '')
311 | }).then(() => location.reload());
312 | }
313 | else if (command.startsWith('ask-before-delete=')) {
314 | engine.storage.set({
315 | 'ask-before-delete': command.replace('ask-before-delete=', '') === 'false' ? false : true
316 | });
317 | }
318 | else if (command.startsWith('ask-before-directory-delete=')) {
319 | engine.storage.set({
320 | 'ask-before-directory-delete': command.replace('ask-before-directory-delete=', '') === 'false' ? false : true
321 | });
322 | }
323 | else if (command.startsWith('views=')) {
324 | const views = Math.min(2, Math.max(1, Number(command.replace('views=', ''))));
325 | engine.storage.set({
326 | views
327 | });
328 | }
329 | else if (command.startsWith('column-widths=')) {
330 | const widths = [...command.replace('column-widths=', '').split(/\s*,\s*/).map(s => parseInt(s))].slice(0, 3);
331 | widths[0] = widths[0] ? Math.min(1000, Math.max(32, widths[0])) : 200;
332 | widths[1] = widths[1] ? Math.min(1000, Math.max(32, widths[1])) : 90;
333 | widths[2] = widths[2] ? Math.min(1000, Math.max(32, widths[2])) : 90;
334 |
335 | engine.storage.set({
336 | widths: {
337 | name: widths[0],
338 | added: widths[1],
339 | modified: widths[2]
340 | }
341 | });
342 | }
343 | });
344 | e.preventDefault();
345 | }
346 | if (command) {
347 | const code = this.validate(command);
348 | if (code === 0 || code === 1 || code === -1) {
349 | e.preventDefault();
350 | e.stopPropagation(); // to prevent other modules from running
351 | }
352 | if (code === 1) {
353 | callback(command, e);
354 | }
355 | if (code === 0) {
356 | engine.notify('beep');
357 | }
358 | }
359 | }
360 | state(command, enabled) {
361 | const e = this.shadow.querySelector(`[data-command="${command}"]`);
362 | if (e) {
363 | e.dataset.enabled = enabled;
364 | }
365 | }
366 | load(es) {
367 | this.commands = es;
368 |
369 | for (const {name, shortcuts} of es) {
370 | const e = this.shadow.querySelector(`[data-command=${name}]`) || this.shadow.getElementById(name);
371 | if (e) {
372 | e.title = shortcuts.map(({keys, description}) => {
373 | return keys.join(' or ') + ' ➝ ' + description;
374 | }).join('\n\n');
375 | }
376 | }
377 | }
378 | }
379 | window.customElements.define('tools-view', ToolsView);
380 |
--------------------------------------------------------------------------------
/v3/data/commander/engine.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const bookmarks = {
4 | rootID: typeof InstallTrigger !== 'undefined' ? 'root________' : '0',
5 | isRoot(id) {
6 | return id === '' || id === bookmarks.rootID;
7 | },
8 | isSearch(id) {
9 | return Boolean(id.query);
10 | },
11 | parent(id) {
12 | return new Promise((resolve, reject) => {
13 | chrome.bookmarks.get(id, arr => {
14 | const lastError = chrome.runtime.lastError;
15 | if (lastError) {
16 | reject(lastError);
17 | }
18 | else {
19 | resolve(arr[0]);
20 | }
21 | });
22 | });
23 | },
24 | async hierarchy(id) {
25 | const cache = [];
26 | if (bookmarks.isSearch(id)) {
27 | let title = 'Search: ' + id.query;
28 | if (id.query.startsWith('duplicates')) {
29 | const openerId = id.query.replace('duplicates:', '') || bookmarks.rootID;
30 | title = `Duplicates for "${openerId}"`;
31 | }
32 | cache.push({
33 | title,
34 | id
35 | });
36 | }
37 | else {
38 | while (this.isRoot(id) === false) {
39 | const node = await bookmarks.parent(id);
40 | id = node.parentId;
41 | cache.unshift(node);
42 | }
43 | cache.unshift({
44 | title: '/',
45 | id: bookmarks.rootID
46 | });
47 | }
48 |
49 | return cache;
50 | },
51 | children(id) {
52 | // duplicate finder
53 | if (id.query && id.query.startsWith('duplicates')) {
54 | let openerId = id.query.replace('duplicates:', '') || bookmarks.rootID;
55 | if (/Firefox/.test(navigator.userAgent)) {
56 | if (typeof openerId !== 'string' || openerId.trim() === '') {
57 | openerId = bookmarks.rootID;
58 | }
59 | }
60 | else if (isNaN(openerId)) { // Chrome
61 | openerId = bookmarks.rootID;
62 | }
63 | return new Promise(resolve => chrome.bookmarks.getSubTree(openerId, children => {
64 | const links = {};
65 | const swipe = (root, path = '.') => {
66 | for (const node of root.children) {
67 | if ('children' in node) {
68 | swipe(node, path + '/' + (node.title || ''));
69 | }
70 | else if (node.url) {
71 | links[node.url] = links[node.url] || [];
72 | node.relativePath = path.replace('.//', '/');
73 | links[node.url].push(node);
74 | }
75 | }
76 | };
77 | swipe({
78 | children
79 | });
80 | return resolve(Object.values(links).filter(nodes => nodes.length > 1).flat());
81 | }));
82 | }
83 | else if (id.query) {
84 | return new Promise(resolve => chrome.bookmarks.search({
85 | query: id.query
86 | }, async nodes => {
87 | for (const node of nodes) {
88 | const arr = await bookmarks.hierarchy(node.id);
89 | arr.shift();
90 | arr.pop();
91 | node.relativePath = ['', ...arr, ''].map(n => n.title).join('/');
92 | }
93 | resolve(nodes);
94 | }));
95 | }
96 | return new Promise((resolve, reject) => {
97 | chrome.bookmarks.getChildren(id, nodes => {
98 | const lastError = chrome.runtime.lastError;
99 | if (lastError) {
100 | reject(lastError);
101 | }
102 | else {
103 | // You cannot use this API to add or remove entries in the root folder.
104 | if (id === '' || id === bookmarks.rootID) {
105 | nodes.forEach(n => n.readonly = true);
106 | }
107 | resolve(nodes);
108 | }
109 | });
110 | });
111 | },
112 | tree(id) {
113 | return new Promise((resolve, reject) => {
114 | chrome.bookmarks.getSubTree(id, nodes => {
115 | const lastError = chrome.runtime.lastError;
116 | if (lastError) {
117 | reject(lastError);
118 | }
119 | else {
120 | resolve(nodes);
121 | }
122 | });
123 | });
124 | },
125 | update(id, o) {
126 | return new Promise((resolve, reject) => chrome.bookmarks.update(id, o, nodes => {
127 | const lastError = chrome.runtime.lastError;
128 | if (lastError) {
129 | reject(lastError);
130 | }
131 | else {
132 | resolve(nodes);
133 | }
134 | }));
135 | },
136 | move(id, o) {
137 | return new Promise((resolve, reject) => chrome.bookmarks.move(id, o, node => {
138 | const lastError = chrome.runtime.lastError;
139 | if (lastError) {
140 | reject(lastError);
141 | }
142 | else {
143 | resolve(node);
144 | }
145 | }));
146 | },
147 | create(o) {
148 | return new Promise((resolve, reject) => chrome.bookmarks.create(o, node => {
149 | const lastError = chrome.runtime.lastError;
150 | if (lastError) {
151 | reject(lastError);
152 | }
153 | else {
154 | resolve(node);
155 | }
156 | }));
157 | },
158 | remove(id, recursive = false) {
159 | return new Promise((resolve, reject) => chrome.bookmarks[recursive ? 'removeTree' : 'remove'](id, () => {
160 | const lastError = chrome.runtime.lastError;
161 | if (lastError) {
162 | reject(lastError);
163 | }
164 | else {
165 | resolve();
166 | }
167 | }));
168 | }
169 | };
170 |
171 | const tabs = {
172 | create(o) {
173 | return new Promise(resolve => chrome.tabs.create(o, resolve));
174 | },
175 | update(id, o) {
176 | return new Promise(resolve => chrome.tabs.update(id, o, resolve));
177 | },
178 | active() {
179 | return new Promise((resolve, reject) => chrome.tabs.query({
180 | active: true,
181 | windowType: 'normal'
182 | }, tabs => tabs.length ? resolve(tabs[0]) : reject(Error('no active tab'))));
183 | }
184 | };
185 |
186 | const windows = {
187 | create(o) {
188 | return new Promise(resolve => chrome.windows.create(o, resolve));
189 | }
190 | };
191 |
192 | const storage = {
193 | get(o) {
194 | return new Promise(resolve => chrome.storage.local.get(o, resolve));
195 | },
196 | set(o) {
197 | return new Promise(resolve => chrome.storage.local.set(o, resolve));
198 | },
199 | changed(callback) {
200 | chrome.storage.onChanged.addListener(callback);
201 | }
202 | };
203 |
204 | const ue = document.querySelector('prompt-view');
205 | const user = {
206 | ask(msg, value, history = []) {
207 | return ue.ask(msg, value, history);
208 | },
209 | on(name, callback) {
210 | ue.on(name, callback);
211 | }
212 | };
213 |
214 | window.engine = {
215 | bookmarks,
216 | tabs,
217 | windows,
218 | storage,
219 | user,
220 | notify(e) {
221 | if (e === 'beep') {
222 | return (new Audio('/data/assets/bell.wav')).play();
223 | }
224 | console.warn(e);
225 | document.querySelector('notify-view').notify(e.message || e);
226 | },
227 | clipboard: {
228 | copy(str) {
229 | return navigator.clipboard.writeText(str).catch(() => new Promise(resolve => {
230 | document.oncopy = e => {
231 | e.clipboardData.setData('text/plain', str);
232 | e.preventDefault();
233 | resolve();
234 | };
235 | document.execCommand('Copy', false, null);
236 | }));
237 | },
238 | read() {
239 | return navigator.clipboard.readText();
240 | }
241 | },
242 | download(content, name, type) {
243 | const a = document.createElement('a');
244 | const b = new Blob([content], {
245 | type
246 | });
247 | a.href = URL.createObjectURL(b);
248 | a.download = name;
249 | a.click();
250 | setTimeout(() => URL.revokeObjectURL(a.href), 1000);
251 | }
252 | };
253 |
--------------------------------------------------------------------------------
/v3/data/commander/images/directory-readonly.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/v3/data/commander/images/directory.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v3/data/commander/images/drop-after.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/v3/data/commander/images/drop-inside.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/v3/data/commander/images/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/v3/data/commander/images/page.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v3/data/commander/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color: #3e3e3e;
3 | --bg: #eee;
4 | --bg-light: #dadada;
5 | --bg-active: #fff;
6 | --bg-header: #f5f5f5;
7 | --bg-even-row: #f5f5f5;
8 | --bg-selected-row: #c0e7ff;
9 | --bg-indicator: #000;
10 | --bg-path: #dadada;
11 | --bg-path-active: #fff;
12 | --bg-blur: rgba(0, 0, 0, 0.6);
13 | --disabled-color: #a0a0a0;
14 | --disabled-shadow: #fcffff;
15 | --border: #cacaca;
16 | --border-alt: #e8e3e9;
17 | --selection: #8a8c8d;
18 | --bg-command: rgba(0, 0, 0, 0.15);
19 | }
20 | :root.dark {
21 | --color: #9c9c9c;
22 | --bg: #18191b;
23 | --bg-light: #35363a;
24 | --bg-even-row: rgba(255, 255, 255, 0.05);
25 | --bg-selected-row: #0f488e;
26 | --bg-indicator: #9c9c9c;
27 | --bg-header: #35363a;
28 | --bg-active: #000;
29 | --bg-path: #202124;
30 | --bg-path-active: #000;
31 | --bg-blur: rgba(255, 255, 255, 0.05);
32 | --disabled-color: #5b5b5b;
33 | --disabled-shadow: #393b42;
34 | --border: #4f5052;
35 | --border-alt: #4f5052;
36 | --selection: #000;
37 | --bg-command: rgba(255, 255, 255, 0.25);
38 | }
39 |
40 | body {
41 | font-size: 13px;
42 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
43 | height: 100vh;
44 | margin: 0;
45 | display: flex;
46 | flex-direction: column;
47 | background-color: var(--bg, #eee);
48 | color: var(--color, #3e3e3e);
49 | }
50 | #directories {
51 | overflow: hidden;
52 | display: grid;
53 | grid-template-columns: 1fr 1fr;
54 | flex: 1;
55 | margin: 2px;
56 | }
57 | #directories label {
58 | display: flex;
59 | overflow: hidden;
60 | }
61 | #directories input[type=radio] {
62 | display: none;
63 | }
64 | directory-view {
65 | flex: 1;
66 | overflow: hidden;
67 | }
68 | label:not(:first-child) directory-view {
69 | margin-left: 2px;
70 | }
71 | @media screen and (max-width: 600px) {
72 | #directories {
73 | grid-template-columns: 1fr;
74 | }
75 | }
76 |
77 | prompt-view {
78 | background-color: var(--bg-blur, rgba(0, 0, 0, 0.6));
79 | }
80 |
81 | #toast {
82 | padding: 10px;
83 | position: fixed;
84 | top: 10px;
85 | right: 10px;
86 | width: 300px;
87 | background-color: var(--bg, rgba(0, 0, 0, 0.6));
88 | border: solid 1px var(--border);
89 | }
90 | #toast:empty {
91 | display: none;
92 | }
93 |
94 | .hidden {
95 | display: none;
96 | }
97 |
98 | body[data-views="1"] #directories {
99 | grid-template-columns: 1fr;
100 | }
101 | body[data-views="1"] label[data-id="right"] {
102 | display: none !important;
103 | }
104 |
--------------------------------------------------------------------------------
/v3/data/commander/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bookmarks Commander
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/v3/data/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/128.png
--------------------------------------------------------------------------------
/v3/data/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/16.png
--------------------------------------------------------------------------------
/v3/data/icons/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/19.png
--------------------------------------------------------------------------------
/v3/data/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/256.png
--------------------------------------------------------------------------------
/v3/data/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/32.png
--------------------------------------------------------------------------------
/v3/data/icons/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/38.png
--------------------------------------------------------------------------------
/v3/data/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/48.png
--------------------------------------------------------------------------------
/v3/data/icons/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/512.png
--------------------------------------------------------------------------------
/v3/data/icons/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/64.png
--------------------------------------------------------------------------------
/v3/data/icons/dark/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/dark/128.png
--------------------------------------------------------------------------------
/v3/data/icons/light/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brian-girko/bookmarks-commander/08c858d0eb0727cc274ccc222c5695ec3d71271f/v3/data/icons/light/128.png
--------------------------------------------------------------------------------
/v3/data/icons/svgs/dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
50 |
51 |
--------------------------------------------------------------------------------
/v3/data/icons/svgs/default.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
50 |
51 |
--------------------------------------------------------------------------------
/v3/data/icons/svgs/light.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
50 |
51 |
--------------------------------------------------------------------------------
/v3/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "version": "0.5.0",
4 | "name": "Bookmarks Commander",
5 | "description": "__MSG_description__",
6 | "default_locale": "en",
7 | "offline_enabled": true,
8 | "permissions": [
9 | "bookmarks",
10 | "contextMenus",
11 | "favicon",
12 | "storage"
13 | ],
14 | "homepage_url": "https://add0n.com/bookmarks-commander.html",
15 | "background": {
16 | "service_worker": "worker.js"
17 | },
18 | "icons": {
19 | "16": "/data/icons/16.png",
20 | "19": "/data/icons/19.png",
21 | "32": "/data/icons/32.png",
22 | "38": "/data/icons/38.png",
23 | "48": "/data/icons/48.png",
24 | "64": "/data/icons/64.png",
25 | "128": "/data/icons/128.png",
26 | "256": "/data/icons/256.png",
27 | "512": "/data/icons/512.png"
28 | },
29 | "action": {},
30 | "incognito": "split"
31 | }
32 |
--------------------------------------------------------------------------------
/v3/worker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | chrome.action.onClicked.addListener(() => {
4 | chrome.storage.local.get({
5 | 'mode': 'tab'
6 | }, async prefs => {
7 | // try to find an open instance
8 | try {
9 | await new Promise((resolve, reject) => {
10 | chrome.runtime.sendMessage({
11 | method: 'instance'
12 | }, r => r ? resolve() : reject(chrome.runtime.lastError));
13 | });
14 | }
15 | catch (e) {
16 | if (prefs.mode === 'tab') {
17 | chrome.tabs.create({
18 | url: '/data/commander/index.html'
19 | }, tab => chrome.storage.local.set({
20 | tab: tab.id
21 | }));
22 | }
23 | else if (prefs.mode === 'window') {
24 | chrome.windows.getCurrent(win => {
25 | chrome.storage.local.get({
26 | 'window.width': 750,
27 | 'window.height': 600,
28 | 'window.left': win.left + Math.round((win.width - 700) / 2),
29 | 'window.top': win.top + Math.round((win.height - 500) / 2)
30 | }, prefs => {
31 | chrome.windows.create({
32 | url: '/data/commander/index.html?mode=window',
33 | width: Math.max(400, prefs['window.width']),
34 | height: Math.max(300, prefs['window.height']),
35 | left: prefs['window.left'],
36 | top: prefs['window.top'],
37 | type: 'popup'
38 | });
39 | });
40 | });
41 | }
42 | }
43 | });
44 | });
45 |
46 | const icon = mode => chrome.action.setIcon({
47 | path: {
48 | '16': '/data/icons/' + mode + '/128.png'
49 | }
50 | });
51 |
52 | {
53 | const startup = () => chrome.storage.local.get({
54 | 'mode': 'tab',
55 | 'popup.width': 800,
56 | 'popup.height': 600,
57 | 'custom-icon': ''
58 | }, prefs => {
59 | if (prefs['custom-icon']) {
60 | icon(prefs['custom-icon']);
61 | }
62 | chrome.contextMenus.create({
63 | id: 'mode-tab',
64 | title: 'Open in Tab',
65 | contexts: ['browser_action'],
66 | type: 'radio',
67 | checked: prefs.mode === 'tab'
68 | });
69 | chrome.contextMenus.create({
70 | id: 'mode-window',
71 | title: 'Open in Window',
72 | contexts: ['browser_action'],
73 | type: 'radio',
74 | checked: prefs.mode === 'window'
75 | });
76 | chrome.contextMenus.create({
77 | id: 'mode-popup',
78 | title: 'Open in Popup',
79 | contexts: ['browser_action'],
80 | type: 'radio',
81 | checked: prefs.mode === 'popup'
82 | });
83 | chrome.contextMenus.create({
84 | id: 'restart',
85 | title: 'Restart Commander',
86 | contexts: ['browser_action']
87 | });
88 | if (prefs.mode === 'popup') {
89 | chrome.action.setPopup({
90 | popup: `data/commander/index.html?mode=popup&width=${prefs['popup.width']}&height=${prefs['popup.height']}`
91 | });
92 | }
93 | });
94 | chrome.runtime.onInstalled.addListener(startup);
95 | chrome.runtime.onStartup.addListener(startup);
96 | }
97 | chrome.contextMenus.onClicked.addListener(info => {
98 | if (info.menuItemId === 'restart') {
99 | chrome.runtime.reload();
100 | }
101 | else if (info.menuItemId.startsWith('mode-')) {
102 | chrome.storage.local.set({
103 | mode: info.menuItemId.replace('mode-', '')
104 | });
105 | }
106 | });
107 |
108 | chrome.storage.onChanged.addListener(ps => {
109 | if (ps.mode) {
110 | chrome.storage.local.get({
111 | 'popup.width': 800,
112 | 'popup.height': 600
113 | }, prefs => {
114 | chrome.action.setPopup({
115 | popup: ps.mode.newValue === 'popup' ?
116 | `data/commander/index.html?mode=popup&width=${prefs['popup.width']}&height=${prefs['popup.height']}` :
117 | ''
118 | });
119 | });
120 | }
121 | if (ps['custom-icon']) {
122 | icon(ps['custom-icon'].newValue);
123 | }
124 | });
125 |
126 | chrome.runtime.onMessage.addListener((request, sender) => {
127 | if (request.method === 'save-size') {
128 | chrome.storage.local.set(request.prefs);
129 | }
130 | else if (request.method === 'activate') {
131 | chrome.windows.update(sender.tab.windowId, {
132 | focused: true
133 | });
134 | chrome.tabs.update(sender.tab.id, {
135 | active: true
136 | });
137 | }
138 | });
139 |
140 | /* FAQs & Feedback */
141 | {
142 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome;
143 | if (navigator.webdriver !== true) {
144 | const page = getManifest().homepage_url;
145 | const {name, version} = getManifest();
146 | onInstalled.addListener(({reason, previousVersion}) => {
147 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({
148 | 'faqs': true,
149 | 'last-update': 0
150 | }, prefs => {
151 | if (reason === 'install' || (prefs.faqs && reason === 'update')) {
152 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45;
153 | if (doUpdate && previousVersion !== version) {
154 | tabs.query({active: true, lastFocusedWindow: true}, tbs => tabs.create({
155 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason,
156 | active: reason === 'install',
157 | ...(tbs && tbs.length && {index: tbs[0].index + 1})
158 | }));
159 | storage.local.set({'last-update': Date.now()});
160 | }
161 | }
162 | }));
163 | });
164 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version);
165 | }
166 | }
167 |
--------------------------------------------------------------------------------