diff --git a/package.json b/package.json index a22e99e..d319ea2 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "@types/react": "^16.9.35", "@types/react-dom": "^16.9.8", "@types/streamsaver": "^2.0.1", + "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/parser": "^2.33.0", + "ahooks": "^3.5.2", "artplayer": "^4.3.4", "axios": "^0.21.1", "babel-eslint": "10.0.3", diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index 09c65d1..28c581d 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -161,7 +161,17 @@ "serverBatchDownloadDescription": "Archive by the server and sent to the client for download on-the-fly.", "selectArchiveMethod": "Select archive method", "batchDownloadStarted": "Batch download has started, please do not close this tab", - "batchDownloadError": "Failed to archive: {{msg}}" + "batchDownloadError": "Failed to archive: {{msg}}", + "userDenied": "User denied.", + "directoryDownloadReplace": "Overwrite", + "directoryDownloadReplaceDescription": "{{num}} objects including {{duplicates}} will be overwritten.", + "directoryDownloadSkip": "Skip", + "directoryDownloadSkipDescription": "{{num}} objects including {{duplicates}} will be skipped.", + "selectDirectoryDuplicationMethod": "How to handle duplicate files?", + "directoryDownloadStarted": "Download started, please do not close this tab.", + "directoryDownloadFinished": "Download finished, no failed objects.", + "directoryDownloadFinishedWithError": "Download finished, {{failed}} object failed.", + "directoryDownloadPermissionError": "Permission denied, please allow read and write local files." }, "modals": { "processing": "Processing...", @@ -220,7 +230,16 @@ "allowPreview": "Enable preview", "allowPreviewDescription": "Whether to allow preview of file content from the share link", "shareLink": "Share link", - "sendLink": "Send the link" + "sendLink": "Send the link", + "directoryDownloadReplaceNotifiction": "Overwrite {{name}}", + "directoryDownloadSkipNotifiction": "Skipped {{name}}", + "directoryDownloadTitle": "Download", + "directoryDownloadStarted": "Start downloading {{name}}", + "directoryDownloadFinished": "Download finished", + "directoryDownloadError": "Error: {{msg}}", + "directoryDownloadErrorNotification": "Error occurs while download {{name}}: {{msg}}", + "directoryDownloadAutoscroll": "Auto scroll", + "directoryDownloadCancelled": "Download cancelled" }, "uploader": { "fileNotMatchError": "The selected file does not match the original file.", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index fb0f511..adefa8c 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -162,7 +162,16 @@ "selectArchiveMethod": "选择打包下载方式", "batchDownloadStarted": "打包下载已开始,请不要关闭此标签页", "batchDownloadError": "打包遇到错误:{{msg}}", - "userDenied": "用户拒绝" + "userDenied": "用户拒绝", + "directoryDownloadReplace": "替换对象", + "directoryDownloadReplaceDescription": "将会替换 {{duplicates}} 等共 {{num}} 个对象。", + "directoryDownloadSkip": "跳过对象", + "directoryDownloadSkipDescription": "将会跳过 {{duplicates}} 等共 {{num}} 个对象。", + "selectDirectoryDuplicationMethod": "重复对象处理方式", + "directoryDownloadStarted": "下载已开始,请不要关闭此标签页", + "directoryDownloadFinished": "下载完成,无失败对象", + "directoryDownloadFinishedWithError": "下载完成, 失败 {{failed}} 个对象", + "directoryDownloadPermissionError": "无权限操作,请允许读写本地文件" }, "modals": { "processing": "处理中...", @@ -221,7 +230,16 @@ "allowPreview": "允许预览", "allowPreviewDescription": "是否允许在分享页面预览文件内容", "shareLink": "分享链接", - "sendLink": "发送链接" + "sendLink": "发送链接", + "directoryDownloadReplaceNotifiction": "已覆盖 {{name}}", + "directoryDownloadSkipNotifiction": "已跳过 {{name}}", + "directoryDownloadTitle": "下载", + "directoryDownloadStarted": "开始下载 {{name}}", + "directoryDownloadFinished": "下载完成", + "directoryDownloadError": "遇到错误:{{msg}}", + "directoryDownloadErrorNotification": "下载 {{name}} 遇到错误:{{msg}}", + "directoryDownloadAutoscroll": "自动滚动", + "directoryDownloadCancelled": "已取消下载" }, "uploader": { "fileNotMatchError": "所选择文件与原始文件不符", @@ -259,7 +277,7 @@ "hideCompletedTooltip": "列表中不显示已完成、失败、被取消的任务", "hideCompleted": "隐藏已完成任务", "addTimeAscTooltip": "最先添加的任务排在最前", - "addTimeAsc":"最先添加靠前", + "addTimeAsc": "最先添加靠前", "addTimeDescTooltip": "最后添加的任务排在最前", "addTimeDesc": "最后添加靠前", "showInstantSpeedTooltip": "单个任务上传速度展示为瞬时速度", @@ -292,7 +310,7 @@ }, "share": { "expireInXDays": "{{num}} 天后到期", - "expireInXHours":"{{num}} 小时后到期", + "expireInXHours": "{{num}} 小时后到期", "createdBy": "此分享由 <0>{{nick}} 创建", "sharedBy": "<0>{{nick}} 向您分享了 {{num}} 个文件", "statistics": "{{views}} 次浏览 • {{downloads}} 次下载 • {{time}}", @@ -305,9 +323,9 @@ "createdAtDesc": "创建日期由晚到早", "createdAtAsc": "创建日期由早到晚", "downloadsDesc": "下载次数由大到小", - "downloadsAsc":"下载次数由小到大", - "viewsDesc":"浏览次数由大到小", - "viewsAsc":"浏览次数由小到大", + "downloadsAsc": "下载次数由小到大", + "viewsDesc": "浏览次数由大到小", + "viewsAsc": "浏览次数由小到大", "noRecords": "没有分享记录.", "sourceNotFound": "[原始对象不存在]", "expired": "已失效", @@ -442,4 +460,4 @@ "viewNumber": "浏览次数", "language": "语言" } -} \ No newline at end of file +} diff --git a/src/component/FileManager/ContextMenu.js b/src/component/FileManager/ContextMenu.js index 0c100e7..0ef0f2a 100644 --- a/src/component/FileManager/ContextMenu.js +++ b/src/component/FileManager/ContextMenu.js @@ -18,7 +18,12 @@ import MoveIcon from "@material-ui/icons/Input"; import LinkIcon from "@material-ui/icons/InsertLink"; import OpenIcon from "@material-ui/icons/OpenInNew"; import ShareIcon from "@material-ui/icons/Share"; -import { FolderUpload, MagnetOn, FilePlus } from "mdi-material-ui"; +import { + FolderDownload, + FolderUpload, + MagnetOn, + FilePlus, +} from "mdi-material-ui"; import PropTypes from "prop-types"; import React, { Component } from "react"; import { connect } from "react-redux"; @@ -32,6 +37,7 @@ import { openPreview, setSelectedTarget, startBatchDownload, + startDirectoryDownload, startDownload, toggleObjectInfoSidebar, } from "../../redux/explorer/action"; @@ -174,6 +180,9 @@ const mapDispatchToProps = (dispatch) => { batchGetSource: () => { dispatch(batchGetSource()); }, + startDirectoryDownload: (share) => { + dispatch(startDirectoryDownload(share)); + }, }; }; @@ -196,6 +205,10 @@ class ContextMenuCompoment extends Component { this.props.startBatchDownload(this.props.share); }; + openDirectoryDownload = () => { + this.props.startDirectoryDownload(this.props.share); + }; + openDownload = () => { this.props.startDownload(this.props.share, this.props.selected[0]); }; @@ -460,6 +473,24 @@ class ContextMenuCompoment extends Component { )} + {(this.props.isMultiple || this.props.withFolder) && + window.showDirectoryPicker && + window.isSecureContext && ( + + this.openDirectoryDownload() + } + > + + + + + {t("fileManager.download")} + + + )} + {(this.props.isMultiple || this.props.withFolder) && ( + ); } diff --git a/src/component/Modals/DirectoryDownload.js b/src/component/Modals/DirectoryDownload.js new file mode 100644 index 0000000..0d83637 --- /dev/null +++ b/src/component/Modals/DirectoryDownload.js @@ -0,0 +1,107 @@ +import React, { useCallback, useState, useEffect, useRef } from "react"; +import { + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + makeStyles, + FormControl, + FormControlLabel, + Checkbox, +} from "@material-ui/core"; +import { useDispatch } from "react-redux"; +import TextField from "@material-ui/core/TextField"; +import { useTranslation } from "react-i18next"; +import { useInterval } from "ahooks"; +import { cancelDirectoryDownload } from "../../redux/explorer/action"; + +const useStyles = makeStyles((theme) => ({ + contentFix: { + padding: "10px 24px 0px 24px", + backgroundColor: theme.palette.background.default, + }, + buttonProgress: { + color: theme.palette.secondary.light, + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, +})); + +export default function DirectoryDownloadDialog(props) { + const { t } = useTranslation(); + + const classes = useStyles(); + + const logRef = useRef(); + const autoScroll = useRef(true); + + useInterval(() => { + if (autoScroll.current && !props.done && logRef.current) { + logRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); + } + }, 1000); + + return ( + + + {t("modals.directoryDownloadTitle")} + + + + + + + + (autoScroll.current = !autoScroll.current) + } + /> + } + label={t("modals.directoryDownloadAutoscroll")} + /> + +
+ +
+
+
+ ); +} diff --git a/src/component/Navbar/Navbar.js b/src/component/Navbar/Navbar.js index b0de0f9..903dc4b 100644 --- a/src/component/Navbar/Navbar.js +++ b/src/component/Navbar/Navbar.js @@ -19,7 +19,12 @@ import SezrchBar from "./SearchBar"; import StorageBar from "./StorageBar"; import UserAvatar from "./UserAvatar"; import UserInfo from "./UserInfo"; -import { AccountArrowRight, AccountPlus, LogoutVariant } from "mdi-material-ui"; +import { + FolderDownload, + AccountArrowRight, + AccountPlus, + LogoutVariant, +} from "mdi-material-ui"; import { withRouter } from "react-router-dom"; import { AppBar, @@ -63,7 +68,11 @@ import { showImgPreivew, toggleSnackbar, } from "../../redux/explorer"; -import { startBatchDownload, startDownload } from "../../redux/explorer/action"; +import { + startBatchDownload, + startDirectoryDownload, + startDownload, +} from "../../redux/explorer/action"; import { withTranslation } from "react-i18next"; vhCheck(); @@ -145,6 +154,9 @@ const mapDispatchToProps = (dispatch) => { startBatchDownload: (share) => { dispatch(startBatchDownload(share)); }, + startDirectoryDownload: (share) => { + dispatch(startDirectoryDownload(share)); + }, startDownload: (share, file) => { dispatch(startDownload(share, file)); }, @@ -334,6 +346,10 @@ class NavbarCompoment extends Component { this.props.startDownload(this.props.shareInfo, this.props.selected[0]); }; + openDirectoryDownload = (e) => { + this.props.startDirectoryDownload(this.props.shareInfo); + }; + archiveDownload = (e) => { this.props.startBatchDownload(this.props.shareInfo); }; @@ -688,6 +704,35 @@ class NavbarCompoment extends Component { )} + {(this.props.isMultiple || + this.props.withFolder) && + window.showDirectoryPicker && + window.isSecureContext && ( + + + + this.openDirectoryDownload() + } + > + + + + + )} {(this.props.isMultiple || this.props.withFolder) && ( + directoryDownloadAbortController.abort(); + +export const startDirectoryDownload = ( + share: any +): ThunkAction => { + return async (dispatch, getState): Promise => { + directoryDownloadAbortController = new AbortController(); + if (!window.showDirectoryPicker || !window.isSecureContext) { + return; + } + let handle: FileSystemDirectoryHandle; + // we should show directory picker at first + // https://web.dev/file-system-access/#:~:text=handle%3B%0A%7D-,Gotchas,-Sometimes%20processing%20the + try { + // can't use suggestedName for showDirectoryPicker (only available showSaveFilePicker) + handle = await window.showDirectoryPicker({ + startIn: "downloads", + mode: "readwrite", + }); + // we should obtain the readwrite permission for the directory at first + if (!(await verifyFileSystemRWPermission(handle))) { + throw new Error( + i18next.t("fileManager.directoryDownloadPermissionError") + ); + } + dispatch(closeAllModals()); + } catch (e) { + dispatch( + toggleSnackbar( + "top", + "right", + i18next.t("modals.directoryDownloadError", { + msg: e && e.message, + }), + "error" + ) + ); + dispatch(closeAllModals()); + return; + } + + dispatch(changeContextMenu("file", false)); + const { + explorer: { selected }, + navigator: { path }, + } = getState(); + + // list files to download + dispatch(openLoadingDialog(i18next.t("modals.listingFiles"))); + + let queue: CloudreveFile[] = []; + try { + queue = await walk(selected, share); + } catch (e) { + dispatch( + toggleSnackbar( + "top", + "right", + i18next.t("modals.listingFileError", { + message: e.message, + }), + "warning" + ) + ); + dispatch(closeAllModals()); + return; + } + + dispatch(closeAllModals()); + + let failed = 0; + let option: any; + // preparation for downloading + // get the files in the directory to compare with queue files + // parent: "" + const fsPaths = await getFileSystemDirectoryPaths(handle, ""); + + // path: / or /abc (no sep suffix) + // file.path: /abc/d (no sep suffix) + // fsPaths: ["abc/d/e.bin",] + const duplicates = queue + .map((file) => + trimPrefix( + `${file.path}/${file.name}`, + path === "/" ? "/" : path + "/" + ) + ) + .filter((path) => fsPaths.includes(path)); + + // we should ask users for the duplication handle method + if (duplicates.length > 0) { + try { + option = await dispatch( + askForOption( + [ + { + key: "replace", + name: i18next.t( + "fileManager.directoryDownloadReplace" + ), + description: i18next.t( + "fileManager.directoryDownloadReplaceDescription", + { + // display the first three duplications + duplicates: duplicates + .slice( + 0, + duplicates.length >= 3 + ? 3 + : duplicates.length + ) + .join(", "), + num: duplicates.length, + } + ), + }, + { + key: "skip", + name: i18next.t( + "fileManager.directoryDownloadSkip" + ), + description: i18next.t( + "fileManager.directoryDownloadSkipDescription", + { + duplicates: duplicates + .slice( + 0, + duplicates.length >= 3 + ? 3 + : duplicates.length + ) + .join(", "), + num: duplicates.length, + } + ), + }, + ], + i18next.t( + "fileManager.selectDirectoryDuplicationMethod" + ) + ) + ); + } catch (e) { + return; + } + } + dispatch(closeAllModals()); + + // start the download + dispatch( + toggleSnackbar( + "top", + "center", + i18next.t("fileManager.directoryDownloadStarted"), + "info" + ) + ); + + const updateLog = (log, done) => { + dispatch(openDirectoryDownloadDialog(true, log, done)); + }; + let log = ""; + + while (queue.length > 0) { + const next = queue.pop(); + if (next && next.type === "file") { + // donload url + const previewPath = getPreviewPath(next); + const url = + getBaseURL() + + (pathHelper.isSharePage(location.pathname) + ? "/share/preview/" + + share.key + + (previewPath !== "" ? "?path=" + previewPath : "") + : "/file/preview/" + next.id); + + // path to save this file + // path: / or /abc (no sep suffix) + // next.path: /abc/d (no sep suffix) + // name: d/e.bin + const name = trimPrefix( + pathJoin([next.path, next.name]), + path === "/" ? "/" : path + "/" + ); + // TODO: improve the display of log + // can we turn the upload queue component to the transition queue? + // then we can easily cancel or retry the download + // and the batch download queue can show as well. + log = + (log === "" ? "" : log + "\n\n") + + i18next.t("modals.directoryDownloadStarted", { name }); + updateLog(log, false); + try { + if (duplicates.includes(name)) { + if (option.key === "skip") { + log += + "\n" + + i18next.t( + "modals.directoryDownloadSkipNotifiction", + { + name, + } + ); + updateLog(log, false); + continue; + } else { + log += + "\n" + + i18next.t( + "modals.directoryDownloadReplaceNotifiction", + { + name, + } + ); + updateLog(log, false); + } + } + + // TODO: need concurrent task queue? + const res = await fetch(url, { + cache: "no-cache", + signal: directoryDownloadAbortController.signal, + }); + await saveFileToFileSystemDirectory( + handle, + await res.blob(), + name + ); + log += "\n" + i18next.t("modals.directoryDownloadFinished"); + updateLog(log, false); + } catch (e) { + if (e.name === "AbortError") { + dispatch( + toggleSnackbar( + "top", + "right", + i18next.t("modals.directoryDownloadCancelled"), + "warning" + ) + ); + log += + "\n\n" + + i18next.t("modals.directoryDownloadCancelled"); + updateLog(log, true); + return; + } + + failed++; + dispatch( + toggleSnackbar( + "top", + "right", + i18next.t( + "modals.directoryDownloadErrorNotification", + { + name, + msg: e && e.message, + } + ), + "warning" + ) + ); + log += + "\n" + + i18next.t("modals.directoryDownloadError", { + msg: e.message, + }); + updateLog(log, false); + } + } + } + log += + "\n" + + (failed === 0 + ? i18next.t("fileManager.directoryDownloadFinished") + : i18next.t("fileManager.directoryDownloadFinishedWithError", { + failed, + })); + updateLog(log, true); + + dispatch( + toggleSnackbar( + "top", + "center", + failed === 0 + ? i18next.t("fileManager.directoryDownloadFinished") + : i18next.t( + "fileManager.directoryDownloadFinishedWithError", + { + failed, + } + ), + "success" + ) + ); + }; +}; + export const getViewerURL = ( viewer: string, file: any, @@ -543,13 +849,8 @@ export const selectFile = (file: any, event: any, fileIndex: any) => { } const isMacbook = isMac(); const { explorer } = getState(); - const { - selected, - lastSelect, - dirList, - fileList, - shiftSelectedIds, - } = explorer; + const { selected, lastSelect, dirList, fileList, shiftSelectedIds } = + explorer; if (shiftKey && !ctrlKey && !metaKey && selected.length !== 0) { // shift 多选 // 取消原有选择 diff --git a/src/redux/explorer/index.ts b/src/redux/explorer/index.ts index e9adfe1..d0eb76f 100644 --- a/src/redux/explorer/index.ts +++ b/src/redux/explorer/index.ts @@ -281,3 +281,12 @@ export const setSiteConfig = (config) => { config: config, }; }; + +export const openDirectoryDownloadDialog = (downloading, log, done) => { + return { + type: "OPEN_DIRECTORY_DOWNLOAD_DIALOG", + downloading, + log, + done, + }; +}; diff --git a/src/redux/viewUpdate/reducer.ts b/src/redux/viewUpdate/reducer.ts index 4a1e304..c28e1f8 100644 --- a/src/redux/viewUpdate/reducer.ts +++ b/src/redux/viewUpdate/reducer.ts @@ -35,6 +35,9 @@ export interface ViewUpdateState { decompress: boolean; loading: boolean; loadingText: string; + directoryDownloading: boolean; + directoryDownloadLog: string; + directoryDownloadDone: boolean; option?: { options: { open: boolean; @@ -96,6 +99,9 @@ export const initState: ViewUpdateState = { decompress: false, loading: false, loadingText: "", + directoryDownloading: false, + directoryDownloadLog: "", + directoryDownloadDone: false, }, snackbar: { toggle: false, @@ -246,6 +252,15 @@ const viewUpdate = (state: ViewUpdateState = initState, action: AnyAction) => { }), contextOpen: false, }); + case "OPEN_DIRECTORY_DOWNLOAD_DIALOG": + return Object.assign({}, state, { + modals: Object.assign({}, state.modals, { + directoryDownloading: action.downloading, + directoryDownloadLog: action.log, + directoryDownloadDone: action.done, + }), + contextOpen: false, + }); case "CLOSE_CONTEXT_MENU": return Object.assign({}, state, { contextOpen: false, @@ -269,6 +284,9 @@ const viewUpdate = (state: ViewUpdateState = initState, action: AnyAction) => { compress: false, decompress: false, option: undefined, + directoryDownloading: false, + directoryDownloadLog: "", + directoryDownloadDone: false, }), }); case "TOGGLE_SNACKBAR": diff --git a/src/utils/filesystem.ts b/src/utils/filesystem.ts new file mode 100644 index 0000000..8eed7f9 --- /dev/null +++ b/src/utils/filesystem.ts @@ -0,0 +1,81 @@ +// get the paths of files (no directories) in the directory +// parent: "" or "/" +export const getFileSystemDirectoryPaths = async ( + handle: FileSystemDirectoryHandle, + parent = "" +): Promise => { + const paths: Array = []; + + for await (const [path, fileSystemHandle] of handle.entries()) { + if (fileSystemHandle instanceof window.FileSystemFileHandle) { + paths.push(`${parent}${path}`); + } else { + paths.push( + ...(await getFileSystemDirectoryPaths( + fileSystemHandle, + `${parent}${path}/` + )) + ); + } + } + + return paths; +}; + +// create the dst directory if it doesn't exist +// return the dst directory handle +// paths: "/dir1/dir2" => ["dir1","dir2"] +export const createFileSystemDirectory = async ( + handle: FileSystemDirectoryHandle, + paths: string[] +) => { + let cur = handle; + while (paths.length > 0) { + const path = paths.shift(); + if (!path) { + break; + } + cur = await cur.getDirectoryHandle(path, { create: true }); + } + return cur; +}; + +// save file into the dst directory +// create the dst file if it doesn't exist by default +// path: a/b/c.jpg +export const saveFileToFileSystemDirectory = async ( + handle: FileSystemDirectoryHandle, + stream: FileSystemWriteChunkType, + path: string, + create = true +) => { + const paths = path.split("/"); + const fileName = paths.pop(); + if (!fileName) return; + + const dir = await createFileSystemDirectory(handle, paths); + const file = await dir.getFileHandle(fileName, { create }); + const writable = await file.createWritable(); + await writable.write(stream); + await writable.close(); +}; + +// verify or request the permission of the readwrite permission +export async function verifyFileSystemRWPermission( + fileHandle: FileSystemDirectoryHandle +) { + const opts = { mode: "readwrite" as FileSystemPermissionMode }; + + // Check if we already have permission, if so, return true. + if ((await fileHandle.queryPermission(opts)) === "granted") { + return true; + } + + // Request permission to the file, if the user grants permission, return true. + if ((await fileHandle.requestPermission(opts)) === "granted") { + return true; + } + + // The user did not grant permission, return false. + return false; +} diff --git a/yarn.lock b/yarn.lock index 7bcd6a6..7db8301 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1775,6 +1775,11 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" +"@types/js-cookie@^2.x.x": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" + integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== + "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -1875,6 +1880,11 @@ resolved "https://registry.yarnpkg.com/@types/streamsaver/-/streamsaver-2.0.1.tgz#fa5e5b891d1b282be3078c232a30ee004b8e0be0" integrity sha512-I49NtT8w6syBI3Zg3ixCyygTHoTVMY0z2TMRcTgccdIsVd2MwlKk7ITLHLsJtgchUHcOd7QEARG9h0ifcA6l2Q== +"@types/wicg-file-system-access@^2020.9.5": + version "2020.9.5" + resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz#4a0c8f3d1ed101525f329e86c978f7735404474f" + integrity sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -2172,6 +2182,25 @@ adjust-sourcemap-loader@2.0.0: object-path "0.11.4" regex-parser "2.2.10" +ahooks-v3-count@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz#ddeb392e009ad6e748905b3cbf63a9fd8262ca80" + integrity sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ== + +ahooks@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ahooks/-/ahooks-3.5.2.tgz#283e4b2796d3e00f0a280f0f09e2517feaa434e3" + integrity sha512-0OrV3/9s339comBg/F+d9Q7lhZERZK3K5f1J8ebK3z076Kc4KkeGc1MLalFRG+95nVA0wUCZ71oJMs66fIU2Uw== + dependencies: + "@types/js-cookie" "^2.x.x" + ahooks-v3-count "^1.0.0" + dayjs "^1.9.1" + intersection-observer "^0.12.0" + js-cookie "^2.x.x" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -3980,6 +4009,11 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41" integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow== +dayjs@^1.9.1: + version "1.11.3" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258" + integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -6021,6 +6055,11 @@ internmap@^1.0.0: resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== +intersection-observer@^0.12.0: + version "0.12.2" + resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" + integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== + invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -6846,6 +6885,11 @@ jest@24.9.0: import-local "^2.0.0" jest-cli "^24.9.0" +js-cookie@^2.x.x: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -10374,6 +10418,11 @@ schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.2.0: ajv "^6.12.4" ajv-keywords "^3.5.2" +screenfull@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + screenfull@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-6.0.1.tgz#3b71e6f06b72d817a8d3be73c45ebe71fa8da1ce"