, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
index d82e5131..cafa817d 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,21 @@
-# ScrapBee -- An Extension For Firefox Quantum
+# Scrapyard
-Captures web page into local storage and manages captured pages in sidebar. Compatible with ScrapBook documents. Win/Mac/Linux supported.
+A Scrapbook alternative for Firefox Quantum.
+
+This is a development page. Please visit the main site at: https://gchristensen.github.io/scrapyard/
+
+ScrapBook was a brilliant extension. I haven't found a usable alternative, and I hope that I was able
+to make an extension that is as usable as the legacy ScrapBook itself.
+
+### Credits
+
+* vclfence's [ScrapBee](https://github.com/vctfence/scrapbee) was used as a starting point,
+although currently almost no code from ScrapBee is left in Scrapyard.
+* The modified engine of [SavePageWe](https://addons.mozilla.org/en-US/firefox/addon/save-page-we/)
+is used to capture pages.
+
+
+### JSON Scrapbook File Format
+
+The JSON Scrapbook archive format used by Scrapyard is documented
+[here](https://github.com/GChristensen/scrapyard/wiki/JSON-Scrapbook-File-Format).
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 00000000..e86c23f0
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1,3 @@
+theme: jekyll-theme-minimal
+title: Scrapyard
+logo: addon/icons/scrapyard.svg
diff --git a/_layouts/default.html b/_layouts/default.html
new file mode 100644
index 00000000..c2a44177
--- /dev/null
+++ b/_layouts/default.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+ {% seo %}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/_locales/en/help.html b/_locales/en/help.html
deleted file mode 100644
index 936ab647..00000000
--- a/_locales/en/help.html
+++ /dev/null
@@ -1,94 +0,0 @@
-Only Firefox 60 and later are supported now
-
-Hint since v1.3.2: Please re-download and install backend to make ScrapBee safer
-What's ScrapBee?
-ScrapBee is a Firefox Quantum extension trying to inherit some properties of ScrapBook ,
-at least, it can read/write data left over from ScrapBook (of course, we can create new). For ScrapBook do not support Firefox Quantum, ScrapBee is expected to be
-an extension acts like ScrapBook under Firefox Quantum.
-With ScrapBee, you can save web page scraps to local storage, and manage scraps in browser sidebar。
-What's Rdf?
-Rdf is ScrapBee/ScrapBook's data file in which properties of scraps stored in. And downloaded scraps will be saved in "data" folder beside the rdf file.
-Install/Setup
-After ScrapBee installed, please just finish Initiate steps before using ScrapBee.
-How to use?
-
- Click on ScrapBee Icon, ScrapBee will be opened in sidebar(Mark1), create folders in sidebar if needed.(Mark2)
-
-Full load the page you want to capture, click on menu item "Capture Page" in content menu to capture the page(Mark3)
-
-You can also save selected part of the page, and click on menu item Capture Selection to save it(Mark4)
-
-When capture started, an entity will be showed up in sidebar(Mark5),
- and it's icon will be showed in status waiting until capture finished, then you can open local content(Mark6) by
- clicking on it, you can also open origin URL by click on the origin URL button(Mark7).
-Be Ware
-Whatever you are capturing, please keep ScrapBee opened in sidebar, entity of scraps captured will be saved to position in sidebar currently selected(if no folder/entity selected, it will be saved to the end of top level). Also, you can move entities around by dragging them.
-Trouble Shooting
-Port
-If ScrapBee stop working, and if you see error message in Log window like:
- error: listen tcp :9900: bind: address already in use
-Please try to kill the process binds on that port manually, e.g. under Linux you can do:
-fuser -k 9900/tcp
-Or try another backend port in Settings
-File system permission
-Please do not register rdfs under a directory without read/write permission for current user,
-e.g. common users have no write permission of Windows root directory like c:\, but sub directory of that like c:\foobar works in general.
-Reported as Trojan malware
-Backend of ScrapBee is likely treated as Trojan by scanners, please add ScrapBee to white list of scanners to prevent miss-block/mis-kill, please contact me if you have a better solution。
-Download installation scripts fails
-If you can not download the installation scripts for some reason, e.g. mis-blocked by security tools.
-Please try to re-download the backend:
- Close Firefox->Delete files under “FIREFOX-DOWNLOAD-DIRECTORY /scrapbee/”->Start Firefox->Download again
- If this not helps, please create the scripts manually in “FIREFOX-DOWNLOAD-DIRECTORY /scrapbee/”:
-
-scrapbee_backend.json: (Important: replace "FIREFOX-DOWNLOAD-DIRECTORY " to your real FireFox DOWNLOAD DIRECTORY )
-
- {
- "allowed_extensions": [
- "scrapbee@scrapbee.org"
- ],
- "description": "Scrapbee backend",
- "name": "scrapbee_backend",
- "path": "FIREFOX-DOWNLOAD-DIRECTORY /scrapbee/scrapbee_backend",
- "type": "stdio"
- }
-
-install.bat(Windows, CODEPAGE UTF-8):
-
- chcp 65001
- reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\scrapbee_backend" /f
- reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\scrapbee_backend" /d "FIREFOX-DOWNLOAD-DIRECTORY \scrapbee\scrapbee_backend.json" /f
- reg delete "HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\scrapbee_backend" /f
- reg add "HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\scrapbee_backend" /d "FIREFOX-DOWNLOAD-DIRECTORY \scrapbee\scrapbee_backend.json" /f
- echo done
- pause
-
-install.sh(Linux)
-
- #!/bin/bash
- chmod +x scrapbee_backend
- dest="${HOME}/.mozilla/native-messaging-hosts"
- if [ ! -d "$dest" ];then
- mkdir -p "$dest"
- fi
- cp scrapbee_backend.json "$dest"
- echo done
-
-install.sh(Mac)
-
- #!/bin/bash
- chmod +x scrapbee_backend
- dest="${HOME}/Library/Application Support/Mozilla/NativeMessagingHosts"
- if [ ! -d $dest ];then
- mkdir -p $dest
- fi
- cp scrapbee_backend.json $dest
- echo done
-
-Still do not work?
-Check the log , there are maybe infomation shows what happend.
- Restart Firefox, this helps sometime.
- Or, you can make questions in our github repo: https://github.com/vctfence/scrapbee
- Or email us to scrapbee@163.com
-Backup
-ScrapBee provides no backup function, please backup rdfs and scrap files manually often :).
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
deleted file mode 100644
index 2c9ef93b..00000000
--- a/_locales/en/messages.json
+++ /dev/null
@@ -1,238 +0,0 @@
-{
- "extensionName": {
- "message": "ScrapBee",
- "description": "Name of the extension."
- },
- "extensionDescription": {
- "message": "Captures the web page into local storage and manages captured pages in browser sidebar. Compatible with ScrapBook documents.",
- "description": "Description of the extension."
- },
- "Firefox":{
- "message": "Firefox"
- },
- "DLG_OK": {
- "message": "OK"
- },
- "DLG_CANCEL": {
- "message": "CANCEL"
- },
- "Setting": {
- "message": "Settings"
- },
- "Reload": {
- "message": "Reload"
- },
- "Search": {
- "message": "Search"
- },
- "Help": {
- "message": "Help"
- },
- "Folder": {
- "message": "Folder"
- },
- "Open Folder": {
- "message": "Open Folder"
- },
- "Title": {
- "message": "Title"
- },
- "Rename": {
- "message": "Rename"
- },
- "Step": {
- "message": "Step"
- },
- "Name": {
- "message": "Name"
- },
- "Save": {
- "message": "Save"
- },
- "MODIFY_DOM_ON": {
- "message": "Page Cleaning"
- },
- "MODIFY_DOM_OFF": {
- "message": "Stop Page Cleaning"
- },
- "MARK_PEN": {
- "message": "Mark Pen"
- },
- "File": {
- "message": "File"
- },
- "Donate": {
- "message": "Donate"
- },
- "DONATE_WITH_PAYPAL": {
- "message": "Donate With PayPal"
- },
- "New Folder": {
- "message": "New Folder"
- },
- "New Separator": {
- "message": "New Separator"
- },
- "CapturePage": {
- "message": "CapturePage"
- },
- "As Root Node": {
- "message": "As Root Node"
- },
- "At Current Position": {
- "message": "At Current Position"
- },
- "CaptureSelection": {
- "message": "Capture Selection"
- },
- "CaptureUrl": {
- "message": "Capture Url"
- },
- "Download": {
- "message": "Download"
- },
- "ToggleSidebar": {
- "message": "Toggle Sidebar"
- },
- "Delete": {
- "message": "Delete"
- },
- "Initiate": {
- "message": "Initiate"
- },
- "History": {
- "message": "History"
- },
- "Log": {
- "message": "Log"
- },
- "Rdf Files":{
- "message": "Rdf File"
- },
- "Backend Port":{
- "message": "Backend Port"
- },
- "Background": {
- "message": "Background"
- },
- "FIREFOX_DOWNLOAD_DIRECTORY":{
- "message": "Firefox Download Directory"
- },
- "STEP1_TEXT_1": {
- "message": "Download ScrapBee backend executable and 2 installation files to\"{FIREFOX_DOWNLOAD_DIRECTORY} /screepbee/\", by just click on the button below."
- },
- "STEP1_TEXT_2": {
- "message": "You maybe meet problem that Firefox does not download the 3 files correctly, then you can close Firefox and remove files under\"{FIREFOX_DOWNLOAD_DIRECTORY} /screepbee/\", start Firefox and download again. (Please use standard version of {Firefox} 60 and later)"
- },
- "STEP2_TEXT_1": {
- "message": "Run the installation script downloaded above manually:"
- },
- "STEP2_TEXT_2": {
- "message": "Run the commands in your terminal:"
- },
- "STEP2_TEXT_3": {
- "message": "You maybe need to run the script as administrator"
- },
- "STEP3_TEXT_1": {
- "message": "Finish"
- },
- "STEP4_TEXT_1": {
- "message": "Restart {Firefox}"
- },
- "STEP5_TEXT_1": {
- "message": "Enjoy"
- },
- "BASIC_TEXT": {
- "message": "Basic text"
- },
- "BOOKMARK_TEXT": {
- "message": "Bookmark text"
- },
- "ENTITY_SIZE": {
- "message": "Entity Size"
- },
- "Language": {
- "message": "lanugage"
- },
- "Separator": {
- "message": "Separator"
- },
- "RDF_FILES_TIP": {
- "message": "Set absolute path rdf files ending with extension '.rdf' to register information of scraps,downloaded scraps will be saved in \"data\" folder beside the rdf file. For example: c:\\scrapbee\\test.rdf (Windows) or: /home/userID/scrapbee/test.rdf (Linux) /Users/userID/scrapbee/test.rdf (Mac) You can setup multiple rdf files to classify your scraps, You can have different rdfs inside same directory. No need to create these files maunally. Good news for ScrapBook user: ScrapBook rdfs are also supported."
- },
- "BACKEND_PORT_TIP":{
- "message": "TCP port ScrapBee backend listens. e.g. 9900"
- },
- "_COLOR":{
- "message": " Color"
- },
- "Appearance":{
- "message": "Appearance"
- },
- "Scrap":{
- "message": "scrap"
- },
- "COLOR_TIP":{
- "message": "Change color of some elements to make ScrapBee a better looking. These colors should be in format: #FFFFFF or #FFF"
- },
- "ALREADY_DOWNLOADED_TO": {
- "message": "already downloaded to"
- },
- "Behavior":{
- "message": "Behavior"
- },
- "OPEN_IN_CURRENT_TAB":{
- "message": "Open in current tab"
- },
- "OPEN_IN_CURRENT_TAB_TIP":{
- "message": "By default, ScrapBee opens each scrap in new tab when clicking on an entity, and open scraps in current tab by holding control(meta for Mac) while clicking. You can reverse this by turn on this option. "
- },
- "DONATE_WORDS":{
- "message": "To make ScrapBee grows better, your donations are very welcome, thanks for your support !!!"
- },
- "MORE_DONATE_WAY_TOGGLE":{
- "message": "In other way"
- },
- "MORE_DONATE_WAY":{
- "message": ""
- },
- "RESULTS_FOUND": {
- "message": " RESULT(S) FOUND"
- },
- "NOT_EXISTS": {
- "message": "do not exists"
- },
- "Yes": {
- "message": "Yes"
- },
- "No": {
- "message": "No"
- },
- "CREATE_OR_NOT": {
- "message": "create it"
- },
- "NewDirectory": {
- "message": "New Directory"
- },
- "ConfirmDeleteDirectory": {
- "message": "This is a folder, do you really want delete it and items within it?"
- },
- "ConfirmDeleteItem": {
- "message": "Realy delete the selected item?"
- },
- "Warning": {
- "message": "Warning"
- },
- "Confirm": {
- "message": "Confirm"
- },
- "Loading...": {
- "message": "Loading..."
- },
- "SAME_RDF_MODIFIED": {
- "message": "Same rdf modified, click OK to refresh"
- },
- "NO_SELECTION_ACTIVATED": {
- "message": "No selection activated"
- }
-}
diff --git a/_locales/zh-CN/help.html b/_locales/zh-CN/help.html
deleted file mode 100644
index 1a040d5a..00000000
--- a/_locales/zh-CN/help.html
+++ /dev/null
@@ -1,91 +0,0 @@
-现在,ScrapBee仅支持火狐60以后的版本
-
-1.3.2及以后版本提示: 请重新下载和安装后端使ScrapBee更加安全
-ScrapBee是什么?
-ScrapBee 是一个继承了一些ScrapBook 特性的火狐Quantum扩展,
-至少,它可以读写ScrapBook遗留下来的的文档(当然,也可以新建)。 由于ScrapBook没有支持火狐Quantum,所以在期望中,ScrapBee是一个用于替代ScrapBook的火狐Quantum扩展.
-
-有了ScrapBee,您可以保存网页内容到本地存储,并且可以在火狐侧边栏中进行管理。
-什么是Rdf?
-Rdf是ScrapBee及ScrapBook的主要数据文件,用于登记已下载数据的各类相关信息。同时,下载内容被保存到同一目录下的"data"文件夹。
-安装/设置
-安装ScrapBee之后, 只需要简单的完成初始化 即可开始使用ScrapBee。
-使用
-
- 点击ScrapBee图标,在测边栏中打开ScrapBee(图示1),需要的话,在侧边栏中新建所需文件夹(图示2)
-
-打开并充分加载想保存的网页内容,在其上通过右键菜单抓取页面(图示3)
-
-您也可以保存选中的局部内容,通过右键菜单抓取选区(图示4)
-
-抓取内容时,将在侧边栏中显示相应条目(图示5),并且其图标为等待状态,直到抓取完成,这时可以通过点击条目打开本地内容(图示6),也可以通过点击原始地址按钮(图示7),打开原始链接
-请注意
-无论抓取什么内容,都请保持ScrapBee在侧边栏中打开,抓取内容将保存到ScrapBee侧边栏中当前所选位置(如果没有选中任何文件夹,将保存到顶层目录)。另外,您可以在抓取内容后通过拖动来移动条目位置。
-问题解决
-端口
-如果ScrapBee工作不正常, 同时发现在日志 窗口中有:
- error: listen tcp :9900: bind: address already in use
-请手工结束占用此端口的进程,比如在Linux中你可以执行:
-fuser -k 9900/tcp
-或者在设置 中换用其它端口
-文件系统权限
-请不要注册当前用户不具有读写权限的目录中的Rdf文件,
-比如,对于Windows,普通用户通常没有c:\目录的写权限,而像c:\foobar这样的子目录通常可行。
-安全软件误报
-ScrapBee的后端软件及安装脚本容易被杀毒、防木马软件误报为风险软件,请将ScrapBee后端列入其白名单来防止误报、误杀。如果您有好的改进方案,请联系我。
-下载安装脚本失败
-由于某些原因,比如安全软件,可能会影响后端安装脚本正常下载,此时请尝试重新下载:
- 关闭火狐->删除“火狐下载目录 /scrapbee/”中的所有文件->启动火狐->再次下载
- 如果仍然失败,请在“火狐下载目录 /scrapbee/”中尝试手工建立这些文件
-
-scrapbee_backend.json: (重要: 请替换"火狐下载目录 "为你的实际火狐下载目录 )
-
- {
- "allowed_extensions": [
- "scrapbee@scrapbee.org"
- ],
- "description": "Scrapbee backend",
- "name": "scrapbee_backend",
- "path": "火狐下载目录 /scrapbee/scrapbee_backend",
- "type": "stdio"
- }
-
-install.bat(Windows, CODEPAGE UTF-8):
-
- chcp 65001
- reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\scrapbee_backend" /f
- reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\scrapbee_backend" /d "FIREFOX-DOWNLOAD-DIRECTORY \scrapbee\scrapbee_backend.json" /f
- reg delete "HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\scrapbee_backend" /f
- reg add "HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\scrapbee_backend" /d "FIREFOX-DOWNLOAD-DIRECTORY \scrapbee\scrapbee_backend.json" /f
- echo done
- pause
-
-install.sh(Linux)
-
- #!/bin/bash
- chmod +x scrapbee_backend
- dest="${HOME}/.mozilla/native-messaging-hosts"
- if [ ! -d "$dest" ];then
- mkdir -p "$dest"
- fi
- cp scrapbee_backend.json "$dest"
- echo done
-
-install.sh(Mac)
-
- #!/bin/bash
- chmod +x scrapbee_backend
- dest="${HOME}/Library/Application Support/Mozilla/NativeMessagingHosts"
- if [ ! -d $dest ];then
- mkdir -p $dest
- fi
- cp scrapbee_backend.json $dest
- echo done
-
-仍然有未解决问题?
-查看日志 ,你可能找到一些线索.
- 重启火狐有时也能解决一些问题
- 也可以在github代码库中提问 https://github.com/vctfence/scrapbee
- 或者发邮件联系我们scrapbee@163.com
-备份
-ScrapBee不提供备份功能,请经常手工备份Rdf和下载数据 :)
diff --git a/_locales/zh-CN/messages.json b/_locales/zh-CN/messages.json
deleted file mode 100644
index cbe62829..00000000
--- a/_locales/zh-CN/messages.json
+++ /dev/null
@@ -1,238 +0,0 @@
-{
- "extensionName": {
- "message": "ScrapBee",
- "description": "Name of the extension."
- },
- "extensionDescription": {
- "message": "抓取网页内容到本地存储,并可以在浏览器侧边栏中进行管理。兼容ScrapBook文档。",
- "description": "Description of the extension."
- },
- "Firefox":{
- "message": "火狐"
- },
- "DLG_OK": {
- "message": "确认"
- },
- "DLG_CANCEL": {
- "message": "取消"
- },
- "Setting": {
- "message": "设置"
- },
- "Reload": {
- "message": "刷新"
- },
- "Search": {
- "message": "搜索"
- },
- "Help": {
- "message": "帮助"
- },
- "Folder": {
- "message": "文件夹"
- },
- "Open Folder": {
- "message": "打开文件夹"
- },
- "Title": {
- "message": "标题"
- },
- "Rename": {
- "message": "重命名"
- },
- "Step": {
- "message": "步骤"
- },
- "Name": {
- "message": "名称"
- },
- "Save": {
- "message": "保存"
- },
- "MODIFY_DOM_ON": {
- "message": "页面清理"
- },
- "MODIFY_DOM_OFF": {
- "message": "停止页面清理"
- },
- "MARK_PEN": {
- "message": "记号笔"
- },
- "File": {
- "message": "文件"
- },
- "Donate": {
- "message": "捐助"
- },
- "DONATE_WITH_PAYPAL": {
- "message": "通过 PayPal 捐助"
- },
- "New Folder": {
- "message": "新建文件夹"
- },
- "New Separator": {
- "message": "新建分隔条"
- },
- "CapturePage": {
- "message": "抓取页面"
- },
- "As Root Node": {
- "message": "作为根目录"
- },
- "At Current Position": {
- "message": "在当前位置"
- },
- "CaptureSelection": {
- "message": "抓取选区"
- },
- "CaptureUrl": {
- "message": "抓取网址"
- },
- "Download": {
- "message": "下载"
- },
- "ToggleSidebar": {
- "message": "Toggle Sidebar"
- },
- "Delete": {
- "message": "删除"
- },
- "Initiate": {
- "message": "初始化"
- },
- "History": {
- "message": "更新历史"
- },
- "Log": {
- "message": "日志"
- },
- "Rdf Files":{
- "message": "Rdf文件"
- },
- "Backend Port":{
- "message": "端口"
- },
- "Background": {
- "message": "背景"
- },
- "FIREFOX_DOWNLOAD_DIRECTORY":{
- "message": "火狐下载目录"
- },
- "STEP1_TEXT_1": {
- "message": "下载ScrapBee后端可执行文件和2个安装文件到\"{FIREFOX_DOWNLOAD_DIRECTORY} /screepbee/\",只需点击下方按钮。"
- },
- "STEP1_TEXT_2": {
- "message": "您可能会遇到文件不能正确下载的问题,此时可以尝试关闭{Firefox}并删除\"{FIREFOX_DOWNLOAD_DIRECTORY} /screepbee/\"中的所有文件,重启{Firefox}再次下载。另外,请使用{Firefox}60及以后的标准版{Firefox}。"
- },
- "STEP2_TEXT_1": {
- "message": "手工运行上面下载的安装脚本:"
- },
- "STEP2_TEXT_2": {
- "message": "在终端中执行:"
- },
- "STEP2_TEXT_3": {
- "message": "你可能需要以管理员身份运行此脚本"
- },
- "STEP3_TEXT_1": {
- "message": "完成"
- },
- "STEP4_TEXT_1": {
- "message": "重启{Firefox}"
- },
- "STEP5_TEXT_1": {
- "message": "敬请使用"
- },
- "BASIC_TEXT": {
- "message": "基础文本"
- },
- "BOOKMARK_TEXT": {
- "message": "书签文本"
- },
- "ENTITY_SIZE": {
- "message": "条目大小"
- },
- "Language": {
- "message": "语言"
- },
- "Separator": {
- "message": "分隔符"
- },
- "RDF_FILES_TIP": {
- "message": "设置rdf文件的绝对路径,用于登记下载信息,以'.rdf'为扩展名(下载数据将保存到rdf文件所在目录下的data文件夹) 例如: c:\\scrapbee\\test.rdf (Windows) 或者: /home/userID/scrapbee/test.rdf (Linux) /Users/userID/scrapbee/test.rdf (Mac) 您可以设置多个rdf文件来实现{Scrap}的分类,可以在同一文件夹下放置多个rdf文件,不需要手工创建这些文件。 ScrapBee也支持读写ScrapBook 的rdf文件,这对于使用过ScrapBook的用户是一个好消息。"
- },
- "BACKEND_PORT_TIP":{
- "message": "后端程序监听的TCP端口,比如9900"
- },
- "_COLOR":{
- "message": "颜色"
- },
- "Appearance":{
- "message": "外观"
- },
- "Scrap":{
- "message": "收藏内容"
- },
- "COLOR_TIP":{
- "message": "通过改变某些元素的颜色来美化ScrapBee,颜色格式应为 #FFFFFF 或 #FFF"
- },
- "ALREADY_DOWNLOADED_TO": {
- "message": "已下载到"
- },
- "Behavior":{
- "message": "行为"
- },
- "OPEN_IN_CURRENT_TAB":{
- "message": "在当前标签打开"
- },
- "OPEN_IN_CURRENT_TAB_TIP":{
- "message": "点击条目时,ScrapBook默认在新标签页中打开{Scrap}, 如果按住Control(苹果系统中使用Meta),则在当前标签中打开, 你可以通过此选项来切换两种操作"
- },
- "DONATE_WORDS":{
- "message": "为了使ScrapBee更健康地成长, 任何数量的捐助都是非常欢迎的, 感谢您的支持!!!"
- },
- "MORE_DONATE_WAY_TOGGLE":{
- "message": "其它方式"
- },
- "MORE_DONATE_WAY":{
- "message": "您也可以通过以下方式捐助: "
- },
- "RESULTS_FOUND": {
- "message": "个结果"
- },
- "NOT_EXISTS": {
- "message": "不存在"
- },
- "Yes": {
- "message": "是"
- },
- "No": {
- "message": "否"
- },
- "CREATE_OR_NOT": {
- "message": "是否生成"
- },
- "NewDirectory": {
- "message": "新建文件夹"
- },
- "ConfirmDeleteDirectory": {
- "message": "这是一个文件夹, 真的要删除它和其中所有内容?"
- },
- "ConfirmDeleteItem": {
- "message": "真的要删除所选项?"
- },
- "Warning": {
- "message": "警告"
- },
- "Confirm": {
- "message": "确认"
- },
- "Loading...": {
- "message": "加载中..."
- },
- "SAME_RDF_MODIFIED": {
- "message": "同一RDF被修改,点击确认后刷新"
- },
- "NO_SELECTION_ACTIVATED": {
- "message": "没有激活选区"
- }
-}
diff --git a/addon/_locales/en/messages.json b/addon/_locales/en/messages.json
new file mode 100644
index 00000000..ce2cae10
--- /dev/null
+++ b/addon/_locales/en/messages.json
@@ -0,0 +1,10 @@
+{
+ "extensionName": {
+ "message": "Scrapyard",
+ "description": "Name of the extension."
+ },
+ "extensionDescription": {
+ "message": "Advanced bookmark manager with page archiving functionality.",
+ "description": "Description of the extension."
+ }
+}
diff --git a/addon/background.html b/addon/background.html
new file mode 100644
index 00000000..f28d3941
--- /dev/null
+++ b/addon/background.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You have got to this page by accident.
+
+
diff --git a/addon/background_worker.js b/addon/background_worker.js
new file mode 100644
index 00000000..89da8c44
--- /dev/null
+++ b/addon/background_worker.js
@@ -0,0 +1,6 @@
+import "./global.js";
+import "./runtime_listeners.js";
+import "./lib/xhr-shim.js";
+import "./mv3_persistent.js";
+import "./core.js";
+import "./savepage/background.js";
diff --git a/addon/bookmarking.js b/addon/bookmarking.js
new file mode 100644
index 00000000..e0c9e531
--- /dev/null
+++ b/addon/bookmarking.js
@@ -0,0 +1,523 @@
+import {settings} from "./settings.js";
+import {
+ getActiveTab,
+ hasCSRPermission,
+ injectCSSFile,
+ injectScriptFile,
+ showNotification,
+ isHTMLTab, askCSRPermission, ACTION_ICONS
+} from "./utils_browser.js";
+import {capitalize, getMimetypeByExt, sleep} from "./utils.js";
+import {send, sendLocal} from "./proxy.js";
+import {
+ ARCHIVE_TYPE_FILES, CHROME_BOOKMARK_TOOLBAR,
+ DEFAULT_SHELF_ID,
+ FIREFOX_BOOKMARK_TOOLBAR,
+ NODE_TYPE_ARCHIVE,
+ NODE_TYPE_BOOKMARK
+} from "./storage.js";
+import {fetchText, fetchWithTimeout} from "./utils_io.js";
+import {Node} from "./storage_entities.js";
+import {getFaviconFromContent, getFaviconFromTab} from "./favicon.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import * as crawler from "./crawler.js";
+import {Folder} from "./bookmarks_folder.js";
+import {isHTMLLink, parseHtml} from "./utils_html.js";
+import {getSidebarWindow, toggleSidebarWindow} from "./utils_sidebar.js";
+import {helperApp} from "./helper_app.js";
+
+const SCRAPYARD_FOLDER_NAME = "Scrapyard";
+
+export function formatShelfName(name) {
+ if (name && settings.capitalize_builtin_shelf_names())
+ return capitalize(name);
+
+ return name;
+}
+
+export function isSpecialPage(url) {
+ return (url.startsWith("about:")
+ || url.startsWith("view-source:") || url.startsWith("moz-extension:")
+ || url.startsWith("https://addons.mozilla.org") || url.startsWith("https://support.mozilla.org")
+ || url.startsWith("chrome:") || url.startsWith("chrome-extension:")
+ || url.startsWith("https://chrome.google.com/webstore")
+ || url.startsWith(helperApp.url("/")));
+}
+
+export function notifySpecialPage() {
+ showNotification("Scrapyard cannot be used with special or already captured pages.");
+}
+
+export async function getTabMetadata(tab) {
+ const result = {
+ name: tab.title,
+ uri: tab.url
+ };
+
+ const favicon = await getFaviconFromTab(tab);
+ if (favicon)
+ result.icon = favicon;
+
+ return result;
+}
+
+export async function getActiveTabMetadata() {
+ const tab = await getActiveTab();
+ return await getTabMetadata(tab);
+}
+
+export async function captureTab(tab, bookmark) {
+ if (isSpecialPage(tab.url))
+ notifySpecialPage();
+ else {
+ if (await isHTMLTab(tab))
+ await captureHTMLTab(tab, bookmark)
+ else
+ await captureNonHTMLTab(tab, bookmark);
+ }
+}
+
+async function extractSelection(tab, bookmark) {
+ const frames = await browser.webNavigation.getAllFrames({tabId: tab.id});
+ let selection;
+
+ for (let frame of frames) {
+ try {
+ await injectScriptFile(tab.id, {file: "/content_selection.js", frameId: frame.frameId});
+ selection = await browser.tabs.sendMessage(
+ tab.id,
+ {type: "CAPTURE_SELECTION", options: bookmark},
+ {frameId: frame.frameId}
+ );
+
+ if (selection)
+ break;
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ return selection;
+}
+
+async function captureHTMLTab(tab, bookmark) {
+ if (!_BACKGROUND_PAGE)
+ await injectScriptFile(tab.id, {file: "/lib/browser-polyfill.js", allFrames: true});
+
+ let response;
+ const selection = await extractSelection(tab, bookmark);
+ try { response = await startSavePageCapture(tab, bookmark, selection); } catch (e) {}
+
+ if (typeof response == "undefined") { /* no response received - content script not loaded in active tab */
+ let onScriptInitialized = async (message, sender) => {
+ if (message.type === "CAPTURE_SCRIPT_INITIALIZED" && tab.id === sender.tab.id) {
+ browser.runtime.onMessage.removeListener(onScriptInitialized);
+
+ try {
+ response = await startSavePageCapture(tab, bookmark, selection);
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (typeof response == "undefined")
+ showNotification("Cannot initialize the capture script, please retry.");
+ }
+ };
+ browser.runtime.onMessage.addListener(onScriptInitialized);
+
+ await injectSavePageScripts(tab)
+ }
+}
+
+function startSavePageCapture(tab, bookmark, selection) {
+ if (settings.save_unpacked_archives())
+ bookmark.contains = ARCHIVE_TYPE_FILES;
+
+ return browser.tabs.sendMessage(tab.id, {
+ type: "performAction",
+ menuaction: 1,
+ saveditems: 2,
+ bookmark,
+ selection
+ });
+}
+
+async function injectSavePageScripts(tab, onError) {
+ if (!await hasCSRPermission())
+ return;
+
+ try {
+ try {
+ await injectScriptFile(tab.id, {file: "/savepage/content-frame.js", allFrames: true});
+ await injectScriptFile(tab.id, {file: "/savepage/content-fontface.js", allFrames: true});
+ } catch (e) {
+ console.error(e);
+ }
+
+ await injectScriptFile(tab.id, {file: "/savepage/content.js"});
+ }
+ catch (e) {
+ console.error(e);
+
+ if (onError)
+ onError(e);
+ }
+}
+
+async function captureNonHTMLTab(tab, bookmark) {
+ if (/^file:/i.exec(tab.url) && settings.platform.firefox) {
+ showNotification("Firefox version of Scrapyard can not be used with file:// URLs.");
+ return;
+ }
+
+ try {
+ const headers = {"Cache-Control": "no-store"};
+ const response = await fetchWithTimeout(tab.url, {timeout: 60000, headers});
+
+ if (response.ok) {
+ let contentType = response.headers.get("content-type");
+
+ if (!contentType)
+ contentType = getMimetypeByExt(new URL(tab.url).pathname) || "application/pdf";
+
+ bookmark.content_type = contentType;
+
+ await Bookmark.storeArchive(bookmark, await response.arrayBuffer(), contentType);
+ }
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ finalizeCapture(bookmark);
+}
+
+export function finalizeCapture(bookmark) {
+ if (bookmark?.__automation && bookmark?.select)
+ send.bookmarkCreated({node: bookmark});
+ else if (bookmark && !bookmark.__automation && !bookmark.__type_change)
+ send.bookmarkAdded({node: bookmark});
+}
+
+export async function archiveBookmark(node) {
+ const bookmark = await Node.get(node.id);
+ bookmark.type = NODE_TYPE_ARCHIVE;
+ await Node.idb.update(bookmark); // storage updated in Archive.add
+
+ const isHTML = await isHTMLLink(bookmark.uri);
+ if (isHTML === true) {
+ bookmark.__type_change = true;
+ await packPage(bookmark.uri, bookmark, () => null, () => null, false);
+ }
+ else if (isHTML === false) {
+ let response;
+ try {
+ response = await fetchWithTimeout(bookmark.uri);
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (response.ok)
+ await Bookmark.storeArchive(bookmark, await response.arrayBuffer(), response.headers.get("content-type"));
+ }
+}
+
+export async function showSiteCaptureOptions(tab, bookmark) {
+ try {
+ if (!_BACKGROUND_PAGE)
+ await injectScriptFile(tab.id, {file: "/lib/browser-polyfill.js", allFrames: true});
+
+ await injectScriptFile(tab.id, {file: "/savepage/content-frame.js", allFrames: true});
+ await injectCSSFile(tab.id, {file: "/ui/site_capture_content.css"});
+ await injectScriptFile(tab.id, {file: "/ui/site_capture_content.js", frameId: 0});
+ browser.tabs.sendMessage(tab.id, {type: "storeBookmark", bookmark});
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+export async function performSiteCapture(bookmark) {
+ if (crawler.initialize(bookmark)) {
+ const folder = await Folder.addSite(bookmark.parent_id, bookmark.name);
+ bookmark.parent_id = folder.id;
+
+ sendLocal.createArchive({node: bookmark});
+ }
+}
+
+export function startCrawling(bookmark) {
+ bookmark.__site_capture.level = 0;
+
+ crawler.crawl(bookmark);
+
+ send.startProcessingIndication({noWait: true});
+ send.toggleAbortMenu({show: true});
+}
+
+export function abortCrawling() {
+ crawler.abort();
+}
+
+export async function packPage(url, bookmark, initializer, resolver, hide_tab) {
+ return new Promise(async (resolve, reject) => {
+ let initializationListener;
+ let changedTab;
+ let packing;
+
+ let completionListener = function (message, sender, sendResponse) {
+ if (message.type === "storePageHtml" && message.bookmark.__tab_id === packingTab.id) {
+ removeListeners();
+ browser.tabs.remove(packingTab?.id);
+
+ resolve(resolver(message, changedTab));
+ }
+ };
+
+ let tabRemovedListener = function (tabId) {
+ if (tabId === changedTab.id) {
+ removeListeners();
+ const message = {bookmark};
+ resolve(resolver(message, changedTab));
+ }
+ };
+
+ browser.runtime.onMessage.addListener(completionListener);
+
+ var tabUpdateListener = async (id, changed, tab) => {
+ if (!changedTab && id === packingTab.id)
+ changedTab = tab;
+ if (id === packingTab.id && changed.favIconUrl)
+ changedTab.favIconUrl = changed.favIconUrl;
+ if (id === packingTab.id && changed.title)
+ changedTab.title = changed.title;
+ if (id === packingTab.id && changed.status === "complete") { // may be invoked several times
+ if (packing)
+ return;
+ packing = true;
+
+ initializationListener = async function (message, sender, sendResponse) {
+ if (message.type === "CAPTURE_SCRIPT_INITIALIZED" && sender.tab.id === packingTab.id) {
+ if (initializer)
+ await initializer(bookmark, tab);
+ bookmark.__tab_id = packingTab.id;
+
+ try {
+ await startSavePageCapture(packingTab, bookmark);
+ } catch (e) {
+ console.error(e);
+ reject(e);
+ }
+ }
+ };
+
+ browser.runtime.onMessage.addListener(initializationListener);
+
+ if (!_BACKGROUND_PAGE)
+ await injectScriptFile(packingTab.id, {file: "/lib/browser-polyfill.js", allFrames: true});
+
+ await injectSavePageScripts(packingTab, reject);
+ }
+ };
+
+ function removeListeners() {
+ browser.tabs.onUpdated.removeListener(tabUpdateListener);
+ browser.tabs.onRemoved.removeListener(tabRemovedListener);
+ browser.runtime.onMessage.removeListener(completionListener);
+ browser.runtime.onMessage.removeListener(initializationListener);
+ }
+
+ browser.tabs.onUpdated.addListener(tabUpdateListener);
+ browser.tabs.onRemoved.addListener(tabRemovedListener);
+
+ var packingTab = await browser.tabs.create({url: url, active: false});
+
+ if (hide_tab)
+ browser.tabs.hide(packingTab.id)
+ });
+}
+
+export async function packUrl(url, hide_tab) {
+ return packPage(url, {}, b => b.__url_packing = true, m => m.html, hide_tab);
+}
+
+export async function packUrlExt(url, hide_tab) {
+ let resolver = (m, t) => ({html: m.html, title: url.endsWith(t.title)? undefined: t.title, icon: t.favIconUrl});
+ return packPage(url, {}, b => b.__url_packing = true, resolver, hide_tab);
+}
+
+export function addBookmarkOnCommand(command) {
+ let type = command === "archive_to_default_shelf"? NODE_TYPE_ARCHIVE: NODE_TYPE_BOOKMARK;
+
+ if (settings.platform.firefox)
+ addBookmarkOnCommandFirefox(type);
+ else
+ addBookmarkOnCommandNonFirefox(type);
+}
+
+function addBookmarkOnCommandFirefox(type) {
+ if (localStorage.getItem("option-open-sidebar-from-shortcut") === "open") {
+ localStorage.setItem("sidebar-select-shelf", DEFAULT_SHELF_ID);
+ browser.sidebarAction.open();
+ }
+
+ if (type === NODE_TYPE_ARCHIVE)
+ askCSRPermission() // requires non-async function
+ .then(response => {
+ if (response)
+ addBookmarkOnCommandSendPayload(type);
+ })
+ .catch(e => console.error(e));
+ else
+ addBookmarkOnCommandSendPayload(type);
+}
+
+async function addBookmarkOnCommandNonFirefox(type) {
+ const payload = await getActiveTabMetadata();
+ await addBookmarkOnCommandSendPayload(type, payload);
+
+ await settings.load();
+ if (settings.open_sidebar_from_shortcut()) {
+ const window = await getSidebarWindow();
+ if (!window) {
+ await browser.storage.session.set({"sidebar-select-shelf": DEFAULT_SHELF_ID});
+ await toggleSidebarWindow();
+ }
+ }
+}
+
+async function addBookmarkOnCommandSendPayload(type, payload) {
+ if (!payload)
+ payload = await getActiveTabMetadata();
+
+ payload.type = type;
+ payload.parent_id = DEFAULT_SHELF_ID;
+
+ return sendLocal.captureHighlightedTabs({options: payload});
+}
+
+export async function createBookmarkFromURL (url, parentId) {
+ let options = {
+ parent_id: parentId,
+ uri: url,
+ name: "Untitled"
+ };
+
+ if (!/^https?:\/\/.*/.exec(options.uri))
+ options.uri = "http://" + options.uri;
+
+ sendLocal.startProcessingIndication();
+
+ try {
+ const html = await fetchText(options.uri);
+
+ let doc;
+ if (html)
+ doc = parseHtml(html);
+
+ if (doc) {
+ const title = doc.getElementsByTagName("title")[0]?.textContent;
+ options.name = title || options.uri;
+
+ const icon = await getFaviconFromContent(options.uri, doc);
+ if (icon)
+ options.icon = icon;
+ }
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ const bookmark = await Bookmark.add(options, NODE_TYPE_BOOKMARK);
+ await sendLocal.stopProcessingIndication();
+ sendLocal.bookmarkCreated({node: bookmark});
+}
+
+export async function addToBookmarksToolbar(node) {
+ let scrapyardFolder = await findScrapyardBookmarkFolder();
+
+ if (!scrapyardFolder)
+ scrapyardFolder = await createScrapyardBookmarkFolder();
+
+ await browser.bookmarks.create({
+ parentId: scrapyardFolder.id,
+ title: node.name,
+ url: createScrapyardToolbarReference(node.uuid),
+ index: 0
+ });
+
+ const maxReferences = settings.number_of_bookmarks_toolbar_references();
+
+ if (maxReferences) {
+ const references = await browser.bookmarks.getChildren(scrapyardFolder.id);
+
+ if (references.length > maxReferences) {
+ const refsToDelete = references.slice(maxReferences, references.length);
+
+ for (const ref of refsToDelete)
+ browser.bookmarks.remove(ref.id);
+ }
+ }
+}
+
+async function findScrapyardBookmarkFolder() {
+ const scrapyardFolders = await browser.bookmarks.search({title: SCRAPYARD_FOLDER_NAME});
+
+ return scrapyardFolders.find(f => !f.url &&
+ (settings.platform.firefox && f.parentId === FIREFOX_BOOKMARK_TOOLBAR
+ || settings.platform.chrome && f.parentId === CHROME_BOOKMARK_TOOLBAR));
+}
+
+async function createScrapyardBookmarkFolder() {
+ let parentId = FIREFOX_BOOKMARK_TOOLBAR;
+
+ if (settings.platform.chrome)
+ parentId = "1";
+
+ const options = {
+ parentId: parentId,
+ title: SCRAPYARD_FOLDER_NAME
+ };
+
+ if (settings.platform.firefox)
+ options.type = "folder";
+
+ return browser.bookmarks.create(options);
+}
+
+export function createScrapyardToolbarReference(uuid = "") {
+ let referenceURL = `ext+scrapyard://${uuid}`;
+
+ return browser.runtime.getURL(`/reference.html?toolbar#${referenceURL}`);
+}
+
+export async function removeFromBookmarksToolbar(uuid) {
+ const scrapyardFolder = await findScrapyardBookmarkFolder();
+
+ if (scrapyardFolder) {
+ const references = await browser.bookmarks.getChildren(scrapyardFolder.id);
+ const refToRemove = references.find(r => r.url.endsWith(uuid));
+
+ if (refToRemove)
+ return browser.bookmarks.remove(refToRemove.id);
+ }
+}
+
+export async function setBookmarkedActionIcon(url) {
+ const action = _MANIFEST_V3? browser.action: browser.browserAction;
+
+ if (!url)
+ url = (await getActiveTab())?.url;
+
+ if (await Node.urlExists(url)) {
+ if (settings.platform.firefox)
+ action.setIcon({path: "/icons/scrapyard-star.svg"});
+ else
+ action.setIcon({path: "/icons/scrapyard-star.png"});
+ }
+ else {
+ if (settings.platform.firefox)
+ action.setIcon({path: "/icons/scrapyard.svg"});
+ else
+ action.setIcon({path: ACTION_ICONS});
+ }
+}
diff --git a/addon/bookmarks.js b/addon/bookmarks.js
new file mode 100644
index 00000000..6d6360ab
--- /dev/null
+++ b/addon/bookmarks.js
@@ -0,0 +1,131 @@
+import {isContainerNode} from "./storage.js";
+import {Node} from "./storage_entities.js";
+import {Query} from "./storage_query.js";
+
+// a proxy class that calls handlers of the registered external backends if they are implemented
+// an external backend may have the "initialize" method which is called after the settings are loaded
+// the corresponding backend is chosen by the first found value of the "external" field in any argument
+export class PluginContainer {
+ constructor() {
+ this.externalBackends = {};
+
+ this._addHandler("createBookmarkFolder");
+ this._addHandler("createBookmark");
+ this._addHandler("renameBookmark");
+ this._addHandler("moveBookmarks");
+ this._addHandler("beforeBookmarkCopied")
+ this._addHandler("copyBookmarks");
+ this._addHandler("deleteBookmarks");
+ this._addHandler("updateBookmark");
+ this._addHandler("updateBookmarks");
+ this._addHandler("reorderBookmarks");
+ this._addHandler("storeBookmarkData");
+ this._addHandler("updateBookmarkData");
+ this._addHandler("storeBookmarkNotes");
+ this._addHandler("storeBookmarkComments");
+ }
+
+ registerPlugin(name, backend) {
+ if (backend.initialize)
+ backend.initialize();
+ this.externalBackends[name] = backend;
+ }
+
+ unregisterExternalBackend(name) {
+ delete this.externalBackends[name];
+ }
+
+ _addHandler(methodName) {
+ const handler = async (...args) => {
+ const external = this._findExternal(args);
+
+ if (external) {
+ const backend = this.externalBackends[external];
+ if (backend[methodName])
+ await backend[methodName].apply(backend, args);
+ }
+ };
+
+ const proto = Object.getPrototypeOf(this);
+ proto[methodName] = handler;
+ }
+
+ _findExternal(args) {
+ let external;
+ for (const arg of args) {
+ if (Array.isArray(arg)) {
+ const node = arg.find(n => n.hasOwnProperty("external"));
+ if (node) {
+ external = node.external;
+ break;
+ }
+ }
+ else {
+ if (arg?.hasOwnProperty("external")) {
+ external = arg.external;
+ break;
+ }
+ }
+ }
+ return external;
+ }
+}
+
+export let plugins = new PluginContainer();
+
+// the base class for high-level bookmarking entities: Bookmark, Shelf, etc.
+export class EntityManager {
+ constructor() {
+ this.plugins = plugins;
+ }
+
+ async traverse(root, visitor) {
+ let doTraverse = async (parent, root) => {
+ await visitor(parent, root);
+ let children = isContainerNode(root)
+ ? await Node.getChildren(root.id)
+ : null;
+ if (children)
+ for (let c of children)
+ await doTraverse(root, c);
+ };
+
+ return doTraverse(null, root);
+ }
+
+ async ensureUniqueName(parentId, name, oldName) {
+ if (!name)
+ return "";
+
+ let children;
+
+ if (parentId)
+ children = (await Node.getChildren(parentId)).map(c => c.name);
+ else
+ children = (await Query.allShelves()).map(c => c.name);
+
+ children = children.filter(c => !!c);
+
+ if (oldName)
+ children = children.filter(c => c !== oldName);
+
+ children = children.map(c => c.toLocaleUpperCase());
+
+ let uname = name.toLocaleUpperCase();
+ let original = name;
+ let n = 1;
+
+ let m = original.match(/.*( \(\d+\))$/);
+
+ if (m)
+ original = original.replace(m[1], "");
+
+ while (children.some(c => c === uname)) {
+ name = original + " (" + n + ")";
+ uname = name.toLocaleUpperCase();
+ n += 1
+ }
+
+ return name;
+ }
+}
diff --git a/addon/bookmarks_bookmark.js b/addon/bookmarks_bookmark.js
new file mode 100644
index 00000000..9070a542
--- /dev/null
+++ b/addon/bookmarks_bookmark.js
@@ -0,0 +1,597 @@
+import {EntityManager} from "./bookmarks.js";
+import {
+ byDateAddedDesc,
+ byPosition,
+ DEFAULT_SHELF_UUID,
+ DONE_SHELF_NAME,
+ nodeHasSomeContent, isVirtualShelf,
+ NODE_TYPE_ARCHIVE,
+ NODE_TYPE_BOOKMARK,
+ NODE_TYPE_FOLDER, NODE_TYPE_NOTES,
+ NODE_TYPE_SEPARATOR,
+ NODE_TYPE_SHELF, NON_IMPORTABLE_SHELVES,
+ TODO_SHELF_NAME, DEFAULT_POSITION,
+ RDF_EXTERNAL_TYPE, EVERYTHING_SHELF_NAME, ARCHIVE_TYPE_FILES
+} from "./storage.js";
+import {indexString} from "./utils_html.js";
+import {Query} from "./storage_query.js";
+import {Path} from "./path.js";
+import {Folder} from "./bookmarks_folder.js";
+import {ishellConnector} from "./plugin_ishell.js";
+import {cleanObject, getMimetypeByExt} from "./utils.js";
+import {getFaviconFromContent} from "./favicon.js";
+import {Archive, Comments, Icon, Node, Notes} from "./storage_entities.js";
+import {undoManager} from "./bookmarks_undo.js";
+
+export class BookmarkManager extends EntityManager {
+ _Node;
+
+ static newInstance() {
+ const instance = new BookmarkManager();
+
+ instance.idb = new BookmarkManager();
+
+ return instance;
+ }
+
+ configure() {
+ this._Node = Node;
+ this.idb._Node = Node.idb;
+ }
+
+ _splitTags(tags, separator = ",") {
+ if (tags && typeof tags === "string")
+ return tags.split(separator)
+ .filter(t => !!t)
+ .map(t => t.trim())
+ .map(t => t.toLocaleUpperCase());
+
+ return tags;
+ }
+
+ setTentativeId(node) {
+ node.__tentative_id = "tentative_" + Math.floor(Math.random() * 1000);
+ return node.__tentative_id;
+ }
+
+ async add(data, nodeType = NODE_TYPE_BOOKMARK) {
+ if (data.parent_id)
+ data.parent_id = parseInt(data.parent_id);
+ else
+ throw new Error("No bookmark parent id");
+
+ const parent = await Node.get(data.parent_id);
+
+ if (nodeType === NODE_TYPE_BOOKMARK && parent.external === RDF_EXTERNAL_TYPE)
+ throw new Error("Only archives could be added to an RDF file.");
+
+ data.external = parent.external;
+ data.name = await this.ensureUniqueName(data.parent_id, data.name);
+
+ data.type = nodeType;
+ if (data.tags)
+ data.tag_list = this._splitTags(data.tags);
+ //await this.addTags(data.tag_list);
+
+ const [iconId, dataUrl] = await this.storeIcon(data);
+
+ if (iconId)
+ data.content_modified = new Date();
+
+ const node = await this._Node.add(data);
+
+ if (iconId) {
+ await Icon.update(iconId, {node_id: node.id});
+ await Icon.persist(node, dataUrl);
+ }
+
+ await this.plugins.createBookmark(node, parent);
+
+ return node;
+ }
+
+ async addSeparator(parentId) {
+ const parent = await Node.get(parentId);
+ const options = {
+ name: "-",
+ type: NODE_TYPE_SEPARATOR,
+ parent_id: parentId,
+ external: parent.external
+ };
+
+ let node = await this._Node.add(options);
+
+ try {
+ await this.plugins.createBookmark(node, parent);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ return node;
+ }
+
+ async addNotes(parentId, name) {
+ let folder = await Node.get(parentId);
+ let node = await this._Node.add({
+ parent_id: parentId,
+ name: name,
+ //has_notes: true,
+ type: NODE_TYPE_NOTES,
+ external: folder.external
+ });
+
+ try {
+ await this.plugins.createBookmark(node, folder);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ return node;
+ }
+
+ async import(data, sync) {
+ if (data.uuid === DEFAULT_SHELF_UUID)
+ return;
+
+ if (data.type !== NODE_TYPE_SHELF)
+ data.parent_id = data.parent_id || (await Folder.getOrCreateByPath(data.path)).id;
+
+ data = Object.assign({}, data);
+
+ if (data.tags)
+ data.tag_list = this._splitTags(data.tags);
+ //this.addTags(data.tag_list);
+
+ const exists = await Node.exists(data);
+ let forceNewUuid = data.uuid && (!sync && (exists || NON_IMPORTABLE_SHELVES.some(uuid => uuid === data.uuid)));
+
+ const now = new Date();
+
+ if (!data.date_added)
+ data.date_added = now;
+
+ // sync uses date_modified field to track changes, so this field should be updated on a regular import,
+ // but it should not be touched when performing a sync import
+ if (!sync) {
+ data.date_modified = now;
+ if (data.content_modified || nodeHasSomeContent(data))
+ data.content_modified = data.date_modified;
+ }
+
+ if (!data.uuid || forceNewUuid)
+ Node.setUUID(data);
+
+ let result;
+
+ if (sync && exists) {
+ const node = await Node.getByUUID(data.uuid);
+ data.id = node.id;
+ result = this._Node.update(data, false);
+ }
+ else
+ result = this._Node.import(data);
+
+ return result;
+ }
+
+ async update(data) {
+ let update = {};
+ Object.assign(update, data);
+
+ //update.name = await this.ensureUniqueName(update.parent_id, update.name)
+
+ if (data.tags)
+ update.tag_list = this._splitTags(update.tags);
+ //this.addTags(update.tag_list);
+
+ await this.plugins.updateBookmark(update);
+
+ return this._Node.update(update);
+ }
+
+ clean(bookmark) {
+ cleanObject(bookmark, true);
+
+ if (!bookmark.name)
+ bookmark.name = "";
+ }
+
+ async list(options //{search, // filter by node name or URL
+ // path, // filter by hierarchical node group path (string), the first item in the path is a name of a shelf
+ // tags, // filter for node tags (string, containing comma separated list)
+ // date, // filter nodes by date
+ // date2, // the second date in query
+ // period, // chronological period: "between", "before", "after"
+ // types, // filter for node types (array of integers)
+ // limit, // limit for the returned record number
+ // depth, // specify depth of search: "group" (sic!, a folder, used in external api), "subtree" or "root+subtree"
+ // order // order mode to sort the output if specified: "custom", "todo", "date_desc"
+ // content // search in content instead of node name (boolean)
+ // index // index to use: "content", "comments", "notes"
+ // partial // partially match words (boolean)
+ //}
+ ) {
+ const path = options.path || EVERYTHING_SHELF_NAME;
+ let folder = isVirtualShelf(path)? null: await Folder.getByPath(path);
+
+ if (!options.depth)
+ options.depth = "subtree";
+
+ if (options.tags)
+ options.tags = this._splitTags(options.tags);
+
+ let result;
+
+ if (options.content && options.search) {
+ const search = indexString(options.search);
+
+ let subtree;
+ if (path) {
+ subtree = [];
+
+ if (path.toLowerCase() === EVERYTHING_SHELF_NAME)
+ subtree = null;
+ else if (path.toUpperCase() === TODO_SHELF_NAME)
+ subtree = (await Query.todo()).map(n => n.id);
+ else if (path.toUpperCase() === DONE_SHELF_NAME)
+ subtree = (await Query.done()).map(n => n.id);
+ else
+ await Query.selectAllChildrenIdsOf(folder.id, subtree);
+ }
+
+ result = await Query.nodesByIndex(subtree, search, options.index, options.partial);
+ }
+ else {
+ result = await Query.nodes(folder, options);
+ }
+
+ if (path?.toUpperCase() === TODO_SHELF_NAME || path?.toUpperCase() === DONE_SHELF_NAME) {
+ for (let node of result) {
+ node.__extended_todo = true;
+ let pathList = await Path.compute(node);
+
+ node.__path = [];
+ for (let i = 0; i < pathList.length - 1; ++i) {
+ node.__path.push(pathList[i].name)
+ }
+ }
+ }
+
+ result.forEach(n => n.__filtering = true);
+
+ if (options.order === "custom")
+ result.sort(byPosition);
+ else if (options.order === "date_desc")
+ result.sort(byDateAddedDesc);
+
+ return result;
+ }
+
+ async reorder(positions, posProperty = "pos") {
+ try {
+ await this.plugins.reorderBookmarks(positions);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ const id2pos = new Map(positions.map(n => [n.id, n[posProperty]]));
+ await this._Node.batchUpdate(n => n[posProperty] = id2pos.get(n.id), Array.from(id2pos.keys()));
+ }
+
+ async move(ids, destId, moveLast) {
+ const dest = await Node.get(destId);
+ const nodes = await Node.get(ids);
+
+ try {
+ await this.plugins.moveBookmarks(dest, nodes);
+ }
+ catch (e) {
+ console.error(e);
+
+ if (e.name === "EScrapyardPluginError")
+ throw e;
+ }
+
+ // a check for circular references
+ const ascendants = new Set(await Query.ascendantIdsOf(dest));
+ ascendants.add(destId);
+
+ for (const node of nodes)
+ if (ascendants.has(node.id)) {
+ const error = new Error("A circular reference while moving nodes");
+ error.name = "EScrapyardCircularReference";
+ throw error;
+ }
+
+ for (let n of nodes) {
+ n.parent_id = destId;
+ n.name = await this.ensureUniqueName(destId, n.name);
+
+ if (moveLast)
+ n.pos = DEFAULT_POSITION;
+
+ await this._Node.update(n);
+ }
+
+ if (nodes.some(n => n.type === NODE_TYPE_FOLDER))
+ ishellConnector.invalidateCompletion();
+
+ return Query.fullSubtree(ids, true);
+ }
+
+ async copy(ids, destId, moveLast) {
+ const dest = await Node.get(destId);
+ let sourceNodes = await Query.fullSubtree(ids, true);
+ let newNodes = [];
+
+ for (let newNode of sourceNodes) {
+ const sourceNode = {...newNode};
+ const sourceNodeId = newNode.source_node_id = newNode.id;
+
+ if (ids.some(id => id === sourceNodeId)) {
+ newNode.parent_id = destId;
+ newNode.name = await this.ensureUniqueName(destId, newNode.name);
+ }
+ else {
+ let newParent = newNodes.find(nn => nn.source_node_id === newNode.parent_id);
+ if (newParent)
+ newNode.parent_id = newParent.id;
+ }
+
+ delete newNode.id;
+ delete newNode.date_modified;
+
+ if (moveLast && ids.some(id => id === newNode.source_node_id))
+ newNode.pos = DEFAULT_POSITION;
+
+ await this.plugins.beforeBookmarkCopied(dest, newNode);
+
+ newNodes.push(Object.assign(newNode, await this._Node.add(newNode)));
+
+ try {
+ await this.copyContent(sourceNode, newNode);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ let rootNodes = newNodes.filter(n => ids.some(id => id === n.source_node_id));
+
+ try {
+ await this.plugins.copyBookmarks(dest, rootNodes);
+
+ if (rootNodes.some(n => n.type === NODE_TYPE_FOLDER))
+ ishellConnector.invalidateCompletion();
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ return newNodes;
+ }
+
+ async copyContent(sourceNode, newNode) {
+ if (sourceNode.stored_icon) {
+ let icon = await Icon.get(sourceNode);
+ if (icon)
+ await Icon.add(newNode, icon);
+ }
+
+ if (sourceNode.type === NODE_TYPE_ARCHIVE) {
+ let archive = await Archive.get(sourceNode);
+
+ if (archive) {
+ const index = await Archive.fetchIndex(sourceNode);
+ await Archive.add(newNode, archive, index);
+ }
+ }
+
+ if (sourceNode.has_notes) {
+ let notes = await Notes.get(sourceNode);
+ if (notes) {
+ delete notes.id;
+ notes.node_id = newNode.id;
+ await Notes.add(newNode, notes);
+ }
+ }
+
+ if (sourceNode.has_comments) {
+ let comments = await Comments.get(sourceNode);
+ if (comments)
+ await Comments.add(newNode, comments);
+ }
+ }
+
+ async _delete(nodes, deletef) {
+ try {
+ await this.plugins.deleteBookmarks(nodes);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ await deletef(nodes);
+
+ if (nodes.some(n => n.type === NODE_TYPE_FOLDER || n.type === NODE_TYPE_SHELF))
+ ishellConnector.invalidateCompletion();
+ }
+
+ async _hardDelete(nodes) {
+ return this._delete(nodes, nodes => this._Node.delete(nodes));
+ }
+
+ async delete(ids) {
+ const nodes = await Query.fullSubtree(ids);
+
+ return this._hardDelete(nodes);
+ }
+
+ async softDelete(ids) {
+ const nodes = await Query.fullSubtree(ids);
+
+ if (nodes.some(n => n.external === RDF_EXTERNAL_TYPE))
+ return this._hardDelete(nodes);
+
+ return this._delete(nodes, this._undoDelete.bind(this));
+ }
+
+ async _undoDelete(nodes, ids) {
+ await undoManager.pushDeleted(ids, nodes);
+
+ return this._Node.deleteShallow(nodes);
+ }
+
+ async deleteChildren(id) {
+ let all_nodes = await Query.fullSubtree(id);
+
+ await this._Node.delete(all_nodes.filter(n => n.id !== id));
+
+ ishellConnector.invalidateCompletion();
+ }
+
+ async restore(node) {
+ await this._Node.put(node);
+
+ if (node.parent_id) {
+ const parent = await Node.get(node.parent_id);
+ await this.plugins.createBookmark(node, parent);
+ }
+ }
+
+ async storeIcon(node, iconData, contentType) {
+ const convertAndStore = async (iconData, contentType) => {
+ if (iconData.byteLength && contentType && contentType.startsWith("image")) {
+ const byteArray = new Uint8Array(iconData);
+
+ let binaryString = "";
+ for (let i = 0; i < byteArray.byteLength; i++)
+ binaryString += String.fromCharCode(byteArray[i]);
+
+ contentType = contentType.split(";")[0];
+
+ let iconUrl = `data:${contentType};base64,${btoa(binaryString)}`;
+
+ const id = await Icon.import.add(node, iconUrl);
+
+ return [id, iconUrl];
+ }
+ };
+
+ const updateNode = async (node, iconUrl) => {
+ node.stored_icon = true;
+ node.icon = await Icon.computeHash(iconUrl);
+ if (node.id)
+ await this._Node.updateContentModified(node);
+ };
+
+ let iconId;
+ let dataUrl;
+
+ if (node.icon) {
+ try {
+ if (node.icon.startsWith("data:")) {
+ iconId = await Icon.import.add(node, node.icon);
+ dataUrl = node.icon;
+ await updateNode(node, node.icon);
+ }
+ else {
+ if (iconData && contentType) {
+ const [id, iconUrl] = await convertAndStore(iconData, contentType);
+ await updateNode(node, iconUrl);
+ iconId = id;
+ dataUrl = iconUrl;
+ }
+ else {
+ try {
+ const response = await fetch(node.icon);
+
+ if (response.ok) {
+ let type = response.headers.get("content-type");
+
+ if (!type) {
+ let iconUrl = new URL(node.icon);
+ type = getMimetypeByExt(iconUrl.pathname);
+ }
+
+ if (type.startsWith("image")) {
+ const buffer = await response.arrayBuffer();
+ if (buffer.byteLength) {
+ const [id, iconUrl] = await convertAndStore(buffer, type);
+ await updateNode(node, iconUrl);
+ iconId = id;
+ dataUrl = iconUrl;
+ }
+ }
+ }
+ }
+ catch (e) {
+ node.icon = undefined;
+ node.stored_icon = undefined;
+ if (node.id)
+ await this._Node.update(node);
+ console.error(e);
+ }
+ }
+ }
+ }
+ catch (e) {
+ console.error(e);
+ }
+ }
+
+ return [iconId, dataUrl];
+ }
+
+ async storeIconFromURI(node) {
+ try {
+ node.icon = await getFaviconFromContent(node.uri);
+ await this.storeIcon(node);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ async storeArchive(node, data, contentType, index) {
+ const archive = Archive.entity(node, data, contentType);
+
+ if (node.contains === ARCHIVE_TYPE_FILES) {
+ await Archive.storeIndex(node, index.words);
+ await Archive.saveFile(node, "index.html", data);
+ await Archive.updateContentModified(node, archive);
+ }
+ else
+ await Archive.add(node, archive, index);
+
+ await this.plugins.storeBookmarkData(node, data, contentType);
+ }
+
+ async updateArchive(uuid, data) {
+ const node = await Node.getByUUID(uuid);
+ await Archive.updateHTML(node, data);
+ await this.plugins.updateBookmarkData(node, data);
+ }
+
+ async storeNotes(options, propertyChange) {
+ const node = await Node.get(options.node_id);
+ await Notes.add(node, options, propertyChange);
+ await this.plugins.storeBookmarkNotes(node, options, propertyChange);
+ }
+
+ async storeComments(nodeId, comments) {
+ const node = await Node.get(nodeId);
+ await Comments.add(node, comments);
+ await this.plugins.storeBookmarkComments(node, comments);
+ }
+
+ async isSitePage(node) {
+ const parent = await Node.get(node.parent_id);
+ return parent.site;
+ }
+}
+
+export let Bookmark = BookmarkManager.newInstance();
+
+
diff --git a/addon/bookmarks_folder.js b/addon/bookmarks_folder.js
new file mode 100644
index 00000000..0d6565e9
--- /dev/null
+++ b/addon/bookmarks_folder.js
@@ -0,0 +1,168 @@
+import {Query} from "./storage_query.js";
+import {Path} from "./path.js";
+import {EntityManager} from "./bookmarks.js";
+import {NODE_TYPE_FOLDER, NODE_TYPE_SHELF} from "./storage.js";
+import {ishellConnector} from "./plugin_ishell.js";
+import {Node} from "./storage_entities.js";
+
+class FolderManager extends EntityManager {
+ #Node;
+
+ static newInstance() {
+ const instance = new FolderManager();
+
+ instance.idb = new FolderManager();
+
+ return instance;
+ }
+
+ configure() {
+ this.#Node = Node;
+ this.idb.#Node = Node.idb;
+ }
+
+ async add(parent, name, nodeType = NODE_TYPE_FOLDER) {
+ if (parent && typeof parent === "number")
+ parent = await this.#Node.get(parent);
+
+ return this._addNode({
+ name,
+ type: nodeType,
+ parent_id: parent?.id
+ }, parent);
+ }
+
+ async addSite(parentId, name) {
+ const parent = await this.#Node.get(parentId);
+
+ return this._addNode({
+ name,
+ type: NODE_TYPE_FOLDER,
+ parent_id: parentId,
+ site: true
+ }, parent);
+ }
+
+ async _addNode(node, parent) {
+ node.name = await this.ensureUniqueName(parent?.id, node.name);
+ node.external = parent?.external;
+ node = await this.#Node.add(node);
+
+ try {
+ ishellConnector.invalidateCompletion();
+
+ if (parent)
+ await this.plugins.createBookmarkFolder(node, parent);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ return node;
+ }
+
+ // returns map of folders the function was able to find in the path
+ async _queryFolders(pathList) {
+ pathList = [...pathList];
+
+ let folders = [];
+ let shelfName = pathList.shift();
+ let shelf = await Query.shelf(shelfName);
+
+ if (shelf)
+ folders.push(shelf);
+ else
+ return [];
+
+ let parent = shelf;
+ for (let name of pathList) {
+ if (parent) {
+ let folder = await Query.subfolder(parent.id, name);
+ folders.push(folder);
+ parent = folder;
+ }
+ else
+ break;
+ }
+
+ return folders;
+ }
+
+ // returns the last folder in the path if it exists
+ async getByPath(path) {
+ let pathList = Path.split(path);
+ let folders = await this._queryFolders(pathList);
+
+ return folders.at(-1);
+ }
+
+ // creates all non-existent folders in the path
+ async getOrCreateByPath(path) {
+ let pathList = Path.split(path);
+ let folders = await this._queryFolders(pathList);
+ let shelfName = pathList.shift();
+ let parent = folders.shift();
+
+ if (!parent) {
+ parent = await this.#Node.add({
+ name: shelfName,
+ type: NODE_TYPE_SHELF
+ });
+ ishellConnector.invalidateCompletion();
+ }
+
+ let ctr = 0;
+ for (let name of pathList) {
+ let folder = folders[ctr++];
+
+ if (folder) {
+ parent = folder;
+ }
+ else {
+ let node = await this.#Node.add({
+ parent_id: parent.id,
+ external: parent.external,
+ name: name,
+ type: NODE_TYPE_FOLDER
+ });
+
+ try {
+ await this.plugins.createBookmarkFolder(node, parent);
+ ishellConnector.invalidateCompletion();
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ parent = node;
+ }
+ }
+
+ return parent;
+ }
+
+ async rename(id, newName) {
+ let folder = await Node.get(id);
+
+ if (folder.name !== newName) {
+ if (folder.name.toLocaleUpperCase() !== newName.toLocaleUpperCase())
+ folder.name = await this.ensureUniqueName(folder.parent_id, newName, folder.name);
+ else
+ folder.name = newName;
+
+ try {
+ await this.plugins.renameBookmark(folder);
+ ishellConnector.invalidateCompletion();
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ await this.#Node.update(folder);
+ }
+ return folder;
+ }
+
+}
+
+export let Folder = FolderManager.newInstance();
diff --git a/addon/bookmarks_init.js b/addon/bookmarks_init.js
new file mode 100644
index 00000000..1cd1491b
--- /dev/null
+++ b/addon/bookmarks_init.js
@@ -0,0 +1,25 @@
+import {settings} from "./settings.js";
+import {BROWSER_EXTERNAL_TYPE, CLOUD_EXTERNAL_TYPE, FILES_EXTERNAL_TYPE, RDF_EXTERNAL_TYPE} from "./storage.js";
+import {browserShelf} from "./plugin_browser_shelf.js";
+import {cloudShelf} from "./plugin_cloud_shelf.js";
+import {rdfShelf} from "./plugin_rdf_shelf.js";
+import {ishellConnector} from "./plugin_ishell.js";
+import {plugins} from "./bookmarks.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {Folder} from "./bookmarks_folder.js";
+import {filesShelf} from "./plugin_files_shelf.js";
+
+export let systemInitialization = new Promise(async resolve => {
+ await settings.load();
+
+ Bookmark.configure();
+ Folder.configure();
+
+ plugins.registerPlugin(FILES_EXTERNAL_TYPE, filesShelf);
+ plugins.registerPlugin(BROWSER_EXTERNAL_TYPE, browserShelf);
+ plugins.registerPlugin(CLOUD_EXTERNAL_TYPE, cloudShelf);
+ plugins.registerPlugin(RDF_EXTERNAL_TYPE, rdfShelf);
+ plugins.registerPlugin("ishell", ishellConnector);
+
+ resolve(true);
+});
diff --git a/addon/bookmarks_shelf.js b/addon/bookmarks_shelf.js
new file mode 100644
index 00000000..f7c2781a
--- /dev/null
+++ b/addon/bookmarks_shelf.js
@@ -0,0 +1,39 @@
+import {EntityManager} from "./bookmarks.js";
+import {
+ byPosition,
+ EVERYTHING_SHELF_NAME,
+ NODE_TYPE_SHELF,
+ NODE_TYPE_UNLISTED
+} from "./storage.js";
+import {Query} from "./storage_query.js";
+import {Folder} from "./bookmarks_folder.js";
+import {Node} from "./storage_entities.js";
+
+class ShelfManager extends EntityManager {
+
+ add(name) {
+ return Folder.add(null, name, NODE_TYPE_SHELF);
+ }
+
+ async listContent(shelfName) {
+ let nodes = [];
+
+ if (shelfName === EVERYTHING_SHELF_NAME)
+ nodes = await Node.get();
+ else {
+ const shelfNode = await Query.shelf(shelfName);
+ if (shelfNode)
+ nodes = await Query.fullSubtree(shelfNode.id);
+ }
+
+ if (nodes) {
+ nodes = nodes.filter(n => !(n._unlisted || n.type === NODE_TYPE_UNLISTED));
+ nodes.sort(byPosition);
+ }
+
+ return nodes;
+ }
+
+}
+
+export let Shelf = new ShelfManager();
diff --git a/addon/bookmarks_todo.js b/addon/bookmarks_todo.js
new file mode 100644
index 00000000..02e04c5e
--- /dev/null
+++ b/addon/bookmarks_todo.js
@@ -0,0 +1,71 @@
+import {Query} from "./storage_query.js";
+import {Path} from "./path.js";
+
+import {EntityManager} from "./bookmarks.js";
+import {Node} from "./storage_entities.js";
+import {byTODOPosition} from "./storage.js";
+
+class TODOManager extends EntityManager {
+
+ async setState(states) {
+ await Node.update(states);
+ return this.plugins.updateBookmarks(states);
+ }
+
+ async listTODO() {
+ let todo = await Query.todo();
+ todo.reverse();
+ todo.sort(byTODOPosition);
+ todo.sort((a, b) => a.todo_state - b.todo_state);
+
+ let now = new Date();
+ now.setUTCHours(0, 0, 0, 0);
+
+ for (let node of todo) {
+ let todo_date;
+
+ if (node.todo_date && node.todo_date != "")
+ try {
+ todo_date = new Date(node.todo_date);
+ todo_date.setUTCHours(0, 0, 0, 0);
+ }
+ catch (e) {
+ }
+
+ if (todo_date && now >= todo_date)
+ node.__overdue = true;
+
+ let path = await Path.compute(node);
+
+ node.__path = [];
+ for (let i = 0; i < path.length - 1; ++i) {
+ node.__path.push(path[i].name)
+ }
+
+ node.__extended_todo = true;
+ }
+
+ return todo.filter(n => n.__overdue).concat(todo.filter(n => !n.__overdue));
+ }
+
+ async listDONE() {
+ let done = await Query.done();
+ done.sort(byTODOPosition);
+ done.sort((a, b) => a.todo_state - b.todo_state);
+
+ for (let node of done) {
+ let path = await Path.compute(node);
+
+ node.__path = [];
+ for (let i = 0; i < path.length - 1; ++i) {
+ node.__path.push(path[i].name)
+ }
+
+ node.__extended_todo = true;
+ }
+
+ return done;
+ }
+}
+
+export let TODO = new TODOManager();
diff --git a/addon/bookmarks_undo.js b/addon/bookmarks_undo.js
new file mode 100644
index 00000000..c298de69
--- /dev/null
+++ b/addon/bookmarks_undo.js
@@ -0,0 +1,79 @@
+import {Undo} from "./storage_undo.js";
+import {NODE_TYPE_SHELF, UNDO_DELETE} from "./storage.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {Query} from "./storage_query.js";
+import {Node} from "./storage_entities.js";
+
+class UndoManager {
+
+ async canUndo() {
+ return (await Undo.peek()).stack >= 0;
+ }
+
+ async undo() {
+ const undoTop = await Undo.peek();
+
+ if (undoTop.stack >= 0)
+ switch (undoTop.operation) {
+ case UNDO_DELETE:
+ return this.#undoDelete();
+ }
+ }
+
+ async pushDeleted(ids, subtree) {
+ const stackIndex = (await Undo.peek()).stack + 1;
+
+ let ctr = 0;
+ for (const node of subtree) {
+ const undoItem = {
+ stack: stackIndex,
+ operation: UNDO_DELETE,
+ node,
+ selectedIDs: ctr++ === 0? ids: undefined // (!) currently not used
+ };
+
+ await Undo.add(undoItem);
+ }
+ }
+
+ async #undoDelete() {
+ const batch = await Undo.pop();
+ const selectedIDs = batch[0].selectedIDs;
+
+ let shelf;
+ for (const undo of batch) {
+ if (undo.node.type === NODE_TYPE_SHELF)
+ shelf = undo.node;
+
+ await Bookmark.restore(undo.node);
+ }
+
+ if (!shelf)
+ shelf = await Query.rootOf(batch[0].node);
+
+ return {operation: UNDO_DELETE, selectedIDs, shelf};
+ }
+
+ async commit() {
+ let batch = await Undo.pop();
+
+ while (batch) {
+ switch (batch[0].operation) {
+ case UNDO_DELETE:
+ await this.#commitDelete(batch);
+ break;
+ }
+
+ batch = await Undo.pop();
+ }
+ }
+
+ async #commitDelete(batch) {
+ const nodes = batch.map(u => u.node);
+
+ await Node.deleteDependencies(nodes);
+ }
+
+}
+
+export const undoManager = new UndoManager();
diff --git a/addon/browse.js b/addon/browse.js
new file mode 100644
index 00000000..b7a38133
--- /dev/null
+++ b/addon/browse.js
@@ -0,0 +1,323 @@
+import {
+ CLOUD_EXTERNAL_TYPE,
+ NODE_TYPE_ARCHIVE,
+ NODE_TYPE_BOOKMARK, NODE_TYPE_FILE,
+ NODE_TYPE_FOLDER,
+ NODE_TYPE_NOTES,
+ RDF_EXTERNAL_TYPE
+} from "./storage.js";
+import {Archive, Node} from "./storage_entities.js";
+import {Query} from "./storage_query.js";
+import {
+ injectCSSFile,
+ injectScriptFile,
+ openContainerTab,
+ openPage, showNotification,
+ updateTabURL
+} from "./utils_browser.js";
+import {settings} from "./settings.js";
+import {HELPER_APP_v2_1_IS_REQUIRED, HELPER_APP_v2_IS_REQUIRED, helperApp} from "./helper_app.js";
+import {send, sendLocal} from "./proxy.js";
+import {rdfShelf} from "./plugin_rdf_shelf.js";
+import {Bookmark} from "./bookmarks_bookmark.js"
+
+function configureArchiveTab(node, archiveTab) {
+ var tabUpdateListener = async (id, changed, tab) => {
+ if (tab.id === archiveTab.id) {
+ if (changed?.hasOwnProperty("attention"))
+ return;
+
+ if (changed.status === "complete")
+ await configureArchivePage(tab, node);
+ }
+ };
+
+ browser.tabs.onUpdated.addListener(tabUpdateListener);
+
+ function tabRemoveListener(tabId) {
+ if (tabId === archiveTab.id) {
+ if (settings.storage_mode_internal())
+ revokeTrackedObjectURLs(tabId);
+
+ browser.tabs.onRemoved.removeListener(tabRemoveListener);
+ browser.tabs.onUpdated.removeListener(tabUpdateListener);
+ }
+ }
+
+ browser.tabs.onRemoved.addListener(tabRemoveListener);
+}
+
+async function configureArchivePage(tab, node) {
+ if (node.external === CLOUD_EXTERNAL_TYPE && Archive.isUnpacked(node))
+ return;
+
+ await injectCSSFile(tab.id, {file: "ui/edit_toolbar.css"});
+ await injectScriptFile(tab.id, {file: "lib/jquery.js", frameId: 0});
+ if (!_BACKGROUND_PAGE)
+ await injectScriptFile(tab.id, {file: "lib/browser-polyfill.js", frameId: 0});
+ await injectScriptFile(tab.id, {file: "ui/edit_toolbar.js", frameId: 0});
+
+ if ((tab.url?.startsWith("blob:") || tab.url?.startsWith(helperApp.url("/browse")))
+ && settings.open_bookmark_in_active_tab()) {
+ const uuid = tab.url.split("/").at(-2);
+ node = await Node.getByUUID(uuid);
+ }
+ else if (settings.open_bookmark_in_active_tab())
+ node = undefined;
+
+ if (node && await Bookmark.isSitePage(node))
+ await configureSiteLinks(node, tab);
+}
+
+async function configureSiteLinks(node, tab) {
+ await injectScriptFile(tab.id, {file: "content_site.js", allFrames: true});
+ const siteMap = await buildSiteMap(node);
+ browser.tabs.sendMessage(tab.id, {type: "CONFIGURE_SITE_LINKS", siteMap, useProtocol: _BACKGROUND_PAGE});
+}
+
+async function buildSiteMap(node) {
+ const archives = await listSiteArchives(node);
+ return archives.reduce((acc, n) => {
+ acc[n.uri] = n.uuid;
+ return acc;
+ }, {});
+}
+
+function openURL(url, options, newtabf = openPage) {
+ if (options?.tab)
+ return updateTabURL(options.tab, url, options.preserveHistory);
+
+ return newtabf(url, options?.container);
+}
+
+function browseBookmark(node, options) {
+ let url = node.uri;
+ if (url) {
+ try {
+ new URL(url);
+ } catch (e) {
+ url = "http://" + url;
+ }
+
+ if (options)
+ options.container = options.container || node.container;
+ else
+ options = {container: node.container};
+
+ return openURL(url, options, openContainerTab);
+ }
+}
+
+var archiveTabs = {};
+
+function trackArchiveTab(tabId, url) {
+ let urls = archiveTabs[tabId];
+ if (!urls) {
+ urls = new Set([url]);
+ archiveTabs[tabId] = urls;
+ }
+ else
+ urls.add(url);
+}
+
+function isArchiveTabTracked(tabId) {
+ return !!archiveTabs[tabId];
+}
+
+function revokeTrackedObjectURLs(tabId) {
+ const objectURLs = archiveTabs[tabId];
+
+ if (objectURLs) {
+ delete archiveTabs[tabId];
+
+ for (const url of objectURLs)
+ if (url.startsWith("blob:"))
+ URL.revokeObjectURL(url);
+ }
+}
+
+async function browseArchiveIDB(node, options) {
+ if (node.__tentative)
+ return;
+
+ if (node.external === RDF_EXTERNAL_TYPE)
+ return browseRDFArchive(node, options);
+
+ const archive = await Archive.get(node);
+ if (archive) {
+ let objectURL = await getBlobURL(archive);
+
+ if (objectURL) {
+ const archiveURL = objectURL + "#/" + node.uuid + "/";
+ const archiveTab = await openURL(archiveURL, options);
+ const tabTracked = isArchiveTabTracked(archiveTab.id);
+
+ // configureArchiveTab depends on the tracked url
+ trackArchiveTab(archiveTab.id, objectURL);
+
+ if (!tabTracked)
+ configureArchiveTab(node, archiveTab);
+ }
+ }
+ else
+ showNotification({message: "No data is stored."});
+}
+
+async function browseRDFArchive(node, options) {
+ const helper = await helperApp.hasVersion("2.0", HELPER_APP_v2_IS_REQUIRED);
+
+ if (helper) {
+ const url = helperApp.url(`/rdf/browse/${node.uuid}/`);
+ return openURL(url, options);
+ }
+}
+
+async function getBlobURL(archive) {
+ if (archive.data) { // legacy string content
+ let object = new Blob([await Archive.reify(archive)],
+ {type: archive.type? archive.type: "text/html"});
+ return URL.createObjectURL(object);
+ }
+ else
+ return URL.createObjectURL(archive.object);
+}
+
+async function browseArchiveHelper(node, options) {
+ if (node.__tentative)
+ return;
+
+ const helper = await helperApp.hasVersion("2.0", HELPER_APP_v2_IS_REQUIRED);
+
+ if (helper) {
+ let urlPrefix = "";
+ if (node.external === RDF_EXTERNAL_TYPE)
+ urlPrefix = "/rdf";
+
+ const archiveURL = helperApp.url(`${urlPrefix}/browse/${node.uuid}/`);
+ const archiveTab = await openURL(archiveURL, options);
+ return configureArchiveTab(node, archiveTab);
+ }
+}
+
+helperApp.addMessageHandler("REQUEST_ARCHIVE", onRequestArchiveMessage);
+
+export async function onRequestArchiveMessage(msg) {
+ const result = {type: "ARCHIVE_INFO", kind: "empty"};
+
+ try {
+ const node = await Node.getByUUID(msg.uuid);
+
+ if (node) {
+ result.data_path = settings.data_folder_path() || null;
+
+ if (node.external === CLOUD_EXTERNAL_TYPE) {
+ try {
+ const archive = await Archive.get(node);
+
+ if (archive) {
+ const content = await Archive.reify(archive, true);
+
+ result.kind = "content";
+ result.name = node.name;
+ result.uuid = node.uuid;
+ result.content_type = node.content_type || "text/html";
+ result.content = content;
+ result.contains = node.contains || null;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ else {
+ result.kind = "metadata";
+ result.name = node.name;
+ result.content_type = node.content_type || "text/html";
+ result.contains = node.contains || null;
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ return result;
+}
+
+helperApp.addMessageHandler("REQUEST_RDF_PATH", onRequestRdfPathMessage);
+
+export async function onRequestRdfPathMessage(msg) {
+ try {
+ const node = await Node.getByUUID(msg.uuid);
+
+ if (node) {
+ const path = await rdfShelf.getRDFArchiveDir(node);
+
+ return {
+ type: "RDF_PATH",
+ uuid: node.uuid,
+ rdf_archive_path: path
+ };
+ }
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+async function browseFolder(node, options) {
+ if (node.__filtering)
+ send.selectNode({node, open: true, forceScroll: true});
+ else if (node.site) {
+ const archives = await listSiteArchives(node);
+ const page = archives[0];
+ if (page)
+ return browseNode(page, options);
+ }
+}
+
+async function listSiteArchives(node) {
+ const parentId = node.type === NODE_TYPE_ARCHIVE? node.parent_id: node.id;
+ const parent = await Node.get(parentId);
+ const pages = await Query.fullSubtree(parent.id, true);
+ return pages.filter(n => n.type === NODE_TYPE_ARCHIVE);
+}
+
+async function browseFile(node) {
+ const helper = await helperApp.hasVersion("2.1", HELPER_APP_v2_1_IS_REQUIRED);
+
+ if (helper) {
+ return helperApp.postJSON("/files/shell_open_asset", {
+ path: node.uri
+ });
+ }
+}
+
+export async function browseNodeBackground(node, options) {
+ switch (node.type) {
+ case NODE_TYPE_BOOKMARK:
+ return browseBookmark(node, options);
+
+ case NODE_TYPE_ARCHIVE:
+ if (settings.storage_mode_internal())
+ return browseArchiveIDB(node, options);
+ else
+ return browseArchiveHelper(node, options);
+
+ case NODE_TYPE_NOTES: {
+ const edit = options?.edit? "?edit": "";
+ return openURL(`ui/notes.html${edit}#` + node.uuid, options);
+ }
+
+ case NODE_TYPE_FOLDER:
+ return browseFolder(node, options);
+
+ case NODE_TYPE_FILE:
+ return browseFile(node);
+ }
+}
+
+export async function browseNode(node) {
+ if (!_BACKGROUND_PAGE && settings.storage_mode_internal())
+ return sendLocal.browseNode({node});
+ else
+ return browseNodeBackground(node);
+}
+
diff --git a/addon/cloud_client_base.js b/addon/cloud_client_base.js
new file mode 100644
index 00000000..4ba6e0af
--- /dev/null
+++ b/addon/cloud_client_base.js
@@ -0,0 +1,195 @@
+import {CloudStorage} from "./cloud_node_db.js";
+
+const OBJECT_DIRECTORY = "objects";
+const NODE_OBJECT_FILE = "item.json";
+const ICON_OBJECT_FILE = "icon.json";
+const ARCHIVE_INDEX_OBJECT_FILE = "archive_index.json";
+const ARCHIVE_OBJECT_FILE = "archive.json";
+const ARCHIVE_CONTENT_FILE = "archive_content.blob";
+const NOTES_INDEX_OBJECT_FILE = "notes_index.json";
+const NOTES_OBJECT_FILE = "notes.json";
+const COMMENTS_INDEX_OBJECT_FILE = "comments_index.json";
+const COMMENTS_OBJECT_FILE = "comments.json";
+
+export class CloudError {
+ constructor(message) {
+ this.message = message;
+ }
+}
+
+export class CloudItemNotFoundError extends CloudError {
+}
+
+export class CloudClientBase {
+ static CLOUD_SHELF_PATH = "/Cloud";
+ static CLOUD_SHELF_INDEX = "cloud.jsbk";
+ static REDIRECT_URL = "https://gchristensen.github.io/scrapyard/";
+
+ constructor() {
+ this._assetMethods = this._createAssetMethods();
+ }
+
+ get assets() {
+ return this._assetMethods;
+ }
+
+ async authenticate() {
+ return new Promise(async (resolve, reject) => {
+ if (this.isAuthenticated())
+ resolve(true);
+ else {
+ try {
+ let authTab = await browser.tabs.create({url: await this._getAuthorizationUrl()});
+
+ let listener = async (id, changed, tab) => {
+ if (id === authTab.id && changed.url?.startsWith(CloudClientBase.REDIRECT_URL)) {
+ await browser.tabs.onUpdated.removeListener(listener);
+ browser.tabs.remove(authTab.id);
+
+ if (changed.url.includes("code=")) {
+ try {
+ await this._obtainRefreshToken(changed.url);
+ resolve(true);
+ } catch (e) {
+ console.error(e);
+ resolve(false);
+ }
+ }
+ else
+ resolve(false);
+ }
+ };
+
+ browser.tabs.onUpdated.addListener(listener);
+ } catch (e) {
+ console.error(e);
+ resolve(false);
+ }
+ }
+ });
+ }
+
+ _getObjectDirectory(uuid) {
+ return `${CloudClientBase.CLOUD_SHELF_PATH}/${OBJECT_DIRECTORY}/${uuid}`;
+ }
+
+ _getAssetPath(uuid, asset) {
+ return `${CloudClientBase.CLOUD_SHELF_PATH}/${OBJECT_DIRECTORY}/${uuid}/${asset}`;
+ }
+
+ _createAssetMethods() {
+ const storeAsset = asset => {
+ return async (uuid, data) => {
+ try {
+ const path = this._getAssetPath(uuid, asset);
+ await this.uploadFile(path, data);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+ }
+
+ const storeFile = async (uuid, data, file) => {
+ try {
+ const path = this._getAssetPath(uuid, file);
+ await this.uploadFile(path, data);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ const fetchAsset = (asset, binary) => {
+ return async (uuid) => {
+ try {
+ const path = this._getAssetPath(uuid, asset);
+ return await this.downloadFile(path, binary);
+ }
+ catch (e) {
+ console.error(e);
+ }
+ };
+ }
+
+ const fetchFile = async (uuid, file) => {
+ try {
+ const path = this._getAssetPath(uuid, file);
+ return this.downloadFile(path, file);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ const fetchBinaryAsset = (asset, binary) => fetchAsset(asset, true);
+
+ let methods = {};
+
+ methods.storeNode = storeAsset(NODE_OBJECT_FILE);
+
+ methods.storeNotes = storeAsset(NOTES_OBJECT_FILE);
+ methods.fetchNotes = fetchAsset(NOTES_OBJECT_FILE);
+ methods.storeNotesIndex = storeAsset(NOTES_INDEX_OBJECT_FILE);
+ methods.fetchNotesIndex = fetchAsset(NOTES_INDEX_OBJECT_FILE);
+
+ methods.storeArchiveObject = storeAsset(ARCHIVE_OBJECT_FILE);
+ methods.fetchArchiveObject = fetchAsset(ARCHIVE_OBJECT_FILE);
+ methods.storeArchiveContent = storeAsset(ARCHIVE_CONTENT_FILE);
+ methods.storeArchiveFile = storeFile;
+ methods.fetchArchiveContent = fetchBinaryAsset(ARCHIVE_CONTENT_FILE);
+ methods.fetchArchiveFile = fetchFile;
+ methods.storeArchiveIndex = storeAsset(ARCHIVE_INDEX_OBJECT_FILE);
+ methods.fetchArchiveIndex = fetchAsset(ARCHIVE_INDEX_OBJECT_FILE);
+
+ methods.storeIcon = storeAsset(ICON_OBJECT_FILE);
+ methods.fetchIcon = fetchAsset(ICON_OBJECT_FILE);
+
+ methods.storeComments = storeAsset(COMMENTS_OBJECT_FILE);
+ methods.fetchComments = fetchAsset(COMMENTS_OBJECT_FILE);
+ methods.storeCommentsIndex = storeAsset(COMMENTS_INDEX_OBJECT_FILE);
+ methods.fetchCommentsIndex = fetchAsset(COMMENTS_INDEX_OBJECT_FILE);
+
+ return methods;
+ }
+
+ async deleteAssets(uuids) {
+ for (const uuid of uuids) {
+ try {
+ const path = this._getObjectDirectory(uuid);
+ await this.deleteFile(path);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ async downloadDB() {
+ let storage = null;
+
+ try {
+ const path = `${CloudClientBase.CLOUD_SHELF_PATH}/${CloudClientBase.CLOUD_SHELF_INDEX}`;
+ const content = await this.downloadFile(path);
+ storage = CloudStorage.deserialize(content);
+ }
+ catch (e) {
+ if (e instanceof CloudItemNotFoundError) {
+ storage = new CloudStorage();
+ }
+ else if (e instanceof CloudError)
+ throw e;
+ }
+
+ if (storage)
+ Object.assign(storage, this._assetMethods);
+
+ return storage;
+ }
+
+ async persistDB(db) {
+ const path = `${CloudClientBase.CLOUD_SHELF_PATH}/${CloudClientBase.CLOUD_SHELF_INDEX}`;
+ const content = db.serialize();
+ this.uploadFile(path, content);
+ }
+
+ _replaceSpecialChars(filename) {
+ return filename.replace(/[\\\/:*?"<>|\[\]()^#%&!@:+={}'~]/g, "_");
+ }
+}
diff --git a/addon/cloud_client_dropbox.js b/addon/cloud_client_dropbox.js
new file mode 100644
index 00000000..a2fe15c7
--- /dev/null
+++ b/addon/cloud_client_dropbox.js
@@ -0,0 +1,133 @@
+import {send} from "./proxy.js";
+import {settings} from "./settings.js";
+import DropboxAuth from "./lib/dropbox/auth.js";
+import Dropbox from "./lib/dropbox/dropbox.js"
+import {readBlob} from "./utils_io.js";
+import {CloudClientBase, CloudItemNotFoundError} from "./cloud_client_base.js";
+
+const APP_KEY = "0y7co3j1k4oc7up";
+
+export class DropboxClient extends CloudClientBase {
+ constructor() {
+ super()
+ this.ID = "dropbox";
+ this.dbxAuth = new DropboxAuth({clientId: APP_KEY});
+ this.dbx = new Dropbox({auth: this.dbxAuth});
+ }
+
+ initialize() {
+ let refreshToken = settings.dropbox_refresh_token();
+ if (refreshToken) {
+ this.dbxAuth.setRefreshToken(refreshToken);
+ }
+ else {
+ browser.runtime.onMessage.addListener((request) => {
+ if (request.type === "dropboxAuthenticated") {
+ this.dbxAuth.setRefreshToken(request.refreshToken);
+ }
+ });
+ }
+ }
+
+ isAuthenticated() {
+ return !!settings.dropbox_refresh_token();
+ }
+
+ signOut() {
+ settings.dropbox_refresh_token(null);
+ }
+
+ _getAuthorizationUrl() {
+ return this.dbxAuth.getAuthenticationUrl(CloudClientBase.REDIRECT_URL, undefined,
+ 'code', 'offline', undefined, undefined, true);
+ }
+
+ async _obtainRefreshToken(url) {
+ const code = url.match(/.*code=(.*)$/i)[1];
+ let response = await this.dbxAuth.getAccessTokenFromCode(CloudClientBase.REDIRECT_URL, code);
+ const refreshToken = response.result.refresh_token;
+ this.dbxAuth.setRefreshToken(refreshToken);
+
+ await settings.dropbox_refresh_token(refreshToken);
+ send.dropboxAuthenticated({refreshToken});
+
+ if (settings.dropbox___dbat())
+ settings.dropbox___dbat(null);
+ }
+
+ async uploadFile(path, data) {
+ await this.dbx.filesUpload({
+ path,
+ contents: data,
+ mode: "overwrite",
+ mute: true
+ });
+ }
+
+ async downloadFile(path, binary) {
+ let result = null;
+
+ try {
+ const {result: {fileBlob}} = await this.dbx.filesDownload({path});
+ result = readBlob(fileBlob, binary? "binary": null);
+ }
+ catch (e) {
+ if (e.status === 409) { // no index.js file
+ if (e.error.error_summary.startsWith("path/not_found"))
+ throw new CloudItemNotFoundError();
+ }
+ else
+ console.error(e);
+ }
+
+ return result;
+ }
+
+ async deleteFile(path) {
+ await this.dbx.filesDeleteV2({path});
+ }
+
+ async share(path, filename, content) {
+ await this.authenticate();
+ return this.dbx.filesUpload({
+ path: path + this._replaceSpecialChars(filename),
+ mode: "add",
+ autorename: true,
+ mute: false,
+ strict_conflict: false,
+ contents: content
+ });
+ };
+
+ async reset() {
+ try {
+ const {result: {entries}} = await this.dbx.filesListFolder({path: CloudClientBase.CLOUD_SHELF_PATH});
+
+ if (entries && entries.length) {
+ const files = {entries: entries.map(f => ({path: f.path_display}))};
+ await this.dbx.filesDeleteBatch(files);
+ }
+ }
+ catch (e) {
+ console.error(e);
+ }
+ }
+
+ async getLastModified() {
+ try {
+ const {result: meta} = await this.dbx.filesGetMetadata({
+ path: `${CloudClientBase.CLOUD_SHELF_PATH}/${CloudClientBase.CLOUD_SHELF_INDEX}`
+ });
+
+ if (meta && meta.server_modified)
+ return new Date(meta.server_modified);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ return null;
+ }
+}
+
+export let dropboxClient = new DropboxClient();
diff --git a/addon/cloud_client_onedrive.js b/addon/cloud_client_onedrive.js
new file mode 100644
index 00000000..069f989c
--- /dev/null
+++ b/addon/cloud_client_onedrive.js
@@ -0,0 +1,274 @@
+// TODO: has a known issue with multiple login attempts: state string from storage and url differ, currently unresolved
+// to reproduce log out from OneDrive and try to log in again; some wait time (probably lifetime of a refresh token) helps
+// to login successfully
+
+import {CloudClientBase, CloudError, CloudItemNotFoundError} from "./cloud_client_base.js";
+import {PKCE} from "./lib/PKCE.js";
+import {settings} from "./settings.js";
+import {send} from "./proxy.js";
+
+const GRAPH_API_ENDPOINT = "https://graph.microsoft.com/v1.0";
+
+export class OneDriveClient extends CloudClientBase {
+ constructor() {
+ super()
+ this.ID = "onedrive"
+ this._pkce = new PKCE({
+ client_id: "c4d0a237-f00c-41a4-ac9f-f7aa4d88e857",
+ redirect_uri: 'https://gchristensen.github.io/scrapyard',
+ authorization_endpoint: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize',
+ token_endpoint: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token',
+ requested_scopes: 'offline_access User.Read Files.ReadWrite Files.ReadWrite.AppFolder',
+ });
+ }
+
+ initialize() {
+ let refreshToken = settings.onedrive_refresh_token();
+ if (refreshToken) {
+ this._refreshToken = refreshToken;
+ }
+ else {
+ browser.runtime.onMessage.addListener((request) => {
+ if (request.type === "onedriveAuthenticated") {
+ this._refreshToken = request.refreshToken;
+ }
+ });
+ }
+ }
+
+ isAuthenticated() {
+ return !!settings.onedrive_refresh_token();
+ }
+
+ signOut() {
+ settings.onedrive_refresh_token(null);
+ }
+
+ _getAuthorizationUrl() {
+ return this._pkce.getAuthorizationUrl();
+ }
+
+ async _applyTokens(response) {
+ if (response.access_token) {
+ this._refreshToken = response.refresh_token;
+ this._accessToken = response.access_token;
+ this._accessTokenExpires = Date.now() + response.expires_in * 1000;
+
+ await settings.onedrive_refresh_token(this._refreshToken);
+ }
+ else if (response.error?.code) { // MS Graph error on refresh token
+ this._refreshToken = null;
+ this._accessToken = null;
+
+ await settings.onedrive_refresh_token(null);
+ }
+ }
+
+ async _obtainRefreshToken(url) {
+ const response = await this._pkce.exchangeForAccessToken(url);
+ await this._applyTokens(response);
+ send.onedriveAuthenticated({refreshToken: this._refreshToken});
+ }
+
+ async _refreshAccessToken() {
+ const response = await this._pkce.refreshAccessToken(this._refreshToken);
+ await this._applyTokens(response);
+ }
+
+ _injectToken(params) {
+ if (!this._accessToken)
+ throw new CloudError("OneDrive is not authorized.");
+
+ params = params || {};
+ params.headers = params.headers || {}
+ params.headers["Authorization"] = `Bearer ${this._accessToken}`;
+ return params;
+ }
+
+ async _makeGraphRequest(path, params) {
+ params = this._injectToken(params);
+ const response = await fetch(`${GRAPH_API_ENDPOINT}/${path}`, params);
+ if (response.ok)
+ return response;
+ else
+ throw await response.json();
+ }
+
+ async _makeRequest(path, params) {
+ if (this._accessToken && this._accessTokenExpires - 5000 > Date.now()) {
+ try {
+ return this._makeGraphRequest(path, params);
+ }
+ catch (e) {
+ if (e.error?.code === "InvalidAuthenticationToken") {
+ await this._refreshAccessToken();
+ return this._makeGraphRequest(path, params);
+ }
+ else
+ throw e;
+ }
+ }
+ else {
+ await this._refreshAccessToken();
+ return this._makeGraphRequest(path, params);
+ }
+ }
+
+ async _makeTextRequest(path, params) {
+ const response = await this._makeRequest(path, params);
+ return response.text();
+ }
+
+ async _makeBinaryRequest(path, params) {
+ const response = await this._makeRequest(path, params);
+ return response.arrayBuffer();
+ }
+
+ async _makeJSONRequest(path, params) {
+ const response = await this._makeRequest(path, params);
+ return response.json();
+ }
+
+ _getDrivePath(path) {
+ return `/me/drive/special/approot:${path}`;
+ }
+
+ _uploadSmallFile(path, bytes) {
+ const headers = {"Content-Type": "text/plain"};
+ return this._makeRequest(path + ":/content", {method: "put", body: bytes, headers});
+ }
+
+ async _uploadLargeFile(path, bytes, mode = "replace") {
+ const sessionParams = {
+ item: {
+ "@microsoft.graph.conflictBehavior": mode
+ }
+ };
+
+ const sessionPath = path + ":/createUploadSession";
+ const session = await this._makeJSONRequest(sessionPath, {
+ method: "post",
+ body: JSON.stringify(sessionParams),
+ headers: {"Content-Type": "application/json"}
+ });
+
+ const CHUNK_SIZE = 60 * 320 * 1024;
+ let fullChunks = Math.floor(bytes.byteLength / CHUNK_SIZE);
+
+ for (let i = 0; i < fullChunks; ++i) {
+ const start = i * CHUNK_SIZE;
+ const end = start + CHUNK_SIZE;
+ const chunk = bytes.slice(start, end);
+
+ await this._sendSessionBytes(session.uploadUrl, chunk, start, end - 1, bytes.byteLength);
+ }
+
+ const remained = bytes.byteLength - fullChunks * CHUNK_SIZE;
+ if (remained > 0) {
+ const start = fullChunks * CHUNK_SIZE;
+ const end = bytes.byteLength;
+ const chunk = bytes.slice(start, end);
+
+ await this._sendSessionBytes(session.uploadUrl, chunk, start, end - 1, bytes.byteLength);
+ }
+ }
+
+ async _sendSessionBytes(url, bytes, start, end, size) {
+ const headers = {
+ "Content-Length": `${bytes.byteLength}`,
+ "Content-Range": `bytes ${start}-${end}/${size}`
+ };
+ return fetch(url, {method: "put", body: bytes, headers});
+ }
+
+ async uploadFile(path, data) {
+ const requestPath = this._getDrivePath(path);
+ let bytes = data;
+
+ if (typeof data === "string") {
+ const encoder = new TextEncoder();
+ bytes = encoder.encode(data);
+ }
+
+ if (bytes.byteLength < 4 * 1024 * 1024)
+ return this._uploadSmallFile(requestPath, bytes)
+ else
+ return this._uploadLargeFile(requestPath, bytes);
+ }
+
+ async downloadFile(path, binary) {
+ const requestPath = this._getDrivePath(path) + ":/content";
+ try {
+ if (binary)
+ return await this._makeBinaryRequest(requestPath);
+ else
+ return await this._makeTextRequest(requestPath);
+ }
+ catch (e) {
+ if (e instanceof CloudError)
+ throw e;
+ else if (e.error?.code === "itemNotFound")
+ throw new CloudItemNotFoundError();
+
+ console.error(e)
+ }
+ }
+
+ async deleteFile(path) {
+ const requestPath = this._getDrivePath(path)
+ return this._makeRequest(requestPath, {method: "delete"});
+ }
+
+ async share(path, filename, content) {
+ await this.authenticate();
+
+ let bytes = content;
+ if (content instanceof Blob)
+ bytes = await content.arrayBuffer();
+ else if (typeof content === "string") {
+ const encoder = new TextEncoder();
+ bytes = encoder.encode(content);
+ }
+
+ if (path === "/")
+ path = "";
+
+ filename = this._replaceSpecialChars(filename);
+ const requestPath = this._getDrivePath(`${path}/${filename}`);
+ return this._uploadLargeFile(requestPath, bytes, "rename");
+ };
+
+ async reset() {
+ try {
+ // TODO: add suport for @odata.nextLink
+ const requestPath = this._getDrivePath(CloudClientBase.CLOUD_SHELF_PATH) + ":/children";
+ const driveItems = await this._makeJSONRequest(requestPath);
+
+ for (let item of driveItems.value) {
+ const path = `${CloudClientBase.CLOUD_SHELF_PATH}/${item.name}`;
+ await this.deleteFile(path);
+ }
+ }
+ catch (e) {
+ console.error(e);
+ }
+ }
+
+ async getLastModified() {
+ const requestPath =
+ this._getDrivePath(`${CloudClientBase.CLOUD_SHELF_PATH}/${CloudClientBase.CLOUD_SHELF_INDEX}`);
+
+ try {
+ const driveItem = await this._makeJSONRequest(requestPath);
+ return new Date(driveItem.lastModifiedDateTime);
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ return null;
+ }
+}
+
+
+export let oneDriveClient = new OneDriveClient();
diff --git a/addon/cloud_node_db.js b/addon/cloud_node_db.js
new file mode 100644
index 00000000..ce979b48
--- /dev/null
+++ b/addon/cloud_node_db.js
@@ -0,0 +1,124 @@
+import {
+ CLOUD_SHELF_UUID,
+ NODE_TYPE_SHELF,
+ createJSONScrapBookMeta,
+ updateJSONScrapBookMeta,
+ JSON_SCRAPBOOK_FOLDERS
+} from "./storage.js";
+import UUID from "./uuid.js";
+
+export class CloudStorage {
+ constructor() {
+ this._objects = new Map();
+ }
+
+ static deserialize(jsonLines) {
+ const storage = new CloudStorage();
+
+ const lines = jsonLines.split("\n").filter(s => !!s);
+ storage._meta = lines.length? JSON.parse(lines.shift()): {};
+
+ if (lines.length > 0) {
+ for (const line of lines) {
+ const object = JSON.parse(line);
+ storage._objects.set(object.uuid, object);
+ }
+ }
+
+ return storage;
+ }
+
+ serialize() {
+ const hasMeta = !!this._meta;
+ this._meta = this._meta || createJSONScrapBookMeta("cloud");
+
+ if (!hasMeta)
+ this._meta.uuid = UUID.numeric();
+
+ const objects = this._treeSortObjects();
+
+ updateJSONScrapBookMeta(this._meta, objects.length);
+
+ let lines = [JSON.stringify(this._meta)];
+ lines = [...lines, ...objects.map(o => JSON.stringify(o))];
+ return lines.join("\n");
+ }
+
+ _sanitized(object) {
+ object = {...object};
+
+ delete object.external;
+ delete object.external_id;
+
+ return object;
+ }
+
+ addNode(object) {
+ if (object.type !== NODE_TYPE_SHELF)
+ this._objects.set(object.uuid, this._sanitized(object));
+ }
+
+ getNode(uuid) {
+ return this._objects.get(uuid);
+ }
+
+ updateNode(object) {
+ if (object.type !== NODE_TYPE_SHELF) {
+ const existing = this._objects.get(object.uuid);
+
+ if (existing) {
+ const node = existing;
+ Object.assign(node, object);
+ this.addNode(node);
+ }
+ else
+ this.addNode(object);
+ }
+ }
+
+ deleteNodes(nodes) {
+ if (!Array.isArray(nodes))
+ nodes = [nodes];
+
+ for (let node of nodes)
+ this._objects.delete(node.uuid);
+ }
+
+ get meta() {
+ return this._meta;
+ }
+
+ get nodes() {
+ return Array.from(this._objects.values());
+ }
+
+ get sortedNodes() {
+ return this._treeSortObjects();
+ }
+
+ _treeSortObjects() {
+ const children = new Map();
+ children.set(CLOUD_SHELF_UUID, []);
+
+ for (const object of this._objects.values()) {
+ if (children.has(object.parent))
+ children.get(object.parent).push(object.uuid);
+ else
+ children.set(object.parent, [object.uuid]);
+ }
+
+ const getSubtree = (parentUUID, acc = []) => {
+ const childrenUUIDs = children.get(parentUUID);
+
+ if (childrenUUIDs)
+ for (const uuid of childrenUUIDs) {
+ acc.push(this._objects.get(uuid));
+ getSubtree(uuid, acc);
+ }
+
+ return acc;
+ }
+
+ return getSubtree(CLOUD_SHELF_UUID);
+ }
+}
diff --git a/addon/content_edit_frames.js b/addon/content_edit_frames.js
new file mode 100644
index 00000000..4a5f4d13
--- /dev/null
+++ b/addon/content_edit_frames.js
@@ -0,0 +1,8 @@
+
+browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ switch (message.type) {
+ case "GET_FRAME_HTML":
+ configureSiteLinks(message.siteMap);
+ break;
+ }
+});
diff --git a/addon/content_favicon.js b/addon/content_favicon.js
new file mode 100644
index 00000000..d20c1fcb
--- /dev/null
+++ b/addon/content_favicon.js
@@ -0,0 +1 @@
+document.querySelector("link[rel*='icon'], link[rel*='shortcut']")?.href
diff --git a/addon/content_mark.js b/addon/content_mark.js
new file mode 100644
index 00000000..b6d8cc7e
--- /dev/null
+++ b/addon/content_mark.js
@@ -0,0 +1 @@
+console.log(location.href)
diff --git a/addon/content_selection.js b/addon/content_selection.js
new file mode 100644
index 00000000..97b6b4a2
--- /dev/null
+++ b/addon/content_selection.js
@@ -0,0 +1,115 @@
+
+function captureSelection(options) {
+ const EXTRACTION_ID_ATTR = "scrapyard-selection-extraction-id";
+
+ let sel = window.getSelection();
+ let root = null;
+ let id = 1;
+
+ function retainStructure(parent, content) {
+ let parents = [];
+
+ if (parent.nodeType === 3)
+ parent = parent.parentNode;
+
+ // mark all encountered parents including
+ while (parent && parent.localName !== "html") {
+ let parentId = parent.getAttribute(EXTRACTION_ID_ATTR);
+ if (!parentId)
+ parent.setAttribute(EXTRACTION_ID_ATTR, id++);
+
+ parents.push(parent.cloneNode(false));
+ parent = parent.parentElement;
+ }
+
+ parents.reverse();
+ parents.shift(); // drop ;
+
+ if (parents.length) {
+ let next = parents.shift();
+
+ // traverse parents, drop ones that already attached to root
+ while (parents.length
+ && root.querySelector(`*[${EXTRACTION_ID_ATTR}='${parents[0].getAttribute(EXTRACTION_ID_ATTR)}']`)) {
+ next = parents.shift();
+ }
+
+ let existing = root.querySelector(`*[${EXTRACTION_ID_ATTR}='${next.getAttribute(EXTRACTION_ID_ATTR)}']`);
+
+ if (!existing)
+ root.appendChild(next)
+ else
+ next = existing;
+
+ // append all unseen nodes
+ for (let parent of parents) {
+ next.appendChild(parent);
+ next = parent;
+ }
+
+ next.appendChild(content);
+ }
+ else
+ root.appendChild(content);
+ }
+
+
+ if ((!sel || sel.isCollapsed) && (options._selector || options._filter)) {
+ root = document.createElement("div")
+
+ let parts = options._selector
+ ? Array.prototype.slice.call(document.querySelectorAll(options._selector))
+ : Array.prototype.slice.call(document.body.childNodes);
+
+ for (let part of parts) {
+ retainStructure(part.parentNode, part.cloneNode(true));
+ }
+
+ if (options._filter) {
+ let filtered = root.querySelectorAll(options._filter);
+
+ filtered.forEach(n => {
+ n.parentNode.removeChild(n);
+ })
+ }
+ }
+ else if (sel && !sel.isCollapsed) {
+ root = document.createElement("div")
+
+ for (let i = 0; i < sel.rangeCount; ++i) {
+ let range = sel.getRangeAt(i);
+
+ if (range.isCollapsed)
+ continue;
+
+ retainStructure(range.commonAncestorContainer, range.cloneContents());
+ }
+ }
+
+ document.querySelectorAll(`*[${EXTRACTION_ID_ATTR}]`)
+ .forEach(e => e.removeAttribute(EXTRACTION_ID_ATTR));
+
+ let html;
+
+ if (root) {
+ root.querySelectorAll(`*[${EXTRACTION_ID_ATTR}]`).forEach(e => e.removeAttribute(EXTRACTION_ID_ATTR));
+
+ html = root.innerHTML;
+
+ if (options._style) {
+ let style = options._style.replace(//g, '>')
+ html = `` + html;
+ }
+ }
+
+ return html;
+}
+
+browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ switch (message.type) {
+ case "CAPTURE_SELECTION":
+ sendResponse(captureSelection(message.options));
+ break;
+ }
+});
diff --git a/addon/content_site.js b/addon/content_site.js
new file mode 100644
index 00000000..3a55be8d
--- /dev/null
+++ b/addon/content_site.js
@@ -0,0 +1,75 @@
+var configured;
+
+function makeReferenceURL(uuid, useProtocol) {
+ let referenceURL = `ext+scrapyard://${uuid}`;
+
+ if (!useProtocol)
+ referenceURL = browser.runtime.getURL(`/reference.html#${referenceURL}`);
+
+ return referenceURL;
+}
+
+function parseHtml(htmlText) {
+ let doc = document.implementation.createHTMLDocument("")
+ , doc_elt = doc.documentElement
+ , first_elt;
+
+ doc_elt.innerHTML = htmlText;
+ first_elt = doc_elt.firstElementChild;
+
+ if (doc_elt.childElementCount === 1
+ && first_elt.localName.toLowerCase() === "html") {
+ doc.replaceChild(first_elt, doc_elt);
+ }
+
+ return doc;
+}
+
+function installScrapyardURLs(doc, siteMap, blank, useProtocol) {
+ doc.querySelectorAll("a").forEach(
+ function (element) {
+ const url = element.getAttribute("data-scrapyard-href");
+ const uuid = siteMap[url];
+ if (uuid) {
+ element.href = makeReferenceURL(uuid, useProtocol);
+ if (blank)
+ element.setAttribute("target", "_blank");
+ }
+ });
+}
+
+function processIFrames(doc, siteMap, useProtocol) {
+ doc.querySelectorAll("iframe").forEach(
+ function (element) {
+ const html = element.srcdoc;
+
+ if (html) {
+ const srcdoc = parseHtml(html);
+ processIFrames(srcdoc, siteMap, useProtocol);
+ installScrapyardURLs(srcdoc, siteMap, true, useProtocol);
+ element.srcdoc = "" + srcdoc.documentElement.outerHTML;
+ }
+ else if (element.contentWindow?.document) {
+ processIFrames(element.contentWindow.document, siteMap, useProtocol);
+ installScrapyardURLs(element.contentWindow.document, siteMap, true, useProtocol);
+ }
+ });
+}
+
+function configureSiteLinks(siteMap, useProtocol) {
+ installScrapyardURLs(document, siteMap, false, useProtocol);
+ processIFrames(document, siteMap, useProtocol);
+}
+
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ switch (message.type) {
+ case "CONFIGURE_SITE_LINKS":
+ if (!configured) {
+ configured = true;
+ configureSiteLinks(message.siteMap, message.useProtocol);
+ }
+ break;
+ }
+});
+
+console.log("content_site.js loaded")
diff --git a/addon/core.js b/addon/core.js
new file mode 100644
index 00000000..ac78552e
--- /dev/null
+++ b/addon/core.js
@@ -0,0 +1,99 @@
+import {grantPersistenceQuota, startupLatch} from "./utils_browser.js";
+import {receive, receiveExternal, sendLocal} from "./proxy.js";
+import {systemInitialization} from "./bookmarks_init.js";
+import {browserShelf} from "./plugin_browser_shelf.js";
+import {cloudShelf} from "./plugin_cloud_shelf.js";
+import {addBookmarkOnCommand, setBookmarkedActionIcon} from "./bookmarking.js";
+import {toggleSidebarWindow} from "./utils_sidebar.js";
+import {undoManager} from "./bookmarks_undo.js";
+import {helperApp} from "./helper_app.js";
+import {settings} from "./settings.js";
+import * as search from "./search.js";
+import "./core_bookmarking.js";
+import "./core_share.js";
+import "./core_backup.js"
+import "./core_backends.js";
+import "./core_maintenance.js";
+import "./core_ishell.js";
+import "./core_automation.js";
+import "./core_sync.js";
+import "./core_transition.js";
+import {filesShelf} from "./plugin_files_shelf.js";
+
+if (_BACKGROUND_PAGE)
+ import("./core_import.js");
+
+if (_BACKGROUND_PAGE && _MANIFEST_V3)
+ import("./mv3_persistent.js");
+
+receiveExternal.startListener(true);
+receive.startListener(true);
+
+(async () => {
+ if (await grantPersistenceQuota()) {
+ await systemInitialization;
+
+ await startupLatch(performStartupInitialization);
+ }
+})();
+
+async function performStartupInitialization() {
+ search.initializeOmnibox();
+
+ await browserShelf.reconcileBrowserBookmarksDB();
+
+ if (settings.enable_files_shelf()) {
+ await filesShelf.createIfMissing();
+ }
+
+ if (settings.cloud_enabled()) {
+ await cloudShelf.createIfMissing();
+ await cloudShelf.enableBackgroundSync(settings.cloud_background_sync());
+ }
+
+ if (!settings.storage_mode_internal())
+ await helperApp.probe();
+
+ await undoManager.commit();
+
+ if (!settings.storage_mode_internal() && settings.synchronize_storage_at_startup())
+ await sendLocal.performSync();
+
+ console.log("==> core.js initialized");
+}
+
+if (browser.webRequest) {
+ // remove the Origin header from add-on fetch requests
+ function originWithId(header) {
+ return header.name.toLowerCase() === 'origin' && header.value.startsWith('moz-extension://');
+ }
+
+ browser.webRequest.onBeforeSendHeaders.addListener(
+ (details) => {
+ return {
+ requestHeaders: details.requestHeaders.filter(x => !originWithId(x))
+ }
+ },
+ {urls: [""]},
+ ["blocking", "requestHeaders"]
+ );
+}
+
+browser.commands.onCommand.addListener(function(command) {
+ if (!_SIDEBAR && command === "toggle_sidebar_window")
+ toggleSidebarWindow();
+ else
+ addBookmarkOnCommand(command);
+});
+
+browser.tabs.onActivated.addListener(async activeInfo => {
+ const tab = await browser.tabs.get(activeInfo.tabId);
+ return setBookmarkedActionIcon(tab.url);
+});
+
+browser.tabs.onUpdated.addListener(async (tabId, changed, tab) => {
+ if (tab.status === "complete")
+ return setBookmarkedActionIcon(tab.url);
+});
+
+console.log("==> core.js loaded");
diff --git a/addon/core_automation.js b/addon/core_automation.js
new file mode 100644
index 00000000..45504984
--- /dev/null
+++ b/addon/core_automation.js
@@ -0,0 +1,602 @@
+import {helperApp} from "./helper_app.js";
+import UUID from "./uuid.js";
+import {
+ isContainerNode, isBuiltInShelf, byPosition,
+ TODO_STATE_NAMES, TODO_STATES,
+ CLOUD_SHELF_NAME, CLOUD_SHELF_UUID,
+ DEFAULT_SHELF_NAME, DEFAULT_SHELF_UUID,
+ BROWSER_SHELF_NAME, BROWSER_SHELF_UUID,
+ NODE_TYPE_ARCHIVE, NODE_TYPE_BOOKMARK, NODE_TYPE_NOTES, NODE_TYPE_NAMES, ARCHIVE_TYPE_TEXT
+} from "./storage.js";
+import {settings} from "./settings.js";
+import {getFaviconFromContent, getFaviconFromTab} from "./favicon.js";
+import {send, receiveExternal, sendLocal} from "./proxy.js";
+import {getActiveTab} from "./utils_browser.js";
+import {getMimetypeByExt} from "./utils.js";
+import {fetchText} from "./utils_io.js";
+import {ishellConnector} from "./plugin_ishell.js";
+import {captureTab, isSpecialPage, notifySpecialPage, packUrlExt} from "./bookmarking.js";
+import {Query} from "./storage_query.js";
+import {Path} from "./path.js";
+import {Folder} from "./bookmarks_folder.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {Archive, Comments, Icon, Node, Notes} from "./storage_entities.js";
+import {browseNode} from "./browse.js";
+import {DiskStorage} from "./storage_external.js";
+import {notes2html} from "./notes_render.js";
+
+export function isAutomationAllowed(sender) {
+ const extension_whitelist = settings.extension_whitelist();
+
+ return ishellConnector.isIShell(sender.id)
+ || (settings.enable_automation() && (!extension_whitelist
+ || extension_whitelist.some(id => id.toLowerCase() === sender.id.toLowerCase())));
+}
+
+const ALLOWED_API_FIELDS = ["type", "uuid", "title", "url", "icon", "path", "tags", "details", "todo_state", "todo_date",
+ "comments", "container", "contains", "content", "content_type", "pack", "local", "select", "refresh",
+ "hide_tab"];
+
+function sanitizeIncomingObject(object) {
+ for (let key of Object.keys(object))
+ if (!ALLOWED_API_FIELDS.some(k => k === key))
+ delete object[key];
+
+ return object;
+}
+
+receiveExternal.scrapyardGetVersion = (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ sendLocal.scrapyardIdRequested({senderId: sender.id});
+ return browser.runtime.getManifest().version;
+};
+
+export async function createBookmarkNode(message, sender, activeTab) {
+ const node = {...message};
+
+ sanitizeIncomingObject(node);
+
+ node.__automation = true;
+
+ if (node.type === NODE_TYPE_NOTES || node.type === NODE_TYPE_ARCHIVE && node.url === "")
+ node.uri = undefined;
+ else if (!node.uri)
+ node.uri = node.url || activeTab.url;
+
+ if (node.type !== NODE_TYPE_NOTES && (node.uri === null || node.uri === undefined || isSpecialPage(node.uri))) {
+ notifySpecialPage();
+ return null;
+ }
+
+ if (!node.name)
+ node.name = node.title || activeTab.title;
+
+ if (node.icon === "" || node.pack)
+ node.icon = undefined;
+ else if (!node.icon && !node.local)
+ node.icon = await getFaviconFromTab(activeTab);
+
+ if (node.todo_state)
+ node.todo_state = TODO_STATES[node.todo_state];
+
+ const path = Path.expand(node.path);
+ const folder = await Folder.getOrCreateByPath(path);
+ node.parent_id = folder.id;
+ delete node.path;
+
+ return node;
+}
+
+receiveExternal.scrapyardAddBookmark = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ message.type = NODE_TYPE_BOOKMARK;
+
+ const node = await createBookmarkNode(message, sender, await getActiveTab());
+ if (!node)
+ return;
+
+ if (node.icon === true || node.title === true) {
+ try {
+ const content = await fetchText(node.uri);
+
+ if (node.icon === true)
+ node.icon = await getFaviconFromContent(node.uri, content);
+
+ if (node.title === true) {
+ const title = content.match(/]*>([^<]*) {
+ if (node.comments)
+ await Bookmark.storeComments(bookmark.id, node.comments);
+
+ if (node.select)
+ send.bookmarkCreated({node: bookmark});
+
+ return bookmark.uuid;
+ });
+};
+
+receiveExternal.scrapyardAddArchive = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ let activeTab = await getActiveTab();
+
+ message.type = NODE_TYPE_ARCHIVE;
+
+ const node = await createBookmarkNode(message, sender, activeTab);
+ if (!node)
+ return;
+
+ if (!node.name || node.name === true)
+ node.name = "Unnamed";
+
+ if (!node.content_type)
+ node.content_type = getMimetypeByExt(node.uri);
+
+ let saveContent = (bookmark, content) => {
+ const contentType = node.pack? "text/html": node.content_type;
+
+ return Bookmark.storeArchive(bookmark, content, contentType, bookmark.__index)
+ .then(() => {
+ if (node.select)
+ send.bookmarkCreated({node: bookmark});
+
+ return bookmark.uuid;
+ });
+ };
+
+ return Bookmark.idb.add(node, NODE_TYPE_ARCHIVE) // added to storage in Archive.add
+ .then(async bookmark => {
+
+ if (node.comments)
+ await Bookmark.storeComments(bookmark.id, node.comments);
+
+ if (node.local) {
+ let content = await downloadLocalContent(bookmark);
+
+ bookmark.uri = "";
+ //await Bookmark.update(bookmark);
+
+ return saveContent(bookmark, content);
+ }
+ else if (node.pack) {
+ const page = await packUrlExt(node.url, node.hide_tab);
+
+ if (page.icon) {
+ bookmark.icon = page.icon
+ await Bookmark.storeIcon(bookmark);
+ }
+
+ bookmark.name = page.title;
+
+ //await Bookmark.update(bookmark);
+
+ return saveContent(bookmark, page.html);
+ }
+ else if (node.content) {
+ return saveContent(bookmark, node.content)
+ }
+ else {
+ Object.assign(bookmark, node);
+ bookmark.__tab_id = activeTab.id;
+ captureTab(activeTab, bookmark);
+
+ return bookmark.uuid;
+ }
+ });
+};
+
+async function downloadLocalContent(node) {
+ const localURI = await setUpLocalFileCapture(node);
+
+ let content;
+ if (node.content_type === "text/html") {
+ const page = await packUrlExt(localURI, node.hide_tab);
+
+ if (page.icon && (node.icon === null || node.icon === undefined)) {
+ node.icon = page.icon
+ await Bookmark.storeIcon(node);
+ }
+
+ if (page.title)
+ node.name = page.title;
+
+ content = page.html;
+ }
+ else {
+ const response = await fetch(localURI);
+ if (response.ok) {
+ node.content_type = response.headers.get("content-type") || node.content_type;
+ content = await response.arrayBuffer();
+ }
+ }
+
+ await cleanUpLocalFileCapture(node);
+ return content;
+}
+
+async function setUpLocalFileCapture(message) {
+ if (message.uri?.startsWith("http"))
+ throw new Error("HTTP URL is processed as a local path.");
+
+ let local_uri;
+ if (await helperApp.probe()) {
+ message.uri = message.uri.replace(/^file:\/+/i, "");
+
+ message.__local_uuid = UUID.numeric();
+ await helperApp.post(`/serve/set_path/${message.__local_uuid}`, {path: message.uri});
+ local_uri = helperApp.url(`/serve/file/${message.__local_uuid}/`);
+ message.uri = "";
+ return local_uri;
+ }
+ else {
+ throw new Error("Can not connect to the backend application.");
+ }
+}
+
+async function cleanUpLocalFileCapture(message) {
+ if (message.local)
+ await helperApp.fetch(`/serve/release_path/${message.__local_uuid}`);
+}
+
+receiveExternal.scrapyardAddNotes = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ message.type = NODE_TYPE_NOTES;
+ message.icon = "";
+
+ const node = await createBookmarkNode(message, sender, {});
+ if (!node)
+ return;
+
+ return Bookmark.add(node, NODE_TYPE_NOTES)
+ .then(async bookmark => {
+ const options = {
+ node_id: bookmark.id,
+ content: message.content || "",
+ format: message.format || "text"
+ };
+
+ options.html = notes2html(options);
+
+ await Bookmark.storeNotes(options);
+
+ if (node.comments)
+ await Bookmark.storeComments(bookmark.id, node.comments);
+
+ if (node.select)
+ send.bookmarkCreated({node: bookmark});
+
+ return bookmark.uuid;
+ });
+};
+
+receiveExternal.scrapyardAddSeparator = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ if (message.path) {
+ const path = Path.expand(message.path);
+ const folder = await Folder.getOrCreateByPath(path);
+
+ const node = await Bookmark.addSeparator(folder.id);
+
+ if (message.select)
+ send.bookmarkCreated({node});
+ }
+}
+
+async function nodeToAPIObject(node) {
+ const comments =
+ node.has_comments
+ ? await Comments.get(node)
+ : undefined;
+
+ const icon =
+ node.stored_icon
+ ? await Icon.get(node)
+ : node.icon;
+
+ const uuid = node.uuid;
+
+ const options = {
+ type: NODE_TYPE_NAMES[node.type],
+ uuid: uuid,
+ title: node.name || ""
+ };
+
+ if (node.uri)
+ options.url = node.uri;
+
+ if (icon)
+ options.icon = icon;
+
+ if (node.tags)
+ options.tags = node.tags;
+
+ if (node.details)
+ options.details = node.details;
+
+ if (node.todo_state)
+ options.todo_state = TODO_STATE_NAMES[node.todo_state];
+
+ if (node.todo_date)
+ options.todo_date = node.todo_date;
+
+ if (comments)
+ options.comments = comments;
+
+ if (node.container)
+ options.container = node.container;
+
+ if (node.contains)
+ options.contains = node.contains;
+
+ options.path = await Path.asString(node);
+
+ return options;
+}
+
+receiveExternal.scrapyardGetUuid = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ const node = await Node.getByUUID(message.uuid);
+
+ if (node)
+ return nodeToAPIObject(node);
+};
+
+receiveExternal.scrapyardGetUuidContent = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ const node = await Node.getByUUID(message.uuid);
+ let result = {};
+
+ if (node) {
+ if (node.type === NODE_TYPE_ARCHIVE) {
+ const archive = await Archive.get(node);
+
+ if (archive) {
+ result.content = archive.object;
+ result.contains = node.contains || ARCHIVE_TYPE_TEXT;
+ result.content_type = node.content_type;
+ }
+ }
+ else if (node.type === NODE_TYPE_NOTES) {
+ const notes = await Notes.get(node);
+
+ if (notes) {
+ result.content = notes.content;
+ result.format = notes.format;
+ }
+ }
+ }
+
+ if (result.content)
+ return result;
+};
+
+
+receiveExternal.scrapyardListUuid = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ let entries;
+ let container;
+ if (message.uuid === null) {
+ entries = await Query.allShelves();
+ container = true;
+ }
+ else {
+ const API_UUID_TO_DB = {
+ [CLOUD_SHELF_NAME]: CLOUD_SHELF_UUID,
+ [BROWSER_SHELF_NAME]: BROWSER_SHELF_UUID,
+ [DEFAULT_SHELF_NAME]: DEFAULT_SHELF_UUID,
+ };
+
+ const uuid =
+ isBuiltInShelf(message.uuid)
+ ? API_UUID_TO_DB[message.uuid]
+ : message.uuid;
+
+ const node = await Node.getByUUID(uuid);
+ container = node && isContainer(node);
+ if (container)
+ entries = await Node.getChildren(node.id);
+ else
+ entries = [];
+ }
+
+ entries.sort(byPosition);
+
+ let result = [];
+
+ for (let entry of entries) {
+ result.push(await nodeToAPIObject(entry));
+ }
+
+ return container? result: undefined;
+};
+
+receiveExternal.scrapyardListPath = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ let entries;
+ let container;
+
+ if (message.path === "/") {
+ entries = await Query.allShelves();
+ container = true;
+ }
+ else {
+ const path = Path.expand(message.path);
+ const node = await Folder.getByPath(path);
+
+ container = !!node;
+
+ if (container)
+ entries = await Node.getChildren(node.id);
+ else
+ entries = [];
+ }
+
+ entries.sort(byPosition);
+
+ let result = [];
+
+ for (let entry of entries) {
+ result.push(await nodeToAPIObject(entry));
+ }
+
+ return container? result: undefined;
+};
+
+receiveExternal.scrapyardGetSelection = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ let result = [];
+
+ try {
+ const selectedNodes = await send.getTreeSelection();
+
+ for (let entry of selectedNodes) {
+ result.push(await nodeToAPIObject(entry));
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ return result;
+};
+
+receiveExternal.scrapyardUpdateUuid = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ if (isBuiltInShelf(message.uuid))
+ throw new Error("Can not modify built-in shelves.");
+
+ const refresh = message.refresh;
+
+ delete message.type;
+ sanitizeIncomingObject(message);
+
+ if (message.url) {
+ message.uri = message.url;
+ delete message.url;
+ }
+
+ message.name = message.title || "";
+ delete message.title;
+
+ if (message.todo_state) {
+ if (typeof message.todo_state === "number"
+ && (message.todo_state < TODO_STATE_TODO
+ || message.todo_state > TODO_STATE_CANCELLED)) {
+ message.todo_state = undefined;
+ }
+ else if (typeof message.todo_state === "string")
+ message.todo_state = TODO_STATES[message.todo_state.toUpperCase()];
+ }
+
+ const node = await Node.getByUUID(message.uuid);
+
+ Object.assign(node, message);
+
+ if (message.icon === "") {
+ message.icon = undefined;
+ message.stored_icon = undefined;
+ }
+ else if (message.icon)
+ await Bookmark.storeIcon(node);
+
+ if (message.hasOwnProperty("comments")) {
+ await Bookmark.storeComments(node.id, message.comments);
+ delete node.comments;
+ }
+
+ await Bookmark.update(node);
+
+ if (refresh)
+ send.nodesUpdated();
+};
+
+receiveExternal.scrapyardRemoveUuid = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ const node = await Node.getByUUID(message.uuid);
+
+ if (node)
+ await Bookmark.delete(node.id);
+
+ if (message.refresh)
+ send.nodesUpdated();
+};
+
+receiveExternal.scrapyardPackPage = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ if (message.local) {
+ if (!message.uri)
+ message.uri = message.url;
+
+ let local_uri = await setUpLocalFileCapture(message);
+
+ let result = await packUrlExt(local_uri, message.hide_tab);
+
+ await cleanUpLocalFileCapture(message);
+
+ return result;
+ }
+ else
+ return packUrlExt(message.url, message.hide_tab);
+};
+
+receiveExternal.scrapyardBrowseUuid = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ const node = await Node.getByUUID(message.uuid);
+ if (node)
+ browseNode(node);
+};
+
+receiveExternal.scrapyardOpenBatchSession = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ return DiskStorage.openBatchSession();
+};
+
+receiveExternal.scrapyardCloseBatchSession = async (message, sender) => {
+ if (!isAutomationAllowed(sender))
+ throw new Error();
+
+ return DiskStorage.closeBatchSession();
+};
diff --git a/addon/core_backends.js b/addon/core_backends.js
new file mode 100644
index 00000000..41d54dc5
--- /dev/null
+++ b/addon/core_backends.js
@@ -0,0 +1,70 @@
+import {browserShelf} from "./plugin_browser_shelf.js";
+import {settings} from "./settings.js";
+import {cloudShelf} from "./plugin_cloud_shelf.js";
+import {helperApp} from "./helper_app.js";
+import {receive} from "./proxy.js";
+import {filesShelf} from "./plugin_files_shelf.js";
+
+receive.uiLockGet = message => {
+ browserShelf.getUILock();
+};
+
+receive.uiLockRelease = message => {
+ browserShelf.releaseUILock();
+};
+
+receive.memorizeUIBookmarks = message => {
+ browserShelf.lockUIBookmarks(message.bookmarks, message.category);
+}
+
+receive.getListenerLockState = message => {
+ return browserShelf.isLockedByListeners();
+};
+
+receive.reconcileBrowserBookmarkDb = async message => {
+ await settings.load()
+ browserShelf.reconcileBrowserBookmarksDB();
+};
+
+receive.reconcileCloudBookmarkDb = async message => {
+ await settings.load();
+ cloudShelf.reconcileCloudBookmarksDB(message.verbose);
+};
+
+receive.enableCloudBackgroundSync = async message => {
+ cloudShelf.enableBackgroundSync(message.enable);
+};
+
+receive.addFilesDirectory = async message => {
+ return filesShelf.addDirectory(message.options);
+};
+
+receive.reconcileExternalFiles = async message => {
+ return filesShelf.reconcileExternalFiles();
+};
+
+receive.openWithEditor = async message => {
+ return filesShelf.openWithEditor(message.node);
+};
+
+receive.browseOrgWikiReference = message => {
+ return filesShelf.openExternalLink(message.link, message.node);
+};
+
+receive.helperAppProbe = message => {
+ return helperApp.probe(message.verbose);
+};
+
+receive.helperAppGetVersion = async message => {
+ await helperApp.probe();
+ return helperApp.getVersion();
+};
+
+receive.helperAppHasVersion = async message => {
+ return helperApp.hasVersion(message.version, message.alert);
+};
+
+receive.helperAppGetBackgroundAuth = message => {
+ return helperApp.auth;
+};
+
diff --git a/addon/core_backup.js b/addon/core_backup.js
new file mode 100644
index 00000000..b80e13cc
--- /dev/null
+++ b/addon/core_backup.js
@@ -0,0 +1,143 @@
+import {send} from "./proxy.js";
+import {helperApp} from "./helper_app.js";
+import {isVirtualShelf} from "./storage.js";
+import {receive} from "./proxy.js"
+import UUID from "./uuid.js";
+import {sleep} from "./utils.js";
+import {Export, Import} from "./import.js";
+import {Query} from "./storage_query.js";
+import {LineStream} from "./utils_io.js";
+
+receive.listBackups = message => {
+ let form = new FormData();
+ form.append("directory", message.directory);
+
+ return helperApp.fetchJSON(`/backup/list`, {method: "POST", body: form});
+};
+
+receive.backupShelf = async message => {
+ let shelf, shelfName, shelfUUID;
+
+ if (isVirtualShelf(message.shelf))
+ shelf = shelfUUID = shelfName = message.shelf;
+ else {
+ shelf = await Query.shelf(message.shelf);
+ shelfUUID = shelf.uuid;
+ shelfName = shelf.name;
+ }
+
+ let nodes = await Export.nodes(shelf);
+
+ let backupFile = `${UUID.date()}_${shelfUUID}.jsonl`
+
+ const process = helperApp.post("/backup/initialize", {
+ directory: message.directory,
+ file: backupFile,
+ compress: message.compress,
+ method: message.method,
+ level: message.level
+ });
+
+ const port = await helperApp.getPort();
+
+ const file = {
+ append: async function (text) {
+ port.postMessage({
+ type: "BACKUP_PUSH_TEXT",
+ text: text
+ })
+ }
+ };
+
+ await sleep(50);
+
+ try {
+ const exporter = Export.create("json")
+ .setName(shelfName)
+ .setUUID(shelfUUID)
+ .setComment(message.comment)
+ .setReportProgress(true)
+ .setMuteSidebar(true)
+ .setObjects(nodes)
+ .setStream(file)
+ .build();
+
+ await exporter.export();
+ }
+ finally {
+ port.postMessage({
+ type: "BACKUP_FINISH"
+ });
+ }
+
+ await process;
+};
+
+receive.restoreShelf = async message => {
+ send.startProcessingIndication({noWait: true});
+
+ let error;
+ let shelf;
+
+ try {
+ await helperApp.post("/restore/initialize", {
+ directory: message.directory,
+ file: message.meta.file
+ });
+
+ const Reader = class {
+ async* lines() {
+ while (true) {
+ const response = await helperApp.fetch("/restore/get_line");
+ if (response.ok) {
+ const line = await response.text();
+ if (line)
+ yield line;
+ else
+ break;
+ }
+ else
+ throw new Error("unknown error");
+ }
+ }
+ };
+
+ const shelfName = message.new_shelf? message.meta.alt_name: message.meta.name;
+ const importer = Import.create("json")
+ .setName(shelfName)
+ .setReportProgress(true)
+ .setMuteSidebar(true)
+ .setStream(new LineStream(new Reader()))
+ .build();
+
+ shelf = await Import.transaction(importer);
+ } catch (e) {
+ console.log(e.stack);
+ error = e;
+ }
+ finally {
+ await helperApp.fetch("/restore/finalize");
+ send.stopProcessingIndication();
+ send.nodesImported({shelf});
+ }
+
+ if (error)
+ throw error;
+};
+
+receive.deleteBackup = async message => {
+ send.startProcessingIndication({noWait: true});
+
+ try {
+ await helperApp.post("/backup/delete", {
+ directory: message.directory,
+ file: message.meta.file
+ });
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
+
+ send.stopProcessingIndication();
+ return true;
+}
diff --git a/addon/core_bookmarking.js b/addon/core_bookmarking.js
new file mode 100644
index 00000000..6ffb6b84
--- /dev/null
+++ b/addon/core_bookmarking.js
@@ -0,0 +1,439 @@
+import {formatBytes, getMimetypeByExt} from "./utils.js";
+import {receive, send, sendLocal} from "./proxy.js";
+import {
+ BROWSER_EXTERNAL_TYPE,
+ CLOUD_SHELF_ID,
+ NODE_TYPE_ARCHIVE,
+ NODE_TYPE_BOOKMARK,
+ NODE_TYPE_NOTES,
+ UNDO_DELETE
+} from "./storage.js";
+import {askCSRPermission, getActiveTab, gettingStarted, showNotification, updateTabURL} from "./utils_browser.js";
+import {HELPER_APP_v2_IS_REQUIRED, helperApp} from "./helper_app.js";
+import {settings} from "./settings.js";
+import {
+ captureTab,
+ finalizeCapture,
+ isSpecialPage,
+ notifySpecialPage,
+ packUrlExt,
+ showSiteCaptureOptions,
+ performSiteCapture,
+ startCrawling,
+ abortCrawling,
+ archiveBookmark, addToBookmarksToolbar
+} from "./bookmarking.js";
+import {fetchText} from "./utils_io.js";
+import {TODO} from "./bookmarks_todo.js";
+import {Folder} from "./bookmarks_folder.js";
+import {Shelf} from "./bookmarks_shelf.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {Archive, Node} from "./storage_entities.js";
+import {undoManager} from "./bookmarks_undo.js";
+import {browseNodeBackground} from "./browse.js";
+import UUID from "./uuid.js";
+import {ensureSidebarWindow} from "./utils_sidebar.js";
+import {getFaviconFromContent, getFaviconFromTab} from "./favicon.js";
+
+async function canUseBookmarking() {
+ return settings.storage_mode_internal() || await helperApp.hasVersion("2.0", HELPER_APP_v2_IS_REQUIRED);
+}
+
+receive.createShelf = message => Shelf.add(message.name);
+
+receive.createFolder = message => Folder.add(message.parent, message.name);
+
+receive.renameFolder = message => Folder.rename(message.id, message.name);
+
+receive.addSeparator = message => Bookmark.addSeparator(message.parent_id);
+
+receive.createBookmark = async message => {
+ if (!settings.storage_mode_internal() && !settings.data_folder_path())
+ return gettingStarted();
+
+ if (!await canUseBookmarking())
+ return;
+
+ const node = message.node;
+
+ if (isSpecialPage(node.uri))
+ return notifySpecialPage();
+
+ async function addBookmark() {
+ try {
+ const bookmark = await Bookmark.add(node, NODE_TYPE_BOOKMARK);
+
+ if (settings.add_to_bookmarks_toolbar())
+ await addToBookmarksToolbar(bookmark);
+
+ send.bookmarkAdded({node: bookmark});
+ return bookmark;
+ }
+ catch (e) {
+ showNotification(e.message);
+ send.bookmarkCreationFailed({node});
+ }
+ }
+
+ Bookmark.setTentativeId(node);
+ node.type = NODE_TYPE_BOOKMARK; // needed for beforeBookmarkAdded
+ return send.beforeBookmarkAdded({node})
+ .then(addBookmark)
+ .catch(addBookmark);
+};
+
+receive.updateBookmark = message => Bookmark.update(message.node);
+
+receive.createArchive = async message => {
+ if (!settings.storage_mode_internal() && !settings.data_folder_path())
+ return gettingStarted();
+
+ if (!await canUseBookmarking())
+ return;
+
+ const node = message.node;
+ const tab = message.tab || await getActiveTab();
+
+ if (isSpecialPage(node.uri))
+ return notifySpecialPage();
+
+ async function addBookmark() {
+ try {
+ const bookmark = await Bookmark.idb.add(node, NODE_TYPE_ARCHIVE); // added to the storage on archive content update
+
+ if (settings.add_to_bookmarks_toolbar())
+ await addToBookmarksToolbar(bookmark);
+
+ bookmark.__tab_id = tab.id;
+ captureTab(tab, bookmark); // !sic
+ return bookmark;
+ }
+ catch (e) {
+ showNotification(e.message);
+ send.bookmarkCreationFailed({node});
+ }
+ }
+
+ if (node.__crawl && !node.__site_capture) {
+ const tab = await getActiveTab();
+ showSiteCaptureOptions(tab, node);
+ return;
+ }
+
+ Bookmark.setTentativeId(node);
+ node.type = NODE_TYPE_ARCHIVE; // needed for beforeBookmarkAdded
+ return send.beforeBookmarkAdded({node: node})
+ .then(addBookmark)
+ .catch(addBookmark);
+};
+
+receive.archiveBookmarks = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ send.startProcessingIndication();
+
+ try {
+ for (const node of message.nodes) {
+ if (node.type === NODE_TYPE_BOOKMARK)
+ await archiveBookmark(node);
+ }
+ }
+ finally {
+ send.nodesUpdated();
+ send.stopProcessingIndication();
+ }
+};
+
+receive.updateArchive = message => Bookmark.updateArchive(message.uuid, message.data);
+
+receive.createNotes = async message => {
+ if (!settings.storage_mode_internal() && !settings.data_folder_path())
+ return gettingStarted();
+
+ if (!await canUseBookmarking())
+ return;
+
+ const node = message.node;
+
+ async function addNotes() {
+ try {
+ const bookmark = await Bookmark.addNotes(node.parent_id, node.name);
+
+ if (settings.add_to_bookmarks_toolbar())
+ await addToBookmarksToolbar(bookmark);
+
+ send.bookmarkAdded({node: bookmark});
+ return bookmark;
+ }
+ catch (e) {
+ showNotification(e.message);
+ send.bookmarkCreationFailed({node});
+ }
+ }
+
+ Bookmark.setTentativeId(node);
+ node.type = NODE_TYPE_NOTES; // needed for beforeBookmarkAdded
+
+ return send.beforeBookmarkAdded({node})
+ .then(addNotes)
+ .catch(addNotes);
+};
+
+receive.captureHighlightedTabs = async message => {
+ const options = message.options;
+ const highlightedTabs = await browser.tabs.query({highlighted: true, currentWindow: true});
+ let parentNode;
+
+ if (options.type === NODE_TYPE_ARCHIVE) {
+ parentNode = await Node.get(options.parent_id);
+
+ if (parentNode.external === BROWSER_EXTERNAL_TYPE) {
+ showNotification({title: "Warning", message: "Only bookmarks are saved to the browser bookmarks folder."});
+ }
+ }
+
+ if (highlightedTabs.length === 1)
+ return captureBookmarkForTab(highlightedTabs[0], message.options, parentNode);
+ else
+ for (const tab of highlightedTabs) {
+ const node = {...message.options};
+
+ node.uri = tab.url;
+ node.name = tab.title;
+ node.icon = await getFaviconFromTab(tab);
+
+ await captureBookmarkForTab(tab, node, parentNode);
+ }
+} ;
+
+async function captureBookmarkForTab(tab, node, parentNode) {
+ if (node.type === NODE_TYPE_ARCHIVE) {
+ if (parentNode?.external === BROWSER_EXTERNAL_TYPE)
+ return sendLocal.createBookmark({node});
+ else
+ return sendLocal.createArchive({node, tab});
+ }
+ else
+ return sendLocal.createBookmark({node});
+}
+
+receive.setTODOState = message => TODO.setState(message.nodes);
+
+receive.getBookmarkInfo = async (message, sender) => {
+ const node = await Node.getByUUID(message.uuid);
+
+ node.__formatted_size = node.size ? formatBytes(node.size) : null;
+ node.__formatted_date = node.date_added
+ ? node.date_added.toString().replace(/:[^:]*$/, "")
+ : null;
+ node.__tab_id = sender.tab.id;
+
+ return node;
+};
+
+receive.closeNotes = async message => browser.tabs.sendMessage(message.tabId, {type: "SCRAPYARD_CLOSE_NOTES"});
+
+receive.getHideToolbarSetting = async message => {
+ await settings.load();
+ return settings.do_not_show_archive_toolbar();
+};
+
+receive.copyNodes = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ return Bookmark.copy(message.node_ids, message.dest_id, message.move_last);
+};
+
+receive.shareToCloud = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ return Bookmark.copy(message.node_ids, CLOUD_SHELF_ID, true);
+}
+
+receive.moveNodes = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ return Bookmark.move(message.node_ids, message.dest_id, message.move_last);
+};
+
+receive.deleteNodes = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ return Bookmark.delete(message.node_ids);
+};
+
+receive.softDeleteNodes = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ return Bookmark.softDelete(message.node_ids);
+};
+
+receive.reorderNodes = async message => {
+ if (!await canUseBookmarking())
+ return;
+
+ return Bookmark.reorder(message.positions, message.posProperty);
+};
+
+receive.storePageHtml = message => {
+ if (message.bookmark.__url_packing)
+ return;
+
+ return Bookmark.storeArchive(message.bookmark, message.html, "text/html", message.bookmark.__index)
+ .then(() => {
+ if (!message.bookmark.__mute_ui) {
+ browser.tabs.sendMessage(message.bookmark.__tab_id, {type: "UNLOCK_DOCUMENT"});
+
+ finalizeCapture(message.bookmark);
+
+ if (message.bookmark.__crawl)
+ startCrawling(message.bookmark);
+ }
+ })
+ .catch(e => {
+ console.error(e);
+ if (!message.bookmark.__mute_ui) {
+ chrome.tabs.sendMessage(message.bookmark.__tab_id, {type: "UNLOCK_DOCUMENT"});
+ showNotification("Error archiving page.");
+ }
+ });
+};
+
+receive.addNotes = message => Bookmark.addNotes(message.parent_id, message.name);
+
+receive.storeNotes = message => Bookmark.storeNotes(message.options, message.property_change);
+
+receive.uploadFiles = async message => {
+ send.startProcessingIndication();
+
+ try {
+ const helper = await helperApp.hasVersion("0.4", `Scrapyard backend application v0.4+ is required for this feature.`);
+
+ if (helper) {
+ const fileUUID = UUID.numeric();
+ await helperApp.post(`/serve/set_path/${fileUUID}`, {path: message.file_name});
+
+ //const uuids = await helperApp.fetchJSON("/upload/open_file_dialog");
+ const uuids = {[fileUUID]: message.file_name};
+
+ for (const [uuid, file] of Object.entries(uuids)) {
+ const url = helperApp.url(`/serve/file/${uuid}/`);
+ const isHtml = /\.html?$/i.test(file);
+
+ let bookmark = {uri: "", parent_id: message.parent_id};
+
+ bookmark.name = file.replaceAll("\\", "/").split("/");
+ bookmark.name = bookmark.name[bookmark.name.length - 1];
+
+ let content;
+ let contentType = getMimetypeByExt(file);
+
+ try {
+ if (isHtml) {
+ const page = await packUrlExt(url);
+ bookmark.name = page.title || bookmark.name;
+ bookmark.icon = page.icon;
+ content = page.html;
+ }
+ else {
+ const response = await fetch(url);
+ if (response.ok) {
+ contentType = response.headers.get("content-type") || contentType;
+ content = await response.arrayBuffer();
+ }
+ }
+
+ bookmark = await Bookmark.add(bookmark, NODE_TYPE_ARCHIVE);
+ if (content)
+ await Bookmark.storeArchive(bookmark, content, contentType);
+ else
+ throw new Error();
+ } catch (e) {
+ console.error(e);
+ showNotification(`Can not upload ${bookmark.name}`);
+ }
+
+ await helperApp.fetch(`/serve/release_path/${uuid}`);
+ }
+ if (Object.entries(uuids).length)
+ send.nodesUpdated();
+ }
+ }
+ finally {
+ send.stopProcessingIndication();
+ }
+}
+
+// receive.browseNode = async message => {
+// return browseNode(message.node, message);
+// };
+
+receive.browseNode = async message => {
+ if (!_BACKGROUND_PAGE && settings.storage_mode_internal()) {
+ await ensureSidebarWindow();
+ return send.browseNodeSidebar(message);
+ }
+ else
+ return browseNodeBackground(message.node, message);
+};
+
+receive.browseNotes = message => {
+ (message.tab
+ ? updateTabURL(message.tab, "ui/notes.html#" + message.uuid, false)
+ : browser.tabs.create({"url": "ui/notes.html#" + message.uuid}));
+};
+
+receive.browseOrgReference = message => {
+ location.href = message.link;
+};
+
+receive.loadInternalResource = async message => {
+ const url = browser.runtime.getURL(message.path);
+ return await fetchText(url);
+};
+
+receive.abortRequested = message => {
+ abortCrawling();
+};
+
+receive.replyFrameSiteCapture = (message, sender) => {
+ browser.tabs.sendMessage(sender.tab.id, message);
+};
+
+receive.cancelSiteCapture = (message, sender) => {
+ browser.tabs.sendMessage(sender.tab.id, message);
+};
+
+receive.continueSiteCapture = (message, sender) => {
+ browser.tabs.sendMessage(sender.tab.id, message);
+};
+
+receive.performSiteCapture = (message, sender) => {
+ performSiteCapture(message.bookmark);
+};
+
+receive.performUndo = async message => {
+ send.startProcessingIndication();
+ try {
+ const result = await undoManager.undo();
+
+ switch (result.operation) {
+ case UNDO_DELETE:
+ send.nodesImported({shelf: result.shelf});
+ break;
+ }
+ }
+ finally {
+ send.stopProcessingIndication();
+ }
+};
+
+receive.saveResource = async message => {
+ return Archive.saveFile(message.node, message.filename, message.content);
+};
diff --git a/addon/core_import.js b/addon/core_import.js
new file mode 100644
index 00000000..63602d51
--- /dev/null
+++ b/addon/core_import.js
@@ -0,0 +1,199 @@
+import {isBuiltInShelf} from "./storage.js";
+import {LineStream, LineReader, readFile} from "./utils_io.js";
+import {ishellConnector} from "./plugin_ishell.js";
+import {settings} from "./settings.js";
+import {receive} from "./proxy.js";
+import {Import, Export} from "./import.js";
+import {helperApp} from "./helper_app.js";
+import {sleep} from "./utils.js";
+import UUID from "./uuid.js";
+import {ExportArea} from "./storage_export.js";
+
+receive.importFile = async message => {
+ const shelf = isBuiltInShelf(message.file_name)? message.file_name.toLocaleLowerCase(): message.file_name;
+ const format = message.file_ext.toLowerCase();
+ const importerBuilder = Import.create(format);
+
+ if (importerBuilder) {
+ importerBuilder.setName(shelf)
+ importerBuilder.setReportProgress(true);
+ importerBuilder.setSidebarContext(!_BACKGROUND_PAGE);
+
+ switch (format) {
+ case "json":
+ case "jsonl":
+ case "jsbk":
+ importerBuilder.setStream(new LineStream(new LineReader(message.file)));
+ break;
+ case "org":
+ case "html":
+ importerBuilder.setStream(await readFile(message.file));
+ break;
+ case "rdf":
+ importerBuilder.setStream(message.file);
+ importerBuilder.setNumberOfThreads(message.threads);
+ importerBuilder.setQuickImport(message.quick);
+ importerBuilder.setCreateIndex(message.createIndex);
+ break;
+ }
+
+ const importer = importerBuilder.build();
+
+ let invalidationState = ishellConnector.isInvalidationEnabled();
+ ishellConnector.enableInvalidation(false);
+ return Import.transaction(importer).finally(() => {
+ ishellConnector.enableInvalidation(invalidationState);
+ ishellConnector.invalidateCompletion();
+ });
+ }
+};
+
+receive.exportFile = async message => {
+ let shelf = message.shelf;
+ let shelfName = message.shelf;
+
+ if (typeof shelf === "string") {
+ shelfName = shelf;
+
+ if (isBuiltInShelf(shelfName))
+ shelfName = shelfName.toLocaleLowerCase();
+ }
+ else
+ shelfName = shelf.name;
+
+ let format = message.format || "json";
+
+ let shallowExport = false;
+ if (format === "org_links") {
+ shallowExport = true;
+ format = "org";
+ }
+
+ let fileExt = ".jsbk";
+ if (format === "org")
+ fileExt = ".org";
+
+ const fileName = message.fileName.replace(/[\\\/:*?"<>|^#%&!@+={}'~]/g, "_") + fileExt;
+
+ let nodes = await Export.nodes(shelf, format === "org");
+
+ const exportBuilder = Export.create(format)
+ .setName(shelfName)
+ .setUUID(message.uuid)
+ .setLinksOnly(shallowExport)
+ .setReportProgress(true)
+ .setObjects(nodes)
+ .setSidebarContext(!_BACKGROUND_PAGE);
+
+
+ if (settings.storage_mode_internal())
+ await exportStandalone(exportBuilder, fileName, format);
+ else
+ await exportWithHelperApp(exportBuilder, fileName, format);
+};
+
+async function exportWithHelperApp(exportBuilder, fileName, format) {
+ try {
+ helperApp.fetch("/export/initialize");
+ } catch (e) {
+ console.error(e);
+ }
+
+ const port = await helperApp.getPort();
+
+ const file = {
+ append: async function (text) {
+ port.postMessage({
+ type: "EXPORT_PUSH_TEXT",
+ text: text
+ })
+ }
+ };
+
+ await sleep(50);
+
+ exportBuilder.setStream(file);
+ const exporter = exportBuilder.build();
+ await exporter.export();
+
+ port.postMessage({
+ type: "EXPORT_FINISH"
+ });
+
+ let url = helperApp.url("/export/download");
+ let download;
+
+ try {
+ download = await browser.downloads.download({url: url, filename: fileName, saveAs: true});
+ } catch (e) {
+ console.error(e);
+ helperApp.fetch("/export/finalize");
+ }
+
+ if (download) {
+ let download_listener = delta => {
+ if (delta.id === download) {
+ if (delta.state && delta.state.current === "complete" || delta.error) {
+ browser.downloads.onChanged.removeListener(download_listener);
+ helperApp.fetch("/export/finalize");
+ }
+ }
+ };
+ browser.downloads.onChanged.addListener(download_listener);
+ }
+}
+
+async function exportStandalone(exportBuilder, fileName, format) {
+ const MAX_BLOB_SIZE = 1024 * 1024 * 10; // ~20 mb of UTF-16
+ const exportId = UUID.numeric();
+
+ let file = {
+ content: [],
+ size: 0,
+ append: async function (text) { // store intermediate export results to IDB
+ this.content.push(text);
+ this.size += text.length;
+
+ if (this.size >= MAX_BLOB_SIZE) {
+ await ExportArea.addBlob(exportId, new Blob(this.content, {type: "text/plain"}));
+ this.content = [];
+ this.size = 0;
+ }
+ },
+ flush: async function () {
+ if (this.size && this.content.length)
+ await ExportArea.addBlob(exportId, new Blob(this.content, {type: "text/plain"}));
+ }
+ };
+
+ await ExportArea.wipe();
+ exportBuilder.setStream(file);
+ const exporter = exportBuilder.build();
+ await exporter.export();
+ await file.flush();
+
+ const mimeType = format === "json"? "application/json": "text/plain";
+ let blob = new Blob(await ExportArea.getBlobs(exportId), {type: mimeType});
+ let url = URL.createObjectURL(blob);
+ let download;
+
+ try {
+ download = await browser.downloads.download({url: url, filename: fileName, saveAs: true});
+ } catch (e) {
+ console.error(e);
+ ExportArea.removeBlobs(exportId);
+ }
+
+ if (download) {
+ let download_listener = delta => {
+ if (delta.id === download) {
+ if (delta.state && delta.state.current === "complete" || delta.error) {
+ browser.downloads.onChanged.removeListener(download_listener);
+ URL.revokeObjectURL(url);
+ ExportArea.removeBlobs(exportId);
+ }
+ }
+ };
+ browser.downloads.onChanged.addListener(download_listener);
+ }
+}
diff --git a/addon/core_ishell.js b/addon/core_ishell.js
new file mode 100644
index 00000000..419521ee
--- /dev/null
+++ b/addon/core_ishell.js
@@ -0,0 +1,175 @@
+import {receiveExternal, send, sendLocal} from "./proxy.js";
+import {
+ BROWSER_EXTERNAL_TYPE,
+ DEFAULT_SHELF_NAME, DONE_SHELF_NAME, EVERYTHING_SHELF_NAME, FIREFOX_BOOKMARK_MENU,
+ FIREFOX_BOOKMARK_UNFILED, NODE_TYPE_ARCHIVE, NODE_TYPE_BOOKMARK, NODE_TYPE_NOTES,
+ NODE_TYPE_FOLDER, NODE_TYPE_SHELF, TODO_SHELF_NAME
+} from "./storage.js";
+import {ishellConnector} from "./plugin_ishell.js";
+import {getActiveTabMetadata} from "./bookmarking.js";
+import {Query} from "./storage_query.js";
+import {Path} from "./path.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {Icon, Node} from "./storage_entities.js";
+import {Folder} from "./bookmarks_folder.js";
+import {browseNode} from "./browse.js";
+import {showNotification} from "./utils_browser.js";
+
+receiveExternal.scrapyardListShelvesIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ let shelves = await Query.allShelves();
+ return shelves.map(n => ({name: n.name}));
+};
+
+receiveExternal.scrapyardListGroupsIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ let shelves = await Query.allShelves();
+ shelves = shelves.map(n => ({name: n.name}));
+ const builtin = [EVERYTHING_SHELF_NAME, TODO_SHELF_NAME, DONE_SHELF_NAME].map(s => ({name: s}));
+
+ shelves = [...builtin, ...shelves];
+
+ let folders = await Query.allFolders();
+ folders.forEach(n => renderPath(n, folders));
+ folders = folders.map(n => ({name: n.name, path: n.path}));
+
+ return [...shelves, ...folders];
+};
+
+receiveExternal.scrapyardListTagsIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ let tags = []; //await bookmarkManager.queryTags();
+ return tags.map(t => ({name: t.name.toLocaleLowerCase()}));
+};
+
+receiveExternal.scrapyardListNodesIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ delete message.type;
+
+ let no_shelves = message.types && !message.types.some(t => t === NODE_TYPE_SHELF);
+
+ if (message.types)
+ message.types = message.types.concat([NODE_TYPE_SHELF]);
+
+ message.path = Path.expand(message.path);
+
+ let nodes = await Bookmark.list(message);
+
+ for (let node of nodes) {
+ if (node.type === NODE_TYPE_FOLDER) {
+ renderPath(node, nodes);
+ }
+
+ if (node.stored_icon)
+ node.icon = await Icon.get(node);
+ }
+ if (no_shelves)
+ return nodes.filter(n => n.type !== NODE_TYPE_SHELF);
+ else
+ return nodes;
+};
+
+receiveExternal.scrapyardBrowseNodeIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ if (message.node.uuid)
+ Node.getByUUID(message.node.uuid).then(node => browseNode(node));
+ else
+ browseNode(message.node);
+};
+
+function renderPath(node, nodes) {
+ let path = [];
+ let parent = node;
+
+ while (parent) {
+ path.push(parent);
+ parent = nodes.find(n => n.id === parent.parent_id);
+ }
+
+ if (path[path.length - 1].name === DEFAULT_SHELF_NAME) {
+ path[path.length - 1].name = "~";
+ }
+
+ if (path.length >= 2 && path[path.length - 1].external === BROWSER_EXTERNAL_TYPE
+ && path[path.length - 2].external_id === FIREFOX_BOOKMARK_UNFILED) {
+ path.pop();
+ path[path.length - 1].name = "@@";
+ }
+
+ if (path.length >= 2 && path[path.length - 1].external === BROWSER_EXTERNAL_TYPE
+ && path[path.length - 2].external_id === FIREFOX_BOOKMARK_MENU) {
+ path.pop();
+ path[path.length - 1].name = "@";
+ }
+
+ node.path = path.reverse().map(n => n.name).join("/");
+}
+
+receiveExternal.scrapyardAddBookmarkIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ addBookmarkFromIshell(message, NODE_TYPE_BOOKMARK);
+}
+
+receiveExternal.scrapyardAddArchiveIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ addBookmarkFromIshell(message, NODE_TYPE_ARCHIVE);
+}
+
+receiveExternal.scrapyardAddSiteIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ message.__crawl = true;
+ addBookmarkFromIshell(message, NODE_TYPE_ARCHIVE);
+}
+
+receiveExternal.scrapyardAddNotesIshell = async (message, sender) => {
+ if (!ishellConnector.isIShell(sender.id))
+ throw new Error();
+
+ if (!message.name) {
+ showNotification({title: "Error", message: "Bookmark name is empty!"});
+ return;
+ }
+
+ const bookmark = await addBookmarkFromIshell(message, NODE_TYPE_NOTES);
+ return browseNode(bookmark, {edit: true});
+}
+
+async function addBookmarkFromIshell(message, type) {
+ const node = await getActiveTabMetadata();
+
+ node.name = message.name || node.name;
+ node.type = type;
+ node.tags = message.tags;
+ node.todo_state = message.todo_state;
+ node.todo_date = message.todo_date;
+ node.details = message.details;
+
+ if (message.__crawl)
+ node.__crawl = true;
+
+ const path = Path.expand(message.path);
+ const folder = await Folder.getOrCreateByPath(path);
+ node.parent_id = folder.id;
+ delete message.path;
+
+ if (type === NODE_TYPE_NOTES)
+ return sendLocal.createNotes({node});
+ else
+ return sendLocal.captureHighlightedTabs({options: node});
+}
diff --git a/addon/core_maintenance.js b/addon/core_maintenance.js
new file mode 100644
index 00000000..6e5744ac
--- /dev/null
+++ b/addon/core_maintenance.js
@@ -0,0 +1,183 @@
+import {receive, send, sendLocal} from "./proxy.js";
+import {
+ isContentNode,
+ NODE_TYPE_ARCHIVE,
+ NODE_TYPE_BOOKMARK,
+ NODE_TYPE_NOTES,
+ DEFAULT_SHELF_UUID
+} from "./storage.js";
+import {cloudShelf} from "./plugin_cloud_shelf.js";
+import {filesShelf} from "./plugin_files_shelf.js";
+import {Node} from "./storage_entities.js";
+import {Database} from "./storage_database.js";
+import {settings} from "./settings.js";
+import {helperApp} from "./helper_app.js";
+import {Export} from "./import.js";
+import {FORMAT_DEFAULT_SHELF_UUID, UnmarshallerJSONScrapbook} from "./marshaller_json_scrapbook.js";
+import {isDeepEqual} from "./utils.js";
+import {browserShelf} from "./plugin_browser_shelf.js";
+
+receive.resetCloud = async message => {
+ if (!cloudShelf.isAuthenticated())
+ return false;
+
+ send.startProcessingIndication({noWait: true});
+
+ await cloudShelf.reset();
+
+ send.stopProcessingIndication();
+
+ return true;
+}
+
+receive.resetScrapyard = async message => {
+ send.startProcessingIndication({noWait: true});
+
+ await Database.wipeEverything();
+ await settings.last_sync_date(null);
+
+ if (settings.enable_files_shelf())
+ await filesShelf.createIfMissing();
+
+ if (settings.cloud_enabled())
+ await cloudShelf.createIfMissing();
+
+ if (settings.show_firefox_bookmarks())
+ /*await*/ browserShelf.reconcileBrowserBookmarksDB();
+
+ send.stopProcessingIndication();
+
+ if (!settings.storage_mode_internal())
+ return sendLocal.performSync();
+}
+
+receive.computeStatistics = async message => {
+ let items = 0;
+ let bookmarks = 0;
+ let archives = 0;
+ let notes = 0
+ let size = 0;
+
+ send.startProcessingIndication();
+
+ await Node.iterate(node => {
+ if (isContentNode(node))
+ items += 1;
+
+ if (node.type === NODE_TYPE_BOOKMARK)
+ bookmarks += 1;
+
+ if (node.type === NODE_TYPE_ARCHIVE) {
+ archives += 1;
+ size += node.size || 0;
+ }
+
+ if (node.type === NODE_TYPE_NOTES) {
+ notes += 1;
+ size += node.size || 0;
+ }
+ });
+
+ send.stopProcessingIndication();
+
+ return {items, bookmarks, archives, notes, size};
+}
+
+receive.getOrphanedItems = async message => {
+ const helper = await helperApp.probe(true);
+
+ if (helper) {
+ await settings.load();
+ const params = {data_path: settings.data_folder_path()};
+ return await helperApp.fetchJSON_postJSON("/storage/get_orphaned_items", params);
+ }
+};
+
+receive.rebuildItemIndex = async message => {
+ const helper = await helperApp.probe(true);
+
+ if (helper) {
+ await settings.load();
+ const params = {data_path: settings.data_folder_path()};
+ return await helperApp.postJSON("/storage/rebuild_item_index", params);
+ }
+};
+
+receive.compareDatabaseStorage = async message => {
+ const helper = await helperApp.probe(true);
+
+ if (helper) {
+ await settings.load();
+ const params = {data_path: settings.data_folder_path()};
+ const storedNodes = await helperApp.fetchJSON_postJSON("/storage/debug_get_stored_node_instances", params);
+ const nodes = await Export.nodes("everything");
+ const unmarshaller = new UnmarshallerJSONScrapbook();
+
+ const idbKeys = new Set(nodes.map(n => n.uuid === DEFAULT_SHELF_UUID? FORMAT_DEFAULT_SHELF_UUID: n.uuid));
+ const storageKeys = new Set(Object.keys(storedNodes));
+
+ let result = idbKeys.size === storageKeys.size && [...idbKeys].every(x => storageKeys.has(x));
+
+ if (!result) {
+ for (const node of [...nodes]) {
+ if (node.uuid === DEFAULT_SHELF_UUID)
+ node.uuid = FORMAT_DEFAULT_SHELF_UUID;
+
+ if (storedNodes[node.uuid]) {
+ nodes.splice(nodes.indexOf(node), 1);
+ delete storedNodes[node.uuid];
+ }
+ }
+
+ console.log("Nodes only in IDB:");
+ console.log(nodes);
+ console.log("Nodes only in storage:")
+ console.log(storedNodes);
+
+ return result;
+ }
+
+ for (const node of nodes) {
+ if (node.uuid === DEFAULT_SHELF_UUID)
+ continue;
+
+ const storedObjects = storedNodes[node.uuid];
+
+ if (storedObjects) {
+ let indexItem = unmarshaller.unconvertNode(storedObjects.db_item);
+ indexItem = unmarshaller.deserializeNode(indexItem);
+ await unmarshaller.findParentInIDB(indexItem)
+
+ let objectItem = unmarshaller.unconvertNode(storedObjects.object_item);
+ objectItem = unmarshaller.deserializeNode(objectItem);
+ await unmarshaller.findParentInIDB(objectItem)
+
+ delete node.id;
+ delete node.icon;
+ delete node.tag_list;
+
+ if (node.tags === "")
+ delete node.tags;
+
+ if (!isDeepEqual(node, indexItem, true) || !isDeepEqual(node, objectItem, true)) {
+ console.log("Objects do not match:\nIDB object:");
+ console.log(node);
+ console.log("Index object:");
+ console.log(indexItem);
+ console.log("File object:");
+ console.log(objectItem);
+
+ result = false;
+ }
+ }
+ else {
+ console.log("No corresponding storage item for object:");
+ console.log(node);
+
+ result = false;
+ }
+ }
+
+ return result;
+ }
+};
diff --git a/addon/core_share.js b/addon/core_share.js
new file mode 100644
index 00000000..62334c3d
--- /dev/null
+++ b/addon/core_share.js
@@ -0,0 +1,124 @@
+import {receive} from "./proxy.js";
+import {GetPocket} from "./lib/pocket.js";
+import {settings} from "./settings.js";
+import {showNotification} from "./utils_browser.js";
+import {NODE_TYPE_ARCHIVE, NODE_TYPE_BOOKMARK, NODE_TYPE_FILE, NODE_TYPE_NOTES} from "./storage.js";
+import {notes2html} from "./notes_render.js";
+import {dropboxClient} from "./cloud_client_dropbox.js";
+import {oneDriveClient} from "./cloud_client_onedrive.js";
+import {CONTENT_TYPE_TO_EXT} from "./utils.js";
+import {Archive, Notes} from "./storage_entities.js";
+
+receive.shareToPocket = async message => {
+ const auth_handler = auth_url => new Promise(async (resolve, reject) => {
+ let pocket_tab = await browser.tabs.create({url: auth_url});
+ let listener = async (id, changed, tab) => {
+ if (id === pocket_tab.id) {
+ if (changed.url && !changed.url.includes("getpocket.com")) {
+ await browser.tabs.onUpdated.removeListener(listener);
+ browser.tabs.remove(pocket_tab.id);
+ resolve();
+ }
+ }
+ };
+ browser.tabs.onUpdated.addListener(listener);
+ });
+
+ let pocket = new GetPocket({
+ consumer_key: "87251-b8d5db3009affab6297bc799",
+ access_token: settings.pocket_access_token(),
+ redirect_uri: "https://gchristensen.github.io/scrapyard/",
+ auth_handler: auth_handler,
+ persist_token: token => settings.pocket_access_token(token)
+ });
+
+ let actions = message.nodes.map(n => ({
+ action: "add",
+ title: n.name,
+ url: n.uri,
+ tags: n.tags
+ }));
+ await pocket.modify(actions).catch(e => console.error(e));
+
+ showNotification(`Successfully added bookmark${message.nodes.length > 1
+ ? "s"
+ : ""} to Pocket.`)
+};
+
+receive.shareToDropbox = async message => {
+ let shared = false;
+
+ for (let node of message.nodes) {
+ let {filename, content} = await prepareForCloudSharing(node);
+
+ if (filename && content) {
+ shared = true;
+ await dropboxClient.share("/", filename, content);
+ }
+ }
+
+ if (shared)
+ showNotification(`Successfully shared bookmark${message.nodes.length > 1? "s": ""} to Dropbox.`)
+}
+
+receive.shareToOneDrive = async message => {
+ let shared = false;
+
+ for (let node of message.nodes) {
+ let {filename, content} = await prepareForCloudSharing(node);
+
+ if (filename && content) {
+ shared = true;
+ await oneDriveClient.share("/", filename, content);
+ }
+ }
+
+ if (shared)
+ showNotification(`Successfully shared bookmark${message.nodes.length > 1? "s": ""} to OneDrive.`)
+}
+
+async function prepareForCloudSharing(node) {
+ let filename, content;
+
+ if (node.type === NODE_TYPE_ARCHIVE || node.type === NODE_TYPE_FILE) {
+ let archive = await Archive.get(node);
+ if (archive) {
+ let type = archive.type? archive.type: "text/html";
+
+ if (Archive.isUnpacked(node))
+ type = "application/octet-stream";
+
+ filename = node.name
+
+ if (!/\.[a-z]{2,8}$/.test(node.name?.toLowerCase())) {
+ let ext = CONTENT_TYPE_TO_EXT[type] || "bin";
+
+ if (Archive.isUnpacked(node))
+ ext = "zip";
+
+ filename = node.name + `.${ext}`;
+ }
+
+ content = await Archive.reify(archive);
+
+ if (!(content instanceof Blob))
+ content = new Blob([content], {type});
+ }
+ }
+ else if (node.type === NODE_TYPE_BOOKMARK) {
+ filename = node.name + ".url";
+ content = "[InternetShortcut]\nURL=" + node.uri;
+ }
+ else if (node.type === NODE_TYPE_NOTES) {
+ let notes = await Notes.get(node);
+
+ if (notes) {
+ filename = node.name + ".html";
+ content = ` `
+ + `${notes2html(notes)}`
+ + ``;
+ }
+ }
+
+ return {filename, content};
+}
diff --git a/addon/core_sync.js b/addon/core_sync.js
new file mode 100644
index 00000000..ae286974
--- /dev/null
+++ b/addon/core_sync.js
@@ -0,0 +1,276 @@
+import {receive, send} from "./proxy.js";
+import {SCRAPYARD_SYNC_METADATA, settings} from "./settings.js";
+import {Node} from "./storage_entities.js";
+import {HELPER_APP_v2_IS_REQUIRED, helperApp} from "./helper_app.js";
+import {ACTION_ICONS, showNotification} from "./utils_browser.js";
+import {DEFAULT_SHELF_UUID, NON_SYNCHRONIZED_EXTERNALS, JSON_SCRAPBOOK_VERSION} from "./storage.js";
+import {chunk, ProgressCounter} from "./utils.js";
+import {MarshallerSync, UnmarshallerSync} from "./marshaller_sync.js";
+import {Database} from "./storage_database.js";
+import {undoManager} from "./bookmarks_undo.js";
+
+const SYNC_NODE_CHUNK_SIZE = 10;
+
+let syncing = false;
+
+receive.checkSyncDirectory = async message => {
+ try {
+ send.startProcessingIndication();
+ const helper = await helperApp.hasVersion("2.0", HELPER_APP_v2_IS_REQUIRED);
+
+ if (helper) {
+ const status = await helperApp.fetchJSON_postJSON("/storage/check_directory", {
+ data_path: message.path
+ });
+
+ if (status)
+ return status.status;
+ }
+ }
+ finally {
+ send.stopProcessingIndication();
+ }
+};
+
+receive.performSync = async message => {
+ let synced;
+
+ send.startProcessingIndication();
+
+ try {
+ synced = await performSync();
+ }
+ finally {
+ send.stopProcessingIndication();
+
+ if (synced)
+ send.shelvesChanged();
+ }
+};
+
+async function performSync() {
+ let result;
+
+ await settings.load();
+
+ const syncDirectory = settings.data_folder_path();
+
+ if (syncing || !syncDirectory || !await helperApp.probe(true))
+ return;
+
+ try {
+ syncing = true;
+
+ let storageMetadata = await getStorageMetadata(syncDirectory);
+
+ if (storageMetadata) {
+ const dbMetadata = await settings.get(SCRAPYARD_SYNC_METADATA);
+
+ if (await prepareDatabase(storageMetadata, dbMetadata)) {
+
+ const syncOperations = await computeSync(syncDirectory);
+
+ if (syncOperations) {
+ result = await syncWithStorage(syncOperations, syncDirectory);
+ await helperApp.fetch("/storage/sync_close_session");
+ await settings.set(SCRAPYARD_SYNC_METADATA, storageMetadata);
+ }
+ else
+ showNotification("Synchronization could not be performed because of an error.");
+ }
+ }
+ }
+ finally {
+ syncing = false;
+ }
+
+ return result;
+}
+
+async function getStorageMetadata(syncDirectory) {
+ let storageMetadata
+ try {
+ storageMetadata = await helperApp.fetchJSON_postJSON("/storage/get_metadata", {
+ data_path: syncDirectory
+ });
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (!storageMetadata || storageMetadata.error === "error") {
+ showNotification("Synchronization error.");
+ return;
+ }
+ else if (storageMetadata.error === "empty" || !storageMetadata.entities) {
+ showNotification("The disk storage is missing or empty.\n"
+ + "If you are just starting to work with Scrapyard, please create a bookmark to mute this message.");
+ return;
+ }
+ else if (storageMetadata.type !== "index") {
+ showNotification("Unknown storage format type.");
+ return;
+ }
+ else if (typeof storageMetadata.version === "number" && storageMetadata.version > JSON_SCRAPBOOK_VERSION) {
+ showNotification("Unknown storage format version.");
+ return;
+ }
+
+ return storageMetadata;
+}
+
+async function computeSync(syncDirectory) {
+ const syncNodes = await getNodesForSync();
+
+ const syncParams = {
+ data_path: syncDirectory,
+ nodes: JSON.stringify(syncNodes),
+ last_sync_date: settings.last_sync_date() || -1
+ };
+
+ let syncOperations;
+ try {
+ syncOperations = await helperApp.fetchJSON_postJSON("/storage/sync_compute", syncParams);
+ } catch (e) {
+ console.error(e);
+ }
+ return syncOperations;
+}
+
+async function getNodesForSync() {
+ const syncNodes = [];
+ const marshaller = new MarshallerSync();
+
+ await Node.iterate(node => {
+ const nonSyncable = node.external && NON_SYNCHRONIZED_EXTERNALS.some(ex => ex === node.external);
+
+ if (!nonSyncable) {
+ const syncNode = marshaller.createSyncNode(node);
+ syncNodes.push(syncNode);
+ }
+ });
+
+ return syncNodes;
+}
+
+async function prepareDatabase(storageMetadata, dbMetadata) {
+ if (storageMetadata.type !== "index") {
+ showNotification("Storage format type is not supported.");
+ return false;
+ }
+
+ const resetDatabase = storageMetadata.uuid !== dbMetadata?.uuid
+ || storageMetadata.timestamp < dbMetadata?.timestamp
+ || storageMetadata.version !== dbMetadata?.version;
+
+ if (resetDatabase) {
+ await undoManager.commit();
+ await Database.wipeImportable();
+ await settings.last_sync_date(null);
+ }
+
+ return true;
+}
+
+async function syncWithStorage(syncOperations, syncDirectory) {
+ if (areChangesPresent(syncOperations)) {
+ const action = _MANIFEST_V3? browser.action: browser.browserAction;
+
+ if (settings.platform.firefox)
+ action.setIcon({path: "/icons/action-sync.svg"});
+ else
+ action.setIcon({path: "/icons/action-sync.png"});
+
+ try {
+ await performOperations(syncOperations, syncDirectory);
+ } finally {
+ if (settings.platform.firefox)
+ action.setIcon({path: "/icons/scrapyard.svg"});
+ else
+ action.setIcon({path: ACTION_ICONS});
+ }
+
+ return true;
+ }
+}
+
+function areChangesPresent(syncOperations) {
+ //console.log(syncOperations);
+
+ const changes = syncOperations.push.length
+ || syncOperations.pull.length
+ || syncOperations.delete.length
+ || syncOperations.delete_in_storage.length;
+
+ return !!changes;
+}
+
+async function performOperations(syncOperations, syncDirectory) {
+ await helperApp.fetchJSON_postJSON("/storage/sync_open_session", {data_path: syncDirectory});
+
+ let errors = false;
+
+ // try {
+ // await deleteStorageNodes(syncOperations.delete_in_storage);
+ // }
+ // catch (e) {
+ // errors = true;
+ // console.error(e);
+ // }
+
+ const total = syncOperations.push.length
+ + Math.floor(syncOperations.pull.length / SYNC_NODE_CHUNK_SIZE)
+ /*+ syncOperations.delete.length*/;
+ const progress = new ProgressCounter(total, "syncProgress");
+
+ // const syncMarshaller = new MarshallerSync();
+ // for (const syncNode of syncOperations.push)
+ // try {
+ // await syncMarshaller.marshal(syncNode);
+ // progress.incrementAndNotify();
+ // }
+ // catch (e) {
+ // errors = true;
+ // console.error(e);
+ // }
+
+ const syncUnmarshaller = new UnmarshallerSync();
+ for (const syncNodes of chunk(syncOperations.pull, SYNC_NODE_CHUNK_SIZE))
+ try {
+ const success = await syncUnmarshaller.unmarshall(syncNodes)
+ progress.incrementAndNotify();
+
+ if (!success)
+ errors = true;
+ } catch (e) {
+ errors = true;
+ console.error(e);
+ }
+
+ await deleteNodes(syncOperations.delete);
+
+ progress.finish();
+
+ await settings.load();
+ settings.last_sync_date(Date.now());
+
+ if (errors)
+ showNotification("Synchronization finished with errors.");
+}
+
+async function deleteStorageNodes(syncNodes) {
+ if (syncNodes.length)
+ await helperApp.post("/storage/sync_delete_nodes", {nodes: JSON.stringify(syncNodes)});
+}
+
+async function deleteNodes(syncNodes) {
+ for (const syncNode of syncNodes)
+ try {
+ if (syncNode.uuid === DEFAULT_SHELF_UUID)
+ continue;
+ const node = await Node.getByUUID(syncNode.uuid)
+ await Node.idb.delete(node)
+ }
+ catch (e) {
+ console.error(e);
+ }
+}
diff --git a/addon/core_transition.js b/addon/core_transition.js
new file mode 100644
index 00000000..5f13ab53
--- /dev/null
+++ b/addon/core_transition.js
@@ -0,0 +1,113 @@
+import {EVERYTHING_SHELF_NAME, NODE_TYPE_ARCHIVE, CLOUD_SHELF_NAME, CLOUD_SHELF_ID} from "./storage.js";
+import {Node, Archive, Comments, Icon, Notes} from "./storage_entities.js";
+import {HELPER_APP_v2_IS_REQUIRED, helperApp} from "./helper_app.js";
+import {showNotification} from "./utils_browser.js";
+import {receive, send} from "./proxy.js";
+import {Export} from "./import.js";
+import {ProgressCounter} from "./utils.js";
+import {settings} from "./settings.js";
+import {Query} from "./storage_query.js";
+import UUID from "./uuid.js";
+import {DiskStorage} from "./storage_external.js";
+import {indexHTML} from "./utils_html.js";
+
+receive.transferContentToDisk = async message => {
+ if (!settings.data_folder_path()) {
+ showNotification("Data folder path is not set.");
+ return;
+ }
+
+ const helper = await helperApp.hasVersion("2.0", HELPER_APP_v2_IS_REQUIRED);
+
+ if (!helper)
+ return;
+
+ send.startProcessingIndication({noWait: true});
+
+ const nodes = await collectNodes();
+ const progressCounter = new ProgressCounter(nodes.length, "exportProgress");
+
+ try {
+ await DiskStorage.openBatchSession();
+
+ for (const node of nodes) {
+ await transferNode(node);
+ progressCounter.incrementAndNotify();
+ }
+
+ await settings.transition_to_disk(false);
+ showNotification("Content transfer finished.");
+ return true;
+ } catch (e) {
+ console.error(e);
+ showNotification("Content transfer finished with errors.");
+ return false;
+ }
+ finally {
+ await DiskStorage.closeBatchSession();
+ send.stopProcessingIndication();
+ await progressCounter.finish();
+ }
+};
+
+async function collectNodes() {
+ let nodes = await Export.nodes(EVERYTHING_SHELF_NAME);
+
+ if (settings.cloud_enabled()) {
+ let cloudShelf = await Node.get(CLOUD_SHELF_ID);
+
+ if (cloudShelf) {
+ const cloudNodes = await Query.fullSubtree(cloudShelf.id);
+ cloudShelf = cloudNodes.find(n => n.id === CLOUD_SHELF_ID);
+
+ for (const cloudNode of cloudNodes) {
+ delete cloudNode.external;
+ delete cloudNode.external_id;
+ }
+
+ cloudShelf.uuid = UUID.numeric();
+ cloudShelf.name = CLOUD_SHELF_NAME + " (transferred)";
+ cloudShelf.date_modifed = cloudShelf.date_added;
+
+ nodes = [...nodes, ...cloudNodes];
+ }
+ }
+
+ return nodes;
+}
+
+async function transferNode(node) {
+ Node.put(node);
+
+ if (node.stored_icon) {
+ let icon = await Icon.idb.import.get(node);
+ if (icon)
+ await Icon.add(node, icon);
+ }
+
+ if (node.type === NODE_TYPE_ARCHIVE) {
+ let archive = await Archive.idb.import.get(node);
+
+ if (archive) {
+ if (!archive.byte_length) {
+ const content = await Archive.reify(archive);
+ const index = indexHTML(content);
+ await Archive.storeIndex(node, index);
+ }
+
+ await Archive.add(node, archive);
+ }
+ }
+
+ if (node.has_notes) {
+ let notes = await Notes.idb.import.get(node);
+ if (notes)
+ await Notes.add(node, notes);
+ }
+
+ if (node.has_comments) {
+ let comments = await Comments.idb.import.get(node);
+ if (comments)
+ await Comments.add(node, comments);
+ }
+}
diff --git a/addon/crawler.js b/addon/crawler.js
new file mode 100644
index 00000000..2044e0ad
--- /dev/null
+++ b/addon/crawler.js
@@ -0,0 +1,317 @@
+import {packPage} from "./bookmarking.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {NODE_TYPE_ARCHIVE} from "./storage.js";
+import {fetchWithTimeout} from "./utils_io.js";
+import {send} from "./proxy.js";
+import {sleep} from "./utils.js";
+import {isHTMLLink} from "./utils_html.js";
+import {showNotification} from "./utils_browser.js";
+
+class Rules {
+ #rules;
+
+ constructor(rules) {
+ this.#rules = this.#constructRules(rules);
+ }
+
+ match(link) {
+ let result = false;
+ if (!this.#rules)
+ result = true;
+ else
+ for (const rule of this.#rules) {
+ const content = rule.scope === "url"? link.url: link.text;
+ let matches = false;
+
+ if (rule.mode === "regex")
+ matches = rule.matcher.test(content);
+ else
+ matches = content?.toLowerCase() === rule.matcher.toLowerCase();
+
+ if (matches) {
+ result = true;
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ get empty() {
+ return !this.#rules;
+ }
+
+ #constructRules(rules) {
+ const lines = rules.trim().split("\n");
+
+ if (lines.length) {
+ const result = [];
+
+ for (const line of lines) {
+ const rule = this.#constructRule(line);
+ if (rule)
+ result.push(rule);
+ }
+
+ if (result.length)
+ return result;
+ }
+ }
+
+ #constructRule(line) {
+ const rule = {scope: "url", mode: "regex"};
+ line = line.trim();
+
+ if (line) {
+ if (line.startsWith("$text:")) {
+ rule.scope = "text";
+ line = line.replace(/^\$text:/, "");
+ }
+
+ if (!line.startsWith("/"))
+ rule.mode = "string";
+
+ if (rule.scope !== "text")
+ line = line.split(" ")[0];
+
+ if (rule.mode === "regex") {
+ line = line.replace("\\/", "/");
+ const matches = line.match(/^\/(.*)\/([a-zA-Z]*)?$/);
+ if (matches)
+ rule.matcher = new RegExp(matches[1], matches[2]);
+ else {
+ rule.mode = "string";
+ rule.matcher = line;
+ }
+ }
+ else
+ rule.matcher = line;
+
+ return rule;
+ }
+ }
+}
+
+class Queue {
+ #options;
+ #rootURL;
+ #rootHost;
+ #links = [];
+ #visitedURLs;
+
+ constructor(rootURL, options) {
+ this.#rootURL = rootURL;
+ this.#rootHost = new URL(rootURL).host;
+ this.#options = options;
+
+ const normalizedURL = this.#normalizeURL(rootURL);
+ this.#visitedURLs = new Set([normalizedURL]);
+ }
+
+ #normalizeURL(url) {
+ if (this.#options.ignoreHashes)
+ url = url.replace(/#.*$/, "");
+
+ return url;
+ }
+
+ push(link) {
+ const normalizedURL = this.#normalizeURL(link.url);
+ if (!this.#visitedURLs.has(normalizedURL)) {
+ this.#visitedURLs.add(normalizedURL);
+ this.#links.push(link);
+ }
+ }
+
+ pop() {
+ return this.#links.shift();
+ }
+
+ get size() {
+ return this.#links.length;
+ }
+}
+
+class Crawler {
+ #queue;
+ #options;
+ #includeRules;
+ #excludeRules;
+ #siteBookmark;
+ #threads = 0;
+ #abort = false;
+ onFinish = () => null;
+
+ constructor(bookmark) {
+ this.#siteBookmark = bookmark;
+ this.#options = {...bookmark.__site_capture};
+ this.#includeRules = new Rules(this.#options.includeRules);
+ this.#excludeRules = new Rules(this.#options.excludeRules);
+ this.#queue = new Queue(bookmark.uri, this.#options);
+ }
+
+ enqueue(bookmark) {
+ const options = bookmark.__site_capture;
+
+ if (options.level < this.#options.depth && options.links) {
+ for (const link of options.links) {
+ if (this.#isLinkAllowed(link)) {
+ link.level = options.level + 1;
+ this.#queue.push(link);
+ }
+ }
+ }
+ }
+
+ abort() {
+ this.#abort = true;
+ }
+
+ async startThreads() {
+ this.#threads = Math.min(this.#options.threads, this.#queue.size);
+ for (let i = 0; i < this.#threads; ++i) {
+ this.#visitLink(this.#queue.pop());
+
+ if (this.#options.delay)
+ await sleep(this.#options.delay * 1000);
+ }
+ }
+
+ #isLinkAllowed(link) {
+ const include = this.#includeRules.match(link);
+ const exclude = !this.#excludeRules.empty && this.#excludeRules.match(link);
+ return include && !exclude;
+ }
+
+ async #visitLink(link) {
+ const options = {...this.#options};
+ options.level = link.level;
+
+ try {
+ const bookmark = await this.#savePage(link, options);
+
+ if (bookmark)
+ this.enqueue(bookmark);
+ } catch (e) {
+ console.error(e);
+ }
+
+ this.#crawl();
+ }
+
+ async #crawl() {
+ if (this.#abort)
+ return;
+
+ if (this.#options.delay)
+ await sleep(this.#options.delay * 1000);
+
+ const nextLink = this.#queue.pop();
+
+ if (nextLink)
+ this.#visitLink(nextLink);
+ else {
+ this.#threads -= 1;
+ if (this.#threads === 0)
+ this.onFinish();
+ }
+ }
+
+ async #savePage(link, options) {
+ const bookmark = {
+ uri: link.url,
+ name: "Site Page",
+ __site_capture: options,
+ _unlisted: true,
+ parent_id: this.#siteBookmark.parent_id
+ };
+
+ const node = await Bookmark.idb.add(bookmark, NODE_TYPE_ARCHIVE);
+ const isHTML = await isHTMLLink(link.url);
+ let resource;
+
+ if (isHTML === true)
+ resource = await this.#captureHTMLPage(node, link);
+ else if (isHTML === false)
+ resource = await this.#captureNonHTMLPage(node, link);
+
+ if (resource)
+ await this.#saveArchive(resource);
+
+ return resource?.bookmark
+ }
+
+ #captureHTMLPage(node, link) {
+ node.__url_packing = true;
+ const resolver = (m, t) => ({bookmark: m.bookmark, content: m.html, title: t.title, icon: t.favIconUrl});
+ return packPage(link.url, node, null, resolver);
+ }
+
+ async #captureNonHTMLPage(node, link) {
+ let response;
+ try {
+ response = await fetchWithTimeout(link.url);
+ } catch (e) {
+ console.error(e);
+ }
+ const result = {bookmark: node, title: link.text};
+
+ if (response?.ok) {
+ result.contentType = response.headers.get("content-type");
+ result.content = await response.arrayBuffer();
+ }
+
+ return result;
+ }
+
+ async #saveArchive(result) {
+ const node = result.bookmark;
+ const content = result.content || "";
+ const contentType = result.contentType || "text/html";
+
+ node.name = result.title;
+ node._unlisted = undefined;
+
+ if (result.icon) {
+ node.icon = result.icon;
+ await Bookmark.storeIcon(node);
+ }
+
+ return Bookmark.storeArchive(node, content, contentType, node.__index);
+ }
+}
+
+let crawler;
+
+export function initialize(bookmark) {
+ if (crawler) {
+ showNotification("Another site capture process is running");
+ return false;
+ }
+ else {
+ crawler = new Crawler(bookmark);
+ crawler.onFinish = finalize;
+ return true;
+ }
+}
+
+export function crawl(bookmark) {
+ if (crawler) {
+ crawler.enqueue(bookmark);
+ crawler.startThreads();
+ }
+}
+
+export function abort() {
+ if (crawler) {
+ crawler.abort();
+ setTimeout(finalize, 500);
+ }
+}
+
+async function finalize() {
+ crawler = undefined;
+ await send.stopProcessingIndication();
+ await send.toggleAbortMenu({show: false});
+ await send.nodesUpdated();
+}
diff --git a/addon/favicon.js b/addon/favicon.js
new file mode 100644
index 00000000..fb359096
--- /dev/null
+++ b/addon/favicon.js
@@ -0,0 +1,96 @@
+import {settings} from "./settings.js";
+import {fetchWithTimeout} from "./utils_io.js";
+import {parseHtml} from "./utils_html.js";
+import {injectScriptFile} from "./utils_browser.js";
+
+export async function testFavicon(url) {
+ try {
+ // get a nice favicon for wikipedia
+ if (url.origin && url.origin.endsWith("wikipedia.org"))
+ return "https://en.wikipedia.org/favicon.ico";
+
+ let response = await fetch(url)
+
+ if (response.ok) {
+ let type = response.headers.get("content-type") || "image";
+ //let length = response.headers.get("content-length") || "0";
+
+ if (type.startsWith("image") /*&& parseInt(length) > 0*/)
+ return url.toString();
+ }
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+export async function getFaviconFromTab(tab, tabOnly = false) {
+ let favicon;
+ let origin = new URL(tab.url).origin;
+
+ if (!origin || origin === "null")
+ return;
+
+ if (tab.favIconUrl)
+ return tab.favIconUrl;
+ else if (tabOnly)
+ return;
+
+ try {
+ let icon = await injectScriptFile(tab.id, {file: "/content_favicon.js"});
+
+ if (icon && icon.length && icon[0])
+ favicon = await testFavicon(new URL(icon[0].result || icon[0], origin)); // TODO: leave only .result in MV3
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (!favicon)
+ favicon = await testFavicon(new URL("/favicon.ico", origin));
+
+ return favicon;
+}
+
+export async function getFaviconFromContent(url, doc) {
+ if (!doc) {
+ let timeout = settings.link_check_timeout()? settings.link_check_timeout() * 1000: 10000;
+ try {
+ const response = await fetchWithTimeout(url, {timeout});
+
+ if (response.ok) {
+ if (_BACKGROUND_PAGE)
+ doc = parseHtml(await response.text());
+ else
+ doc = await response.text();
+ }
+ }
+ catch (e) {}
+ }
+ else if (_BACKGROUND_PAGE && typeof doc === "string")
+ doc = parseHtml(doc);
+
+ let favIcon;
+
+ try {
+ if (typeof doc === "string") {
+ const linkTag = doc.match(/ ]*rel=['"](?:icon|shortcut)[^>]*>/i)?.[0];
+
+ if (linkTag)
+ favIcon = linkTag.match(/href=['"]?([^'" ]+)/i)?.[1];
+ }
+ else if (doc) {
+ const faviconElt = doc.querySelector("link[rel*='icon'], link[rel*='shortcut']");
+
+ if (faviconElt)
+ favIcon = faviconElt.href;
+ }
+ }
+ catch (e) {
+ console.error(e);
+ }
+
+ const origin = new URL(url).origin;
+
+ if (origin && origin !== "null")
+ return favIcon && await testFavicon(new URL(favIcon, origin))
+ || await testFavicon(new URL("/favicon.ico", origin));
+}
diff --git a/addon/fonts/fontawesome.css b/addon/fonts/fontawesome.css
new file mode 100644
index 00000000..1054d193
--- /dev/null
+++ b/addon/fonts/fontawesome.css
@@ -0,0 +1,6 @@
+@font-face {
+ font-family:'FontAwesome';
+ src:url('fontawesome.woff2') format('woff2');
+ font-weight:normal;
+ font-style:normal
+}
\ No newline at end of file
diff --git a/addon/fonts/fontawesome.woff2 b/addon/fonts/fontawesome.woff2
new file mode 100644
index 00000000..dc52d954
Binary files /dev/null and b/addon/fonts/fontawesome.woff2 differ
diff --git a/addon/global.js b/addon/global.js
new file mode 100644
index 00000000..cec2a7a7
--- /dev/null
+++ b/addon/global.js
@@ -0,0 +1,22 @@
+if (typeof browser === "undefined")
+ globalThis.browser = chrome;
+
+const _MANIFEST = chrome.runtime.getManifest();
+
+globalThis._ADDON_VERSION = _MANIFEST.version;
+
+globalThis._MANIFEST_VERSION = _MANIFEST.manifest_version;
+
+globalThis._MANIFEST_V3 = globalThis._MANIFEST_VERSION === 3;
+
+globalThis._BACKGROUND_PAGE = !!_MANIFEST.background?.page;
+
+globalThis._SIDEBAR = !!globalThis.browser.sidebarAction;
+
+globalThis._log = console.log.bind(console);
+
+globalThis._tm = (name = "timer") => console.time(name);
+
+globalThis._te = (name = "timer") => console.timeEnd(name);
+
+globalThis._tr = console.trace.bind(console);
diff --git a/addon/helper_app.js b/addon/helper_app.js
new file mode 100644
index 00000000..1cbdc01e
--- /dev/null
+++ b/addon/helper_app.js
@@ -0,0 +1,254 @@
+import UUID from "./uuid.js"
+import {settings} from "./settings.js"
+import {CONTEXT_BACKGROUND, getContextType, hasCSRPermission, showNotification} from "./utils_browser.js";
+import {send} from "./proxy.js"
+
+export const HELPER_APP_v2_IS_REQUIRED = "Scrapyard backend application v2.0+ is required.";
+export const HELPER_APP_v2_1_IS_REQUIRED = "Scrapyard backend application v2.1+ is required.";
+
+class HelperApp {
+ #auth;
+ #externalEventHandlers = {};
+
+ constructor() {
+ this.auth = UUID.numeric();
+ this.version = undefined;
+ }
+
+ get auth() {
+ return this.#auth;
+ }
+
+ set auth(uuid) {
+ this.#auth = uuid;
+ this.authHeader = "Basic " + btoa("default:" + uuid);
+ }
+
+ async getPort() {
+ if (this.port) {
+ return this.port;
+ }
+ else {
+ this.port = new Promise(async (resolve, reject) => {
+ let port = browser.runtime.connectNative("scrapyard_helper");
+
+ port.onDisconnect.addListener(error => {
+ resolve(null);
+ this.port = null;
+ })
+
+ let initListener = async response => {
+ response = JSON.parse(response);
+ if (response.type === "INITIALIZED") {
+ port.onMessage.removeListener(initListener);
+
+ await this._onInitialized(response, port);
+
+ resolve(port);
+ }
+ }
+
+ port.onMessage.addListener(initListener);
+
+ await settings.load();
+
+ try {
+ port.postMessage({
+ type: "INITIALIZE",
+ port: settings.helper_port_number(),
+ auth: this.auth,
+ logging: !!settings.enable_helper_app_logging()
+ });
+ }
+ catch (e) {
+ //console.error(e, e.name)
+ resolve(null);
+ this.port = null;
+ }
+ });
+
+ return this.port;
+ }
+ }
+
+ async _onInitialized(msg, port) {
+ this.port = port;
+ this.version = msg.version;
+ port.onMessage.addListener(HelperApp._incomingMessages.bind(this));
+
+ if (msg.error === "address_in_use")
+ showNotification(`The backend application HTTP port ${settings.helper_port_number()} is not available.`);
+ }
+
+ async probe(verbose) {
+ if (getContextType() === CONTEXT_BACKGROUND)
+ return this._probe(verbose);
+ else
+ return send.helperAppProbe({verbose});
+ }
+
+ async _probe(verbose = false) {
+ if (!await hasCSRPermission())
+ return false;
+
+ const port = await this.getPort();
+
+ if (!port && verbose)
+ showNotification({message: "Can not connect to the backend application."})
+
+ return !!port;
+ }
+
+ getVersion() {
+ if (getContextType() !== CONTEXT_BACKGROUND)
+ throw new Error("Can not call this method in the foreground context.");
+
+ if (this.port) {
+ if (!this.version)
+ return "0.1";
+ return this.version;
+ }
+ }
+
+ async hasVersion(version, msg) {
+ if (getContextType() === CONTEXT_BACKGROUND)
+ return this._hasVersion(version, msg);
+ else
+ return send.helperAppHasVersion({version, alert: msg});
+ }
+
+ async _hasVersion(version, msg) {
+ if (!(await this.probe())) {
+ if (msg)
+ showNotification(msg);
+ return false;
+ }
+
+ let installed = this.getVersion();
+
+ if (installed) {
+ if (installed.startsWith(version))
+ return true;
+
+ version = version.split(".").map(d => parseInt(d));
+ installed = installed.split(".").map(d => parseInt(d));
+ installed.length = version.length;
+
+ for (let i = 0; i < version.length; ++i) {
+ if (installed[i] > version[i])
+ return true;
+ }
+
+ if (msg)
+ showNotification(msg);
+ return false;
+ }
+ }
+
+ static async _incomingMessages(msg) {
+ const port = await this.getPort();
+ msg = JSON.parse(msg);
+
+ const handler = this.#externalEventHandlers[msg.type];
+
+ if (handler) {
+ const response = await handler(msg);
+
+ if (response !== undefined)
+ port.postMessage(response);
+ }
+ }
+
+ addMessageHandler(name, handler) {
+ this.#externalEventHandlers[name] = handler;
+ }
+
+ url(path) {
+ return `http://localhost:${settings.helper_port_number()}${path}`;
+ }
+
+ _injectAuth(init) {
+ init = init || {};
+ init.headers = init.headers || {};
+ init.headers["Authorization"] = this.authHeader;
+ return init;
+ }
+
+ async _handleHTTPError(response) {
+ if (response.status === 204 || response.status === 404)
+ return null;
+ else {
+ const errorMessage = `Scrapyard native client error ${response.status} (${response.statusText})\n`;
+ console.error(errorMessage, await response.text());
+ throw {httpError: {status: response.status, statusText: response.statusText}};
+ }
+ }
+
+ fetch(path, init) {
+ init = this._injectAuth(init);
+ return globalThis.fetch(this.url(path), init);
+ }
+
+ async post(path, fields) {
+ let form = new FormData();
+
+ for (let [k, v] of Object.entries(fields)) {
+ if (v instanceof Blob)
+ form.append(k, v, k);
+ else {
+ v = v + "";
+ form.append(k, v);
+ }
+ }
+
+ const init = this._injectAuth({method: "POST", body: form});
+
+ return this.fetch(path, init);
+ }
+
+ async postJSON(path, fields) {
+ const init = this._injectAuth({
+ method: "POST",
+ body: JSON.stringify(fields),
+ headers: {"content-type": "application/json"}
+ });
+
+ return this.fetch(path, init);
+ }
+
+ async fetchText(path, init) {
+ init = this._injectAuth(init);
+ let response = await globalThis.fetch(this.url(path), init);
+
+ if (response.ok)
+ return response.text();
+ else
+ return this._handleHTTPError(response);
+ }
+
+ async fetchJSON(path, init) {
+ init = this._injectAuth(init);
+ let response = await globalThis.fetch(this.url(path), init);
+
+ if (response.ok)
+ return response.json();
+ else
+ return this._handleHTTPError(response);
+ }
+
+
+ async fetchJSON_postJSON(path, fields) {
+ let response = await this.postJSON(path, fields);
+
+ if (response.ok)
+ return response.json();
+ else
+ return this._handleHTTPError(response);
+ }
+}
+
+export const helperApp = new HelperApp();
+
+if (getContextType() !== CONTEXT_BACKGROUND) {
+ send.helperAppGetBackgroundAuth().then(auth => helperApp.auth = auth);
+}
diff --git a/addon/icons/action-sync.png b/addon/icons/action-sync.png
new file mode 100644
index 00000000..4052518b
Binary files /dev/null and b/addon/icons/action-sync.png differ
diff --git a/addon/icons/action-sync.svg b/addon/icons/action-sync.svg
new file mode 100644
index 00000000..c1e2a6f0
--- /dev/null
+++ b/addon/icons/action-sync.svg
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/amo.ico b/addon/icons/amo.ico
new file mode 100644
index 00000000..9534e263
Binary files /dev/null and b/addon/icons/amo.ico differ
diff --git a/addon/icons/batch_warning.svg b/addon/icons/batch_warning.svg
new file mode 100644
index 00000000..8ad65ff6
--- /dev/null
+++ b/addon/icons/batch_warning.svg
@@ -0,0 +1 @@
+multiple-files-processing
\ No newline at end of file
diff --git a/addon/icons/bell.svg b/addon/icons/bell.svg
new file mode 100644
index 00000000..7e5cce1b
--- /dev/null
+++ b/addon/icons/bell.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/bitcoin.png b/addon/icons/bitcoin.png
new file mode 100644
index 00000000..bd666c7f
Binary files /dev/null and b/addon/icons/bitcoin.png differ
diff --git a/addon/icons/bookmark.svg b/addon/icons/bookmark.svg
new file mode 100644
index 00000000..1c6aa2d9
--- /dev/null
+++ b/addon/icons/bookmark.svg
@@ -0,0 +1,71 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/bookmarks.svg b/addon/icons/bookmarks.svg
new file mode 100644
index 00000000..d9290999
--- /dev/null
+++ b/addon/icons/bookmarks.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/bookmarksMenu.svg b/addon/icons/bookmarksMenu.svg
new file mode 100644
index 00000000..c337b7bd
--- /dev/null
+++ b/addon/icons/bookmarksMenu.svg
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/bookmarksMenu2.svg b/addon/icons/bookmarksMenu2.svg
new file mode 100644
index 00000000..4ce73758
--- /dev/null
+++ b/addon/icons/bookmarksMenu2.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/bookmarksToolbar.svg b/addon/icons/bookmarksToolbar.svg
new file mode 100644
index 00000000..2e6f3d8d
--- /dev/null
+++ b/addon/icons/bookmarksToolbar.svg
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/bookmarksToolbar2.svg b/addon/icons/bookmarksToolbar2.svg
new file mode 100644
index 00000000..dd2c15ad
--- /dev/null
+++ b/addon/icons/bookmarksToolbar2.svg
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/books.svg b/addon/icons/books.svg
new file mode 100644
index 00000000..b684a0cd
--- /dev/null
+++ b/addon/icons/books.svg
@@ -0,0 +1,74 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/calendar.svg b/addon/icons/calendar.svg
new file mode 100644
index 00000000..daa536f2
--- /dev/null
+++ b/addon/icons/calendar.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/cancelled.svg b/addon/icons/cancelled.svg
new file mode 100644
index 00000000..d7ba5fee
--- /dev/null
+++ b/addon/icons/cancelled.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/catalogue.svg b/addon/icons/catalogue.svg
new file mode 100644
index 00000000..6cd9d09d
--- /dev/null
+++ b/addon/icons/catalogue.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/chrome-webstore.png b/addon/icons/chrome-webstore.png
new file mode 100644
index 00000000..b66ee34b
Binary files /dev/null and b/addon/icons/chrome-webstore.png differ
diff --git a/addon/icons/chrome.svg b/addon/icons/chrome.svg
new file mode 100644
index 00000000..b102d58b
--- /dev/null
+++ b/addon/icons/chrome.svg
@@ -0,0 +1,44 @@
+
+image/svg+xml
\ No newline at end of file
diff --git a/addon/icons/chrome2.svg b/addon/icons/chrome2.svg
new file mode 100644
index 00000000..05db5907
--- /dev/null
+++ b/addon/icons/chrome2.svg
@@ -0,0 +1,44 @@
+
+image/svg+xml
\ No newline at end of file
diff --git a/addon/icons/cloud.png b/addon/icons/cloud.png
new file mode 100644
index 00000000..ca468df8
Binary files /dev/null and b/addon/icons/cloud.png differ
diff --git a/addon/icons/cloud.svg b/addon/icons/cloud.svg
new file mode 100644
index 00000000..2b60f17d
--- /dev/null
+++ b/addon/icons/cloud.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/cloud2.png b/addon/icons/cloud2.png
new file mode 100644
index 00000000..a2379a06
Binary files /dev/null and b/addon/icons/cloud2.png differ
diff --git a/addon/icons/cloud2.svg b/addon/icons/cloud2.svg
new file mode 100644
index 00000000..3a2e8395
--- /dev/null
+++ b/addon/icons/cloud2.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/cloud_sync.svg b/addon/icons/cloud_sync.svg
new file mode 100644
index 00000000..78d421e6
--- /dev/null
+++ b/addon/icons/cloud_sync.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/cloud_sync2.svg b/addon/icons/cloud_sync2.svg
new file mode 100644
index 00000000..451c71e4
--- /dev/null
+++ b/addon/icons/cloud_sync2.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/code.svg b/addon/icons/code.svg
new file mode 100644
index 00000000..46c8a98f
Binary files /dev/null and b/addon/icons/code.svg differ
diff --git a/addon/icons/comments-filled.svg b/addon/icons/comments-filled.svg
new file mode 100644
index 00000000..30f9e4b7
--- /dev/null
+++ b/addon/icons/comments-filled.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/comments-filled2.svg b/addon/icons/comments-filled2.svg
new file mode 100644
index 00000000..705ff07a
--- /dev/null
+++ b/addon/icons/comments-filled2.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/comments.svg b/addon/icons/comments.svg
new file mode 100644
index 00000000..0cebe7ce
--- /dev/null
+++ b/addon/icons/comments.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/comments2.svg b/addon/icons/comments2.svg
new file mode 100644
index 00000000..bd5b3d75
--- /dev/null
+++ b/addon/icons/comments2.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/containers.svg b/addon/icons/containers.svg
new file mode 100644
index 00000000..e73cdbc1
--- /dev/null
+++ b/addon/icons/containers.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/containers2.svg b/addon/icons/containers2.svg
new file mode 100644
index 00000000..c26c8455
--- /dev/null
+++ b/addon/icons/containers2.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/content-comments.svg b/addon/icons/content-comments.svg
new file mode 100644
index 00000000..34642bef
--- /dev/null
+++ b/addon/icons/content-comments.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/content-notes.svg b/addon/icons/content-notes.svg
new file mode 100644
index 00000000..782e8624
--- /dev/null
+++ b/addon/icons/content-notes.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/content-web.svg b/addon/icons/content-web.svg
new file mode 100644
index 00000000..4c84af29
--- /dev/null
+++ b/addon/icons/content-web.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/copy.svg b/addon/icons/copy.svg
new file mode 100644
index 00000000..584df116
--- /dev/null
+++ b/addon/icons/copy.svg
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/dec.svg b/addon/icons/dec.svg
new file mode 100644
index 00000000..554d020b
Binary files /dev/null and b/addon/icons/dec.svg differ
diff --git a/icons/delete.svg b/addon/icons/delete.svg
similarity index 100%
rename from icons/delete.svg
rename to addon/icons/delete.svg
diff --git a/addon/icons/disk_warning.svg b/addon/icons/disk_warning.svg
new file mode 100644
index 00000000..7cbd6a8b
--- /dev/null
+++ b/addon/icons/disk_warning.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/done.svg b/addon/icons/done.svg
new file mode 100644
index 00000000..2534ee67
--- /dev/null
+++ b/addon/icons/done.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/dropbox.png b/addon/icons/dropbox.png
new file mode 100644
index 00000000..87bcb985
Binary files /dev/null and b/addon/icons/dropbox.png differ
diff --git a/addon/icons/ethereum.svg b/addon/icons/ethereum.svg
new file mode 100644
index 00000000..bc0c279c
--- /dev/null
+++ b/addon/icons/ethereum.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addon/icons/file.svg b/addon/icons/file.svg
new file mode 100644
index 00000000..8823d1e6
--- /dev/null
+++ b/addon/icons/file.svg
@@ -0,0 +1,53 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/file2.svg b/addon/icons/file2.svg
new file mode 100644
index 00000000..6d2c5fd5
--- /dev/null
+++ b/addon/icons/file2.svg
@@ -0,0 +1,53 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/files-box.svg b/addon/icons/files-box.svg
new file mode 100644
index 00000000..3c34580e
Binary files /dev/null and b/addon/icons/files-box.svg differ
diff --git a/addon/icons/files-box2.svg b/addon/icons/files-box2.svg
new file mode 100644
index 00000000..55c8bd7e
Binary files /dev/null and b/addon/icons/files-box2.svg differ
diff --git a/addon/icons/filter-folder.svg b/addon/icons/filter-folder.svg
new file mode 100644
index 00000000..43b279d5
--- /dev/null
+++ b/addon/icons/filter-folder.svg
@@ -0,0 +1,65 @@
+
+
+
+image/svg+xml
+
+
+
+
+
\ No newline at end of file
diff --git a/addon/icons/firefox.svg b/addon/icons/firefox.svg
new file mode 100644
index 00000000..fe2335a3
--- /dev/null
+++ b/addon/icons/firefox.svg
@@ -0,0 +1,58 @@
+
+
+
+
+
+ image/svg+xml
+
+ newtab-firefox-gry
+
+
+
+
+
+ newtab-firefox-gry
+
+
diff --git a/addon/icons/firefox2.svg b/addon/icons/firefox2.svg
new file mode 100644
index 00000000..07892be8
--- /dev/null
+++ b/addon/icons/firefox2.svg
@@ -0,0 +1,58 @@
+
+
+
+
+
+ image/svg+xml
+
+ newtab-firefox-gry
+
+
+
+
+
+ newtab-firefox-gry
+
+
diff --git a/addon/icons/font-default.svg b/addon/icons/font-default.svg
new file mode 100644
index 00000000..3e312313
Binary files /dev/null and b/addon/icons/font-default.svg differ
diff --git a/addon/icons/font-larger.svg b/addon/icons/font-larger.svg
new file mode 100644
index 00000000..555091fa
Binary files /dev/null and b/addon/icons/font-larger.svg differ
diff --git a/addon/icons/font-smaller.svg b/addon/icons/font-smaller.svg
new file mode 100644
index 00000000..5027f83d
Binary files /dev/null and b/addon/icons/font-smaller.svg differ
diff --git a/addon/icons/format-image.svg b/addon/icons/format-image.svg
new file mode 100644
index 00000000..90fa30cb
--- /dev/null
+++ b/addon/icons/format-image.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/format-image2.svg b/addon/icons/format-image2.svg
new file mode 100644
index 00000000..a6e2f1bf
--- /dev/null
+++ b/addon/icons/format-image2.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/format-pdf.svg b/addon/icons/format-pdf.svg
new file mode 100644
index 00000000..cc326106
--- /dev/null
+++ b/addon/icons/format-pdf.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/format-pdf2.svg b/addon/icons/format-pdf2.svg
new file mode 100644
index 00000000..0f6d9973
--- /dev/null
+++ b/addon/icons/format-pdf2.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/github.ico b/addon/icons/github.ico
new file mode 100644
index 00000000..a59308e2
Binary files /dev/null and b/addon/icons/github.ico differ
diff --git a/addon/icons/globe.svg b/addon/icons/globe.svg
new file mode 100644
index 00000000..136518f0
--- /dev/null
+++ b/addon/icons/globe.svg
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
diff --git a/addon/icons/globe2.svg b/addon/icons/globe2.svg
new file mode 100644
index 00000000..eb0e242f
--- /dev/null
+++ b/addon/icons/globe2.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/grid.svg b/addon/icons/grid.svg
new file mode 100644
index 00000000..fdfe926a
--- /dev/null
+++ b/addon/icons/grid.svg
@@ -0,0 +1,179 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/group.svg b/addon/icons/group.svg
new file mode 100644
index 00000000..da1f1385
--- /dev/null
+++ b/addon/icons/group.svg
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/group2.svg b/addon/icons/group2.svg
new file mode 100644
index 00000000..4c7388bd
--- /dev/null
+++ b/addon/icons/group2.svg
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/help-mark.svg b/addon/icons/help-mark.svg
new file mode 100644
index 00000000..1edc2a8a
--- /dev/null
+++ b/addon/icons/help-mark.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/help-mark2.svg b/addon/icons/help-mark2.svg
new file mode 100644
index 00000000..5faa0380
--- /dev/null
+++ b/addon/icons/help-mark2.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/icons/help.svg b/addon/icons/help.svg
similarity index 88%
rename from icons/help.svg
rename to addon/icons/help.svg
index 478c0d36..a1054318 100644
--- a/icons/help.svg
+++ b/addon/icons/help.svg
@@ -19,10 +19,10 @@
xml:space="preserve"
id="svg9"
sodipodi:docname="help.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">image/svg+xml
@@ -44,7 +44,7 @@
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="16.656764"
- inkscape:cx="-8.2857918"
+ inkscape:cx="-21.583693"
inkscape:cy="10.310466"
inkscape:window-x="1920"
inkscape:window-y="0"
@@ -68,18 +68,18 @@
inkscape:locked="false" />
diff --git a/addon/icons/inc.svg b/addon/icons/inc.svg
new file mode 100644
index 00000000..db92f260
Binary files /dev/null and b/addon/icons/inc.svg differ
diff --git a/addon/icons/jar.svg b/addon/icons/jar.svg
new file mode 100644
index 00000000..301776f9
--- /dev/null
+++ b/addon/icons/jar.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/jar2.svg b/addon/icons/jar2.svg
new file mode 100644
index 00000000..8da7beb1
--- /dev/null
+++ b/addon/icons/jar2.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/line-hover.svg b/addon/icons/line-hover.svg
new file mode 100644
index 00000000..13b80fa1
--- /dev/null
+++ b/addon/icons/line-hover.svg
@@ -0,0 +1,58 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/line.svg b/addon/icons/line.svg
new file mode 100644
index 00000000..c302fa74
--- /dev/null
+++ b/addon/icons/line.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/icons/link.svg b/addon/icons/link.svg
similarity index 100%
rename from icons/link.svg
rename to addon/icons/link.svg
diff --git a/addon/icons/lock.js b/addon/icons/lock.js
new file mode 100644
index 00000000..666c9c2d
--- /dev/null
+++ b/addon/icons/lock.js
@@ -0,0 +1,1266 @@
+(function (t, n) {
+ t.__SVGATOR_PLAYER__ = n()
+})(this, (function () {
+ "use strict";
+
+ function t(n) {
+ return (t = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (t) {
+ return typeof t
+ } : function (t) {
+ return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
+ })(n)
+ }
+
+ function n(t, n) {
+ if (!(t instanceof n)) throw new TypeError("Cannot call a class as a function")
+ }
+
+ function r(t, n) {
+ for (var r = 0; r < n.length; r++) {
+ var e = n[r];
+ e.enumerable = e.enumerable || !1, e.configurable = !0, "value" in e && (e.writable = !0), Object.defineProperty(t, e.key, e)
+ }
+ }
+
+ function e(t, n, e) {
+ return n && r(t.prototype, n), e && r(t, e), t
+ }
+
+ function i(t) {
+ return (i = Object.setPrototypeOf ? Object.getPrototypeOf : function (t) {
+ return t.__proto__ || Object.getPrototypeOf(t)
+ })(t)
+ }
+
+ function o(t, n) {
+ return (o = Object.setPrototypeOf || function (t, n) {
+ return t.__proto__ = n, t
+ })(t, n)
+ }
+
+ function u(t, n) {
+ return !n || "object" != typeof n && "function" != typeof n ? function (t) {
+ if (void 0 === t) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+ return t
+ }(t) : n
+ }
+
+ function a(t) {
+ var n = function () {
+ if ("undefined" == typeof Reflect || !Reflect.construct) return !1;
+ if (Reflect.construct.sham) return !1;
+ if ("function" == typeof Proxy) return !0;
+ try {
+ return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], (function () {
+ }))), !0
+ } catch (t) {
+ return !1
+ }
+ }();
+ return function () {
+ var r, e = i(t);
+ if (n) {
+ var o = i(this).constructor;
+ r = Reflect.construct(e, arguments, o)
+ }
+ else r = e.apply(this, arguments);
+ return u(this, r)
+ }
+ }
+
+ function l(t, n, r) {
+ return (l = "undefined" != typeof Reflect && Reflect.get ? Reflect.get : function (t, n, r) {
+ var e = function (t, n) {
+ for (; !Object.prototype.hasOwnProperty.call(t, n) && null !== (t = i(t));) ;
+ return t
+ }(t, n);
+ if (e) {
+ var o = Object.getOwnPropertyDescriptor(e, n);
+ return o.get ? o.get.call(r) : o.value
+ }
+ })(t, n, r || t)
+ }
+
+ var f = Math.abs;
+
+ function s(t) {
+ return t
+ }
+
+ function c(t, n, r) {
+ var e = 1 - r;
+ return 3 * r * e * (t * e + n * r) + r * r * r
+ }
+
+ function h() {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0,
+ r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 1,
+ e = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 1;
+ return t < 0 || t > 1 || r < 0 || r > 1 ? null : f(t - n) <= 1e-5 && f(r - e) <= 1e-5 ? s : function (i) {
+ if (i <= 0) return t > 0 ? i * n / t : 0 === n && r > 0 ? i * e / r : 0;
+ if (i >= 1) return r < 1 ? 1 + (i - 1) * (e - 1) / (r - 1) : 1 === r && t < 1 ? 1 + (i - 1) * (n - 1) / (t - 1) : 1;
+ for (var o, u = 0, a = 1; u < a;) {
+ var l = c(t, r, o = (u + a) / 2);
+ if (f(i - l) < 1e-5) break;
+ l < i ? u = o : a = o
+ }
+ return c(n, e, o)
+ }
+ }
+
+ function v() {
+ return 1
+ }
+
+ function y(t) {
+ return 1 === t ? 1 : 0
+ }
+
+ function d() {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 1,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0;
+ if (1 === t) {
+ if (0 === n) return y;
+ if (1 === n) return v
+ }
+ var r = 1 / t;
+ return function (t) {
+ return t >= 1 ? 1 : (t += n * r) - t % r
+ }
+ }
+
+ function g(t) {
+ var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 2;
+ if (Number.isInteger(t)) return t;
+ var r = Math.pow(10, n);
+ return Math.round(t * r) / r
+ }
+
+ var p = Math.PI / 180;
+
+ function m(t, n, r) {
+ return t >= .5 ? r : n
+ }
+
+ function b(t, n, r) {
+ return 0 === t || n === r ? n : t * (r - n) + n
+ }
+
+ function w(t, n, r) {
+ var e = b(t, n, r);
+ return e <= 0 ? 0 : e
+ }
+
+ function x(t, n, r) {
+ return 0 === t ? n : 1 === t ? r : {
+ x: b(t, n.x, r.x),
+ y: b(t, n.y, r.y)
+ }
+ }
+
+ function k(t, n, r) {
+ var e = function (t, n, r) {
+ return Math.round(b(t, n, r))
+ }(t, n, r);
+ return e <= 0 ? 0 : e >= 255 ? 255 : e
+ }
+
+ function A(t, n, r) {
+ return 0 === t ? n : 1 === t ? r : {
+ r: k(t, n.r, r.r),
+ g: k(t, n.g, r.g),
+ b: k(t, n.b, r.b),
+ a: b(t, null == n.a ? 1 : n.a, null == r.a ? 1 : r.a)
+ }
+ }
+
+ function _(t, n, r) {
+ if (0 === t) return n;
+ if (1 === t) return r;
+ var e = n.length;
+ if (e !== r.length) return m(t, n, r);
+ for (var i = [], o = 0; o < e; o++) i.push(A(t, n[o], r[o]));
+ return i
+ }
+
+ function S(t, n) {
+ for (var r = [], e = 0; e < t; e++) r.push(n);
+ return r
+ }
+
+ function O(t, n) {
+ if (--n <= 0) return t;
+ var r = (t = Object.assign([], t)).length;
+ do {
+ for (var e = 0; e < r; e++) t.push(t[e])
+ } while (--n > 0);
+ return t
+ }
+
+ var M, j = function () {
+ function t(r) {
+ n(this, t), this.list = r, this.length = r.length
+ }
+
+ return e(t, [{
+ key: "setAttribute",
+ value: function (t, n) {
+ for (var r = this.list, e = 0; e < this.length; e++) r[e].setAttribute(t, n)
+ }
+ }, {
+ key: "removeAttribute",
+ value: function (t) {
+ for (var n = this.list, r = 0; r < this.length; r++) n[r].removeAttribute(t)
+ }
+ }, {
+ key: "style",
+ value: function (t, n) {
+ for (var r = this.list, e = 0; e < this.length; e++) r[e].style[t] = n
+ }
+ }]), t
+ }(),
+ F = /-./g,
+ P = function (t, n) {
+ return n.toUpperCase()
+ };
+
+ function B(t) {
+ return "function" == typeof t ? t : m
+ }
+
+ function I(t) {
+ return t ? "function" == typeof t ? t : Array.isArray(t) ? function (t) {
+ var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : s;
+ if (!Array.isArray(t)) return n;
+ switch (t.length) {
+ case 1:
+ return d(t[0]) || n;
+ case 2:
+ return d(t[0], t[1]) || n;
+ case 4:
+ return h(t[0], t[1], t[2], t[3]) || n
+ }
+ return n
+ }(t, null) : function (t, n) {
+ var r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : s;
+ switch (t) {
+ case "linear":
+ return s;
+ case "steps":
+ return d(n.steps || 1, n.jump || 0) || r;
+ case "bezier":
+ case "cubic-bezier":
+ return h(n.x1 || 0, n.y1 || 0, n.x2 || 0, n.y2 || 0) || r
+ }
+ return r
+ }(t.type, t.value, null) : null
+ }
+
+ function R(t, n, r) {
+ var e = arguments.length > 3 && void 0 !== arguments[3] && arguments[3],
+ i = n.length - 1;
+ if (t <= n[0].t) return e ? [0, 0, n[0].v] : n[0].v;
+ if (t >= n[i].t) return e ? [i, 1, n[i].v] : n[i].v;
+ var o, u = n[0],
+ a = null;
+ for (o = 1; o <= i; o++) {
+ if (!(t > n[o].t)) {
+ a = n[o];
+ break
+ }
+ u = n[o]
+ }
+ return null == a ? e ? [i, 1, n[i].v] : n[i].v : u.t === a.t ? e ? [o, 1, a.v] : a.v : (t = (t - u.t) / (a.t - u.t), u.e && (t = u.e(t)),
+ e ? [o, t, r(t, u.v, a.v)]
+ : r(t, u.v, a.v))
+ }
+
+ function q(t, n) {
+ var r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null;
+ return t && t.length ? "function" != typeof n ? null : ("function" != typeof r && (r = null), function (e) {
+ var i = R(e, t, n);
+ return null != i && r && (i = r(i)), i
+ }) : null
+ }
+
+ function E(t, n) {
+ return t.t - n.t
+ }
+
+ function T(n, r, e, i, o) {
+ var u, a = "@" === e[0],
+ l = "#" === e[0],
+ f = M[e],
+ s = m;
+ switch (a ? (u = e.substr(1), e = u.replace(F, P)) : l && (e = e.substr(1)), t(f)) {
+ case "function":
+ if (s = f(i, o, R, I, e, a, r, n), l) return s;
+ break;
+ case "string":
+ s = q(i, B(f));
+ break;
+ case "object":
+ if ((s = q(i, B(f.i), f.f)) && "function" == typeof f.u) return f.u(r, s, e, a, n)
+ }
+ return s ? function (t, n, r) {
+ var e = arguments.length > 3 && void 0 !== arguments[3] && arguments[3];
+ if (e) return t instanceof j ? function (e) {
+ return t.style(n, r(e))
+ } : function (e) {
+ return t.style[n] = r(e)
+ };
+ if (Array.isArray(n)) {
+ var i = n.length;
+ return function (e) {
+ var o = r(e);
+ if (null == o)
+ for (var u = 0; u < i; u++) t[u].removeAttribute(n);
+ else
+ for (var a = 0; a < i; a++) t[a].setAttribute(n, o)
+ }
+ }
+ return function (e) {
+ var i = r(e);
+ null == i ? t.removeAttribute(n) : t.setAttribute(n, i)
+ }
+ }(r, e, s, a) : null
+ }
+
+ function N(n, r, e, i) {
+ if (!i || "object" !== t(i)) return null;
+ var o = null,
+ u = null;
+ return Array.isArray(i) ? u = function (t) {
+ if (!t || !t.length) return null;
+ for (var n = 0; n < t.length; n++) t[n].e && (t[n].e = I(t[n].e));
+ return t.sort(E)
+ }(i) : (u = i.keys, o = i.data || null), u ? T(n, r, e, u, o) : null
+ }
+
+ function C(t, n, r) {
+ if (!r) return null;
+ var e = [];
+ for (var i in r)
+ if (r.hasOwnProperty(i)) {
+ var o = N(t, n, i, r[i]);
+ o && e.push(o)
+ }
+ return e.length ? e : null
+ }
+
+ function z(t, n) {
+ if (!n.duration || n.duration < 0) return null;
+ var r = function (t, n) {
+ if (!n) return null;
+ var r = [];
+ if (Array.isArray(n))
+ for (var e = n.length, i = 0; i < e; i++) {
+ var o = n[i];
+ if (2 === o.length) {
+ var u = null;
+ if ("string" == typeof o[0]) u = t.getElementById(o[0]);
+ else if (Array.isArray(o[0])) {
+ u = [];
+ for (var a = 0; a < o[0].length; a++)
+ if ("string" == typeof o[0][a]) {
+ var l = t.getElementById(o[0][a]);
+ l && u.push(l)
+ }
+ u = u.length ? 1 === u.length ? u[0] : new j(u) : null
+ }
+ if (u) {
+ var f = C(t, u, o[1]);
+ f && (r = r.concat(f))
+ }
+ }
+ } else
+ for (var s in n)
+ if (n.hasOwnProperty(s)) {
+ var c = t.getElementById(s);
+ if (c) {
+ var h = C(t, c, n[s]);
+ h && (r = r.concat(h))
+ }
+ }
+ return r.length ? r : null
+ }(t, n.elements);
+ return r ? function (t, n) {
+ var r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 1 / 0,
+ e = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 1,
+ i = arguments.length > 4 && void 0 !== arguments[4] && arguments[4],
+ o = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : 1,
+ u = t.length,
+ a = e > 0 ? n : 0;
+ i && r % 2 == 0 && (a = n - a);
+ var l = null;
+ return function (f, s) {
+ var c = f % n,
+ h = 1 + (f - c) / n;
+ s *= e, i && h % 2 == 0 && (s = -s);
+ var v = !1;
+ if (h > r) c = a, v = !0, -1 === o && (c = e > 0 ? 0 : n);
+ else if (s < 0 && (c = n - c), c === l) return !1;
+ l = c;
+ for (var y = 0; y < u; y++) t[y](c);
+ return v
+ }
+ }(r, n.duration, n.iterations || 1 / 0, n.direction || 1, !!n.alternate, n.fill || 1) : null
+ }
+
+ var L = function () {
+ function t(r, e) {
+ var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {};
+ n(this, t), this._id = 0, this._running = !1, this._rollingBack = !1, this._animations = r,
+ this.duration = e.duration, this.alternate = e.alternate, this.fill = e.fill, this.iterations = e.iterations,
+ this.direction = i.direction || 1, this.speed = i.speed || 1, this.fps = i.fps || 100, this.offset = i.offset || 0,
+ this.rollbackStartOffset = 0
+ }
+
+ return e(t, [{
+ key: "_rollback",
+ value: function () {
+ var t = this,
+ n = 1 / 0,
+ r = null;
+ this.rollbackStartOffset = this.offset, this._rollingBack || (this._rollingBack = !0, this._running = !0);
+ this._id = window.requestAnimationFrame((function e(i) {
+ if (t._rollingBack) {
+ null == r && (r = i);
+ var o = i - r,
+ u = t.rollbackStartOffset - o,
+ a = Math.round(u * t.speed);
+ if (a > t.duration && n != 1 / 0) {
+ var l = !!t.alternate && a / t.duration % 2 > 1,
+ f = a % t.duration;
+ a = (f += l ? t.duration : 0) || t.duration
+ }
+ var s = t.fps ? 1e3 / t.fps : 0,
+ c = Math.max(0, a);
+ if (c < n - s) {
+ t.offset = c, n = c;
+ for (var h = t._animations, v = h.length, y = 0; y < v; y++) h[y](c, t.direction)
+ }
+ var d = !1;
+ if (t.iterations > 0 && -1 === t.fill) {
+ var g = t.iterations * t.duration,
+ p = g == a;
+ a = p ? 0 : a, t.offset = p ? 0 : t.offset, d = a > g
+ }
+ a > 0 && t.offset >= a && !d ? t._id = window.requestAnimationFrame(e) : t.stop()
+ }
+ }))
+ }
+ }, {
+ key: "_start",
+ value: function () {
+ var t = this,
+ n = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0,
+ r = -1 / 0,
+ e = null,
+ i = {},
+ o = function o(u) {
+ t._running = !0, null == e && (e = u);
+ var a = Math.round((u - e + n) * t.speed),
+ l = t.fps ? 1e3 / t.fps : 0;
+ if (a > r + l && !t._rollingBack) {
+ t.offset = a, r = a;
+ for (var f = t._animations, s = f.length, c = 0, h = 0; h < s; h++) i[h] ? c++ : (i[h] = f[h](a, t.direction), i[h] && c++);
+ if (c === s) return void t._stop()
+ }
+ t._id = window.requestAnimationFrame(o)
+ };
+ this._id = window.requestAnimationFrame(o)
+ }
+ }, {
+ key: "_stop",
+ value: function () {
+ this._id && window.cancelAnimationFrame(this._id), this._running = !1, this._rollingBack = !1
+ }
+ }, {
+ key: "play",
+ value: function () {
+ !this._rollingBack && this._running || (this._rollingBack = !1, this.rollbackStartOffset > this.duration
+ && (this.offset = this.rollbackStartOffset - (this.rollbackStartOffset - this.offset) % this.duration, this.rollbackStartOffset = 0),
+ this._start(this.offset))
+ }
+ }, {
+ key: "stop",
+ value: function () {
+ this._stop(), this.offset = 0, this.rollbackStartOffset = 0;
+ var t = this.direction,
+ n = this._animations;
+ window.requestAnimationFrame((function () {
+ for (var r = 0; r < n.length; r++) n[r](0, t)
+ }))
+ }
+ }, {
+ key: "reachedToEnd",
+ value: function () {
+ return this.iterations > 0 && this.offset >= this.iterations * this.duration
+ }
+ }, {
+ key: "restart",
+ value: function () {
+ this._stop(), this.offset = 0, this._start()
+ }
+ }, {
+ key: "pause",
+ value: function () {
+ this._stop()
+ }
+ }, {
+ key: "reverse",
+ value: function () {
+ this.direction = -this.direction
+ }
+ }], [{
+ key: "build",
+ value: function (n, r) {
+ return (n = function (t, n) {
+ if (M = n, !t || !t.root || !Array.isArray(t.animations)) return null;
+ for (var r = document.getElementsByTagName("svg"), e = !1, i = 0; i < r.length; i++)
+ if (r[i].id === t.root && !r[i].svgatorAnimation) {
+ (e = r[i]).svgatorAnimation = !0;
+ break
+ }
+ if (!e) return null;
+ var o = t.animations.map((function (t) {
+ return z(e, t)
+ })).filter((function (t) {
+ return !!t
+ }));
+ return o.length ? {
+ element: e,
+ animations: o,
+ animationSettings: t.animationSettings,
+ options: t.options || void 0
+ } : null
+ }(n, r)) ? {
+ el: n.element,
+ options: n.options || {},
+ player: new t(n.animations, n.animationSettings, n.options)
+ } : null
+ }
+ }]), t
+ }();
+ !function () {
+ for (var t = 0, n = ["ms", "moz", "webkit", "o"], r = 0; r < n.length && !window.requestAnimationFrame; ++r)
+ window.requestAnimationFrame = window[n[r] + "RequestAnimationFrame"], window.cancelAnimationFrame = window[n[r]
+ + "CancelAnimationFrame"] || window[n[r] + "CancelRequestAnimationFrame"];
+ window.requestAnimationFrame || (window.requestAnimationFrame = function (n) {
+ var r = Date.now(),
+ e = Math.max(0, 16 - (r - t)),
+ i = window.setTimeout((function () {
+ n(r + e)
+ }), e);
+ return t = r + e, i
+ }, window.cancelAnimationFrame = window.clearTimeout)
+ }();
+ var D = /\.0+$/g;
+
+ function Q(t) {
+ return Number.isInteger(t) ? t + "" : t.toFixed(6).replace(D, "")
+ }
+
+ function U(t) {
+ var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : " ";
+ return t && t.length ? t.map(Q).join(n) : ""
+ }
+
+ function V(t) {
+ return t ? null == t.a || t.a >= 1 ? "rgb(" + t.r + "," + t.g + "," + t.b + ")" : "rgba(" + t.r + "," + t.g + "," + t.b + "," + t.a + ")"
+ : "transparent"
+ }
+
+ var $ = {
+ f: null,
+ i: function (t, n, r) {
+ return 0 === t ? n : 1 === t ? r : {
+ x: w(t, n.x, r.x),
+ y: w(t, n.y, r.y)
+ }
+ },
+ u: function (t, n) {
+ return function (r) {
+ var e = n(r);
+ t.setAttribute("rx", Q(e.x)), t.setAttribute("ry", Q(e.y))
+ }
+ }
+ },
+ G = {
+ f: null,
+ i: function (t, n, r) {
+ return 0 === t ? n : 1 === t ? r : {
+ width: w(t, n.width, r.width),
+ height: w(t, n.height, r.height)
+ }
+ },
+ u: function (t, n) {
+ return function (r) {
+ var e = n(r);
+ t.setAttribute("width", Q(e.width)), t.setAttribute("height", Q(e.height))
+ }
+ }
+ },
+ H = Math.sin,
+ Y = Math.cos,
+ Z = Math.acos,
+ J = Math.asin,
+ K = Math.tan,
+ W = Math.atan2,
+ X = Math.PI / 180,
+ tt = 180 / Math.PI,
+ nt = Math.sqrt,
+ rt = function () {
+ function t() {
+ var r = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 1,
+ e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0,
+ i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0,
+ o = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 1,
+ u = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 0,
+ a = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : 0;
+ n(this, t), this.m = [r, e, i, o, u, a], this.i = null, this.w = null, this.s = null
+ }
+
+ return e(t, [{
+ key: "determinant",
+ get: function () {
+ var t = this.m;
+ return t[0] * t[3] - t[1] * t[2]
+ }
+ }, {
+ key: "isIdentity",
+ get: function () {
+ if (null === this.i) {
+ var t = this.m;
+ this.i = 1 === t[0] && 0 === t[1] && 0 === t[2] && 1 === t[3] && 0 === t[4] && 0 === t[5]
+ }
+ return this.i
+ }
+ }, {
+ key: "point",
+ value: function (t, n) {
+ var r = this.m;
+ return {
+ x: r[0] * t + r[2] * n + r[4],
+ y: r[1] * t + r[3] * n + r[5]
+ }
+ }
+ }, {
+ key: "translateSelf",
+ value: function () {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0;
+ if (!t && !n) return this;
+ var r = this.m;
+ return r[4] += r[0] * t + r[2] * n, r[5] += r[1] * t + r[3] * n, this.w = this.s = this.i = null, this
+ }
+ }, {
+ key: "rotateSelf",
+ value: function () {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
+ if (t %= 360) {
+ var n = H(t *= X),
+ r = Y(t),
+ e = this.m,
+ i = e[0],
+ o = e[1];
+ e[0] = i * r + e[2] * n, e[1] = o * r + e[3] * n, e[2] = e[2] * r - i * n, e[3] = e[3] * r - o * n,
+ this.w = this.s = this.i = null
+ }
+ return this
+ }
+ }, {
+ key: "scaleSelf",
+ value: function () {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 1,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 1;
+ if (1 !== t || 1 !== n) {
+ var r = this.m;
+ r[0] *= t, r[1] *= t, r[2] *= n, r[3] *= n, this.w = this.s = this.i = null
+ }
+ return this
+ }
+ }, {
+ key: "skewSelf",
+ value: function (t, n) {
+ if (n %= 360, (t %= 360) || n) {
+ var r = this.m,
+ e = r[0],
+ i = r[1],
+ o = r[2],
+ u = r[3];
+ t && (t = K(t * X), r[2] += e * t, r[3] += i * t), n && (n = K(n * X), r[0] += o * n, r[1] += u * n),
+ this.w = this.s = this.i = null
+ }
+ return this
+ }
+ }, {
+ key: "resetSelf",
+ value: function () {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 1,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0,
+ r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : 0,
+ e = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : 1,
+ i = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 0,
+ o = arguments.length > 5 && void 0 !== arguments[5] ? arguments[5] : 0,
+ u = this.m;
+ return u[0] = t, u[1] = n, u[2] = r, u[3] = e, u[4] = i, u[5] = o, this.w = this.s = this.i = null, this
+ }
+ }, {
+ key: "recomposeSelf",
+ value: function () {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : null,
+ r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : null,
+ e = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : null,
+ i = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : null;
+ return this.isIdentity || this.resetSelf(), t && (t.x || t.y) && this.translateSelf(t.x, t.y), n
+ && this.rotateSelf(n), r && (r.x && this.skewSelf(r.x, 0), r.y && this.skewSelf(0, r.y)), !e || 1 === e.x && 1 === e.y
+ || this.scaleSelf(e.x, e.y), i && (i.x || i.y) && this.translateSelf(i.x, i.y), this
+ }
+ }, {
+ key: "decompose",
+ value: function () {
+ var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0,
+ n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0,
+ r = this.m,
+ e = r[0] * r[0] + r[1] * r[1],
+ i = [
+ [r[0], r[1]],
+ [r[2], r[3]]
+ ],
+ o = nt(e);
+ if (0 === o) return {
+ origin: {
+ x: r[4],
+ y: r[5]
+ },
+ translate: {
+ x: t,
+ y: n
+ },
+ scale: {
+ x: 0,
+ y: 0
+ },
+ skew: {
+ x: 0,
+ y: 0
+ },
+ rotate: 0
+ };
+ i[0][0] /= o, i[0][1] /= o;
+ var u = r[0] * r[3] - r[1] * r[2] < 0;
+ u && (o = -o);
+ var a = i[0][0] * i[1][0] + i[0][1] * i[1][1];
+ i[1][0] -= i[0][0] * a, i[1][1] -= i[0][1] * a;
+ var l = nt(i[1][0] * i[1][0] + i[1][1] * i[1][1]);
+ if (0 === l) return {
+ origin: {
+ x: r[4],
+ y: r[5]
+ },
+ translate: {
+ x: t,
+ y: n
+ },
+ scale: {
+ x: o,
+ y: 0
+ },
+ skew: {
+ x: 0,
+ y: 0
+ },
+ rotate: 0
+ };
+ i[1][0] /= l, i[1][1] /= l, a /= l;
+ var f = 0;
+ return i[1][1] < 0 ? (f = Z(i[1][1]) * tt, i[0][1] < 0 && (f = 360 - f)) : f = J(i[0][1]) * tt, u && (f = -f),
+ a = W(a, nt(i[0][0] * i[0][0] + i[0][1] * i[0][1])) * tt, u && (a = -a), {
+ origin: {
+ x: r[4],
+ y: r[5]
+ },
+ translate: {
+ x: t,
+ y: n
+ },
+ scale: {
+ x: o,
+ y: l
+ },
+ skew: {
+ x: a,
+ y: 0
+ },
+ rotate: f
+ }
+ }
+ }, {
+ key: "toString",
+ value: function () {
+ return null === this.s && (this.s = "matrix(" + this.m.map(it).join(" ") + ")"), this.s
+ }
+ }]), t
+ }(),
+ et = /\.0+$/;
+
+ function it(t) {
+ return Number.isInteger(t) ? t : t.toFixed(14).replace(et, "")
+ }
+
+ function ot(t, n, r) {
+ return t + (n - t) * r
+ }
+
+ function ut(t, n, r) {
+ var e = arguments.length > 3 && void 0 !== arguments[3] && arguments[3],
+ i = {
+ x: ot(t.x, n.x, r),
+ y: ot(t.y, n.y, r)
+ };
+ return e && (i.a = at(t, n)), i
+ }
+
+ function at(t, n) {
+ return Math.atan2(n.y - t.y, n.x - t.x)
+ }
+
+ Object.freeze({
+ M: 2,
+ L: 2,
+ Z: 0,
+ H: 1,
+ V: 1,
+ C: 6,
+ Q: 4,
+ T: 2,
+ S: 4,
+ A: 7
+ });
+ var lt = {},
+ ft = null;
+
+ function st(n) {
+ var r = function () {
+ if (ft) return ft;
+ if ("object" !== ("undefined" == typeof document ? "undefined" : t(document))) return {};
+ var n = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ return n.style.position = "absolute", n.style.opacity = "0.01", n.style.zIndex = "-9999", n.style.left = "-9999px",
+ n.style.width = "1px", n.style.height = "1px", ft = {
+ svg: n
+ }
+ }().svg;
+ if (!r) return function (t) {
+ return null
+ };
+ var e = document.createElementNS(r.namespaceURI, "path");
+ e.setAttributeNS(null, "d", n), e.setAttributeNS(null, "fill", "none"),
+ e.setAttributeNS(null, "stroke", "none"), r.appendChild(e);
+ var i = e.getTotalLength();
+ return function (t) {
+ var n = e.getPointAtLength(i * t);
+ return {
+ x: n.x,
+ y: n.y
+ }
+ }
+ }
+
+ function ct(t) {
+ return lt[t] ? lt[t] : lt[t] = st(t)
+ }
+
+ function ht(t, n, r, e) {
+ if (!t || !e) return !1;
+ var i = ["M", t.x, t.y];
+ if (n && r && (i.push("C"), i.push(n.x), i.push(n.y), i.push(r.x), i.push(r.y)), n ? !r : r) {
+ var o = n || r;
+ i.push("Q"), i.push(o.x), i.push(o.y)
+ }
+ return n || r || i.push("L"), i.push(e.x), i.push(e.y), i.join(" ")
+ }
+
+ function vt(t, n, r, e) {
+ var i = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 1,
+ o = ht(t, n, r, e),
+ u = ct(o);
+ try {
+ return u(i)
+ } catch (t) {
+ return null
+ }
+ }
+
+ function yt(t, n, r, e) {
+ var i = 1 - e;
+ return i * i * t + 2 * i * e * n + e * e * r
+ }
+
+ function dt(t, n, r, e) {
+ return 2 * (1 - e) * (n - t) + 2 * e * (r - n)
+ }
+
+ function gt(t, n, r, e) {
+ var i = arguments.length > 4 && void 0 !== arguments[4] && arguments[4],
+ o = vt(t, n, null, r, e);
+ return o || (o = {
+ x: yt(t.x, n.x, r.x, e),
+ y: yt(t.y, n.y, r.y, e)
+ }), i && (o.a = pt(t, n, r, e)), o
+ }
+
+ function pt(t, n, r, e) {
+ return Math.atan2(dt(t.y, n.y, r.y, e), dt(t.x, n.x, r.x, e))
+ }
+
+ function mt(t, n, r, e, i) {
+ var o = i * i;
+ return i * o * (e - t + 3 * (n - r)) + 3 * o * (t + r - 2 * n) + 3 * i * (n - t) + t
+ }
+
+ function bt(t, n, r, e, i) {
+ var o = 1 - i;
+ return 3 * (o * o * (n - t) + 2 * o * i * (r - n) + i * i * (e - r))
+ }
+
+ function wt(t, n, r, e, i) {
+ var o = arguments.length > 5 && void 0 !== arguments[5] && arguments[5],
+ u = vt(t, n, r, e, i);
+ return u || (u = {
+ x: mt(t.x, n.x, r.x, e.x, i),
+ y: mt(t.y, n.y, r.y, e.y, i)
+ }), o && (u.a = xt(t, n, r, e, i)), u
+ }
+
+ function xt(t, n, r, e, i) {
+ return Math.atan2(bt(t.y, n.y, r.y, e.y, i), bt(t.x, n.x, r.x, e.x, i))
+ }
+
+ function kt(t, n, r) {
+ var e = arguments.length > 3 && void 0 !== arguments[3] && arguments[3];
+ if (_t(n)) {
+ if (St(r)) return gt(n, r.start, r, t, e)
+ }
+ else if (_t(r)) {
+ if (n.end) return gt(n, n.end, r, t, e)
+ }
+ else {
+ if (n.end) return r.start ? wt(n, n.end, r.start, r, t, e) : gt(n, n.end, r, t, e);
+ if (r.start) return gt(n, r.start, r, t, e)
+ }
+ return ut(n, r, t, e)
+ }
+
+ function At(t, n, r) {
+ var e = kt(t, n, r, !0);
+ return e.a = function (t) {
+ var n = arguments.length > 1 && void 0 !== arguments[1] && arguments[1];
+ return n ? t + Math.PI : t
+ }(e.a) / p, e
+ }
+
+ function _t(t) {
+ return !t.type || "corner" === t.type
+ }
+
+ function St(t) {
+ return null != t.start && !_t(t)
+ }
+
+ var Ot = new rt;
+ var Mt = {
+ f: Q,
+ i: b
+ },
+ jt = {
+ f: Q,
+ i: function (t, n, r) {
+ var e = b(t, n, r);
+ return e <= 0 ? 0 : e >= 1 ? 1 : e
+ }
+ };
+
+ function Ft(t, n, r, e, i, o, u, a) {
+ return n = function (t, n, r) {
+ for (var e, i, o, u = t.length - 1, a = {}, l = 0; l <= u; l++) (e = t[l]).e && (e.e = n(e.e)), e.v && "g" === (i = e.v).t && i.r
+ && (o = r.getElementById(i.r)) && (a[i.r] = o.querySelectorAll("stop"));
+ return a
+ }(t, e, a),
+ function (e) {
+ var i, o = r(e, t, Pt);
+ return o ? "c" === o.t ? V(o.v) : "g" === o.t ? (n[o.r] && function (t, n) {
+ for (var r = 0, e = t.length; r < e; r++) t[r].setAttribute("stop-color", V(n[r]))
+ }(n[o.r], o.v), (i = o.r) ? "url(#" + i + ")" : "none") : "none" : "none"
+ }
+ }
+
+ function Pt(t, n, r) {
+ if (0 === t) return n;
+ if (1 === t) return r;
+ if (n && r) {
+ var e = n.t;
+ if (e === r.t) switch (n.t) {
+ case "c":
+ return {
+ t: e,
+ v: A(t, n.v, r.v)
+ };
+ case "g":
+ if (n.r === r.r) return {
+ t: e,
+ v: _(t, n.v, r.v),
+ r: n.r
+ }
+ }
+ }
+ return m(t, n, r)
+ }
+
+ var Bt = {
+ fill: Ft,
+ "fill-opacity": jt,
+ stroke: Ft,
+ "stroke-opacity": jt,
+ "stroke-width": Mt,
+ "stroke-dashoffset": {
+ f: Q,
+ i: b
+ },
+ "stroke-dasharray": {
+ f: function (t) {
+ var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : " ";
+ return t && t.length > 0 && (t = t.map((function (t) {
+ return Math.floor(1e4 * t) / 1e4
+ }))), U(t, n)
+ },
+ i: function (t, n, r) {
+ var e, i, o, u = n.length,
+ a = r.length;
+ if (u !== a)
+ if (0 === u) n = S(u = a, 0);
+ else if (0 === a) a = u, r = S(u, 0);
+ else {
+ var l = (o = (e = u) * (i = a) / function (t, n) {
+ for (var r; n;) r = n, n = t % n, t = r;
+ return t || 1
+ }(e, i)) < 0 ? -o : o;
+ n = O(n, Math.floor(l / u)), r = O(r, Math.floor(l / a)), u = a = l
+ }
+ for (var f = [], s = 0; s < u; s++) f.push(g(w(t, n[s], r[s]), 6));
+ return f
+ }
+ },
+ opacity: jt,
+ transform: function (n, r, e, i) {
+ if (!(n = function (n, r) {
+ if (!n || "object" !== t(n)) return null;
+ var e = !1;
+ for (var i in n) n.hasOwnProperty(i) && (n[i] && n[i].length ? (n[i].forEach((function (t) {
+ t.e && (t.e = r(t.e))
+ })), e = !0) : delete n[i]);
+ return e ? n : null
+ }(n, i))) return null;
+ var o = function (t, i, o) {
+ var u = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : null;
+ return n[t] ? e(i, n[t], o) : r && r[t] ? r[t] : u
+ };
+ return r && r.a && n.o ? function (t) {
+ var r = e(t, n.o, At);
+ return Ot.recomposeSelf(r, o("r", t, b, 0) + r.a, o("k", t, x), o("s", t, x), o("t", t, x)).toString()
+ } : function (t) {
+ return Ot.recomposeSelf(o("o", t, kt, null), o("r", t, b, 0), o("k", t, x), o("s", t, x), o("t", t, x)).toString()
+ }
+ },
+ r: Mt,
+ "#size": G,
+ "#radius": $,
+ _: function (t, n) {
+ if (Array.isArray(t))
+ for (var r = 0; r < t.length; r++) this[t[r]] = n;
+ else this[t] = n
+ }
+ };
+ return function (t) {
+ !function (t, n) {
+ if ("function" != typeof n && null !== n) throw new TypeError("Super expression must either be null or a function");
+ t.prototype = Object.create(n && n.prototype, {
+ constructor: {
+ value: t,
+ writable: !0,
+ configurable: !0
+ }
+ }), n && o(t, n)
+ }(u, t);
+ var r = a(u);
+
+ function u() {
+ return n(this, u), r.apply(this, arguments)
+ }
+
+ return e(u, null, [{
+ key: "build",
+ value: function (t) {
+ var n = l(i(u), "build", this).call(this, t, Bt);
+ n.el, n.options;
+ var r = n.player;
+ return function (t, n, r) {
+ t.play()
+ }(r), r
+ }
+ }]), u
+ }(L)
+}));
+
+var animation = {
+ "root": "eT9VLHb5slv1",
+ "animations": [{
+ "duration": 5000,
+ "direction": 1,
+ "iterations": 0,
+ "fill": 1,
+ "alternate": false,
+ "speed": 1,
+ "elements": {
+ "eT9VLHb5slv8": {
+ "opacity": [{
+ "t": 0,
+ "v": 1
+ }, {
+ "t": 1000,
+ "v": 0.3
+ }, {
+ "t": 2000,
+ "v": 1
+ }, {
+ "t": 3000,
+ "v": 0.3
+ }, {
+ "t": 4000,
+ "v": 1
+ }]
+ },
+ "eT9VLHb5slv9": {
+ "opacity": [{
+ "t": 500,
+ "v": 1
+ }, {
+ "t": 1500,
+ "v": 0.3
+ }, {
+ "t": 2500,
+ "v": 1
+ }, {
+ "t": 3500,
+ "v": 0.3
+ }, {
+ "t": 4500,
+ "v": 1
+ }]
+ },
+ "eT9VLHb5slv10": {
+ "opacity": [{
+ "t": 1100,
+ "v": 1
+ }, {
+ "t": 2100,
+ "v": 0.3
+ }, {
+ "t": 3100,
+ "v": 1
+ }, {
+ "t": 4100,
+ "v": 0.3
+ }, {
+ "t": 5000,
+ "v": 1
+ }]
+ },
+ "eT9VLHb5slv14": {
+ "transform": {
+ "data": {
+ "t": {
+ "x": -1178.445898290934,
+ "y": -962.2699999951578
+ }
+ },
+ "keys": {
+ "o": [{
+ "t": 1500,
+ "v": {
+ "x": 990.6801366500003,
+ "y": 1335.8325074900001,
+ "type": "cusp",
+ "start": {
+ "x": 1018.2508121561755,
+ "y": 1334.957403885074
+ },
+ "end": {
+ "x": 1088.1251614916594,
+ "y": 1053.313533141961
+ }
+ }
+ }, {
+ "t": 5000,
+ "v": {
+ "x": 169.744174,
+ "y": 133.288452,
+ "type": "cusp",
+ "start": {
+ "x": 990.6590500556304,
+ "y": 195.34145411460256
+ }
+ }
+ }],
+ "s": [{
+ "t": 1500,
+ "v": {
+ "x": 0.5,
+ "y": 0.5
+ }
+ }, {
+ "t": 5000,
+ "v": {
+ "x": 1,
+ "y": 1
+ }
+ }]
+ }
+ }
+ }
+ }
+ }],
+ "options": {
+ "start": "load",
+ "hover": "freeze",
+ "click": "freeze",
+ "scroll": 25,
+ "font": "embed",
+ "exportedIds": "unique",
+ "svgFormat": "animated",
+ "title": "lock"
+ },
+ "animationSettings": {
+ "duration": 5000,
+ "direction": 1,
+ "iterations": 0,
+ "fill": 1,
+ "alternate": false,
+ "speed": 1
+ }
+};
+
+var clip = document.querySelector('#eT9VLHb5slv4');
+clip.addEventListener('mouseenter', startAnimation);
+clip.addEventListener('mousemove', startAnimation);
+
+var animated = false;
+function startAnimation() {
+ if (animated)
+ return;
+ animated = true;
+ __SVGATOR_PLAYER__.build(animation)
+}
diff --git a/addon/icons/lock.svg b/addon/icons/lock.svg
new file mode 100644
index 00000000..34eddb5e
--- /dev/null
+++ b/addon/icons/lock.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addon/icons/logo128.png b/addon/icons/logo128.png
new file mode 100644
index 00000000..d2c96627
Binary files /dev/null and b/addon/icons/logo128.png differ
diff --git a/addon/icons/logo16.png b/addon/icons/logo16.png
new file mode 100644
index 00000000..7077aca8
Binary files /dev/null and b/addon/icons/logo16.png differ
diff --git a/addon/icons/logo24.png b/addon/icons/logo24.png
new file mode 100644
index 00000000..22685743
Binary files /dev/null and b/addon/icons/logo24.png differ
diff --git a/addon/icons/logo32.png b/addon/icons/logo32.png
new file mode 100644
index 00000000..b67fa1f9
Binary files /dev/null and b/addon/icons/logo32.png differ
diff --git a/addon/icons/logo64.png b/addon/icons/logo64.png
new file mode 100644
index 00000000..02c5b154
Binary files /dev/null and b/addon/icons/logo64.png differ
diff --git a/addon/icons/logo96.png b/addon/icons/logo96.png
new file mode 100644
index 00000000..59680a89
Binary files /dev/null and b/addon/icons/logo96.png differ
diff --git a/addon/icons/menu.svg b/addon/icons/menu.svg
new file mode 100644
index 00000000..feb517ed
--- /dev/null
+++ b/addon/icons/menu.svg
@@ -0,0 +1,64 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/mode-page.svg b/addon/icons/mode-page.svg
new file mode 100644
index 00000000..0da0a3ae
--- /dev/null
+++ b/addon/icons/mode-page.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/mode-site.svg b/addon/icons/mode-site.svg
new file mode 100644
index 00000000..da176e86
--- /dev/null
+++ b/addon/icons/mode-site.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/new-group.svg b/addon/icons/new-group.svg
new file mode 100644
index 00000000..9c319453
--- /dev/null
+++ b/addon/icons/new-group.svg
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/new-group2.svg b/addon/icons/new-group2.svg
new file mode 100644
index 00000000..8d6f11a9
--- /dev/null
+++ b/addon/icons/new-group2.svg
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/new-shelf.svg b/addon/icons/new-shelf.svg
new file mode 100644
index 00000000..7c3828da
--- /dev/null
+++ b/addon/icons/new-shelf.svg
@@ -0,0 +1,118 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/new-shelf2.svg b/addon/icons/new-shelf2.svg
new file mode 100644
index 00000000..98808000
--- /dev/null
+++ b/addon/icons/new-shelf2.svg
@@ -0,0 +1,118 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/notes.svg b/addon/icons/notes.svg
new file mode 100644
index 00000000..e7d88390
--- /dev/null
+++ b/addon/icons/notes.svg
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/notes2.svg b/addon/icons/notes2.svg
new file mode 100644
index 00000000..4e9a37a1
--- /dev/null
+++ b/addon/icons/notes2.svg
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/onedrive.png b/addon/icons/onedrive.png
new file mode 100644
index 00000000..857442ea
Binary files /dev/null and b/addon/icons/onedrive.png differ
diff --git a/addon/icons/open-link-this-tab.svg b/addon/icons/open-link-this-tab.svg
new file mode 100644
index 00000000..0c13801f
--- /dev/null
+++ b/addon/icons/open-link-this-tab.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/open-link.svg b/addon/icons/open-link.svg
new file mode 100644
index 00000000..8c30c490
--- /dev/null
+++ b/addon/icons/open-link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addon/icons/page-blank.svg b/addon/icons/page-blank.svg
new file mode 100644
index 00000000..0e0b35b6
--- /dev/null
+++ b/addon/icons/page-blank.svg
@@ -0,0 +1,63 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/page-blank2.svg b/addon/icons/page-blank2.svg
new file mode 100644
index 00000000..bcca2f0b
--- /dev/null
+++ b/addon/icons/page-blank2.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/page-info.svg b/addon/icons/page-info.svg
new file mode 100644
index 00000000..30b646bb
--- /dev/null
+++ b/addon/icons/page-info.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/page.svg b/addon/icons/page.svg
new file mode 100644
index 00000000..d5afffbd
Binary files /dev/null and b/addon/icons/page.svg differ
diff --git a/addon/icons/page2.svg b/addon/icons/page2.svg
new file mode 100644
index 00000000..b8cb70fb
Binary files /dev/null and b/addon/icons/page2.svg differ
diff --git a/addon/icons/patreon.png b/addon/icons/patreon.png
new file mode 100644
index 00000000..5c2af68c
Binary files /dev/null and b/addon/icons/patreon.png differ
diff --git a/addon/icons/pocket.svg b/addon/icons/pocket.svg
new file mode 100644
index 00000000..60425e8b
--- /dev/null
+++ b/addon/icons/pocket.svg
@@ -0,0 +1,72 @@
+
+image/svg+xml
\ No newline at end of file
diff --git a/addon/icons/postponed.svg b/addon/icons/postponed.svg
new file mode 100644
index 00000000..dcbdaac3
--- /dev/null
+++ b/addon/icons/postponed.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/properties.svg b/addon/icons/properties.svg
new file mode 100644
index 00000000..42ec9067
--- /dev/null
+++ b/addon/icons/properties.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/properties2.svg b/addon/icons/properties2.svg
new file mode 100644
index 00000000..78f1131e
--- /dev/null
+++ b/addon/icons/properties2.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/icons/reload.svg b/addon/icons/reload.svg
similarity index 94%
rename from icons/reload.svg
rename to addon/icons/reload.svg
index f218227f..13bb16e4 100644
--- a/icons/reload.svg
+++ b/addon/icons/reload.svg
@@ -13,7 +13,7 @@
version="1.1"
id="svg6"
sodipodi:docname="reload.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">
+ inkscape:version="0.92.4 5da689c313, 2019-01-14">
@@ -44,7 +44,7 @@
id="namedview8"
showgrid="false"
inkscape:zoom="22.627417"
- inkscape:cx="-7.7743024"
+ inkscape:cx="-17.563312"
inkscape:cy="13.364672"
inkscape:window-x="1920"
inkscape:window-y="0"
@@ -85,6 +85,6 @@
diff --git a/addon/icons/scrapyard-star.png b/addon/icons/scrapyard-star.png
new file mode 100644
index 00000000..cc987522
Binary files /dev/null and b/addon/icons/scrapyard-star.png differ
diff --git a/addon/icons/scrapyard-star.svg b/addon/icons/scrapyard-star.svg
new file mode 100644
index 00000000..ffe00b47
--- /dev/null
+++ b/addon/icons/scrapyard-star.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/scrapyard.svg b/addon/icons/scrapyard.svg
new file mode 100644
index 00000000..cb1cdc4d
--- /dev/null
+++ b/addon/icons/scrapyard.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/scrapyard_mg.svg b/addon/icons/scrapyard_mg.svg
new file mode 100644
index 00000000..386da0cd
--- /dev/null
+++ b/addon/icons/scrapyard_mg.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/search.svg b/addon/icons/search.svg
new file mode 100644
index 00000000..578bd2b1
--- /dev/null
+++ b/addon/icons/search.svg
@@ -0,0 +1,72 @@
+
+
+
+image/svg+xml
+
+
+
+
\ No newline at end of file
diff --git a/icons/selection.svg b/addon/icons/selection.svg
similarity index 100%
rename from icons/selection.svg
rename to addon/icons/selection.svg
diff --git a/addon/icons/separator.svg b/addon/icons/separator.svg
new file mode 100644
index 00000000..0d646d2c
--- /dev/null
+++ b/addon/icons/separator.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/separator2.svg b/addon/icons/separator2.svg
new file mode 100644
index 00000000..ba20164a
--- /dev/null
+++ b/addon/icons/separator2.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/icons/settings.svg b/addon/icons/settings.svg
similarity index 91%
rename from icons/settings.svg
rename to addon/icons/settings.svg
index 321c3d74..fee2ff35 100644
--- a/icons/settings.svg
+++ b/addon/icons/settings.svg
@@ -19,7 +19,7 @@
xml:space="preserve"
id="svg6"
sodipodi:docname="settings.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">image/svg+xml
+ style="opacity:1;fill:#000000;fill-opacity:1">
+ style="fill:#000000;fill-opacity:1">
diff --git a/addon/icons/shelf.svg b/addon/icons/shelf.svg
new file mode 100644
index 00000000..0322d89a
--- /dev/null
+++ b/addon/icons/shelf.svg
@@ -0,0 +1,95 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/shelf2.svg b/addon/icons/shelf2.svg
new file mode 100644
index 00000000..9efbd012
--- /dev/null
+++ b/addon/icons/shelf2.svg
@@ -0,0 +1,95 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/sidebar.svg b/addon/icons/sidebar.svg
new file mode 100644
index 00000000..57e773cd
--- /dev/null
+++ b/addon/icons/sidebar.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/addon/icons/star.svg b/addon/icons/star.svg
new file mode 100644
index 00000000..756594a6
--- /dev/null
+++ b/addon/icons/star.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/sync.svg b/addon/icons/sync.svg
new file mode 100644
index 00000000..f17cd394
--- /dev/null
+++ b/addon/icons/sync.svg
@@ -0,0 +1,97 @@
+
+
+
+
+
+ image/svg+xml
+
+
+ reload
+
+
+
+
+
+
+
+
+
+
+
+
+
+ reload
+
+
+
+
diff --git a/addon/icons/tags.svg b/addon/icons/tags.svg
new file mode 100644
index 00000000..20c9f6f2
--- /dev/null
+++ b/addon/icons/tags.svg
@@ -0,0 +1,71 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/tape.svg b/addon/icons/tape.svg
new file mode 100644
index 00000000..210dc31c
--- /dev/null
+++ b/addon/icons/tape.svg
@@ -0,0 +1,96 @@
+
+
+
+image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addon/icons/tape2.svg b/addon/icons/tape2.svg
new file mode 100644
index 00000000..9de122c4
--- /dev/null
+++ b/addon/icons/tape2.svg
@@ -0,0 +1,140 @@
+
+
+
+image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addon/icons/text.svg b/addon/icons/text.svg
new file mode 100644
index 00000000..64ba9537
--- /dev/null
+++ b/addon/icons/text.svg
@@ -0,0 +1,71 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/toc.svg b/addon/icons/toc.svg
new file mode 100644
index 00000000..d9246f43
--- /dev/null
+++ b/addon/icons/toc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addon/icons/todo.svg b/addon/icons/todo.svg
new file mode 100644
index 00000000..eecbe400
--- /dev/null
+++ b/addon/icons/todo.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/tree-select.svg b/addon/icons/tree-select.svg
new file mode 100644
index 00000000..a4758a41
--- /dev/null
+++ b/addon/icons/tree-select.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/unfiledBookmarks.svg b/addon/icons/unfiledBookmarks.svg
new file mode 100644
index 00000000..c26fc6d8
--- /dev/null
+++ b/addon/icons/unfiledBookmarks.svg
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/unfiledBookmarks2.svg b/addon/icons/unfiledBookmarks2.svg
new file mode 100644
index 00000000..a61c400e
--- /dev/null
+++ b/addon/icons/unfiledBookmarks2.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/url.svg b/addon/icons/url.svg
new file mode 100644
index 00000000..f2c1a140
--- /dev/null
+++ b/addon/icons/url.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/waiting.svg b/addon/icons/waiting.svg
new file mode 100644
index 00000000..ec64401a
--- /dev/null
+++ b/addon/icons/waiting.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/web-archive.svg b/addon/icons/web-archive.svg
new file mode 100644
index 00000000..5f8c4063
--- /dev/null
+++ b/addon/icons/web-archive.svg
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/web.svg b/addon/icons/web.svg
new file mode 100644
index 00000000..f768870f
--- /dev/null
+++ b/addon/icons/web.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/web2.svg b/addon/icons/web2.svg
new file mode 100644
index 00000000..82c1f4b0
--- /dev/null
+++ b/addon/icons/web2.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/icons/zip.svg b/addon/icons/zip.svg
new file mode 100644
index 00000000..e63cb41f
--- /dev/null
+++ b/addon/icons/zip.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/images/android_qrcode.png b/addon/images/android_qrcode.png
new file mode 100644
index 00000000..2a911105
Binary files /dev/null and b/addon/images/android_qrcode.png differ
diff --git a/addon/images/checkbox.png b/addon/images/checkbox.png
new file mode 100644
index 00000000..e0faf6a4
Binary files /dev/null and b/addon/images/checkbox.png differ
diff --git a/addon/images/circuit.png b/addon/images/circuit.png
new file mode 100644
index 00000000..52da8aec
Binary files /dev/null and b/addon/images/circuit.png differ
diff --git a/addon/images/donation_kitty.png b/addon/images/donation_kitty.png
new file mode 100644
index 00000000..0889f535
Binary files /dev/null and b/addon/images/donation_kitty.png differ
diff --git a/addon/images/dropdown.svg b/addon/images/dropdown.svg
new file mode 100644
index 00000000..dbab466f
--- /dev/null
+++ b/addon/images/dropdown.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/images/radiobutton.png b/addon/images/radiobutton.png
new file mode 100644
index 00000000..3e38e000
Binary files /dev/null and b/addon/images/radiobutton.png differ
diff --git a/addon/images/ufo.svg b/addon/images/ufo.svg
new file mode 100644
index 00000000..292dd616
--- /dev/null
+++ b/addon/images/ufo.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/import.js b/addon/import.js
new file mode 100644
index 00000000..f4c5c578
--- /dev/null
+++ b/addon/import.js
@@ -0,0 +1,141 @@
+import {
+ BROWSER_SHELF_ID,
+ CLOUD_SHELF_ID,
+ DEFAULT_SHELF_NAME,
+ DONE_SHELF_NAME,
+ EVERYTHING_SHELF_NAME, FILES_SHELF_ID,
+ RDF_EXTERNAL_TYPE,
+ TODO_SHELF_NAME
+} from "./storage.js";
+import {Query} from "./storage_query.js";
+import {TODO} from "./bookmarks_todo.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {MarshallerORG, UnmarshallerORG} from "./marshaller_org.js";
+import {NetscapeImporterBuilder} from "./import_html.js";
+import {RDFImporterBuilder} from "./import_rdf.js";
+import {StreamExporterBuilder, StreamImporterBuilder, StructuredStreamImporterBuilder} from "./import_drivers.js";
+import {undoManager} from "./bookmarks_undo.js";
+import {Database} from "./storage_database.js";
+import {DiskStorage} from "./storage_external.js";
+import {MarshallerJSONScrapbook, UnmarshallerJSONScrapbook} from "./marshaller_json_scrapbook.js";
+
+export class Export {
+ static create(format) {
+ switch (format) {
+ case "json":
+ return new StreamExporterBuilder(new MarshallerJSONScrapbook());
+ case "org":
+ return new StreamExporterBuilder(new MarshallerORG());
+ }
+ }
+
+ // get nodes of the specified shelf for export
+ static async nodes(shelf, computeLevel) {
+ const isShelfName = typeof shelf === "string";
+ let nodes;
+
+ if (isShelfName && shelf.toUpperCase() === TODO_SHELF_NAME) {
+ nodes = await TODO.listTODO();
+
+ if (computeLevel)
+ nodes.forEach(n => n.__level = 1)
+
+ return nodes;
+ }
+ else if (isShelfName && shelf.toUpperCase() === DONE_SHELF_NAME) {
+ nodes = await TODO.listDONE();
+
+ if (computeLevel)
+ nodes.forEach(n => n.__level = 1)
+
+ return nodes;
+ }
+
+ const everything = isShelfName && shelf === EVERYTHING_SHELF_NAME;
+
+ if (!everything && isShelfName)
+ shelf = await Query.shelf(shelf);
+
+ let level = computeLevel? (everything? 1: 0): undefined;
+
+ if (everything) {
+ const shelves = await Query.allShelves();
+ const cloud = shelves.find(s => s.id === CLOUD_SHELF_ID);
+
+ if (cloud)
+ shelves.splice(shelves.indexOf(cloud), 1);
+
+ const browser = shelves.find(s => s.id === BROWSER_SHELF_ID);
+ if (browser)
+ shelves.splice(shelves.indexOf(browser), 1);
+
+ const files = shelves.find(s => s.id === FILES_SHELF_ID);
+ if (files)
+ shelves.splice(shelves.indexOf(files), 1);
+
+ const openRDFShelves = shelves.filter(n => n.external === RDF_EXTERNAL_TYPE);
+ for (const node of openRDFShelves)
+ shelves.splice(shelves.indexOf(node), 1);
+
+ nodes = await Query.fullSubtree(shelves.map(s => s.id), true, level);
+ }
+ else {
+ nodes = await Query.fullSubtree(shelf.id,true, level);
+ nodes.shift();
+ }
+
+ return nodes;
+ }
+}
+
+export class Import {
+ static create(format) {
+ switch (format) {
+ case "json":
+ case "jsonl":
+ case "jsbk":
+ return new StructuredStreamImporterBuilder(new UnmarshallerJSONScrapbook());
+ case "org":
+ return new StreamImporterBuilder(new UnmarshallerORG());
+ case "html":
+ return new NetscapeImporterBuilder(null);
+ case "rdf":
+ return new RDFImporterBuilder(null);
+ }
+ }
+
+ static async prepare(shelf) {
+ try {
+ await undoManager.commit();
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (shelf === EVERYTHING_SHELF_NAME) {
+ await Database.wipeImportable();
+ await DiskStorage.wipeStorage();
+ }
+ else {
+ shelf = await Query.shelf(shelf);
+
+ if (shelf && shelf.name === DEFAULT_SHELF_NAME) {
+ return Bookmark.deleteChildren(shelf.id);
+ } else if (shelf) {
+ return Bookmark.delete(shelf.id);
+ }
+ }
+ }
+
+ static async transaction(importer) {
+ try {
+ await DiskStorage.openBatchSession();
+ return importer.import();
+ }
+ finally {
+ await DiskStorage.closeBatchSession();
+ }
+ }
+}
+
+
+
diff --git a/addon/import_drivers.js b/addon/import_drivers.js
new file mode 100644
index 00000000..f8726a80
--- /dev/null
+++ b/addon/import_drivers.js
@@ -0,0 +1,274 @@
+import {ProgressCounter} from "./utils.js";
+import {
+ DEFAULT_SHELF_ID,
+ DEFAULT_SHELF_UUID,
+ DEFAULT_SHELF_NAME,
+ BROWSER_SHELF_ID,
+ isContainerNode,
+ NODE_TYPE_FOLDER,
+ NODE_TYPE_SHELF, EVERYTHING_SHELF_NAME
+} from "./storage.js";
+import {Folder} from "./bookmarks_folder.js";
+import UUID from "./uuid.js";
+import {formatShelfName} from "./bookmarking.js";
+import {Import} from "./import.js";
+import {SCRAPYARD_STORAGE_FORMAT, UnmarshallerJSON} from "./marshaller_json.js";
+
+export class StreamExporterBuilder {
+ constructor(marshaller) {
+ this._marshaller = marshaller;
+ this._exportOptions = {};
+ }
+
+ setName(name) {
+ this._exportOptions.name = name;
+ return this;
+ }
+
+ setComment(comment) {
+ this._exportOptions.comment = comment;
+ return this;
+ }
+
+ setUUID(uuid) {
+ this._exportOptions.uuid = uuid;
+ return this;
+ }
+
+ setLinksOnly(linksOnly) {
+ this._exportOptions.linksOnly = linksOnly;
+ return this;
+ }
+
+ setStream(stream) {
+ this._exportOptions.stream = stream;
+ return this;
+ }
+
+ setObjects(objects) {
+ this._exportOptions.objects = objects;
+ return this;
+ }
+
+ setReportProgress(report) {
+ this._exportOptions.progress = report;
+ return this;
+ }
+
+ setMuteSidebar(mute) {
+ this._exportOptions.muteSidebar = mute;
+ return this;
+ }
+
+ setSidebarContext(val) {
+ this._exportOptions.sidebarContext = val;
+ return this;
+ }
+
+ _createExporter(options) {
+ const exporter = new StreamExporter(this._marshaller);
+ exporter._exportOptions = options;
+ return exporter;
+ }
+
+ build() {
+ this._marshaller?.configure(this._exportOptions);
+ return this._createExporter(this._exportOptions);
+ }
+}
+
+export class StreamExporter {
+ constructor(marshaller) {
+ this._marshaller = marshaller;
+ }
+
+ async export() {
+ const {progress, muteSidebar, objects} = this._exportOptions;
+ const marshaller = this._marshaller;
+
+ await marshaller.marshalMeta(this._exportOptions);
+
+ if (objects.length) {
+ const local = !_BACKGROUND_PAGE && !muteSidebar;
+ const progressCounter = progress
+ ? new ProgressCounter(objects.length, "exportProgress", {muteSidebar}, local)
+ : null;
+
+ for (let object of objects) {
+ await marshaller.marshal(object);
+ progressCounter?.incrementAndNotify()
+ }
+
+ progressCounter?.finish();
+ }
+ }
+}
+
+export class StreamImporterBuilder {
+ constructor(unmarshaller) {
+ this._unmarshaller = unmarshaller;
+ this._importOptions = {};
+ }
+
+ setName(name) {
+ this._importOptions.name = name;
+ return this;
+ }
+
+ setStream(stream) {
+ this._importOptions.stream = stream;
+ return this;
+ }
+
+ setReportProgress(report) {
+ this._importOptions.progress = report;
+ return this;
+ }
+
+ setMuteSidebar(mute) {
+ this._importOptions.muteSidebar = mute;
+ return this;
+ }
+
+ setSidebarContext(val) {
+ this._importOptions.sidebarContext = val;
+ return this;
+ }
+
+ _createImporter(options) {
+ const importer = new StreamImporter(this._unmarshaller);
+ importer._importOptions = options;
+ return importer;
+ }
+
+ build() {
+ this._unmarshaller?.configure(this._importOptions);
+ return this._createImporter(this._importOptions);
+ }
+}
+
+export class StreamImporter {
+ constructor(unmarshaller) {
+ this._unmarshaller = unmarshaller;
+ }
+
+ async import() {
+ let {name: shelfName, progress, muteSidebar} = this._importOptions;
+ const unmarshaller = this._unmarshaller;
+
+ const meta = await unmarshaller.unmarshalMeta();
+
+ await Import.prepare(shelfName);
+
+ const local = !_BACKGROUND_PAGE && !muteSidebar;
+ const progressCounter = progress && !!meta?.entities
+ ? new ProgressCounter(meta.entities, "importProgress", {muteSidebar}, local)
+ : null;
+
+ while (await unmarshaller.unmarshal())
+ progressCounter?.incrementAndNotify();
+
+ progressCounter?.finish();
+ }
+}
+
+export class StructuredStreamImporterBuilder extends StreamImporterBuilder {
+ _createImporter(options) {
+ const importer = new StructuredStreamImporter(this._unmarshaller);
+ importer._importOptions = options;
+ return importer;
+ }
+}
+
+// Imports JSON Lines explicitly structured as a tree through id references
+export class StructuredStreamImporter {
+ constructor(unmarshaller) {
+ this._unmarshaller = unmarshaller;
+ }
+
+ async import() {
+ let unmarshaller = this._unmarshaller;
+ const meta = await unmarshaller.unmarshalMeta();
+
+ if (!meta)
+ throw new Error("Invalid file format.");
+
+ if (meta.export === SCRAPYARD_STORAGE_FORMAT) {
+ unmarshaller = new UnmarshallerJSON(meta);
+ unmarshaller.configure(this._importOptions);
+ }
+
+ let firstObject = await unmarshaller.unmarshal();
+
+ if (!firstObject)
+ throw new Error("Invalid file format.");
+
+ let {name: shelfName, progress, muteSidebar} = this._importOptions;
+
+ await Import.prepare(shelfName);
+
+ progress = progress && !!meta.entities;
+ const localSender = !_BACKGROUND_PAGE && !muteSidebar;
+ this._progressCounter = progress
+ ? new ProgressCounter(meta.entities, "importProgress", {muteSidebar}, localSender)
+ : null;
+
+ this._importParentId2DBParentId = new Map();
+ this._importParentId2DBParentId.set(DEFAULT_SHELF_ID, DEFAULT_SHELF_ID);
+ this._everythingAsShelf = !firstObject.node.parent_id && shelfName !== EVERYTHING_SHELF_NAME;
+ this._shelfNode = shelfName !== EVERYTHING_SHELF_NAME? await Folder.getOrCreateByPath(shelfName): null;
+
+ if (this._shelfNode) // first object contains id of its parent shelf if a shelf (not everything) is imported
+ this._importParentId2DBParentId.set(firstObject.node.parent_id, this._shelfNode.id);
+
+ await this._importObject(firstObject);
+
+ let object;
+ while (object = await unmarshaller.unmarshal()) {
+ await this._importObject(object);
+ }
+
+ if (progress && !this._progressCounter.isFinished())
+ throw new Error("some records are missing");
+ else
+ this._progressCounter?.finish();
+
+ return this._shelfNode;
+ }
+
+ async _importObject(object) {
+ const {_importParentId2DBParentId, _shelfNode, _everythingAsShelf, _progressCounter} = this;
+ this._renameBuiltinShelves(object.node);
+
+ // importing the default shelf
+ if (object.node.type === NODE_TYPE_SHELF && object.node.name?.toLowerCase() === DEFAULT_SHELF_NAME) {
+ if (_everythingAsShelf) // import default shelf as a folder
+ object.node.uuid = UUID.numeric();
+ else { // force import to the external storage
+ object.node.id = DEFAULT_SHELF_ID;
+ object.node.uuid = DEFAULT_SHELF_UUID;
+ object.node.date_modified = 0;
+ }
+ }
+
+ if (object.node.parent_id)
+ object.node.parent_id = _importParentId2DBParentId.get(object.node.parent_id);
+ else if (_everythingAsShelf && object.node.type === NODE_TYPE_SHELF) {
+ object.node.type = NODE_TYPE_FOLDER;
+ object.node.parent_id = _shelfNode.id;
+ }
+
+ let objectImportId = object.node.id;
+ const node = await object.persist();
+
+ if (objectImportId && isContainerNode(node))
+ _importParentId2DBParentId.set(objectImportId, node.id);
+
+ _progressCounter?.incrementAndNotify();
+ }
+
+ _renameBuiltinShelves(node) {
+ if (node && node.id === BROWSER_SHELF_ID)
+ node.name = `${formatShelfName(node.name)} (imported)`;
+ }
+}
diff --git a/addon/import_html.js b/addon/import_html.js
new file mode 100644
index 00000000..54d0322b
--- /dev/null
+++ b/addon/import_html.js
@@ -0,0 +1,60 @@
+import {NODE_TYPE_BOOKMARK} from "./storage.js";
+import {Import} from "./import.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {StreamImporterBuilder} from "./import_drivers.js";
+
+async function importHtml(shelf, html) {
+ await Import.prepare(shelf);
+
+ let doc = jQuery.parseHTML(html);
+ let root = doc.find(e => e.localName === "dl");
+ let path = [shelf];
+
+ function peekType(node) {
+ for (let child of node.childNodes) {
+ if (child.localName === "a")
+ return ["link", child];
+ else if (child.localName === "h3")
+ return ["folder", child];
+ }
+ return ["unknown", null];
+ }
+
+ async function traverseHtml(root, path) {
+ for (let child of root.childNodes) {
+ if (child.localName === "dt") {
+ let [type, node] = peekType(child);
+ if (type === "folder") {
+ path.push(node.textContent);
+ await traverseHtml(child, path);
+ path.pop();
+ }
+ else if (type === "link") {
+ node = await Bookmark.import({
+ uri: node.href,
+ name: node.textContent,
+ type: NODE_TYPE_BOOKMARK,
+ path: path.join("/")
+ });
+
+ await Bookmark.storeIconFromURI(node)
+ }
+ }
+ else if (child.localName === "dl") {
+ await traverseHtml(child, path);
+ }
+ }
+ }
+
+ await traverseHtml(root, path);
+}
+
+export class NetscapeImporterBuilder extends StreamImporterBuilder {
+ _createImporter(options) {
+ return {
+ import() {
+ return importHtml(options.name, options.stream);
+ }
+ };
+ }
+}
diff --git a/addon/import_rdf.js b/addon/import_rdf.js
new file mode 100644
index 00000000..7666f320
--- /dev/null
+++ b/addon/import_rdf.js
@@ -0,0 +1,319 @@
+import {helperApp} from "./helper_app.js";
+import {
+ ARCHIVE_TYPE_FILES,
+ NODE_TYPE_ARCHIVE,
+ NODE_TYPE_FOLDER,
+ NODE_TYPE_SEPARATOR,
+ RDF_EXTERNAL_TYPE
+} from "./storage.js";
+import {ProgressCounter} from "./utils.js";
+import {send, sendLocal} from "./proxy.js";
+import {Folder} from "./bookmarks_folder.js";
+import {Bookmark} from "./bookmarks_bookmark.js";
+import {Archive, Comments, Node} from "./storage_entities.js";
+import {StreamImporterBuilder} from "./import_drivers.js";
+import {RDFNamespaces} from "./utils_html.js";
+import {settings} from "./settings.js";
+
+class RDFImporter {
+ #options;
+ #nodeID_SB2SY = new Map();
+ #nodeID_SY2SB = new Map();
+ #shelf;
+ #bookmarks = [];
+ #cancelled = false;
+ #progressCounter;
+ #threadCount;
+ #sidebarSender;
+ #importType = "full";
+
+ #Bookmark = Bookmark;
+ #Folder = Folder;
+
+ constructor(importOptions) {
+ this.#options = importOptions;
+ this.#sidebarSender = importOptions.sidebarContext? sendLocal: send;
+
+ if (importOptions.quick) {
+ this.#Bookmark = Bookmark.idb;
+ this.#Folder = Folder.idb;
+ this.#importType = "index";
+ }
+ }
+
+ async import() {
+ const helper = await helperApp.probe(true);
+
+ if (helper) {
+ const path = this.#options.stream.replace(/\\/g, "/");
+ const rdfFile = path.split("/").at(-1);
+ const rdfDirectory = path.substring(0, path.lastIndexOf("/"));
+ const xml = await this.#getRDFXML(rdfFile, rdfDirectory);
+
+ if (!xml)
+ return Promise.reject(new Error("RDF file not found."));
+
+ await this.#buildBookmarkTree(path, xml);
+ await this.#importArchives(rdfDirectory);
+ }
+ }
+
+ #traverseRDFTree(doc, visitor, data) {
+ const namespaces = new RDFNamespaces(doc);
+ const seqs = this.#mapURNToNodes("//RDF:Seq", doc, namespaces, false);
+ const separators = this.#mapURNToNodes("//NC:BookmarkSeparator", doc, namespaces)
+ const descriptions = this.#mapURNToNodes("//RDF:Description", doc, namespaces);
+ const leaves = new Map([...separators, ...descriptions]);
+
+ async function doTraverse(parent, current, visitor) {
+ let seq = seqs.get(current? current.__sb_about: "urn:scrapbook:root");
+ let children = seq.children;
+
+ if (children && children.length) {
+ for (let i = 0; i < children.length; ++i) {
+ if (children[i].localName === "li") {
+ let resource = children[i].getAttributeNS(namespaces.NS_RDF, "resource");
+ let node = leaves.get(resource);
+
+ if (node) {
+ await visitor(current, node, data);
+ if (node.__sb_type === "folder")
+ await doTraverse(current, node, visitor);
+ }
+ }
+ }
+ }
+ }
+
+ return doTraverse(null, null, visitor);
+ }
+
+ #mapURNToNodes(xpath, doc, namespaces, collectAttributes = true) {
+ const result = new Map();
+ const nodes = doc.evaluate(xpath, doc, namespaces.resolver, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
+ let node;
+
+ while (node = nodes.iterateNext()) {
+ if (collectAttributes) {
+ node.__sb_about = node.getAttributeNS(namespaces.NS_RDF, "about");
+ node.__sb_id = node.getAttributeNS(namespaces.NS_SCRAPBOOK, "id");
+ node.__sb_type = node.getAttributeNS(namespaces.NS_SCRAPBOOK, "type");
+ node.__sb_title = node.getAttributeNS(namespaces.NS_SCRAPBOOK, "title");
+ node.__sb_source = node.getAttributeNS(namespaces.NS_SCRAPBOOK, "source");
+ node.__sb_icon = node.getAttributeNS(namespaces.NS_SCRAPBOOK, "icon");
+ node.__sb_comment = node.getAttributeNS(namespaces.NS_SCRAPBOOK, "comment");
+
+ if (node.__sb_comment)
+ node.__sb_comment = node.__sb_comment.replace(/ __BR__ /g, "\n");
+ }
+
+ result.set(node.getAttributeNS(namespaces.NS_RDF, "about"), node);
+ }
+
+ return result;
+ }
+
+ async #getRDFXML(rdfFile, rdfDirectory) {
+ let xml = null;
+
+ try {
+ let form = new FormData();
+ form.append("rdf_file", rdfFile);
+ form.append("rdf_directory", rdfDirectory);
+
+ xml = await helperApp.fetchText(`/rdf/import/${rdfFile}`, {method: "POST", body: form});
+ } catch (e) {
+ console.error(e);
+ }
+
+ return xml;
+ }
+
+ async #buildBookmarkTree(path, xml) {
+ this.#shelf = await this.#createShelf(path);
+ const rdfDoc = new DOMParser().parseFromString(xml, 'application/xml');
+
+ await this.#traverseRDFTree(rdfDoc, this.#createBookmark.bind(this), {pos: 0});
+ }
+
+ async #importArchives(path) {
+ let cancelListener = (message, sender, sendResponse) => {
+ if (message.type === "cancelRdfImport")
+ this.#cancelled = true;
+ };
+ browser.runtime.onMessage.addListener(cancelListener);
+
+ try {
+ if (this.#options.createIndex) { // createIndex is always on when performing a full import
+ this.#progressCounter =
+ new ProgressCounter(this.#bookmarks.length, "rdfImportProgress", {muteSidebar: true});
+ await this.#startThreads(this.#importThread.bind(this, path), this.#options.threads);
+ }
+ else
+ await this.#onFinish();
+ } finally {
+ browser.runtime.onMessage.removeListener(cancelListener);
+ }
+ }
+
+ async #startThreads(threadf, maxThreads) {
+ const bookmarks = [...this.#bookmarks];
+ this.#threadCount = Math.min(maxThreads, this.#bookmarks.length);
+
+ const promises = [];
+ for (let i = 0; i < maxThreads; ++i)
+ promises.push(threadf(bookmarks));
+
+ return Promise.all(promises);
+ }
+
+ async #importThread(path, bookmarks) {
+ if (bookmarks.length && !this.#cancelled) {
+ let bookmark = bookmarks.shift();
+
+ try {
+ let scrapbookId = this.#nodeID_SY2SB.get(bookmark.id);
+ await this.#importRDFArchive(path, bookmark, scrapbookId);
+ } catch (e) {
+ send.rdfImportError({bookmark: bookmark, error: e.message});
+ }
+
+ this.#progressCounter.incrementAndNotify();
+
+ return this.#importThread(path, bookmarks);
+ }
+ else {
+ this.#threadCount -= 1;
+ if (this.#threadCount === 0)
+ return this.#onFinish();
+ }
+ }
+
+ async #importRDFArchive(path, node, scrapbookId) {
+ const params = {
+ data_path: settings.data_folder_path(),
+ rdf_archive_path: path,
+ uuid: node.uuid,
+ scrapbook_id: scrapbookId
+ };
+
+ try {
+ const url = `/rdf/import/archive?type=${this.#importType}`;
+ const response = await helperApp.fetchJSON_postJSON(url, params);
+
+ if (response?.size) {
+ node.size = response.size;
+ await Node.update(node);
+ }
+
+ if (response?.archive_index)
+ Archive.idb.import.storeIndex(node, response.archive_index);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ async #onFinish() {
+ this.#sidebarSender.nodesImported({shelf: this.#shelf});
+
+ if (this.#options.createIndex)
+ this.#progressCounter.finish();
+
+ send.obtainingIcons({shelf: this.#shelf});
+ await this.#startThreads(this.#iconImportThread.bind(this), this.#options.threads);
+ }
+
+ async #iconImportThread(bookmarks) {
+ if (bookmarks.length && !this.#cancelled) {
+ let bookmark = bookmarks.shift();
+
+ if (bookmark.icon && bookmark.icon.startsWith("resource://scrapbook/")) {
+ bookmark.icon = bookmark.icon.replace("resource://scrapbook/", "");
+ bookmark.icon = helperApp.url(`/rdf/import/files/${bookmark.icon}`);
+ await this.#Bookmark.storeIcon(bookmark);
+ }
+
+ return this.#iconImportThread(bookmarks);
+ }
+ else {
+ this.#threadCount -= 1;
+ if (this.#threadCount === 0)
+ this.#sidebarSender.nodesReady({shelf: this.#shelf});
+ }
+ }
+
+ async #createShelf(path) {
+ const shelfNode = await this.#Folder.getOrCreateByPath(this.#options.name);
+
+ if (shelfNode) {
+ if (this.#options.quick) {
+ shelfNode.external = RDF_EXTERNAL_TYPE;
+ shelfNode.uri = path.substring(0, path.lastIndexOf("/"));
+ await Node.idb.update(shelfNode);
+ }
+ this.#nodeID_SB2SY.set(null, shelfNode.id);
+ }
+
+ return shelfNode;
+ }
+
+ async #createBookmark(parent, node, vars) {
+ const now = new Date();
+
+ let data = {
+ pos: vars.pos++,
+ uri: node.__sb_source,
+ name: node.__sb_title,
+ type: node.__sb_type === "folder"
+ ? NODE_TYPE_FOLDER
+ : (node.__sb_type === "separator"
+ ? NODE_TYPE_SEPARATOR
+ : NODE_TYPE_ARCHIVE),
+ parent_id: parent ? this.#nodeID_SB2SY.get(parent.__sb_id) : this.#shelf.id,
+ todo_state: node.__sb_type === "marked" ? 1 : undefined,
+ contains: ARCHIVE_TYPE_FILES,
+ content_type: "text/html",
+ has_comments: node.__sb_comment? true: undefined,
+ icon: node.__sb_icon,
+ date_added: now,
+ date_modified: now
+ };
+
+ if (this.#options.quick) {
+ data.external = RDF_EXTERNAL_TYPE;
+ data.external_id = node.__sb_id;
+ }
+
+ let bookmark = await this.#Bookmark.import(data);
+
+ if (node.__sb_comment) // commends json file on disk is also created by helper
+ await Comments.idb.import.add(bookmark, node.__sb_comment);
+
+ this.#nodeID_SB2SY.set(node.__sb_id, bookmark.id);
+
+ if (data.type === NODE_TYPE_FOLDER)
+ this.#nodeID_SB2SY.set(node.__sb_id, bookmark.id);
+ else if (data.type === NODE_TYPE_ARCHIVE) {
+ this.#nodeID_SY2SB.set(bookmark.id, node.__sb_id);
+ this.#bookmarks.push(bookmark);
+ }
+ }
+}
+
+export class RDFImporterBuilder extends StreamImporterBuilder {
+ setNumberOfThreads(threads) {
+ this._importOptions.threads = threads;
+ }
+
+ setQuickImport(quick) {
+ this._importOptions.quick = quick;
+ }
+
+ setCreateIndex(quick) {
+ this._importOptions.createIndex = quick;
+ }
+
+ _createImporter(options) {
+ return new RDFImporter(options);
+ }
+}
diff --git a/addon/import_versions.js b/addon/import_versions.js
new file mode 100644
index 00000000..5282e3a0
--- /dev/null
+++ b/addon/import_versions.js
@@ -0,0 +1,96 @@
+import {NODE_TYPE_ARCHIVE} from "./storage.js";
+
+export function parseJSONObject_v1(line) {
+ let object;
+ line = line.trim();
+ try {
+ if (line.endsWith(",")) // support for old JSON format files
+ object = JSON.parse(line.slice(0, line.length - 1));
+ else
+ object = JSON.parse(line);
+ } catch (e) {
+ console.error(e)
+ }
+
+ //object = transformFromV1ToV3(object);
+ return transformFromV1ToV3(object);
+}
+
+// This function is also used to import ORG v2 objects which are essentially
+// v1 objects with base64 binary blob representation
+export function transformFromV1ToV3(object, blobVersion = 1) {
+ let notes = object.notes;
+ delete object.notes;
+
+ let notes_html = object.notes_html;
+ delete object.notes_html;
+
+ let notes_format = object.notes_format;
+ delete object.notes_format;
+
+ let notes_align = object.notes_align;
+ delete object.notes_align;
+
+ let notes_width = object.notes_width;
+ delete object.notes_width;
+
+ let comments = object.comments;
+ delete object.comments;
+
+ let icon_data = object.icon_data;
+ delete object.icon_data;
+
+ const result = {
+ node: object
+ };
+
+ if (object.type === NODE_TYPE_ARCHIVE) {
+ let data = object.data;
+ let byte_length = object.byte_length;
+
+ delete object.data;
+ delete object.byte_length;
+
+ if (blobVersion === 1 && byte_length)
+ data = btoa(data);
+
+ result.archive = {object: data, type: object.mime_type, byte_length};
+ }
+
+ if (notes)
+ result.notes = {content: notes, html: notes_html, format: notes_format, align: notes_align, width: notes_width};
+
+ if (icon_data)
+ result.icon = {data_url: icon_data};
+
+ if (comments)
+ result.comments = {text: comments};
+
+ return result;
+}
+
+export function parseJSONObject_v2(line) {
+ let object = JSON.parse(line);
+ return transformFromV2ToV3(object);
+}
+
+export function transformFromV2ToV3(object) {
+ const result = {
+ node: object,
+ notes: object.notes,
+ archive: object.blob
+ };
+
+ if (object.icon_data)
+ result.icon = {data_url: object.icon_data};
+
+ if (object.comments)
+ result.comments = {text: object.comments};
+
+ delete object.blob;
+ delete object.notes;
+ delete object.comments;
+ delete object.icon_data;
+
+ return result;
+}
diff --git a/addon/lib/PKCE.js b/addon/lib/PKCE.js
new file mode 100644
index 00000000..66e7fe0a
--- /dev/null
+++ b/addon/lib/PKCE.js
@@ -0,0 +1,178 @@
+// derived from: https://github.com/bpedroza/js-pkce and https://github.com/aaronpk/pkce-vanilla-js
+
+/**
+ * Initialize the instance with configuration
+ * @param {IConfig} config
+ */
+export function PKCE(config) {
+ this.state = '';
+ this.codeVerifier = '';
+ this.config = config;
+}
+/**
+ * Generate the authorize url
+ * @param {object} additionalParams include additional parameters in the query
+ * @return Promise
+ */
+PKCE.prototype.getAuthorizationUrl = async function (additionalParams) {
+ if (additionalParams === void 0) { additionalParams = {}; }
+ var codeChallenge = await this.pkceChallengeFromVerifier();
+ var queryString = new URLSearchParams(Object.assign({
+ response_type: 'code',
+ response_mode: 'query',
+ client_id: this.config.client_id,
+ state: this.getState(additionalParams.state || null),
+ scope: this.config.requested_scopes,
+ redirect_uri: this.config.redirect_uri,
+ code_challenge: codeChallenge,
+ code_challenge_method: 'S256',
+ }, additionalParams)).toString();
+ return this.config.authorization_endpoint + "?" + queryString;
+};
+/**
+ * Given the return url, get a token from the oauth server
+ * @param url current urlwith params from server
+ * @param {object} additionalParams include additional parameters in the request body
+ * @return {Promise}
+ */
+PKCE.prototype.exchangeForAccessToken = function (url, additionalParams) {
+ var _this = this;
+ if (additionalParams === void 0) { additionalParams = {}; }
+ return this.parseAuthResponseUrl(url).then(function (q) {
+ return fetch(_this.config.token_endpoint, {
+ method: 'POST',
+ body: new URLSearchParams(Object.assign({
+ grant_type: 'authorization_code',
+ code: q.code,
+ client_id: _this.config.client_id,
+ redirect_uri: _this.config.redirect_uri,
+ code_verifier: _this.getCodeVerifier(),
+ }, additionalParams)),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ },
+ }).then(function (response) { return response.json(); });
+ });
+};
+
+PKCE.prototype.refreshAccessToken = function (refreshToken, additionalParams) {
+ var _this = this;
+ if (additionalParams === void 0) { additionalParams = {}; }
+ return fetch(_this.config.token_endpoint, {
+ method: 'POST',
+ body: new URLSearchParams(Object.assign({
+ grant_type: 'refresh_token',
+ refresh_token: refreshToken,
+ client_id: _this.config.client_id,
+ redirect_uri: _this.config.redirect_uri,
+ scope: this.config.requested_scopes
+ }, additionalParams)),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ },
+ }).then(function (response) { return response.json(); });
+};
+
+/**
+ * Get the current codeVerifier or generate a new one
+ * @return {string}
+ */
+PKCE.prototype.getCodeVerifier = function () {
+ if (this.codeVerifier === '') {
+ this.codeVerifier = this.randomStringFromStorage('pkce_code_verifier');
+ }
+ return this.codeVerifier;
+};
+/**
+ * Get the current state or generate a new one
+ * @return {string}
+ */
+PKCE.prototype.getState = function (explicit) {
+ if (explicit === void 0) { explicit = null; }
+ var stateKey = 'pkce_state';
+ if (explicit !== null) {
+ sessionStorage.setItem(stateKey, explicit);
+ }
+ if (this.state === '') {
+ this.state = this.randomStringFromStorage(stateKey);
+ }
+ return this.state;
+};
+/**
+ * Get the query params as json from a auth response url
+ * @param {string} url a url expected to have AuthResponse params
+ * @return {Promise}
+ */
+PKCE.prototype.parseAuthResponseUrl = function (url) {
+ var params = new URL(url).searchParams;
+ return this.validateAuthResponse({
+ error: params.get('error'),
+ query: params.get('query'),
+ state: params.get('state'),
+ code: params.get('code'),
+ });
+};
+/**
+ * Generate a code challenge
+ * @return {Promise}
+ */
+PKCE.prototype.pkceChallengeFromVerifier = async function () {
+ var hashed = await sha256(this.getCodeVerifier());
+ return base64urlencode(hashed);
+};
+/**
+ * Get a random string from storage or store a new one and return it's value
+ * @param {string} key
+ * @return {string}
+ */
+PKCE.prototype.randomStringFromStorage = function (key) {
+ var fromStorage = sessionStorage.getItem(key);
+ if (fromStorage === null) {
+ sessionStorage.setItem(key, generateRandomString());
+ }
+ return sessionStorage.getItem(key) || '';
+};
+/**
+ * Validates params from auth response
+ * @param {AuthResponse} queryParams
+ * @return {Promise}
+ */
+PKCE.prototype.validateAuthResponse = function (queryParams) {
+ var _this = this;
+ return new Promise(function (resolve, reject) {
+ if (queryParams.error) {
+ return reject({ error: queryParams.error });
+ }
+ if (queryParams.state !== _this.getState()) {
+ return reject({ error: 'Invalid State' });
+ }
+ return resolve(queryParams);
+ });
+};
+
+// Generate a secure random string using the browser crypto functions
+function generateRandomString() {
+ var array = new Uint32Array(28);
+ window.crypto.getRandomValues(array);
+ return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
+}
+
+// Calculate the SHA256 hash of the input text.
+// Returns a promise that resolves to an ArrayBuffer
+function sha256(plain) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(plain);
+ return window.crypto.subtle.digest('SHA-256', data);
+}
+
+// Base64-urlencodes the input string
+function base64urlencode(str) {
+ // Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts.
+ // btoa accepts chars only within ascii 0-255 and base64 encodes them.
+ // Then convert the base64 encoded to base64url encoded
+ // (replace + with -, replace / with _, trim trailing =)
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+}
diff --git a/addon/lib/browser-polyfill.js b/addon/lib/browser-polyfill.js
new file mode 100644
index 00000000..f78670de
--- /dev/null
+++ b/addon/lib/browser-polyfill.js
@@ -0,0 +1,2 @@
+if (!globalThis.browser)
+ globalThis.browser = chrome;
diff --git a/addon/lib/dexie.js b/addon/lib/dexie.js
new file mode 100644
index 00000000..cdcc7b7d
--- /dev/null
+++ b/addon/lib/dexie.js
@@ -0,0 +1,5179 @@
+/*
+ * Dexie.js - a minimalistic wrapper for IndexedDB
+ * ===============================================
+ *
+ * By David Fahlander, david.fahlander@gmail.com
+ *
+ * Version 3.2.2, Wed Apr 27 2022
+ *
+ * https://dexie.org
+ *
+ * Apache License Version 2.0, January 2004, http://www.apache.org/licenses/
+ */
+
+/*! *****************************************************************************
+Copyright (c) Microsoft Corporation.
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+***************************************************************************** */
+var __assign = function() {
+ __assign = Object.assign || function __assign(t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
+ }
+ return t;
+ };
+ return __assign.apply(this, arguments);
+};
+function __spreadArray(to, from, pack) {
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
+ if (ar || !(i in from)) {
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
+ ar[i] = from[i];
+ }
+ }
+ return to.concat(ar || Array.prototype.slice.call(from));
+}
+
+var _global = typeof globalThis !== 'undefined' ? globalThis :
+ typeof self !== 'undefined' ? self :
+ typeof window !== 'undefined' ? window :
+ global;
+
+var keys = Object.keys;
+var isArray = Array.isArray;
+if (typeof Promise !== 'undefined' && !_global.Promise) {
+ _global.Promise = Promise;
+}
+function extend(obj, extension) {
+ if (typeof extension !== 'object')
+ return obj;
+ keys(extension).forEach(function (key) {
+ obj[key] = extension[key];
+ });
+ return obj;
+}
+var getProto = Object.getPrototypeOf;
+var _hasOwn = {}.hasOwnProperty;
+function hasOwn(obj, prop) {
+ return _hasOwn.call(obj, prop);
+}
+function props(proto, extension) {
+ if (typeof extension === 'function')
+ extension = extension(getProto(proto));
+ (typeof Reflect === "undefined" ? keys : Reflect.ownKeys)(extension).forEach(function (key) {
+ setProp(proto, key, extension[key]);
+ });
+}
+var defineProperty = Object.defineProperty;
+function setProp(obj, prop, functionOrGetSet, options) {
+ defineProperty(obj, prop, extend(functionOrGetSet && hasOwn(functionOrGetSet, "get") && typeof functionOrGetSet.get === 'function' ?
+ { get: functionOrGetSet.get, set: functionOrGetSet.set, configurable: true } :
+ { value: functionOrGetSet, configurable: true, writable: true }, options));
+}
+function derive(Child) {
+ return {
+ from: function (Parent) {
+ Child.prototype = Object.create(Parent.prototype);
+ setProp(Child.prototype, "constructor", Child);
+ return {
+ extend: props.bind(null, Child.prototype)
+ };
+ }
+ };
+}
+var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
+function getPropertyDescriptor(obj, prop) {
+ var pd = getOwnPropertyDescriptor(obj, prop);
+ var proto;
+ return pd || (proto = getProto(obj)) && getPropertyDescriptor(proto, prop);
+}
+var _slice = [].slice;
+function slice(args, start, end) {
+ return _slice.call(args, start, end);
+}
+function override(origFunc, overridedFactory) {
+ return overridedFactory(origFunc);
+}
+function assert(b) {
+ if (!b)
+ throw new Error("Assertion Failed");
+}
+function asap$1(fn) {
+ if (_global.setImmediate)
+ setImmediate(fn);
+ else
+ setTimeout(fn, 0);
+}
+function arrayToObject(array, extractor) {
+ return array.reduce(function (result, item, i) {
+ var nameAndValue = extractor(item, i);
+ if (nameAndValue)
+ result[nameAndValue[0]] = nameAndValue[1];
+ return result;
+ }, {});
+}
+function tryCatch(fn, onerror, args) {
+ try {
+ fn.apply(null, args);
+ }
+ catch (ex) {
+ onerror && onerror(ex);
+ }
+}
+function getByKeyPath(obj, keyPath) {
+ if (hasOwn(obj, keyPath))
+ return obj[keyPath];
+ if (!keyPath)
+ return obj;
+ if (typeof keyPath !== 'string') {
+ var rv = [];
+ for (var i = 0, l = keyPath.length; i < l; ++i) {
+ var val = getByKeyPath(obj, keyPath[i]);
+ rv.push(val);
+ }
+ return rv;
+ }
+ var period = keyPath.indexOf('.');
+ if (period !== -1) {
+ var innerObj = obj[keyPath.substr(0, period)];
+ return innerObj === undefined ? undefined : getByKeyPath(innerObj, keyPath.substr(period + 1));
+ }
+ return undefined;
+}
+function setByKeyPath(obj, keyPath, value) {
+ if (!obj || keyPath === undefined)
+ return;
+ if ('isFrozen' in Object && Object.isFrozen(obj))
+ return;
+ if (typeof keyPath !== 'string' && 'length' in keyPath) {
+ assert(typeof value !== 'string' && 'length' in value);
+ for (var i = 0, l = keyPath.length; i < l; ++i) {
+ setByKeyPath(obj, keyPath[i], value[i]);
+ }
+ }
+ else {
+ var period = keyPath.indexOf('.');
+ if (period !== -1) {
+ var currentKeyPath = keyPath.substr(0, period);
+ var remainingKeyPath = keyPath.substr(period + 1);
+ if (remainingKeyPath === "")
+ if (value === undefined) {
+ if (isArray(obj) && !isNaN(parseInt(currentKeyPath)))
+ obj.splice(currentKeyPath, 1);
+ else
+ delete obj[currentKeyPath];
+ }
+ else
+ obj[currentKeyPath] = value;
+ else {
+ var innerObj = obj[currentKeyPath];
+ if (!innerObj || !hasOwn(obj, currentKeyPath))
+ innerObj = (obj[currentKeyPath] = {});
+ setByKeyPath(innerObj, remainingKeyPath, value);
+ }
+ }
+ else {
+ if (value === undefined) {
+ if (isArray(obj) && !isNaN(parseInt(keyPath)))
+ obj.splice(keyPath, 1);
+ else
+ delete obj[keyPath];
+ }
+ else
+ obj[keyPath] = value;
+ }
+ }
+}
+function delByKeyPath(obj, keyPath) {
+ if (typeof keyPath === 'string')
+ setByKeyPath(obj, keyPath, undefined);
+ else if ('length' in keyPath)
+ [].map.call(keyPath, function (kp) {
+ setByKeyPath(obj, kp, undefined);
+ });
+}
+function shallowClone(obj) {
+ var rv = {};
+ for (var m in obj) {
+ if (hasOwn(obj, m))
+ rv[m] = obj[m];
+ }
+ return rv;
+}
+var concat = [].concat;
+function flatten(a) {
+ return concat.apply([], a);
+}
+var intrinsicTypeNames = "Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey"
+ .split(',').concat(flatten([8, 16, 32, 64].map(function (num) { return ["Int", "Uint", "Float"].map(function (t) { return t + num + "Array"; }); }))).filter(function (t) { return _global[t]; });
+var intrinsicTypes = intrinsicTypeNames.map(function (t) { return _global[t]; });
+arrayToObject(intrinsicTypeNames, function (x) { return [x, true]; });
+var circularRefs = null;
+function deepClone(any) {
+ circularRefs = typeof WeakMap !== 'undefined' && new WeakMap();
+ var rv = innerDeepClone(any);
+ circularRefs = null;
+ return rv;
+}
+function innerDeepClone(any) {
+ if (!any || typeof any !== 'object')
+ return any;
+ var rv = circularRefs && circularRefs.get(any);
+ if (rv)
+ return rv;
+ if (isArray(any)) {
+ rv = [];
+ circularRefs && circularRefs.set(any, rv);
+ for (var i = 0, l = any.length; i < l; ++i) {
+ rv.push(innerDeepClone(any[i]));
+ }
+ }
+ else if (intrinsicTypes.indexOf(any.constructor) >= 0) {
+ rv = any;
+ }
+ else {
+ var proto = getProto(any);
+ rv = proto === Object.prototype ? {} : Object.create(proto);
+ circularRefs && circularRefs.set(any, rv);
+ for (var prop in any) {
+ if (hasOwn(any, prop)) {
+ rv[prop] = innerDeepClone(any[prop]);
+ }
+ }
+ }
+ return rv;
+}
+var toString = {}.toString;
+function toStringTag(o) {
+ return toString.call(o).slice(8, -1);
+}
+var iteratorSymbol = typeof Symbol !== 'undefined' ?
+ Symbol.iterator :
+ '@@iterator';
+var getIteratorOf = typeof iteratorSymbol === "symbol" ? function (x) {
+ var i;
+ return x != null && (i = x[iteratorSymbol]) && i.apply(x);
+} : function () { return null; };
+var NO_CHAR_ARRAY = {};
+function getArrayOf(arrayLike) {
+ var i, a, x, it;
+ if (arguments.length === 1) {
+ if (isArray(arrayLike))
+ return arrayLike.slice();
+ if (this === NO_CHAR_ARRAY && typeof arrayLike === 'string')
+ return [arrayLike];
+ if ((it = getIteratorOf(arrayLike))) {
+ a = [];
+ while ((x = it.next()), !x.done)
+ a.push(x.value);
+ return a;
+ }
+ if (arrayLike == null)
+ return [arrayLike];
+ i = arrayLike.length;
+ if (typeof i === 'number') {
+ a = new Array(i);
+ while (i--)
+ a[i] = arrayLike[i];
+ return a;
+ }
+ return [arrayLike];
+ }
+ i = arguments.length;
+ a = new Array(i);
+ while (i--)
+ a[i] = arguments[i];
+ return a;
+}
+var isAsyncFunction = typeof Symbol !== 'undefined'
+ ? function (fn) { return fn[Symbol.toStringTag] === 'AsyncFunction'; }
+ : function () { return false; };
+
+var debug = typeof location !== 'undefined' &&
+ /^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href);
+function setDebug(value, filter) {
+ debug = value;
+ libraryFilter = filter;
+}
+var libraryFilter = function () { return true; };
+var NEEDS_THROW_FOR_STACK = !new Error("").stack;
+function getErrorWithStack() {
+ if (NEEDS_THROW_FOR_STACK)
+ try {
+ //getErrorWithStack.arguments;
+ throw new Error();
+ }
+ catch (e) {
+ return e;
+ }
+ return new Error();
+}
+function prettyStack(exception, numIgnoredFrames) {
+ var stack = exception.stack;
+ if (!stack)
+ return "";
+ numIgnoredFrames = (numIgnoredFrames || 0);
+ if (stack.indexOf(exception.name) === 0)
+ numIgnoredFrames += (exception.name + exception.message).split('\n').length;
+ return stack.split('\n')
+ .slice(numIgnoredFrames)
+ .filter(libraryFilter)
+ .map(function (frame) { return "\n" + frame; })
+ .join('');
+}
+
+var dexieErrorNames = [
+ 'Modify',
+ 'Bulk',
+ 'OpenFailed',
+ 'VersionChange',
+ 'Schema',
+ 'Upgrade',
+ 'InvalidTable',
+ 'MissingAPI',
+ 'NoSuchDatabase',
+ 'InvalidArgument',
+ 'SubTransaction',
+ 'Unsupported',
+ 'Internal',
+ 'DatabaseClosed',
+ 'PrematureCommit',
+ 'ForeignAwait'
+];
+var idbDomErrorNames = [
+ 'Unknown',
+ 'Constraint',
+ 'Data',
+ 'TransactionInactive',
+ 'ReadOnly',
+ 'Version',
+ 'NotFound',
+ 'InvalidState',
+ 'InvalidAccess',
+ 'Abort',
+ 'Timeout',
+ 'QuotaExceeded',
+ 'Syntax',
+ 'DataClone'
+];
+var errorList = dexieErrorNames.concat(idbDomErrorNames);
+var defaultTexts = {
+ VersionChanged: "Database version changed by other database connection",
+ DatabaseClosed: "Database has been closed",
+ Abort: "Transaction aborted",
+ TransactionInactive: "Transaction has already completed or failed",
+ MissingAPI: "IndexedDB API missing. Please visit https://tinyurl.com/y2uuvskb"
+};
+function DexieError(name, msg) {
+ this._e = getErrorWithStack();
+ this.name = name;
+ this.message = msg;
+}
+derive(DexieError).from(Error).extend({
+ stack: {
+ get: function () {
+ return this._stack ||
+ (this._stack = this.name + ": " + this.message + prettyStack(this._e, 2));
+ }
+ },
+ toString: function () { return this.name + ": " + this.message; }
+});
+function getMultiErrorMessage(msg, failures) {
+ return msg + ". Errors: " + Object.keys(failures)
+ .map(function (key) { return failures[key].toString(); })
+ .filter(function (v, i, s) { return s.indexOf(v) === i; })
+ .join('\n');
+}
+function ModifyError(msg, failures, successCount, failedKeys) {
+ this._e = getErrorWithStack();
+ this.failures = failures;
+ this.failedKeys = failedKeys;
+ this.successCount = successCount;
+ this.message = getMultiErrorMessage(msg, failures);
+}
+derive(ModifyError).from(DexieError);
+function BulkError(msg, failures) {
+ this._e = getErrorWithStack();
+ this.name = "BulkError";
+ this.failures = Object.keys(failures).map(function (pos) { return failures[pos]; });
+ this.failuresByPos = failures;
+ this.message = getMultiErrorMessage(msg, failures);
+}
+derive(BulkError).from(DexieError);
+var errnames = errorList.reduce(function (obj, name) { return (obj[name] = name + "Error", obj); }, {});
+var BaseException = DexieError;
+var exceptions = errorList.reduce(function (obj, name) {
+ var fullName = name + "Error";
+ function DexieError(msgOrInner, inner) {
+ this._e = getErrorWithStack();
+ this.name = fullName;
+ if (!msgOrInner) {
+ this.message = defaultTexts[name] || fullName;
+ this.inner = null;
+ }
+ else if (typeof msgOrInner === 'string') {
+ this.message = "" + msgOrInner + (!inner ? '' : '\n ' + inner);
+ this.inner = inner || null;
+ }
+ else if (typeof msgOrInner === 'object') {
+ this.message = msgOrInner.name + " " + msgOrInner.message;
+ this.inner = msgOrInner;
+ }
+ }
+ derive(DexieError).from(BaseException);
+ obj[name] = DexieError;
+ return obj;
+}, {});
+exceptions.Syntax = SyntaxError;
+exceptions.Type = TypeError;
+exceptions.Range = RangeError;
+var exceptionMap = idbDomErrorNames.reduce(function (obj, name) {
+ obj[name + "Error"] = exceptions[name];
+ return obj;
+}, {});
+function mapError(domError, message) {
+ if (!domError || domError instanceof DexieError || domError instanceof TypeError || domError instanceof SyntaxError || !domError.name || !exceptionMap[domError.name])
+ return domError;
+ var rv = new exceptionMap[domError.name](message || domError.message, domError);
+ if ("stack" in domError) {
+ setProp(rv, "stack", { get: function () {
+ return this.inner.stack;
+ } });
+ }
+ return rv;
+}
+var fullNameExceptions = errorList.reduce(function (obj, name) {
+ if (["Syntax", "Type", "Range"].indexOf(name) === -1)
+ obj[name + "Error"] = exceptions[name];
+ return obj;
+}, {});
+fullNameExceptions.ModifyError = ModifyError;
+fullNameExceptions.DexieError = DexieError;
+fullNameExceptions.BulkError = BulkError;
+
+function nop() { }
+function mirror(val) { return val; }
+function pureFunctionChain(f1, f2) {
+ if (f1 == null || f1 === mirror)
+ return f2;
+ return function (val) {
+ return f2(f1(val));
+ };
+}
+function callBoth(on1, on2) {
+ return function () {
+ on1.apply(this, arguments);
+ on2.apply(this, arguments);
+ };
+}
+function hookCreatingChain(f1, f2) {
+ if (f1 === nop)
+ return f2;
+ return function () {
+ var res = f1.apply(this, arguments);
+ if (res !== undefined)
+ arguments[0] = res;
+ var onsuccess = this.onsuccess,
+ onerror = this.onerror;
+ this.onsuccess = null;
+ this.onerror = null;
+ var res2 = f2.apply(this, arguments);
+ if (onsuccess)
+ this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess;
+ if (onerror)
+ this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror;
+ return res2 !== undefined ? res2 : res;
+ };
+}
+function hookDeletingChain(f1, f2) {
+ if (f1 === nop)
+ return f2;
+ return function () {
+ f1.apply(this, arguments);
+ var onsuccess = this.onsuccess,
+ onerror = this.onerror;
+ this.onsuccess = this.onerror = null;
+ f2.apply(this, arguments);
+ if (onsuccess)
+ this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess;
+ if (onerror)
+ this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror;
+ };
+}
+function hookUpdatingChain(f1, f2) {
+ if (f1 === nop)
+ return f2;
+ return function (modifications) {
+ var res = f1.apply(this, arguments);
+ extend(modifications, res);
+ var onsuccess = this.onsuccess,
+ onerror = this.onerror;
+ this.onsuccess = null;
+ this.onerror = null;
+ var res2 = f2.apply(this, arguments);
+ if (onsuccess)
+ this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess;
+ if (onerror)
+ this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror;
+ return res === undefined ?
+ (res2 === undefined ? undefined : res2) :
+ (extend(res, res2));
+ };
+}
+function reverseStoppableEventChain(f1, f2) {
+ if (f1 === nop)
+ return f2;
+ return function () {
+ if (f2.apply(this, arguments) === false)
+ return false;
+ return f1.apply(this, arguments);
+ };
+}
+function promisableChain(f1, f2) {
+ if (f1 === nop)
+ return f2;
+ return function () {
+ var res = f1.apply(this, arguments);
+ if (res && typeof res.then === 'function') {
+ var thiz = this, i = arguments.length, args = new Array(i);
+ while (i--)
+ args[i] = arguments[i];
+ return res.then(function () {
+ return f2.apply(thiz, args);
+ });
+ }
+ return f2.apply(this, arguments);
+ };
+}
+
+var INTERNAL = {};
+var LONG_STACKS_CLIP_LIMIT = 100,
+MAX_LONG_STACKS = 20, ZONE_ECHO_LIMIT = 100, _a$1 = typeof Promise === 'undefined' ?
+ [] :
+ (function () {
+ var globalP = Promise.resolve();
+ if (typeof crypto === 'undefined' || !crypto.subtle)
+ return [globalP, getProto(globalP), globalP];
+ var nativeP = crypto.subtle.digest("SHA-512", new Uint8Array([0]));
+ return [
+ nativeP,
+ getProto(nativeP),
+ globalP
+ ];
+ })(), resolvedNativePromise = _a$1[0], nativePromiseProto = _a$1[1], resolvedGlobalPromise = _a$1[2], nativePromiseThen = nativePromiseProto && nativePromiseProto.then;
+var NativePromise = resolvedNativePromise && resolvedNativePromise.constructor;
+var patchGlobalPromise = !!resolvedGlobalPromise;
+var stack_being_generated = false;
+var schedulePhysicalTick = resolvedGlobalPromise ?
+ function () { resolvedGlobalPromise.then(physicalTick); }
+ :
+ _global.setImmediate ?
+ setImmediate.bind(null, physicalTick) :
+ _global.MutationObserver ?
+ function () {
+ var hiddenDiv = document.createElement("div");
+ (new MutationObserver(function () {
+ physicalTick();
+ hiddenDiv = null;
+ })).observe(hiddenDiv, { attributes: true });
+ hiddenDiv.setAttribute('i', '1');
+ } :
+ function () { setTimeout(physicalTick, 0); };
+var asap = function (callback, args) {
+ microtickQueue.push([callback, args]);
+ if (needsNewPhysicalTick) {
+ schedulePhysicalTick();
+ needsNewPhysicalTick = false;
+ }
+};
+var isOutsideMicroTick = true,
+needsNewPhysicalTick = true,
+unhandledErrors = [],
+rejectingErrors = [],
+currentFulfiller = null, rejectionMapper = mirror;
+var globalPSD = {
+ id: 'global',
+ global: true,
+ ref: 0,
+ unhandleds: [],
+ onunhandled: globalError,
+ pgp: false,
+ env: {},
+ finalize: function () {
+ this.unhandleds.forEach(function (uh) {
+ try {
+ globalError(uh[0], uh[1]);
+ }
+ catch (e) { }
+ });
+ }
+};
+var PSD = globalPSD;
+var microtickQueue = [];
+var numScheduledCalls = 0;
+var tickFinalizers = [];
+function DexiePromise(fn) {
+ if (typeof this !== 'object')
+ throw new TypeError('Promises must be constructed via new');
+ this._listeners = [];
+ this.onuncatched = nop;
+ this._lib = false;
+ var psd = (this._PSD = PSD);
+ if (debug) {
+ this._stackHolder = getErrorWithStack();
+ this._prev = null;
+ this._numPrev = 0;
+ }
+ if (typeof fn !== 'function') {
+ if (fn !== INTERNAL)
+ throw new TypeError('Not a function');
+ this._state = arguments[1];
+ this._value = arguments[2];
+ if (this._state === false)
+ handleRejection(this, this._value);
+ return;
+ }
+ this._state = null;
+ this._value = null;
+ ++psd.ref;
+ executePromiseTask(this, fn);
+}
+var thenProp = {
+ get: function () {
+ var psd = PSD, microTaskId = totalEchoes;
+ function then(onFulfilled, onRejected) {
+ var _this = this;
+ var possibleAwait = !psd.global && (psd !== PSD || microTaskId !== totalEchoes);
+ var cleanup = possibleAwait && !decrementExpectedAwaits();
+ var rv = new DexiePromise(function (resolve, reject) {
+ propagateToListener(_this, new Listener(nativeAwaitCompatibleWrap(onFulfilled, psd, possibleAwait, cleanup), nativeAwaitCompatibleWrap(onRejected, psd, possibleAwait, cleanup), resolve, reject, psd));
+ });
+ debug && linkToPreviousPromise(rv, this);
+ return rv;
+ }
+ then.prototype = INTERNAL;
+ return then;
+ },
+ set: function (value) {
+ setProp(this, 'then', value && value.prototype === INTERNAL ?
+ thenProp :
+ {
+ get: function () {
+ return value;
+ },
+ set: thenProp.set
+ });
+ }
+};
+props(DexiePromise.prototype, {
+ then: thenProp,
+ _then: function (onFulfilled, onRejected) {
+ propagateToListener(this, new Listener(null, null, onFulfilled, onRejected, PSD));
+ },
+ catch: function (onRejected) {
+ if (arguments.length === 1)
+ return this.then(null, onRejected);
+ var type = arguments[0], handler = arguments[1];
+ return typeof type === 'function' ? this.then(null, function (err) {
+ return err instanceof type ? handler(err) : PromiseReject(err);
+ })
+ : this.then(null, function (err) {
+ return err && err.name === type ? handler(err) : PromiseReject(err);
+ });
+ },
+ finally: function (onFinally) {
+ return this.then(function (value) {
+ onFinally();
+ return value;
+ }, function (err) {
+ onFinally();
+ return PromiseReject(err);
+ });
+ },
+ stack: {
+ get: function () {
+ if (this._stack)
+ return this._stack;
+ try {
+ stack_being_generated = true;
+ var stacks = getStack(this, [], MAX_LONG_STACKS);
+ var stack = stacks.join("\nFrom previous: ");
+ if (this._state !== null)
+ this._stack = stack;
+ return stack;
+ }
+ finally {
+ stack_being_generated = false;
+ }
+ }
+ },
+ timeout: function (ms, msg) {
+ var _this = this;
+ return ms < Infinity ?
+ new DexiePromise(function (resolve, reject) {
+ var handle = setTimeout(function () { return reject(new exceptions.Timeout(msg)); }, ms);
+ _this.then(resolve, reject).finally(clearTimeout.bind(null, handle));
+ }) : this;
+ }
+});
+if (typeof Symbol !== 'undefined' && Symbol.toStringTag)
+ setProp(DexiePromise.prototype, Symbol.toStringTag, 'Dexie.Promise');
+globalPSD.env = snapShot();
+function Listener(onFulfilled, onRejected, resolve, reject, zone) {
+ this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
+ this.onRejected = typeof onRejected === 'function' ? onRejected : null;
+ this.resolve = resolve;
+ this.reject = reject;
+ this.psd = zone;
+}
+props(DexiePromise, {
+ all: function () {
+ var values = getArrayOf.apply(null, arguments)
+ .map(onPossibleParallellAsync);
+ return new DexiePromise(function (resolve, reject) {
+ if (values.length === 0)
+ resolve([]);
+ var remaining = values.length;
+ values.forEach(function (a, i) { return DexiePromise.resolve(a).then(function (x) {
+ values[i] = x;
+ if (!--remaining)
+ resolve(values);
+ }, reject); });
+ });
+ },
+ resolve: function (value) {
+ if (value instanceof DexiePromise)
+ return value;
+ if (value && typeof value.then === 'function')
+ return new DexiePromise(function (resolve, reject) {
+ value.then(resolve, reject);
+ });
+ var rv = new DexiePromise(INTERNAL, true, value);
+ linkToPreviousPromise(rv, currentFulfiller);
+ return rv;
+ },
+ reject: PromiseReject,
+ race: function () {
+ var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync);
+ return new DexiePromise(function (resolve, reject) {
+ values.map(function (value) { return DexiePromise.resolve(value).then(resolve, reject); });
+ });
+ },
+ PSD: {
+ get: function () { return PSD; },
+ set: function (value) { return PSD = value; }
+ },
+ totalEchoes: { get: function () { return totalEchoes; } },
+ newPSD: newScope,
+ usePSD: usePSD,
+ scheduler: {
+ get: function () { return asap; },
+ set: function (value) { asap = value; }
+ },
+ rejectionMapper: {
+ get: function () { return rejectionMapper; },
+ set: function (value) { rejectionMapper = value; }
+ },
+ follow: function (fn, zoneProps) {
+ return new DexiePromise(function (resolve, reject) {
+ return newScope(function (resolve, reject) {
+ var psd = PSD;
+ psd.unhandleds = [];
+ psd.onunhandled = reject;
+ psd.finalize = callBoth(function () {
+ var _this = this;
+ run_at_end_of_this_or_next_physical_tick(function () {
+ _this.unhandleds.length === 0 ? resolve() : reject(_this.unhandleds[0]);
+ });
+ }, psd.finalize);
+ fn();
+ }, zoneProps, resolve, reject);
+ });
+ }
+});
+if (NativePromise) {
+ if (NativePromise.allSettled)
+ setProp(DexiePromise, "allSettled", function () {
+ var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync);
+ return new DexiePromise(function (resolve) {
+ if (possiblePromises.length === 0)
+ resolve([]);
+ var remaining = possiblePromises.length;
+ var results = new Array(remaining);
+ possiblePromises.forEach(function (p, i) { return DexiePromise.resolve(p).then(function (value) { return results[i] = { status: "fulfilled", value: value }; }, function (reason) { return results[i] = { status: "rejected", reason: reason }; })
+ .then(function () { return --remaining || resolve(results); }); });
+ });
+ });
+ if (NativePromise.any && typeof AggregateError !== 'undefined')
+ setProp(DexiePromise, "any", function () {
+ var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync);
+ return new DexiePromise(function (resolve, reject) {
+ if (possiblePromises.length === 0)
+ reject(new AggregateError([]));
+ var remaining = possiblePromises.length;
+ var failures = new Array(remaining);
+ possiblePromises.forEach(function (p, i) { return DexiePromise.resolve(p).then(function (value) { return resolve(value); }, function (failure) {
+ failures[i] = failure;
+ if (!--remaining)
+ reject(new AggregateError(failures));
+ }); });
+ });
+ });
+}
+function executePromiseTask(promise, fn) {
+ try {
+ fn(function (value) {
+ if (promise._state !== null)
+ return;
+ if (value === promise)
+ throw new TypeError('A promise cannot be resolved with itself.');
+ var shouldExecuteTick = promise._lib && beginMicroTickScope();
+ if (value && typeof value.then === 'function') {
+ executePromiseTask(promise, function (resolve, reject) {
+ value instanceof DexiePromise ?
+ value._then(resolve, reject) :
+ value.then(resolve, reject);
+ });
+ }
+ else {
+ promise._state = true;
+ promise._value = value;
+ propagateAllListeners(promise);
+ }
+ if (shouldExecuteTick)
+ endMicroTickScope();
+ }, handleRejection.bind(null, promise));
+ }
+ catch (ex) {
+ handleRejection(promise, ex);
+ }
+}
+function handleRejection(promise, reason) {
+ rejectingErrors.push(reason);
+ if (promise._state !== null)
+ return;
+ var shouldExecuteTick = promise._lib && beginMicroTickScope();
+ reason = rejectionMapper(reason);
+ promise._state = false;
+ promise._value = reason;
+ debug && reason !== null && typeof reason === 'object' && !reason._promise && tryCatch(function () {
+ var origProp = getPropertyDescriptor(reason, "stack");
+ reason._promise = promise;
+ setProp(reason, "stack", {
+ get: function () {
+ return stack_being_generated ?
+ origProp && (origProp.get ?
+ origProp.get.apply(reason) :
+ origProp.value) :
+ promise.stack;
+ }
+ });
+ });
+ addPossiblyUnhandledError(promise);
+ propagateAllListeners(promise);
+ if (shouldExecuteTick)
+ endMicroTickScope();
+}
+function propagateAllListeners(promise) {
+ var listeners = promise._listeners;
+ promise._listeners = [];
+ for (var i = 0, len = listeners.length; i < len; ++i) {
+ propagateToListener(promise, listeners[i]);
+ }
+ var psd = promise._PSD;
+ --psd.ref || psd.finalize();
+ if (numScheduledCalls === 0) {
+ ++numScheduledCalls;
+ asap(function () {
+ if (--numScheduledCalls === 0)
+ finalizePhysicalTick();
+ }, []);
+ }
+}
+function propagateToListener(promise, listener) {
+ if (promise._state === null) {
+ promise._listeners.push(listener);
+ return;
+ }
+ var cb = promise._state ? listener.onFulfilled : listener.onRejected;
+ if (cb === null) {
+ return (promise._state ? listener.resolve : listener.reject)(promise._value);
+ }
+ ++listener.psd.ref;
+ ++numScheduledCalls;
+ asap(callListener, [cb, promise, listener]);
+}
+function callListener(cb, promise, listener) {
+ try {
+ currentFulfiller = promise;
+ var ret, value = promise._value;
+ if (promise._state) {
+ ret = cb(value);
+ }
+ else {
+ if (rejectingErrors.length)
+ rejectingErrors = [];
+ ret = cb(value);
+ if (rejectingErrors.indexOf(value) === -1)
+ markErrorAsHandled(promise);
+ }
+ listener.resolve(ret);
+ }
+ catch (e) {
+ listener.reject(e);
+ }
+ finally {
+ currentFulfiller = null;
+ if (--numScheduledCalls === 0)
+ finalizePhysicalTick();
+ --listener.psd.ref || listener.psd.finalize();
+ }
+}
+function getStack(promise, stacks, limit) {
+ if (stacks.length === limit)
+ return stacks;
+ var stack = "";
+ if (promise._state === false) {
+ var failure = promise._value, errorName, message;
+ if (failure != null) {
+ errorName = failure.name || "Error";
+ message = failure.message || failure;
+ stack = prettyStack(failure, 0);
+ }
+ else {
+ errorName = failure;
+ message = "";
+ }
+ stacks.push(errorName + (message ? ": " + message : "") + stack);
+ }
+ if (debug) {
+ stack = prettyStack(promise._stackHolder, 2);
+ if (stack && stacks.indexOf(stack) === -1)
+ stacks.push(stack);
+ if (promise._prev)
+ getStack(promise._prev, stacks, limit);
+ }
+ return stacks;
+}
+function linkToPreviousPromise(promise, prev) {
+ var numPrev = prev ? prev._numPrev + 1 : 0;
+ if (numPrev < LONG_STACKS_CLIP_LIMIT) {
+ promise._prev = prev;
+ promise._numPrev = numPrev;
+ }
+}
+function physicalTick() {
+ beginMicroTickScope() && endMicroTickScope();
+}
+function beginMicroTickScope() {
+ var wasRootExec = isOutsideMicroTick;
+ isOutsideMicroTick = false;
+ needsNewPhysicalTick = false;
+ return wasRootExec;
+}
+function endMicroTickScope() {
+ var callbacks, i, l;
+ do {
+ while (microtickQueue.length > 0) {
+ callbacks = microtickQueue;
+ microtickQueue = [];
+ l = callbacks.length;
+ for (i = 0; i < l; ++i) {
+ var item = callbacks[i];
+ item[0].apply(null, item[1]);
+ }
+ }
+ } while (microtickQueue.length > 0);
+ isOutsideMicroTick = true;
+ needsNewPhysicalTick = true;
+}
+function finalizePhysicalTick() {
+ var unhandledErrs = unhandledErrors;
+ unhandledErrors = [];
+ unhandledErrs.forEach(function (p) {
+ p._PSD.onunhandled.call(null, p._value, p);
+ });
+ var finalizers = tickFinalizers.slice(0);
+ var i = finalizers.length;
+ while (i)
+ finalizers[--i]();
+}
+function run_at_end_of_this_or_next_physical_tick(fn) {
+ function finalizer() {
+ fn();
+ tickFinalizers.splice(tickFinalizers.indexOf(finalizer), 1);
+ }
+ tickFinalizers.push(finalizer);
+ ++numScheduledCalls;
+ asap(function () {
+ if (--numScheduledCalls === 0)
+ finalizePhysicalTick();
+ }, []);
+}
+function addPossiblyUnhandledError(promise) {
+ if (!unhandledErrors.some(function (p) { return p._value === promise._value; }))
+ unhandledErrors.push(promise);
+}
+function markErrorAsHandled(promise) {
+ var i = unhandledErrors.length;
+ while (i)
+ if (unhandledErrors[--i]._value === promise._value) {
+ unhandledErrors.splice(i, 1);
+ return;
+ }
+}
+function PromiseReject(reason) {
+ return new DexiePromise(INTERNAL, false, reason);
+}
+function wrap(fn, errorCatcher) {
+ var psd = PSD;
+ return function () {
+ var wasRootExec = beginMicroTickScope(), outerScope = PSD;
+ try {
+ switchToZone(psd, true);
+ return fn.apply(this, arguments);
+ }
+ catch (e) {
+ errorCatcher && errorCatcher(e);
+ }
+ finally {
+ switchToZone(outerScope, false);
+ if (wasRootExec)
+ endMicroTickScope();
+ }
+ };
+}
+var task = { awaits: 0, echoes: 0, id: 0 };
+var taskCounter = 0;
+var zoneStack = [];
+var zoneEchoes = 0;
+var totalEchoes = 0;
+var zone_id_counter = 0;
+function newScope(fn, props, a1, a2) {
+ var parent = PSD, psd = Object.create(parent);
+ psd.parent = parent;
+ psd.ref = 0;
+ psd.global = false;
+ psd.id = ++zone_id_counter;
+ var globalEnv = globalPSD.env;
+ psd.env = patchGlobalPromise ? {
+ Promise: DexiePromise,
+ PromiseProp: { value: DexiePromise, configurable: true, writable: true },
+ all: DexiePromise.all,
+ race: DexiePromise.race,
+ allSettled: DexiePromise.allSettled,
+ any: DexiePromise.any,
+ resolve: DexiePromise.resolve,
+ reject: DexiePromise.reject,
+ nthen: getPatchedPromiseThen(globalEnv.nthen, psd),
+ gthen: getPatchedPromiseThen(globalEnv.gthen, psd)
+ } : {};
+ if (props)
+ extend(psd, props);
+ ++parent.ref;
+ psd.finalize = function () {
+ --this.parent.ref || this.parent.finalize();
+ };
+ var rv = usePSD(psd, fn, a1, a2);
+ if (psd.ref === 0)
+ psd.finalize();
+ return rv;
+}
+function incrementExpectedAwaits() {
+ if (!task.id)
+ task.id = ++taskCounter;
+ ++task.awaits;
+ task.echoes += ZONE_ECHO_LIMIT;
+ return task.id;
+}
+function decrementExpectedAwaits() {
+ if (!task.awaits)
+ return false;
+ if (--task.awaits === 0)
+ task.id = 0;
+ task.echoes = task.awaits * ZONE_ECHO_LIMIT;
+ return true;
+}
+if (('' + nativePromiseThen).indexOf('[native code]') === -1) {
+ incrementExpectedAwaits = decrementExpectedAwaits = nop;
+}
+function onPossibleParallellAsync(possiblePromise) {
+ if (task.echoes && possiblePromise && possiblePromise.constructor === NativePromise) {
+ incrementExpectedAwaits();
+ return possiblePromise.then(function (x) {
+ decrementExpectedAwaits();
+ return x;
+ }, function (e) {
+ decrementExpectedAwaits();
+ return rejection(e);
+ });
+ }
+ return possiblePromise;
+}
+function zoneEnterEcho(targetZone) {
+ ++totalEchoes;
+ if (!task.echoes || --task.echoes === 0) {
+ task.echoes = task.id = 0;
+ }
+ zoneStack.push(PSD);
+ switchToZone(targetZone, true);
+}
+function zoneLeaveEcho() {
+ var zone = zoneStack[zoneStack.length - 1];
+ zoneStack.pop();
+ switchToZone(zone, false);
+}
+function switchToZone(targetZone, bEnteringZone) {
+ var currentZone = PSD;
+ if (bEnteringZone ? task.echoes && (!zoneEchoes++ || targetZone !== PSD) : zoneEchoes && (!--zoneEchoes || targetZone !== PSD)) {
+ enqueueNativeMicroTask(bEnteringZone ? zoneEnterEcho.bind(null, targetZone) : zoneLeaveEcho);
+ }
+ if (targetZone === PSD)
+ return;
+ PSD = targetZone;
+ if (currentZone === globalPSD)
+ globalPSD.env = snapShot();
+ if (patchGlobalPromise) {
+ var GlobalPromise_1 = globalPSD.env.Promise;
+ var targetEnv = targetZone.env;
+ nativePromiseProto.then = targetEnv.nthen;
+ GlobalPromise_1.prototype.then = targetEnv.gthen;
+ if (currentZone.global || targetZone.global) {
+ Object.defineProperty(_global, 'Promise', targetEnv.PromiseProp);
+ GlobalPromise_1.all = targetEnv.all;
+ GlobalPromise_1.race = targetEnv.race;
+ GlobalPromise_1.resolve = targetEnv.resolve;
+ GlobalPromise_1.reject = targetEnv.reject;
+ if (targetEnv.allSettled)
+ GlobalPromise_1.allSettled = targetEnv.allSettled;
+ if (targetEnv.any)
+ GlobalPromise_1.any = targetEnv.any;
+ }
+ }
+}
+function snapShot() {
+ var GlobalPromise = _global.Promise;
+ return patchGlobalPromise ? {
+ Promise: GlobalPromise,
+ PromiseProp: Object.getOwnPropertyDescriptor(_global, "Promise"),
+ all: GlobalPromise.all,
+ race: GlobalPromise.race,
+ allSettled: GlobalPromise.allSettled,
+ any: GlobalPromise.any,
+ resolve: GlobalPromise.resolve,
+ reject: GlobalPromise.reject,
+ nthen: nativePromiseProto.then,
+ gthen: GlobalPromise.prototype.then
+ } : {};
+}
+function usePSD(psd, fn, a1, a2, a3) {
+ var outerScope = PSD;
+ try {
+ switchToZone(psd, true);
+ return fn(a1, a2, a3);
+ }
+ finally {
+ switchToZone(outerScope, false);
+ }
+}
+function enqueueNativeMicroTask(job) {
+ nativePromiseThen.call(resolvedNativePromise, job);
+}
+function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) {
+ return typeof fn !== 'function' ? fn : function () {
+ var outerZone = PSD;
+ if (possibleAwait)
+ incrementExpectedAwaits();
+ switchToZone(zone, true);
+ try {
+ return fn.apply(this, arguments);
+ }
+ finally {
+ switchToZone(outerZone, false);
+ if (cleanup)
+ enqueueNativeMicroTask(decrementExpectedAwaits);
+ }
+ };
+}
+function getPatchedPromiseThen(origThen, zone) {
+ return function (onResolved, onRejected) {
+ return origThen.call(this, nativeAwaitCompatibleWrap(onResolved, zone), nativeAwaitCompatibleWrap(onRejected, zone));
+ };
+}
+var UNHANDLEDREJECTION = "unhandledrejection";
+function globalError(err, promise) {
+ var rv;
+ try {
+ rv = promise.onuncatched(err);
+ }
+ catch (e) { }
+ if (rv !== false)
+ try {
+ var event, eventData = { promise: promise, reason: err };
+ if (_global.document && document.createEvent) {
+ event = document.createEvent('Event');
+ event.initEvent(UNHANDLEDREJECTION, true, true);
+ extend(event, eventData);
+ }
+ else if (_global.CustomEvent) {
+ event = new CustomEvent(UNHANDLEDREJECTION, { detail: eventData });
+ extend(event, eventData);
+ }
+ if (event && _global.dispatchEvent) {
+ dispatchEvent(event);
+ if (!_global.PromiseRejectionEvent && _global.onunhandledrejection)
+ try {
+ _global.onunhandledrejection(event);
+ }
+ catch (_) { }
+ }
+ if (debug && event && !event.defaultPrevented) {
+ console.warn("Unhandled rejection: " + (err.stack || err));
+ }
+ }
+ catch (e) { }
+}
+var rejection = DexiePromise.reject;
+
+function tempTransaction(db, mode, storeNames, fn) {
+ if (!db.idbdb || (!db._state.openComplete && (!PSD.letThrough && !db._vip))) {
+ if (db._state.openComplete) {
+ return rejection(new exceptions.DatabaseClosed(db._state.dbOpenError));
+ }
+ if (!db._state.isBeingOpened) {
+ if (!db._options.autoOpen)
+ return rejection(new exceptions.DatabaseClosed());
+ db.open().catch(nop);
+ }
+ return db._state.dbReadyPromise.then(function () { return tempTransaction(db, mode, storeNames, fn); });
+ }
+ else {
+ var trans = db._createTransaction(mode, storeNames, db._dbSchema);
+ try {
+ trans.create();
+ db._state.PR1398_maxLoop = 3;
+ }
+ catch (ex) {
+ if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) {
+ console.warn('Dexie: Need to reopen db');
+ db._close();
+ return db.open().then(function () { return tempTransaction(db, mode, storeNames, fn); });
+ }
+ return rejection(ex);
+ }
+ return trans._promise(mode, function (resolve, reject) {
+ return newScope(function () {
+ PSD.trans = trans;
+ return fn(resolve, reject, trans);
+ });
+ }).then(function (result) {
+ return trans._completion.then(function () { return result; });
+ });
+ }
+}
+
+var DEXIE_VERSION = '3.2.2';
+var maxString = String.fromCharCode(65535);
+var minKey = -Infinity;
+var INVALID_KEY_ARGUMENT = "Invalid key provided. Keys must be of type string, number, Date or Array.";
+var STRING_EXPECTED = "String expected.";
+var connections = [];
+var isIEOrEdge = typeof navigator !== 'undefined' && /(MSIE|Trident|Edge)/.test(navigator.userAgent);
+var hasIEDeleteObjectStoreBug = isIEOrEdge;
+var hangsOnDeleteLargeKeyRange = isIEOrEdge;
+var dexieStackFrameFilter = function (frame) { return !/(dexie\.js|dexie\.min\.js)/.test(frame); };
+var DBNAMES_DB = '__dbnames';
+var READONLY = 'readonly';
+var READWRITE = 'readwrite';
+
+function combine(filter1, filter2) {
+ return filter1 ?
+ filter2 ?
+ function () { return filter1.apply(this, arguments) && filter2.apply(this, arguments); } :
+ filter1 :
+ filter2;
+}
+
+var AnyRange = {
+ type: 3 ,
+ lower: -Infinity,
+ lowerOpen: false,
+ upper: [[]],
+ upperOpen: false
+};
+
+function workaroundForUndefinedPrimKey(keyPath) {
+ return typeof keyPath === "string" && !/\./.test(keyPath)
+ ? function (obj) {
+ if (obj[keyPath] === undefined && (keyPath in obj)) {
+ obj = deepClone(obj);
+ delete obj[keyPath];
+ }
+ return obj;
+ }
+ : function (obj) { return obj; };
+}
+
+var Table = (function () {
+ function Table() {
+ }
+ Table.prototype._trans = function (mode, fn, writeLocked) {
+ var trans = this._tx || PSD.trans;
+ var tableName = this.name;
+ function checkTableInTransaction(resolve, reject, trans) {
+ if (!trans.schema[tableName])
+ throw new exceptions.NotFound("Table " + tableName + " not part of transaction");
+ return fn(trans.idbtrans, trans);
+ }
+ var wasRootExec = beginMicroTickScope();
+ try {
+ return trans && trans.db === this.db ?
+ trans === PSD.trans ?
+ trans._promise(mode, checkTableInTransaction, writeLocked) :
+ newScope(function () { return trans._promise(mode, checkTableInTransaction, writeLocked); }, { trans: trans, transless: PSD.transless || PSD }) :
+ tempTransaction(this.db, mode, [this.name], checkTableInTransaction);
+ }
+ finally {
+ if (wasRootExec)
+ endMicroTickScope();
+ }
+ };
+ Table.prototype.get = function (keyOrCrit, cb) {
+ var _this = this;
+ if (keyOrCrit && keyOrCrit.constructor === Object)
+ return this.where(keyOrCrit).first(cb);
+ return this._trans('readonly', function (trans) {
+ return _this.core.get({ trans: trans, key: keyOrCrit })
+ .then(function (res) { return _this.hook.reading.fire(res); });
+ }).then(cb);
+ };
+ Table.prototype.where = function (indexOrCrit) {
+ if (typeof indexOrCrit === 'string')
+ return new this.db.WhereClause(this, indexOrCrit);
+ if (isArray(indexOrCrit))
+ return new this.db.WhereClause(this, "[" + indexOrCrit.join('+') + "]");
+ var keyPaths = keys(indexOrCrit);
+ if (keyPaths.length === 1)
+ return this
+ .where(keyPaths[0])
+ .equals(indexOrCrit[keyPaths[0]]);
+ var compoundIndex = this.schema.indexes.concat(this.schema.primKey).filter(function (ix) {
+ return ix.compound &&
+ keyPaths.every(function (keyPath) { return ix.keyPath.indexOf(keyPath) >= 0; }) &&
+ ix.keyPath.every(function (keyPath) { return keyPaths.indexOf(keyPath) >= 0; });
+ })[0];
+ if (compoundIndex && this.db._maxKey !== maxString)
+ return this
+ .where(compoundIndex.name)
+ .equals(compoundIndex.keyPath.map(function (kp) { return indexOrCrit[kp]; }));
+ if (!compoundIndex && debug)
+ console.warn("The query " + JSON.stringify(indexOrCrit) + " on " + this.name + " would benefit of a " +
+ ("compound index [" + keyPaths.join('+') + "]"));
+ var idxByName = this.schema.idxByName;
+ var idb = this.db._deps.indexedDB;
+ function equals(a, b) {
+ try {
+ return idb.cmp(a, b) === 0;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+ var _a = keyPaths.reduce(function (_a, keyPath) {
+ var prevIndex = _a[0], prevFilterFn = _a[1];
+ var index = idxByName[keyPath];
+ var value = indexOrCrit[keyPath];
+ return [
+ prevIndex || index,
+ prevIndex || !index ?
+ combine(prevFilterFn, index && index.multi ?
+ function (x) {
+ var prop = getByKeyPath(x, keyPath);
+ return isArray(prop) && prop.some(function (item) { return equals(value, item); });
+ } : function (x) { return equals(value, getByKeyPath(x, keyPath)); })
+ : prevFilterFn
+ ];
+ }, [null, null]), idx = _a[0], filterFunction = _a[1];
+ return idx ?
+ this.where(idx.name).equals(indexOrCrit[idx.keyPath])
+ .filter(filterFunction) :
+ compoundIndex ?
+ this.filter(filterFunction) :
+ this.where(keyPaths).equals('');
+ };
+ Table.prototype.filter = function (filterFunction) {
+ return this.toCollection().and(filterFunction);
+ };
+ Table.prototype.count = function (thenShortcut) {
+ return this.toCollection().count(thenShortcut);
+ };
+ Table.prototype.offset = function (offset) {
+ return this.toCollection().offset(offset);
+ };
+ Table.prototype.limit = function (numRows) {
+ return this.toCollection().limit(numRows);
+ };
+ Table.prototype.each = function (callback) {
+ return this.toCollection().each(callback);
+ };
+ Table.prototype.toArray = function (thenShortcut) {
+ return this.toCollection().toArray(thenShortcut);
+ };
+ Table.prototype.toCollection = function () {
+ return new this.db.Collection(new this.db.WhereClause(this));
+ };
+ Table.prototype.orderBy = function (index) {
+ return new this.db.Collection(new this.db.WhereClause(this, isArray(index) ?
+ "[" + index.join('+') + "]" :
+ index));
+ };
+ Table.prototype.reverse = function () {
+ return this.toCollection().reverse();
+ };
+ Table.prototype.mapToClass = function (constructor) {
+ this.schema.mappedClass = constructor;
+ var readHook = function (obj) {
+ if (!obj)
+ return obj;
+ var res = Object.create(constructor.prototype);
+ for (var m in obj)
+ if (hasOwn(obj, m))
+ try {
+ res[m] = obj[m];
+ }
+ catch (_) { }
+ return res;
+ };
+ if (this.schema.readHook) {
+ this.hook.reading.unsubscribe(this.schema.readHook);
+ }
+ this.schema.readHook = readHook;
+ this.hook("reading", readHook);
+ return constructor;
+ };
+ Table.prototype.defineClass = function () {
+ function Class(content) {
+ extend(this, content);
+ }
+ return this.mapToClass(Class);
+ };
+ Table.prototype.add = function (obj, key) {
+ var _this = this;
+ var _a = this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath;
+ var objToAdd = obj;
+ if (keyPath && auto) {
+ objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj);
+ }
+ return this._trans('readwrite', function (trans) {
+ return _this.core.mutate({ trans: trans, type: 'add', keys: key != null ? [key] : null, values: [objToAdd] });
+ }).then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; })
+ .then(function (lastResult) {
+ if (keyPath) {
+ try {
+ setByKeyPath(obj, keyPath, lastResult);
+ }
+ catch (_) { }
+ }
+ return lastResult;
+ });
+ };
+ Table.prototype.update = function (keyOrObject, modifications) {
+ if (typeof keyOrObject === 'object' && !isArray(keyOrObject)) {
+ var key = getByKeyPath(keyOrObject, this.schema.primKey.keyPath);
+ if (key === undefined)
+ return rejection(new exceptions.InvalidArgument("Given object does not contain its primary key"));
+ try {
+ if (typeof modifications !== "function") {
+ keys(modifications).forEach(function (keyPath) {
+ setByKeyPath(keyOrObject, keyPath, modifications[keyPath]);
+ });
+ }
+ else {
+ modifications(keyOrObject, { value: keyOrObject, primKey: key });
+ }
+ }
+ catch (_a) {
+ }
+ return this.where(":id").equals(key).modify(modifications);
+ }
+ else {
+ return this.where(":id").equals(keyOrObject).modify(modifications);
+ }
+ };
+ Table.prototype.put = function (obj, key) {
+ var _this = this;
+ var _a = this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath;
+ var objToAdd = obj;
+ if (keyPath && auto) {
+ objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj);
+ }
+ return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'put', values: [objToAdd], keys: key != null ? [key] : null }); })
+ .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; })
+ .then(function (lastResult) {
+ if (keyPath) {
+ try {
+ setByKeyPath(obj, keyPath, lastResult);
+ }
+ catch (_) { }
+ }
+ return lastResult;
+ });
+ };
+ Table.prototype.delete = function (key) {
+ var _this = this;
+ return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'delete', keys: [key] }); })
+ .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined; });
+ };
+ Table.prototype.clear = function () {
+ var _this = this;
+ return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'deleteRange', range: AnyRange }); })
+ .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined; });
+ };
+ Table.prototype.bulkGet = function (keys) {
+ var _this = this;
+ return this._trans('readonly', function (trans) {
+ return _this.core.getMany({
+ keys: keys,
+ trans: trans
+ }).then(function (result) { return result.map(function (res) { return _this.hook.reading.fire(res); }); });
+ });
+ };
+ Table.prototype.bulkAdd = function (objects, keysOrOptions, options) {
+ var _this = this;
+ var keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined;
+ options = options || (keys ? undefined : keysOrOptions);
+ var wantResults = options ? options.allKeys : undefined;
+ return this._trans('readwrite', function (trans) {
+ var _a = _this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath;
+ if (keyPath && keys)
+ throw new exceptions.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys");
+ if (keys && keys.length !== objects.length)
+ throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length");
+ var numObjects = objects.length;
+ var objectsToAdd = keyPath && auto ?
+ objects.map(workaroundForUndefinedPrimKey(keyPath)) :
+ objects;
+ return _this.core.mutate({ trans: trans, type: 'add', keys: keys, values: objectsToAdd, wantResults: wantResults })
+ .then(function (_a) {
+ var numFailures = _a.numFailures, results = _a.results, lastResult = _a.lastResult, failures = _a.failures;
+ var result = wantResults ? results : lastResult;
+ if (numFailures === 0)
+ return result;
+ throw new BulkError(_this.name + ".bulkAdd(): " + numFailures + " of " + numObjects + " operations failed", failures);
+ });
+ });
+ };
+ Table.prototype.bulkPut = function (objects, keysOrOptions, options) {
+ var _this = this;
+ var keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined;
+ options = options || (keys ? undefined : keysOrOptions);
+ var wantResults = options ? options.allKeys : undefined;
+ return this._trans('readwrite', function (trans) {
+ var _a = _this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath;
+ if (keyPath && keys)
+ throw new exceptions.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys");
+ if (keys && keys.length !== objects.length)
+ throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length");
+ var numObjects = objects.length;
+ var objectsToPut = keyPath && auto ?
+ objects.map(workaroundForUndefinedPrimKey(keyPath)) :
+ objects;
+ return _this.core.mutate({ trans: trans, type: 'put', keys: keys, values: objectsToPut, wantResults: wantResults })
+ .then(function (_a) {
+ var numFailures = _a.numFailures, results = _a.results, lastResult = _a.lastResult, failures = _a.failures;
+ var result = wantResults ? results : lastResult;
+ if (numFailures === 0)
+ return result;
+ throw new BulkError(_this.name + ".bulkPut(): " + numFailures + " of " + numObjects + " operations failed", failures);
+ });
+ });
+ };
+ Table.prototype.bulkDelete = function (keys) {
+ var _this = this;
+ var numKeys = keys.length;
+ return this._trans('readwrite', function (trans) {
+ return _this.core.mutate({ trans: trans, type: 'delete', keys: keys });
+ }).then(function (_a) {
+ var numFailures = _a.numFailures, lastResult = _a.lastResult, failures = _a.failures;
+ if (numFailures === 0)
+ return lastResult;
+ throw new BulkError(_this.name + ".bulkDelete(): " + numFailures + " of " + numKeys + " operations failed", failures);
+ });
+ };
+ return Table;
+}());
+
+function Events(ctx) {
+ var evs = {};
+ var rv = function (eventName, subscriber) {
+ if (subscriber) {
+ var i = arguments.length, args = new Array(i - 1);
+ while (--i)
+ args[i - 1] = arguments[i];
+ evs[eventName].subscribe.apply(null, args);
+ return ctx;
+ }
+ else if (typeof (eventName) === 'string') {
+ return evs[eventName];
+ }
+ };
+ rv.addEventType = add;
+ for (var i = 1, l = arguments.length; i < l; ++i) {
+ add(arguments[i]);
+ }
+ return rv;
+ function add(eventName, chainFunction, defaultFunction) {
+ if (typeof eventName === 'object')
+ return addConfiguredEvents(eventName);
+ if (!chainFunction)
+ chainFunction = reverseStoppableEventChain;
+ if (!defaultFunction)
+ defaultFunction = nop;
+ var context = {
+ subscribers: [],
+ fire: defaultFunction,
+ subscribe: function (cb) {
+ if (context.subscribers.indexOf(cb) === -1) {
+ context.subscribers.push(cb);
+ context.fire = chainFunction(context.fire, cb);
+ }
+ },
+ unsubscribe: function (cb) {
+ context.subscribers = context.subscribers.filter(function (fn) { return fn !== cb; });
+ context.fire = context.subscribers.reduce(chainFunction, defaultFunction);
+ }
+ };
+ evs[eventName] = rv[eventName] = context;
+ return context;
+ }
+ function addConfiguredEvents(cfg) {
+ keys(cfg).forEach(function (eventName) {
+ var args = cfg[eventName];
+ if (isArray(args)) {
+ add(eventName, cfg[eventName][0], cfg[eventName][1]);
+ }
+ else if (args === 'asap') {
+ var context = add(eventName, mirror, function fire() {
+ var i = arguments.length, args = new Array(i);
+ while (i--)
+ args[i] = arguments[i];
+ context.subscribers.forEach(function (fn) {
+ asap$1(function fireEvent() {
+ fn.apply(null, args);
+ });
+ });
+ });
+ }
+ else
+ throw new exceptions.InvalidArgument("Invalid event config");
+ });
+ }
+}
+
+function makeClassConstructor(prototype, constructor) {
+ derive(constructor).from({ prototype: prototype });
+ return constructor;
+}
+
+function createTableConstructor(db) {
+ return makeClassConstructor(Table.prototype, function Table(name, tableSchema, trans) {
+ this.db = db;
+ this._tx = trans;
+ this.name = name;
+ this.schema = tableSchema;
+ this.hook = db._allTables[name] ? db._allTables[name].hook : Events(null, {
+ "creating": [hookCreatingChain, nop],
+ "reading": [pureFunctionChain, mirror],
+ "updating": [hookUpdatingChain, nop],
+ "deleting": [hookDeletingChain, nop]
+ });
+ });
+}
+
+function isPlainKeyRange(ctx, ignoreLimitFilter) {
+ return !(ctx.filter || ctx.algorithm || ctx.or) &&
+ (ignoreLimitFilter ? ctx.justLimit : !ctx.replayFilter);
+}
+function addFilter(ctx, fn) {
+ ctx.filter = combine(ctx.filter, fn);
+}
+function addReplayFilter(ctx, factory, isLimitFilter) {
+ var curr = ctx.replayFilter;
+ ctx.replayFilter = curr ? function () { return combine(curr(), factory()); } : factory;
+ ctx.justLimit = isLimitFilter && !curr;
+}
+function addMatchFilter(ctx, fn) {
+ ctx.isMatch = combine(ctx.isMatch, fn);
+}
+function getIndexOrStore(ctx, coreSchema) {
+ if (ctx.isPrimKey)
+ return coreSchema.primaryKey;
+ var index = coreSchema.getIndexByKeyPath(ctx.index);
+ if (!index)
+ throw new exceptions.Schema("KeyPath " + ctx.index + " on object store " + coreSchema.name + " is not indexed");
+ return index;
+}
+function openCursor(ctx, coreTable, trans) {
+ var index = getIndexOrStore(ctx, coreTable.schema);
+ return coreTable.openCursor({
+ trans: trans,
+ values: !ctx.keysOnly,
+ reverse: ctx.dir === 'prev',
+ unique: !!ctx.unique,
+ query: {
+ index: index,
+ range: ctx.range
+ }
+ });
+}
+function iter(ctx, fn, coreTrans, coreTable) {
+ var filter = ctx.replayFilter ? combine(ctx.filter, ctx.replayFilter()) : ctx.filter;
+ if (!ctx.or) {
+ return iterate(openCursor(ctx, coreTable, coreTrans), combine(ctx.algorithm, filter), fn, !ctx.keysOnly && ctx.valueMapper);
+ }
+ else {
+ var set_1 = {};
+ var union = function (item, cursor, advance) {
+ if (!filter || filter(cursor, advance, function (result) { return cursor.stop(result); }, function (err) { return cursor.fail(err); })) {
+ var primaryKey = cursor.primaryKey;
+ var key = '' + primaryKey;
+ if (key === '[object ArrayBuffer]')
+ key = '' + new Uint8Array(primaryKey);
+ if (!hasOwn(set_1, key)) {
+ set_1[key] = true;
+ fn(item, cursor, advance);
+ }
+ }
+ };
+ return Promise.all([
+ ctx.or._iterate(union, coreTrans),
+ iterate(openCursor(ctx, coreTable, coreTrans), ctx.algorithm, union, !ctx.keysOnly && ctx.valueMapper)
+ ]);
+ }
+}
+function iterate(cursorPromise, filter, fn, valueMapper) {
+ var mappedFn = valueMapper ? function (x, c, a) { return fn(valueMapper(x), c, a); } : fn;
+ var wrappedFn = wrap(mappedFn);
+ return cursorPromise.then(function (cursor) {
+ if (cursor) {
+ return cursor.start(function () {
+ var c = function () { return cursor.continue(); };
+ if (!filter || filter(cursor, function (advancer) { return c = advancer; }, function (val) { cursor.stop(val); c = nop; }, function (e) { cursor.fail(e); c = nop; }))
+ wrappedFn(cursor.value, cursor, function (advancer) { return c = advancer; });
+ c();
+ });
+ }
+ });
+}
+
+function cmp(a, b) {
+ try {
+ var ta = type(a);
+ var tb = type(b);
+ if (ta !== tb) {
+ if (ta === 'Array')
+ return 1;
+ if (tb === 'Array')
+ return -1;
+ if (ta === 'binary')
+ return 1;
+ if (tb === 'binary')
+ return -1;
+ if (ta === 'string')
+ return 1;
+ if (tb === 'string')
+ return -1;
+ if (ta === 'Date')
+ return 1;
+ if (tb !== 'Date')
+ return NaN;
+ return -1;
+ }
+ switch (ta) {
+ case 'number':
+ case 'Date':
+ case 'string':
+ return a > b ? 1 : a < b ? -1 : 0;
+ case 'binary': {
+ return compareUint8Arrays(getUint8Array(a), getUint8Array(b));
+ }
+ case 'Array':
+ return compareArrays(a, b);
+ }
+ }
+ catch (_a) { }
+ return NaN;
+}
+function compareArrays(a, b) {
+ var al = a.length;
+ var bl = b.length;
+ var l = al < bl ? al : bl;
+ for (var i = 0; i < l; ++i) {
+ var res = cmp(a[i], b[i]);
+ if (res !== 0)
+ return res;
+ }
+ return al === bl ? 0 : al < bl ? -1 : 1;
+}
+function compareUint8Arrays(a, b) {
+ var al = a.length;
+ var bl = b.length;
+ var l = al < bl ? al : bl;
+ for (var i = 0; i < l; ++i) {
+ if (a[i] !== b[i])
+ return a[i] < b[i] ? -1 : 1;
+ }
+ return al === bl ? 0 : al < bl ? -1 : 1;
+}
+function type(x) {
+ var t = typeof x;
+ if (t !== 'object')
+ return t;
+ if (ArrayBuffer.isView(x))
+ return 'binary';
+ var tsTag = toStringTag(x);
+ return tsTag === 'ArrayBuffer' ? 'binary' : tsTag;
+}
+function getUint8Array(a) {
+ if (a instanceof Uint8Array)
+ return a;
+ if (ArrayBuffer.isView(a))
+ return new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
+ return new Uint8Array(a);
+}
+
+var Collection = (function () {
+ function Collection() {
+ }
+ Collection.prototype._read = function (fn, cb) {
+ var ctx = this._ctx;
+ return ctx.error ?
+ ctx.table._trans(null, rejection.bind(null, ctx.error)) :
+ ctx.table._trans('readonly', fn).then(cb);
+ };
+ Collection.prototype._write = function (fn) {
+ var ctx = this._ctx;
+ return ctx.error ?
+ ctx.table._trans(null, rejection.bind(null, ctx.error)) :
+ ctx.table._trans('readwrite', fn, "locked");
+ };
+ Collection.prototype._addAlgorithm = function (fn) {
+ var ctx = this._ctx;
+ ctx.algorithm = combine(ctx.algorithm, fn);
+ };
+ Collection.prototype._iterate = function (fn, coreTrans) {
+ return iter(this._ctx, fn, coreTrans, this._ctx.table.core);
+ };
+ Collection.prototype.clone = function (props) {
+ var rv = Object.create(this.constructor.prototype), ctx = Object.create(this._ctx);
+ if (props)
+ extend(ctx, props);
+ rv._ctx = ctx;
+ return rv;
+ };
+ Collection.prototype.raw = function () {
+ this._ctx.valueMapper = null;
+ return this;
+ };
+ Collection.prototype.each = function (fn) {
+ var ctx = this._ctx;
+ return this._read(function (trans) { return iter(ctx, fn, trans, ctx.table.core); });
+ };
+ Collection.prototype.count = function (cb) {
+ var _this = this;
+ return this._read(function (trans) {
+ var ctx = _this._ctx;
+ var coreTable = ctx.table.core;
+ if (isPlainKeyRange(ctx, true)) {
+ return coreTable.count({
+ trans: trans,
+ query: {
+ index: getIndexOrStore(ctx, coreTable.schema),
+ range: ctx.range
+ }
+ }).then(function (count) { return Math.min(count, ctx.limit); });
+ }
+ else {
+ var count = 0;
+ return iter(ctx, function () { ++count; return false; }, trans, coreTable)
+ .then(function () { return count; });
+ }
+ }).then(cb);
+ };
+ Collection.prototype.sortBy = function (keyPath, cb) {
+ var parts = keyPath.split('.').reverse(), lastPart = parts[0], lastIndex = parts.length - 1;
+ function getval(obj, i) {
+ if (i)
+ return getval(obj[parts[i]], i - 1);
+ return obj[lastPart];
+ }
+ var order = this._ctx.dir === "next" ? 1 : -1;
+ function sorter(a, b) {
+ var aVal = getval(a, lastIndex), bVal = getval(b, lastIndex);
+ return aVal < bVal ? -order : aVal > bVal ? order : 0;
+ }
+ return this.toArray(function (a) {
+ return a.sort(sorter);
+ }).then(cb);
+ };
+ Collection.prototype.toArray = function (cb) {
+ var _this = this;
+ return this._read(function (trans) {
+ var ctx = _this._ctx;
+ if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) {
+ var valueMapper_1 = ctx.valueMapper;
+ var index = getIndexOrStore(ctx, ctx.table.core.schema);
+ return ctx.table.core.query({
+ trans: trans,
+ limit: ctx.limit,
+ values: true,
+ query: {
+ index: index,
+ range: ctx.range
+ }
+ }).then(function (_a) {
+ var result = _a.result;
+ return valueMapper_1 ? result.map(valueMapper_1) : result;
+ });
+ }
+ else {
+ var a_1 = [];
+ return iter(ctx, function (item) { return a_1.push(item); }, trans, ctx.table.core).then(function () { return a_1; });
+ }
+ }, cb);
+ };
+ Collection.prototype.offset = function (offset) {
+ var ctx = this._ctx;
+ if (offset <= 0)
+ return this;
+ ctx.offset += offset;
+ if (isPlainKeyRange(ctx)) {
+ addReplayFilter(ctx, function () {
+ var offsetLeft = offset;
+ return function (cursor, advance) {
+ if (offsetLeft === 0)
+ return true;
+ if (offsetLeft === 1) {
+ --offsetLeft;
+ return false;
+ }
+ advance(function () {
+ cursor.advance(offsetLeft);
+ offsetLeft = 0;
+ });
+ return false;
+ };
+ });
+ }
+ else {
+ addReplayFilter(ctx, function () {
+ var offsetLeft = offset;
+ return function () { return (--offsetLeft < 0); };
+ });
+ }
+ return this;
+ };
+ Collection.prototype.limit = function (numRows) {
+ this._ctx.limit = Math.min(this._ctx.limit, numRows);
+ addReplayFilter(this._ctx, function () {
+ var rowsLeft = numRows;
+ return function (cursor, advance, resolve) {
+ if (--rowsLeft <= 0)
+ advance(resolve);
+ return rowsLeft >= 0;
+ };
+ }, true);
+ return this;
+ };
+ Collection.prototype.until = function (filterFunction, bIncludeStopEntry) {
+ addFilter(this._ctx, function (cursor, advance, resolve) {
+ if (filterFunction(cursor.value)) {
+ advance(resolve);
+ return bIncludeStopEntry;
+ }
+ else {
+ return true;
+ }
+ });
+ return this;
+ };
+ Collection.prototype.first = function (cb) {
+ return this.limit(1).toArray(function (a) { return a[0]; }).then(cb);
+ };
+ Collection.prototype.last = function (cb) {
+ return this.reverse().first(cb);
+ };
+ Collection.prototype.filter = function (filterFunction) {
+ addFilter(this._ctx, function (cursor) {
+ return filterFunction(cursor.value);
+ });
+ addMatchFilter(this._ctx, filterFunction);
+ return this;
+ };
+ Collection.prototype.and = function (filter) {
+ return this.filter(filter);
+ };
+ Collection.prototype.or = function (indexName) {
+ return new this.db.WhereClause(this._ctx.table, indexName, this);
+ };
+ Collection.prototype.reverse = function () {
+ this._ctx.dir = (this._ctx.dir === "prev" ? "next" : "prev");
+ if (this._ondirectionchange)
+ this._ondirectionchange(this._ctx.dir);
+ return this;
+ };
+ Collection.prototype.desc = function () {
+ return this.reverse();
+ };
+ Collection.prototype.eachKey = function (cb) {
+ var ctx = this._ctx;
+ ctx.keysOnly = !ctx.isMatch;
+ return this.each(function (val, cursor) { cb(cursor.key, cursor); });
+ };
+ Collection.prototype.eachUniqueKey = function (cb) {
+ this._ctx.unique = "unique";
+ return this.eachKey(cb);
+ };
+ Collection.prototype.eachPrimaryKey = function (cb) {
+ var ctx = this._ctx;
+ ctx.keysOnly = !ctx.isMatch;
+ return this.each(function (val, cursor) { cb(cursor.primaryKey, cursor); });
+ };
+ Collection.prototype.keys = function (cb) {
+ var ctx = this._ctx;
+ ctx.keysOnly = !ctx.isMatch;
+ var a = [];
+ return this.each(function (item, cursor) {
+ a.push(cursor.key);
+ }).then(function () {
+ return a;
+ }).then(cb);
+ };
+ Collection.prototype.primaryKeys = function (cb) {
+ var ctx = this._ctx;
+ if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) {
+ return this._read(function (trans) {
+ var index = getIndexOrStore(ctx, ctx.table.core.schema);
+ return ctx.table.core.query({
+ trans: trans,
+ values: false,
+ limit: ctx.limit,
+ query: {
+ index: index,
+ range: ctx.range
+ }
+ });
+ }).then(function (_a) {
+ var result = _a.result;
+ return result;
+ }).then(cb);
+ }
+ ctx.keysOnly = !ctx.isMatch;
+ var a = [];
+ return this.each(function (item, cursor) {
+ a.push(cursor.primaryKey);
+ }).then(function () {
+ return a;
+ }).then(cb);
+ };
+ Collection.prototype.uniqueKeys = function (cb) {
+ this._ctx.unique = "unique";
+ return this.keys(cb);
+ };
+ Collection.prototype.firstKey = function (cb) {
+ return this.limit(1).keys(function (a) { return a[0]; }).then(cb);
+ };
+ Collection.prototype.lastKey = function (cb) {
+ return this.reverse().firstKey(cb);
+ };
+ Collection.prototype.distinct = function () {
+ var ctx = this._ctx, idx = ctx.index && ctx.table.schema.idxByName[ctx.index];
+ if (!idx || !idx.multi)
+ return this;
+ var set = {};
+ addFilter(this._ctx, function (cursor) {
+ var strKey = cursor.primaryKey.toString();
+ var found = hasOwn(set, strKey);
+ set[strKey] = true;
+ return !found;
+ });
+ return this;
+ };
+ Collection.prototype.modify = function (changes) {
+ var _this = this;
+ var ctx = this._ctx;
+ return this._write(function (trans) {
+ var modifyer;
+ if (typeof changes === 'function') {
+ modifyer = changes;
+ }
+ else {
+ var keyPaths = keys(changes);
+ var numKeys = keyPaths.length;
+ modifyer = function (item) {
+ var anythingModified = false;
+ for (var i = 0; i < numKeys; ++i) {
+ var keyPath = keyPaths[i], val = changes[keyPath];
+ if (getByKeyPath(item, keyPath) !== val) {
+ setByKeyPath(item, keyPath, val);
+ anythingModified = true;
+ }
+ }
+ return anythingModified;
+ };
+ }
+ var coreTable = ctx.table.core;
+ var _a = coreTable.schema.primaryKey, outbound = _a.outbound, extractKey = _a.extractKey;
+ var limit = _this.db._options.modifyChunkSize || 200;
+ var totalFailures = [];
+ var successCount = 0;
+ var failedKeys = [];
+ var applyMutateResult = function (expectedCount, res) {
+ var failures = res.failures, numFailures = res.numFailures;
+ successCount += expectedCount - numFailures;
+ for (var _i = 0, _a = keys(failures); _i < _a.length; _i++) {
+ var pos = _a[_i];
+ totalFailures.push(failures[pos]);
+ }
+ };
+ return _this.clone().primaryKeys().then(function (keys) {
+ var nextChunk = function (offset) {
+ var count = Math.min(limit, keys.length - offset);
+ return coreTable.getMany({
+ trans: trans,
+ keys: keys.slice(offset, offset + count),
+ cache: "immutable"
+ }).then(function (values) {
+ var addValues = [];
+ var putValues = [];
+ var putKeys = outbound ? [] : null;
+ var deleteKeys = [];
+ for (var i = 0; i < count; ++i) {
+ var origValue = values[i];
+ var ctx_1 = {
+ value: deepClone(origValue),
+ primKey: keys[offset + i]
+ };
+ if (modifyer.call(ctx_1, ctx_1.value, ctx_1) !== false) {
+ if (ctx_1.value == null) {
+ deleteKeys.push(keys[offset + i]);
+ }
+ else if (!outbound && cmp(extractKey(origValue), extractKey(ctx_1.value)) !== 0) {
+ deleteKeys.push(keys[offset + i]);
+ addValues.push(ctx_1.value);
+ }
+ else {
+ putValues.push(ctx_1.value);
+ if (outbound)
+ putKeys.push(keys[offset + i]);
+ }
+ }
+ }
+ var criteria = isPlainKeyRange(ctx) &&
+ ctx.limit === Infinity &&
+ (typeof changes !== 'function' || changes === deleteCallback) && {
+ index: ctx.index,
+ range: ctx.range
+ };
+ return Promise.resolve(addValues.length > 0 &&
+ coreTable.mutate({ trans: trans, type: 'add', values: addValues })
+ .then(function (res) {
+ for (var pos in res.failures) {
+ deleteKeys.splice(parseInt(pos), 1);
+ }
+ applyMutateResult(addValues.length, res);
+ })).then(function () { return (putValues.length > 0 || (criteria && typeof changes === 'object')) &&
+ coreTable.mutate({
+ trans: trans,
+ type: 'put',
+ keys: putKeys,
+ values: putValues,
+ criteria: criteria,
+ changeSpec: typeof changes !== 'function'
+ && changes
+ }).then(function (res) { return applyMutateResult(putValues.length, res); }); }).then(function () { return (deleteKeys.length > 0 || (criteria && changes === deleteCallback)) &&
+ coreTable.mutate({
+ trans: trans,
+ type: 'delete',
+ keys: deleteKeys,
+ criteria: criteria
+ }).then(function (res) { return applyMutateResult(deleteKeys.length, res); }); }).then(function () {
+ return keys.length > offset + count && nextChunk(offset + limit);
+ });
+ });
+ };
+ return nextChunk(0).then(function () {
+ if (totalFailures.length > 0)
+ throw new ModifyError("Error modifying one or more objects", totalFailures, successCount, failedKeys);
+ return keys.length;
+ });
+ });
+ });
+ };
+ Collection.prototype.delete = function () {
+ var ctx = this._ctx, range = ctx.range;
+ if (isPlainKeyRange(ctx) &&
+ ((ctx.isPrimKey && !hangsOnDeleteLargeKeyRange) || range.type === 3 ))
+ {
+ return this._write(function (trans) {
+ var primaryKey = ctx.table.core.schema.primaryKey;
+ var coreRange = range;
+ return ctx.table.core.count({ trans: trans, query: { index: primaryKey, range: coreRange } }).then(function (count) {
+ return ctx.table.core.mutate({ trans: trans, type: 'deleteRange', range: coreRange })
+ .then(function (_a) {
+ var failures = _a.failures; _a.lastResult; _a.results; var numFailures = _a.numFailures;
+ if (numFailures)
+ throw new ModifyError("Could not delete some values", Object.keys(failures).map(function (pos) { return failures[pos]; }), count - numFailures);
+ return count - numFailures;
+ });
+ });
+ });
+ }
+ return this.modify(deleteCallback);
+ };
+ return Collection;
+}());
+var deleteCallback = function (value, ctx) { return ctx.value = null; };
+
+function createCollectionConstructor(db) {
+ return makeClassConstructor(Collection.prototype, function Collection(whereClause, keyRangeGenerator) {
+ this.db = db;
+ var keyRange = AnyRange, error = null;
+ if (keyRangeGenerator)
+ try {
+ keyRange = keyRangeGenerator();
+ }
+ catch (ex) {
+ error = ex;
+ }
+ var whereCtx = whereClause._ctx;
+ var table = whereCtx.table;
+ var readingHook = table.hook.reading.fire;
+ this._ctx = {
+ table: table,
+ index: whereCtx.index,
+ isPrimKey: (!whereCtx.index || (table.schema.primKey.keyPath && whereCtx.index === table.schema.primKey.name)),
+ range: keyRange,
+ keysOnly: false,
+ dir: "next",
+ unique: "",
+ algorithm: null,
+ filter: null,
+ replayFilter: null,
+ justLimit: true,
+ isMatch: null,
+ offset: 0,
+ limit: Infinity,
+ error: error,
+ or: whereCtx.or,
+ valueMapper: readingHook !== mirror ? readingHook : null
+ };
+ });
+}
+
+function simpleCompare(a, b) {
+ return a < b ? -1 : a === b ? 0 : 1;
+}
+function simpleCompareReverse(a, b) {
+ return a > b ? -1 : a === b ? 0 : 1;
+}
+
+function fail(collectionOrWhereClause, err, T) {
+ var collection = collectionOrWhereClause instanceof WhereClause ?
+ new collectionOrWhereClause.Collection(collectionOrWhereClause) :
+ collectionOrWhereClause;
+ collection._ctx.error = T ? new T(err) : new TypeError(err);
+ return collection;
+}
+function emptyCollection(whereClause) {
+ return new whereClause.Collection(whereClause, function () { return rangeEqual(""); }).limit(0);
+}
+function upperFactory(dir) {
+ return dir === "next" ?
+ function (s) { return s.toUpperCase(); } :
+ function (s) { return s.toLowerCase(); };
+}
+function lowerFactory(dir) {
+ return dir === "next" ?
+ function (s) { return s.toLowerCase(); } :
+ function (s) { return s.toUpperCase(); };
+}
+function nextCasing(key, lowerKey, upperNeedle, lowerNeedle, cmp, dir) {
+ var length = Math.min(key.length, lowerNeedle.length);
+ var llp = -1;
+ for (var i = 0; i < length; ++i) {
+ var lwrKeyChar = lowerKey[i];
+ if (lwrKeyChar !== lowerNeedle[i]) {
+ if (cmp(key[i], upperNeedle[i]) < 0)
+ return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1);
+ if (cmp(key[i], lowerNeedle[i]) < 0)
+ return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1);
+ if (llp >= 0)
+ return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1);
+ return null;
+ }
+ if (cmp(key[i], lwrKeyChar) < 0)
+ llp = i;
+ }
+ if (length < lowerNeedle.length && dir === "next")
+ return key + upperNeedle.substr(key.length);
+ if (length < key.length && dir === "prev")
+ return key.substr(0, upperNeedle.length);
+ return (llp < 0 ? null : key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1));
+}
+function addIgnoreCaseAlgorithm(whereClause, match, needles, suffix) {
+ var upper, lower, compare, upperNeedles, lowerNeedles, direction, nextKeySuffix, needlesLen = needles.length;
+ if (!needles.every(function (s) { return typeof s === 'string'; })) {
+ return fail(whereClause, STRING_EXPECTED);
+ }
+ function initDirection(dir) {
+ upper = upperFactory(dir);
+ lower = lowerFactory(dir);
+ compare = (dir === "next" ? simpleCompare : simpleCompareReverse);
+ var needleBounds = needles.map(function (needle) {
+ return { lower: lower(needle), upper: upper(needle) };
+ }).sort(function (a, b) {
+ return compare(a.lower, b.lower);
+ });
+ upperNeedles = needleBounds.map(function (nb) { return nb.upper; });
+ lowerNeedles = needleBounds.map(function (nb) { return nb.lower; });
+ direction = dir;
+ nextKeySuffix = (dir === "next" ? "" : suffix);
+ }
+ initDirection("next");
+ var c = new whereClause.Collection(whereClause, function () { return createRange(upperNeedles[0], lowerNeedles[needlesLen - 1] + suffix); });
+ c._ondirectionchange = function (direction) {
+ initDirection(direction);
+ };
+ var firstPossibleNeedle = 0;
+ c._addAlgorithm(function (cursor, advance, resolve) {
+ var key = cursor.key;
+ if (typeof key !== 'string')
+ return false;
+ var lowerKey = lower(key);
+ if (match(lowerKey, lowerNeedles, firstPossibleNeedle)) {
+ return true;
+ }
+ else {
+ var lowestPossibleCasing = null;
+ for (var i = firstPossibleNeedle; i < needlesLen; ++i) {
+ var casing = nextCasing(key, lowerKey, upperNeedles[i], lowerNeedles[i], compare, direction);
+ if (casing === null && lowestPossibleCasing === null)
+ firstPossibleNeedle = i + 1;
+ else if (lowestPossibleCasing === null || compare(lowestPossibleCasing, casing) > 0) {
+ lowestPossibleCasing = casing;
+ }
+ }
+ if (lowestPossibleCasing !== null) {
+ advance(function () { cursor.continue(lowestPossibleCasing + nextKeySuffix); });
+ }
+ else {
+ advance(resolve);
+ }
+ return false;
+ }
+ });
+ return c;
+}
+function createRange(lower, upper, lowerOpen, upperOpen) {
+ return {
+ type: 2 ,
+ lower: lower,
+ upper: upper,
+ lowerOpen: lowerOpen,
+ upperOpen: upperOpen
+ };
+}
+function rangeEqual(value) {
+ return {
+ type: 1 ,
+ lower: value,
+ upper: value
+ };
+}
+
+var WhereClause = (function () {
+ function WhereClause() {
+ }
+ Object.defineProperty(WhereClause.prototype, "Collection", {
+ get: function () {
+ return this._ctx.table.db.Collection;
+ },
+ enumerable: false,
+ configurable: true
+ });
+ WhereClause.prototype.between = function (lower, upper, includeLower, includeUpper) {
+ includeLower = includeLower !== false;
+ includeUpper = includeUpper === true;
+ try {
+ if ((this._cmp(lower, upper) > 0) ||
+ (this._cmp(lower, upper) === 0 && (includeLower || includeUpper) && !(includeLower && includeUpper)))
+ return emptyCollection(this);
+ return new this.Collection(this, function () { return createRange(lower, upper, !includeLower, !includeUpper); });
+ }
+ catch (e) {
+ return fail(this, INVALID_KEY_ARGUMENT);
+ }
+ };
+ WhereClause.prototype.equals = function (value) {
+ if (value == null)
+ return fail(this, INVALID_KEY_ARGUMENT);
+ return new this.Collection(this, function () { return rangeEqual(value); });
+ };
+ WhereClause.prototype.above = function (value) {
+ if (value == null)
+ return fail(this, INVALID_KEY_ARGUMENT);
+ return new this.Collection(this, function () { return createRange(value, undefined, true); });
+ };
+ WhereClause.prototype.aboveOrEqual = function (value) {
+ if (value == null)
+ return fail(this, INVALID_KEY_ARGUMENT);
+ return new this.Collection(this, function () { return createRange(value, undefined, false); });
+ };
+ WhereClause.prototype.below = function (value) {
+ if (value == null)
+ return fail(this, INVALID_KEY_ARGUMENT);
+ return new this.Collection(this, function () { return createRange(undefined, value, false, true); });
+ };
+ WhereClause.prototype.belowOrEqual = function (value) {
+ if (value == null)
+ return fail(this, INVALID_KEY_ARGUMENT);
+ return new this.Collection(this, function () { return createRange(undefined, value); });
+ };
+ WhereClause.prototype.startsWith = function (str) {
+ if (typeof str !== 'string')
+ return fail(this, STRING_EXPECTED);
+ return this.between(str, str + maxString, true, true);
+ };
+ WhereClause.prototype.startsWithIgnoreCase = function (str) {
+ if (str === "")
+ return this.startsWith(str);
+ return addIgnoreCaseAlgorithm(this, function (x, a) { return x.indexOf(a[0]) === 0; }, [str], maxString);
+ };
+ WhereClause.prototype.equalsIgnoreCase = function (str) {
+ return addIgnoreCaseAlgorithm(this, function (x, a) { return x === a[0]; }, [str], "");
+ };
+ WhereClause.prototype.anyOfIgnoreCase = function () {
+ var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
+ if (set.length === 0)
+ return emptyCollection(this);
+ return addIgnoreCaseAlgorithm(this, function (x, a) { return a.indexOf(x) !== -1; }, set, "");
+ };
+ WhereClause.prototype.startsWithAnyOfIgnoreCase = function () {
+ var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
+ if (set.length === 0)
+ return emptyCollection(this);
+ return addIgnoreCaseAlgorithm(this, function (x, a) { return a.some(function (n) { return x.indexOf(n) === 0; }); }, set, maxString);
+ };
+ WhereClause.prototype.anyOf = function () {
+ var _this = this;
+ var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
+ var compare = this._cmp;
+ try {
+ set.sort(compare);
+ }
+ catch (e) {
+ return fail(this, INVALID_KEY_ARGUMENT);
+ }
+ if (set.length === 0)
+ return emptyCollection(this);
+ var c = new this.Collection(this, function () { return createRange(set[0], set[set.length - 1]); });
+ c._ondirectionchange = function (direction) {
+ compare = (direction === "next" ?
+ _this._ascending :
+ _this._descending);
+ set.sort(compare);
+ };
+ var i = 0;
+ c._addAlgorithm(function (cursor, advance, resolve) {
+ var key = cursor.key;
+ while (compare(key, set[i]) > 0) {
+ ++i;
+ if (i === set.length) {
+ advance(resolve);
+ return false;
+ }
+ }
+ if (compare(key, set[i]) === 0) {
+ return true;
+ }
+ else {
+ advance(function () { cursor.continue(set[i]); });
+ return false;
+ }
+ });
+ return c;
+ };
+ WhereClause.prototype.notEqual = function (value) {
+ return this.inAnyRange([[minKey, value], [value, this.db._maxKey]], { includeLowers: false, includeUppers: false });
+ };
+ WhereClause.prototype.noneOf = function () {
+ var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
+ if (set.length === 0)
+ return new this.Collection(this);
+ try {
+ set.sort(this._ascending);
+ }
+ catch (e) {
+ return fail(this, INVALID_KEY_ARGUMENT);
+ }
+ var ranges = set.reduce(function (res, val) { return res ?
+ res.concat([[res[res.length - 1][1], val]]) :
+ [[minKey, val]]; }, null);
+ ranges.push([set[set.length - 1], this.db._maxKey]);
+ return this.inAnyRange(ranges, { includeLowers: false, includeUppers: false });
+ };
+ WhereClause.prototype.inAnyRange = function (ranges, options) {
+ var _this = this;
+ var cmp = this._cmp, ascending = this._ascending, descending = this._descending, min = this._min, max = this._max;
+ if (ranges.length === 0)
+ return emptyCollection(this);
+ if (!ranges.every(function (range) {
+ return range[0] !== undefined &&
+ range[1] !== undefined &&
+ ascending(range[0], range[1]) <= 0;
+ })) {
+ return fail(this, "First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower", exceptions.InvalidArgument);
+ }
+ var includeLowers = !options || options.includeLowers !== false;
+ var includeUppers = options && options.includeUppers === true;
+ function addRange(ranges, newRange) {
+ var i = 0, l = ranges.length;
+ for (; i < l; ++i) {
+ var range = ranges[i];
+ if (cmp(newRange[0], range[1]) < 0 && cmp(newRange[1], range[0]) > 0) {
+ range[0] = min(range[0], newRange[0]);
+ range[1] = max(range[1], newRange[1]);
+ break;
+ }
+ }
+ if (i === l)
+ ranges.push(newRange);
+ return ranges;
+ }
+ var sortDirection = ascending;
+ function rangeSorter(a, b) { return sortDirection(a[0], b[0]); }
+ var set;
+ try {
+ set = ranges.reduce(addRange, []);
+ set.sort(rangeSorter);
+ }
+ catch (ex) {
+ return fail(this, INVALID_KEY_ARGUMENT);
+ }
+ var rangePos = 0;
+ var keyIsBeyondCurrentEntry = includeUppers ?
+ function (key) { return ascending(key, set[rangePos][1]) > 0; } :
+ function (key) { return ascending(key, set[rangePos][1]) >= 0; };
+ var keyIsBeforeCurrentEntry = includeLowers ?
+ function (key) { return descending(key, set[rangePos][0]) > 0; } :
+ function (key) { return descending(key, set[rangePos][0]) >= 0; };
+ function keyWithinCurrentRange(key) {
+ return !keyIsBeyondCurrentEntry(key) && !keyIsBeforeCurrentEntry(key);
+ }
+ var checkKey = keyIsBeyondCurrentEntry;
+ var c = new this.Collection(this, function () { return createRange(set[0][0], set[set.length - 1][1], !includeLowers, !includeUppers); });
+ c._ondirectionchange = function (direction) {
+ if (direction === "next") {
+ checkKey = keyIsBeyondCurrentEntry;
+ sortDirection = ascending;
+ }
+ else {
+ checkKey = keyIsBeforeCurrentEntry;
+ sortDirection = descending;
+ }
+ set.sort(rangeSorter);
+ };
+ c._addAlgorithm(function (cursor, advance, resolve) {
+ var key = cursor.key;
+ while (checkKey(key)) {
+ ++rangePos;
+ if (rangePos === set.length) {
+ advance(resolve);
+ return false;
+ }
+ }
+ if (keyWithinCurrentRange(key)) {
+ return true;
+ }
+ else if (_this._cmp(key, set[rangePos][1]) === 0 || _this._cmp(key, set[rangePos][0]) === 0) {
+ return false;
+ }
+ else {
+ advance(function () {
+ if (sortDirection === ascending)
+ cursor.continue(set[rangePos][0]);
+ else
+ cursor.continue(set[rangePos][1]);
+ });
+ return false;
+ }
+ });
+ return c;
+ };
+ WhereClause.prototype.startsWithAnyOf = function () {
+ var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
+ if (!set.every(function (s) { return typeof s === 'string'; })) {
+ return fail(this, "startsWithAnyOf() only works with strings");
+ }
+ if (set.length === 0)
+ return emptyCollection(this);
+ return this.inAnyRange(set.map(function (str) { return [str, str + maxString]; }));
+ };
+ return WhereClause;
+}());
+
+function createWhereClauseConstructor(db) {
+ return makeClassConstructor(WhereClause.prototype, function WhereClause(table, index, orCollection) {
+ this.db = db;
+ this._ctx = {
+ table: table,
+ index: index === ":id" ? null : index,
+ or: orCollection
+ };
+ var indexedDB = db._deps.indexedDB;
+ if (!indexedDB)
+ throw new exceptions.MissingAPI();
+ this._cmp = this._ascending = indexedDB.cmp.bind(indexedDB);
+ this._descending = function (a, b) { return indexedDB.cmp(b, a); };
+ this._max = function (a, b) { return indexedDB.cmp(a, b) > 0 ? a : b; };
+ this._min = function (a, b) { return indexedDB.cmp(a, b) < 0 ? a : b; };
+ this._IDBKeyRange = db._deps.IDBKeyRange;
+ });
+}
+
+function eventRejectHandler(reject) {
+ return wrap(function (event) {
+ preventDefault(event);
+ reject(event.target.error);
+ return false;
+ });
+}
+function preventDefault(event) {
+ if (event.stopPropagation)
+ event.stopPropagation();
+ if (event.preventDefault)
+ event.preventDefault();
+}
+
+var DEXIE_STORAGE_MUTATED_EVENT_NAME = 'storagemutated';
+var STORAGE_MUTATED_DOM_EVENT_NAME = 'x-storagemutated-1';
+var globalEvents = Events(null, DEXIE_STORAGE_MUTATED_EVENT_NAME);
+
+var Transaction = (function () {
+ function Transaction() {
+ }
+ Transaction.prototype._lock = function () {
+ assert(!PSD.global);
+ ++this._reculock;
+ if (this._reculock === 1 && !PSD.global)
+ PSD.lockOwnerFor = this;
+ return this;
+ };
+ Transaction.prototype._unlock = function () {
+ assert(!PSD.global);
+ if (--this._reculock === 0) {
+ if (!PSD.global)
+ PSD.lockOwnerFor = null;
+ while (this._blockedFuncs.length > 0 && !this._locked()) {
+ var fnAndPSD = this._blockedFuncs.shift();
+ try {
+ usePSD(fnAndPSD[1], fnAndPSD[0]);
+ }
+ catch (e) { }
+ }
+ }
+ return this;
+ };
+ Transaction.prototype._locked = function () {
+ return this._reculock && PSD.lockOwnerFor !== this;
+ };
+ Transaction.prototype.create = function (idbtrans) {
+ var _this = this;
+ if (!this.mode)
+ return this;
+ var idbdb = this.db.idbdb;
+ var dbOpenError = this.db._state.dbOpenError;
+ assert(!this.idbtrans);
+ if (!idbtrans && !idbdb) {
+ switch (dbOpenError && dbOpenError.name) {
+ case "DatabaseClosedError":
+ throw new exceptions.DatabaseClosed(dbOpenError);
+ case "MissingAPIError":
+ throw new exceptions.MissingAPI(dbOpenError.message, dbOpenError);
+ default:
+ throw new exceptions.OpenFailed(dbOpenError);
+ }
+ }
+ if (!this.active)
+ throw new exceptions.TransactionInactive();
+ assert(this._completion._state === null);
+ idbtrans = this.idbtrans = idbtrans ||
+ (this.db.core
+ ? this.db.core.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability })
+ : idbdb.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability }));
+ idbtrans.onerror = wrap(function (ev) {
+ preventDefault(ev);
+ _this._reject(idbtrans.error);
+ });
+ idbtrans.onabort = wrap(function (ev) {
+ preventDefault(ev);
+ _this.active && _this._reject(new exceptions.Abort(idbtrans.error));
+ _this.active = false;
+ _this.on("abort").fire(ev);
+ });
+ idbtrans.oncomplete = wrap(function () {
+ _this.active = false;
+ _this._resolve();
+ if ('mutatedParts' in idbtrans) {
+ globalEvents.storagemutated.fire(idbtrans["mutatedParts"]);
+ }
+ });
+ return this;
+ };
+ Transaction.prototype._promise = function (mode, fn, bWriteLock) {
+ var _this = this;
+ if (mode === 'readwrite' && this.mode !== 'readwrite')
+ return rejection(new exceptions.ReadOnly("Transaction is readonly"));
+ if (!this.active)
+ return rejection(new exceptions.TransactionInactive());
+ if (this._locked()) {
+ return new DexiePromise(function (resolve, reject) {
+ _this._blockedFuncs.push([function () {
+ _this._promise(mode, fn, bWriteLock).then(resolve, reject);
+ }, PSD]);
+ });
+ }
+ else if (bWriteLock) {
+ return newScope(function () {
+ var p = new DexiePromise(function (resolve, reject) {
+ _this._lock();
+ var rv = fn(resolve, reject, _this);
+ if (rv && rv.then)
+ rv.then(resolve, reject);
+ });
+ p.finally(function () { return _this._unlock(); });
+ p._lib = true;
+ return p;
+ });
+ }
+ else {
+ var p = new DexiePromise(function (resolve, reject) {
+ var rv = fn(resolve, reject, _this);
+ if (rv && rv.then)
+ rv.then(resolve, reject);
+ });
+ p._lib = true;
+ return p;
+ }
+ };
+ Transaction.prototype._root = function () {
+ return this.parent ? this.parent._root() : this;
+ };
+ Transaction.prototype.waitFor = function (promiseLike) {
+ var root = this._root();
+ var promise = DexiePromise.resolve(promiseLike);
+ if (root._waitingFor) {
+ root._waitingFor = root._waitingFor.then(function () { return promise; });
+ }
+ else {
+ root._waitingFor = promise;
+ root._waitingQueue = [];
+ var store = root.idbtrans.objectStore(root.storeNames[0]);
+ (function spin() {
+ ++root._spinCount;
+ while (root._waitingQueue.length)
+ (root._waitingQueue.shift())();
+ if (root._waitingFor)
+ store.get(-Infinity).onsuccess = spin;
+ }());
+ }
+ var currentWaitPromise = root._waitingFor;
+ return new DexiePromise(function (resolve, reject) {
+ promise.then(function (res) { return root._waitingQueue.push(wrap(resolve.bind(null, res))); }, function (err) { return root._waitingQueue.push(wrap(reject.bind(null, err))); }).finally(function () {
+ if (root._waitingFor === currentWaitPromise) {
+ root._waitingFor = null;
+ }
+ });
+ });
+ };
+ Transaction.prototype.abort = function () {
+ if (this.active) {
+ this.active = false;
+ if (this.idbtrans)
+ this.idbtrans.abort();
+ this._reject(new exceptions.Abort());
+ }
+ };
+ Transaction.prototype.table = function (tableName) {
+ var memoizedTables = (this._memoizedTables || (this._memoizedTables = {}));
+ if (hasOwn(memoizedTables, tableName))
+ return memoizedTables[tableName];
+ var tableSchema = this.schema[tableName];
+ if (!tableSchema) {
+ throw new exceptions.NotFound("Table " + tableName + " not part of transaction");
+ }
+ var transactionBoundTable = new this.db.Table(tableName, tableSchema, this);
+ transactionBoundTable.core = this.db.core.table(tableName);
+ memoizedTables[tableName] = transactionBoundTable;
+ return transactionBoundTable;
+ };
+ return Transaction;
+}());
+
+function createTransactionConstructor(db) {
+ return makeClassConstructor(Transaction.prototype, function Transaction(mode, storeNames, dbschema, chromeTransactionDurability, parent) {
+ var _this = this;
+ this.db = db;
+ this.mode = mode;
+ this.storeNames = storeNames;
+ this.schema = dbschema;
+ this.chromeTransactionDurability = chromeTransactionDurability;
+ this.idbtrans = null;
+ this.on = Events(this, "complete", "error", "abort");
+ this.parent = parent || null;
+ this.active = true;
+ this._reculock = 0;
+ this._blockedFuncs = [];
+ this._resolve = null;
+ this._reject = null;
+ this._waitingFor = null;
+ this._waitingQueue = null;
+ this._spinCount = 0;
+ this._completion = new DexiePromise(function (resolve, reject) {
+ _this._resolve = resolve;
+ _this._reject = reject;
+ });
+ this._completion.then(function () {
+ _this.active = false;
+ _this.on.complete.fire();
+ }, function (e) {
+ var wasActive = _this.active;
+ _this.active = false;
+ _this.on.error.fire(e);
+ _this.parent ?
+ _this.parent._reject(e) :
+ wasActive && _this.idbtrans && _this.idbtrans.abort();
+ return rejection(e);
+ });
+ });
+}
+
+function createIndexSpec(name, keyPath, unique, multi, auto, compound, isPrimKey) {
+ return {
+ name: name,
+ keyPath: keyPath,
+ unique: unique,
+ multi: multi,
+ auto: auto,
+ compound: compound,
+ src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath)
+ };
+}
+function nameFromKeyPath(keyPath) {
+ return typeof keyPath === 'string' ?
+ keyPath :
+ keyPath ? ('[' + [].join.call(keyPath, '+') + ']') : "";
+}
+
+function createTableSchema(name, primKey, indexes) {
+ return {
+ name: name,
+ primKey: primKey,
+ indexes: indexes,
+ mappedClass: null,
+ idxByName: arrayToObject(indexes, function (index) { return [index.name, index]; })
+ };
+}
+
+function safariMultiStoreFix(storeNames) {
+ return storeNames.length === 1 ? storeNames[0] : storeNames;
+}
+var getMaxKey = function (IdbKeyRange) {
+ try {
+ IdbKeyRange.only([[]]);
+ getMaxKey = function () { return [[]]; };
+ return [[]];
+ }
+ catch (e) {
+ getMaxKey = function () { return maxString; };
+ return maxString;
+ }
+};
+
+function getKeyExtractor(keyPath) {
+ if (keyPath == null) {
+ return function () { return undefined; };
+ }
+ else if (typeof keyPath === 'string') {
+ return getSinglePathKeyExtractor(keyPath);
+ }
+ else {
+ return function (obj) { return getByKeyPath(obj, keyPath); };
+ }
+}
+function getSinglePathKeyExtractor(keyPath) {
+ var split = keyPath.split('.');
+ if (split.length === 1) {
+ return function (obj) { return obj[keyPath]; };
+ }
+ else {
+ return function (obj) { return getByKeyPath(obj, keyPath); };
+ }
+}
+
+function arrayify(arrayLike) {
+ return [].slice.call(arrayLike);
+}
+var _id_counter = 0;
+function getKeyPathAlias(keyPath) {
+ return keyPath == null ?
+ ":id" :
+ typeof keyPath === 'string' ?
+ keyPath :
+ "[" + keyPath.join('+') + "]";
+}
+function createDBCore(db, IdbKeyRange, tmpTrans) {
+ function extractSchema(db, trans) {
+ var tables = arrayify(db.objectStoreNames);
+ return {
+ schema: {
+ name: db.name,
+ tables: tables.map(function (table) { return trans.objectStore(table); }).map(function (store) {
+ var keyPath = store.keyPath, autoIncrement = store.autoIncrement;
+ var compound = isArray(keyPath);
+ var outbound = keyPath == null;
+ var indexByKeyPath = {};
+ var result = {
+ name: store.name,
+ primaryKey: {
+ name: null,
+ isPrimaryKey: true,
+ outbound: outbound,
+ compound: compound,
+ keyPath: keyPath,
+ autoIncrement: autoIncrement,
+ unique: true,
+ extractKey: getKeyExtractor(keyPath)
+ },
+ indexes: arrayify(store.indexNames).map(function (indexName) { return store.index(indexName); })
+ .map(function (index) {
+ var name = index.name, unique = index.unique, multiEntry = index.multiEntry, keyPath = index.keyPath;
+ var compound = isArray(keyPath);
+ var result = {
+ name: name,
+ compound: compound,
+ keyPath: keyPath,
+ unique: unique,
+ multiEntry: multiEntry,
+ extractKey: getKeyExtractor(keyPath)
+ };
+ indexByKeyPath[getKeyPathAlias(keyPath)] = result;
+ return result;
+ }),
+ getIndexByKeyPath: function (keyPath) { return indexByKeyPath[getKeyPathAlias(keyPath)]; }
+ };
+ indexByKeyPath[":id"] = result.primaryKey;
+ if (keyPath != null) {
+ indexByKeyPath[getKeyPathAlias(keyPath)] = result.primaryKey;
+ }
+ return result;
+ })
+ },
+ hasGetAll: tables.length > 0 && ('getAll' in trans.objectStore(tables[0])) &&
+ !(typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) &&
+ !/(Chrome\/|Edge\/)/.test(navigator.userAgent) &&
+ [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604)
+ };
+ }
+ function makeIDBKeyRange(range) {
+ if (range.type === 3 )
+ return null;
+ if (range.type === 4 )
+ throw new Error("Cannot convert never type to IDBKeyRange");
+ var lower = range.lower, upper = range.upper, lowerOpen = range.lowerOpen, upperOpen = range.upperOpen;
+ var idbRange = lower === undefined ?
+ upper === undefined ?
+ null :
+ IdbKeyRange.upperBound(upper, !!upperOpen) :
+ upper === undefined ?
+ IdbKeyRange.lowerBound(lower, !!lowerOpen) :
+ IdbKeyRange.bound(lower, upper, !!lowerOpen, !!upperOpen);
+ return idbRange;
+ }
+ function createDbCoreTable(tableSchema) {
+ var tableName = tableSchema.name;
+ function mutate(_a) {
+ var trans = _a.trans, type = _a.type, keys = _a.keys, values = _a.values, range = _a.range;
+ return new Promise(function (resolve, reject) {
+ resolve = wrap(resolve);
+ var store = trans.objectStore(tableName);
+ var outbound = store.keyPath == null;
+ var isAddOrPut = type === "put" || type === "add";
+ if (!isAddOrPut && type !== 'delete' && type !== 'deleteRange')
+ throw new Error("Invalid operation type: " + type);
+ var length = (keys || values || { length: 1 }).length;
+ if (keys && values && keys.length !== values.length) {
+ throw new Error("Given keys array must have same length as given values array.");
+ }
+ if (length === 0)
+ return resolve({ numFailures: 0, failures: {}, results: [], lastResult: undefined });
+ var req;
+ var reqs = [];
+ var failures = [];
+ var numFailures = 0;
+ var errorHandler = function (event) {
+ ++numFailures;
+ preventDefault(event);
+ };
+ if (type === 'deleteRange') {
+ if (range.type === 4 )
+ return resolve({ numFailures: numFailures, failures: failures, results: [], lastResult: undefined });
+ if (range.type === 3 )
+ reqs.push(req = store.clear());
+ else
+ reqs.push(req = store.delete(makeIDBKeyRange(range)));
+ }
+ else {
+ var _a = isAddOrPut ?
+ outbound ?
+ [values, keys] :
+ [values, null] :
+ [keys, null], args1 = _a[0], args2 = _a[1];
+ if (isAddOrPut) {
+ for (var i = 0; i < length; ++i) {
+ reqs.push(req = (args2 && args2[i] !== undefined ?
+ store[type](args1[i], args2[i]) :
+ store[type](args1[i])));
+ req.onerror = errorHandler;
+ }
+ }
+ else {
+ for (var i = 0; i < length; ++i) {
+ reqs.push(req = store[type](args1[i]));
+ req.onerror = errorHandler;
+ }
+ }
+ }
+ var done = function (event) {
+ var lastResult = event.target.result;
+ reqs.forEach(function (req, i) { return req.error != null && (failures[i] = req.error); });
+ resolve({
+ numFailures: numFailures,
+ failures: failures,
+ results: type === "delete" ? keys : reqs.map(function (req) { return req.result; }),
+ lastResult: lastResult
+ });
+ };
+ req.onerror = function (event) {
+ errorHandler(event);
+ done(event);
+ };
+ req.onsuccess = done;
+ });
+ }
+ function openCursor(_a) {
+ var trans = _a.trans, values = _a.values, query = _a.query, reverse = _a.reverse, unique = _a.unique;
+ return new Promise(function (resolve, reject) {
+ resolve = wrap(resolve);
+ var index = query.index, range = query.range;
+ var store = trans.objectStore(tableName);
+ var source = index.isPrimaryKey ?
+ store :
+ store.index(index.name);
+ var direction = reverse ?
+ unique ?
+ "prevunique" :
+ "prev" :
+ unique ?
+ "nextunique" :
+ "next";
+ var req = values || !('openKeyCursor' in source) ?
+ source.openCursor(makeIDBKeyRange(range), direction) :
+ source.openKeyCursor(makeIDBKeyRange(range), direction);
+ req.onerror = eventRejectHandler(reject);
+ req.onsuccess = wrap(function (ev) {
+ var cursor = req.result;
+ if (!cursor) {
+ resolve(null);
+ return;
+ }
+ cursor.___id = ++_id_counter;
+ cursor.done = false;
+ var _cursorContinue = cursor.continue.bind(cursor);
+ var _cursorContinuePrimaryKey = cursor.continuePrimaryKey;
+ if (_cursorContinuePrimaryKey)
+ _cursorContinuePrimaryKey = _cursorContinuePrimaryKey.bind(cursor);
+ var _cursorAdvance = cursor.advance.bind(cursor);
+ var doThrowCursorIsNotStarted = function () { throw new Error("Cursor not started"); };
+ var doThrowCursorIsStopped = function () { throw new Error("Cursor not stopped"); };
+ cursor.trans = trans;
+ cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsNotStarted;
+ cursor.fail = wrap(reject);
+ cursor.next = function () {
+ var _this = this;
+ var gotOne = 1;
+ return this.start(function () { return gotOne-- ? _this.continue() : _this.stop(); }).then(function () { return _this; });
+ };
+ cursor.start = function (callback) {
+ var iterationPromise = new Promise(function (resolveIteration, rejectIteration) {
+ resolveIteration = wrap(resolveIteration);
+ req.onerror = eventRejectHandler(rejectIteration);
+ cursor.fail = rejectIteration;
+ cursor.stop = function (value) {
+ cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsStopped;
+ resolveIteration(value);
+ };
+ });
+ var guardedCallback = function () {
+ if (req.result) {
+ try {
+ callback();
+ }
+ catch (err) {
+ cursor.fail(err);
+ }
+ }
+ else {
+ cursor.done = true;
+ cursor.start = function () { throw new Error("Cursor behind last entry"); };
+ cursor.stop();
+ }
+ };
+ req.onsuccess = wrap(function (ev) {
+ req.onsuccess = guardedCallback;
+ guardedCallback();
+ });
+ cursor.continue = _cursorContinue;
+ cursor.continuePrimaryKey = _cursorContinuePrimaryKey;
+ cursor.advance = _cursorAdvance;
+ guardedCallback();
+ return iterationPromise;
+ };
+ resolve(cursor);
+ }, reject);
+ });
+ }
+ function query(hasGetAll) {
+ return function (request) {
+ return new Promise(function (resolve, reject) {
+ resolve = wrap(resolve);
+ var trans = request.trans, values = request.values, limit = request.limit, query = request.query;
+ var nonInfinitLimit = limit === Infinity ? undefined : limit;
+ var index = query.index, range = query.range;
+ var store = trans.objectStore(tableName);
+ var source = index.isPrimaryKey ? store : store.index(index.name);
+ var idbKeyRange = makeIDBKeyRange(range);
+ if (limit === 0)
+ return resolve({ result: [] });
+ if (hasGetAll) {
+ var req = values ?
+ source.getAll(idbKeyRange, nonInfinitLimit) :
+ source.getAllKeys(idbKeyRange, nonInfinitLimit);
+ req.onsuccess = function (event) { return resolve({ result: event.target.result }); };
+ req.onerror = eventRejectHandler(reject);
+ }
+ else {
+ var count_1 = 0;
+ var req_1 = values || !('openKeyCursor' in source) ?
+ source.openCursor(idbKeyRange) :
+ source.openKeyCursor(idbKeyRange);
+ var result_1 = [];
+ req_1.onsuccess = function (event) {
+ var cursor = req_1.result;
+ if (!cursor)
+ return resolve({ result: result_1 });
+ result_1.push(values ? cursor.value : cursor.primaryKey);
+ if (++count_1 === limit)
+ return resolve({ result: result_1 });
+ cursor.continue();
+ };
+ req_1.onerror = eventRejectHandler(reject);
+ }
+ });
+ };
+ }
+ return {
+ name: tableName,
+ schema: tableSchema,
+ mutate: mutate,
+ getMany: function (_a) {
+ var trans = _a.trans, keys = _a.keys;
+ return new Promise(function (resolve, reject) {
+ resolve = wrap(resolve);
+ var store = trans.objectStore(tableName);
+ var length = keys.length;
+ var result = new Array(length);
+ var keyCount = 0;
+ var callbackCount = 0;
+ var req;
+ var successHandler = function (event) {
+ var req = event.target;
+ if ((result[req._pos] = req.result) != null)
+ ;
+ if (++callbackCount === keyCount)
+ resolve(result);
+ };
+ var errorHandler = eventRejectHandler(reject);
+ for (var i = 0; i < length; ++i) {
+ var key = keys[i];
+ if (key != null) {
+ req = store.get(keys[i]);
+ req._pos = i;
+ req.onsuccess = successHandler;
+ req.onerror = errorHandler;
+ ++keyCount;
+ }
+ }
+ if (keyCount === 0)
+ resolve(result);
+ });
+ },
+ get: function (_a) {
+ var trans = _a.trans, key = _a.key;
+ return new Promise(function (resolve, reject) {
+ resolve = wrap(resolve);
+ var store = trans.objectStore(tableName);
+ var req = store.get(key);
+ req.onsuccess = function (event) { return resolve(event.target.result); };
+ req.onerror = eventRejectHandler(reject);
+ });
+ },
+ query: query(hasGetAll),
+ openCursor: openCursor,
+ count: function (_a) {
+ var query = _a.query, trans = _a.trans;
+ var index = query.index, range = query.range;
+ return new Promise(function (resolve, reject) {
+ var store = trans.objectStore(tableName);
+ var source = index.isPrimaryKey ? store : store.index(index.name);
+ var idbKeyRange = makeIDBKeyRange(range);
+ var req = idbKeyRange ? source.count(idbKeyRange) : source.count();
+ req.onsuccess = wrap(function (ev) { return resolve(ev.target.result); });
+ req.onerror = eventRejectHandler(reject);
+ });
+ }
+ };
+ }
+ var _a = extractSchema(db, tmpTrans), schema = _a.schema, hasGetAll = _a.hasGetAll;
+ var tables = schema.tables.map(function (tableSchema) { return createDbCoreTable(tableSchema); });
+ var tableMap = {};
+ tables.forEach(function (table) { return tableMap[table.name] = table; });
+ return {
+ stack: "dbcore",
+ transaction: db.transaction.bind(db),
+ table: function (name) {
+ var result = tableMap[name];
+ if (!result)
+ throw new Error("Table '" + name + "' not found");
+ return tableMap[name];
+ },
+ MIN_KEY: -Infinity,
+ MAX_KEY: getMaxKey(IdbKeyRange),
+ schema: schema
+ };
+}
+
+function createMiddlewareStack(stackImpl, middlewares) {
+ return middlewares.reduce(function (down, _a) {
+ var create = _a.create;
+ return (__assign(__assign({}, down), create(down)));
+ }, stackImpl);
+}
+function createMiddlewareStacks(middlewares, idbdb, _a, tmpTrans) {
+ var IDBKeyRange = _a.IDBKeyRange; _a.indexedDB;
+ var dbcore = createMiddlewareStack(createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore);
+ return {
+ dbcore: dbcore
+ };
+}
+function generateMiddlewareStacks(_a, tmpTrans) {
+ var db = _a._novip;
+ var idbdb = tmpTrans.db;
+ var stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans);
+ db.core = stacks.dbcore;
+ db.tables.forEach(function (table) {
+ var tableName = table.name;
+ if (db.core.schema.tables.some(function (tbl) { return tbl.name === tableName; })) {
+ table.core = db.core.table(tableName);
+ if (db[tableName] instanceof db.Table) {
+ db[tableName].core = table.core;
+ }
+ }
+ });
+}
+
+function setApiOnPlace(_a, objs, tableNames, dbschema) {
+ var db = _a._novip;
+ tableNames.forEach(function (tableName) {
+ var schema = dbschema[tableName];
+ objs.forEach(function (obj) {
+ var propDesc = getPropertyDescriptor(obj, tableName);
+ if (!propDesc || ("value" in propDesc && propDesc.value === undefined)) {
+ if (obj === db.Transaction.prototype || obj instanceof db.Transaction) {
+ setProp(obj, tableName, {
+ get: function () { return this.table(tableName); },
+ set: function (value) {
+ defineProperty(this, tableName, { value: value, writable: true, configurable: true, enumerable: true });
+ }
+ });
+ }
+ else {
+ obj[tableName] = new db.Table(tableName, schema);
+ }
+ }
+ });
+ });
+}
+function removeTablesApi(_a, objs) {
+ var db = _a._novip;
+ objs.forEach(function (obj) {
+ for (var key in obj) {
+ if (obj[key] instanceof db.Table)
+ delete obj[key];
+ }
+ });
+}
+function lowerVersionFirst(a, b) {
+ return a._cfg.version - b._cfg.version;
+}
+function runUpgraders(db, oldVersion, idbUpgradeTrans, reject) {
+ var globalSchema = db._dbSchema;
+ var trans = db._createTransaction('readwrite', db._storeNames, globalSchema);
+ trans.create(idbUpgradeTrans);
+ trans._completion.catch(reject);
+ var rejectTransaction = trans._reject.bind(trans);
+ var transless = PSD.transless || PSD;
+ newScope(function () {
+ PSD.trans = trans;
+ PSD.transless = transless;
+ if (oldVersion === 0) {
+ keys(globalSchema).forEach(function (tableName) {
+ createTable(idbUpgradeTrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes);
+ });
+ generateMiddlewareStacks(db, idbUpgradeTrans);
+ DexiePromise.follow(function () { return db.on.populate.fire(trans); }).catch(rejectTransaction);
+ }
+ else
+ updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans).catch(rejectTransaction);
+ });
+}
+function updateTablesAndIndexes(_a, oldVersion, trans, idbUpgradeTrans) {
+ var db = _a._novip;
+ var queue = [];
+ var versions = db._versions;
+ var globalSchema = db._dbSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans);
+ var anyContentUpgraderHasRun = false;
+ var versToRun = versions.filter(function (v) { return v._cfg.version >= oldVersion; });
+ versToRun.forEach(function (version) {
+ queue.push(function () {
+ var oldSchema = globalSchema;
+ var newSchema = version._cfg.dbschema;
+ adjustToExistingIndexNames(db, oldSchema, idbUpgradeTrans);
+ adjustToExistingIndexNames(db, newSchema, idbUpgradeTrans);
+ globalSchema = db._dbSchema = newSchema;
+ var diff = getSchemaDiff(oldSchema, newSchema);
+ diff.add.forEach(function (tuple) {
+ createTable(idbUpgradeTrans, tuple[0], tuple[1].primKey, tuple[1].indexes);
+ });
+ diff.change.forEach(function (change) {
+ if (change.recreate) {
+ throw new exceptions.Upgrade("Not yet support for changing primary key");
+ }
+ else {
+ var store_1 = idbUpgradeTrans.objectStore(change.name);
+ change.add.forEach(function (idx) { return addIndex(store_1, idx); });
+ change.change.forEach(function (idx) {
+ store_1.deleteIndex(idx.name);
+ addIndex(store_1, idx);
+ });
+ change.del.forEach(function (idxName) { return store_1.deleteIndex(idxName); });
+ }
+ });
+ var contentUpgrade = version._cfg.contentUpgrade;
+ if (contentUpgrade && version._cfg.version > oldVersion) {
+ generateMiddlewareStacks(db, idbUpgradeTrans);
+ trans._memoizedTables = {};
+ anyContentUpgraderHasRun = true;
+ var upgradeSchema_1 = shallowClone(newSchema);
+ diff.del.forEach(function (table) {
+ upgradeSchema_1[table] = oldSchema[table];
+ });
+ removeTablesApi(db, [db.Transaction.prototype]);
+ setApiOnPlace(db, [db.Transaction.prototype], keys(upgradeSchema_1), upgradeSchema_1);
+ trans.schema = upgradeSchema_1;
+ var contentUpgradeIsAsync_1 = isAsyncFunction(contentUpgrade);
+ if (contentUpgradeIsAsync_1) {
+ incrementExpectedAwaits();
+ }
+ var returnValue_1;
+ var promiseFollowed = DexiePromise.follow(function () {
+ returnValue_1 = contentUpgrade(trans);
+ if (returnValue_1) {
+ if (contentUpgradeIsAsync_1) {
+ var decrementor = decrementExpectedAwaits.bind(null, null);
+ returnValue_1.then(decrementor, decrementor);
+ }
+ }
+ });
+ return (returnValue_1 && typeof returnValue_1.then === 'function' ?
+ DexiePromise.resolve(returnValue_1) : promiseFollowed.then(function () { return returnValue_1; }));
+ }
+ });
+ queue.push(function (idbtrans) {
+ if (!anyContentUpgraderHasRun || !hasIEDeleteObjectStoreBug) {
+ var newSchema = version._cfg.dbschema;
+ deleteRemovedTables(newSchema, idbtrans);
+ }
+ removeTablesApi(db, [db.Transaction.prototype]);
+ setApiOnPlace(db, [db.Transaction.prototype], db._storeNames, db._dbSchema);
+ trans.schema = db._dbSchema;
+ });
+ });
+ function runQueue() {
+ return queue.length ? DexiePromise.resolve(queue.shift()(trans.idbtrans)).then(runQueue) :
+ DexiePromise.resolve();
+ }
+ return runQueue().then(function () {
+ createMissingTables(globalSchema, idbUpgradeTrans);
+ });
+}
+function getSchemaDiff(oldSchema, newSchema) {
+ var diff = {
+ del: [],
+ add: [],
+ change: []
+ };
+ var table;
+ for (table in oldSchema) {
+ if (!newSchema[table])
+ diff.del.push(table);
+ }
+ for (table in newSchema) {
+ var oldDef = oldSchema[table], newDef = newSchema[table];
+ if (!oldDef) {
+ diff.add.push([table, newDef]);
+ }
+ else {
+ var change = {
+ name: table,
+ def: newDef,
+ recreate: false,
+ del: [],
+ add: [],
+ change: []
+ };
+ if ((
+ '' + (oldDef.primKey.keyPath || '')) !== ('' + (newDef.primKey.keyPath || '')) ||
+ (oldDef.primKey.auto !== newDef.primKey.auto && !isIEOrEdge))
+ {
+ change.recreate = true;
+ diff.change.push(change);
+ }
+ else {
+ var oldIndexes = oldDef.idxByName;
+ var newIndexes = newDef.idxByName;
+ var idxName = void 0;
+ for (idxName in oldIndexes) {
+ if (!newIndexes[idxName])
+ change.del.push(idxName);
+ }
+ for (idxName in newIndexes) {
+ var oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName];
+ if (!oldIdx)
+ change.add.push(newIdx);
+ else if (oldIdx.src !== newIdx.src)
+ change.change.push(newIdx);
+ }
+ if (change.del.length > 0 || change.add.length > 0 || change.change.length > 0) {
+ diff.change.push(change);
+ }
+ }
+ }
+ }
+ return diff;
+}
+function createTable(idbtrans, tableName, primKey, indexes) {
+ var store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ?
+ { keyPath: primKey.keyPath, autoIncrement: primKey.auto } :
+ { autoIncrement: primKey.auto });
+ indexes.forEach(function (idx) { return addIndex(store, idx); });
+ return store;
+}
+function createMissingTables(newSchema, idbtrans) {
+ keys(newSchema).forEach(function (tableName) {
+ if (!idbtrans.db.objectStoreNames.contains(tableName)) {
+ createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes);
+ }
+ });
+}
+function deleteRemovedTables(newSchema, idbtrans) {
+ [].slice.call(idbtrans.db.objectStoreNames).forEach(function (storeName) {
+ return newSchema[storeName] == null && idbtrans.db.deleteObjectStore(storeName);
+ });
+}
+function addIndex(store, idx) {
+ store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi });
+}
+function buildGlobalSchema(db, idbdb, tmpTrans) {
+ var globalSchema = {};
+ var dbStoreNames = slice(idbdb.objectStoreNames, 0);
+ dbStoreNames.forEach(function (storeName) {
+ var store = tmpTrans.objectStore(storeName);
+ var keyPath = store.keyPath;
+ var primKey = createIndexSpec(nameFromKeyPath(keyPath), keyPath || "", false, false, !!store.autoIncrement, keyPath && typeof keyPath !== "string", true);
+ var indexes = [];
+ for (var j = 0; j < store.indexNames.length; ++j) {
+ var idbindex = store.index(store.indexNames[j]);
+ keyPath = idbindex.keyPath;
+ var index = createIndexSpec(idbindex.name, keyPath, !!idbindex.unique, !!idbindex.multiEntry, false, keyPath && typeof keyPath !== "string", false);
+ indexes.push(index);
+ }
+ globalSchema[storeName] = createTableSchema(storeName, primKey, indexes);
+ });
+ return globalSchema;
+}
+function readGlobalSchema(_a, idbdb, tmpTrans) {
+ var db = _a._novip;
+ db.verno = idbdb.version / 10;
+ var globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans);
+ db._storeNames = slice(idbdb.objectStoreNames, 0);
+ setApiOnPlace(db, [db._allTables], keys(globalSchema), globalSchema);
+}
+function verifyInstalledSchema(db, tmpTrans) {
+ var installedSchema = buildGlobalSchema(db, db.idbdb, tmpTrans);
+ var diff = getSchemaDiff(installedSchema, db._dbSchema);
+ return !(diff.add.length || diff.change.some(function (ch) { return ch.add.length || ch.change.length; }));
+}
+function adjustToExistingIndexNames(_a, schema, idbtrans) {
+ var db = _a._novip;
+ var storeNames = idbtrans.db.objectStoreNames;
+ for (var i = 0; i < storeNames.length; ++i) {
+ var storeName = storeNames[i];
+ var store = idbtrans.objectStore(storeName);
+ db._hasGetAll = 'getAll' in store;
+ for (var j = 0; j < store.indexNames.length; ++j) {
+ var indexName = store.indexNames[j];
+ var keyPath = store.index(indexName).keyPath;
+ var dexieName = typeof keyPath === 'string' ? keyPath : "[" + slice(keyPath).join('+') + "]";
+ if (schema[storeName]) {
+ var indexSpec = schema[storeName].idxByName[dexieName];
+ if (indexSpec) {
+ indexSpec.name = indexName;
+ delete schema[storeName].idxByName[dexieName];
+ schema[storeName].idxByName[indexName] = indexSpec;
+ }
+ }
+ }
+ }
+ if (typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) &&
+ !/(Chrome\/|Edge\/)/.test(navigator.userAgent) &&
+ _global.WorkerGlobalScope && _global instanceof _global.WorkerGlobalScope &&
+ [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) {
+ db._hasGetAll = false;
+ }
+}
+function parseIndexSyntax(primKeyAndIndexes) {
+ return primKeyAndIndexes.split(',').map(function (index, indexNum) {
+ index = index.trim();
+ var name = index.replace(/([&*]|\+\+)/g, "");
+ var keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split('+') : name;
+ return createIndexSpec(name, keyPath || null, /\&/.test(index), /\*/.test(index), /\+\+/.test(index), isArray(keyPath), indexNum === 0);
+ });
+}
+
+var Version = (function () {
+ function Version() {
+ }
+ Version.prototype._parseStoresSpec = function (stores, outSchema) {
+ keys(stores).forEach(function (tableName) {
+ if (stores[tableName] !== null) {
+ var indexes = parseIndexSyntax(stores[tableName]);
+ var primKey = indexes.shift();
+ if (primKey.multi)
+ throw new exceptions.Schema("Primary key cannot be multi-valued");
+ indexes.forEach(function (idx) {
+ if (idx.auto)
+ throw new exceptions.Schema("Only primary key can be marked as autoIncrement (++)");
+ if (!idx.keyPath)
+ throw new exceptions.Schema("Index must have a name and cannot be an empty string");
+ });
+ outSchema[tableName] = createTableSchema(tableName, primKey, indexes);
+ }
+ });
+ };
+ Version.prototype.stores = function (stores) {
+ var db = this.db;
+ this._cfg.storesSource = this._cfg.storesSource ?
+ extend(this._cfg.storesSource, stores) :
+ stores;
+ var versions = db._versions;
+ var storesSpec = {};
+ var dbschema = {};
+ versions.forEach(function (version) {
+ extend(storesSpec, version._cfg.storesSource);
+ dbschema = (version._cfg.dbschema = {});
+ version._parseStoresSpec(storesSpec, dbschema);
+ });
+ db._dbSchema = dbschema;
+ removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]);
+ setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema);
+ db._storeNames = keys(dbschema);
+ return this;
+ };
+ Version.prototype.upgrade = function (upgradeFunction) {
+ this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction);
+ return this;
+ };
+ return Version;
+}());
+
+function createVersionConstructor(db) {
+ return makeClassConstructor(Version.prototype, function Version(versionNumber) {
+ this.db = db;
+ this._cfg = {
+ version: versionNumber,
+ storesSource: null,
+ dbschema: {},
+ tables: {},
+ contentUpgrade: null
+ };
+ });
+}
+
+function getDbNamesTable(indexedDB, IDBKeyRange) {
+ var dbNamesDB = indexedDB["_dbNamesDB"];
+ if (!dbNamesDB) {
+ dbNamesDB = indexedDB["_dbNamesDB"] = new Dexie$1(DBNAMES_DB, {
+ addons: [],
+ indexedDB: indexedDB,
+ IDBKeyRange: IDBKeyRange,
+ });
+ dbNamesDB.version(1).stores({ dbnames: "name" });
+ }
+ return dbNamesDB.table("dbnames");
+}
+function hasDatabasesNative(indexedDB) {
+ return indexedDB && typeof indexedDB.databases === "function";
+}
+function getDatabaseNames(_a) {
+ var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange;
+ return hasDatabasesNative(indexedDB)
+ ? Promise.resolve(indexedDB.databases()).then(function (infos) {
+ return infos
+ .map(function (info) { return info.name; })
+ .filter(function (name) { return name !== DBNAMES_DB; });
+ })
+ : getDbNamesTable(indexedDB, IDBKeyRange).toCollection().primaryKeys();
+}
+function _onDatabaseCreated(_a, name) {
+ var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange;
+ !hasDatabasesNative(indexedDB) &&
+ name !== DBNAMES_DB &&
+ getDbNamesTable(indexedDB, IDBKeyRange).put({ name: name }).catch(nop);
+}
+function _onDatabaseDeleted(_a, name) {
+ var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange;
+ !hasDatabasesNative(indexedDB) &&
+ name !== DBNAMES_DB &&
+ getDbNamesTable(indexedDB, IDBKeyRange).delete(name).catch(nop);
+}
+
+function vip(fn) {
+ return newScope(function () {
+ PSD.letThrough = true;
+ return fn();
+ });
+}
+
+function idbReady() {
+ var isSafari = !navigator.userAgentData &&
+ /Safari\//.test(navigator.userAgent) &&
+ !/Chrom(e|ium)\//.test(navigator.userAgent);
+ if (!isSafari || !indexedDB.databases)
+ return Promise.resolve();
+ var intervalId;
+ return new Promise(function (resolve) {
+ var tryIdb = function () { return indexedDB.databases().finally(resolve); };
+ intervalId = setInterval(tryIdb, 100);
+ tryIdb();
+ }).finally(function () { return clearInterval(intervalId); });
+}
+
+function dexieOpen(db) {
+ var state = db._state;
+ var indexedDB = db._deps.indexedDB;
+ if (state.isBeingOpened || db.idbdb)
+ return state.dbReadyPromise.then(function () { return state.dbOpenError ?
+ rejection(state.dbOpenError) :
+ db; });
+ debug && (state.openCanceller._stackHolder = getErrorWithStack());
+ state.isBeingOpened = true;
+ state.dbOpenError = null;
+ state.openComplete = false;
+ var openCanceller = state.openCanceller;
+ function throwIfCancelled() {
+ if (state.openCanceller !== openCanceller)
+ throw new exceptions.DatabaseClosed('db.open() was cancelled');
+ }
+ var resolveDbReady = state.dbReadyResolve,
+ upgradeTransaction = null, wasCreated = false;
+ return DexiePromise.race([openCanceller, (typeof navigator === 'undefined' ? DexiePromise.resolve() : idbReady()).then(function () { return new DexiePromise(function (resolve, reject) {
+ throwIfCancelled();
+ if (!indexedDB)
+ throw new exceptions.MissingAPI();
+ var dbName = db.name;
+ var req = state.autoSchema ?
+ indexedDB.open(dbName) :
+ indexedDB.open(dbName, Math.round(db.verno * 10));
+ if (!req)
+ throw new exceptions.MissingAPI();
+ req.onerror = eventRejectHandler(reject);
+ req.onblocked = wrap(db._fireOnBlocked);
+ req.onupgradeneeded = wrap(function (e) {
+ upgradeTransaction = req.transaction;
+ if (state.autoSchema && !db._options.allowEmptyDB) {
+ req.onerror = preventDefault;
+ upgradeTransaction.abort();
+ req.result.close();
+ var delreq = indexedDB.deleteDatabase(dbName);
+ delreq.onsuccess = delreq.onerror = wrap(function () {
+ reject(new exceptions.NoSuchDatabase("Database " + dbName + " doesnt exist"));
+ });
+ }
+ else {
+ upgradeTransaction.onerror = eventRejectHandler(reject);
+ var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion;
+ wasCreated = oldVer < 1;
+ db._novip.idbdb = req.result;
+ runUpgraders(db, oldVer / 10, upgradeTransaction, reject);
+ }
+ }, reject);
+ req.onsuccess = wrap(function () {
+ upgradeTransaction = null;
+ var idbdb = db._novip.idbdb = req.result;
+ var objectStoreNames = slice(idbdb.objectStoreNames);
+ if (objectStoreNames.length > 0)
+ try {
+ var tmpTrans = idbdb.transaction(safariMultiStoreFix(objectStoreNames), 'readonly');
+ if (state.autoSchema)
+ readGlobalSchema(db, idbdb, tmpTrans);
+ else {
+ adjustToExistingIndexNames(db, db._dbSchema, tmpTrans);
+ if (!verifyInstalledSchema(db, tmpTrans)) {
+ console.warn("Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Some queries may fail.");
+ }
+ }
+ generateMiddlewareStacks(db, tmpTrans);
+ }
+ catch (e) {
+ }
+ connections.push(db);
+ idbdb.onversionchange = wrap(function (ev) {
+ state.vcFired = true;
+ db.on("versionchange").fire(ev);
+ });
+ idbdb.onclose = wrap(function (ev) {
+ db.on("close").fire(ev);
+ });
+ if (wasCreated)
+ _onDatabaseCreated(db._deps, dbName);
+ resolve();
+ }, reject);
+ }); })]).then(function () {
+ throwIfCancelled();
+ state.onReadyBeingFired = [];
+ return DexiePromise.resolve(vip(function () { return db.on.ready.fire(db.vip); })).then(function fireRemainders() {
+ if (state.onReadyBeingFired.length > 0) {
+ var remainders_1 = state.onReadyBeingFired.reduce(promisableChain, nop);
+ state.onReadyBeingFired = [];
+ return DexiePromise.resolve(vip(function () { return remainders_1(db.vip); })).then(fireRemainders);
+ }
+ });
+ }).finally(function () {
+ state.onReadyBeingFired = null;
+ state.isBeingOpened = false;
+ }).then(function () {
+ return db;
+ }).catch(function (err) {
+ state.dbOpenError = err;
+ try {
+ upgradeTransaction && upgradeTransaction.abort();
+ }
+ catch (_a) { }
+ if (openCanceller === state.openCanceller) {
+ db._close();
+ }
+ return rejection(err);
+ }).finally(function () {
+ state.openComplete = true;
+ resolveDbReady();
+ });
+}
+
+function awaitIterator(iterator) {
+ var callNext = function (result) { return iterator.next(result); }, doThrow = function (error) { return iterator.throw(error); }, onSuccess = step(callNext), onError = step(doThrow);
+ function step(getNext) {
+ return function (val) {
+ var next = getNext(val), value = next.value;
+ return next.done ? value :
+ (!value || typeof value.then !== 'function' ?
+ isArray(value) ? Promise.all(value).then(onSuccess, onError) : onSuccess(value) :
+ value.then(onSuccess, onError));
+ };
+ }
+ return step(callNext)();
+}
+
+function extractTransactionArgs(mode, _tableArgs_, scopeFunc) {
+ var i = arguments.length;
+ if (i < 2)
+ throw new exceptions.InvalidArgument("Too few arguments");
+ var args = new Array(i - 1);
+ while (--i)
+ args[i - 1] = arguments[i];
+ scopeFunc = args.pop();
+ var tables = flatten(args);
+ return [mode, tables, scopeFunc];
+}
+function enterTransactionScope(db, mode, storeNames, parentTransaction, scopeFunc) {
+ return DexiePromise.resolve().then(function () {
+ var transless = PSD.transless || PSD;
+ var trans = db._createTransaction(mode, storeNames, db._dbSchema, parentTransaction);
+ var zoneProps = {
+ trans: trans,
+ transless: transless
+ };
+ if (parentTransaction) {
+ trans.idbtrans = parentTransaction.idbtrans;
+ }
+ else {
+ try {
+ trans.create();
+ db._state.PR1398_maxLoop = 3;
+ }
+ catch (ex) {
+ if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) {
+ console.warn('Dexie: Need to reopen db');
+ db._close();
+ return db.open().then(function () { return enterTransactionScope(db, mode, storeNames, null, scopeFunc); });
+ }
+ return rejection(ex);
+ }
+ }
+ var scopeFuncIsAsync = isAsyncFunction(scopeFunc);
+ if (scopeFuncIsAsync) {
+ incrementExpectedAwaits();
+ }
+ var returnValue;
+ var promiseFollowed = DexiePromise.follow(function () {
+ returnValue = scopeFunc.call(trans, trans);
+ if (returnValue) {
+ if (scopeFuncIsAsync) {
+ var decrementor = decrementExpectedAwaits.bind(null, null);
+ returnValue.then(decrementor, decrementor);
+ }
+ else if (typeof returnValue.next === 'function' && typeof returnValue.throw === 'function') {
+ returnValue = awaitIterator(returnValue);
+ }
+ }
+ }, zoneProps);
+ return (returnValue && typeof returnValue.then === 'function' ?
+ DexiePromise.resolve(returnValue).then(function (x) { return trans.active ?
+ x
+ : rejection(new exceptions.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn")); })
+ : promiseFollowed.then(function () { return returnValue; })).then(function (x) {
+ if (parentTransaction)
+ trans._resolve();
+ return trans._completion.then(function () { return x; });
+ }).catch(function (e) {
+ trans._reject(e);
+ return rejection(e);
+ });
+ });
+}
+
+function pad(a, value, count) {
+ var result = isArray(a) ? a.slice() : [a];
+ for (var i = 0; i < count; ++i)
+ result.push(value);
+ return result;
+}
+function createVirtualIndexMiddleware(down) {
+ return __assign(__assign({}, down), { table: function (tableName) {
+ var table = down.table(tableName);
+ var schema = table.schema;
+ var indexLookup = {};
+ var allVirtualIndexes = [];
+ function addVirtualIndexes(keyPath, keyTail, lowLevelIndex) {
+ var keyPathAlias = getKeyPathAlias(keyPath);
+ var indexList = (indexLookup[keyPathAlias] = indexLookup[keyPathAlias] || []);
+ var keyLength = keyPath == null ? 0 : typeof keyPath === 'string' ? 1 : keyPath.length;
+ var isVirtual = keyTail > 0;
+ var virtualIndex = __assign(__assign({}, lowLevelIndex), { isVirtual: isVirtual, keyTail: keyTail, keyLength: keyLength, extractKey: getKeyExtractor(keyPath), unique: !isVirtual && lowLevelIndex.unique });
+ indexList.push(virtualIndex);
+ if (!virtualIndex.isPrimaryKey) {
+ allVirtualIndexes.push(virtualIndex);
+ }
+ if (keyLength > 1) {
+ var virtualKeyPath = keyLength === 2 ?
+ keyPath[0] :
+ keyPath.slice(0, keyLength - 1);
+ addVirtualIndexes(virtualKeyPath, keyTail + 1, lowLevelIndex);
+ }
+ indexList.sort(function (a, b) { return a.keyTail - b.keyTail; });
+ return virtualIndex;
+ }
+ var primaryKey = addVirtualIndexes(schema.primaryKey.keyPath, 0, schema.primaryKey);
+ indexLookup[":id"] = [primaryKey];
+ for (var _i = 0, _a = schema.indexes; _i < _a.length; _i++) {
+ var index = _a[_i];
+ addVirtualIndexes(index.keyPath, 0, index);
+ }
+ function findBestIndex(keyPath) {
+ var result = indexLookup[getKeyPathAlias(keyPath)];
+ return result && result[0];
+ }
+ function translateRange(range, keyTail) {
+ return {
+ type: range.type === 1 ?
+ 2 :
+ range.type,
+ lower: pad(range.lower, range.lowerOpen ? down.MAX_KEY : down.MIN_KEY, keyTail),
+ lowerOpen: true,
+ upper: pad(range.upper, range.upperOpen ? down.MIN_KEY : down.MAX_KEY, keyTail),
+ upperOpen: true
+ };
+ }
+ function translateRequest(req) {
+ var index = req.query.index;
+ return index.isVirtual ? __assign(__assign({}, req), { query: {
+ index: index,
+ range: translateRange(req.query.range, index.keyTail)
+ } }) : req;
+ }
+ var result = __assign(__assign({}, table), { schema: __assign(__assign({}, schema), { primaryKey: primaryKey, indexes: allVirtualIndexes, getIndexByKeyPath: findBestIndex }), count: function (req) {
+ return table.count(translateRequest(req));
+ }, query: function (req) {
+ return table.query(translateRequest(req));
+ }, openCursor: function (req) {
+ var _a = req.query.index, keyTail = _a.keyTail, isVirtual = _a.isVirtual, keyLength = _a.keyLength;
+ if (!isVirtual)
+ return table.openCursor(req);
+ function createVirtualCursor(cursor) {
+ function _continue(key) {
+ key != null ?
+ cursor.continue(pad(key, req.reverse ? down.MAX_KEY : down.MIN_KEY, keyTail)) :
+ req.unique ?
+ cursor.continue(cursor.key.slice(0, keyLength)
+ .concat(req.reverse
+ ? down.MIN_KEY
+ : down.MAX_KEY, keyTail)) :
+ cursor.continue();
+ }
+ var virtualCursor = Object.create(cursor, {
+ continue: { value: _continue },
+ continuePrimaryKey: {
+ value: function (key, primaryKey) {
+ cursor.continuePrimaryKey(pad(key, down.MAX_KEY, keyTail), primaryKey);
+ }
+ },
+ primaryKey: {
+ get: function () {
+ return cursor.primaryKey;
+ }
+ },
+ key: {
+ get: function () {
+ var key = cursor.key;
+ return keyLength === 1 ?
+ key[0] :
+ key.slice(0, keyLength);
+ }
+ },
+ value: {
+ get: function () {
+ return cursor.value;
+ }
+ }
+ });
+ return virtualCursor;
+ }
+ return table.openCursor(translateRequest(req))
+ .then(function (cursor) { return cursor && createVirtualCursor(cursor); });
+ } });
+ return result;
+ } });
+}
+var virtualIndexMiddleware = {
+ stack: "dbcore",
+ name: "VirtualIndexMiddleware",
+ level: 1,
+ create: createVirtualIndexMiddleware
+};
+
+function getObjectDiff(a, b, rv, prfx) {
+ rv = rv || {};
+ prfx = prfx || '';
+ keys(a).forEach(function (prop) {
+ if (!hasOwn(b, prop)) {
+ rv[prfx + prop] = undefined;
+ }
+ else {
+ var ap = a[prop], bp = b[prop];
+ if (typeof ap === 'object' && typeof bp === 'object' && ap && bp) {
+ var apTypeName = toStringTag(ap);
+ var bpTypeName = toStringTag(bp);
+ if (apTypeName !== bpTypeName) {
+ rv[prfx + prop] = b[prop];
+ }
+ else if (apTypeName === 'Object') {
+ getObjectDiff(ap, bp, rv, prfx + prop + '.');
+ }
+ else if (ap !== bp) {
+ rv[prfx + prop] = b[prop];
+ }
+ }
+ else if (ap !== bp)
+ rv[prfx + prop] = b[prop];
+ }
+ });
+ keys(b).forEach(function (prop) {
+ if (!hasOwn(a, prop)) {
+ rv[prfx + prop] = b[prop];
+ }
+ });
+ return rv;
+}
+
+function getEffectiveKeys(primaryKey, req) {
+ if (req.type === 'delete')
+ return req.keys;
+ return req.keys || req.values.map(primaryKey.extractKey);
+}
+
+var hooksMiddleware = {
+ stack: "dbcore",
+ name: "HooksMiddleware",
+ level: 2,
+ create: function (downCore) { return (__assign(__assign({}, downCore), { table: function (tableName) {
+ var downTable = downCore.table(tableName);
+ var primaryKey = downTable.schema.primaryKey;
+ var tableMiddleware = __assign(__assign({}, downTable), { mutate: function (req) {
+ var dxTrans = PSD.trans;
+ var _a = dxTrans.table(tableName).hook, deleting = _a.deleting, creating = _a.creating, updating = _a.updating;
+ switch (req.type) {
+ case 'add':
+ if (creating.fire === nop)
+ break;
+ return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true);
+ case 'put':
+ if (creating.fire === nop && updating.fire === nop)
+ break;
+ return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true);
+ case 'delete':
+ if (deleting.fire === nop)
+ break;
+ return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true);
+ case 'deleteRange':
+ if (deleting.fire === nop)
+ break;
+ return dxTrans._promise('readwrite', function () { return deleteRange(req); }, true);
+ }
+ return downTable.mutate(req);
+ function addPutOrDelete(req) {
+ var dxTrans = PSD.trans;
+ var keys = req.keys || getEffectiveKeys(primaryKey, req);
+ if (!keys)
+ throw new Error("Keys missing");
+ req = req.type === 'add' || req.type === 'put' ? __assign(__assign({}, req), { keys: keys }) : __assign({}, req);
+ if (req.type !== 'delete')
+ req.values = __spreadArray([], req.values, true);
+ if (req.keys)
+ req.keys = __spreadArray([], req.keys, true);
+ return getExistingValues(downTable, req, keys).then(function (existingValues) {
+ var contexts = keys.map(function (key, i) {
+ var existingValue = existingValues[i];
+ var ctx = { onerror: null, onsuccess: null };
+ if (req.type === 'delete') {
+ deleting.fire.call(ctx, key, existingValue, dxTrans);
+ }
+ else if (req.type === 'add' || existingValue === undefined) {
+ var generatedPrimaryKey = creating.fire.call(ctx, key, req.values[i], dxTrans);
+ if (key == null && generatedPrimaryKey != null) {
+ key = generatedPrimaryKey;
+ req.keys[i] = key;
+ if (!primaryKey.outbound) {
+ setByKeyPath(req.values[i], primaryKey.keyPath, key);
+ }
+ }
+ }
+ else {
+ var objectDiff = getObjectDiff(existingValue, req.values[i]);
+ var additionalChanges_1 = updating.fire.call(ctx, objectDiff, key, existingValue, dxTrans);
+ if (additionalChanges_1) {
+ var requestedValue_1 = req.values[i];
+ Object.keys(additionalChanges_1).forEach(function (keyPath) {
+ if (hasOwn(requestedValue_1, keyPath)) {
+ requestedValue_1[keyPath] = additionalChanges_1[keyPath];
+ }
+ else {
+ setByKeyPath(requestedValue_1, keyPath, additionalChanges_1[keyPath]);
+ }
+ });
+ }
+ }
+ return ctx;
+ });
+ return downTable.mutate(req).then(function (_a) {
+ var failures = _a.failures, results = _a.results, numFailures = _a.numFailures, lastResult = _a.lastResult;
+ for (var i = 0; i < keys.length; ++i) {
+ var primKey = results ? results[i] : keys[i];
+ var ctx = contexts[i];
+ if (primKey == null) {
+ ctx.onerror && ctx.onerror(failures[i]);
+ }
+ else {
+ ctx.onsuccess && ctx.onsuccess(req.type === 'put' && existingValues[i] ?
+ req.values[i] :
+ primKey
+ );
+ }
+ }
+ return { failures: failures, results: results, numFailures: numFailures, lastResult: lastResult };
+ }).catch(function (error) {
+ contexts.forEach(function (ctx) { return ctx.onerror && ctx.onerror(error); });
+ return Promise.reject(error);
+ });
+ });
+ }
+ function deleteRange(req) {
+ return deleteNextChunk(req.trans, req.range, 10000);
+ }
+ function deleteNextChunk(trans, range, limit) {
+ return downTable.query({ trans: trans, values: false, query: { index: primaryKey, range: range }, limit: limit })
+ .then(function (_a) {
+ var result = _a.result;
+ return addPutOrDelete({ type: 'delete', keys: result, trans: trans }).then(function (res) {
+ if (res.numFailures > 0)
+ return Promise.reject(res.failures[0]);
+ if (result.length < limit) {
+ return { failures: [], numFailures: 0, lastResult: undefined };
+ }
+ else {
+ return deleteNextChunk(trans, __assign(__assign({}, range), { lower: result[result.length - 1], lowerOpen: true }), limit);
+ }
+ });
+ });
+ }
+ } });
+ return tableMiddleware;
+ } })); }
+};
+function getExistingValues(table, req, effectiveKeys) {
+ return req.type === "add"
+ ? Promise.resolve([])
+ : table.getMany({ trans: req.trans, keys: effectiveKeys, cache: "immutable" });
+}
+
+function getFromTransactionCache(keys, cache, clone) {
+ try {
+ if (!cache)
+ return null;
+ if (cache.keys.length < keys.length)
+ return null;
+ var result = [];
+ for (var i = 0, j = 0; i < cache.keys.length && j < keys.length; ++i) {
+ if (cmp(cache.keys[i], keys[j]) !== 0)
+ continue;
+ result.push(clone ? deepClone(cache.values[i]) : cache.values[i]);
+ ++j;
+ }
+ return result.length === keys.length ? result : null;
+ }
+ catch (_a) {
+ return null;
+ }
+}
+var cacheExistingValuesMiddleware = {
+ stack: "dbcore",
+ level: -1,
+ create: function (core) {
+ return {
+ table: function (tableName) {
+ var table = core.table(tableName);
+ return __assign(__assign({}, table), { getMany: function (req) {
+ if (!req.cache) {
+ return table.getMany(req);
+ }
+ var cachedResult = getFromTransactionCache(req.keys, req.trans["_cache"], req.cache === "clone");
+ if (cachedResult) {
+ return DexiePromise.resolve(cachedResult);
+ }
+ return table.getMany(req).then(function (res) {
+ req.trans["_cache"] = {
+ keys: req.keys,
+ values: req.cache === "clone" ? deepClone(res) : res,
+ };
+ return res;
+ });
+ }, mutate: function (req) {
+ if (req.type !== "add")
+ req.trans["_cache"] = null;
+ return table.mutate(req);
+ } });
+ },
+ };
+ },
+};
+
+var _a;
+function isEmptyRange(node) {
+ return !("from" in node);
+}
+var RangeSet = function (fromOrTree, to) {
+ if (this) {
+ extend(this, arguments.length ? { d: 1, from: fromOrTree, to: arguments.length > 1 ? to : fromOrTree } : { d: 0 });
+ }
+ else {
+ var rv = new RangeSet();
+ if (fromOrTree && ("d" in fromOrTree)) {
+ extend(rv, fromOrTree);
+ }
+ return rv;
+ }
+};
+props(RangeSet.prototype, (_a = {
+ add: function (rangeSet) {
+ mergeRanges(this, rangeSet);
+ return this;
+ },
+ addKey: function (key) {
+ addRange(this, key, key);
+ return this;
+ },
+ addKeys: function (keys) {
+ var _this = this;
+ keys.forEach(function (key) { return addRange(_this, key, key); });
+ return this;
+ }
+ },
+ _a[iteratorSymbol] = function () {
+ return getRangeSetIterator(this);
+ },
+ _a));
+function addRange(target, from, to) {
+ var diff = cmp(from, to);
+ if (isNaN(diff))
+ return;
+ if (diff > 0)
+ throw RangeError();
+ if (isEmptyRange(target))
+ return extend(target, { from: from, to: to, d: 1 });
+ var left = target.l;
+ var right = target.r;
+ if (cmp(to, target.from) < 0) {
+ left
+ ? addRange(left, from, to)
+ : (target.l = { from: from, to: to, d: 1, l: null, r: null });
+ return rebalance(target);
+ }
+ if (cmp(from, target.to) > 0) {
+ right
+ ? addRange(right, from, to)
+ : (target.r = { from: from, to: to, d: 1, l: null, r: null });
+ return rebalance(target);
+ }
+ if (cmp(from, target.from) < 0) {
+ target.from = from;
+ target.l = null;
+ target.d = right ? right.d + 1 : 1;
+ }
+ if (cmp(to, target.to) > 0) {
+ target.to = to;
+ target.r = null;
+ target.d = target.l ? target.l.d + 1 : 1;
+ }
+ var rightWasCutOff = !target.r;
+ if (left && !target.l) {
+ mergeRanges(target, left);
+ }
+ if (right && rightWasCutOff) {
+ mergeRanges(target, right);
+ }
+}
+function mergeRanges(target, newSet) {
+ function _addRangeSet(target, _a) {
+ var from = _a.from, to = _a.to, l = _a.l, r = _a.r;
+ addRange(target, from, to);
+ if (l)
+ _addRangeSet(target, l);
+ if (r)
+ _addRangeSet(target, r);
+ }
+ if (!isEmptyRange(newSet))
+ _addRangeSet(target, newSet);
+}
+function rangesOverlap(rangeSet1, rangeSet2) {
+ var i1 = getRangeSetIterator(rangeSet2);
+ var nextResult1 = i1.next();
+ if (nextResult1.done)
+ return false;
+ var a = nextResult1.value;
+ var i2 = getRangeSetIterator(rangeSet1);
+ var nextResult2 = i2.next(a.from);
+ var b = nextResult2.value;
+ while (!nextResult1.done && !nextResult2.done) {
+ if (cmp(b.from, a.to) <= 0 && cmp(b.to, a.from) >= 0)
+ return true;
+ cmp(a.from, b.from) < 0
+ ? (a = (nextResult1 = i1.next(b.from)).value)
+ : (b = (nextResult2 = i2.next(a.from)).value);
+ }
+ return false;
+}
+function getRangeSetIterator(node) {
+ var state = isEmptyRange(node) ? null : { s: 0, n: node };
+ return {
+ next: function (key) {
+ var keyProvided = arguments.length > 0;
+ while (state) {
+ switch (state.s) {
+ case 0:
+ state.s = 1;
+ if (keyProvided) {
+ while (state.n.l && cmp(key, state.n.from) < 0)
+ state = { up: state, n: state.n.l, s: 1 };
+ }
+ else {
+ while (state.n.l)
+ state = { up: state, n: state.n.l, s: 1 };
+ }
+ case 1:
+ state.s = 2;
+ if (!keyProvided || cmp(key, state.n.to) <= 0)
+ return { value: state.n, done: false };
+ case 2:
+ if (state.n.r) {
+ state.s = 3;
+ state = { up: state, n: state.n.r, s: 0 };
+ continue;
+ }
+ case 3:
+ state = state.up;
+ }
+ }
+ return { done: true };
+ },
+ };
+}
+function rebalance(target) {
+ var _a, _b;
+ var diff = (((_a = target.r) === null || _a === void 0 ? void 0 : _a.d) || 0) - (((_b = target.l) === null || _b === void 0 ? void 0 : _b.d) || 0);
+ var r = diff > 1 ? "r" : diff < -1 ? "l" : "";
+ if (r) {
+ var l = r === "r" ? "l" : "r";
+ var rootClone = __assign({}, target);
+ var oldRootRight = target[r];
+ target.from = oldRootRight.from;
+ target.to = oldRootRight.to;
+ target[r] = oldRootRight[r];
+ rootClone[r] = oldRootRight[l];
+ target[l] = rootClone;
+ rootClone.d = computeDepth(rootClone);
+ }
+ target.d = computeDepth(target);
+}
+function computeDepth(_a) {
+ var r = _a.r, l = _a.l;
+ return (r ? (l ? Math.max(r.d, l.d) : r.d) : l ? l.d : 0) + 1;
+}
+
+var observabilityMiddleware = {
+ stack: "dbcore",
+ level: 0,
+ create: function (core) {
+ var dbName = core.schema.name;
+ var FULL_RANGE = new RangeSet(core.MIN_KEY, core.MAX_KEY);
+ return __assign(__assign({}, core), { table: function (tableName) {
+ var table = core.table(tableName);
+ var schema = table.schema;
+ var primaryKey = schema.primaryKey;
+ var extractKey = primaryKey.extractKey, outbound = primaryKey.outbound;
+ var tableClone = __assign(__assign({}, table), { mutate: function (req) {
+ var trans = req.trans;
+ var mutatedParts = trans.mutatedParts || (trans.mutatedParts = {});
+ var getRangeSet = function (indexName) {
+ var part = "idb://" + dbName + "/" + tableName + "/" + indexName;
+ return (mutatedParts[part] ||
+ (mutatedParts[part] = new RangeSet()));
+ };
+ var pkRangeSet = getRangeSet("");
+ var delsRangeSet = getRangeSet(":dels");
+ var type = req.type;
+ var _a = req.type === "deleteRange"
+ ? [req.range]
+ : req.type === "delete"
+ ? [req.keys]
+ : req.values.length < 50
+ ? [[], req.values]
+ : [], keys = _a[0], newObjs = _a[1];
+ var oldCache = req.trans["_cache"];
+ return table.mutate(req).then(function (res) {
+ if (isArray(keys)) {
+ if (type !== "delete")
+ keys = res.results;
+ pkRangeSet.addKeys(keys);
+ var oldObjs = getFromTransactionCache(keys, oldCache);
+ if (!oldObjs && type !== "add") {
+ delsRangeSet.addKeys(keys);
+ }
+ if (oldObjs || newObjs) {
+ trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs);
+ }
+ }
+ else if (keys) {
+ var range = { from: keys.lower, to: keys.upper };
+ delsRangeSet.add(range);
+ pkRangeSet.add(range);
+ }
+ else {
+ pkRangeSet.add(FULL_RANGE);
+ delsRangeSet.add(FULL_RANGE);
+ schema.indexes.forEach(function (idx) { return getRangeSet(idx.name).add(FULL_RANGE); });
+ }
+ return res;
+ });
+ } });
+ var getRange = function (_a) {
+ var _b, _c;
+ var _d = _a.query, index = _d.index, range = _d.range;
+ return [
+ index,
+ new RangeSet((_b = range.lower) !== null && _b !== void 0 ? _b : core.MIN_KEY, (_c = range.upper) !== null && _c !== void 0 ? _c : core.MAX_KEY),
+ ];
+ };
+ var readSubscribers = {
+ get: function (req) { return [primaryKey, new RangeSet(req.key)]; },
+ getMany: function (req) { return [primaryKey, new RangeSet().addKeys(req.keys)]; },
+ count: getRange,
+ query: getRange,
+ openCursor: getRange,
+ };
+ keys(readSubscribers).forEach(function (method) {
+ tableClone[method] = function (req) {
+ var subscr = PSD.subscr;
+ if (subscr) {
+ var getRangeSet = function (indexName) {
+ var part = "idb://" + dbName + "/" + tableName + "/" + indexName;
+ return (subscr[part] ||
+ (subscr[part] = new RangeSet()));
+ };
+ var pkRangeSet_1 = getRangeSet("");
+ var delsRangeSet_1 = getRangeSet(":dels");
+ var _a = readSubscribers[method](req), queriedIndex = _a[0], queriedRanges = _a[1];
+ getRangeSet(queriedIndex.name || "").add(queriedRanges);
+ if (!queriedIndex.isPrimaryKey) {
+ if (method === "count") {
+ delsRangeSet_1.add(FULL_RANGE);
+ }
+ else {
+ var keysPromise_1 = method === "query" &&
+ outbound &&
+ req.values &&
+ table.query(__assign(__assign({}, req), { values: false }));
+ return table[method].apply(this, arguments).then(function (res) {
+ if (method === "query") {
+ if (outbound && req.values) {
+ return keysPromise_1.then(function (_a) {
+ var resultingKeys = _a.result;
+ pkRangeSet_1.addKeys(resultingKeys);
+ return res;
+ });
+ }
+ var pKeys = req.values
+ ? res.result.map(extractKey)
+ : res.result;
+ if (req.values) {
+ pkRangeSet_1.addKeys(pKeys);
+ }
+ else {
+ delsRangeSet_1.addKeys(pKeys);
+ }
+ }
+ else if (method === "openCursor") {
+ var cursor_1 = res;
+ var wantValues_1 = req.values;
+ return (cursor_1 &&
+ Object.create(cursor_1, {
+ key: {
+ get: function () {
+ delsRangeSet_1.addKey(cursor_1.primaryKey);
+ return cursor_1.key;
+ },
+ },
+ primaryKey: {
+ get: function () {
+ var pkey = cursor_1.primaryKey;
+ delsRangeSet_1.addKey(pkey);
+ return pkey;
+ },
+ },
+ value: {
+ get: function () {
+ wantValues_1 && pkRangeSet_1.addKey(cursor_1.primaryKey);
+ return cursor_1.value;
+ },
+ },
+ }));
+ }
+ return res;
+ });
+ }
+ }
+ }
+ return table[method].apply(this, arguments);
+ };
+ });
+ return tableClone;
+ } });
+ },
+};
+function trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs) {
+ function addAffectedIndex(ix) {
+ var rangeSet = getRangeSet(ix.name || "");
+ function extractKey(obj) {
+ return obj != null ? ix.extractKey(obj) : null;
+ }
+ var addKeyOrKeys = function (key) { return ix.multiEntry && isArray(key)
+ ? key.forEach(function (key) { return rangeSet.addKey(key); })
+ : rangeSet.addKey(key); };
+ (oldObjs || newObjs).forEach(function (_, i) {
+ var oldKey = oldObjs && extractKey(oldObjs[i]);
+ var newKey = newObjs && extractKey(newObjs[i]);
+ if (cmp(oldKey, newKey) !== 0) {
+ if (oldKey != null)
+ addKeyOrKeys(oldKey);
+ if (newKey != null)
+ addKeyOrKeys(newKey);
+ }
+ });
+ }
+ schema.indexes.forEach(addAffectedIndex);
+}
+
+var Dexie$1 = (function () {
+ function Dexie(name, options) {
+ var _this = this;
+ this._middlewares = {};
+ this.verno = 0;
+ var deps = Dexie.dependencies;
+ this._options = options = __assign({
+ addons: Dexie.addons, autoOpen: true,
+ indexedDB: deps.indexedDB, IDBKeyRange: deps.IDBKeyRange }, options);
+ this._deps = {
+ indexedDB: options.indexedDB,
+ IDBKeyRange: options.IDBKeyRange
+ };
+ var addons = options.addons;
+ this._dbSchema = {};
+ this._versions = [];
+ this._storeNames = [];
+ this._allTables = {};
+ this.idbdb = null;
+ this._novip = this;
+ var state = {
+ dbOpenError: null,
+ isBeingOpened: false,
+ onReadyBeingFired: null,
+ openComplete: false,
+ dbReadyResolve: nop,
+ dbReadyPromise: null,
+ cancelOpen: nop,
+ openCanceller: null,
+ autoSchema: true,
+ PR1398_maxLoop: 3
+ };
+ state.dbReadyPromise = new DexiePromise(function (resolve) {
+ state.dbReadyResolve = resolve;
+ });
+ state.openCanceller = new DexiePromise(function (_, reject) {
+ state.cancelOpen = reject;
+ });
+ this._state = state;
+ this.name = name;
+ this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] });
+ this.on.ready.subscribe = override(this.on.ready.subscribe, function (subscribe) {
+ return function (subscriber, bSticky) {
+ Dexie.vip(function () {
+ var state = _this._state;
+ if (state.openComplete) {
+ if (!state.dbOpenError)
+ DexiePromise.resolve().then(subscriber);
+ if (bSticky)
+ subscribe(subscriber);
+ }
+ else if (state.onReadyBeingFired) {
+ state.onReadyBeingFired.push(subscriber);
+ if (bSticky)
+ subscribe(subscriber);
+ }
+ else {
+ subscribe(subscriber);
+ var db_1 = _this;
+ if (!bSticky)
+ subscribe(function unsubscribe() {
+ db_1.on.ready.unsubscribe(subscriber);
+ db_1.on.ready.unsubscribe(unsubscribe);
+ });
+ }
+ });
+ };
+ });
+ this.Collection = createCollectionConstructor(this);
+ this.Table = createTableConstructor(this);
+ this.Transaction = createTransactionConstructor(this);
+ this.Version = createVersionConstructor(this);
+ this.WhereClause = createWhereClauseConstructor(this);
+ this.on("versionchange", function (ev) {
+ if (ev.newVersion > 0)
+ console.warn("Another connection wants to upgrade database '" + _this.name + "'. Closing db now to resume the upgrade.");
+ else
+ console.warn("Another connection wants to delete database '" + _this.name + "'. Closing db now to resume the delete request.");
+ _this.close();
+ });
+ this.on("blocked", function (ev) {
+ if (!ev.newVersion || ev.newVersion < ev.oldVersion)
+ console.warn("Dexie.delete('" + _this.name + "') was blocked");
+ else
+ console.warn("Upgrade '" + _this.name + "' blocked by other connection holding version " + ev.oldVersion / 10);
+ });
+ this._maxKey = getMaxKey(options.IDBKeyRange);
+ this._createTransaction = function (mode, storeNames, dbschema, parentTransaction) { return new _this.Transaction(mode, storeNames, dbschema, _this._options.chromeTransactionDurability, parentTransaction); };
+ this._fireOnBlocked = function (ev) {
+ _this.on("blocked").fire(ev);
+ connections
+ .filter(function (c) { return c.name === _this.name && c !== _this && !c._state.vcFired; })
+ .map(function (c) { return c.on("versionchange").fire(ev); });
+ };
+ this.use(virtualIndexMiddleware);
+ this.use(hooksMiddleware);
+ this.use(observabilityMiddleware);
+ this.use(cacheExistingValuesMiddleware);
+ this.vip = Object.create(this, { _vip: { value: true } });
+ addons.forEach(function (addon) { return addon(_this); });
+ }
+ Dexie.prototype.version = function (versionNumber) {
+ if (isNaN(versionNumber) || versionNumber < 0.1)
+ throw new exceptions.Type("Given version is not a positive number");
+ versionNumber = Math.round(versionNumber * 10) / 10;
+ if (this.idbdb || this._state.isBeingOpened)
+ throw new exceptions.Schema("Cannot add version when database is open");
+ this.verno = Math.max(this.verno, versionNumber);
+ var versions = this._versions;
+ var versionInstance = versions.filter(function (v) { return v._cfg.version === versionNumber; })[0];
+ if (versionInstance)
+ return versionInstance;
+ versionInstance = new this.Version(versionNumber);
+ versions.push(versionInstance);
+ versions.sort(lowerVersionFirst);
+ versionInstance.stores({});
+ this._state.autoSchema = false;
+ return versionInstance;
+ };
+ Dexie.prototype._whenReady = function (fn) {
+ var _this = this;
+ return (this.idbdb && (this._state.openComplete || PSD.letThrough || this._vip)) ? fn() : new DexiePromise(function (resolve, reject) {
+ if (_this._state.openComplete) {
+ return reject(new exceptions.DatabaseClosed(_this._state.dbOpenError));
+ }
+ if (!_this._state.isBeingOpened) {
+ if (!_this._options.autoOpen) {
+ reject(new exceptions.DatabaseClosed());
+ return;
+ }
+ _this.open().catch(nop);
+ }
+ _this._state.dbReadyPromise.then(resolve, reject);
+ }).then(fn);
+ };
+ Dexie.prototype.use = function (_a) {
+ var stack = _a.stack, create = _a.create, level = _a.level, name = _a.name;
+ if (name)
+ this.unuse({ stack: stack, name: name });
+ var middlewares = this._middlewares[stack] || (this._middlewares[stack] = []);
+ middlewares.push({ stack: stack, create: create, level: level == null ? 10 : level, name: name });
+ middlewares.sort(function (a, b) { return a.level - b.level; });
+ return this;
+ };
+ Dexie.prototype.unuse = function (_a) {
+ var stack = _a.stack, name = _a.name, create = _a.create;
+ if (stack && this._middlewares[stack]) {
+ this._middlewares[stack] = this._middlewares[stack].filter(function (mw) {
+ return create ? mw.create !== create :
+ name ? mw.name !== name :
+ false;
+ });
+ }
+ return this;
+ };
+ Dexie.prototype.open = function () {
+ return dexieOpen(this);
+ };
+ Dexie.prototype._close = function () {
+ var state = this._state;
+ var idx = connections.indexOf(this);
+ if (idx >= 0)
+ connections.splice(idx, 1);
+ if (this.idbdb) {
+ try {
+ this.idbdb.close();
+ }
+ catch (e) { }
+ this._novip.idbdb = null;
+ }
+ state.dbReadyPromise = new DexiePromise(function (resolve) {
+ state.dbReadyResolve = resolve;
+ });
+ state.openCanceller = new DexiePromise(function (_, reject) {
+ state.cancelOpen = reject;
+ });
+ };
+ Dexie.prototype.close = function () {
+ this._close();
+ var state = this._state;
+ this._options.autoOpen = false;
+ state.dbOpenError = new exceptions.DatabaseClosed();
+ if (state.isBeingOpened)
+ state.cancelOpen(state.dbOpenError);
+ };
+ Dexie.prototype.delete = function () {
+ var _this = this;
+ var hasArguments = arguments.length > 0;
+ var state = this._state;
+ return new DexiePromise(function (resolve, reject) {
+ var doDelete = function () {
+ _this.close();
+ var req = _this._deps.indexedDB.deleteDatabase(_this.name);
+ req.onsuccess = wrap(function () {
+ _onDatabaseDeleted(_this._deps, _this.name);
+ resolve();
+ });
+ req.onerror = eventRejectHandler(reject);
+ req.onblocked = _this._fireOnBlocked;
+ };
+ if (hasArguments)
+ throw new exceptions.InvalidArgument("Arguments not allowed in db.delete()");
+ if (state.isBeingOpened) {
+ state.dbReadyPromise.then(doDelete);
+ }
+ else {
+ doDelete();
+ }
+ });
+ };
+ Dexie.prototype.backendDB = function () {
+ return this.idbdb;
+ };
+ Dexie.prototype.isOpen = function () {
+ return this.idbdb !== null;
+ };
+ Dexie.prototype.hasBeenClosed = function () {
+ var dbOpenError = this._state.dbOpenError;
+ return dbOpenError && (dbOpenError.name === 'DatabaseClosed');
+ };
+ Dexie.prototype.hasFailed = function () {
+ return this._state.dbOpenError !== null;
+ };
+ Dexie.prototype.dynamicallyOpened = function () {
+ return this._state.autoSchema;
+ };
+ Object.defineProperty(Dexie.prototype, "tables", {
+ get: function () {
+ var _this = this;
+ return keys(this._allTables).map(function (name) { return _this._allTables[name]; });
+ },
+ enumerable: false,
+ configurable: true
+ });
+ Dexie.prototype.transaction = function () {
+ var args = extractTransactionArgs.apply(this, arguments);
+ return this._transaction.apply(this, args);
+ };
+ Dexie.prototype._transaction = function (mode, tables, scopeFunc) {
+ var _this = this;
+ var parentTransaction = PSD.trans;
+ if (!parentTransaction || parentTransaction.db !== this || mode.indexOf('!') !== -1)
+ parentTransaction = null;
+ var onlyIfCompatible = mode.indexOf('?') !== -1;
+ mode = mode.replace('!', '').replace('?', '');
+ var idbMode, storeNames;
+ try {
+ storeNames = tables.map(function (table) {
+ var storeName = table instanceof _this.Table ? table.name : table;
+ if (typeof storeName !== 'string')
+ throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed");
+ return storeName;
+ });
+ if (mode == "r" || mode === READONLY)
+ idbMode = READONLY;
+ else if (mode == "rw" || mode == READWRITE)
+ idbMode = READWRITE;
+ else
+ throw new exceptions.InvalidArgument("Invalid transaction mode: " + mode);
+ if (parentTransaction) {
+ if (parentTransaction.mode === READONLY && idbMode === READWRITE) {
+ if (onlyIfCompatible) {
+ parentTransaction = null;
+ }
+ else
+ throw new exceptions.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY");
+ }
+ if (parentTransaction) {
+ storeNames.forEach(function (storeName) {
+ if (parentTransaction && parentTransaction.storeNames.indexOf(storeName) === -1) {
+ if (onlyIfCompatible) {
+ parentTransaction = null;
+ }
+ else
+ throw new exceptions.SubTransaction("Table " + storeName +
+ " not included in parent transaction.");
+ }
+ });
+ }
+ if (onlyIfCompatible && parentTransaction && !parentTransaction.active) {
+ parentTransaction = null;
+ }
+ }
+ }
+ catch (e) {
+ return parentTransaction ?
+ parentTransaction._promise(null, function (_, reject) { reject(e); }) :
+ rejection(e);
+ }
+ var enterTransaction = enterTransactionScope.bind(null, this, idbMode, storeNames, parentTransaction, scopeFunc);
+ return (parentTransaction ?
+ parentTransaction._promise(idbMode, enterTransaction, "lock") :
+ PSD.trans ?
+ usePSD(PSD.transless, function () { return _this._whenReady(enterTransaction); }) :
+ this._whenReady(enterTransaction));
+ };
+ Dexie.prototype.table = function (tableName) {
+ if (!hasOwn(this._allTables, tableName)) {
+ throw new exceptions.InvalidTable("Table " + tableName + " does not exist");
+ }
+ return this._allTables[tableName];
+ };
+ return Dexie;
+}());
+
+var symbolObservable = typeof Symbol !== "undefined" && "observable" in Symbol
+ ? Symbol.observable
+ : "@@observable";
+var Observable = (function () {
+ function Observable(subscribe) {
+ this._subscribe = subscribe;
+ }
+ Observable.prototype.subscribe = function (x, error, complete) {
+ return this._subscribe(!x || typeof x === "function" ? { next: x, error: error, complete: complete } : x);
+ };
+ Observable.prototype[symbolObservable] = function () {
+ return this;
+ };
+ return Observable;
+}());
+
+function extendObservabilitySet(target, newSet) {
+ keys(newSet).forEach(function (part) {
+ var rangeSet = target[part] || (target[part] = new RangeSet());
+ mergeRanges(rangeSet, newSet[part]);
+ });
+ return target;
+}
+
+function liveQuery(querier) {
+ return new Observable(function (observer) {
+ var scopeFuncIsAsync = isAsyncFunction(querier);
+ function execute(subscr) {
+ if (scopeFuncIsAsync) {
+ incrementExpectedAwaits();
+ }
+ var exec = function () { return newScope(querier, { subscr: subscr, trans: null }); };
+ var rv = PSD.trans
+ ?
+ usePSD(PSD.transless, exec)
+ : exec();
+ if (scopeFuncIsAsync) {
+ rv.then(decrementExpectedAwaits, decrementExpectedAwaits);
+ }
+ return rv;
+ }
+ var closed = false;
+ var accumMuts = {};
+ var currentObs = {};
+ var subscription = {
+ get closed() {
+ return closed;
+ },
+ unsubscribe: function () {
+ closed = true;
+ globalEvents.storagemutated.unsubscribe(mutationListener);
+ },
+ };
+ observer.start && observer.start(subscription);
+ var querying = false, startedListening = false;
+ function shouldNotify() {
+ return keys(currentObs).some(function (key) {
+ return accumMuts[key] && rangesOverlap(accumMuts[key], currentObs[key]);
+ });
+ }
+ var mutationListener = function (parts) {
+ extendObservabilitySet(accumMuts, parts);
+ if (shouldNotify()) {
+ doQuery();
+ }
+ };
+ var doQuery = function () {
+ if (querying || closed)
+ return;
+ accumMuts = {};
+ var subscr = {};
+ var ret = execute(subscr);
+ if (!startedListening) {
+ globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, mutationListener);
+ startedListening = true;
+ }
+ querying = true;
+ Promise.resolve(ret).then(function (result) {
+ querying = false;
+ if (closed)
+ return;
+ if (shouldNotify()) {
+ doQuery();
+ }
+ else {
+ accumMuts = {};
+ currentObs = subscr;
+ observer.next && observer.next(result);
+ }
+ }, function (err) {
+ querying = false;
+ observer.error && observer.error(err);
+ subscription.unsubscribe();
+ });
+ };
+ doQuery();
+ return subscription;
+ });
+}
+
+var domDeps;
+try {
+ domDeps = {
+ indexedDB: _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB,
+ IDBKeyRange: _global.IDBKeyRange || _global.webkitIDBKeyRange
+ };
+}
+catch (e) {
+ domDeps = { indexedDB: null, IDBKeyRange: null };
+}
+
+export var Dexie = Dexie$1;
+props(Dexie, __assign(__assign({}, fullNameExceptions), {
+ delete: function (databaseName) {
+ var db = new Dexie(databaseName, { addons: [] });
+ return db.delete();
+ },
+ exists: function (name) {
+ return new Dexie(name, { addons: [] }).open().then(function (db) {
+ db.close();
+ return true;
+ }).catch('NoSuchDatabaseError', function () { return false; });
+ },
+ getDatabaseNames: function (cb) {
+ try {
+ return getDatabaseNames(Dexie.dependencies).then(cb);
+ }
+ catch (_a) {
+ return rejection(new exceptions.MissingAPI());
+ }
+ },
+ defineClass: function () {
+ function Class(content) {
+ extend(this, content);
+ }
+ return Class;
+ }, ignoreTransaction: function (scopeFunc) {
+ return PSD.trans ?
+ usePSD(PSD.transless, scopeFunc) :
+ scopeFunc();
+ }, vip: vip, async: function (generatorFn) {
+ return function () {
+ try {
+ var rv = awaitIterator(generatorFn.apply(this, arguments));
+ if (!rv || typeof rv.then !== 'function')
+ return DexiePromise.resolve(rv);
+ return rv;
+ }
+ catch (e) {
+ return rejection(e);
+ }
+ };
+ }, spawn: function (generatorFn, args, thiz) {
+ try {
+ var rv = awaitIterator(generatorFn.apply(thiz, args || []));
+ if (!rv || typeof rv.then !== 'function')
+ return DexiePromise.resolve(rv);
+ return rv;
+ }
+ catch (e) {
+ return rejection(e);
+ }
+ },
+ currentTransaction: {
+ get: function () { return PSD.trans || null; }
+ }, waitFor: function (promiseOrFunction, optionalTimeout) {
+ var promise = DexiePromise.resolve(typeof promiseOrFunction === 'function' ?
+ Dexie.ignoreTransaction(promiseOrFunction) :
+ promiseOrFunction)
+ .timeout(optionalTimeout || 60000);
+ return PSD.trans ?
+ PSD.trans.waitFor(promise) :
+ promise;
+ },
+ Promise: DexiePromise,
+ debug: {
+ get: function () { return debug; },
+ set: function (value) {
+ setDebug(value, value === 'dexie' ? function () { return true; } : dexieStackFrameFilter);
+ }
+ },
+ derive: derive, extend: extend, props: props, override: override,
+ Events: Events, on: globalEvents, liveQuery: liveQuery, extendObservabilitySet: extendObservabilitySet,
+ getByKeyPath: getByKeyPath, setByKeyPath: setByKeyPath, delByKeyPath: delByKeyPath, shallowClone: shallowClone, deepClone: deepClone, getObjectDiff: getObjectDiff, cmp: cmp, asap: asap$1,
+ minKey: minKey,
+ addons: [],
+ connections: connections,
+ errnames: errnames,
+ dependencies: domDeps,
+ semVer: DEXIE_VERSION, version: DEXIE_VERSION.split('.')
+ .map(function (n) { return parseInt(n); })
+ .reduce(function (p, c, i) { return p + (c / Math.pow(10, i * 2)); }) }));
+Dexie.maxKey = getMaxKey(Dexie.dependencies.IDBKeyRange);
+
+if (typeof dispatchEvent !== 'undefined' && typeof addEventListener !== 'undefined') {
+ globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (updatedParts) {
+ if (!propagatingLocally) {
+ var event_1;
+ if (isIEOrEdge) {
+ event_1 = document.createEvent('CustomEvent');
+ event_1.initCustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, true, true, updatedParts);
+ }
+ else {
+ event_1 = new CustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, {
+ detail: updatedParts
+ });
+ }
+ propagatingLocally = true;
+ dispatchEvent(event_1);
+ propagatingLocally = false;
+ }
+ });
+ addEventListener(STORAGE_MUTATED_DOM_EVENT_NAME, function (_a) {
+ var detail = _a.detail;
+ if (!propagatingLocally) {
+ propagateLocally(detail);
+ }
+ });
+}
+function propagateLocally(updateParts) {
+ var wasMe = propagatingLocally;
+ try {
+ propagatingLocally = true;
+ globalEvents.storagemutated.fire(updateParts);
+ }
+ finally {
+ propagatingLocally = wasMe;
+ }
+}
+var propagatingLocally = false;
+
+if (typeof BroadcastChannel !== 'undefined') {
+ var bc_1 = new BroadcastChannel(STORAGE_MUTATED_DOM_EVENT_NAME);
+ globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (changedParts) {
+ if (!propagatingLocally) {
+ bc_1.postMessage(changedParts);
+ }
+ });
+ bc_1.onmessage = function (ev) {
+ if (ev.data)
+ propagateLocally(ev.data);
+ };
+}
+else if (typeof self !== 'undefined' && typeof navigator !== 'undefined') {
+ globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (changedParts) {
+ try {
+ if (!propagatingLocally) {
+ if (typeof localStorage !== 'undefined') {
+ localStorage.setItem(STORAGE_MUTATED_DOM_EVENT_NAME, JSON.stringify({
+ trig: Math.random(),
+ changedParts: changedParts,
+ }));
+ }
+ if (typeof self['clients'] === 'object') {
+ __spreadArray([], self['clients'].matchAll({ includeUncontrolled: true }), true).forEach(function (client) {
+ return client.postMessage({
+ type: STORAGE_MUTATED_DOM_EVENT_NAME,
+ changedParts: changedParts,
+ });
+ });
+ }
+ }
+ }
+ catch (_a) { }
+ });
+ if (typeof addEventListener !== 'undefined') {
+ addEventListener('storage', function (ev) {
+ if (ev.key === STORAGE_MUTATED_DOM_EVENT_NAME) {
+ var data = JSON.parse(ev.newValue);
+ if (data)
+ propagateLocally(data.changedParts);
+ }
+ });
+ }
+ var swContainer = self.document && navigator.serviceWorker;
+ if (swContainer) {
+ swContainer.addEventListener('message', propagateMessageLocally);
+ }
+}
+function propagateMessageLocally(_a) {
+ var data = _a.data;
+ if (data && data.type === STORAGE_MUTATED_DOM_EVENT_NAME) {
+ propagateLocally(data.changedParts);
+ }
+}
+
+DexiePromise.rejectionMapper = mapError;
+setDebug(debug, dexieStackFrameFilter);
+
+var namedExports = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ Dexie: Dexie$1,
+ liveQuery: liveQuery,
+ 'default': Dexie$1,
+ RangeSet: RangeSet,
+ mergeRanges: mergeRanges,
+ rangesOverlap: rangesOverlap
+});
+
+__assign(Dexie$1, namedExports, { default: Dexie$1 });
+
diff --git a/addon/lib/dropbox/auth.js b/addon/lib/dropbox/auth.js
new file mode 100644
index 00000000..b4fa2245
--- /dev/null
+++ b/addon/lib/dropbox/auth.js
@@ -0,0 +1,436 @@
+import {
+ getTokenExpiresAtDate,
+ isBrowserEnv,
+ createBrowserSafeString,
+} from './utils.js';
+import { parseResponse } from './response.js';
+
+let fetch;
+if (isBrowserEnv()) {
+ fetch = globalThis.fetch.bind(globalThis);
+} else {
+ fetch = require('node-fetch'); // eslint-disable-line global-require
+}
+
+let crypto;
+if (isBrowserEnv()) {
+ crypto = globalThis.crypto || globalThis.msCrypto; // for IE11
+} else {
+ crypto = require('crypto'); // eslint-disable-line global-require
+}
+
+let Encoder;
+if (typeof TextEncoder === 'undefined') {
+ Encoder = require('util').TextEncoder; // eslint-disable-line global-require
+} else {
+ Encoder = TextEncoder;
+}
+
+// Expiration is 300 seconds but needs to be in milliseconds for Date object
+const TokenExpirationBuffer = 300 * 1000;
+const PKCELength = 128;
+const TokenAccessTypes = ['legacy', 'offline', 'online'];
+const GrantTypes = ['code', 'token'];
+const IncludeGrantedScopes = ['none', 'user', 'team'];
+const BaseAuthorizeUrl = 'https://www.dropbox.com/oauth2/authorize';
+const BaseTokenUrl = 'https://api.dropboxapi.com/oauth2/token';
+
+/**
+ * @class DropboxAuth
+ * @classdesc The DropboxAuth class that provides methods to manage, acquire, and refresh tokens.
+ * @arg {Object} options
+ * @arg {Function} [options.fetch] - fetch library for making requests.
+ * @arg {String} [options.accessToken] - An access token for making authenticated
+ * requests.
+ * @arg {Date} [options.AccessTokenExpiresAt] - Date of the current access token's
+ * expiration (if available)
+ * @arg {String} [options.refreshToken] - A refresh token for retrieving access tokens
+ * @arg {String} [options.clientId] - The client id for your app. Used to create
+ * authentication URL.
+ * @arg {String} [options.clientSecret] - The client secret for your app. Used to create
+ * authentication URL and refresh access tokens.
+ */
+export default class DropboxAuth {
+ constructor(options) {
+ options = options || {};
+
+ this.fetch = options.fetch || fetch;
+ this.accessToken = options.accessToken;
+ this.accessTokenExpiresAt = options.accessTokenExpiresAt;
+ this.refreshToken = options.refreshToken;
+ this.clientId = options.clientId;
+ this.clientSecret = options.clientSecret;
+ }
+
+ /**
+ * Set the access token used to authenticate requests to the API.
+ * @arg {String} accessToken - An access token
+ * @returns {undefined}
+ */
+ setAccessToken(accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ /**
+ * Get the access token
+ * @returns {String} Access token
+ */
+ getAccessToken() {
+ return this.accessToken;
+ }
+
+ /**
+ * Set the client id, which is used to help gain an access token.
+ * @arg {String} clientId - Your apps client id
+ * @returns {undefined}
+ */
+ setClientId(clientId) {
+ this.clientId = clientId;
+ }
+
+ /**
+ * Get the client id
+ * @returns {String} Client id
+ */
+ getClientId() {
+ return this.clientId;
+ }
+
+ /**
+ * Set the client secret
+ * @arg {String} clientSecret - Your app's client secret
+ * @returns {undefined}
+ */
+ setClientSecret(clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ /**
+ * Get the client secret
+ * @returns {String} Client secret
+ */
+ getClientSecret() {
+ return this.clientSecret;
+ }
+
+ /**
+ * Gets the refresh token
+ * @returns {String} Refresh token
+ */
+ getRefreshToken() {
+ return this.refreshToken;
+ }
+
+ /**
+ * Sets the refresh token
+ * @param refreshToken - A refresh token
+ */
+ setRefreshToken(refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ /**
+ * Gets the access token's expiration date
+ * @returns {Date} date of token expiration
+ */
+ getAccessTokenExpiresAt() {
+ return this.accessTokenExpiresAt;
+ }
+
+ /**
+ * Sets the access token's expiration date
+ * @param accessTokenExpiresAt - new expiration date
+ */
+ setAccessTokenExpiresAt(accessTokenExpiresAt) {
+ this.accessTokenExpiresAt = accessTokenExpiresAt;
+ }
+
+ /**
+ * Sets the code verifier for PKCE flow
+ * @param {String} codeVerifier - new code verifier
+ */
+ setCodeVerifier(codeVerifier) {
+ this.codeVerifier = codeVerifier;
+ }
+
+ /**
+ * Gets the code verifier for PKCE flow
+ * @returns {String} - code verifier for PKCE
+ */
+ getCodeVerifier() {
+ return this.codeVerifier;
+ }
+
+ generateCodeChallenge() {
+ const encoder = new Encoder();
+ const codeData = encoder.encode(this.codeVerifier);
+ let codeChallenge;
+ if (isBrowserEnv()) {
+ return crypto.subtle.digest('SHA-256', codeData)
+ .then((digestedHash) => {
+ const base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(digestedHash)));
+ codeChallenge = createBrowserSafeString(base64String).substr(0, 128);
+ this.codeChallenge = codeChallenge;
+ });
+ }
+ const digestedHash = crypto.createHash('sha256').update(codeData).digest();
+ codeChallenge = createBrowserSafeString(digestedHash);
+ this.codeChallenge = codeChallenge;
+ return Promise.resolve();
+ }
+
+ generatePKCECodes() {
+ let codeVerifier;
+ if (isBrowserEnv()) {
+ const array = new Uint8Array(PKCELength);
+ const randomValueArray = crypto.getRandomValues(array);
+ const base64String = btoa(randomValueArray);
+ codeVerifier = createBrowserSafeString(base64String).substr(0, 128);
+ } else {
+ const randomBytes = crypto.randomBytes(PKCELength);
+ codeVerifier = createBrowserSafeString(randomBytes).substr(0, 128);
+ }
+ this.codeVerifier = codeVerifier;
+
+ return this.generateCodeChallenge();
+ }
+
+ /**
+ * Get a URL that can be used to authenticate users for the Dropbox API.
+ * @arg {String} redirectUri - A URL to redirect the user to after
+ * authenticating. This must be added to your app through the admin interface.
+ * @arg {String} [state] - State that will be returned in the redirect URL to help
+ * prevent cross site scripting attacks.
+ * @arg {String} [authType] - auth type, defaults to 'token', other option is 'code'
+ * @arg {String} [tokenAccessType] - type of token to request. From the following:
+ * null - creates a token with the app default (either legacy or online)
+ * legacy - creates one long-lived token with no expiration
+ * online - create one short-lived token with an expiration
+ * offline - create one short-lived token with an expiration with a refresh token
+ * @arg {Array} [scope] - scopes to request for the grant
+ * @arg {String} [includeGrantedScopes] - whether or not to include previously granted scopes.
+ * From the following:
+ * user - include user scopes in the grant
+ * team - include team scopes in the grant
+ * Note: if this user has never linked the app, include_granted_scopes must be None
+ * @arg {boolean} [usePKCE] - Whether or not to use Sha256 based PKCE. PKCE should be only use
+ * on client apps which doesn't call your server. It is less secure than non-PKCE flow but
+ * can be used if you are unable to safely retrieve your app secret
+ * @returns {Promise} - Url to send user to for Dropbox API authentication
+ * returned in a promise
+ */
+ getAuthenticationUrl(redirectUri, state, authType = 'token', tokenAccessType = null, scope = null, includeGrantedScopes = 'none', usePKCE = false) {
+ const clientId = this.getClientId();
+ const baseUrl = BaseAuthorizeUrl;
+
+ if (!clientId) {
+ throw new Error('A client id is required. You can set the client id using .setClientId().');
+ }
+ if (authType !== 'code' && !redirectUri) {
+ throw new Error('A redirect uri is required.');
+ }
+ if (!GrantTypes.includes(authType)) {
+ throw new Error('Authorization type must be code or token');
+ }
+ if (tokenAccessType && !TokenAccessTypes.includes(tokenAccessType)) {
+ throw new Error('Token Access Type must be legacy, offline, or online');
+ }
+ if (scope && !(scope instanceof Array)) {
+ throw new Error('Scope must be an array of strings');
+ }
+ if (!IncludeGrantedScopes.includes(includeGrantedScopes)) {
+ throw new Error('includeGrantedScopes must be none, user, or team');
+ }
+
+ let authUrl;
+ if (authType === 'code') {
+ authUrl = `${baseUrl}?response_type=code&client_id=${clientId}`;
+ } else {
+ authUrl = `${baseUrl}?response_type=token&client_id=${clientId}`;
+ }
+
+ if (redirectUri) {
+ authUrl += `&redirect_uri=${redirectUri}`;
+ }
+ if (state) {
+ authUrl += `&state=${state}`;
+ }
+ if (tokenAccessType) {
+ authUrl += `&token_access_type=${tokenAccessType}`;
+ }
+ if (scope) {
+ authUrl += `&scope=${scope.join(' ')}`;
+ }
+ if (includeGrantedScopes !== 'none') {
+ authUrl += `&include_granted_scopes=${includeGrantedScopes}`;
+ }
+ if (usePKCE) {
+ return this.generatePKCECodes()
+ .then(() => {
+ authUrl += '&code_challenge_method=S256';
+ authUrl += `&code_challenge=${this.codeChallenge}`;
+ return authUrl;
+ });
+ }
+ return Promise.resolve(authUrl);
+ }
+
+ /**
+ * Get an OAuth2 access token from an OAuth2 Code.
+ * @arg {String} redirectUri - A URL to redirect the user to after
+ * authenticating. This must be added to your app through the admin interface.
+ * @arg {String} code - An OAuth2 code.
+ * @returns {Object} An object containing the token and related info (if applicable)
+ */
+ getAccessTokenFromCode(redirectUri, code) {
+ const clientId = this.getClientId();
+ const clientSecret = this.getClientSecret();
+
+ if (!clientId) {
+ throw new Error('A client id is required. You can set the client id using .setClientId().');
+ }
+ let path = BaseTokenUrl;
+ path += '?grant_type=authorization_code';
+ path += `&code=${code}`;
+ path += `&client_id=${clientId}`;
+
+ if (clientSecret) {
+ path += `&client_secret=${clientSecret}`;
+ } else {
+ if (!this.codeVerifier) {
+ throw new Error('You must use PKCE when generating the authorization URL to not include a client secret');
+ }
+ path += `&code_verifier=${this.codeVerifier}`;
+ }
+ if (redirectUri) {
+ path += `&redirect_uri=${redirectUri}`;
+ }
+
+ const fetchOptions = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ };
+ return this.fetch(path, fetchOptions)
+ .then((res) => parseResponse(res));
+ }
+
+ /**
+ * Checks if a token is needed, can be refreshed and if the token is expired.
+ * If so, attempts to refresh access token
+ * @returns {Promise<*>}
+ */
+ checkAndRefreshAccessToken() {
+ const canRefresh = this.getRefreshToken() && this.getClientId();
+ const needsRefresh = !this.getAccessTokenExpiresAt()
+ || (new Date(Date.now() + TokenExpirationBuffer)) >= this.getAccessTokenExpiresAt();
+ const needsToken = !this.getAccessToken();
+ if ((needsRefresh || needsToken) && canRefresh) {
+ return this.refreshAccessToken();
+ }
+ return Promise.resolve();
+ }
+
+ /**
+ * Refreshes the access token using the refresh token, if available
+ * @arg {Array} scope - a subset of scopes from the original
+ * refresh to acquire with an access token
+ * @returns {Promise<*>}
+ */
+ refreshAccessToken(scope = null) {
+ let refreshUrl = BaseTokenUrl;
+ const clientId = this.getClientId();
+ const clientSecret = this.getClientSecret();
+
+ if (!clientId) {
+ throw new Error('A client id is required. You can set the client id using .setClientId().');
+ }
+ if (scope && !(scope instanceof Array)) {
+ throw new Error('Scope must be an array of strings');
+ }
+
+ const headers = {};
+ headers['Content-Type'] = 'application/json';
+ refreshUrl += `?grant_type=refresh_token&refresh_token=${this.getRefreshToken()}`;
+ refreshUrl += `&client_id=${clientId}`;
+ if (clientSecret) {
+ refreshUrl += `&client_secret=${clientSecret}`;
+ }
+ if (scope) {
+ refreshUrl += `&scope=${scope.join(' ')}`;
+ }
+ const fetchOptions = {
+ method: 'POST',
+ };
+
+ fetchOptions.headers = headers;
+
+ return this.fetch(refreshUrl, fetchOptions)
+ .then((res) => parseResponse(res))
+ .then((res) => {
+ this.setAccessToken(res.result.access_token);
+ this.setAccessTokenExpiresAt(getTokenExpiresAtDate(res.result.expires_in));
+ });
+ }
+
+ /**
+ * An authentication process that works with cordova applications.
+ * @param {successCallback} successCallback
+ * @param {errorCallback} errorCallback
+ */
+ authenticateWithCordova(successCallback, errorCallback) {
+ const redirectUrl = 'https://www.dropbox.com/1/oauth2/redirect_receiver';
+ this.getAuthenticationUrl(redirectUrl)
+ .then((url) => {
+ let removed = false;
+ const browser = globalThis.open(url, '_blank');
+
+ function onLoadError(event) {
+ // Workaround to fix wrong behavior on cordova-plugin-inappbrowser
+ if (event.code !== -999) {
+ // Try to avoid a browser crash on browser.close().
+ globalThis.setTimeout(() => { browser.close(); }, 10);
+ errorCallback();
+ }
+ }
+
+ function onLoadStop(event) {
+ const errorLabel = '&error=';
+ const errorIndex = event.url.indexOf(errorLabel);
+
+ if (errorIndex > -1) {
+ // Try to avoid a browser crash on browser.close().
+ globalThis.setTimeout(() => { browser.close(); }, 10);
+ errorCallback();
+ } else {
+ const tokenLabel = '#access_token=';
+ let tokenIndex = event.url.indexOf(tokenLabel);
+ const tokenTypeIndex = event.url.indexOf('&token_type=');
+ if (tokenIndex > -1) {
+ tokenIndex += tokenLabel.length;
+ // Try to avoid a browser crash on browser.close().
+ globalThis.setTimeout(() => { browser.close(); }, 10);
+
+ const accessToken = event.url.substring(tokenIndex, tokenTypeIndex);
+ successCallback(accessToken);
+ }
+ }
+ }
+
+ function onExit() {
+ if (removed) {
+ return;
+ }
+ browser.removeEventListener('loaderror', onLoadError);
+ browser.removeEventListener('loadstop', onLoadStop);
+ browser.removeEventListener('exit', onExit);
+ removed = true;
+ }
+
+ browser.addEventListener('loaderror', onLoadError);
+ browser.addEventListener('loadstop', onLoadStop);
+ browser.addEventListener('exit', onExit);
+ });
+ }
+}
diff --git a/addon/lib/dropbox/constants.js b/addon/lib/dropbox/constants.js
new file mode 100644
index 00000000..747ffc38
--- /dev/null
+++ b/addon/lib/dropbox/constants.js
@@ -0,0 +1,8 @@
+export const RPC = 'rpc';
+export const UPLOAD = 'upload';
+export const DOWNLOAD = 'download';
+
+export const APP_AUTH = 'app';
+export const USER_AUTH = 'user';
+export const TEAM_AUTH = 'team';
+export const NO_AUTH = 'noauth';
diff --git a/addon/lib/dropbox/dropbox.js b/addon/lib/dropbox/dropbox.js
new file mode 100644
index 00000000..49b6bafe
--- /dev/null
+++ b/addon/lib/dropbox/dropbox.js
@@ -0,0 +1,194 @@
+import {
+ UPLOAD,
+ DOWNLOAD,
+ RPC,
+ APP_AUTH,
+ TEAM_AUTH,
+ USER_AUTH,
+ NO_AUTH,
+} from './constants.js';
+import { routes } from './routes.js';
+import DropboxAuth from './auth.js';
+import { getBaseURL, httpHeaderSafeJson } from './utils.js';
+import { parseDownloadResponse, parseResponse } from './response.js';
+
+let fetch;
+if (typeof globalThis !== 'undefined') {
+ fetch = globalThis.fetch.bind(globalThis);
+} else {
+ fetch = require('node-fetch'); // eslint-disable-line global-require
+}
+
+const b64 = typeof btoa === 'undefined'
+ ? (str) => Buffer.from(str).toString('base64')
+ : btoa;
+
+/**
+ * @class Dropbox
+ * @classdesc The Dropbox SDK class that provides methods to read, write and
+ * create files or folders in a user or team's Dropbox.
+ * @arg {Object} options
+ * @arg {Function} [options.fetch] - fetch library for making requests.
+ * @arg {String} [options.selectUser] - Select user is only used for team functionality.
+ * It specifies which user the team access token should be acting as.
+ * @arg {String} [options.pathRoot] - root path to access other namespaces
+ * Use to access team folders for example
+ * @arg {String} [options.selectAdmin] - Select admin is only used by team functionality.
+ * It specifies which team admin the team access token should be acting as.
+ * @arg {DropboxAuth} [options.auth] - The DropboxAuth object used to authenticate requests.
+ * If this is set, the remaining parameters will be ignored.
+ * @arg {String} [options.accessToken] - An access token for making authenticated
+ * requests.
+ * @arg {Date} [options.accessTokenExpiresAt] - Date of the current access token's
+ * expiration (if available)
+ * @arg {String} [options.refreshToken] - A refresh token for retrieving access tokens
+ * @arg {String} [options.clientId] - The client id for your app. Used to create
+ * authentication URL.
+ * @arg {String} [options.clientSecret] - The client secret for your app. Used to create
+ * authentication URL and refresh access tokens.
+ */
+export default class Dropbox {
+ constructor(options) {
+ options = options || {};
+
+ if (options.auth) {
+ this.auth = options.auth;
+ } else {
+ this.auth = new DropboxAuth(options);
+ }
+
+ this.fetch = options.fetch || fetch;
+ this.selectUser = options.selectUser;
+ this.selectAdmin = options.selectAdmin;
+ this.pathRoot = options.pathRoot;
+
+ Object.assign(this, routes);
+ }
+
+ request(path, args, auth, host, style) {
+ // checks for multiauth and assigns auth based on priority to create header in switch case
+ if (auth.split(',').length > 1) {
+ const authTypes = auth.replace(' ', '').split(',');
+ if (authTypes.includes(USER_AUTH) && this.auth.getAccessToken()) {
+ auth = USER_AUTH;
+ } else if (authTypes.includes(TEAM_AUTH) && this.auth.getAccessToken()) {
+ auth = TEAM_AUTH;
+ } else if (authTypes.includes(APP_AUTH)) {
+ auth = APP_AUTH;
+ }
+ }
+
+ switch (style) {
+ case RPC:
+ return this.rpcRequest(path, args, auth, host);
+ case DOWNLOAD:
+ return this.downloadRequest(path, args, auth, host);
+ case UPLOAD:
+ return this.uploadRequest(path, args, auth, host);
+ default:
+ throw new Error(`Invalid request style: ${style}`);
+ }
+ }
+
+ rpcRequest(path, body, auth, host) {
+ return this.auth.checkAndRefreshAccessToken()
+ .then(() => {
+ const fetchOptions = {
+ method: 'POST',
+ body: (body) ? JSON.stringify(body) : null,
+ headers: {},
+ };
+
+ if (body) {
+ fetchOptions.headers['Content-Type'] = 'application/json';
+ }
+
+ let authHeader;
+ switch (auth) {
+ case APP_AUTH:
+ if (!this.auth.clientId || !this.auth.clientSecret) {
+ throw new Error('A client id and secret is required for this function');
+ }
+ authHeader = b64(`${this.auth.clientId}:${this.auth.clientSecret}`);
+ fetchOptions.headers.Authorization = `Basic ${authHeader}`;
+ break;
+ case TEAM_AUTH:
+ case USER_AUTH:
+ fetchOptions.headers.Authorization = `Bearer ${this.auth.getAccessToken()}`;
+ break;
+ case NO_AUTH:
+ break;
+ default:
+ throw new Error(`Unhandled auth type: ${auth}`);
+ }
+
+ this.setCommonHeaders(fetchOptions);
+ return fetchOptions;
+ })
+ .then((fetchOptions) => this.fetch(getBaseURL(host) + path, fetchOptions))
+ .then((res) => parseResponse(res));
+ }
+
+ downloadRequest(path, args, auth, host) {
+ return this.auth.checkAndRefreshAccessToken()
+ .then(() => {
+ if (auth !== USER_AUTH) {
+ throw new Error(`Unexpected auth type: ${auth}`);
+ }
+
+ const fetchOptions = {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${this.auth.getAccessToken()}`,
+ 'Dropbox-API-Arg': httpHeaderSafeJson(args),
+ },
+ };
+
+ this.setCommonHeaders(fetchOptions);
+
+ return fetchOptions;
+ })
+ .then((fetchOptions) => fetch(getBaseURL(host) + path, fetchOptions))
+ .then((res) => parseDownloadResponse(res));
+ }
+
+ uploadRequest(path, args, auth, host) {
+ return this.auth.checkAndRefreshAccessToken()
+ .then(() => {
+ if (auth !== USER_AUTH) {
+ throw new Error(`Unexpected auth type: ${auth}`);
+ }
+
+ const { contents } = args;
+ delete args.contents;
+
+ const fetchOptions = {
+ body: contents,
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${this.auth.getAccessToken()}`,
+ 'Content-Type': 'application/octet-stream',
+ 'Dropbox-API-Arg': httpHeaderSafeJson(args),
+ },
+ };
+
+ this.setCommonHeaders(fetchOptions);
+
+ return fetchOptions;
+ })
+ .then((fetchOptions) => this.fetch(getBaseURL(host) + path, fetchOptions))
+ .then((res) => parseResponse(res));
+ }
+
+ setCommonHeaders(options) {
+ if (this.selectUser) {
+ options.headers['Dropbox-API-Select-User'] = this.selectUser;
+ }
+ if (this.selectAdmin) {
+ options.headers['Dropbox-API-Select-Admin'] = this.selectAdmin;
+ }
+ if (this.pathRoot) {
+ options.headers['Dropbox-API-Path-Root'] = this.pathRoot;
+ }
+ }
+}
diff --git a/addon/lib/dropbox/error.js b/addon/lib/dropbox/error.js
new file mode 100644
index 00000000..d3e186d1
--- /dev/null
+++ b/addon/lib/dropbox/error.js
@@ -0,0 +1,17 @@
+/**
+ * The response class of HTTP errors from API calls using the Dropbox SDK.
+ * @class DropboxResponseError
+ * @classdesc The response class of HTTP errors from API calls using the Dropbox SDK.
+ * @arg {number} status - HTTP Status code of the call
+ * @arg {Object} headers - Headers returned from the call
+ * @arg {Object} error - Serialized Error of the call
+ */
+export class DropboxResponseError extends Error {
+ constructor(status, headers, error) {
+ super(`Response failed with a ${status} code`);
+ this.name = 'DropboxResponseError';
+ this.status = status;
+ this.headers = headers;
+ this.error = error;
+ }
+}
diff --git a/addon/lib/dropbox/response.js b/addon/lib/dropbox/response.js
new file mode 100644
index 00000000..2753bb7b
--- /dev/null
+++ b/addon/lib/dropbox/response.js
@@ -0,0 +1,64 @@
+import { isWindowOrWorker } from './utils.js';
+import { DropboxResponseError } from './error.js';
+
+export class DropboxResponse {
+ constructor(status, headers, result) {
+ this.status = status;
+ this.headers = headers;
+ this.result = result;
+ }
+}
+
+function throwAsError(res) {
+ return res.text()
+ .then((data) => {
+ let errorObject;
+ try {
+ errorObject = JSON.parse(data);
+ } catch (error) {
+ errorObject = data;
+ }
+
+ throw new DropboxResponseError(res.status, res.headers, errorObject);
+ });
+}
+
+export function parseResponse(res) {
+ if (!res.ok) {
+ return throwAsError(res);
+ }
+ return res.text()
+ .then((data) => {
+ let responseObject;
+ try {
+ responseObject = JSON.parse(data);
+ } catch (error) {
+ responseObject = data;
+ }
+
+ return new DropboxResponse(res.status, res.headers, responseObject);
+ });
+}
+
+export function parseDownloadResponse(res) {
+ if (!res.ok) {
+ return throwAsError(res);
+ }
+ return new Promise((resolve) => {
+ if (isWindowOrWorker()) {
+ res.blob().then((data) => resolve(data));
+ } else {
+ res.buffer().then((data) => resolve(data));
+ }
+ }).then((data) => {
+ const result = JSON.parse(res.headers.get('dropbox-api-result'));
+
+ if (isWindowOrWorker()) {
+ result.fileBlob = data;
+ } else {
+ result.fileBinary = data;
+ }
+
+ return new DropboxResponse(res.status, res.headers, result);
+ });
+}
diff --git a/addon/lib/dropbox/routes.js b/addon/lib/dropbox/routes.js
new file mode 100644
index 00000000..9aec2b37
--- /dev/null
+++ b/addon/lib/dropbox/routes.js
@@ -0,0 +1,3030 @@
+// Auto-generated by Stone, do not modify.
+var routes = {};
+
+/**
+ * Sets a user's profile photo.
+ * @function Dropbox#accountSetProfilePhoto
+ * @arg {AccountSetProfilePhotoArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.accountSetProfilePhoto = function (arg) {
+ return this.request('account/set_profile_photo', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Creates an OAuth 2.0 access token from the supplied OAuth 1.0 access token.
+ * @function Dropbox#authTokenFromOauth1
+ * @arg {AuthTokenFromOAuth1Arg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.authTokenFromOauth1 = function (arg) {
+ return this.request('auth/token/from_oauth1', arg, 'app', 'api', 'rpc');
+};
+
+/**
+ * Disables the access token used to authenticate the call. If there is a
+ * corresponding refresh token for the access token, this disables that refresh
+ * token, as well as any other access tokens for that refresh token.
+ * @function Dropbox#authTokenRevoke
+ * @returns {Promise., Error.>}
+ */
+routes.authTokenRevoke = function () {
+ return this.request('auth/token/revoke', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * This endpoint performs App Authentication, validating the supplied app key
+ * and secret, and returns the supplied string, to allow you to test your code
+ * and connection to the Dropbox API. It has no other effect. If you receive an
+ * HTTP 200 response with the supplied query, it indicates at least part of the
+ * Dropbox API infrastructure is working and that the app key and secret valid.
+ * @function Dropbox#checkApp
+ * @arg {CheckEchoArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.checkApp = function (arg) {
+ return this.request('check/app', arg, 'app', 'api', 'rpc');
+};
+
+/**
+ * This endpoint performs User Authentication, validating the supplied access
+ * token, and returns the supplied string, to allow you to test your code and
+ * connection to the Dropbox API. It has no other effect. If you receive an HTTP
+ * 200 response with the supplied query, it indicates at least part of the
+ * Dropbox API infrastructure is working and that the access token is valid.
+ * @function Dropbox#checkUser
+ * @arg {CheckEchoArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.checkUser = function (arg) {
+ return this.request('check/user', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Removes all manually added contacts. You'll still keep contacts who are on
+ * your team or who you imported. New contacts will be added when you share.
+ * @function Dropbox#contactsDeleteManualContacts
+ * @returns {Promise., Error.>}
+ */
+routes.contactsDeleteManualContacts = function () {
+ return this.request('contacts/delete_manual_contacts', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * Removes manually added contacts from the given list.
+ * @function Dropbox#contactsDeleteManualContactsBatch
+ * @arg {ContactsDeleteManualContactsArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.contactsDeleteManualContactsBatch = function (arg) {
+ return this.request('contacts/delete_manual_contacts_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Add property groups to a Dropbox file. See templates/add_for_user or
+ * templates/add_for_team to create new templates.
+ * @function Dropbox#filePropertiesPropertiesAdd
+ * @arg {FilePropertiesAddPropertiesArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesPropertiesAdd = function (arg) {
+ return this.request('file_properties/properties/add', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Overwrite property groups associated with a file. This endpoint should be
+ * used instead of properties/update when property groups are being updated via
+ * a "snapshot" instead of via a "delta". In other words, this endpoint will
+ * delete all omitted fields from a property group, whereas properties/update
+ * will only delete fields that are explicitly marked for deletion.
+ * @function Dropbox#filePropertiesPropertiesOverwrite
+ * @arg {FilePropertiesOverwritePropertyGroupArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesPropertiesOverwrite = function (arg) {
+ return this.request('file_properties/properties/overwrite', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Permanently removes the specified property group from the file. To remove
+ * specific property field key value pairs, see properties/update. To update a
+ * template, see templates/update_for_user or templates/update_for_team. To
+ * remove a template, see templates/remove_for_user or
+ * templates/remove_for_team.
+ * @function Dropbox#filePropertiesPropertiesRemove
+ * @arg {FilePropertiesRemovePropertiesArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesPropertiesRemove = function (arg) {
+ return this.request('file_properties/properties/remove', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Search across property templates for particular property field values.
+ * @function Dropbox#filePropertiesPropertiesSearch
+ * @arg {FilePropertiesPropertiesSearchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesPropertiesSearch = function (arg) {
+ return this.request('file_properties/properties/search', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Once a cursor has been retrieved from properties/search, use this to paginate
+ * through all search results.
+ * @function Dropbox#filePropertiesPropertiesSearchContinue
+ * @arg {FilePropertiesPropertiesSearchContinueArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesPropertiesSearchContinue = function (arg) {
+ return this.request('file_properties/properties/search/continue', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Add, update or remove properties associated with the supplied file and
+ * templates. This endpoint should be used instead of properties/overwrite when
+ * property groups are being updated via a "delta" instead of via a "snapshot" .
+ * In other words, this endpoint will not delete any omitted fields from a
+ * property group, whereas properties/overwrite will delete any fields that are
+ * omitted from a property group.
+ * @function Dropbox#filePropertiesPropertiesUpdate
+ * @arg {FilePropertiesUpdatePropertiesArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesPropertiesUpdate = function (arg) {
+ return this.request('file_properties/properties/update', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Add a template associated with a team. See properties/add to add properties
+ * to a file or folder. Note: this endpoint will create team-owned templates.
+ * @function Dropbox#filePropertiesTemplatesAddForTeam
+ * @arg {FilePropertiesAddTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesAddForTeam = function (arg) {
+ return this.request('file_properties/templates/add_for_team', arg, 'team', 'api', 'rpc');
+};
+
+/**
+ * Add a template associated with a user. See properties/add to add properties
+ * to a file. This endpoint can't be called on a team member or admin's behalf.
+ * @function Dropbox#filePropertiesTemplatesAddForUser
+ * @arg {FilePropertiesAddTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesAddForUser = function (arg) {
+ return this.request('file_properties/templates/add_for_user', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Get the schema for a specified template.
+ * @function Dropbox#filePropertiesTemplatesGetForTeam
+ * @arg {FilePropertiesGetTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesGetForTeam = function (arg) {
+ return this.request('file_properties/templates/get_for_team', arg, 'team', 'api', 'rpc');
+};
+
+/**
+ * Get the schema for a specified template. This endpoint can't be called on a
+ * team member or admin's behalf.
+ * @function Dropbox#filePropertiesTemplatesGetForUser
+ * @arg {FilePropertiesGetTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesGetForUser = function (arg) {
+ return this.request('file_properties/templates/get_for_user', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Get the template identifiers for a team. To get the schema of each template
+ * use templates/get_for_team.
+ * @function Dropbox#filePropertiesTemplatesListForTeam
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesListForTeam = function () {
+ return this.request('file_properties/templates/list_for_team', null, 'team', 'api', 'rpc');
+};
+
+/**
+ * Get the template identifiers for a team. To get the schema of each template
+ * use templates/get_for_user. This endpoint can't be called on a team member or
+ * admin's behalf.
+ * @function Dropbox#filePropertiesTemplatesListForUser
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesListForUser = function () {
+ return this.request('file_properties/templates/list_for_user', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * Permanently removes the specified template created from
+ * templates/add_for_user. All properties associated with the template will also
+ * be removed. This action cannot be undone.
+ * @function Dropbox#filePropertiesTemplatesRemoveForTeam
+ * @arg {FilePropertiesRemoveTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesRemoveForTeam = function (arg) {
+ return this.request('file_properties/templates/remove_for_team', arg, 'team', 'api', 'rpc');
+};
+
+/**
+ * Permanently removes the specified template created from
+ * templates/add_for_user. All properties associated with the template will also
+ * be removed. This action cannot be undone.
+ * @function Dropbox#filePropertiesTemplatesRemoveForUser
+ * @arg {FilePropertiesRemoveTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesRemoveForUser = function (arg) {
+ return this.request('file_properties/templates/remove_for_user', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Update a template associated with a team. This route can update the template
+ * name, the template description and add optional properties to templates.
+ * @function Dropbox#filePropertiesTemplatesUpdateForTeam
+ * @arg {FilePropertiesUpdateTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesUpdateForTeam = function (arg) {
+ return this.request('file_properties/templates/update_for_team', arg, 'team', 'api', 'rpc');
+};
+
+/**
+ * Update a template associated with a user. This route can update the template
+ * name, the template description and add optional properties to templates. This
+ * endpoint can't be called on a team member or admin's behalf.
+ * @function Dropbox#filePropertiesTemplatesUpdateForUser
+ * @arg {FilePropertiesUpdateTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filePropertiesTemplatesUpdateForUser = function (arg) {
+ return this.request('file_properties/templates/update_for_user', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the total number of file requests owned by this user. Includes both
+ * open and closed file requests.
+ * @function Dropbox#fileRequestsCount
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsCount = function () {
+ return this.request('file_requests/count', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * Creates a file request for this user.
+ * @function Dropbox#fileRequestsCreate
+ * @arg {FileRequestsCreateFileRequestArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsCreate = function (arg) {
+ return this.request('file_requests/create', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Delete a batch of closed file requests.
+ * @function Dropbox#fileRequestsDelete
+ * @arg {FileRequestsDeleteFileRequestArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsDelete = function (arg) {
+ return this.request('file_requests/delete', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Delete all closed file requests owned by this user.
+ * @function Dropbox#fileRequestsDeleteAllClosed
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsDeleteAllClosed = function () {
+ return this.request('file_requests/delete_all_closed', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the specified file request.
+ * @function Dropbox#fileRequestsGet
+ * @arg {FileRequestsGetFileRequestArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsGet = function (arg) {
+ return this.request('file_requests/get', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns a list of file requests owned by this user. For apps with the app
+ * folder permission, this will only return file requests with destinations in
+ * the app folder.
+ * @function Dropbox#fileRequestsListV2
+ * @arg {FileRequestsListFileRequestsArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsListV2 = function (arg) {
+ return this.request('file_requests/list_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns a list of file requests owned by this user. For apps with the app
+ * folder permission, this will only return file requests with destinations in
+ * the app folder.
+ * @function Dropbox#fileRequestsList
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsList = function () {
+ return this.request('file_requests/list', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * Once a cursor has been retrieved from list_v2, use this to paginate through
+ * all file requests. The cursor must come from a previous call to list_v2 or
+ * list/continue.
+ * @function Dropbox#fileRequestsListContinue
+ * @arg {FileRequestsListFileRequestsContinueArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsListContinue = function (arg) {
+ return this.request('file_requests/list/continue', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Update a file request.
+ * @function Dropbox#fileRequestsUpdate
+ * @arg {FileRequestsUpdateFileRequestArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.fileRequestsUpdate = function (arg) {
+ return this.request('file_requests/update', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the metadata for a file or folder. This is an alpha endpoint
+ * compatible with the properties API. Note: Metadata for the root folder is
+ * unsupported.
+ * @function Dropbox#filesAlphaGetMetadata
+ * @deprecated
+ * @arg {FilesAlphaGetMetadataArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesAlphaGetMetadata = function (arg) {
+ return this.request('files/alpha/get_metadata', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Create a new file with the contents provided in the request. Note that this
+ * endpoint is part of the properties API alpha and is slightly different from
+ * upload. Do not use this to upload a file larger than 150 MB. Instead, create
+ * an upload session with upload_session/start.
+ * @function Dropbox#filesAlphaUpload
+ * @deprecated
+ * @arg {FilesCommitInfoWithProperties} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesAlphaUpload = function (arg) {
+ return this.request('files/alpha/upload', arg, 'user', 'content', 'upload');
+};
+
+/**
+ * Copy a file or folder to a different location in the user's Dropbox. If the
+ * source path is a folder all its contents will be copied.
+ * @function Dropbox#filesCopyV2
+ * @arg {FilesRelocationArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyV2 = function (arg) {
+ return this.request('files/copy_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Copy a file or folder to a different location in the user's Dropbox. If the
+ * source path is a folder all its contents will be copied.
+ * @function Dropbox#filesCopy
+ * @deprecated
+ * @arg {FilesRelocationArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopy = function (arg) {
+ return this.request('files/copy', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Copy multiple files or folders to different locations at once in the user's
+ * Dropbox. This route will replace copy_batch. The main difference is this
+ * route will return status for each entry, while copy_batch raises failure if
+ * any entry fails. This route will either finish synchronously, or return a job
+ * ID and do the async copy job in background. Please use copy_batch/check_v2 to
+ * check the job status.
+ * @function Dropbox#filesCopyBatchV2
+ * @arg {Object} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyBatchV2 = function (arg) {
+ return this.request('files/copy_batch_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Copy multiple files or folders to different locations at once in the user's
+ * Dropbox. This route will return job ID immediately and do the async copy job
+ * in background. Please use copy_batch/check to check the job status.
+ * @function Dropbox#filesCopyBatch
+ * @deprecated
+ * @arg {FilesRelocationBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyBatch = function (arg) {
+ return this.request('files/copy_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for copy_batch_v2. It returns list
+ * of results for each entry.
+ * @function Dropbox#filesCopyBatchCheckV2
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyBatchCheckV2 = function (arg) {
+ return this.request('files/copy_batch/check_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for copy_batch. If success, it
+ * returns list of results for each entry.
+ * @function Dropbox#filesCopyBatchCheck
+ * @deprecated
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyBatchCheck = function (arg) {
+ return this.request('files/copy_batch/check', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Get a copy reference to a file or folder. This reference string can be used
+ * to save that file or folder to another user's Dropbox by passing it to
+ * copy_reference/save.
+ * @function Dropbox#filesCopyReferenceGet
+ * @arg {FilesGetCopyReferenceArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyReferenceGet = function (arg) {
+ return this.request('files/copy_reference/get', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Save a copy reference returned by copy_reference/get to the user's Dropbox.
+ * @function Dropbox#filesCopyReferenceSave
+ * @arg {FilesSaveCopyReferenceArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCopyReferenceSave = function (arg) {
+ return this.request('files/copy_reference/save', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Create a folder at a given path.
+ * @function Dropbox#filesCreateFolderV2
+ * @arg {FilesCreateFolderArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCreateFolderV2 = function (arg) {
+ return this.request('files/create_folder_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Create a folder at a given path.
+ * @function Dropbox#filesCreateFolder
+ * @deprecated
+ * @arg {FilesCreateFolderArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCreateFolder = function (arg) {
+ return this.request('files/create_folder', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Create multiple folders at once. This route is asynchronous for large
+ * batches, which returns a job ID immediately and runs the create folder batch
+ * asynchronously. Otherwise, creates the folders and returns the result
+ * synchronously for smaller inputs. You can force asynchronous behaviour by
+ * using the CreateFolderBatchArg.force_async flag. Use
+ * create_folder_batch/check to check the job status.
+ * @function Dropbox#filesCreateFolderBatch
+ * @arg {FilesCreateFolderBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCreateFolderBatch = function (arg) {
+ return this.request('files/create_folder_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for create_folder_batch. If
+ * success, it returns list of result for each entry.
+ * @function Dropbox#filesCreateFolderBatchCheck
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesCreateFolderBatchCheck = function (arg) {
+ return this.request('files/create_folder_batch/check', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Delete the file or folder at a given path. If the path is a folder, all its
+ * contents will be deleted too. A successful response indicates that the file
+ * or folder was deleted. The returned metadata will be the corresponding
+ * FileMetadata or FolderMetadata for the item at time of deletion, and not a
+ * DeletedMetadata object.
+ * @function Dropbox#filesDeleteV2
+ * @arg {FilesDeleteArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesDeleteV2 = function (arg) {
+ return this.request('files/delete_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Delete the file or folder at a given path. If the path is a folder, all its
+ * contents will be deleted too. A successful response indicates that the file
+ * or folder was deleted. The returned metadata will be the corresponding
+ * FileMetadata or FolderMetadata for the item at time of deletion, and not a
+ * DeletedMetadata object.
+ * @function Dropbox#filesDelete
+ * @deprecated
+ * @arg {FilesDeleteArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesDelete = function (arg) {
+ return this.request('files/delete', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Delete multiple files/folders at once. This route is asynchronous, which
+ * returns a job ID immediately and runs the delete batch asynchronously. Use
+ * delete_batch/check to check the job status.
+ * @function Dropbox#filesDeleteBatch
+ * @arg {FilesDeleteBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesDeleteBatch = function (arg) {
+ return this.request('files/delete_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for delete_batch. If success, it
+ * returns list of result for each entry.
+ * @function Dropbox#filesDeleteBatchCheck
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesDeleteBatchCheck = function (arg) {
+ return this.request('files/delete_batch/check', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Download a file from a user's Dropbox.
+ * @function Dropbox#filesDownload
+ * @arg {FilesDownloadArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesDownload = function (arg) {
+ return this.request('files/download', arg, 'user', 'content', 'download');
+};
+
+/**
+ * Download a folder from the user's Dropbox, as a zip file. The folder must be
+ * less than 20 GB in size and any single file within must be less than 4 GB in
+ * size. The resulting zip must have fewer than 10,000 total file and folder
+ * entries, including the top level folder. The input cannot be a single file.
+ * @function Dropbox#filesDownloadZip
+ * @arg {FilesDownloadZipArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesDownloadZip = function (arg) {
+ return this.request('files/download_zip', arg, 'user', 'content', 'download');
+};
+
+/**
+ * Export a file from a user's Dropbox. This route only supports exporting files
+ * that cannot be downloaded directly and whose ExportResult.file_metadata has
+ * ExportInfo.export_as populated.
+ * @function Dropbox#filesExport
+ * @arg {FilesExportArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesExport = function (arg) {
+ return this.request('files/export', arg, 'user', 'content', 'download');
+};
+
+/**
+ * Return the lock metadata for the given list of paths.
+ * @function Dropbox#filesGetFileLockBatch
+ * @arg {FilesLockFileBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetFileLockBatch = function (arg) {
+ return this.request('files/get_file_lock_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the metadata for a file or folder. Note: Metadata for the root folder
+ * is unsupported.
+ * @function Dropbox#filesGetMetadata
+ * @arg {FilesGetMetadataArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetMetadata = function (arg) {
+ return this.request('files/get_metadata', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Get a preview for a file. Currently, PDF previews are generated for files
+ * with the following extensions: .ai, .doc, .docm, .docx, .eps, .gdoc,
+ * .gslides, .odp, .odt, .pps, .ppsm, .ppsx, .ppt, .pptm, .pptx, .rtf. HTML
+ * previews are generated for files with the following extensions: .csv, .ods,
+ * .xls, .xlsm, .gsheet, .xlsx. Other formats will return an unsupported
+ * extension error.
+ * @function Dropbox#filesGetPreview
+ * @arg {FilesPreviewArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetPreview = function (arg) {
+ return this.request('files/get_preview', arg, 'user', 'content', 'download');
+};
+
+/**
+ * Get a temporary link to stream content of a file. This link will expire in
+ * four hours and afterwards you will get 410 Gone. This URL should not be used
+ * to display content directly in the browser. The Content-Type of the link is
+ * determined automatically by the file's mime type.
+ * @function Dropbox#filesGetTemporaryLink
+ * @arg {FilesGetTemporaryLinkArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetTemporaryLink = function (arg) {
+ return this.request('files/get_temporary_link', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Get a one-time use temporary upload link to upload a file to a Dropbox
+ * location. This endpoint acts as a delayed upload. The returned temporary
+ * upload link may be used to make a POST request with the data to be uploaded.
+ * The upload will then be perfomed with the CommitInfo previously provided to
+ * get_temporary_upload_link but evaluated only upon consumption. Hence, errors
+ * stemming from invalid CommitInfo with respect to the state of the user's
+ * Dropbox will only be communicated at consumption time. Additionally, these
+ * errors are surfaced as generic HTTP 409 Conflict responses, potentially
+ * hiding issue details. The maximum temporary upload link duration is 4 hours.
+ * Upon consumption or expiration, a new link will have to be generated.
+ * Multiple links may exist for a specific upload path at any given time. The
+ * POST request on the temporary upload link must have its Content-Type set to
+ * "application/octet-stream". Example temporary upload link consumption
+ * request: curl -X POST
+ * https://content.dropboxapi.com/apitul/1/bNi2uIYF51cVBND --header
+ * "Content-Type: application/octet-stream" --data-binary @local_file.txt A
+ * successful temporary upload link consumption request returns the content hash
+ * of the uploaded data in JSON format. Example succesful temporary upload link
+ * consumption response: {"content-hash":
+ * "599d71033d700ac892a0e48fa61b125d2f5994"} An unsuccessful temporary upload
+ * link consumption request returns any of the following status codes: HTTP 400
+ * Bad Request: Content-Type is not one of application/octet-stream and
+ * text/plain or request is invalid. HTTP 409 Conflict: The temporary upload
+ * link does not exist or is currently unavailable, the upload failed, or
+ * another error happened. HTTP 410 Gone: The temporary upload link is expired
+ * or consumed. Example unsuccessful temporary upload link consumption
+ * response: Temporary upload link has been recently consumed.
+ * @function Dropbox#filesGetTemporaryUploadLink
+ * @arg {FilesGetTemporaryUploadLinkArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetTemporaryUploadLink = function (arg) {
+ return this.request('files/get_temporary_upload_link', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Get a thumbnail for an image. This method currently supports files with the
+ * following file extensions: jpg, jpeg, png, tiff, tif, gif, webp, ppm and bmp.
+ * Photos that are larger than 20MB in size won't be converted to a thumbnail.
+ * @function Dropbox#filesGetThumbnail
+ * @arg {FilesThumbnailArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetThumbnail = function (arg) {
+ return this.request('files/get_thumbnail', arg, 'user', 'content', 'download');
+};
+
+/**
+ * Get a thumbnail for an image. This method currently supports files with the
+ * following file extensions: jpg, jpeg, png, tiff, tif, gif, webp, ppm and bmp.
+ * Photos that are larger than 20MB in size won't be converted to a thumbnail.
+ * @function Dropbox#filesGetThumbnailV2
+ * @arg {FilesThumbnailV2Arg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetThumbnailV2 = function (arg) {
+ return this.request('files/get_thumbnail_v2', arg, 'app, user', 'content', 'download');
+};
+
+/**
+ * Get thumbnails for a list of images. We allow up to 25 thumbnails in a single
+ * batch. This method currently supports files with the following file
+ * extensions: jpg, jpeg, png, tiff, tif, gif, webp, ppm and bmp. Photos that
+ * are larger than 20MB in size won't be converted to a thumbnail.
+ * @function Dropbox#filesGetThumbnailBatch
+ * @arg {FilesGetThumbnailBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesGetThumbnailBatch = function (arg) {
+ return this.request('files/get_thumbnail_batch', arg, 'user', 'content', 'rpc');
+};
+
+/**
+ * Starts returning the contents of a folder. If the result's
+ * ListFolderResult.has_more field is true, call list_folder/continue with the
+ * returned ListFolderResult.cursor to retrieve more entries. If you're using
+ * ListFolderArg.recursive set to true to keep a local cache of the contents of
+ * a Dropbox account, iterate through each entry in order and process them as
+ * follows to keep your local state in sync: For each FileMetadata, store the
+ * new entry at the given path in your local state. If the required parent
+ * folders don't exist yet, create them. If there's already something else at
+ * the given path, replace it and remove all its children. For each
+ * FolderMetadata, store the new entry at the given path in your local state. If
+ * the required parent folders don't exist yet, create them. If there's already
+ * something else at the given path, replace it but leave the children as they
+ * are. Check the new entry's FolderSharingInfo.read_only and set all its
+ * children's read-only statuses to match. For each DeletedMetadata, if your
+ * local state has something at the given path, remove it and all its children.
+ * If there's nothing at the given path, ignore this entry. Note:
+ * auth.RateLimitError may be returned if multiple list_folder or
+ * list_folder/continue calls with same parameters are made simultaneously by
+ * same API app for same user. If your app implements retry logic, please hold
+ * off the retry until the previous request finishes.
+ * @function Dropbox#filesListFolder
+ * @arg {FilesListFolderArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesListFolder = function (arg) {
+ return this.request('files/list_folder', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Once a cursor has been retrieved from list_folder, use this to paginate
+ * through all files and retrieve updates to the folder, following the same
+ * rules as documented for list_folder.
+ * @function Dropbox#filesListFolderContinue
+ * @arg {FilesListFolderContinueArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesListFolderContinue = function (arg) {
+ return this.request('files/list_folder/continue', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * A way to quickly get a cursor for the folder's state. Unlike list_folder,
+ * list_folder/get_latest_cursor doesn't return any entries. This endpoint is
+ * for app which only needs to know about new files and modifications and
+ * doesn't need to know about files that already exist in Dropbox.
+ * @function Dropbox#filesListFolderGetLatestCursor
+ * @arg {FilesListFolderArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesListFolderGetLatestCursor = function (arg) {
+ return this.request('files/list_folder/get_latest_cursor', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * A longpoll endpoint to wait for changes on an account. In conjunction with
+ * list_folder/continue, this call gives you a low-latency way to monitor an
+ * account for file changes. The connection will block until there are changes
+ * available or a timeout occurs. This endpoint is useful mostly for client-side
+ * apps. If you're looking for server-side notifications, check out our webhooks
+ * documentation https://www.dropbox.com/developers/reference/webhooks.
+ * @function Dropbox#filesListFolderLongpoll
+ * @arg {FilesListFolderLongpollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesListFolderLongpoll = function (arg) {
+ return this.request('files/list_folder/longpoll', arg, 'noauth', 'notify', 'rpc');
+};
+
+/**
+ * Returns revisions for files based on a file path or a file id. The file path
+ * or file id is identified from the latest file entry at the given file path or
+ * id. This end point allows your app to query either by file path or file id by
+ * setting the mode parameter appropriately. In the ListRevisionsMode.path
+ * (default) mode, all revisions at the same file path as the latest file entry
+ * are returned. If revisions with the same file id are desired, then mode must
+ * be set to ListRevisionsMode.id. The ListRevisionsMode.id mode is useful to
+ * retrieve revisions for a given file across moves or renames.
+ * @function Dropbox#filesListRevisions
+ * @arg {FilesListRevisionsArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesListRevisions = function (arg) {
+ return this.request('files/list_revisions', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Lock the files at the given paths. A locked file will be writable only by the
+ * lock holder. A successful response indicates that the file has been locked.
+ * Returns a list of the locked file paths and their metadata after this
+ * operation.
+ * @function Dropbox#filesLockFileBatch
+ * @arg {FilesLockFileBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesLockFileBatch = function (arg) {
+ return this.request('files/lock_file_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Move a file or folder to a different location in the user's Dropbox. If the
+ * source path is a folder all its contents will be moved. Note that we do not
+ * currently support case-only renaming.
+ * @function Dropbox#filesMoveV2
+ * @arg {FilesRelocationArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesMoveV2 = function (arg) {
+ return this.request('files/move_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Move a file or folder to a different location in the user's Dropbox. If the
+ * source path is a folder all its contents will be moved.
+ * @function Dropbox#filesMove
+ * @deprecated
+ * @arg {FilesRelocationArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesMove = function (arg) {
+ return this.request('files/move', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Move multiple files or folders to different locations at once in the user's
+ * Dropbox. Note that we do not currently support case-only renaming. This route
+ * will replace move_batch. The main difference is this route will return status
+ * for each entry, while move_batch raises failure if any entry fails. This
+ * route will either finish synchronously, or return a job ID and do the async
+ * move job in background. Please use move_batch/check_v2 to check the job
+ * status.
+ * @function Dropbox#filesMoveBatchV2
+ * @arg {FilesMoveBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesMoveBatchV2 = function (arg) {
+ return this.request('files/move_batch_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Move multiple files or folders to different locations at once in the user's
+ * Dropbox. This route will return job ID immediately and do the async moving
+ * job in background. Please use move_batch/check to check the job status.
+ * @function Dropbox#filesMoveBatch
+ * @deprecated
+ * @arg {FilesRelocationBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesMoveBatch = function (arg) {
+ return this.request('files/move_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for move_batch_v2. It returns list
+ * of results for each entry.
+ * @function Dropbox#filesMoveBatchCheckV2
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesMoveBatchCheckV2 = function (arg) {
+ return this.request('files/move_batch/check_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for move_batch. If success, it
+ * returns list of results for each entry.
+ * @function Dropbox#filesMoveBatchCheck
+ * @deprecated
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesMoveBatchCheck = function (arg) {
+ return this.request('files/move_batch/check', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Creates a new Paper doc with the provided content.
+ * @function Dropbox#filesPaperCreate
+ * @arg {FilesPaperCreateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPaperCreate = function (arg) {
+ return this.request('files/paper/create', arg, 'user', 'api', 'upload');
+};
+
+/**
+ * Updates an existing Paper doc with the provided content.
+ * @function Dropbox#filesPaperUpdate
+ * @arg {FilesPaperUpdateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPaperUpdate = function (arg) {
+ return this.request('files/paper/update', arg, 'user', 'api', 'upload');
+};
+
+/**
+ * Permanently delete the file or folder at a given path (see
+ * https://www.dropbox.com/en/help/40). If the given file or folder is not yet
+ * deleted, this route will first delete it. It is possible for this route to
+ * successfully delete, then fail to permanently delete. Note: This endpoint is
+ * only available for Dropbox Business apps.
+ * @function Dropbox#filesPermanentlyDelete
+ * @arg {FilesDeleteArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPermanentlyDelete = function (arg) {
+ return this.request('files/permanently_delete', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * @function Dropbox#filesPropertiesAdd
+ * @deprecated
+ * @arg {FilePropertiesAddPropertiesArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPropertiesAdd = function (arg) {
+ return this.request('files/properties/add', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * @function Dropbox#filesPropertiesOverwrite
+ * @deprecated
+ * @arg {FilePropertiesOverwritePropertyGroupArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPropertiesOverwrite = function (arg) {
+ return this.request('files/properties/overwrite', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * @function Dropbox#filesPropertiesRemove
+ * @deprecated
+ * @arg {FilePropertiesRemovePropertiesArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPropertiesRemove = function (arg) {
+ return this.request('files/properties/remove', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * @function Dropbox#filesPropertiesTemplateGet
+ * @deprecated
+ * @arg {FilePropertiesGetTemplateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPropertiesTemplateGet = function (arg) {
+ return this.request('files/properties/template/get', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * @function Dropbox#filesPropertiesTemplateList
+ * @deprecated
+ * @returns {Promise., Error.>}
+ */
+routes.filesPropertiesTemplateList = function () {
+ return this.request('files/properties/template/list', null, 'user', 'api', 'rpc');
+};
+
+/**
+ * @function Dropbox#filesPropertiesUpdate
+ * @deprecated
+ * @arg {FilePropertiesUpdatePropertiesArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesPropertiesUpdate = function (arg) {
+ return this.request('files/properties/update', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Restore a specific revision of a file to the given path.
+ * @function Dropbox#filesRestore
+ * @arg {FilesRestoreArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesRestore = function (arg) {
+ return this.request('files/restore', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Save the data from a specified URL into a file in user's Dropbox. Note that
+ * the transfer from the URL must complete within 5 minutes, or the operation
+ * will time out and the job will fail. If the given path already exists, the
+ * file will be renamed to avoid the conflict (e.g. myfile (1).txt).
+ * @function Dropbox#filesSaveUrl
+ * @arg {FilesSaveUrlArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesSaveUrl = function (arg) {
+ return this.request('files/save_url', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Check the status of a save_url job.
+ * @function Dropbox#filesSaveUrlCheckJobStatus
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesSaveUrlCheckJobStatus = function (arg) {
+ return this.request('files/save_url/check_job_status', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Searches for files and folders. Note: Recent changes will be reflected in
+ * search results within a few seconds and older revisions of existing files may
+ * still match your query for up to a few days.
+ * @function Dropbox#filesSearch
+ * @deprecated
+ * @arg {FilesSearchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesSearch = function (arg) {
+ return this.request('files/search', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Searches for files and folders. Note: search_v2 along with search/continue_v2
+ * can only be used to retrieve a maximum of 10,000 matches. Recent changes may
+ * not immediately be reflected in search results due to a short delay in
+ * indexing. Duplicate results may be returned across pages. Some results may
+ * not be returned.
+ * @function Dropbox#filesSearchV2
+ * @arg {FilesSearchV2Arg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesSearchV2 = function (arg) {
+ return this.request('files/search_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Fetches the next page of search results returned from search_v2. Note:
+ * search_v2 along with search/continue_v2 can only be used to retrieve a
+ * maximum of 10,000 matches. Recent changes may not immediately be reflected in
+ * search results due to a short delay in indexing. Duplicate results may be
+ * returned across pages. Some results may not be returned.
+ * @function Dropbox#filesSearchContinueV2
+ * @arg {FilesSearchV2ContinueArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesSearchContinueV2 = function (arg) {
+ return this.request('files/search/continue_v2', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Unlock the files at the given paths. A locked file can only be unlocked by
+ * the lock holder or, if a business account, a team admin. A successful
+ * response indicates that the file has been unlocked. Returns a list of the
+ * unlocked file paths and their metadata after this operation.
+ * @function Dropbox#filesUnlockFileBatch
+ * @arg {FilesUnlockFileBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUnlockFileBatch = function (arg) {
+ return this.request('files/unlock_file_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Create a new file with the contents provided in the request. Do not use this
+ * to upload a file larger than 150 MB. Instead, create an upload session with
+ * upload_session/start. Calls to this endpoint will count as data transport
+ * calls for any Dropbox Business teams with a limit on the number of data
+ * transport calls allowed per month. For more information, see the Data
+ * transport limit page
+ * https://www.dropbox.com/developers/reference/data-transport-limit.
+ * @function Dropbox#filesUpload
+ * @arg {FilesCommitInfo} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUpload = function (arg) {
+ return this.request('files/upload', arg, 'user', 'content', 'upload');
+};
+
+/**
+ * Append more data to an upload session. When the parameter close is set, this
+ * call will close the session. A single request should not upload more than 150
+ * MB. The maximum size of a file one can upload to an upload session is 350 GB.
+ * Calls to this endpoint will count as data transport calls for any Dropbox
+ * Business teams with a limit on the number of data transport calls allowed per
+ * month. For more information, see the Data transport limit page
+ * https://www.dropbox.com/developers/reference/data-transport-limit.
+ * @function Dropbox#filesUploadSessionAppendV2
+ * @arg {FilesUploadSessionAppendArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUploadSessionAppendV2 = function (arg) {
+ return this.request('files/upload_session/append_v2', arg, 'user', 'content', 'upload');
+};
+
+/**
+ * Append more data to an upload session. A single request should not upload
+ * more than 150 MB. The maximum size of a file one can upload to an upload
+ * session is 350 GB. Calls to this endpoint will count as data transport calls
+ * for any Dropbox Business teams with a limit on the number of data transport
+ * calls allowed per month. For more information, see the Data transport limit
+ * page https://www.dropbox.com/developers/reference/data-transport-limit.
+ * @function Dropbox#filesUploadSessionAppend
+ * @deprecated
+ * @arg {FilesUploadSessionCursor} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUploadSessionAppend = function (arg) {
+ return this.request('files/upload_session/append', arg, 'user', 'content', 'upload');
+};
+
+/**
+ * Finish an upload session and save the uploaded data to the given file path. A
+ * single request should not upload more than 150 MB. The maximum size of a file
+ * one can upload to an upload session is 350 GB. Calls to this endpoint will
+ * count as data transport calls for any Dropbox Business teams with a limit on
+ * the number of data transport calls allowed per month. For more information,
+ * see the Data transport limit page
+ * https://www.dropbox.com/developers/reference/data-transport-limit.
+ * @function Dropbox#filesUploadSessionFinish
+ * @arg {FilesUploadSessionFinishArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUploadSessionFinish = function (arg) {
+ return this.request('files/upload_session/finish', arg, 'user', 'content', 'upload');
+};
+
+/**
+ * This route helps you commit many files at once into a user's Dropbox. Use
+ * upload_session/start and upload_session/append_v2 to upload file contents. We
+ * recommend uploading many files in parallel to increase throughput. Once the
+ * file contents have been uploaded, rather than calling upload_session/finish,
+ * use this route to finish all your upload sessions in a single request.
+ * UploadSessionStartArg.close or UploadSessionAppendArg.close needs to be true
+ * for the last upload_session/start or upload_session/append_v2 call. The
+ * maximum size of a file one can upload to an upload session is 350 GB. This
+ * route will return a job_id immediately and do the async commit job in
+ * background. Use upload_session/finish_batch/check to check the job status.
+ * For the same account, this route should be executed serially. That means you
+ * should not start the next job before current job finishes. We allow up to
+ * 1000 entries in a single request. Calls to this endpoint will count as data
+ * transport calls for any Dropbox Business teams with a limit on the number of
+ * data transport calls allowed per month. For more information, see the Data
+ * transport limit page
+ * https://www.dropbox.com/developers/reference/data-transport-limit.
+ * @function Dropbox#filesUploadSessionFinishBatch
+ * @arg {FilesUploadSessionFinishBatchArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUploadSessionFinishBatch = function (arg) {
+ return this.request('files/upload_session/finish_batch', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for upload_session/finish_batch. If
+ * success, it returns list of result for each entry.
+ * @function Dropbox#filesUploadSessionFinishBatchCheck
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUploadSessionFinishBatchCheck = function (arg) {
+ return this.request('files/upload_session/finish_batch/check', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Upload sessions allow you to upload a single file in one or more requests,
+ * for example where the size of the file is greater than 150 MB. This call
+ * starts a new upload session with the given data. You can then use
+ * upload_session/append_v2 to add more data and upload_session/finish to save
+ * all the data to a file in Dropbox. A single request should not upload more
+ * than 150 MB. The maximum size of a file one can upload to an upload session
+ * is 350 GB. An upload session can be used for a maximum of 7 days. Attempting
+ * to use an UploadSessionStartResult.session_id with upload_session/append_v2
+ * or upload_session/finish more than 7 days after its creation will return a
+ * UploadSessionLookupError.not_found. Calls to this endpoint will count as data
+ * transport calls for any Dropbox Business teams with a limit on the number of
+ * data transport calls allowed per month. For more information, see the Data
+ * transport limit page
+ * https://www.dropbox.com/developers/reference/data-transport-limit. By
+ * default, upload sessions require you to send content of the file in
+ * sequential order via consecutive upload_session/start,
+ * upload_session/append_v2, upload_session/finish calls. For better
+ * performance, you can instead optionally use a UploadSessionType.concurrent
+ * upload session. To start a new concurrent session, set
+ * UploadSessionStartArg.session_type to UploadSessionType.concurrent. After
+ * that, you can send file data in concurrent upload_session/append_v2 requests.
+ * Finally finish the session with upload_session/finish. There are couple of
+ * constraints with concurrent sessions to make them work. You can not send data
+ * with upload_session/start or upload_session/finish call, only with
+ * upload_session/append_v2 call. Also data uploaded in upload_session/append_v2
+ * call must be multiple of 4194304 bytes (except for last
+ * upload_session/append_v2 with UploadSessionStartArg.close to true, that may
+ * contain any remaining data).
+ * @function Dropbox#filesUploadSessionStart
+ * @arg {FilesUploadSessionStartArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.filesUploadSessionStart = function (arg) {
+ return this.request('files/upload_session/start', arg, 'user', 'content', 'upload');
+};
+
+/**
+ * Marks the given Paper doc as archived. This action can be performed or undone
+ * by anyone with edit permissions to the doc. Note that this endpoint will
+ * continue to work for content created by users on the older version of Paper.
+ * To check which version of Paper a user is on, use /users/features/get_values.
+ * If the paper_as_files feature is enabled, then the user is running the new
+ * version of Paper. This endpoint will be retired in September 2020. Refer to
+ * the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * more information.
+ * @function Dropbox#paperDocsArchive
+ * @deprecated
+ * @arg {PaperRefPaperDoc} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsArchive = function (arg) {
+ return this.request('paper/docs/archive', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Creates a new Paper doc with the provided content. Note that this endpoint
+ * will continue to work for content created by users on the older version of
+ * Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. This endpoint will be retired
+ * in September 2020. Refer to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * more information.
+ * @function Dropbox#paperDocsCreate
+ * @deprecated
+ * @arg {PaperPaperDocCreateArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsCreate = function (arg) {
+ return this.request('paper/docs/create', arg, 'user', 'api', 'upload');
+};
+
+/**
+ * Exports and downloads Paper doc either as HTML or markdown. Note that this
+ * endpoint will continue to work for content created by users on the older
+ * version of Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. Refer to the Paper Migration
+ * Guide https://www.dropbox.com/lp/developers/reference/paper-migration-guide
+ * for migration information.
+ * @function Dropbox#paperDocsDownload
+ * @deprecated
+ * @arg {PaperPaperDocExport} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsDownload = function (arg) {
+ return this.request('paper/docs/download', arg, 'user', 'api', 'download');
+};
+
+/**
+ * Lists the users who are explicitly invited to the Paper folder in which the
+ * Paper doc is contained. For private folders all users (including owner)
+ * shared on the folder are listed and for team folders all non-team users
+ * shared on the folder are returned. Note that this endpoint will continue to
+ * work for content created by users on the older version of Paper. To check
+ * which version of Paper a user is on, use /users/features/get_values. If the
+ * paper_as_files feature is enabled, then the user is running the new version
+ * of Paper. Refer to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsFolderUsersList
+ * @deprecated
+ * @arg {PaperListUsersOnFolderArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsFolderUsersList = function (arg) {
+ return this.request('paper/docs/folder_users/list', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Once a cursor has been retrieved from docs/folder_users/list, use this to
+ * paginate through all users on the Paper folder. Note that this endpoint will
+ * continue to work for content created by users on the older version of Paper.
+ * To check which version of Paper a user is on, use /users/features/get_values.
+ * If the paper_as_files feature is enabled, then the user is running the new
+ * version of Paper. Refer to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsFolderUsersListContinue
+ * @deprecated
+ * @arg {PaperListUsersOnFolderContinueArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsFolderUsersListContinue = function (arg) {
+ return this.request('paper/docs/folder_users/list/continue', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Retrieves folder information for the given Paper doc. This includes: -
+ * folder sharing policy; permissions for subfolders are set by the top-level
+ * folder. - full 'filepath', i.e. the list of folders (both folderId and
+ * folderName) from the root folder to the folder directly containing the
+ * Paper doc. If the Paper doc is not in any folder (aka unfiled) the response
+ * will be empty. Note that this endpoint will continue to work for content
+ * created by users on the older version of Paper. To check which version of
+ * Paper a user is on, use /users/features/get_values. If the paper_as_files
+ * feature is enabled, then the user is running the new version of Paper. Refer
+ * to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsGetFolderInfo
+ * @deprecated
+ * @arg {PaperRefPaperDoc} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsGetFolderInfo = function (arg) {
+ return this.request('paper/docs/get_folder_info', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Return the list of all Paper docs according to the argument specifications.
+ * To iterate over through the full pagination, pass the cursor to
+ * docs/list/continue. Note that this endpoint will continue to work for content
+ * created by users on the older version of Paper. To check which version of
+ * Paper a user is on, use /users/features/get_values. If the paper_as_files
+ * feature is enabled, then the user is running the new version of Paper. Refer
+ * to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsList
+ * @deprecated
+ * @arg {PaperListPaperDocsArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsList = function (arg) {
+ return this.request('paper/docs/list', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Once a cursor has been retrieved from docs/list, use this to paginate through
+ * all Paper doc. Note that this endpoint will continue to work for content
+ * created by users on the older version of Paper. To check which version of
+ * Paper a user is on, use /users/features/get_values. If the paper_as_files
+ * feature is enabled, then the user is running the new version of Paper. Refer
+ * to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsListContinue
+ * @deprecated
+ * @arg {PaperListPaperDocsContinueArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsListContinue = function (arg) {
+ return this.request('paper/docs/list/continue', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Permanently deletes the given Paper doc. This operation is final as the doc
+ * cannot be recovered. This action can be performed only by the doc owner. Note
+ * that this endpoint will continue to work for content created by users on the
+ * older version of Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. Refer to the Paper Migration
+ * Guide https://www.dropbox.com/lp/developers/reference/paper-migration-guide
+ * for migration information.
+ * @function Dropbox#paperDocsPermanentlyDelete
+ * @deprecated
+ * @arg {PaperRefPaperDoc} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsPermanentlyDelete = function (arg) {
+ return this.request('paper/docs/permanently_delete', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Gets the default sharing policy for the given Paper doc. Note that this
+ * endpoint will continue to work for content created by users on the older
+ * version of Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. Refer to the Paper Migration
+ * Guide https://www.dropbox.com/lp/developers/reference/paper-migration-guide
+ * for migration information.
+ * @function Dropbox#paperDocsSharingPolicyGet
+ * @deprecated
+ * @arg {PaperRefPaperDoc} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsSharingPolicyGet = function (arg) {
+ return this.request('paper/docs/sharing_policy/get', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Sets the default sharing policy for the given Paper doc. The default
+ * 'team_sharing_policy' can be changed only by teams, omit this field for
+ * personal accounts. The 'public_sharing_policy' policy can't be set to the
+ * value 'disabled' because this setting can be changed only via the team admin
+ * console. Note that this endpoint will continue to work for content created by
+ * users on the older version of Paper. To check which version of Paper a user
+ * is on, use /users/features/get_values. If the paper_as_files feature is
+ * enabled, then the user is running the new version of Paper. Refer to the
+ * Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsSharingPolicySet
+ * @deprecated
+ * @arg {PaperPaperDocSharingPolicy} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsSharingPolicySet = function (arg) {
+ return this.request('paper/docs/sharing_policy/set', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Updates an existing Paper doc with the provided content. Note that this
+ * endpoint will continue to work for content created by users on the older
+ * version of Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. This endpoint will be retired
+ * in September 2020. Refer to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * more information.
+ * @function Dropbox#paperDocsUpdate
+ * @deprecated
+ * @arg {PaperPaperDocUpdateArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsUpdate = function (arg) {
+ return this.request('paper/docs/update', arg, 'user', 'api', 'upload');
+};
+
+/**
+ * Allows an owner or editor to add users to a Paper doc or change their
+ * permissions using their email address or Dropbox account ID. The doc owner's
+ * permissions cannot be changed. Note that this endpoint will continue to work
+ * for content created by users on the older version of Paper. To check which
+ * version of Paper a user is on, use /users/features/get_values. If the
+ * paper_as_files feature is enabled, then the user is running the new version
+ * of Paper. Refer to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsUsersAdd
+ * @deprecated
+ * @arg {PaperAddPaperDocUser} arg - The request parameters.
+ * @returns {Promise.>, Error.>}
+ */
+routes.paperDocsUsersAdd = function (arg) {
+ return this.request('paper/docs/users/add', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Lists all users who visited the Paper doc or users with explicit access. This
+ * call excludes users who have been removed. The list is sorted by the date of
+ * the visit or the share date. The list will include both users, the explicitly
+ * shared ones as well as those who came in using the Paper url link. Note that
+ * this endpoint will continue to work for content created by users on the older
+ * version of Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. Refer to the Paper Migration
+ * Guide https://www.dropbox.com/lp/developers/reference/paper-migration-guide
+ * for migration information.
+ * @function Dropbox#paperDocsUsersList
+ * @deprecated
+ * @arg {PaperListUsersOnPaperDocArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsUsersList = function (arg) {
+ return this.request('paper/docs/users/list', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Once a cursor has been retrieved from docs/users/list, use this to paginate
+ * through all users on the Paper doc. Note that this endpoint will continue to
+ * work for content created by users on the older version of Paper. To check
+ * which version of Paper a user is on, use /users/features/get_values. If the
+ * paper_as_files feature is enabled, then the user is running the new version
+ * of Paper. Refer to the Paper Migration Guide
+ * https://www.dropbox.com/lp/developers/reference/paper-migration-guide for
+ * migration information.
+ * @function Dropbox#paperDocsUsersListContinue
+ * @deprecated
+ * @arg {PaperListUsersOnPaperDocContinueArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsUsersListContinue = function (arg) {
+ return this.request('paper/docs/users/list/continue', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Allows an owner or editor to remove users from a Paper doc using their email
+ * address or Dropbox account ID. The doc owner cannot be removed. Note that
+ * this endpoint will continue to work for content created by users on the older
+ * version of Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. Refer to the Paper Migration
+ * Guide https://www.dropbox.com/lp/developers/reference/paper-migration-guide
+ * for migration information.
+ * @function Dropbox#paperDocsUsersRemove
+ * @deprecated
+ * @arg {PaperRemovePaperDocUser} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperDocsUsersRemove = function (arg) {
+ return this.request('paper/docs/users/remove', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Create a new Paper folder with the provided info. Note that this endpoint
+ * will continue to work for content created by users on the older version of
+ * Paper. To check which version of Paper a user is on, use
+ * /users/features/get_values. If the paper_as_files feature is enabled, then
+ * the user is running the new version of Paper. Refer to the Paper Migration
+ * Guide https://www.dropbox.com/lp/developers/reference/paper-migration-guide
+ * for migration information.
+ * @function Dropbox#paperFoldersCreate
+ * @deprecated
+ * @arg {PaperPaperFolderCreateArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.paperFoldersCreate = function (arg) {
+ return this.request('paper/folders/create', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Adds specified members to a file.
+ * @function Dropbox#sharingAddFileMember
+ * @arg {SharingAddFileMemberArgs} arg - The request parameters.
+ * @returns {Promise.>, Error.>}
+ */
+routes.sharingAddFileMember = function (arg) {
+ return this.request('sharing/add_file_member', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Allows an owner or editor (if the ACL update policy allows) of a shared
+ * folder to add another member. For the new member to get access to all the
+ * functionality for this folder, you will need to call mount_folder on their
+ * behalf.
+ * @function Dropbox#sharingAddFolderMember
+ * @arg {SharingAddFolderMemberArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.sharingAddFolderMember = function (arg) {
+ return this.request('sharing/add_folder_member', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Identical to update_file_member but with less information returned.
+ * @function Dropbox#sharingChangeFileMemberAccess
+ * @deprecated
+ * @arg {SharingChangeFileMemberAccessArgs} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.sharingChangeFileMemberAccess = function (arg) {
+ return this.request('sharing/change_file_member_access', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job.
+ * @function Dropbox#sharingCheckJobStatus
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.sharingCheckJobStatus = function (arg) {
+ return this.request('sharing/check_job_status', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for sharing a folder.
+ * @function Dropbox#sharingCheckRemoveMemberJobStatus
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.>}
+ */
+routes.sharingCheckRemoveMemberJobStatus = function (arg) {
+ return this.request('sharing/check_remove_member_job_status', arg, 'user', 'api', 'rpc');
+};
+
+/**
+ * Returns the status of an asynchronous job for sharing a folder.
+ * @function Dropbox#sharingCheckShareJobStatus
+ * @arg {AsyncPollArg} arg - The request parameters.
+ * @returns {Promise., Error.