commit
9a0a502de2
@ -0,0 +1,11 @@ |
|||||||
|
root = true |
||||||
|
|
||||||
|
[*] |
||||||
|
charset = utf-8 |
||||||
|
end_of_line = lf |
||||||
|
indent_size = 2 |
||||||
|
indent_style = space |
||||||
|
insert_final_newline = false |
||||||
|
trim_trailing_whitespace = true |
||||||
|
max_line_length = 80 |
||||||
|
tab_width = 2 |
@ -0,0 +1,24 @@ |
|||||||
|
# Logs |
||||||
|
logs |
||||||
|
*.log |
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
||||||
|
pnpm-debug.log* |
||||||
|
lerna-debug.log* |
||||||
|
|
||||||
|
node_modules |
||||||
|
dist |
||||||
|
dist-ssr |
||||||
|
*.local |
||||||
|
|
||||||
|
# Editor directories and files |
||||||
|
.vscode/* |
||||||
|
!.vscode/extensions.json |
||||||
|
.idea |
||||||
|
.DS_Store |
||||||
|
*.suo |
||||||
|
*.ntvs* |
||||||
|
*.njsproj |
||||||
|
*.sln |
||||||
|
*.sw? |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"singleAttributePerLine": true |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
# Vue 3 + TypeScript + Vite |
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. |
||||||
|
|
||||||
|
## Recommended IDE Setup |
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). |
||||||
|
|
||||||
|
## Type Support For `.vue` Imports in TS |
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. |
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: |
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension |
||||||
|
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette |
||||||
|
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` |
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. |
@ -0,0 +1,58 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"/> |
||||||
|
<link href="/src/frontend/assets/logo.png" rel="icon" type="image/png"/> |
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> |
||||||
|
<title>Vite + Vue + TS</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"> |
||||||
|
<button id="btn">Show File Manager</button> |
||||||
|
</div> |
||||||
|
<script type="module"> |
||||||
|
const apiUrl = 'https://wfs.yaojiankang.top' |
||||||
|
const artifact = 'genesis' |
||||||
|
|
||||||
|
import {showFileManager} from 'https://wfs.yaojiankang.top/wfs.js?t=12334' |
||||||
|
|
||||||
|
const show = (async () => { |
||||||
|
const response = await fetch(apiUrl + "/login", { |
||||||
|
method: "POST", |
||||||
|
body: new URLSearchParams({ |
||||||
|
name: 'root', |
||||||
|
password: 'root', |
||||||
|
artifact |
||||||
|
}), |
||||||
|
}) |
||||||
|
const res = await response.json(); |
||||||
|
if (!res.ok) { |
||||||
|
throw new Error(res.msg); |
||||||
|
} |
||||||
|
|
||||||
|
const accessToken = res.data.token |
||||||
|
|
||||||
|
showFileManager({ |
||||||
|
artifact, |
||||||
|
accessToken, |
||||||
|
apiUrl, |
||||||
|
select: 'single', |
||||||
|
onOpen() { |
||||||
|
console.log("open") |
||||||
|
}, |
||||||
|
onError(error) { |
||||||
|
console.log('error', error) |
||||||
|
}, |
||||||
|
onDismiss() { |
||||||
|
console.log('dismiss') |
||||||
|
}, |
||||||
|
onConfirm(files) { |
||||||
|
console.log('files', files) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
document.querySelector('#btn').addEventListener('click', show) |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,13 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"/> |
||||||
|
<link href="/src/frontend/assets/logo.png" rel="icon" type="image/png"/> |
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> |
||||||
|
<title>Vite + Vue + TS</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="app"></div> |
||||||
|
<script src="/src/main.ts" type="module"></script> |
||||||
|
</body> |
||||||
|
</html> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,32 @@ |
|||||||
|
{ |
||||||
|
"name": "wfs", |
||||||
|
"private": true, |
||||||
|
"version": "0.0.0", |
||||||
|
"type": "module", |
||||||
|
"scripts": { |
||||||
|
"dev": "vite", |
||||||
|
"build": "vue-tsc && vite build", |
||||||
|
"preview": "vite preview" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@vueuse/core": "^10.7.2", |
||||||
|
"dom-align": "^1.12.4", |
||||||
|
"hls.js": "^1.4.14", |
||||||
|
"viewerjs": "^1.11.6", |
||||||
|
"vue": "^3.3.8" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@tailwindcss/container-queries": "^0.1.1", |
||||||
|
"@tailwindcss/forms": "^0.5.7", |
||||||
|
"@types/node": "^20.10.4", |
||||||
|
"@vitejs/plugin-basic-ssl": "^1.0.2", |
||||||
|
"@vitejs/plugin-vue": "^4.5.0", |
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0", |
||||||
|
"autoprefixer": "^10.4.16", |
||||||
|
"postcss": "^8.4.32", |
||||||
|
"tailwindcss": "^3.4.0", |
||||||
|
"typescript": "^5.2.2", |
||||||
|
"vite": "^5.0.0", |
||||||
|
"vue-tsc": "^1.8.22" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
export default { |
||||||
|
plugins: { |
||||||
|
tailwindcss: {}, |
||||||
|
autoprefixer: {}, |
||||||
|
}, |
||||||
|
}; |
@ -0,0 +1,64 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {onMounted, ref} from 'vue' |
||||||
|
import {installIcons} from "./frontend/widgets/VIcons.ts"; |
||||||
|
import UiContainer from "./frontend/widgets/UiContainer.vue"; |
||||||
|
|
||||||
|
const loading = ref(false) |
||||||
|
|
||||||
|
const apiUrl = 'https://zmf.yaojiankang.top' |
||||||
|
const artifact = 'genesis' |
||||||
|
|
||||||
|
const login = async () => { |
||||||
|
loading.value = true |
||||||
|
const response = await fetch(apiUrl + "/login", { |
||||||
|
method: "POST", |
||||||
|
body: new URLSearchParams({ |
||||||
|
name: 'root', |
||||||
|
password: 'root', |
||||||
|
artifact |
||||||
|
}), |
||||||
|
}) |
||||||
|
const res = await response.json(); |
||||||
|
if (!res.ok) { |
||||||
|
throw new Error(res.msg); |
||||||
|
} |
||||||
|
|
||||||
|
const accessToken = res.data.token |
||||||
|
|
||||||
|
import('./frontend').then(({showFileManager}) => { |
||||||
|
showFileManager({ |
||||||
|
artifact, |
||||||
|
accessToken, |
||||||
|
apiUrl, |
||||||
|
select: 'multiple', |
||||||
|
onOpen() { |
||||||
|
loading.value = false |
||||||
|
}, |
||||||
|
onSelect(files: CloudFile[]) { |
||||||
|
console.log(files) |
||||||
|
}, |
||||||
|
onFail(error: string) { |
||||||
|
console.log(error) |
||||||
|
}, |
||||||
|
onClose() { |
||||||
|
console.log("closed") |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
artifact, |
||||||
|
accessToken, |
||||||
|
apiUrl, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
login() |
||||||
|
installIcons(document.body) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<UiContainer v-if="0" :setup="login"/> |
||||||
|
</template> |
@ -0,0 +1,203 @@ |
|||||||
|
import {$on} from '../shared' |
||||||
|
import {useSocket} from './socket' |
||||||
|
|
||||||
|
let apiUrl: string | undefined; |
||||||
|
let accessToken: string | undefined; |
||||||
|
let artifact: string | undefined; |
||||||
|
|
||||||
|
export async function configApi(cfg: Partial<ApiConfig>) { |
||||||
|
if ( |
||||||
|
cfg.accessToken !== accessToken || |
||||||
|
cfg.apiUrl !== apiUrl || |
||||||
|
cfg.artifact !== artifact |
||||||
|
) { |
||||||
|
await ajax(cfg.apiUrl + "/" + cfg.artifact, { |
||||||
|
headers: {"Authorization": `Bearer ${cfg.accessToken}`} |
||||||
|
}) |
||||||
|
|
||||||
|
accessToken = cfg.accessToken; |
||||||
|
artifact = cfg.artifact; |
||||||
|
apiUrl = cfg.apiUrl; |
||||||
|
|
||||||
|
useSocket(cfg) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function ajax<T>(url: string, init: RequestInit = {}): Promise<T> { |
||||||
|
return new Promise<T>(async (resolve, reject) => { |
||||||
|
if (accessToken) { |
||||||
|
const headers = new Headers(init.headers); |
||||||
|
headers.set("Authorization", `Bearer ${accessToken}`); |
||||||
|
init.headers = headers; |
||||||
|
} |
||||||
|
try { |
||||||
|
const uri = (apiUrl ?? '') + url; |
||||||
|
const response = await fetch(uri, init); |
||||||
|
const res = await response.json(); |
||||||
|
if ( |
||||||
|
res != null && |
||||||
|
typeof res === "object" && |
||||||
|
!Array.isArray(res) && |
||||||
|
"ok" in res && |
||||||
|
"msg" in res && |
||||||
|
typeof res.ok === "boolean" && |
||||||
|
typeof res.msg === "string" |
||||||
|
) { |
||||||
|
if (res.ok) { |
||||||
|
resolve(res.data); |
||||||
|
} else { |
||||||
|
reject(new Error(res.msg)); |
||||||
|
} |
||||||
|
} else { |
||||||
|
reject(new Error("服务器异常")); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
// 服务器返回的有可能不是 JSON 数据
|
||||||
|
if (err instanceof SyntaxError) { |
||||||
|
reject(new Error("服务器异常")); |
||||||
|
} else if (err instanceof Error && err.message === "Failed to fetch") { |
||||||
|
reject(new Error("服务器异常")); |
||||||
|
} else { |
||||||
|
reject(err); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadArgs { |
||||||
|
skip: false; |
||||||
|
token: string; |
||||||
|
key: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadSkip { |
||||||
|
skip: true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 初始化任务 |
||||||
|
*/ |
||||||
|
export function initiateUploadApi( |
||||||
|
fileMime: string, |
||||||
|
fileHash: string, |
||||||
|
signal: AbortSignal, |
||||||
|
): Promise<UploadSkip | UploadArgs> { |
||||||
|
const url = `/${artifact}/objects/initiate`; |
||||||
|
const body = new URLSearchParams({fileMime, fileHash}); |
||||||
|
const init = {method: "POST", body, signal}; |
||||||
|
return ajax<UploadSkip | UploadArgs>(url, init); |
||||||
|
} |
||||||
|
|
||||||
|
export function completeUploadApi( |
||||||
|
fileHash: string, |
||||||
|
fileNames: string[], |
||||||
|
directories: number[], |
||||||
|
signal: AbortSignal, |
||||||
|
) { |
||||||
|
const targets: Map<number, Set<string>> = new Map(); |
||||||
|
for (let i = 0; i < directories.length; i++) { |
||||||
|
const dirid = directories[i]; |
||||||
|
const dirs = targets.get(dirid) ?? new Set(); |
||||||
|
if (!fileNames[i]) return Promise.reject("参数错误"); |
||||||
|
dirs.add(fileNames[i]); |
||||||
|
targets.set(dirid, dirs); |
||||||
|
} |
||||||
|
const body = new URLSearchParams({fileHash}); |
||||||
|
targets.forEach((names, dir) => { |
||||||
|
names.forEach((name) => { |
||||||
|
body.append("directoryId", `${dir}`); |
||||||
|
body.append("fileName", name); |
||||||
|
}); |
||||||
|
}); |
||||||
|
const url = `/${artifact}/objects/complete`; |
||||||
|
return ajax< |
||||||
|
Array<{ |
||||||
|
id: TaskId; |
||||||
|
directoryId: number; |
||||||
|
fileName: string; |
||||||
|
fileHash: string; |
||||||
|
createdAt: string; |
||||||
|
updatedAt: string; |
||||||
|
}> |
||||||
|
>(url, {method: "POST", body, signal}); |
||||||
|
} |
||||||
|
|
||||||
|
interface SimulateQiniuCallbackOptions { |
||||||
|
key: string; |
||||||
|
name: string; |
||||||
|
mime: string; |
||||||
|
md5: FileHash; |
||||||
|
size: number; |
||||||
|
} |
||||||
|
|
||||||
|
export function simulateQiniuCallback( |
||||||
|
data: SimulateQiniuCallbackOptions, |
||||||
|
signal: AbortSignal, |
||||||
|
) { |
||||||
|
const url = `/${artifact}/objects/uploaded`; |
||||||
|
const body = JSON.stringify({...data, etag: "<etag>", bucket: "<bucket>"}); |
||||||
|
return ajax<any>(url, { |
||||||
|
method: "POST", |
||||||
|
body, |
||||||
|
signal, |
||||||
|
headers: {"Content-Type": "application/json"}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
$on('dir', 'create', (data: NewDirEventData): Promise<CloudDirectory> => { |
||||||
|
const {title, pid = 0} = data |
||||||
|
const uri = `/${artifact}/directories`; |
||||||
|
const body = new URLSearchParams({title}); |
||||||
|
if (pid > 0) body.set("parentId", `${pid}`); |
||||||
|
return ajax<CloudDirectory>(uri, {method: "POST", body}); |
||||||
|
}) |
||||||
|
|
||||||
|
// 所有文件夹
|
||||||
|
$on('dir', 'all', (): Promise<CloudDirectory[]> => { |
||||||
|
return ajax<CloudDirectory[]>(`/${artifact}/directories`); |
||||||
|
}) |
||||||
|
|
||||||
|
// 删除文件夹
|
||||||
|
$on('dir', 'delete', (id: number): Promise<boolean> => { |
||||||
|
const url = `/${artifact}/directories/${id}`; |
||||||
|
return ajax<boolean>(url, {method: "DELETE"}); |
||||||
|
}) |
||||||
|
|
||||||
|
// 重命名文件夹
|
||||||
|
$on('dir', 'rename', (data: RenameDirEventData): Promise<CloudDirectory> => { |
||||||
|
const url = `/${artifact}/directories/${data.id}`; |
||||||
|
const body = new URLSearchParams({title: data.title}); |
||||||
|
return ajax<CloudDirectory>(url, {method: "PUT", body}); |
||||||
|
}) |
||||||
|
|
||||||
|
// 文件重命名
|
||||||
|
$on('file', 'rename', async (data: RenameFileEventData): Promise<CloudFile> => { |
||||||
|
const url = `/${artifact}/files/${data.id}`; |
||||||
|
const body = new URLSearchParams({name: data.name}); |
||||||
|
return await ajax(url, {method: "PUT", body}); |
||||||
|
}) |
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
$on('file', 'delete', (files: number[] | number) => { |
||||||
|
let url = `/${artifact}/files`; |
||||||
|
if (!Array.isArray(files)) { |
||||||
|
url += `/${files}`; |
||||||
|
} else { |
||||||
|
const body = new URLSearchParams(); |
||||||
|
files.forEach((file) => body!.append("files", `${file}`)); |
||||||
|
url += "?" + body |
||||||
|
} |
||||||
|
return ajax<void>(url, {method: "DELETE"}); |
||||||
|
}) |
||||||
|
|
||||||
|
// 文件列表(分页)
|
||||||
|
$on('file', 'list', (data: FilesEventData) => { |
||||||
|
const url = `/${artifact}/files`; |
||||||
|
const query = new URLSearchParams(); |
||||||
|
query.set("page", `${data.page}`) |
||||||
|
query.set("limit", `${data.limit}`) |
||||||
|
if (data.directoryId != null) query.set("directoryId", `${data.directoryId}`); |
||||||
|
if (data.mime != null) query.set("mime", data.mime); |
||||||
|
return ajax<FilesEventResult>(url + "?" + query.toString()); |
||||||
|
}) |
@ -0,0 +1,73 @@ |
|||||||
|
import {$on} from '../shared' |
||||||
|
import {configApi} from './api' |
||||||
|
import { |
||||||
|
cleanReadTasks, |
||||||
|
getReadTasks, |
||||||
|
pauseReadTask, |
||||||
|
Reader, |
||||||
|
removeReadTask, |
||||||
|
resumeReadTask, |
||||||
|
} from "./reader"; |
||||||
|
import {handleNetworkEvent} from "./socket"; |
||||||
|
import { |
||||||
|
cleanUploadTasks, |
||||||
|
getUploadTasks, |
||||||
|
pauseUploadTask, |
||||||
|
removeUploadTask, |
||||||
|
resumeUploadTask, |
||||||
|
} from "./uploader"; |
||||||
|
|
||||||
|
// 取消前端任务
|
||||||
|
async function cancel(data: TaskEventData): Promise<void> { |
||||||
|
const {fileHash, taskId} = data; |
||||||
|
await pauseReadTask(taskId); |
||||||
|
await pauseUploadTask(fileHash, taskId); |
||||||
|
} |
||||||
|
|
||||||
|
// 恢复任务
|
||||||
|
async function resume(data: TaskEventData): Promise<void> { |
||||||
|
const {fileHash, taskId} = data; |
||||||
|
await resumeReadTask(taskId); |
||||||
|
await resumeUploadTask(fileHash, taskId); |
||||||
|
} |
||||||
|
|
||||||
|
// 清理任务
|
||||||
|
async function cleanup() { |
||||||
|
await cleanReadTasks(); |
||||||
|
await cleanUploadTasks(); |
||||||
|
} |
||||||
|
|
||||||
|
// 创建任务
|
||||||
|
function create(data: TaskCreateData) { |
||||||
|
Reader.create(data.file, data.dirid); |
||||||
|
} |
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
async function remove(data: TaskEventData): Promise<void> { |
||||||
|
const {fileHash, taskId} = data; |
||||||
|
await removeReadTask(taskId); |
||||||
|
await removeUploadTask(fileHash, taskId); |
||||||
|
} |
||||||
|
|
||||||
|
function allTasks(): Task[] { |
||||||
|
return [ |
||||||
|
...getReadTasks(), |
||||||
|
...getUploadTasks(), |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
// 配置接口事件
|
||||||
|
$on("cfg", "init", configApi); |
||||||
|
|
||||||
|
// 监听前端任务事件
|
||||||
|
$on("task", "all", allTasks) |
||||||
|
$on("task", "cancel", cancel); |
||||||
|
$on("task", "cleanup", cleanup); |
||||||
|
$on("task", "create", create); |
||||||
|
$on("task", "remove", remove); |
||||||
|
$on("task", "resume", resume); |
||||||
|
|
||||||
|
// 网络变化事件
|
||||||
|
$on("net", "status", handleNetworkEvent); |
||||||
|
|
||||||
|
|
@ -0,0 +1,173 @@ |
|||||||
|
import * as utils from '../utils' |
||||||
|
import {normalizeUploadConfig} from '../utils' |
||||||
|
import {Config, InternalConfig, UploadInfo} from '../upload' |
||||||
|
|
||||||
|
interface UpHosts { |
||||||
|
data: { |
||||||
|
up: { |
||||||
|
acc: { |
||||||
|
main: string[] |
||||||
|
backup: string[] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export async function getUpHosts(accessKey: string, bucketName: string, protocol: InternalConfig['upprotocol']): Promise<UpHosts> { |
||||||
|
const params = new URLSearchParams({ak: accessKey, bucket: bucketName}) |
||||||
|
const url = `${protocol}://api.qiniu.com/v2/query?${params}` |
||||||
|
return utils.request(url, {method: 'GET'}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param bucket 空间名 |
||||||
|
* @param key 目标文件名 |
||||||
|
* @param uploadInfo 上传信息 |
||||||
|
*/ |
||||||
|
function getBaseUrl(bucket: string, key: string | null | undefined, uploadInfo: UploadInfo) { |
||||||
|
const {url, id} = uploadInfo |
||||||
|
return `${url}/buckets/${bucket}/objects/${key != null ? utils.urlSafeBase64Encode(key) : '~'}/uploads/${id}` |
||||||
|
} |
||||||
|
|
||||||
|
export interface InitPartsData { |
||||||
|
/** 该文件的上传 id, 后续该文件其他各个块的上传,已上传块的废弃,已上传块的合成文件,都需要该 id */ |
||||||
|
uploadId: string |
||||||
|
/** uploadId 的过期时间 */ |
||||||
|
expireAt: number |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param token 上传鉴权凭证 |
||||||
|
* @param bucket 上传空间 |
||||||
|
* @param key 目标文件名 |
||||||
|
* @param uploadUrl 上传地址 |
||||||
|
*/ |
||||||
|
export function initUploadParts( |
||||||
|
token: string, |
||||||
|
bucket: string, |
||||||
|
key: string | null | undefined, |
||||||
|
uploadUrl: string |
||||||
|
): utils.Response<InitPartsData> { |
||||||
|
const url = `${uploadUrl}/buckets/${bucket}/objects/${key != null ? utils.urlSafeBase64Encode(key) : '~'}/uploads` |
||||||
|
return utils.request<InitPartsData>( |
||||||
|
url, |
||||||
|
{ |
||||||
|
method: 'POST', |
||||||
|
headers: utils.getAuthHeaders(token) |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadChunkData { |
||||||
|
etag: string |
||||||
|
md5: string |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param token 上传鉴权凭证 |
||||||
|
* @param index 当前 chunk 的索引 |
||||||
|
* @param uploadInfo 上传信息 |
||||||
|
* @param options 请求参数 |
||||||
|
*/ |
||||||
|
export function uploadChunk( |
||||||
|
token: string, |
||||||
|
key: string | null | undefined, |
||||||
|
index: number, |
||||||
|
uploadInfo: UploadInfo, |
||||||
|
options: Partial<utils.RequestOptions & { md5: string }> |
||||||
|
): utils.Response<UploadChunkData> { |
||||||
|
const bucket = utils.getPutPolicy(token).bucketName |
||||||
|
const url = getBaseUrl(bucket, key, uploadInfo) + `/${index}` |
||||||
|
const headers = utils.getHeadersForChunkUpload(token) |
||||||
|
if (options.md5) headers['Content-MD5'] = options.md5 |
||||||
|
|
||||||
|
return utils.request<UploadChunkData>(url, { |
||||||
|
...options, |
||||||
|
method: 'PUT', |
||||||
|
headers |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export type UploadCompleteData = any |
||||||
|
|
||||||
|
/** |
||||||
|
* @param token 上传鉴权凭证 |
||||||
|
* @param key 目标文件名 |
||||||
|
* @param uploadInfo 上传信息 |
||||||
|
* @param options 请求参数 |
||||||
|
*/ |
||||||
|
export function uploadComplete( |
||||||
|
token: string, |
||||||
|
key: string | null | undefined, |
||||||
|
uploadInfo: UploadInfo, |
||||||
|
options: Partial<utils.RequestOptions> |
||||||
|
): utils.Response<UploadCompleteData> { |
||||||
|
const bucket = utils.getPutPolicy(token).bucketName |
||||||
|
const url = getBaseUrl(bucket, key, uploadInfo) |
||||||
|
return utils.request<UploadCompleteData>(url, { |
||||||
|
...options, |
||||||
|
method: 'POST', |
||||||
|
headers: utils.getHeadersForMkFile(token) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param token 上传鉴权凭证 |
||||||
|
* @param key 目标文件名 |
||||||
|
* @param uploadInfo 上传信息 |
||||||
|
*/ |
||||||
|
export function deleteUploadedChunks( |
||||||
|
token: string, |
||||||
|
key: string | null | undefined, |
||||||
|
uploadinfo: UploadInfo |
||||||
|
): utils.Response<void> { |
||||||
|
const bucket = utils.getPutPolicy(token).bucketName |
||||||
|
const url = getBaseUrl(bucket, key, uploadinfo) |
||||||
|
return utils.request( |
||||||
|
url, |
||||||
|
{ |
||||||
|
method: 'DELETE', |
||||||
|
headers: utils.getAuthHeaders(token) |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {string} url |
||||||
|
* @param {FormData} data |
||||||
|
* @param {Partial<utils.RequestOptions>} options |
||||||
|
* @returns Promise |
||||||
|
* @description 直传接口 |
||||||
|
*/ |
||||||
|
export function direct( |
||||||
|
url: string, |
||||||
|
data: FormData, |
||||||
|
options: Partial<utils.RequestOptions> |
||||||
|
): Promise<UploadCompleteData> { |
||||||
|
return utils.request<UploadCompleteData>(url, { |
||||||
|
method: 'POST', |
||||||
|
body: data, |
||||||
|
...options |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export type UploadUrlConfig = Partial<Pick<Config, 'upprotocol' | 'uphost' | 'region' | 'useCdnDomain'>> |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {UploadUrlConfig} config |
||||||
|
* @param {string} token |
||||||
|
* @returns Promise |
||||||
|
* @description 获取上传 url |
||||||
|
*/ |
||||||
|
export async function getUploadUrl(_config: UploadUrlConfig, token: string): Promise<string> { |
||||||
|
const config = normalizeUploadConfig(_config) |
||||||
|
const protocol = config.upprotocol |
||||||
|
|
||||||
|
if (config.uphost.length > 0) { |
||||||
|
return `${protocol}://${config.uphost[0]}` |
||||||
|
} |
||||||
|
const putPolicy = utils.getPutPolicy(token) |
||||||
|
const res = await getUpHosts(putPolicy.assessKey, putPolicy.bucketName, protocol) |
||||||
|
const hosts = res.data.up.acc.main |
||||||
|
return `${protocol}://${hosts[0]}` |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export * from './region' |
@ -0,0 +1,37 @@ |
|||||||
|
/** 上传区域 */ |
||||||
|
export const region = { |
||||||
|
z0: 'z0', |
||||||
|
z1: 'z1', |
||||||
|
z2: 'z2', |
||||||
|
na0: 'na0', |
||||||
|
as0: 'as0', |
||||||
|
cnEast2: 'cn-east-2' |
||||||
|
} as const |
||||||
|
|
||||||
|
/** 上传区域对应的 host */ |
||||||
|
export const regionUphostMap = { |
||||||
|
[region.z0]: { |
||||||
|
srcUphost: ['up.qiniup.com'], |
||||||
|
cdnUphost: ['upload.qiniup.com'] |
||||||
|
}, |
||||||
|
[region.z1]: { |
||||||
|
srcUphost: ['up-z1.qiniup.com'], |
||||||
|
cdnUphost: ['upload-z1.qiniup.com'] |
||||||
|
}, |
||||||
|
[region.z2]: { |
||||||
|
srcUphost: ['up-z2.qiniup.com'], |
||||||
|
cdnUphost: ['upload-z2.qiniup.com'] |
||||||
|
}, |
||||||
|
[region.na0]: { |
||||||
|
srcUphost: ['up-na0.qiniup.com'], |
||||||
|
cdnUphost: ['upload-na0.qiniup.com'] |
||||||
|
}, |
||||||
|
[region.as0]: { |
||||||
|
srcUphost: ['up-as0.qiniup.com'], |
||||||
|
cdnUphost: ['upload-as0.qiniup.com'] |
||||||
|
}, |
||||||
|
[region.cnEast2]: { |
||||||
|
srcUphost: ['up-cn-east-2.qiniup.com'], |
||||||
|
cdnUphost: ['upload-cn-east-2.qiniup.com'] |
||||||
|
} |
||||||
|
} as const |
@ -0,0 +1,62 @@ |
|||||||
|
export enum QiniuErrorName { |
||||||
|
// 输入错误
|
||||||
|
InvalidFile = 'InvalidFile', |
||||||
|
InvalidToken = 'InvalidToken', |
||||||
|
InvalidMetadata = 'InvalidMetadata', |
||||||
|
InvalidChunkSize = 'InvalidChunkSize', |
||||||
|
InvalidCustomVars = 'InvalidCustomVars', |
||||||
|
NotAvailableUploadHost = 'NotAvailableUploadHost', |
||||||
|
|
||||||
|
// 缓存相关
|
||||||
|
ReadCacheFailed = 'ReadCacheFailed', |
||||||
|
InvalidCacheData = 'InvalidCacheData', |
||||||
|
WriteCacheFailed = 'WriteCacheFailed', |
||||||
|
RemoveCacheFailed = 'RemoveCacheFailed', |
||||||
|
|
||||||
|
// 图片压缩模块相关
|
||||||
|
GetCanvasContextFailed = 'GetCanvasContextFailed', |
||||||
|
UnsupportedFileType = 'UnsupportedFileType', |
||||||
|
|
||||||
|
// 运行环境相关
|
||||||
|
FileReaderReadFailed = 'FileReaderReadFailed', |
||||||
|
NotAvailableXMLHttpRequest = 'NotAvailableXMLHttpRequest', |
||||||
|
InvalidProgressEventTarget = 'InvalidProgressEventTarget', |
||||||
|
|
||||||
|
// 请求错误
|
||||||
|
RequestError = 'RequestError' |
||||||
|
} |
||||||
|
|
||||||
|
export class QiniuError implements Error { |
||||||
|
public stack: string | undefined |
||||||
|
constructor(public name: QiniuErrorName, public message: string) { |
||||||
|
this.stack = new Error().stack |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class QiniuRequestError extends QiniuError { |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 标记当前的 error 类型是一个 QiniuRequestError |
||||||
|
* @deprecated 下一个大版本将会移除,不推荐使用,推荐直接使用 instanceof 进行判断 |
||||||
|
*/ |
||||||
|
public isRequestError = true |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 发生错误时服务端返回的错误信息,如果返回不是一个合法的 json、则该字段为 undefined |
||||||
|
*/ |
||||||
|
public data?: any |
||||||
|
|
||||||
|
constructor(public code: number, public reqId: string, message: string, data?: any) { |
||||||
|
super(QiniuErrorName.RequestError, message) |
||||||
|
this.data = data |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 由于跨域、证书错误、断网、host 解析失败、系统拦截等原因导致的错误 |
||||||
|
*/ |
||||||
|
export class QiniuNetworkError extends QiniuRequestError { |
||||||
|
constructor(message: string, reqId = '') { |
||||||
|
super(0, reqId, message) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,195 @@ |
|||||||
|
import { request, urlSafeBase64Encode } from '../utils' |
||||||
|
|
||||||
|
export interface ImageViewOptions { |
||||||
|
mode: number |
||||||
|
format?: string |
||||||
|
w?: number |
||||||
|
h?: number |
||||||
|
q?: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface ImageWatermark { |
||||||
|
image: string |
||||||
|
mode: number |
||||||
|
fontsize?: number |
||||||
|
dissolve?: number |
||||||
|
dx?: number |
||||||
|
dy?: number |
||||||
|
gravity?: string |
||||||
|
text?: string |
||||||
|
font?: string |
||||||
|
fill?: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface ImageMogr2 { |
||||||
|
'auto-orient'?: boolean |
||||||
|
strip?: boolean |
||||||
|
thumbnail?: number |
||||||
|
crop?: number |
||||||
|
gravity?: number |
||||||
|
format?: number |
||||||
|
blur?: number |
||||||
|
quality?: number |
||||||
|
rotate?: number |
||||||
|
} |
||||||
|
|
||||||
|
type Pipeline = |
||||||
|
| (ImageWatermark & { fop: 'watermark' }) |
||||||
|
| (ImageViewOptions & { fop: 'imageView2' }) |
||||||
|
| (ImageMogr2 & { fop: 'imageMogr2' }) |
||||||
|
|
||||||
|
export interface Entry { |
||||||
|
domain: string |
||||||
|
key: string |
||||||
|
} |
||||||
|
|
||||||
|
function getImageUrl(key: string, domain: string) { |
||||||
|
key = encodeURIComponent(key) |
||||||
|
if (domain.slice(domain.length - 1) !== '/') { |
||||||
|
domain += '/' |
||||||
|
} |
||||||
|
|
||||||
|
return domain + key |
||||||
|
} |
||||||
|
|
||||||
|
export function imageView2(op: ImageViewOptions, key?: string, domain?: string) { |
||||||
|
if (!/^\d$/.test(String(op.mode))) { |
||||||
|
throw 'mode should be number in imageView2' |
||||||
|
} |
||||||
|
|
||||||
|
const { mode, w, h, q, format } = op |
||||||
|
|
||||||
|
if (!w && !h) { |
||||||
|
throw 'param w and h is empty in imageView2' |
||||||
|
} |
||||||
|
|
||||||
|
let imageUrl = 'imageView2/' + encodeURIComponent(mode) |
||||||
|
imageUrl += w ? '/w/' + encodeURIComponent(w) : '' |
||||||
|
imageUrl += h ? '/h/' + encodeURIComponent(h) : '' |
||||||
|
imageUrl += q ? '/q/' + encodeURIComponent(q) : '' |
||||||
|
imageUrl += format ? '/format/' + encodeURIComponent(format) : '' |
||||||
|
if (key && domain) { |
||||||
|
imageUrl = getImageUrl(key, domain) + '?' + imageUrl |
||||||
|
} |
||||||
|
return imageUrl |
||||||
|
} |
||||||
|
|
||||||
|
// invoke the imageMogr2 api of Qiniu
|
||||||
|
export function imageMogr2(op: ImageMogr2, key?: string, domain?: string) { |
||||||
|
const autoOrient = op['auto-orient'] |
||||||
|
const { thumbnail, strip, gravity, crop, quality, rotate, format, blur } = op |
||||||
|
|
||||||
|
let imageUrl = 'imageMogr2' |
||||||
|
|
||||||
|
imageUrl += autoOrient ? '/auto-orient' : '' |
||||||
|
imageUrl += thumbnail ? '/thumbnail/' + encodeURIComponent(thumbnail) : '' |
||||||
|
imageUrl += strip ? '/strip' : '' |
||||||
|
imageUrl += gravity ? '/gravity/' + encodeURIComponent(gravity) : '' |
||||||
|
imageUrl += quality ? '/quality/' + encodeURIComponent(quality) : '' |
||||||
|
imageUrl += crop ? '/crop/' + encodeURIComponent(crop) : '' |
||||||
|
imageUrl += rotate ? '/rotate/' + encodeURIComponent(rotate) : '' |
||||||
|
imageUrl += format ? '/format/' + encodeURIComponent(format) : '' |
||||||
|
imageUrl += blur ? '/blur/' + encodeURIComponent(blur) : '' |
||||||
|
if (key && domain) { |
||||||
|
imageUrl = getImageUrl(key, domain) + '?' + imageUrl |
||||||
|
} |
||||||
|
return imageUrl |
||||||
|
} |
||||||
|
|
||||||
|
// invoke the watermark api of Qiniu
|
||||||
|
export function watermark(op: ImageWatermark, key?: string, domain?: string) { |
||||||
|
const mode = op.mode |
||||||
|
if (!mode) { |
||||||
|
throw "mode can't be empty in watermark" |
||||||
|
} |
||||||
|
|
||||||
|
let imageUrl = 'watermark/' + mode |
||||||
|
if (mode !== 1 && mode !== 2) { |
||||||
|
throw 'mode is wrong' |
||||||
|
} |
||||||
|
|
||||||
|
if (mode === 1) { |
||||||
|
const image = op.image |
||||||
|
if (!image) { |
||||||
|
throw "image can't be empty in watermark" |
||||||
|
} |
||||||
|
imageUrl += image ? '/image/' + urlSafeBase64Encode(image) : '' |
||||||
|
} |
||||||
|
|
||||||
|
if (mode === 2) { |
||||||
|
const { text, font, fontsize, fill } = op |
||||||
|
if (!text) { |
||||||
|
throw "text can't be empty in watermark" |
||||||
|
} |
||||||
|
imageUrl += text ? '/text/' + urlSafeBase64Encode(text) : '' |
||||||
|
imageUrl += font ? '/font/' + urlSafeBase64Encode(font) : '' |
||||||
|
imageUrl += fontsize ? '/fontsize/' + fontsize : '' |
||||||
|
imageUrl += fill ? '/fill/' + urlSafeBase64Encode(fill) : '' |
||||||
|
} |
||||||
|
|
||||||
|
const { dissolve, gravity, dx, dy } = op |
||||||
|
|
||||||
|
imageUrl += dissolve ? '/dissolve/' + encodeURIComponent(dissolve) : '' |
||||||
|
imageUrl += gravity ? '/gravity/' + encodeURIComponent(gravity) : '' |
||||||
|
imageUrl += dx ? '/dx/' + encodeURIComponent(dx) : '' |
||||||
|
imageUrl += dy ? '/dy/' + encodeURIComponent(dy) : '' |
||||||
|
if (key && domain) { |
||||||
|
imageUrl = getImageUrl(key, domain) + '?' + imageUrl |
||||||
|
} |
||||||
|
return imageUrl |
||||||
|
} |
||||||
|
|
||||||
|
// invoke the imageInfo api of Qiniu
|
||||||
|
export function imageInfo(key: string, domain: string) { |
||||||
|
const url = getImageUrl(key, domain) + '?imageInfo' |
||||||
|
return request(url, { method: 'GET' }) |
||||||
|
} |
||||||
|
|
||||||
|
// invoke the exif api of Qiniu
|
||||||
|
export function exif(key: string, domain: string) { |
||||||
|
const url = getImageUrl(key, domain) + '?exif' |
||||||
|
return request(url, { method: 'GET' }) |
||||||
|
} |
||||||
|
|
||||||
|
export function pipeline(arr: Pipeline[], key?: string, domain?: string) { |
||||||
|
const isArray = Object.prototype.toString.call(arr) === '[object Array]' |
||||||
|
let option: Pipeline |
||||||
|
let errOp = false |
||||||
|
let imageUrl = '' |
||||||
|
if (isArray) { |
||||||
|
for (let i = 0, len = arr.length; i < len; i++) { |
||||||
|
option = arr[i] |
||||||
|
if (!option.fop) { |
||||||
|
throw "fop can't be empty in pipeline" |
||||||
|
} |
||||||
|
switch (option.fop) { |
||||||
|
case 'watermark': |
||||||
|
imageUrl += watermark(option) + '|' |
||||||
|
break |
||||||
|
case 'imageView2': |
||||||
|
imageUrl += imageView2(option) + '|' |
||||||
|
break |
||||||
|
case 'imageMogr2': |
||||||
|
imageUrl += imageMogr2(option) + '|' |
||||||
|
break |
||||||
|
default: |
||||||
|
errOp = true |
||||||
|
break |
||||||
|
} |
||||||
|
if (errOp) { |
||||||
|
throw 'fop is wrong in pipeline' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (key && domain) { |
||||||
|
imageUrl = getImageUrl(key, domain) + '?' + imageUrl |
||||||
|
const length = imageUrl.length |
||||||
|
if (imageUrl.slice(length - 1) === '|') { |
||||||
|
imageUrl = imageUrl.slice(0, length - 1) |
||||||
|
} |
||||||
|
} |
||||||
|
return imageUrl |
||||||
|
} |
||||||
|
|
||||||
|
throw "pipeline's first param should be array" |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
export { |
||||||
|
QiniuErrorName, |
||||||
|
QiniuError, |
||||||
|
QiniuRequestError, |
||||||
|
QiniuNetworkError |
||||||
|
} from './errors' |
||||||
|
export {imageMogr2, watermark, imageInfo, exif, pipeline} from './image' |
||||||
|
export {deleteUploadedChunks, getUploadUrl} from './api' |
||||||
|
export { |
||||||
|
upload, |
||||||
|
type UploadProgress |
||||||
|
} from './upload' |
||||||
|
export {region} from './config' |
||||||
|
|
||||||
|
export { |
||||||
|
// compressImage,
|
||||||
|
// type CompressResult,
|
||||||
|
urlSafeBase64Encode, |
||||||
|
urlSafeBase64Decode, |
||||||
|
getHeadersForMkFile, |
||||||
|
getHeadersForChunkUpload |
||||||
|
} from './utils' |
@ -0,0 +1,73 @@ |
|||||||
|
import {reportV3, V3LogInfo} from './report-v3' |
||||||
|
|
||||||
|
export type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'OFF' |
||||||
|
|
||||||
|
export default class Logger { |
||||||
|
private static id = 0 |
||||||
|
|
||||||
|
// 为每个类分配一个 id
|
||||||
|
// 用以区分不同的上传任务
|
||||||
|
private id = ++Logger.id |
||||||
|
|
||||||
|
constructor( |
||||||
|
private token: string, |
||||||
|
private disableReport = true, |
||||||
|
private level: LogLevel = 'OFF', |
||||||
|
private prefix = 'UPLOAD' |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {V3LogInfo} data 上报的数据。 |
||||||
|
* @param {boolean} retry 重试次数,可选,默认为 3。 |
||||||
|
* @description 向服务端上报统计信息。 |
||||||
|
*/ |
||||||
|
report(data: V3LogInfo, retry?: number) { |
||||||
|
if (this.disableReport) return |
||||||
|
try { |
||||||
|
reportV3(this.token, data, retry) |
||||||
|
} catch (error) { |
||||||
|
this.warn(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 输出 info 级别的调试信息。 |
||||||
|
* @param {unknown[]} args |
||||||
|
*/ |
||||||
|
info(...args: unknown[]) { |
||||||
|
const allowLevel: LogLevel[] = ['INFO'] |
||||||
|
if (allowLevel.includes(this.level)) { |
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(this.getPrintPrefix('INFO'), ...args) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 输出 warn 级别的调试信息。 |
||||||
|
* @param {unknown[]} args |
||||||
|
*/ |
||||||
|
warn(...args: unknown[]) { |
||||||
|
const allowLevel: LogLevel[] = ['INFO', 'WARN'] |
||||||
|
if (allowLevel.includes(this.level)) { |
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(this.getPrintPrefix('WARN'), ...args) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 输出 error 级别的调试信息。 |
||||||
|
* @param {unknown[]} args |
||||||
|
*/ |
||||||
|
error(...args: unknown[]) { |
||||||
|
const allowLevel: LogLevel[] = ['INFO', 'WARN', 'ERROR'] |
||||||
|
if (allowLevel.includes(this.level)) { |
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(this.getPrintPrefix('ERROR'), ...args) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private getPrintPrefix(level: LogLevel) { |
||||||
|
return `Qiniu-JS-SDK [${level}][${this.prefix}#${this.id}]:` |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
import { createXHR, getAuthHeaders } from '../utils' |
||||||
|
|
||||||
|
export interface V3LogInfo { |
||||||
|
code: number |
||||||
|
reqId: string |
||||||
|
host: string |
||||||
|
remoteIp: string |
||||||
|
port: string |
||||||
|
duration: number |
||||||
|
time: number |
||||||
|
bytesSent: number |
||||||
|
upType: 'jssdk-h5' |
||||||
|
size: number |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {string} token 上传使用的 token |
||||||
|
* @param {V3LogInfo} data 上报的统计数据 |
||||||
|
* @param {number} retry 重试的次数,默认值 3 |
||||||
|
* @description v3 版本的日志上传接口,参考文档 https://github.com/qbox/product/blob/master/kodo/uplog.md#%E7%89%88%E6%9C%AC-3。
|
||||||
|
*/ |
||||||
|
export function reportV3(token: string, data: V3LogInfo, retry = 3) { |
||||||
|
const xhr = createXHR() |
||||||
|
xhr.open('POST', 'https://uplog.qbox.me/log/3') |
||||||
|
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') |
||||||
|
xhr.setRequestHeader('Authorization', getAuthHeaders(token).Authorization) |
||||||
|
xhr.onreadystatechange = () => { |
||||||
|
if (xhr.readyState === 4 && xhr.status !== 200 && retry > 0) { |
||||||
|
reportV3(token, data, retry - 1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 顺序参考:https://github.com/qbox/product/blob/master/kodo/uplog.md#%E7%89%88%E6%9C%AC-3
|
||||||
|
const stringifyData = [ |
||||||
|
data.code || '', |
||||||
|
data.reqId || '', |
||||||
|
data.host || '', |
||||||
|
data.remoteIp || '', |
||||||
|
data.port || '', |
||||||
|
data.duration || '', |
||||||
|
data.time || '', |
||||||
|
data.bytesSent || '', |
||||||
|
data.upType || '', |
||||||
|
data.size || '' |
||||||
|
].join(',') |
||||||
|
|
||||||
|
xhr.send(stringifyData) |
||||||
|
} |
@ -0,0 +1,340 @@ |
|||||||
|
import {QiniuError, QiniuErrorName, QiniuRequestError} from '../errors' |
||||||
|
import Logger, {LogLevel} from '../logger' |
||||||
|
import {region} from '../config' |
||||||
|
import * as utils from '../utils' |
||||||
|
|
||||||
|
import {Host, HostPool} from './hosts' |
||||||
|
|
||||||
|
export const DEFAULT_CHUNK_SIZE = 4 // 单位 MB
|
||||||
|
|
||||||
|
// code 信息地址 https://developer.qiniu.com/kodo/3928/error-responses
|
||||||
|
export const FREEZE_CODE_LIST = [0, 502, 503, 504, 599] // 将会冻结当前 host 的 code
|
||||||
|
export const RETRY_CODE_LIST = [...FREEZE_CODE_LIST, 612] // 会进行重试的 code
|
||||||
|
|
||||||
|
/** 上传文件的资源信息配置 */ |
||||||
|
export interface Extra { |
||||||
|
/** 文件原文件名 */ |
||||||
|
fname: string |
||||||
|
/** 用来放置自定义变量 */ |
||||||
|
customVars?: { [key: string]: string } |
||||||
|
/** 自定义元信息 */ |
||||||
|
metadata?: { [key: string]: string } |
||||||
|
/** 文件类型设置 */ |
||||||
|
mimeType?: string //
|
||||||
|
} |
||||||
|
|
||||||
|
export interface InternalConfig { |
||||||
|
/** 是否开启 cdn 加速 */ |
||||||
|
useCdnDomain: boolean |
||||||
|
/** 是否开启服务端校验 */ |
||||||
|
checkByServer: boolean |
||||||
|
/** 是否对分片进行 md5校验 */ |
||||||
|
checkByMD5: boolean |
||||||
|
/** 强制直传 */ |
||||||
|
forceDirect: boolean |
||||||
|
/** 上传失败后重试次数 */ |
||||||
|
retryCount: number |
||||||
|
/** 自定义上传域名 */ |
||||||
|
uphost: string[] |
||||||
|
/** 自定义分片上传并发请求量 */ |
||||||
|
concurrentRequestLimit: number |
||||||
|
/** 分片大小,单位为 MB */ |
||||||
|
chunkSize: number |
||||||
|
/** 上传域名协议 */ |
||||||
|
upprotocol: 'https' | 'http' |
||||||
|
/** 上传区域 */ |
||||||
|
region?: typeof region[keyof typeof region] |
||||||
|
/** 是否禁止统计日志上报 */ |
||||||
|
disableStatisticsReport: boolean |
||||||
|
/** 设置调试日志输出模式,默认 `OFF`,不输出任何日志 */ |
||||||
|
debugLogLevel?: LogLevel |
||||||
|
} |
||||||
|
|
||||||
|
/** 上传任务的配置信息 */ |
||||||
|
export interface Config extends Partial<Omit<InternalConfig, 'upprotocol' | 'uphost'>> { |
||||||
|
/** 上传域名协议 */ |
||||||
|
upprotocol?: InternalConfig['upprotocol'] | 'https:' | 'http:' |
||||||
|
/** 自定义上传域名 */ |
||||||
|
uphost?: InternalConfig['uphost'] | string |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadOptions { |
||||||
|
file: File |
||||||
|
key: string | null | undefined |
||||||
|
token: string |
||||||
|
config: InternalConfig |
||||||
|
putExtra?: Partial<Extra> |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadInfo { |
||||||
|
id: string |
||||||
|
url: string |
||||||
|
} |
||||||
|
|
||||||
|
/** 传递给外部的上传进度信息 */ |
||||||
|
export interface UploadProgress { |
||||||
|
total: ProgressCompose |
||||||
|
uploadInfo?: UploadInfo |
||||||
|
chunks?: ProgressCompose[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadHandlers { |
||||||
|
onData: (data: UploadProgress) => void |
||||||
|
onError: (err: QiniuError) => void |
||||||
|
onComplete: (res: any) => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface Progress { |
||||||
|
total: number |
||||||
|
loaded: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface ProgressCompose { |
||||||
|
size: number |
||||||
|
loaded: number |
||||||
|
percent: number |
||||||
|
fromCache?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export type XHRHandler = (xhr: XMLHttpRequest) => void |
||||||
|
|
||||||
|
const GB = 1024 ** 3 |
||||||
|
|
||||||
|
export default abstract class Base { |
||||||
|
protected config: InternalConfig |
||||||
|
protected putExtra: Extra |
||||||
|
|
||||||
|
protected aborted = false |
||||||
|
protected retryCount = 0 |
||||||
|
|
||||||
|
protected uploadHost?: Host |
||||||
|
protected xhrList: XMLHttpRequest[] = [] |
||||||
|
|
||||||
|
protected file: File |
||||||
|
protected key: string | null | undefined |
||||||
|
|
||||||
|
protected token: string |
||||||
|
protected assessKey: string = '' |
||||||
|
protected bucketName: string = '' |
||||||
|
|
||||||
|
protected uploadAt?: number |
||||||
|
protected progress?: UploadProgress |
||||||
|
|
||||||
|
protected onData: (data: UploadProgress) => void |
||||||
|
protected onError: (err: QiniuError) => void |
||||||
|
protected onComplete: (res: any) => void |
||||||
|
|
||||||
|
constructor( |
||||||
|
options: UploadOptions, |
||||||
|
handlers: UploadHandlers, |
||||||
|
protected hostPool: HostPool, |
||||||
|
protected logger: Logger |
||||||
|
) { |
||||||
|
|
||||||
|
this.config = options.config |
||||||
|
logger.info('config inited.', this.config) |
||||||
|
|
||||||
|
this.putExtra = { |
||||||
|
fname: '', |
||||||
|
...options.putExtra |
||||||
|
} |
||||||
|
|
||||||
|
logger.info('putExtra inited.', this.putExtra) |
||||||
|
|
||||||
|
this.key = options.key |
||||||
|
this.file = options.file |
||||||
|
this.token = options.token |
||||||
|
|
||||||
|
this.onData = handlers.onData |
||||||
|
this.onError = handlers.onError |
||||||
|
this.onComplete = handlers.onComplete |
||||||
|
|
||||||
|
try { |
||||||
|
const putPolicy = utils.getPutPolicy(this.token) |
||||||
|
this.bucketName = putPolicy.bucketName |
||||||
|
this.assessKey = putPolicy.assessKey |
||||||
|
} catch (error) { |
||||||
|
logger.error('get putPolicy from token failed.', error) |
||||||
|
this.onError(error as QiniuError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @returns Promise 返回结果与上传最终状态无关,状态信息请通过 [Subscriber] 获取。 |
||||||
|
* @description 上传文件,状态信息请通过 [Subscriber] 获取。 |
||||||
|
*/ |
||||||
|
public async putFile(): Promise<void> { |
||||||
|
this.aborted = false |
||||||
|
if (!this.putExtra.fname) { |
||||||
|
this.logger.info('use file.name as fname.') |
||||||
|
this.putExtra.fname = this.file.name |
||||||
|
} |
||||||
|
|
||||||
|
if (this.file.size > 10000 * GB) { |
||||||
|
this.handleError(new QiniuError( |
||||||
|
QiniuErrorName.InvalidFile, |
||||||
|
'file size exceed maximum value 10000G' |
||||||
|
)) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (this.putExtra.customVars) { |
||||||
|
if (!utils.isCustomVarsValid(this.putExtra.customVars)) { |
||||||
|
this.handleError(new QiniuError( |
||||||
|
QiniuErrorName.InvalidCustomVars, |
||||||
|
'customVars key should start with x:' |
||||||
|
)) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (this.putExtra.metadata) { |
||||||
|
if (!utils.isMetaDataValid(this.putExtra.metadata)) { |
||||||
|
this.handleError(new QiniuError( |
||||||
|
QiniuErrorName.InvalidMetadata, |
||||||
|
'metadata key should start with x-qn-meta-' |
||||||
|
)) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
this.uploadAt = new Date().getTime() |
||||||
|
await this.checkAndUpdateUploadHost() |
||||||
|
const result = await this.run() |
||||||
|
this.onComplete(result.data) |
||||||
|
this.checkAndUnfreezeHost() |
||||||
|
this.sendLog(result.reqId, 200) |
||||||
|
return |
||||||
|
} catch (err) { |
||||||
|
if (this.aborted) { |
||||||
|
this.logger.warn('upload is aborted.') |
||||||
|
this.sendLog('', -2) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
this.clear() |
||||||
|
this.logger.error(err) |
||||||
|
if (err instanceof QiniuRequestError) { |
||||||
|
this.sendLog(err.reqId, err.code) |
||||||
|
|
||||||
|
// 检查并冻结当前的 host
|
||||||
|
this.checkAndFreezeHost(err) |
||||||
|
|
||||||
|
const notReachRetryCount = ++this.retryCount <= this.config.retryCount |
||||||
|
const needRetry = RETRY_CODE_LIST.includes(err.code) |
||||||
|
|
||||||
|
// 以下条件满足其中之一则会进行重新上传:
|
||||||
|
// 1. 满足 needRetry 的条件且 retryCount 不为 0
|
||||||
|
// 2. uploadId 无效时在 resume 里会清除本地数据,并且这里触发重新上传
|
||||||
|
if (needRetry && notReachRetryCount) { |
||||||
|
this.logger.warn(`error auto retry: ${this.retryCount}/${this.config.retryCount}.`) |
||||||
|
this.putFile() |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.onError(err as QiniuError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public stop() { |
||||||
|
this.logger.info('aborted.') |
||||||
|
this.clear() |
||||||
|
this.aborted = true |
||||||
|
} |
||||||
|
|
||||||
|
public addXhr(xhr: XMLHttpRequest) { |
||||||
|
this.xhrList.push(xhr) |
||||||
|
} |
||||||
|
|
||||||
|
public getProgressInfoItem(loaded: number, size: number, fromCache?: boolean): ProgressCompose { |
||||||
|
return { |
||||||
|
size, |
||||||
|
loaded, |
||||||
|
percent: loaded / size * 100, |
||||||
|
...(fromCache == null ? {} : {fromCache}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @returns utils.Response<any> |
||||||
|
* @description 子类通过该方法实现具体的任务处理 |
||||||
|
*/ |
||||||
|
protected abstract run(): utils.Response<any> |
||||||
|
|
||||||
|
// 检查并更新 upload host
|
||||||
|
protected async checkAndUpdateUploadHost() { |
||||||
|
// 从 hostPool 中获取一个可用的 host 挂载在 this
|
||||||
|
this.logger.info('get available upload host.') |
||||||
|
const newHost = await this.hostPool.getUp( |
||||||
|
this.assessKey, |
||||||
|
this.bucketName, |
||||||
|
this.config.upprotocol |
||||||
|
) |
||||||
|
|
||||||
|
if (newHost == null) { |
||||||
|
throw new QiniuError( |
||||||
|
QiniuErrorName.NotAvailableUploadHost, |
||||||
|
'no available upload host.' |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (this.uploadHost != null && this.uploadHost.host !== newHost.host) { |
||||||
|
this.logger.warn(`host switches from ${this.uploadHost.host} to ${newHost.host}.`) |
||||||
|
} else { |
||||||
|
this.logger.info(`use host ${newHost.host}.`) |
||||||
|
} |
||||||
|
|
||||||
|
this.uploadHost = newHost |
||||||
|
} |
||||||
|
|
||||||
|
// 检查并解冻当前的 host
|
||||||
|
protected checkAndUnfreezeHost() { |
||||||
|
this.logger.info('check unfreeze host.') |
||||||
|
if (this.uploadHost != null && this.uploadHost.isFrozen()) { |
||||||
|
this.logger.warn(`${this.uploadHost.host} will be unfrozen.`) |
||||||
|
this.uploadHost.unfreeze() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 检查并更新冻结当前的 host
|
||||||
|
private checkAndFreezeHost(error: QiniuError) { |
||||||
|
this.logger.info('check freeze host.') |
||||||
|
if (error instanceof QiniuRequestError && this.uploadHost != null) { |
||||||
|
if (FREEZE_CODE_LIST.includes(error.code)) { |
||||||
|
this.logger.warn(`${this.uploadHost.host} will be temporarily frozen.`) |
||||||
|
this.uploadHost.freeze() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private handleError(error: QiniuError) { |
||||||
|
this.logger.error(error.message) |
||||||
|
this.onError(error) |
||||||
|
} |
||||||
|
|
||||||
|
private clear() { |
||||||
|
this.xhrList.forEach(xhr => { |
||||||
|
xhr.onreadystatechange = null |
||||||
|
xhr.abort() |
||||||
|
}) |
||||||
|
this.xhrList = [] |
||||||
|
this.logger.info('cleanup uploading xhr.') |
||||||
|
} |
||||||
|
|
||||||
|
private sendLog(reqId: string, code: number) { |
||||||
|
this.logger.report({ |
||||||
|
code, |
||||||
|
reqId, |
||||||
|
remoteIp: '', |
||||||
|
upType: 'jssdk-h5', |
||||||
|
size: this.file.size, |
||||||
|
time: Math.floor(this.uploadAt! / 1000), |
||||||
|
port: utils.getPortFromUrl(this.uploadHost?.getUrl()), |
||||||
|
host: utils.getDomainFromUrl(this.uploadHost?.getUrl()), |
||||||
|
bytesSent: this.progress ? this.progress.total.loaded : 0, |
||||||
|
duration: Math.floor((new Date().getTime() - this.uploadAt!) / 1000) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
import { CRC32 } from '../utils/crc32' |
||||||
|
|
||||||
|
import { direct } from '../api' |
||||||
|
|
||||||
|
import Base from './base' |
||||||
|
|
||||||
|
export default class Direct extends Base { |
||||||
|
|
||||||
|
protected async run() { |
||||||
|
this.logger.info('start run Direct.') |
||||||
|
|
||||||
|
const formData = new FormData() |
||||||
|
formData.append('file', this.file) |
||||||
|
formData.append('token', this.token) |
||||||
|
if (this.key != null) { |
||||||
|
formData.append('key', this.key) |
||||||
|
} |
||||||
|
formData.append('fname', this.putExtra.fname) |
||||||
|
|
||||||
|
if (this.config.checkByServer) { |
||||||
|
const crcSign = await CRC32.file(this.file) |
||||||
|
formData.append('crc32', crcSign.toString()) |
||||||
|
} |
||||||
|
|
||||||
|
if (this.putExtra.customVars) { |
||||||
|
this.logger.info('init customVars.') |
||||||
|
const { customVars } = this.putExtra |
||||||
|
Object.keys(customVars).forEach(key => formData.append(key, customVars[key].toString())) |
||||||
|
this.logger.info('customVars inited.') |
||||||
|
} |
||||||
|
|
||||||
|
if (this.putExtra.metadata) { |
||||||
|
this.logger.info('init metadata.') |
||||||
|
const { metadata } = this.putExtra |
||||||
|
Object.keys(metadata).forEach(key => formData.append(key, metadata[key].toString())) |
||||||
|
} |
||||||
|
|
||||||
|
this.logger.info('formData inited.') |
||||||
|
const result = await direct(this.uploadHost!.getUrl(), formData, { |
||||||
|
onProgress: data => { |
||||||
|
this.updateDirectProgress(data.loaded, data.total) |
||||||
|
}, |
||||||
|
onCreate: xhr => this.addXhr(xhr) |
||||||
|
}) |
||||||
|
|
||||||
|
this.logger.info('Direct progress finish.') |
||||||
|
this.finishDirectProgress() |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
private updateDirectProgress(loaded: number, total: number) { |
||||||
|
// 当请求未完成时可能进度会达到100,所以total + 1来防止这种情况出现
|
||||||
|
this.progress = { total: this.getProgressInfoItem(loaded, total + 1) } |
||||||
|
this.onData(this.progress) |
||||||
|
} |
||||||
|
|
||||||
|
private finishDirectProgress() { |
||||||
|
// 在某些浏览器环境下,xhr 的 progress 事件无法被触发,progress 为 null,这里 fake 下
|
||||||
|
if (!this.progress) { |
||||||
|
this.logger.warn('progress is null.') |
||||||
|
this.progress = { total: this.getProgressInfoItem(this.file.size, this.file.size) } |
||||||
|
this.onData(this.progress) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const { total } = this.progress |
||||||
|
this.progress = { total: this.getProgressInfoItem(total.loaded + 1, total.size) } |
||||||
|
this.onData(this.progress) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,134 @@ |
|||||||
|
import {getUpHosts} from '../api' |
||||||
|
import {InternalConfig} from './base' |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 解冻时间,key 是 host,value 为解冻时间 |
||||||
|
*/ |
||||||
|
const unfreezeTimeMap = new Map<string, number>() |
||||||
|
|
||||||
|
export class Host { |
||||||
|
constructor(public host: string, public protocol: InternalConfig['upprotocol']) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 当前 host 是否为冻结状态 |
||||||
|
*/ |
||||||
|
isFrozen() { |
||||||
|
const currentTime = new Date().getTime() |
||||||
|
const unfreezeTime = unfreezeTimeMap.get(this.host) |
||||||
|
return unfreezeTime != null && unfreezeTime >= currentTime |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {number} time 单位秒,默认 20s |
||||||
|
* @description 冻结该 host 对象,该 host 将在指定时间内不可用 |
||||||
|
*/ |
||||||
|
freeze(time = 20) { |
||||||
|
const unfreezeTime = new Date().getTime() + (time * 1000) |
||||||
|
unfreezeTimeMap.set(this.host, unfreezeTime) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 解冻该 host |
||||||
|
*/ |
||||||
|
unfreeze() { |
||||||
|
unfreezeTimeMap.delete(this.host) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 获取当前 host 的完整 url |
||||||
|
*/ |
||||||
|
getUrl() { |
||||||
|
return `${this.protocol}://${this.host}` |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 获取解冻时间 |
||||||
|
*/ |
||||||
|
getUnfreezeTime() { |
||||||
|
return unfreezeTimeMap.get(this.host) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class HostPool { |
||||||
|
/** |
||||||
|
* @description 缓存的 host 表,以 bucket 和 accessKey 作为 key |
||||||
|
*/ |
||||||
|
private cachedHostsMap = new Map<string, Host[]>() |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {string[]} initHosts |
||||||
|
* @description 如果在构造时传入 initHosts,则该 host 池始终使用传入的 initHosts 做为可用的数据 |
||||||
|
*/ |
||||||
|
constructor(private initHosts: string[] = []) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {string} accessKey |
||||||
|
* @param {string} bucketName |
||||||
|
* @param {InternalConfig['upprotocol']} protocol |
||||||
|
* @returns {Promise<Host | null>} |
||||||
|
* @description 获取一个可用的上传 Host,排除已冻结的 |
||||||
|
*/ |
||||||
|
public async getUp(accessKey: string, bucketName: string, protocol: InternalConfig['upprotocol']): Promise<Host | null> { |
||||||
|
await this.refresh(accessKey, bucketName, protocol) |
||||||
|
const cachedHostList = this.cachedHostsMap.get(`${accessKey}@${bucketName}`) || [] |
||||||
|
|
||||||
|
if (cachedHostList.length === 0) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
const availableHostList = cachedHostList.filter(host => !host.isFrozen()) |
||||||
|
if (availableHostList.length > 0) { |
||||||
|
return availableHostList[0] |
||||||
|
} |
||||||
|
|
||||||
|
// 无可用的,去取离解冻最近的 host
|
||||||
|
const priorityQueue = cachedHostList |
||||||
|
.slice() |
||||||
|
.sort((hostA, hostB) => (hostA.getUnfreezeTime() || 0) - (hostB.getUnfreezeTime() || 0)) |
||||||
|
|
||||||
|
return priorityQueue[0] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {string} accessKey |
||||||
|
* @param {string} bucketName |
||||||
|
* @param {string[]} hosts |
||||||
|
* @param {InternalConfig['upprotocol']} protocol |
||||||
|
* @returns {void} |
||||||
|
* @description 注册可用 host |
||||||
|
*/ |
||||||
|
private register(accessKey: string, bucketName: string, hosts: string[], protocol: InternalConfig['upprotocol']): void { |
||||||
|
this.cachedHostsMap.set( |
||||||
|
`${accessKey}@${bucketName}`, |
||||||
|
hosts.map(host => new Host(host, protocol)) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {string} accessKey |
||||||
|
* @param {string} bucketName |
||||||
|
* @param {InternalConfig['upprotocol']} protocol |
||||||
|
* @returns {Promise<void>} |
||||||
|
* @description 刷新最新的 host 数据,如果用户在构造时该类时传入了 host 或者已经存在缓存则不会发起请求 |
||||||
|
*/ |
||||||
|
private async refresh(accessKey: string, bucketName: string, protocol: InternalConfig['upprotocol']): Promise<void> { |
||||||
|
const cachedHostList = this.cachedHostsMap.get(`${accessKey}@${bucketName}`) || [] |
||||||
|
if (cachedHostList.length > 0) return |
||||||
|
|
||||||
|
if (this.initHosts.length > 0) { |
||||||
|
this.register(accessKey, bucketName, this.initHosts, protocol) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const response = await getUpHosts(accessKey, bucketName, protocol) |
||||||
|
if (response?.data != null) { |
||||||
|
const stashHosts: string[] = [ |
||||||
|
...(response.data.up?.acc?.main || []), |
||||||
|
...(response.data.up?.acc?.backup || []) |
||||||
|
] |
||||||
|
this.register(accessKey, bucketName, stashHosts, protocol) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
import Resume from './resume' |
||||||
|
import Direct from './direct' |
||||||
|
import Logger from '../logger' |
||||||
|
import {UploadCompleteData} from '../api' |
||||||
|
import {IObserver, MB, normalizeUploadConfig, Observable} from '../utils' |
||||||
|
import {QiniuError, QiniuNetworkError, QiniuRequestError} from '../errors' |
||||||
|
import { |
||||||
|
Config, |
||||||
|
Extra, |
||||||
|
UploadHandlers, |
||||||
|
UploadOptions, |
||||||
|
UploadProgress |
||||||
|
} from './base' |
||||||
|
import {HostPool} from './hosts' |
||||||
|
|
||||||
|
export * from './base' |
||||||
|
export * from './resume' |
||||||
|
|
||||||
|
export type { |
||||||
|
UploadProgress |
||||||
|
} |
||||||
|
|
||||||
|
export function createUploadManager( |
||||||
|
options: UploadOptions, |
||||||
|
handlers: UploadHandlers, |
||||||
|
hostPool: HostPool, |
||||||
|
logger: Logger |
||||||
|
) { |
||||||
|
if (options.config && options.config.forceDirect) { |
||||||
|
logger.info('ues forceDirect mode.') |
||||||
|
return new Direct(options, handlers, hostPool, logger) |
||||||
|
} |
||||||
|
|
||||||
|
if (options.file.size > 4 * MB) { |
||||||
|
logger.info('file size over 4M, use Resume.') |
||||||
|
return new Resume(options, handlers, hostPool, logger) |
||||||
|
} |
||||||
|
|
||||||
|
logger.info('file size less or equal than 4M, use Direct.') |
||||||
|
return new Direct(options, handlers, hostPool, logger) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param file 上传文件 |
||||||
|
* @param key 目标文件名 |
||||||
|
* @param token 上传凭证 |
||||||
|
* @param putExtra 上传文件的相关资源信息配置 |
||||||
|
* @param config 上传任务的配置 |
||||||
|
* @returns 返回用于上传任务的可观察对象 |
||||||
|
*/ |
||||||
|
export function upload( |
||||||
|
file: File, |
||||||
|
key: string | null | undefined, |
||||||
|
token: string, |
||||||
|
putExtra?: Partial<Extra>, |
||||||
|
config?: Config |
||||||
|
): Observable<UploadProgress, QiniuError | QiniuRequestError | QiniuNetworkError, UploadCompleteData> { |
||||||
|
|
||||||
|
// 为每个任务创建单独的 Logger
|
||||||
|
const logger = new Logger(token, config?.disableStatisticsReport, config?.debugLogLevel, file.name) |
||||||
|
|
||||||
|
const options: UploadOptions = { |
||||||
|
file, |
||||||
|
key, |
||||||
|
token, |
||||||
|
putExtra, |
||||||
|
config: normalizeUploadConfig(config, logger) |
||||||
|
} |
||||||
|
|
||||||
|
// 创建 host 池
|
||||||
|
const hostPool = new HostPool(options.config.uphost) |
||||||
|
|
||||||
|
return new Observable((observer: IObserver< |
||||||
|
UploadProgress, |
||||||
|
QiniuError | QiniuRequestError | QiniuNetworkError, |
||||||
|
UploadCompleteData |
||||||
|
>) => { |
||||||
|
const manager = createUploadManager(options, { |
||||||
|
onData: (data: UploadProgress) => observer.next(data), |
||||||
|
onError: (err: QiniuError) => observer.error(err), |
||||||
|
onComplete: (res: any) => observer.complete(res) |
||||||
|
}, hostPool, logger) |
||||||
|
manager.putFile() |
||||||
|
return manager.stop.bind(manager) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,314 @@ |
|||||||
|
import { |
||||||
|
initUploadParts, |
||||||
|
uploadChunk, |
||||||
|
UploadChunkData, |
||||||
|
uploadComplete |
||||||
|
} from '../api' |
||||||
|
import {QiniuError, QiniuErrorName, QiniuRequestError} from '../errors' |
||||||
|
import * as utils from '../utils' |
||||||
|
|
||||||
|
import Base, {Extra, Progress, UploadInfo} from './base' |
||||||
|
|
||||||
|
export interface UploadedChunkStorage extends UploadChunkData { |
||||||
|
size: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface ChunkLoaded { |
||||||
|
mkFileProgress: 0 | 1 |
||||||
|
chunks: number[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface ChunkInfo { |
||||||
|
chunk: Blob |
||||||
|
index: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface LocalInfo { |
||||||
|
data: UploadedChunkStorage[] |
||||||
|
id: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface ChunkPart { |
||||||
|
etag: string |
||||||
|
partNumber: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface UploadChunkBody extends Extra { |
||||||
|
parts: ChunkPart[] |
||||||
|
} |
||||||
|
|
||||||
|
/** 是否为正整数 */ |
||||||
|
function isPositiveInteger(n: number) { |
||||||
|
const re = /^[1-9]\d*$/ |
||||||
|
return re.test(String(n)) |
||||||
|
} |
||||||
|
|
||||||
|
export default class Resume extends Base { |
||||||
|
/** |
||||||
|
* @description 文件的分片 chunks |
||||||
|
*/ |
||||||
|
private chunks: Blob[] = [] |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 使用缓存的 chunks |
||||||
|
*/ |
||||||
|
private usedCacheList: boolean[] = [] |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 来自缓存的上传信息 |
||||||
|
*/ |
||||||
|
private cachedUploadedList: UploadedChunkStorage[] = [] |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 当前上传过程中已完成的上传信息 |
||||||
|
*/ |
||||||
|
private uploadedList: UploadedChunkStorage[] = [] |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 当前上传片进度信息 |
||||||
|
*/ |
||||||
|
private loaded?: ChunkLoaded |
||||||
|
|
||||||
|
/** |
||||||
|
* @description 当前上传任务的 id |
||||||
|
*/ |
||||||
|
private uploadId?: string |
||||||
|
|
||||||
|
/** |
||||||
|
* @returns {Promise<ResponseSuccess<any>>} |
||||||
|
* @description 实现了 Base 的 run 接口,处理具体的分片上传事务,并抛出过程中的异常。 |
||||||
|
*/ |
||||||
|
protected async run() { |
||||||
|
this.logger.info('start run Resume.') |
||||||
|
if (!this.config.chunkSize || !isPositiveInteger(this.config.chunkSize)) { |
||||||
|
throw new QiniuError( |
||||||
|
QiniuErrorName.InvalidChunkSize, |
||||||
|
'chunkSize must be a positive integer' |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (this.config.chunkSize > 1024) { |
||||||
|
throw new QiniuError( |
||||||
|
QiniuErrorName.InvalidChunkSize, |
||||||
|
'chunkSize maximum value is 1024' |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
await this.initBeforeUploadChunks() |
||||||
|
|
||||||
|
const pool = new utils.Pool( |
||||||
|
async (chunkInfo: ChunkInfo) => { |
||||||
|
if (this.aborted) { |
||||||
|
pool.abort() |
||||||
|
throw new Error('pool is aborted') |
||||||
|
} |
||||||
|
|
||||||
|
await this.uploadChunk(chunkInfo) |
||||||
|
}, |
||||||
|
this.config.concurrentRequestLimit |
||||||
|
) |
||||||
|
|
||||||
|
let mkFileResponse = null |
||||||
|
const localKey = this.getLocalKey() |
||||||
|
const uploadChunks = this.chunks.map((chunk, index) => pool.enqueue({ |
||||||
|
chunk, |
||||||
|
index |
||||||
|
})) |
||||||
|
|
||||||
|
try { |
||||||
|
await Promise.all(uploadChunks) |
||||||
|
mkFileResponse = await this.mkFileReq() |
||||||
|
} catch (error) { |
||||||
|
// uploadId 无效,上传参数有误(多由于本地存储信息的 uploadId 失效)
|
||||||
|
if (error instanceof QiniuRequestError && (error.code === 612 || error.code === 400)) { |
||||||
|
utils.removeLocalFileInfo(localKey, this.logger) |
||||||
|
} |
||||||
|
|
||||||
|
throw error |
||||||
|
} |
||||||
|
|
||||||
|
// 上传成功,清理本地缓存数据
|
||||||
|
utils.removeLocalFileInfo(localKey, this.logger) |
||||||
|
return mkFileResponse |
||||||
|
} |
||||||
|
|
||||||
|
private async uploadChunk(chunkInfo: ChunkInfo) { |
||||||
|
const {index, chunk} = chunkInfo |
||||||
|
const cachedInfo = this.cachedUploadedList[index] |
||||||
|
this.logger.info(`upload part ${index}, cache:`, cachedInfo) |
||||||
|
|
||||||
|
const shouldCheckMD5 = this.config.checkByMD5 |
||||||
|
const reuseSaved = () => { |
||||||
|
this.usedCacheList[index] = true |
||||||
|
this.updateChunkProgress(chunk.size, index) |
||||||
|
this.uploadedList[index] = cachedInfo |
||||||
|
this.updateLocalCache() |
||||||
|
} |
||||||
|
|
||||||
|
// FIXME: 至少判断一下 size
|
||||||
|
if (cachedInfo && !shouldCheckMD5) { |
||||||
|
reuseSaved() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const md5 = await utils.computeMd5(chunk) |
||||||
|
this.logger.info('computed part md5.', md5) |
||||||
|
|
||||||
|
if (cachedInfo && md5 === cachedInfo.md5) { |
||||||
|
reuseSaved() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// 没有使用缓存设置标记为 false
|
||||||
|
this.usedCacheList[index] = false |
||||||
|
|
||||||
|
const onProgress = (data: Progress) => { |
||||||
|
this.updateChunkProgress(data.loaded, index) |
||||||
|
} |
||||||
|
|
||||||
|
const requestOptions = { |
||||||
|
body: chunk, |
||||||
|
md5: this.config.checkByServer ? md5 : undefined, |
||||||
|
onProgress, |
||||||
|
onCreate: (xhr: XMLHttpRequest) => this.addXhr(xhr) |
||||||
|
} |
||||||
|
|
||||||
|
this.logger.info(`part ${index} start uploading.`) |
||||||
|
const response = await uploadChunk( |
||||||
|
this.token, |
||||||
|
this.key, |
||||||
|
chunkInfo.index + 1, |
||||||
|
this.getUploadInfo(), |
||||||
|
requestOptions |
||||||
|
) |
||||||
|
this.logger.info(`part ${index} upload completed.`) |
||||||
|
|
||||||
|
// 在某些浏览器环境下,xhr 的 progress 事件无法被触发,progress 为 null,这里在每次分片上传完成后都手动更新下 progress
|
||||||
|
onProgress({ |
||||||
|
loaded: chunk.size, |
||||||
|
total: chunk.size |
||||||
|
}) |
||||||
|
|
||||||
|
this.uploadedList[index] = { |
||||||
|
etag: response.data.etag, |
||||||
|
md5: response.data.md5, |
||||||
|
size: chunk.size |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLocalCache() |
||||||
|
} |
||||||
|
|
||||||
|
private async mkFileReq() { |
||||||
|
const data: UploadChunkBody = { |
||||||
|
parts: this.uploadedList.map((value, index) => ({ |
||||||
|
etag: value.etag, |
||||||
|
// 接口要求 index 需要从 1 开始,所以需要整体 + 1
|
||||||
|
partNumber: index + 1 |
||||||
|
})), |
||||||
|
fname: this.putExtra.fname, |
||||||
|
...this.putExtra.mimeType && {mimeType: this.putExtra.mimeType}, |
||||||
|
...this.putExtra.customVars && {customVars: this.putExtra.customVars}, |
||||||
|
...this.putExtra.metadata && {metadata: this.putExtra.metadata} |
||||||
|
} |
||||||
|
|
||||||
|
this.logger.info('parts upload completed, make file.', data) |
||||||
|
const result = await uploadComplete( |
||||||
|
this.token, |
||||||
|
this.key, |
||||||
|
this.getUploadInfo(), |
||||||
|
{ |
||||||
|
onCreate: xhr => this.addXhr(xhr), |
||||||
|
body: JSON.stringify(data) |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
this.logger.info('finish Resume Progress.') |
||||||
|
this.updateMkFileProgress(1) |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
private async initBeforeUploadChunks() { |
||||||
|
this.uploadedList = [] |
||||||
|
this.usedCacheList = [] |
||||||
|
const cachedInfo = utils.getLocalFileInfo(this.getLocalKey(), this.logger) |
||||||
|
|
||||||
|
// 分片必须和当时使用的 uploadId 配套,所以断点续传需要把本地存储的 uploadId 拿出来
|
||||||
|
// 假如没有 cachedInfo 本地信息并重新获取 uploadId
|
||||||
|
if (!cachedInfo) { |
||||||
|
this.logger.info('init upload parts from api.') |
||||||
|
const res = await initUploadParts( |
||||||
|
this.token, |
||||||
|
this.bucketName, |
||||||
|
this.key, |
||||||
|
this.uploadHost!.getUrl() |
||||||
|
) |
||||||
|
this.logger.info(`initd upload parts of id: ${res.data.uploadId}.`) |
||||||
|
this.uploadId = res.data.uploadId |
||||||
|
this.cachedUploadedList = [] |
||||||
|
} else { |
||||||
|
const infoMessage = [ |
||||||
|
'resume upload parts from local cache,', |
||||||
|
`total ${cachedInfo.data.length} part,`, |
||||||
|
`id is ${cachedInfo.id}.` |
||||||
|
] |
||||||
|
|
||||||
|
this.logger.info(infoMessage.join(' ')) |
||||||
|
this.cachedUploadedList = cachedInfo.data |
||||||
|
this.uploadId = cachedInfo.id |
||||||
|
} |
||||||
|
|
||||||
|
this.chunks = utils.getChunks(this.file, this.config.chunkSize) |
||||||
|
this.loaded = { |
||||||
|
mkFileProgress: 0, |
||||||
|
chunks: this.chunks.map(_ => 0) |
||||||
|
} |
||||||
|
this.notifyResumeProgress() |
||||||
|
} |
||||||
|
|
||||||
|
private getUploadInfo(): UploadInfo { |
||||||
|
return { |
||||||
|
id: this.uploadId!, |
||||||
|
url: this.uploadHost!.getUrl() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private getLocalKey() { |
||||||
|
return utils.createLocalKey(this.file.name, this.key, this.file.size) |
||||||
|
} |
||||||
|
|
||||||
|
private updateLocalCache() { |
||||||
|
utils.setLocalFileInfo(this.getLocalKey(), { |
||||||
|
id: this.uploadId!, |
||||||
|
data: this.uploadedList |
||||||
|
}, this.logger) |
||||||
|
} |
||||||
|
|
||||||
|
private updateChunkProgress(loaded: number, index: number) { |
||||||
|
this.loaded!.chunks[index] = loaded |
||||||
|
this.notifyResumeProgress() |
||||||
|
} |
||||||
|
|
||||||
|
private updateMkFileProgress(progress: 0 | 1) { |
||||||
|
this.loaded!.mkFileProgress = progress |
||||||
|
this.notifyResumeProgress() |
||||||
|
} |
||||||
|
|
||||||
|
private notifyResumeProgress() { |
||||||
|
this.progress = { |
||||||
|
total: this.getProgressInfoItem( |
||||||
|
utils.sum(this.loaded!.chunks) + this.loaded!.mkFileProgress, |
||||||
|
// FIXME: 不准确的 fileSize
|
||||||
|
this.file.size + 1 // 防止在 complete 未调用的时候进度显示 100%
|
||||||
|
), |
||||||
|
chunks: this.chunks.map((chunk, index) => { |
||||||
|
const fromCache = this.usedCacheList[index] |
||||||
|
return this.getProgressInfoItem(this.loaded!.chunks[index], chunk.size, fromCache) |
||||||
|
}), |
||||||
|
uploadInfo: { |
||||||
|
id: this.uploadId!, |
||||||
|
url: this.uploadHost!.getUrl() |
||||||
|
} |
||||||
|
} |
||||||
|
this.onData(this.progress!) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,274 @@ |
|||||||
|
/* eslint-disable */ |
||||||
|
|
||||||
|
// https://github.com/locutusjs/locutus/blob/master/src/php/xml/utf8_encode.js
|
||||||
|
function utf8Encode(argString: string) { |
||||||
|
// http://kevin.vanzonneveld.net
|
||||||
|
// + original by: Webtoolkit.info (http://www.webtoolkit.info/)
|
||||||
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// + improved by: sowberry
|
||||||
|
// + tweaked by: Jack
|
||||||
|
// + bugfixed by: Onno Marsman
|
||||||
|
// + improved by: Yves Sucaet
|
||||||
|
// + bugfixed by: Onno Marsman
|
||||||
|
// + bugfixed by: Ulrich
|
||||||
|
// + bugfixed by: Rafal Kukawski
|
||||||
|
// + improved by: kirilloid
|
||||||
|
// + bugfixed by: kirilloid
|
||||||
|
// * example 1: this.utf8Encode('Kevin van Zonneveld')
|
||||||
|
// * returns 1: 'Kevin van Zonneveld'
|
||||||
|
|
||||||
|
if (argString === null || typeof argString === 'undefined') { |
||||||
|
return '' |
||||||
|
} |
||||||
|
|
||||||
|
let string = argString + '' // .replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
let utftext = '', |
||||||
|
start, |
||||||
|
end, |
||||||
|
stringl = 0 |
||||||
|
|
||||||
|
start = end = 0 |
||||||
|
stringl = string.length |
||||||
|
for (let n = 0; n < stringl; n++) { |
||||||
|
let c1 = string.charCodeAt(n) |
||||||
|
let enc = null |
||||||
|
|
||||||
|
if (c1 < 128) { |
||||||
|
end++ |
||||||
|
} else if (c1 > 127 && c1 < 2048) { |
||||||
|
enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128) |
||||||
|
} else if ((c1 & 0xf800 ^ 0xd800) > 0) { |
||||||
|
enc = String.fromCharCode( |
||||||
|
(c1 >> 12) | 224, |
||||||
|
((c1 >> 6) & 63) | 128, |
||||||
|
(c1 & 63) | 128 |
||||||
|
) |
||||||
|
} else { |
||||||
|
// surrogate pairs
|
||||||
|
if ((c1 & 0xfc00 ^ 0xd800) > 0) { |
||||||
|
throw new RangeError('Unmatched trail surrogate at ' + n) |
||||||
|
} |
||||||
|
let c2 = string.charCodeAt(++n) |
||||||
|
if ((c2 & 0xfc00 ^ 0xdc00) > 0) { |
||||||
|
throw new RangeError('Unmatched lead surrogate at ' + (n - 1)) |
||||||
|
} |
||||||
|
c1 = ((c1 & 0x3ff) << 10) + (c2 & 0x3ff) + 0x10000 |
||||||
|
enc = String.fromCharCode( |
||||||
|
(c1 >> 18) | 240, |
||||||
|
((c1 >> 12) & 63) | 128, |
||||||
|
((c1 >> 6) & 63) | 128, |
||||||
|
(c1 & 63) | 128 |
||||||
|
) |
||||||
|
} |
||||||
|
if (enc !== null) { |
||||||
|
if (end > start) { |
||||||
|
utftext += string.slice(start, end) |
||||||
|
} |
||||||
|
utftext += enc |
||||||
|
start = end = n + 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (end > start) { |
||||||
|
utftext += string.slice(start, stringl) |
||||||
|
} |
||||||
|
|
||||||
|
return utftext |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/locutusjs/locutus/blob/master/src/php/xml/utf8_decode.js
|
||||||
|
function utf8Decode(strData: string) { |
||||||
|
// eslint-disable-line camelcase
|
||||||
|
// discuss at: https://locutus.io/php/utf8_decode/
|
||||||
|
// original by: Webtoolkit.info (https://www.webtoolkit.info/)
|
||||||
|
// input by: Aman Gupta
|
||||||
|
// input by: Brett Zamir (https://brett-zamir.me)
|
||||||
|
// improved by: Kevin van Zonneveld (https://kvz.io)
|
||||||
|
// improved by: Norman "zEh" Fuchs
|
||||||
|
// bugfixed by: hitwork
|
||||||
|
// bugfixed by: Onno Marsman (https://twitter.com/onnomarsman)
|
||||||
|
// bugfixed by: Kevin van Zonneveld (https://kvz.io)
|
||||||
|
// bugfixed by: kirilloid
|
||||||
|
// bugfixed by: w35l3y (https://www.wesley.eti.br)
|
||||||
|
// example 1: utf8_decode('Kevin van Zonneveld')
|
||||||
|
// returns 1: 'Kevin van Zonneveld'
|
||||||
|
|
||||||
|
const tmpArr = [] |
||||||
|
let i = 0 |
||||||
|
let c1 = 0 |
||||||
|
let seqlen = 0 |
||||||
|
|
||||||
|
strData += '' |
||||||
|
|
||||||
|
while (i < strData.length) { |
||||||
|
c1 = strData.charCodeAt(i) & 0xFF |
||||||
|
seqlen = 0 |
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/UTF-8#Codepage_layout
|
||||||
|
if (c1 <= 0xBF) { |
||||||
|
c1 = (c1 & 0x7F) |
||||||
|
seqlen = 1 |
||||||
|
} else if (c1 <= 0xDF) { |
||||||
|
c1 = (c1 & 0x1F) |
||||||
|
seqlen = 2 |
||||||
|
} else if (c1 <= 0xEF) { |
||||||
|
c1 = (c1 & 0x0F) |
||||||
|
seqlen = 3 |
||||||
|
} else { |
||||||
|
c1 = (c1 & 0x07) |
||||||
|
seqlen = 4 |
||||||
|
} |
||||||
|
|
||||||
|
for (let ai = 1; ai < seqlen; ++ai) { |
||||||
|
c1 = ((c1 << 0x06) | (strData.charCodeAt(ai + i) & 0x3F)) |
||||||
|
} |
||||||
|
|
||||||
|
if (seqlen === 4) { |
||||||
|
c1 -= 0x10000 |
||||||
|
tmpArr.push(String.fromCharCode(0xD800 | ((c1 >> 10) & 0x3FF))) |
||||||
|
tmpArr.push(String.fromCharCode(0xDC00 | (c1 & 0x3FF))) |
||||||
|
} else { |
||||||
|
tmpArr.push(String.fromCharCode(c1)) |
||||||
|
} |
||||||
|
|
||||||
|
i += seqlen |
||||||
|
} |
||||||
|
|
||||||
|
return tmpArr.join('') |
||||||
|
} |
||||||
|
|
||||||
|
function base64Encode(data: any) { |
||||||
|
// http://kevin.vanzonneveld.net
|
||||||
|
// + original by: Tyler Akins (http://rumkin.com)
|
||||||
|
// + improved by: Bayron Guevara
|
||||||
|
// + improved by: Thunder.m
|
||||||
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// + bugfixed by: Pellentesque Malesuada
|
||||||
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// - depends on: this.utf8Encode
|
||||||
|
// * example 1: this.base64Encode('Kevin van Zonneveld')
|
||||||
|
// * returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
|
||||||
|
// mozilla has this native
|
||||||
|
// - but breaks in 2.0.0.12!
|
||||||
|
// if (typeof this.window['atob'] == 'function') {
|
||||||
|
// return atob(data)
|
||||||
|
// }
|
||||||
|
let b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' |
||||||
|
let o1, |
||||||
|
o2, |
||||||
|
o3, |
||||||
|
h1, |
||||||
|
h2, |
||||||
|
h3, |
||||||
|
h4, |
||||||
|
bits, |
||||||
|
i = 0, |
||||||
|
ac = 0, |
||||||
|
enc = '', |
||||||
|
tmp_arr = [] |
||||||
|
|
||||||
|
if (!data) { |
||||||
|
return data |
||||||
|
} |
||||||
|
|
||||||
|
data = utf8Encode(data + '') |
||||||
|
|
||||||
|
do { |
||||||
|
// pack three octets into four hexets
|
||||||
|
o1 = data.charCodeAt(i++) |
||||||
|
o2 = data.charCodeAt(i++) |
||||||
|
o3 = data.charCodeAt(i++) |
||||||
|
|
||||||
|
bits = (o1 << 16) | (o2 << 8) | o3 |
||||||
|
|
||||||
|
h1 = (bits >> 18) & 0x3f |
||||||
|
h2 = (bits >> 12) & 0x3f |
||||||
|
h3 = (bits >> 6) & 0x3f |
||||||
|
h4 = bits & 0x3f |
||||||
|
|
||||||
|
// use hexets to index into b64, and append result to encoded string
|
||||||
|
tmp_arr[ac++] = |
||||||
|
b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4) |
||||||
|
} while (i < data.length) |
||||||
|
|
||||||
|
enc = tmp_arr.join('') |
||||||
|
|
||||||
|
switch (data.length % 3) { |
||||||
|
case 1: |
||||||
|
enc = enc.slice(0, -2) + '==' |
||||||
|
break |
||||||
|
case 2: |
||||||
|
enc = enc.slice(0, -1) + '=' |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
return enc |
||||||
|
} |
||||||
|
|
||||||
|
function base64Decode(data: string) { |
||||||
|
// http://kevin.vanzonneveld.net
|
||||||
|
// + original by: Tyler Akins (http://rumkin.com)
|
||||||
|
// + improved by: Thunder.m
|
||||||
|
// + input by: Aman Gupta
|
||||||
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// + bugfixed by: Onno Marsman
|
||||||
|
// + bugfixed by: Pellentesque Malesuada
|
||||||
|
// + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// + input by: Brett Zamir (http://brett-zamir.me)
|
||||||
|
// + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
||||||
|
// * example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==')
|
||||||
|
// * returns 1: 'Kevin van Zonneveld'
|
||||||
|
// mozilla has this native
|
||||||
|
// - but breaks in 2.0.0.12!
|
||||||
|
// if (typeof this.window['atob'] == 'function') {
|
||||||
|
// return atob(data)
|
||||||
|
// }
|
||||||
|
let b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' |
||||||
|
let o1, o2, o3, h1, h2, h3, h4, bits, i = 0, |
||||||
|
ac = 0, |
||||||
|
dec = '', |
||||||
|
tmp_arr = [] |
||||||
|
|
||||||
|
if (!data) { |
||||||
|
return data |
||||||
|
} |
||||||
|
|
||||||
|
data += '' |
||||||
|
|
||||||
|
do { // unpack four hexets into three octets using index points in b64
|
||||||
|
h1 = b64.indexOf(data.charAt(i++)) |
||||||
|
h2 = b64.indexOf(data.charAt(i++)) |
||||||
|
h3 = b64.indexOf(data.charAt(i++)) |
||||||
|
h4 = b64.indexOf(data.charAt(i++)) |
||||||
|
|
||||||
|
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4 |
||||||
|
|
||||||
|
o1 = bits >> 16 & 0xff |
||||||
|
o2 = bits >> 8 & 0xff |
||||||
|
o3 = bits & 0xff |
||||||
|
|
||||||
|
if (h3 === 64) { |
||||||
|
tmp_arr[ac++] = String.fromCharCode(o1) |
||||||
|
} else if (h4 === 64) { |
||||||
|
tmp_arr[ac++] = String.fromCharCode(o1, o2) |
||||||
|
} else { |
||||||
|
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3) |
||||||
|
} |
||||||
|
} while (i < data.length) |
||||||
|
|
||||||
|
dec = tmp_arr.join('') |
||||||
|
|
||||||
|
return utf8Decode(dec) |
||||||
|
} |
||||||
|
|
||||||
|
export function urlSafeBase64Encode(v: any) { |
||||||
|
v = base64Encode(v) |
||||||
|
|
||||||
|
// 参考 https://tools.ietf.org/html/rfc4648#section-5
|
||||||
|
return v.replace(/\//g, '_').replace(/\+/g, '-') |
||||||
|
} |
||||||
|
|
||||||
|
export function urlSafeBase64Decode(v: any) { |
||||||
|
v = v.replace(/_/g, '/').replace(/-/g, '+') |
||||||
|
return base64Decode(v) |
||||||
|
} |
@ -0,0 +1,218 @@ |
|||||||
|
// import {QiniuError, QiniuErrorName} from '../errors'
|
||||||
|
//
|
||||||
|
// import {createObjectURL} from './helper'
|
||||||
|
//
|
||||||
|
// export interface CompressOptions {
|
||||||
|
// quality?: number
|
||||||
|
// noCompressIfLarger?: boolean
|
||||||
|
// maxWidth?: number
|
||||||
|
// maxHeight?: number
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// export interface Dimension {
|
||||||
|
// width?: number
|
||||||
|
// height?: number
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// export interface CompressResult {
|
||||||
|
// dist: Blob | File
|
||||||
|
// width: number
|
||||||
|
// height: number
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const mimeTypes = {
|
||||||
|
// PNG: 'image/png',
|
||||||
|
// JPEG: 'image/jpeg',
|
||||||
|
// WEBP: 'image/webp',
|
||||||
|
// BMP: 'image/bmp'
|
||||||
|
// } as const
|
||||||
|
//
|
||||||
|
// const maxSteps = 4
|
||||||
|
// const scaleFactor = Math.log(2)
|
||||||
|
// // @ts-ignore
|
||||||
|
// const supportMimeTypes = Object.keys(mimeTypes).map(type => mimeTypes[type])
|
||||||
|
// const defaultType = mimeTypes.JPEG
|
||||||
|
//
|
||||||
|
// type MimeKey = keyof typeof mimeTypes
|
||||||
|
//
|
||||||
|
// function isSupportedType(type: string): type is typeof mimeTypes[MimeKey] {
|
||||||
|
// return supportMimeTypes.includes(type)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// class Compress {
|
||||||
|
// private outputType: string
|
||||||
|
// private config: CompressOptions
|
||||||
|
//
|
||||||
|
// constructor(private file: File, config: CompressOptions) {
|
||||||
|
// this.outputType = this.file.type
|
||||||
|
// this.config = {
|
||||||
|
// quality: 0.92,
|
||||||
|
// noCompressIfLarger: false,
|
||||||
|
// ...config
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async process(): Promise<CompressResult> {
|
||||||
|
// this.outputType = this.file.type
|
||||||
|
// const srcDimension: Dimension = {}
|
||||||
|
// if (!isSupportedType(this.file.type)) {
|
||||||
|
// throw new QiniuError(
|
||||||
|
// QiniuErrorName.UnsupportedFileType,
|
||||||
|
// `unsupported file type: ${this.file.type}`
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const originImage = await this.getOriginImage()
|
||||||
|
// const canvas = await this.getCanvas(originImage)
|
||||||
|
// let scale = 1
|
||||||
|
// if (this.config.maxWidth) {
|
||||||
|
// scale = Math.min(1, this.config.maxWidth / canvas.width)
|
||||||
|
// }
|
||||||
|
// if (this.config.maxHeight) {
|
||||||
|
// scale = Math.min(1, scale, this.config.maxHeight / canvas.height)
|
||||||
|
// }
|
||||||
|
// srcDimension.width = canvas.width
|
||||||
|
// srcDimension.height = canvas.height
|
||||||
|
//
|
||||||
|
// const scaleCanvas = await this.doScale(canvas, scale)
|
||||||
|
// const distBlob = this.toBlob(scaleCanvas)
|
||||||
|
// if (distBlob.size > this.file.size && this.config.noCompressIfLarger) {
|
||||||
|
// return {
|
||||||
|
// dist: this.file,
|
||||||
|
// width: srcDimension.width,
|
||||||
|
// height: srcDimension.height
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// dist: distBlob,
|
||||||
|
// width: scaleCanvas.width,
|
||||||
|
// height: scaleCanvas.height
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// clear(ctx: CanvasRenderingContext2D, width: number, height: number) {
|
||||||
|
// // jpeg 没有 alpha 通道,透明区间会被填充成黑色,这里把透明区间填充为白色
|
||||||
|
// if (this.outputType === defaultType) {
|
||||||
|
// ctx.fillStyle = '#fff'
|
||||||
|
// ctx.fillRect(0, 0, width, height)
|
||||||
|
// } else {
|
||||||
|
// ctx.clearRect(0, 0, width, height)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** 通过 file 初始化 image 对象 */
|
||||||
|
// getOriginImage(): Promise<HTMLImageElement> {
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// const url = createObjectURL(this.file)
|
||||||
|
// const img = new Image()
|
||||||
|
// img.onload = () => {
|
||||||
|
// resolve(img)
|
||||||
|
// }
|
||||||
|
// img.onerror = () => {
|
||||||
|
// reject('image load error')
|
||||||
|
// }
|
||||||
|
// img.src = url
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// getCanvas(img: HTMLImageElement): Promise<HTMLCanvasElement> {
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// const canvas = document.createElement('canvas')
|
||||||
|
// const context = canvas.getContext('2d')
|
||||||
|
//
|
||||||
|
// if (!context) {
|
||||||
|
// reject(new QiniuError(
|
||||||
|
// QiniuErrorName.GetCanvasContextFailed,
|
||||||
|
// 'context is null'
|
||||||
|
// ))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const {width, height} = img
|
||||||
|
// canvas.height = height
|
||||||
|
// canvas.width = width
|
||||||
|
//
|
||||||
|
// this.clear(context, width, height)
|
||||||
|
// context.drawImage(img, 0, 0)
|
||||||
|
// resolve(canvas)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// async doScale(source: HTMLCanvasElement, scale: number) {
|
||||||
|
// if (scale === 1) {
|
||||||
|
// return source
|
||||||
|
// }
|
||||||
|
// // 不要一次性画图,通过设定的 step 次数,渐进式的画图,这样可以增加图片的清晰度,防止一次性画图导致的像素丢失严重
|
||||||
|
// const sctx = source.getContext('2d')
|
||||||
|
// const steps = Math.min(maxSteps, Math.ceil((1 / scale) / scaleFactor))
|
||||||
|
//
|
||||||
|
// const factor = scale ** (1 / steps)
|
||||||
|
//
|
||||||
|
// const mirror = document.createElement('canvas')
|
||||||
|
// const mctx = mirror.getContext('2d')
|
||||||
|
//
|
||||||
|
// let {width, height} = source
|
||||||
|
// const originWidth = width
|
||||||
|
// const originHeight = height
|
||||||
|
// mirror.width = width
|
||||||
|
// mirror.height = height
|
||||||
|
// if (!mctx || !sctx) {
|
||||||
|
// throw new QiniuError(
|
||||||
|
// QiniuErrorName.GetCanvasContextFailed,
|
||||||
|
// "mctx or sctx can't be null"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let src!: CanvasImageSource
|
||||||
|
// let context!: CanvasRenderingContext2D
|
||||||
|
// for (let i = 0; i < steps; i++) {
|
||||||
|
//
|
||||||
|
// let dw = width * factor | 0 // eslint-disable-line no-bitwise
|
||||||
|
// let dh = height * factor | 0 // eslint-disable-line no-bitwise
|
||||||
|
// // 到最后一步的时候 dw, dh 用目标缩放尺寸,否则会出现最后尺寸偏小的情况
|
||||||
|
// if (i === steps - 1) {
|
||||||
|
// dw = originWidth * scale
|
||||||
|
// dh = originHeight * scale
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (i % 2 === 0) {
|
||||||
|
// src = source
|
||||||
|
// context = mctx
|
||||||
|
// } else {
|
||||||
|
// src = mirror
|
||||||
|
// context = sctx
|
||||||
|
// }
|
||||||
|
// // 每次画前都清空,避免图像重叠
|
||||||
|
// this.clear(context, width, height)
|
||||||
|
// context.drawImage(src, 0, 0, width, height, 0, 0, dw, dh)
|
||||||
|
// width = dw
|
||||||
|
// height = dh
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const canvas = src === source ? mirror : source
|
||||||
|
// // save data
|
||||||
|
// const data = context.getImageData(0, 0, width, height)
|
||||||
|
//
|
||||||
|
// // resize
|
||||||
|
// canvas.width = width
|
||||||
|
// canvas.height = height
|
||||||
|
//
|
||||||
|
// // store image data
|
||||||
|
// context.putImageData(data, 0, 0)
|
||||||
|
//
|
||||||
|
// return canvas
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /** 这里把 base64 字符串转为 blob 对象 */
|
||||||
|
// toBlob(result: HTMLCanvasElement) {
|
||||||
|
// const dataURL = result.toDataURL(this.outputType, this.config.quality)
|
||||||
|
// const buffer = atob(dataURL.split(',')[1]).split('').map(char => char.charCodeAt(0))
|
||||||
|
// const blob = new Blob([new Uint8Array(buffer)], {type: this.outputType})
|
||||||
|
// return blob
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const compressImage = (file: File, options: CompressOptions) => new Compress(file, options).process()
|
||||||
|
//
|
||||||
|
// export default compressImage
|
@ -0,0 +1,61 @@ |
|||||||
|
import Logger from '../logger' |
||||||
|
import { regionUphostMap } from '../config' |
||||||
|
import { Config, DEFAULT_CHUNK_SIZE, InternalConfig } from '../upload' |
||||||
|
|
||||||
|
export function normalizeUploadConfig(config?: Partial<Config>, logger?: Logger): InternalConfig { |
||||||
|
const { upprotocol, uphost, ...otherConfig } = { ...config } |
||||||
|
|
||||||
|
const normalizeConfig: InternalConfig = { |
||||||
|
uphost: [], |
||||||
|
retryCount: 3, |
||||||
|
|
||||||
|
checkByMD5: false, |
||||||
|
forceDirect: false, |
||||||
|
useCdnDomain: true, |
||||||
|
checkByServer: false, |
||||||
|
concurrentRequestLimit: 3, |
||||||
|
chunkSize: DEFAULT_CHUNK_SIZE, |
||||||
|
|
||||||
|
upprotocol: 'https', |
||||||
|
|
||||||
|
debugLogLevel: 'OFF', |
||||||
|
disableStatisticsReport: false, |
||||||
|
|
||||||
|
...otherConfig |
||||||
|
} |
||||||
|
|
||||||
|
// 兼容原来的 http: https: 的写法
|
||||||
|
if (upprotocol) { |
||||||
|
normalizeConfig.upprotocol = upprotocol |
||||||
|
.replace(/:$/, '') as InternalConfig['upprotocol'] |
||||||
|
} |
||||||
|
|
||||||
|
const hostList: string[] = [] |
||||||
|
|
||||||
|
if (logger && config?.uphost != null && config?.region != null) { |
||||||
|
logger.warn('do not use both the uphost and region config.') |
||||||
|
} |
||||||
|
|
||||||
|
// 如果同时指定了 uphost 参数,添加到可用 host 列表
|
||||||
|
if (uphost) { |
||||||
|
if (Array.isArray(uphost)) { |
||||||
|
hostList.push(...uphost) |
||||||
|
} else { |
||||||
|
hostList.push(uphost) |
||||||
|
} |
||||||
|
|
||||||
|
// 否则如果用户传了 region,添加指定 region 的 host 到可用 host 列表
|
||||||
|
} else if (normalizeConfig?.region) { |
||||||
|
const hostMap = regionUphostMap[normalizeConfig?.region] |
||||||
|
if (normalizeConfig.useCdnDomain) { |
||||||
|
hostList.push(...hostMap.cdnUphost) |
||||||
|
} else { |
||||||
|
hostList.push(...hostMap.srcUphost) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
...normalizeConfig, |
||||||
|
uphost: hostList.filter(Boolean) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
/* eslint-disable no-bitwise */ |
||||||
|
|
||||||
|
import { MB } from './helper' |
||||||
|
|
||||||
|
/** |
||||||
|
* 以下 class 实现参考 |
||||||
|
* https://github.com/Stuk/jszip/blob/d4702a70834bd953d4c2d0bc155fad795076631a/lib/crc32.js
|
||||||
|
* 该实现主要针对大文件优化、对计算的值进行了 `>>> 0` 运算(为与服务端保持一致) |
||||||
|
*/ |
||||||
|
export class CRC32 { |
||||||
|
private crc = -1 |
||||||
|
private table = this.makeTable() |
||||||
|
|
||||||
|
private makeTable() { |
||||||
|
const table = new Array<number>() |
||||||
|
for (let i = 0; i < 256; i++) { |
||||||
|
let t = i |
||||||
|
for (let j = 0; j < 8; j++) { |
||||||
|
if (t & 1) { |
||||||
|
// IEEE 标准
|
||||||
|
t = (t >>> 1) ^ 0xEDB88320 |
||||||
|
} else { |
||||||
|
t >>>= 1 |
||||||
|
} |
||||||
|
} |
||||||
|
table[i] = t |
||||||
|
} |
||||||
|
|
||||||
|
return table |
||||||
|
} |
||||||
|
|
||||||
|
private append(data: Uint8Array) { |
||||||
|
let crc = this.crc |
||||||
|
for (let offset = 0; offset < data.byteLength; offset++) { |
||||||
|
crc = (crc >>> 8) ^ this.table[(crc ^ data[offset]) & 0xFF] |
||||||
|
} |
||||||
|
this.crc = crc |
||||||
|
} |
||||||
|
|
||||||
|
private compute() { |
||||||
|
return (this.crc ^ -1) >>> 0 |
||||||
|
} |
||||||
|
|
||||||
|
private async readAsUint8Array(file: File | Blob): Promise<Uint8Array> { |
||||||
|
if (typeof file.arrayBuffer === 'function') { |
||||||
|
return new Uint8Array(await file.arrayBuffer()) |
||||||
|
} |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const reader = new FileReader() |
||||||
|
reader.onload = () => { |
||||||
|
if (reader.result == null) { |
||||||
|
reject() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof reader.result === 'string') { |
||||||
|
reject() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
resolve(new Uint8Array(reader.result)) |
||||||
|
} |
||||||
|
reader.readAsArrayBuffer(file) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async file(file: File): Promise<number> { |
||||||
|
if (file.size <= MB) { |
||||||
|
this.append(await this.readAsUint8Array(file)) |
||||||
|
return this.compute() |
||||||
|
} |
||||||
|
|
||||||
|
const count = Math.ceil(file.size / MB) |
||||||
|
for (let index = 0; index < count; index++) { |
||||||
|
const start = index * MB |
||||||
|
const end = index === (count - 1) ? file.size : start + MB |
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const chuck = await this.readAsUint8Array(file.slice(start, end)) |
||||||
|
this.append(new Uint8Array(chuck)) |
||||||
|
} |
||||||
|
|
||||||
|
return this.compute() |
||||||
|
} |
||||||
|
|
||||||
|
static file(file: File): Promise<number> { |
||||||
|
const crc = new CRC32() |
||||||
|
return crc.file(file) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,346 @@ |
|||||||
|
import {SparkMD5} from './spark-md5' |
||||||
|
import { |
||||||
|
QiniuError, |
||||||
|
QiniuErrorName, |
||||||
|
QiniuNetworkError, |
||||||
|
QiniuRequestError |
||||||
|
} from '../errors' |
||||||
|
import {LocalInfo, Progress} from '../upload' |
||||||
|
import Logger from '../logger' |
||||||
|
|
||||||
|
import {urlSafeBase64Decode} from './base64' |
||||||
|
|
||||||
|
export const MB = 1024 ** 2 |
||||||
|
|
||||||
|
// 文件分块
|
||||||
|
export function getChunks(file: File, blockSize: number): Blob[] { |
||||||
|
|
||||||
|
let chunkByteSize = blockSize * MB // 转换为字节
|
||||||
|
// 如果 chunkByteSize 比文件大,则直接取文件的大小
|
||||||
|
if (chunkByteSize > file.size) { |
||||||
|
chunkByteSize = file.size |
||||||
|
} else { |
||||||
|
// 因为最多 10000 chunk,所以如果 chunkSize 不符合则把每片 chunk 大小扩大两倍
|
||||||
|
while (file.size > chunkByteSize * 10000) { |
||||||
|
chunkByteSize *= 2 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const chunks: Blob[] = [] |
||||||
|
const count = Math.ceil(file.size / chunkByteSize) |
||||||
|
for (let i = 0; i < count; i++) { |
||||||
|
const chunk = file.slice( |
||||||
|
chunkByteSize * i, |
||||||
|
i === count - 1 ? file.size : chunkByteSize * (i + 1) |
||||||
|
) |
||||||
|
chunks.push(chunk) |
||||||
|
} |
||||||
|
return chunks |
||||||
|
} |
||||||
|
|
||||||
|
export function isMetaDataValid(params: { [key: string]: string }) { |
||||||
|
return Object.keys(params).every(key => key.indexOf('x-qn-meta-') === 0) |
||||||
|
} |
||||||
|
|
||||||
|
export function isCustomVarsValid(params: { [key: string]: string }) { |
||||||
|
return Object.keys(params).every(key => key.indexOf('x:') === 0) |
||||||
|
} |
||||||
|
|
||||||
|
export function sum(list: number[]) { |
||||||
|
return list.reduce((data, loaded) => data + loaded, 0) |
||||||
|
} |
||||||
|
|
||||||
|
export function setLocalFileInfo(localKey: string, info: LocalInfo, logger: Logger) { |
||||||
|
try { |
||||||
|
localStorage.setItem(localKey, JSON.stringify(info)) |
||||||
|
} catch (err) { |
||||||
|
logger.warn(new QiniuError( |
||||||
|
QiniuErrorName.WriteCacheFailed, |
||||||
|
`setLocalFileInfo failed: ${localKey}` |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function createLocalKey(name: string, key: string | null | undefined, size: number): string { |
||||||
|
const localKey = key == null ? '_' : `_key_${key}_` |
||||||
|
return `qiniu_js_sdk_upload_file_name_${name}${localKey}size_${size}` |
||||||
|
} |
||||||
|
|
||||||
|
export function removeLocalFileInfo(localKey: string, logger: Logger) { |
||||||
|
try { |
||||||
|
localStorage.removeItem(localKey) |
||||||
|
} catch (err) { |
||||||
|
logger.warn(new QiniuError( |
||||||
|
QiniuErrorName.RemoveCacheFailed, |
||||||
|
`removeLocalFileInfo failed. key: ${localKey}` |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function getLocalFileInfo(localKey: string, logger: Logger): LocalInfo | null { |
||||||
|
let localInfoString: string | null = null |
||||||
|
try { |
||||||
|
localInfoString = localStorage.getItem(localKey) |
||||||
|
} catch { |
||||||
|
logger.warn(new QiniuError( |
||||||
|
QiniuErrorName.ReadCacheFailed, |
||||||
|
`getLocalFileInfo failed. key: ${localKey}` |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
if (localInfoString == null) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
let localInfo: LocalInfo | null = null |
||||||
|
try { |
||||||
|
localInfo = JSON.parse(localInfoString) |
||||||
|
} catch { |
||||||
|
// 本地信息已被破坏,直接删除
|
||||||
|
removeLocalFileInfo(localKey, logger) |
||||||
|
logger.warn(new QiniuError( |
||||||
|
QiniuErrorName.InvalidCacheData, |
||||||
|
`getLocalFileInfo failed to parse. key: ${localKey}` |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
return localInfo |
||||||
|
} |
||||||
|
|
||||||
|
export function getAuthHeaders(token: string) { |
||||||
|
const auth = 'UpToken ' + token |
||||||
|
return {Authorization: auth} |
||||||
|
} |
||||||
|
|
||||||
|
export function getHeadersForChunkUpload(token: string): Record<string, string> { |
||||||
|
const header = getAuthHeaders(token) |
||||||
|
return { |
||||||
|
'content-type': 'application/octet-stream', |
||||||
|
...header |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function getHeadersForMkFile(token: string) { |
||||||
|
const header = getAuthHeaders(token) |
||||||
|
return { |
||||||
|
'content-type': 'application/json', |
||||||
|
...header |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function createXHR(): XMLHttpRequest { |
||||||
|
if (typeof XMLHttpRequest === "function") { |
||||||
|
return new XMLHttpRequest() |
||||||
|
} |
||||||
|
throw new QiniuError( |
||||||
|
QiniuErrorName.NotAvailableXMLHttpRequest, |
||||||
|
'the current environment does not support.' |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export async function computeMd5(data: Blob): Promise<string> { |
||||||
|
const buffer = await readAsArrayBuffer(data) |
||||||
|
const spark = new SparkMD5() |
||||||
|
spark.append(buffer) |
||||||
|
return spark.end() |
||||||
|
} |
||||||
|
|
||||||
|
export function readAsArrayBuffer(data: Blob): Promise<ArrayBuffer> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const reader = new FileReader() |
||||||
|
// evt 类型目前存在问题 https://github.com/Microsoft/TypeScript/issues/4163
|
||||||
|
reader.onload = (evt: ProgressEvent<FileReader>) => { |
||||||
|
if (evt.target) { |
||||||
|
const body = evt.target.result |
||||||
|
resolve(body as ArrayBuffer) |
||||||
|
} else { |
||||||
|
reject(new QiniuError( |
||||||
|
QiniuErrorName.InvalidProgressEventTarget, |
||||||
|
'progress event target is undefined' |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
reader.onerror = () => { |
||||||
|
reject(new QiniuError( |
||||||
|
QiniuErrorName.FileReaderReadFailed, |
||||||
|
'fileReader read failed' |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
reader.readAsArrayBuffer(data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export interface ResponseSuccess<T> { |
||||||
|
data: T |
||||||
|
reqId: string |
||||||
|
} |
||||||
|
|
||||||
|
export type XHRHandler = (xhr: XMLHttpRequest) => void |
||||||
|
|
||||||
|
export interface RequestOptions { |
||||||
|
method: string |
||||||
|
onProgress?: (data: Progress) => void |
||||||
|
onCreate?: XHRHandler |
||||||
|
body?: XMLHttpRequestBodyInit | null |
||||||
|
headers?: { [key: string]: string } |
||||||
|
} |
||||||
|
|
||||||
|
export type Response<T> = Promise<ResponseSuccess<T>> |
||||||
|
|
||||||
|
export function request<T>(url: string, options: RequestOptions): Response<T> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const xhr = createXHR() |
||||||
|
xhr.open(options.method, url) |
||||||
|
|
||||||
|
if (options.onCreate) { |
||||||
|
options.onCreate(xhr) |
||||||
|
} |
||||||
|
|
||||||
|
if (options.headers) { |
||||||
|
const headers = options.headers |
||||||
|
Object.keys(headers).forEach(k => { |
||||||
|
xhr.setRequestHeader(k, headers[k]) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (evt: ProgressEvent) => { |
||||||
|
if (evt.lengthComputable && options.onProgress) { |
||||||
|
options.onProgress({ |
||||||
|
loaded: evt.loaded, |
||||||
|
total: evt.total |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
xhr.onreadystatechange = () => { |
||||||
|
const responseText = xhr.responseText |
||||||
|
if (xhr.readyState !== 4) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const reqId = xhr.getResponseHeader('x-reqId') || '' |
||||||
|
|
||||||
|
if (xhr.status === 0) { |
||||||
|
// 发生 0 基本都是网络错误,常见的比如跨域、断网、host 解析失败、系统拦截等等
|
||||||
|
reject(new QiniuNetworkError('network error.', reqId)) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (xhr.status !== 200) { |
||||||
|
let message = `xhr request failed, code: ${xhr.status}` |
||||||
|
if (responseText) { |
||||||
|
message += ` response: ${responseText}` |
||||||
|
} |
||||||
|
|
||||||
|
let data |
||||||
|
try { |
||||||
|
data = JSON.parse(responseText) |
||||||
|
} catch { |
||||||
|
// 无需处理该错误、可能拿到非 json 格式的响应是预期的
|
||||||
|
} |
||||||
|
|
||||||
|
reject(new QiniuRequestError(xhr.status, reqId, message, data)) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
resolve({ |
||||||
|
data: JSON.parse(responseText), |
||||||
|
reqId |
||||||
|
}) |
||||||
|
} catch (err) { |
||||||
|
reject(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
xhr.send(options.body) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function getPortFromUrl(url: string | undefined) { |
||||||
|
if (url && url.match) { |
||||||
|
let groups = url.match(/(^https?)/) |
||||||
|
|
||||||
|
if (!groups) { |
||||||
|
return '' |
||||||
|
} |
||||||
|
|
||||||
|
const type = groups[1] |
||||||
|
groups = url.match(/^https?:\/\/([^:^/]*):(\d*)/) |
||||||
|
|
||||||
|
if (groups) { |
||||||
|
return groups[2] |
||||||
|
} |
||||||
|
|
||||||
|
if (type === 'http') { |
||||||
|
return '80' |
||||||
|
} |
||||||
|
|
||||||
|
return '443' |
||||||
|
} |
||||||
|
|
||||||
|
return '' |
||||||
|
} |
||||||
|
|
||||||
|
export function getDomainFromUrl(url: string | undefined): string { |
||||||
|
if (url && url.match) { |
||||||
|
const groups = url.match(/^https?:\/\/([^:^/]*)/) |
||||||
|
return groups ? groups[1] : '' |
||||||
|
} |
||||||
|
|
||||||
|
return '' |
||||||
|
} |
||||||
|
|
||||||
|
// 非标准的 PutPolicy
|
||||||
|
interface PutPolicy { |
||||||
|
assessKey: string |
||||||
|
bucketName: string |
||||||
|
scope: string |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param token |
||||||
|
* @throws {QiniuError} |
||||||
|
*/ |
||||||
|
export function getPutPolicy(token: string): PutPolicy { |
||||||
|
if (!token) throw new QiniuError(QiniuErrorName.InvalidToken, 'invalid token.') |
||||||
|
|
||||||
|
const segments = token.split(':') |
||||||
|
if (segments.length === 1) throw new QiniuError(QiniuErrorName.InvalidToken, 'invalid token segments.') |
||||||
|
|
||||||
|
// token 构造的差异参考:https://github.com/qbox/product/blob/master/kodo/auths/UpToken.md#admin-uptoken-authorization
|
||||||
|
const assessKey = segments.length > 3 ? segments[1] : segments[0] |
||||||
|
if (!assessKey) throw new QiniuError(QiniuErrorName.InvalidToken, 'missing assess key field.') |
||||||
|
|
||||||
|
let putPolicy: PutPolicy | null = null |
||||||
|
|
||||||
|
try { |
||||||
|
putPolicy = JSON.parse(urlSafeBase64Decode(segments[segments.length - 1])) |
||||||
|
} catch (error) { |
||||||
|
throw new QiniuError(QiniuErrorName.InvalidToken, 'token parse failed.') |
||||||
|
} |
||||||
|
|
||||||
|
if (putPolicy == null) { |
||||||
|
throw new QiniuError(QiniuErrorName.InvalidToken, 'putPolicy is null.') |
||||||
|
} |
||||||
|
|
||||||
|
if (putPolicy.scope == null) { |
||||||
|
throw new QiniuError(QiniuErrorName.InvalidToken, 'scope field is null.') |
||||||
|
} |
||||||
|
|
||||||
|
const bucketName = putPolicy.scope.split(':')[0] |
||||||
|
if (!bucketName) { |
||||||
|
throw new QiniuError(QiniuErrorName.InvalidToken, 'resolve bucketName failed.') |
||||||
|
} |
||||||
|
|
||||||
|
return {assessKey, bucketName, scope: putPolicy.scope} |
||||||
|
} |
||||||
|
|
||||||
|
export function createObjectURL(file: File) { |
||||||
|
// @ts-ignore
|
||||||
|
const URL = window.URL || window.webkitURL || window.mozURL |
||||||
|
// FIXME: 需要 revokeObjectURL
|
||||||
|
return URL.createObjectURL(file) |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
export * from './pool' |
||||||
|
export * from './observable' |
||||||
|
|
||||||
|
export * from './base64' |
||||||
|
export * from './helper' |
||||||
|
export * from './config' |
||||||
|
|
||||||
|
// export {default as compressImage, type CompressResult} from './compress'
|
@ -0,0 +1,151 @@ |
|||||||
|
/** 消费者接口 */ |
||||||
|
export interface IObserver<T, E, C> { |
||||||
|
/** 用来接收 Observable 中的 next 类型通知 */ |
||||||
|
next: (value: T) => void |
||||||
|
/** 用来接收 Observable 中的 error 类型通知 */ |
||||||
|
error: (err: E) => void |
||||||
|
/** 用来接收 Observable 中的 complete 类型通知 */ |
||||||
|
complete: (res: C) => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface NextObserver<T, E, C> { |
||||||
|
next: (value: T) => void |
||||||
|
error?: (err: E) => void |
||||||
|
complete?: (res: C) => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface ErrorObserver<T, E, C> { |
||||||
|
next?: (value: T) => void |
||||||
|
error: (err: E) => void |
||||||
|
complete?: (res: C) => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface CompletionObserver<T, E, C> { |
||||||
|
next?: (value: T) => void |
||||||
|
error?: (err: E) => void |
||||||
|
complete: (res: C) => void |
||||||
|
} |
||||||
|
|
||||||
|
export type PartialObserver<T, E, C> = NextObserver<T, E, C> | ErrorObserver<T, E, C> | CompletionObserver<T, E, C> |
||||||
|
|
||||||
|
export interface IUnsubscribable { |
||||||
|
/** 取消 observer 的订阅 */ |
||||||
|
unsubscribe(): void |
||||||
|
} |
||||||
|
|
||||||
|
/** Subscription 的接口 */ |
||||||
|
export interface ISubscriptionLike extends IUnsubscribable { |
||||||
|
readonly closed: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export type TeardownLogic = () => void |
||||||
|
|
||||||
|
export interface ISubscribable<T, E, C> { |
||||||
|
subscribe( |
||||||
|
observer?: PartialObserver<T, E, C> | ((value: T) => void), |
||||||
|
error?: (error: any) => void, |
||||||
|
complete?: () => void |
||||||
|
): IUnsubscribable |
||||||
|
} |
||||||
|
|
||||||
|
/** 表示可清理的资源,比如 Observable 的执行 */ |
||||||
|
class Subscription implements ISubscriptionLike { |
||||||
|
/** 用来标示该 Subscription 是否被取消订阅的标示位 */ |
||||||
|
public closed = false |
||||||
|
|
||||||
|
/** 清理 subscription 持有的资源 */ |
||||||
|
private _unsubscribe: TeardownLogic | undefined |
||||||
|
|
||||||
|
/** 取消 observer 的订阅 */ |
||||||
|
unsubscribe() { |
||||||
|
if (this.closed) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
this.closed = true |
||||||
|
if (this._unsubscribe) { |
||||||
|
this._unsubscribe() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 添加一个 tear down 在该 Subscription 的 unsubscribe() 期间调用 */ |
||||||
|
add(teardown: TeardownLogic) { |
||||||
|
this._unsubscribe = teardown |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 实现 Observer 接口并且继承 Subscription 类,Observer 是消费 Observable 值的公有 API |
||||||
|
* 所有 Observers 都转化成了 Subscriber,以便提供类似 Subscription 的能力,比如 unsubscribe |
||||||
|
*/ |
||||||
|
export class Subscriber<T, E, C> extends Subscription implements IObserver<T, E, C> { |
||||||
|
protected isStopped = false |
||||||
|
protected destination: Partial<IObserver<T, E, C>> |
||||||
|
|
||||||
|
constructor( |
||||||
|
observerOrNext?: PartialObserver<T, E, C> | ((value: T) => void) | null, |
||||||
|
error?: ((err: E) => void) | null, |
||||||
|
complete?: ((res: C) => void) | null |
||||||
|
) { |
||||||
|
super() |
||||||
|
|
||||||
|
if (observerOrNext && typeof observerOrNext === 'object') { |
||||||
|
this.destination = observerOrNext |
||||||
|
} else { |
||||||
|
this.destination = { |
||||||
|
...observerOrNext && { next: observerOrNext }, |
||||||
|
...error && { error }, |
||||||
|
...complete && { complete } |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
unsubscribe(): void { |
||||||
|
if (this.closed) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
this.isStopped = true |
||||||
|
super.unsubscribe() |
||||||
|
} |
||||||
|
|
||||||
|
next(value: T) { |
||||||
|
if (!this.isStopped && this.destination.next) { |
||||||
|
this.destination.next(value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
error(err: E) { |
||||||
|
if (!this.isStopped && this.destination.error) { |
||||||
|
this.isStopped = true |
||||||
|
this.destination.error(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
complete(result: C) { |
||||||
|
if (!this.isStopped && this.destination.complete) { |
||||||
|
this.isStopped = true |
||||||
|
this.destination.complete(result) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 可观察对象,当前的上传事件的集合 */ |
||||||
|
export class Observable<T, E, C> implements ISubscribable<T, E, C> { |
||||||
|
|
||||||
|
constructor(private _subscribe: (subscriber: Subscriber<T, E, C>) => TeardownLogic) {} |
||||||
|
|
||||||
|
subscribe(observer: PartialObserver<T, E, C>): Subscription |
||||||
|
subscribe(next: null | undefined, error: null | undefined, complete: (res: C) => void): Subscription |
||||||
|
subscribe(next: null | undefined, error: (error: E) => void, complete?: (res: C) => void): Subscription |
||||||
|
subscribe(next: (value: T) => void, error: null | undefined, complete: (res: C) => void): Subscription |
||||||
|
subscribe( |
||||||
|
observerOrNext?: PartialObserver<T, E, C> | ((value: T) => void) | null, |
||||||
|
error?: ((err: E) => void) | null, |
||||||
|
complete?: ((res: C) => void) | null |
||||||
|
): Subscription { |
||||||
|
const sink = new Subscriber(observerOrNext, error, complete) |
||||||
|
sink.add(this._subscribe(sink)) |
||||||
|
return sink |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
export type RunTask<T> = (...args: T[]) => Promise<void> |
||||||
|
|
||||||
|
export interface QueueContent<T> { |
||||||
|
task: T |
||||||
|
resolve: () => void |
||||||
|
reject: (err?: any) => void |
||||||
|
} |
||||||
|
|
||||||
|
export class Pool<T> { |
||||||
|
aborted = false |
||||||
|
queue: Array<QueueContent<T>> = [] |
||||||
|
processing: Array<QueueContent<T>> = [] |
||||||
|
|
||||||
|
constructor(private runTask: RunTask<T>, private limit: number) {} |
||||||
|
|
||||||
|
enqueue(task: T) { |
||||||
|
return new Promise<void>((resolve, reject) => { |
||||||
|
this.queue.push({ |
||||||
|
task, |
||||||
|
resolve, |
||||||
|
reject |
||||||
|
}) |
||||||
|
this.check() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
private run(item: QueueContent<T>) { |
||||||
|
this.queue = this.queue.filter(v => v !== item) |
||||||
|
this.processing.push(item) |
||||||
|
this.runTask(item.task).then( |
||||||
|
() => { |
||||||
|
this.processing = this.processing.filter(v => v !== item) |
||||||
|
item.resolve() |
||||||
|
this.check() |
||||||
|
}, |
||||||
|
err => item.reject(err) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
private check() { |
||||||
|
if (this.aborted) return |
||||||
|
const processingNum = this.processing.length |
||||||
|
const availableNum = this.limit - processingNum |
||||||
|
this.queue.slice(0, availableNum).forEach(item => { |
||||||
|
this.run(item) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
abort() { |
||||||
|
this.queue = [] |
||||||
|
this.aborted = true |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,348 @@ |
|||||||
|
// https://www.npmjs.com/package/spark-md5
|
||||||
|
|
||||||
|
export interface State { |
||||||
|
buff: string; |
||||||
|
length: number; |
||||||
|
hash: number[]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* SparkMD5 OOP implementation for array buffers. |
||||||
|
* |
||||||
|
* Use this class to perform an incremental md5 ONLY for array buffers. |
||||||
|
*/ |
||||||
|
export class SparkMD5 { |
||||||
|
_buff: Uint8Array = new Uint8Array(0); |
||||||
|
_length: number = 0; |
||||||
|
_hash: number[] = []; |
||||||
|
|
||||||
|
constructor() { |
||||||
|
this.reset(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Appends an array buffer. |
||||||
|
* |
||||||
|
* @param {ArrayBuffer} arr The array to be appended |
||||||
|
* |
||||||
|
* @return {SparkMD5.ArrayBuffer} The instance itself |
||||||
|
*/ |
||||||
|
append(arr: ArrayBufferLike) { |
||||||
|
const buff = concatenateArrayBuffers(this._buff.buffer, arr); |
||||||
|
const length = buff.length; |
||||||
|
this._length += arr.byteLength; |
||||||
|
|
||||||
|
let i; |
||||||
|
for (i = 64; i <= length; i += 64) { |
||||||
|
md5cycle(this._hash, md5blk_array(buff.subarray(i - 64, i))); |
||||||
|
} |
||||||
|
this._buff = |
||||||
|
i - 64 < length |
||||||
|
? new Uint8Array(buff.buffer.slice(i - 64)) |
||||||
|
: new Uint8Array(0); |
||||||
|
|
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Finishes the incremental computation, resetting the internal state and |
||||||
|
* returning the result. |
||||||
|
* |
||||||
|
* @param {Boolean} raw True to get the raw string, false to get the hex string |
||||||
|
* |
||||||
|
* @return {String} The result |
||||||
|
*/ |
||||||
|
end(raw?: boolean) { |
||||||
|
const buff = this._buff; |
||||||
|
const length = buff.length; |
||||||
|
const tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; |
||||||
|
|
||||||
|
for (let i = 0; i < length; i += 1) { |
||||||
|
tail[i >> 2] |= buff[i] << (i % 4 << 3); |
||||||
|
} |
||||||
|
|
||||||
|
this._finish(tail, length); |
||||||
|
|
||||||
|
let ret = hex(this._hash); |
||||||
|
if (raw) { |
||||||
|
ret = hexToBinaryString(ret); |
||||||
|
} |
||||||
|
|
||||||
|
this.reset(); |
||||||
|
|
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Resets the internal state of the computation. |
||||||
|
*/ |
||||||
|
reset() { |
||||||
|
this._buff = new Uint8Array(0); |
||||||
|
this._length = 0; |
||||||
|
this._hash = [1732584193, -271733879, -1732584194, 271733878]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the internal state of the computation. |
||||||
|
* |
||||||
|
* @return {Object} The state |
||||||
|
*/ |
||||||
|
getState(): State { |
||||||
|
return { |
||||||
|
buff: arrayBuffer2Utf8Str(this._buff), // Convert buffer to a string
|
||||||
|
length: this._length, |
||||||
|
hash: this._hash.slice(), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Releases memory used by the incremental buffer and other additional |
||||||
|
* resources. If you plan to use the instance again, use reset instead. |
||||||
|
*/ |
||||||
|
destroy() { |
||||||
|
// delete this._hash;
|
||||||
|
// delete this._buff;
|
||||||
|
// delete this._length;
|
||||||
|
this.reset(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Finish the final calculation based on the tail. |
||||||
|
* |
||||||
|
* @param {Array} tail The tail (will be modified) |
||||||
|
* @param {Number} length The length of the remaining buffer |
||||||
|
*/ |
||||||
|
_finish(tail: number[], length: number) { |
||||||
|
let i = length; |
||||||
|
tail[i >> 2] |= 0x80 << (i % 4 << 3); |
||||||
|
if (i > 55) { |
||||||
|
md5cycle(this._hash, tail); |
||||||
|
for (i = 0; i < 16; i += 1) { |
||||||
|
tail[i] = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Do the final computation based on the tail and length
|
||||||
|
// Beware that the final length may not fit in 32 bits so we take care of that
|
||||||
|
const tmp = this._length * 8; |
||||||
|
const arr = tmp.toString(16).match(/(.*?)(.{0,8})$/)!; |
||||||
|
const lo = parseInt(arr[2], 16); |
||||||
|
const hi = parseInt(arr[1], 16) || 0; |
||||||
|
|
||||||
|
tail[14] = lo; |
||||||
|
tail[15] = hi; |
||||||
|
md5cycle(this._hash, tail); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function md5cycle(x: number[], k: number[]) { |
||||||
|
let a = x[0]; |
||||||
|
let b = x[1]; |
||||||
|
let c = x[2]; |
||||||
|
let d = x[3]; |
||||||
|
|
||||||
|
a += (((b & c) | (~b & d)) + k[0] - 680876936) | 0; |
||||||
|
a = (((a << 7) | (a >>> 25)) + b) | 0; |
||||||
|
d += (((a & b) | (~a & c)) + k[1] - 389564586) | 0; |
||||||
|
d = (((d << 12) | (d >>> 20)) + a) | 0; |
||||||
|
c += (((d & a) | (~d & b)) + k[2] + 606105819) | 0; |
||||||
|
c = (((c << 17) | (c >>> 15)) + d) | 0; |
||||||
|
b += (((c & d) | (~c & a)) + k[3] - 1044525330) | 0; |
||||||
|
b = (((b << 22) | (b >>> 10)) + c) | 0; |
||||||
|
a += (((b & c) | (~b & d)) + k[4] - 176418897) | 0; |
||||||
|
a = (((a << 7) | (a >>> 25)) + b) | 0; |
||||||
|
d += (((a & b) | (~a & c)) + k[5] + 1200080426) | 0; |
||||||
|
d = (((d << 12) | (d >>> 20)) + a) | 0; |
||||||
|
c += (((d & a) | (~d & b)) + k[6] - 1473231341) | 0; |
||||||
|
c = (((c << 17) | (c >>> 15)) + d) | 0; |
||||||
|
b += (((c & d) | (~c & a)) + k[7] - 45705983) | 0; |
||||||
|
b = (((b << 22) | (b >>> 10)) + c) | 0; |
||||||
|
a += (((b & c) | (~b & d)) + k[8] + 1770035416) | 0; |
||||||
|
a = (((a << 7) | (a >>> 25)) + b) | 0; |
||||||
|
d += (((a & b) | (~a & c)) + k[9] - 1958414417) | 0; |
||||||
|
d = (((d << 12) | (d >>> 20)) + a) | 0; |
||||||
|
c += (((d & a) | (~d & b)) + k[10] - 42063) | 0; |
||||||
|
c = (((c << 17) | (c >>> 15)) + d) | 0; |
||||||
|
b += (((c & d) | (~c & a)) + k[11] - 1990404162) | 0; |
||||||
|
b = (((b << 22) | (b >>> 10)) + c) | 0; |
||||||
|
a += (((b & c) | (~b & d)) + k[12] + 1804603682) | 0; |
||||||
|
a = (((a << 7) | (a >>> 25)) + b) | 0; |
||||||
|
d += (((a & b) | (~a & c)) + k[13] - 40341101) | 0; |
||||||
|
d = (((d << 12) | (d >>> 20)) + a) | 0; |
||||||
|
c += (((d & a) | (~d & b)) + k[14] - 1502002290) | 0; |
||||||
|
c = (((c << 17) | (c >>> 15)) + d) | 0; |
||||||
|
b += (((c & d) | (~c & a)) + k[15] + 1236535329) | 0; |
||||||
|
b = (((b << 22) | (b >>> 10)) + c) | 0; |
||||||
|
|
||||||
|
a += (((b & d) | (c & ~d)) + k[1] - 165796510) | 0; |
||||||
|
a = (((a << 5) | (a >>> 27)) + b) | 0; |
||||||
|
d += (((a & c) | (b & ~c)) + k[6] - 1069501632) | 0; |
||||||
|
d = (((d << 9) | (d >>> 23)) + a) | 0; |
||||||
|
c += (((d & b) | (a & ~b)) + k[11] + 643717713) | 0; |
||||||
|
c = (((c << 14) | (c >>> 18)) + d) | 0; |
||||||
|
b += (((c & a) | (d & ~a)) + k[0] - 373897302) | 0; |
||||||
|
b = (((b << 20) | (b >>> 12)) + c) | 0; |
||||||
|
a += (((b & d) | (c & ~d)) + k[5] - 701558691) | 0; |
||||||
|
a = (((a << 5) | (a >>> 27)) + b) | 0; |
||||||
|
d += (((a & c) | (b & ~c)) + k[10] + 38016083) | 0; |
||||||
|
d = (((d << 9) | (d >>> 23)) + a) | 0; |
||||||
|
c += (((d & b) | (a & ~b)) + k[15] - 660478335) | 0; |
||||||
|
c = (((c << 14) | (c >>> 18)) + d) | 0; |
||||||
|
b += (((c & a) | (d & ~a)) + k[4] - 405537848) | 0; |
||||||
|
b = (((b << 20) | (b >>> 12)) + c) | 0; |
||||||
|
a += (((b & d) | (c & ~d)) + k[9] + 568446438) | 0; |
||||||
|
a = (((a << 5) | (a >>> 27)) + b) | 0; |
||||||
|
d += (((a & c) | (b & ~c)) + k[14] - 1019803690) | 0; |
||||||
|
d = (((d << 9) | (d >>> 23)) + a) | 0; |
||||||
|
c += (((d & b) | (a & ~b)) + k[3] - 187363961) | 0; |
||||||
|
c = (((c << 14) | (c >>> 18)) + d) | 0; |
||||||
|
b += (((c & a) | (d & ~a)) + k[8] + 1163531501) | 0; |
||||||
|
b = (((b << 20) | (b >>> 12)) + c) | 0; |
||||||
|
a += (((b & d) | (c & ~d)) + k[13] - 1444681467) | 0; |
||||||
|
a = (((a << 5) | (a >>> 27)) + b) | 0; |
||||||
|
d += (((a & c) | (b & ~c)) + k[2] - 51403784) | 0; |
||||||
|
d = (((d << 9) | (d >>> 23)) + a) | 0; |
||||||
|
c += (((d & b) | (a & ~b)) + k[7] + 1735328473) | 0; |
||||||
|
c = (((c << 14) | (c >>> 18)) + d) | 0; |
||||||
|
b += (((c & a) | (d & ~a)) + k[12] - 1926607734) | 0; |
||||||
|
b = (((b << 20) | (b >>> 12)) + c) | 0; |
||||||
|
|
||||||
|
a += ((b ^ c ^ d) + k[5] - 378558) | 0; |
||||||
|
a = (((a << 4) | (a >>> 28)) + b) | 0; |
||||||
|
d += ((a ^ b ^ c) + k[8] - 2022574463) | 0; |
||||||
|
d = (((d << 11) | (d >>> 21)) + a) | 0; |
||||||
|
c += ((d ^ a ^ b) + k[11] + 1839030562) | 0; |
||||||
|
c = (((c << 16) | (c >>> 16)) + d) | 0; |
||||||
|
b += ((c ^ d ^ a) + k[14] - 35309556) | 0; |
||||||
|
b = (((b << 23) | (b >>> 9)) + c) | 0; |
||||||
|
a += ((b ^ c ^ d) + k[1] - 1530992060) | 0; |
||||||
|
a = (((a << 4) | (a >>> 28)) + b) | 0; |
||||||
|
d += ((a ^ b ^ c) + k[4] + 1272893353) | 0; |
||||||
|
d = (((d << 11) | (d >>> 21)) + a) | 0; |
||||||
|
c += ((d ^ a ^ b) + k[7] - 155497632) | 0; |
||||||
|
c = (((c << 16) | (c >>> 16)) + d) | 0; |
||||||
|
b += ((c ^ d ^ a) + k[10] - 1094730640) | 0; |
||||||
|
b = (((b << 23) | (b >>> 9)) + c) | 0; |
||||||
|
a += ((b ^ c ^ d) + k[13] + 681279174) | 0; |
||||||
|
a = (((a << 4) | (a >>> 28)) + b) | 0; |
||||||
|
d += ((a ^ b ^ c) + k[0] - 358537222) | 0; |
||||||
|
d = (((d << 11) | (d >>> 21)) + a) | 0; |
||||||
|
c += ((d ^ a ^ b) + k[3] - 722521979) | 0; |
||||||
|
c = (((c << 16) | (c >>> 16)) + d) | 0; |
||||||
|
b += ((c ^ d ^ a) + k[6] + 76029189) | 0; |
||||||
|
b = (((b << 23) | (b >>> 9)) + c) | 0; |
||||||
|
a += ((b ^ c ^ d) + k[9] - 640364487) | 0; |
||||||
|
a = (((a << 4) | (a >>> 28)) + b) | 0; |
||||||
|
d += ((a ^ b ^ c) + k[12] - 421815835) | 0; |
||||||
|
d = (((d << 11) | (d >>> 21)) + a) | 0; |
||||||
|
c += ((d ^ a ^ b) + k[15] + 530742520) | 0; |
||||||
|
c = (((c << 16) | (c >>> 16)) + d) | 0; |
||||||
|
b += ((c ^ d ^ a) + k[2] - 995338651) | 0; |
||||||
|
b = (((b << 23) | (b >>> 9)) + c) | 0; |
||||||
|
|
||||||
|
a += ((c ^ (b | ~d)) + k[0] - 198630844) | 0; |
||||||
|
a = (((a << 6) | (a >>> 26)) + b) | 0; |
||||||
|
d += ((b ^ (a | ~c)) + k[7] + 1126891415) | 0; |
||||||
|
d = (((d << 10) | (d >>> 22)) + a) | 0; |
||||||
|
c += ((a ^ (d | ~b)) + k[14] - 1416354905) | 0; |
||||||
|
c = (((c << 15) | (c >>> 17)) + d) | 0; |
||||||
|
b += ((d ^ (c | ~a)) + k[5] - 57434055) | 0; |
||||||
|
b = (((b << 21) | (b >>> 11)) + c) | 0; |
||||||
|
a += ((c ^ (b | ~d)) + k[12] + 1700485571) | 0; |
||||||
|
a = (((a << 6) | (a >>> 26)) + b) | 0; |
||||||
|
d += ((b ^ (a | ~c)) + k[3] - 1894986606) | 0; |
||||||
|
d = (((d << 10) | (d >>> 22)) + a) | 0; |
||||||
|
c += ((a ^ (d | ~b)) + k[10] - 1051523) | 0; |
||||||
|
c = (((c << 15) | (c >>> 17)) + d) | 0; |
||||||
|
b += ((d ^ (c | ~a)) + k[1] - 2054922799) | 0; |
||||||
|
b = (((b << 21) | (b >>> 11)) + c) | 0; |
||||||
|
a += ((c ^ (b | ~d)) + k[8] + 1873313359) | 0; |
||||||
|
a = (((a << 6) | (a >>> 26)) + b) | 0; |
||||||
|
d += ((b ^ (a | ~c)) + k[15] - 30611744) | 0; |
||||||
|
d = (((d << 10) | (d >>> 22)) + a) | 0; |
||||||
|
c += ((a ^ (d | ~b)) + k[6] - 1560198380) | 0; |
||||||
|
c = (((c << 15) | (c >>> 17)) + d) | 0; |
||||||
|
b += ((d ^ (c | ~a)) + k[13] + 1309151649) | 0; |
||||||
|
b = (((b << 21) | (b >>> 11)) + c) | 0; |
||||||
|
a += ((c ^ (b | ~d)) + k[4] - 145523070) | 0; |
||||||
|
a = (((a << 6) | (a >>> 26)) + b) | 0; |
||||||
|
d += ((b ^ (a | ~c)) + k[11] - 1120210379) | 0; |
||||||
|
d = (((d << 10) | (d >>> 22)) + a) | 0; |
||||||
|
c += ((a ^ (d | ~b)) + k[2] + 718787259) | 0; |
||||||
|
c = (((c << 15) | (c >>> 17)) + d) | 0; |
||||||
|
b += ((d ^ (c | ~a)) + k[9] - 343485551) | 0; |
||||||
|
b = (((b << 21) | (b >>> 11)) + c) | 0; |
||||||
|
|
||||||
|
x[0] = (a + x[0]) | 0; |
||||||
|
x[1] = (b + x[1]) | 0; |
||||||
|
x[2] = (c + x[2]) | 0; |
||||||
|
x[3] = (d + x[3]) | 0; |
||||||
|
} |
||||||
|
|
||||||
|
function md5blk_array(a: Uint8Array) { |
||||||
|
const md5blks: number[] = []; |
||||||
|
for (let i = 0; i < 64; i += 4) { |
||||||
|
md5blks[i >> 2] = |
||||||
|
a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); |
||||||
|
} |
||||||
|
return md5blks; |
||||||
|
} |
||||||
|
|
||||||
|
const hex_chr = [ |
||||||
|
"0", |
||||||
|
"1", |
||||||
|
"2", |
||||||
|
"3", |
||||||
|
"4", |
||||||
|
"5", |
||||||
|
"6", |
||||||
|
"7", |
||||||
|
"8", |
||||||
|
"9", |
||||||
|
"a", |
||||||
|
"b", |
||||||
|
"c", |
||||||
|
"d", |
||||||
|
"e", |
||||||
|
"f", |
||||||
|
]; |
||||||
|
|
||||||
|
function rhex(n: number): string { |
||||||
|
let s = ""; |
||||||
|
for (let j = 0; j < 4; j += 1) { |
||||||
|
s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; |
||||||
|
} |
||||||
|
return s; |
||||||
|
} |
||||||
|
|
||||||
|
function hex(x: number[]) { |
||||||
|
const r = new Array(x.length); |
||||||
|
for (let i = 0; i < x.length; i += 1) { |
||||||
|
r[i] = rhex(x[i]); |
||||||
|
} |
||||||
|
return r.join(""); |
||||||
|
} |
||||||
|
|
||||||
|
function arrayBuffer2Utf8Str(buff: ArrayBufferLike) { |
||||||
|
// @ts-ignore
|
||||||
|
return String.fromCharCode.apply(null, new Uint8Array(buff)); |
||||||
|
} |
||||||
|
|
||||||
|
function concatenateArrayBuffers( |
||||||
|
first: ArrayBufferLike, |
||||||
|
second: ArrayBufferLike, |
||||||
|
): Uint8Array { |
||||||
|
const result = new Uint8Array(first.byteLength + second.byteLength); |
||||||
|
result.set(new Uint8Array(first)); |
||||||
|
result.set(new Uint8Array(second), first.byteLength); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function hexToBinaryString(hex: string) { |
||||||
|
const bytes = []; |
||||||
|
const length = hex.length; |
||||||
|
for (let x = 0; x < length - 1; x += 2) { |
||||||
|
bytes.push(parseInt(hex.substring(x, 2), 16)); |
||||||
|
} |
||||||
|
return String.fromCharCode.apply(String, bytes); |
||||||
|
} |
@ -0,0 +1,181 @@ |
|||||||
|
import { |
||||||
|
abortion, |
||||||
|
type AbortionContext, |
||||||
|
coerce, |
||||||
|
isInvalid, |
||||||
|
isValid, |
||||||
|
SparkMD5, |
||||||
|
uniqueId |
||||||
|
} from "../shared"; |
||||||
|
import {report} from "./report"; |
||||||
|
import {Uploader} from "./uploader"; |
||||||
|
|
||||||
|
const registry: Map<TaskId, Reader> = new Map(); |
||||||
|
|
||||||
|
export function getReadTasks(): Array<Task> { |
||||||
|
return Array.from(registry.values()).map((t) => ({...t.data})); |
||||||
|
} |
||||||
|
|
||||||
|
export async function pauseReadTask(id: TaskId, reason?: any): Promise<void> { |
||||||
|
return registry.get(id)?.pause(reason); |
||||||
|
} |
||||||
|
|
||||||
|
export async function resumeReadTask(id: TaskId): Promise<void> { |
||||||
|
return registry.get(id)?.resume(); |
||||||
|
} |
||||||
|
|
||||||
|
export async function removeReadTask(id: TaskId): Promise<void> { |
||||||
|
return registry.get(id)?.destroy(); |
||||||
|
} |
||||||
|
|
||||||
|
export async function cleanReadTasks(): Promise<void> { |
||||||
|
for (const task of registry.values()) { |
||||||
|
await task.cleanup(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 读取文件,有效的 5 种任务状态: |
||||||
|
* initial、reading、readend、failed、aborted |
||||||
|
*/ |
||||||
|
export class Reader implements AbortionContext { |
||||||
|
ctrl: AbortController; |
||||||
|
|
||||||
|
protected constructor( |
||||||
|
readonly file: File, |
||||||
|
readonly data: Task, |
||||||
|
) { |
||||||
|
this.ctrl = abortion(this); |
||||||
|
this.file = file; |
||||||
|
} |
||||||
|
|
||||||
|
get id(): TaskId { |
||||||
|
return this.data.id; |
||||||
|
} |
||||||
|
|
||||||
|
get status(): TaskStatus { |
||||||
|
return this.data.status; |
||||||
|
} |
||||||
|
|
||||||
|
get signal(): AbortSignal { |
||||||
|
return this.ctrl.signal; |
||||||
|
} |
||||||
|
|
||||||
|
static create(file: File, dirid: number): void { |
||||||
|
if (!file.type) { |
||||||
|
// 有些文件是没有类型的
|
||||||
|
// TODO 需要更加明确的信息来指明是哪一个文件
|
||||||
|
void report("app", "error", "未知文件类型"); |
||||||
|
} else if (!/^(image|video|audio)\//.test(file.type)) { |
||||||
|
// 仅支持图片、视频和音频这三种类型
|
||||||
|
// TODO 需要更加明确的信息来指明是哪一个文件
|
||||||
|
void report("app", "error", "不支持的文件类型"); |
||||||
|
} else { |
||||||
|
const task = new Reader(file, { |
||||||
|
id: uniqueId(), |
||||||
|
dirid, |
||||||
|
name: file.name, |
||||||
|
path: URL.createObjectURL(file), |
||||||
|
hash: "<hash>", |
||||||
|
mime: file.type, |
||||||
|
size: file.size, |
||||||
|
status: "initial", |
||||||
|
}); |
||||||
|
registry.set(task.id, task); |
||||||
|
queueMicrotask(task.start.bind(task)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 暂停任务 */ |
||||||
|
pause(reason?: any): void { |
||||||
|
if (isValid(this) && !this.signal.aborted) { |
||||||
|
this.ctrl.abort(reason ?? new Error("已暂停")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 恢复任务 */ |
||||||
|
async resume(): Promise<void> { |
||||||
|
if (isInvalid(this)) { |
||||||
|
if (this.signal.aborted) { |
||||||
|
this.ctrl = abortion(this); |
||||||
|
} |
||||||
|
await this.start(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 清理任务 */ |
||||||
|
async cleanup(): Promise<void> { |
||||||
|
if (isInvalid(this)) { |
||||||
|
await this.destroy(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 直接删除 */ |
||||||
|
async destroy(): Promise<void> { |
||||||
|
registry.delete(this.id); |
||||||
|
if (!this.signal.aborted) { |
||||||
|
this.ctrl.abort(new Error("删除任务")); |
||||||
|
} |
||||||
|
if (this.status !== "readend") { |
||||||
|
// 如果任务已经完成,文件路径还会被继续使用,
|
||||||
|
// 所以只有不在该状态下才能够释放内存。
|
||||||
|
URL.revokeObjectURL(this.data.path); |
||||||
|
} |
||||||
|
await report("task", "delete", [this.id]); |
||||||
|
} |
||||||
|
|
||||||
|
async onabort(_: Event): Promise<void> { |
||||||
|
if (isValid(this)) { |
||||||
|
await this.report("aborted", { |
||||||
|
error: this.signal.reason, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 开始解析文件 */ |
||||||
|
private async start(): Promise<void> { |
||||||
|
await this.report("initial"); |
||||||
|
if (this.data.hash === "<hash>") { |
||||||
|
await this.report("reading", {progress: 0}); |
||||||
|
const spark = new SparkMD5(); |
||||||
|
let read = 0; |
||||||
|
try { |
||||||
|
await this.file.stream().pipeTo( |
||||||
|
new WritableStream<Uint8Array>({ |
||||||
|
write: async (chunk) => { |
||||||
|
spark.append(chunk); |
||||||
|
read += chunk.length; |
||||||
|
await this.report("reading", { |
||||||
|
progress: Math.min(read / this.file.size, 1), |
||||||
|
}); |
||||||
|
}, |
||||||
|
}), |
||||||
|
{signal: this.signal}, |
||||||
|
); |
||||||
|
} catch (error) { |
||||||
|
if (!this.signal.aborted && isValid(this)) { |
||||||
|
await this.report("failed", {error}); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
this.data.hash = spark.end(); |
||||||
|
} |
||||||
|
await this.report("readend", {progress: 1}); |
||||||
|
Uploader.create(this.file, this.data, (duplicated: boolean) => { |
||||||
|
registry.delete(this.id); |
||||||
|
if (!duplicated) return; |
||||||
|
return report("app", "error", "任务已经存在了,切勿重复上传哦"); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private async report( |
||||||
|
status: TaskStatus, |
||||||
|
info?: { progress?: number; error?: any }, |
||||||
|
): Promise<void> { |
||||||
|
const error = coerce(info?.error); |
||||||
|
this.data.status = status; |
||||||
|
this.data.progress = info?.progress; |
||||||
|
this.data.error = error != null ? String(error) : undefined; |
||||||
|
await report("task", "sync", {...this.data}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
import { |
||||||
|
call, |
||||||
|
coerce, |
||||||
|
createReplier, |
||||||
|
emit, |
||||||
|
isRecvData, |
||||||
|
isSendData, |
||||||
|
on, |
||||||
|
once, |
||||||
|
uniqueId, |
||||||
|
} from "../shared"; |
||||||
|
|
||||||
|
const ports: Array<MessagePort> = []; |
||||||
|
const resolvers: Map<number, VoidFunction> = new Map(); |
||||||
|
|
||||||
|
let online = false |
||||||
|
|
||||||
|
// 处理来自客户端的数据
|
||||||
|
// @ts-ignore
|
||||||
|
self.onconnect = (evt: MessageEvent): void => { |
||||||
|
const port = evt.ports[0]; |
||||||
|
|
||||||
|
// 客户端卸载事件,无需回复
|
||||||
|
once("app:close", () => { |
||||||
|
const index = ports.findIndex((p) => p === port); |
||||||
|
index > -1 && ports.splice(index, 1); |
||||||
|
}) |
||||||
|
|
||||||
|
// 接受客户端消息
|
||||||
|
port.onmessage = (evt: MessageEvent): void => { |
||||||
|
if (isRecvData(evt.data)) { |
||||||
|
call(resolvers.get(evt.data.id)); // 忽略来自客户端的结果
|
||||||
|
} else if (isSendData(evt.data)) { |
||||||
|
const {scope, action, data} = evt.data |
||||||
|
const reply = createReplier(port, evt.data) |
||||||
|
emit(`${scope}:${action}`, data, reply); |
||||||
|
} else { |
||||||
|
console.warn("[wfs] unknown message event", evt) |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
ports.push(port); |
||||||
|
|
||||||
|
void report('app', 'status', online ? 'online' : 'offline') |
||||||
|
} |
||||||
|
|
||||||
|
// WebSocket 事件
|
||||||
|
on("ws:open", () => { |
||||||
|
online = true |
||||||
|
void report("app", "status", "online") |
||||||
|
}); |
||||||
|
|
||||||
|
on("ws:close", () => { |
||||||
|
online = false |
||||||
|
void report("app", "status", "offline") |
||||||
|
}); |
||||||
|
|
||||||
|
on('ws:file:create', (data: CloudFile) => { |
||||||
|
void report('file', 'create', data); |
||||||
|
}) |
||||||
|
|
||||||
|
on('ws:file:update', (data: CloudFile) => { |
||||||
|
void report('file', 'update', data); |
||||||
|
}) |
||||||
|
|
||||||
|
export function report(scope: "app", action: "config", config: Partial<ApiConfig>): Promise<void> |
||||||
|
export function report(scope: "app", action: "status", data: "online" | "offline"): Promise<void>; |
||||||
|
export function report(scope: "app", action: "init", tasks: Array<Task>): Promise<void>; |
||||||
|
export function report(scope: "app", action: "error", error: any): Promise<void>; |
||||||
|
export function report(scope: "task", action: "sync", task: Task): Promise<void>; |
||||||
|
export function report(scope: "task", action: "delete", tasks: number[]): Promise<void>; |
||||||
|
export function report(scope: 'file', action: "create", file: CloudFile): Promise<void> |
||||||
|
export function report(scope: 'file', action: "update", file: CloudFile): Promise<void> |
||||||
|
|
||||||
|
export function report(scope: string, action: string, data?: any): Promise<void> { |
||||||
|
const id = uniqueId() |
||||||
|
if (scope === "app" && action === "error") { |
||||||
|
data = String(coerce(data)); |
||||||
|
} |
||||||
|
return new Promise((resolve) => { |
||||||
|
let sends = 0; // 发送次数
|
||||||
|
let count = 0; // 接收次数
|
||||||
|
resolvers.set(id, () => { |
||||||
|
if (++count >= sends) { |
||||||
|
resolvers.delete(id); |
||||||
|
resolve(); |
||||||
|
} |
||||||
|
}); |
||||||
|
queueMicrotask(() => { |
||||||
|
for (const port of ports) { |
||||||
|
sends++ |
||||||
|
port.postMessage({id, scope, action, data}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,275 @@ |
|||||||
|
// 参考文档链接
|
||||||
|
// https://www.zhihu.com/tardis/zm/art/162808604?source_id=1003
|
||||||
|
// https://www.ruanyifeng.com/blog/2017/05/websocket.html
|
||||||
|
// https://web.dev/articles/websockets-basics?hl=zh-cn
|
||||||
|
|
||||||
|
import {emit} from "../shared"; |
||||||
|
|
||||||
|
const HEARTBEAT = "heartbeat"; |
||||||
|
const CLOSE = "close"; |
||||||
|
|
||||||
|
const closeTimeout = 5000; // 等待关闭超时
|
||||||
|
const reconnectInterval = 3000; // 重连间隔
|
||||||
|
const heartbeatInterval = 60000; // 心跳间隔
|
||||||
|
const heartbeatTimeout = 50000; // 心跳超时
|
||||||
|
|
||||||
|
// WebSocket 对象引用
|
||||||
|
let socket: WebSocket | undefined; |
||||||
|
|
||||||
|
// 自动重连开关。
|
||||||
|
//
|
||||||
|
// 如果客户端网络不可用,我们就只能等到网络可用时
|
||||||
|
// 才去连接服务器。所以该变量是用来配合网络变化事件,
|
||||||
|
// 当连接不可用时能否自动重连。
|
||||||
|
let isReconnect = navigator.onLine; |
||||||
|
|
||||||
|
// 心跳机制
|
||||||
|
type Heartbeat = { |
||||||
|
boot: VoidFunction; |
||||||
|
next: VoidFunction; |
||||||
|
stop: VoidFunction; |
||||||
|
}; |
||||||
|
|
||||||
|
let heartbeat: Heartbeat | undefined; |
||||||
|
|
||||||
|
/** |
||||||
|
* 用于监听网络状态编号时的回调函数 |
||||||
|
*/ |
||||||
|
export function handleNetworkEvent(status: "on" | "off"): void { |
||||||
|
// 只有在网络可用的情况下才支持自动重连
|
||||||
|
isReconnect = status === "on"; |
||||||
|
|
||||||
|
if (status === "on") { |
||||||
|
// 理论上,切换成在线状态时,立即发送一个心跳包,
|
||||||
|
// 这样就可以检测连接是否可以;但是当网络可用时,
|
||||||
|
// 打开的连接可能还不能立即恢复,所以延迟发送。
|
||||||
|
setTimeout(() => heartbeat?.boot(), reconnectInterval); |
||||||
|
} else { |
||||||
|
heartbeat?.stop(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function useSocket(cfg: Partial<ApiConfig>): void { |
||||||
|
if (cfg.apiUrl && cfg.artifact && cfg.accessToken) { |
||||||
|
const uri = new URL(cfg.apiUrl); |
||||||
|
uri.protocol = uri.protocol.replace(/^http/, "ws"); |
||||||
|
uri.pathname = uri.pathname.replace(/\/+$/, "") + `/${cfg.artifact}/notifies`; |
||||||
|
openSocket(uri.toString(), cfg.accessToken); |
||||||
|
} else if (socket != null) { |
||||||
|
closeSocket(socket); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function createHeartbeat(ws: WebSocket): Heartbeat { |
||||||
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined; // 心跳回复超时器
|
||||||
|
let heartbeatTimer: ReturnType<typeof setTimeout> | undefined; // 心跳定时器
|
||||||
|
|
||||||
|
const boot = () => { |
||||||
|
// 引用不存在或被重置
|
||||||
|
if (socket !== ws) { |
||||||
|
closeSocket(ws); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 如果还存在定时器,就说明没有收到心跳回复,
|
||||||
|
// 我们就可以直接关闭连接。
|
||||||
|
if (timeoutTimer != null) { |
||||||
|
closeSocket(ws); |
||||||
|
socket = undefined; |
||||||
|
return; |
||||||
|
} |
||||||
|
// 只有在连接可用时才能发送心跳包
|
||||||
|
if (ws.readyState === ws.OPEN) { |
||||||
|
// 发送心跳包
|
||||||
|
ws.send(HEARTBEAT); |
||||||
|
// 心跳回复超时检测
|
||||||
|
timeoutTimer = setTimeout(() => { |
||||||
|
timeoutTimer = undefined; |
||||||
|
closeSocket(ws); |
||||||
|
}, heartbeatTimeout); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const next = () => { |
||||||
|
// 引用不存在或被重置
|
||||||
|
if (socket !== ws) { |
||||||
|
closeSocket(ws); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 取消心跳回复超时事件
|
||||||
|
if (timeoutTimer) { |
||||||
|
clearTimeout(timeoutTimer); |
||||||
|
timeoutTimer = undefined; |
||||||
|
} |
||||||
|
// 下次心跳
|
||||||
|
heartbeatTimer = setTimeout(() => { |
||||||
|
heartbeatTimer = undefined; |
||||||
|
boot(); |
||||||
|
}, heartbeatInterval); |
||||||
|
}; |
||||||
|
|
||||||
|
const stop = () => { |
||||||
|
// 取消心跳回复超时事件
|
||||||
|
if (timeoutTimer) { |
||||||
|
clearTimeout(timeoutTimer); |
||||||
|
timeoutTimer = undefined; |
||||||
|
} |
||||||
|
// 取消心跳事件
|
||||||
|
if (heartbeatTimer) { |
||||||
|
clearTimeout(heartbeatTimer); |
||||||
|
heartbeatTimer = undefined; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return {boot, next, stop}; |
||||||
|
} |
||||||
|
|
||||||
|
// 关闭 WebSocket 连接
|
||||||
|
function closeSocket(ws: WebSocket, force = false): void { |
||||||
|
console.log("closeSocket") |
||||||
|
switch (ws.readyState) { |
||||||
|
case WebSocket.CONNECTING: |
||||||
|
ws.close(); |
||||||
|
return; |
||||||
|
case WebSocket.CLOSING: |
||||||
|
case WebSocket.CLOSED: |
||||||
|
return; |
||||||
|
case WebSocket.OPEN: |
||||||
|
if (force) { |
||||||
|
ws.close(); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 我们尝试通知服务器关闭,
|
||||||
|
// 如果超过指定时限就强制关闭。
|
||||||
|
ws.send(CLOSE); |
||||||
|
setTimeout(() => { |
||||||
|
closeSocket(ws, true); |
||||||
|
}, closeTimeout); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface NotifyData { |
||||||
|
scope: string; |
||||||
|
action: string; |
||||||
|
data: Record<string, any> | Array<any>; |
||||||
|
} |
||||||
|
|
||||||
|
function isNotifyData(v: any): v is NotifyData { |
||||||
|
return ( |
||||||
|
typeof v === "object" && |
||||||
|
!Array.isArray(v) && |
||||||
|
v != null && |
||||||
|
"scope" in v && |
||||||
|
"action" in v && |
||||||
|
"data" in v && |
||||||
|
typeof v.scope === "string" && |
||||||
|
typeof v.action === "string" && |
||||||
|
typeof v.data === "object" && |
||||||
|
v.data != null |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 打开 WebSocket 连接
|
||||||
|
function openSocket(url: string, protocol: string): void { |
||||||
|
if (socket != null) { |
||||||
|
if ( |
||||||
|
socket.url === url && |
||||||
|
socket.protocol === protocol && |
||||||
|
socket.readyState < WebSocket.CLOSING |
||||||
|
) { |
||||||
|
// 如果关键参数一致而且连接是可用的,
|
||||||
|
// 我们就不需要重新创建连接。
|
||||||
|
return; |
||||||
|
} |
||||||
|
// 关闭之前的连接
|
||||||
|
closeSocket(socket); |
||||||
|
socket = undefined; |
||||||
|
} |
||||||
|
|
||||||
|
// 创建新的连接
|
||||||
|
const ws = new WebSocket(url, protocol); |
||||||
|
const hb = createHeartbeat(ws); |
||||||
|
|
||||||
|
heartbeat = hb; |
||||||
|
|
||||||
|
// Event: WebSocket opened
|
||||||
|
ws.onopen = (): void => { |
||||||
|
if (socket === ws) { |
||||||
|
emit("ws:open"); // 通知连接可用
|
||||||
|
hb.boot(); // 启动心跳检测
|
||||||
|
} else { |
||||||
|
// 运行到这里,说明在 ws 可用之前就被弃用了,
|
||||||
|
// 我们就发送关闭事件通知服务器关闭这个连接。
|
||||||
|
closeSocket(ws); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Event: WebSocket message received
|
||||||
|
ws.onmessage = (evt: MessageEvent): void => { |
||||||
|
// 假定我们只接受字符串消息,服务器不会发送二进制数据
|
||||||
|
const lines = (evt.data as string).split("\n"); |
||||||
|
if (socket !== ws) { |
||||||
|
// 代码运行到这里,就说明 ws 被弃用了,此刻若收到关闭命令,
|
||||||
|
// 我们就主动关闭客户端,相反则通知服务器关闭连接。
|
||||||
|
if (lines.includes(CLOSE)) { |
||||||
|
ws.close(); |
||||||
|
} else { |
||||||
|
closeSocket(ws); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
// 处理消息
|
||||||
|
for (const line of lines) { |
||||||
|
if (line === CLOSE) { |
||||||
|
// 当我们收到的消息里面包含了关闭命令时,
|
||||||
|
// 我们就主动关闭客户端并销毁连接引用。
|
||||||
|
// 同时抛弃后续数据
|
||||||
|
ws.close(); |
||||||
|
socket = undefined; |
||||||
|
return; |
||||||
|
} |
||||||
|
if (line === HEARTBEAT) { |
||||||
|
hb.next(); |
||||||
|
continue; |
||||||
|
} |
||||||
|
try { |
||||||
|
const item = JSON.parse(line); |
||||||
|
if (isNotifyData(item)) { |
||||||
|
// 派发事件
|
||||||
|
const {scope, action, data} = item; |
||||||
|
emit(`ws:${scope}:${action}`, data); |
||||||
|
} else { |
||||||
|
emit("ws:message", item); |
||||||
|
} |
||||||
|
} catch { |
||||||
|
emit("ws:message", line); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Event: WebSocket error
|
||||||
|
ws.onerror = (error) => { |
||||||
|
console.error("WebSocket error:", error); |
||||||
|
socket === ws && emit("ws:error", error); |
||||||
|
closeSocket(ws); // Close the socket if an error occurs
|
||||||
|
}; |
||||||
|
|
||||||
|
// Event: WebSocket closed
|
||||||
|
ws.onclose = () => { |
||||||
|
console.log("WebSocket connection is closed."); |
||||||
|
if (socket === ws) { |
||||||
|
emit("ws:close"); |
||||||
|
} |
||||||
|
socket = undefined |
||||||
|
// 停止心跳
|
||||||
|
hb.stop(); |
||||||
|
// 断线后自动重连
|
||||||
|
if (isReconnect) { |
||||||
|
console.log("Reconnecting..."); |
||||||
|
setTimeout(function () { |
||||||
|
openSocket(url, protocol); |
||||||
|
}, reconnectInterval); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
socket = ws; |
||||||
|
} |
@ -0,0 +1,417 @@ |
|||||||
|
// import {QiniuNetworkError, QiniuRequestError, upload} from './qiniu-js';
|
||||||
|
// import {type UploadProgress} from "./qiniu-js/upload";
|
||||||
|
import { |
||||||
|
QiniuNetworkError, |
||||||
|
QiniuRequestError, |
||||||
|
upload, |
||||||
|
type UploadProgress, |
||||||
|
} from './qiniu' |
||||||
|
import { |
||||||
|
abortion, |
||||||
|
type AbortionContext, |
||||||
|
coerce, |
||||||
|
isInvalid, |
||||||
|
isSameTask, |
||||||
|
isValid, |
||||||
|
promisify, |
||||||
|
} from "../shared"; |
||||||
|
import type {UploadArgs} from './api' |
||||||
|
import { |
||||||
|
completeUploadApi, |
||||||
|
initiateUploadApi, |
||||||
|
simulateQiniuCallback |
||||||
|
} from './api' |
||||||
|
import {report} from "./report"; |
||||||
|
|
||||||
|
const registry: Map<FileHash, Uploader> = new Map(); |
||||||
|
|
||||||
|
export function getUploadTasks(): Array<Task> { |
||||||
|
return Array.from(registry.values()).reduce<Task[]>( |
||||||
|
(l, u) => l.concat(...u.tasks.map((t) => ({...t}))), |
||||||
|
[], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export async function pauseUploadTask( |
||||||
|
hash: FileHash, |
||||||
|
id: TaskId, |
||||||
|
): Promise<void> { |
||||||
|
await registry.get(hash)?.pause(id); |
||||||
|
} |
||||||
|
|
||||||
|
export async function resumeUploadTask( |
||||||
|
hash: FileHash, |
||||||
|
id: TaskId, |
||||||
|
): Promise<void> { |
||||||
|
await registry.get(hash)?.resume(id); |
||||||
|
} |
||||||
|
|
||||||
|
export async function removeUploadTask( |
||||||
|
hash: FileHash, |
||||||
|
id: TaskId, |
||||||
|
): Promise<void> { |
||||||
|
await registry.get(hash)?.destroy(id); |
||||||
|
} |
||||||
|
|
||||||
|
export async function cleanUploadTasks(): Promise<void> { |
||||||
|
for (const tu of registry.values()) { |
||||||
|
await tu.cleanup(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 后端任务状态 |
||||||
|
*/ |
||||||
|
type JobStatus = |
||||||
|
| "initial" // 初始化状态
|
||||||
|
| "pending" // 等待上传
|
||||||
|
| "uploading" // 正在上传
|
||||||
|
| "uploaded" // 上传完成
|
||||||
|
| "errored" // 上传失败
|
||||||
|
| "aborted"; // 终止上传
|
||||||
|
|
||||||
|
const statuses: Array<JobStatus> = [ |
||||||
|
"initial", // 初始化状态
|
||||||
|
"pending", // 等待上传
|
||||||
|
"uploading", // 正在上传
|
||||||
|
"uploaded", // 上传完成
|
||||||
|
"errored", // 上传失败
|
||||||
|
"aborted", // 终止上传
|
||||||
|
]; |
||||||
|
|
||||||
|
export class Uploader implements AbortionContext { |
||||||
|
ctrl: AbortController; |
||||||
|
|
||||||
|
skipUpload?: boolean; |
||||||
|
uploadToken?: string; |
||||||
|
key?: string; |
||||||
|
|
||||||
|
status: JobStatus; |
||||||
|
|
||||||
|
protected constructor( |
||||||
|
readonly file: File, |
||||||
|
readonly hash: FileHash, |
||||||
|
readonly tasks: Array<Task>, |
||||||
|
) { |
||||||
|
this.ctrl = abortion(this); |
||||||
|
this.status = "initial"; |
||||||
|
} |
||||||
|
|
||||||
|
get signal(): AbortSignal { |
||||||
|
return this.ctrl.signal; |
||||||
|
} |
||||||
|
|
||||||
|
get isUploaded(): boolean { |
||||||
|
return this.status === "uploaded"; |
||||||
|
} |
||||||
|
|
||||||
|
get isValid(): boolean { |
||||||
|
return statuses.indexOf(this.status) < statuses.indexOf("uploaded"); |
||||||
|
} |
||||||
|
|
||||||
|
get isInvalid(): boolean { |
||||||
|
return statuses.indexOf(this.status) > statuses.indexOf("uploaded"); |
||||||
|
} |
||||||
|
|
||||||
|
static create( |
||||||
|
file: File, |
||||||
|
task: Task, |
||||||
|
callback: (duplicated: boolean) => void, |
||||||
|
): void { |
||||||
|
let job = registry.get(task.hash); |
||||||
|
if (job == null) { |
||||||
|
job = new Uploader(file, task.hash, [task]); |
||||||
|
registry.set(job.hash, job); |
||||||
|
callback(false); |
||||||
|
queueMicrotask(job.start.bind(job)); |
||||||
|
return; |
||||||
|
} |
||||||
|
const exists = job.tasks.some((t) => isSameTask(t, task)); |
||||||
|
if (exists) { |
||||||
|
callback(true); // 任务重复
|
||||||
|
return; |
||||||
|
} |
||||||
|
job.tasks.push(task); |
||||||
|
callback(false); |
||||||
|
if (!job.isValid) { |
||||||
|
void job.resume(task.id); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async pause(taskId: TaskId): Promise<void> { |
||||||
|
const task = this.tasks.find((t) => t.id === taskId); |
||||||
|
if (!task) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 只能中止状态正常的任务
|
||||||
|
if (isValid(task)) { |
||||||
|
task.status = "aborted"; |
||||||
|
task.error = "已暂停"; |
||||||
|
await report("task", "sync", {...task}); |
||||||
|
} |
||||||
|
// 如果没有正常的任务,就中止文件上传
|
||||||
|
if (this.isValid && !this.signal.aborted && !this.tasks.some(isValid)) { |
||||||
|
this.ctrl.abort(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 恢复上传任务
|
||||||
|
async resume(taskId: TaskId): Promise<void> { |
||||||
|
// 获取前端任务
|
||||||
|
const task = this.tasks.find((t) => t.id === taskId); |
||||||
|
if (task == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 恢复任务
|
||||||
|
if (isInvalid(task)) { |
||||||
|
task.status = "readend"; |
||||||
|
task.error = undefined; |
||||||
|
task.progress = 1; |
||||||
|
await report("task", "sync", {...task}); |
||||||
|
} |
||||||
|
// 确保中断器可用
|
||||||
|
if (this.signal.aborted) { |
||||||
|
this.ctrl = abortion(this); |
||||||
|
} |
||||||
|
// 启动任务
|
||||||
|
if (this.isInvalid) { |
||||||
|
await this.start(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 移除任务
|
||||||
|
async destroy(taskId: TaskId): Promise<void> { |
||||||
|
const index = this.tasks.findIndex((t) => t.id === taskId); |
||||||
|
if (index > -1) { |
||||||
|
const task = this.tasks.splice(index, 1)[0]; |
||||||
|
URL.revokeObjectURL(task.path); // 释放内存
|
||||||
|
await report("task", "delete", [taskId]); |
||||||
|
} |
||||||
|
// 如果没有正常的任务,就中止文件上传
|
||||||
|
if (!this.signal.aborted && !this.tasks.some(isValid)) { |
||||||
|
this.ctrl.abort(); |
||||||
|
} |
||||||
|
// 若没有前端任务,我们就删除文件上传任务
|
||||||
|
if (!this.tasks.length) { |
||||||
|
registry.delete(this.hash); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async cleanup() { |
||||||
|
let hasValidTask = false; |
||||||
|
const deletes: Array<TaskId> = []; |
||||||
|
for (let i = this.tasks.length - 1; i >= 0; i--) { |
||||||
|
const task = this.tasks[i]; |
||||||
|
if (!isValid(task)) { |
||||||
|
this.tasks.splice(i, 1); // 删除任务
|
||||||
|
URL.revokeObjectURL(task.path); // 释放内存
|
||||||
|
deletes.push(task.id); |
||||||
|
} else { |
||||||
|
hasValidTask = true; |
||||||
|
} |
||||||
|
} |
||||||
|
// 如果没有正常的前端任务,就中止后端任务
|
||||||
|
if (!this.signal.aborted && !hasValidTask && !this.isInvalid) { |
||||||
|
this.ctrl.abort(); |
||||||
|
} |
||||||
|
// 如果所有的前端任务都被删除了,就同时把后端任务也删除掉。
|
||||||
|
if (!this.tasks.length) { |
||||||
|
registry.delete(this.hash); |
||||||
|
} |
||||||
|
// 广播:通知前端删除任务。
|
||||||
|
await report("task", "delete", deletes); |
||||||
|
} |
||||||
|
|
||||||
|
async onabort(_?: Event): Promise<void> { |
||||||
|
await this.report("aborted", { |
||||||
|
status: "aborted", |
||||||
|
error: this.signal.reason || "已暂停", |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async report( |
||||||
|
status: JobStatus, |
||||||
|
info: { error?: any; progress?: number; status: TaskStatus }, |
||||||
|
): Promise<void> { |
||||||
|
this.status = status; |
||||||
|
let error = coerce(info.error); |
||||||
|
if (error != null) error = String(error); |
||||||
|
for (const task of this.tasks) { |
||||||
|
if (isValid(task)) { |
||||||
|
task.status = info.status; |
||||||
|
task.error = error; |
||||||
|
task.progress = info.progress; |
||||||
|
await report("task", "sync", task); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async start(): Promise<void> { |
||||||
|
await this.report("initial", {status: "queuing"}); |
||||||
|
await this.getUploadOptions(); |
||||||
|
await this.startUpload(); |
||||||
|
await this.completeUpload(); |
||||||
|
} |
||||||
|
|
||||||
|
private async getUploadOptions(): Promise<void> { |
||||||
|
if (this.skipUpload || this.uploadToken || this.signal.aborted) { |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
const result = await initiateUploadApi( |
||||||
|
this.file.type, |
||||||
|
this.hash, |
||||||
|
this.signal, |
||||||
|
); |
||||||
|
this.skipUpload = result.skip; |
||||||
|
this.uploadToken = (result as UploadArgs).token; |
||||||
|
this.key = (result as UploadArgs).key; |
||||||
|
this.signal.throwIfAborted(); |
||||||
|
} catch (error) { |
||||||
|
if (!this.signal.aborted) { |
||||||
|
await this.report("errored", { |
||||||
|
status: "failed", |
||||||
|
error, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async startUpload(): Promise<void> { |
||||||
|
// 任务被中止或者状态错误
|
||||||
|
if (this.signal.aborted || this.isUploaded) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 跳过文件上传
|
||||||
|
if (this.skipUpload) { |
||||||
|
await this.report("uploaded", { |
||||||
|
status: "uploaded", |
||||||
|
progress: 1, |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 没有状态正常的任务,就不需要上传
|
||||||
|
if (!this.tasks.some(isValid)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const {resolve, promise} = promisify<void>(); |
||||||
|
const observable = upload( |
||||||
|
this.file, |
||||||
|
this.key, |
||||||
|
this.uploadToken!, |
||||||
|
{ |
||||||
|
fname: this.file.name, // 文件原文件名
|
||||||
|
customVars: {"x:md5": this.hash}, // 自定义变量
|
||||||
|
mimeType: this.file.type, // 文件类型设置
|
||||||
|
}, |
||||||
|
{ |
||||||
|
useCdnDomain: false, |
||||||
|
checkByServer: true, |
||||||
|
checkByMD5: true, |
||||||
|
}, |
||||||
|
); |
||||||
|
const subscription = observable.subscribe({ |
||||||
|
next: (res: UploadProgress) => { |
||||||
|
this.report("uploading", { |
||||||
|
status: "uploading", |
||||||
|
progress: res.total.percent / 100, |
||||||
|
}); |
||||||
|
}, |
||||||
|
error: async (error: any) => { |
||||||
|
if (this.signal.aborted) { |
||||||
|
resolve(); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 如果文件已经存在,则模拟七牛云回调,
|
||||||
|
// 这样就可以保证文件被正确上传。
|
||||||
|
if ( |
||||||
|
error instanceof QiniuRequestError && |
||||||
|
error.data?.error === "file exists" |
||||||
|
) { |
||||||
|
try { |
||||||
|
await simulateQiniuCallback( |
||||||
|
{ |
||||||
|
md5: this.hash, |
||||||
|
name: this.file.name, |
||||||
|
mime: this.file.type, |
||||||
|
size: this.file.size, |
||||||
|
key: this.key!, |
||||||
|
}, |
||||||
|
this.signal, |
||||||
|
); |
||||||
|
this.signal.throwIfAborted(); |
||||||
|
await this.report("uploaded", { |
||||||
|
status: "uploaded", |
||||||
|
progress: 1, |
||||||
|
}); |
||||||
|
resolve(); |
||||||
|
return; |
||||||
|
} catch (err) { |
||||||
|
error = err; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!this.signal.aborted) { |
||||||
|
if (error instanceof QiniuNetworkError) { |
||||||
|
await this.report("errored", { |
||||||
|
status: "failed", |
||||||
|
error: "网络异常", |
||||||
|
}); |
||||||
|
} else { |
||||||
|
await this.report("errored", { |
||||||
|
status: "failed", |
||||||
|
error |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
resolve(); |
||||||
|
}, |
||||||
|
complete: async (_: any) => { |
||||||
|
// 参数 _ 是七牛云返回我们服务器返回的结果
|
||||||
|
await this.report("uploaded", { |
||||||
|
status: "uploaded", |
||||||
|
progress: 1, |
||||||
|
}); |
||||||
|
resolve(); |
||||||
|
}, |
||||||
|
}); |
||||||
|
const dispose = (): void => { |
||||||
|
if (!subscription.closed) { |
||||||
|
subscription.unsubscribe(); |
||||||
|
} |
||||||
|
}; |
||||||
|
// 当信号被中止时取消文件上传
|
||||||
|
this.signal.addEventListener("abort", dispose); |
||||||
|
try { |
||||||
|
await promise; |
||||||
|
} finally { |
||||||
|
this.signal.removeEventListener("abort", dispose); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async completeUpload(): Promise<void> { |
||||||
|
if (this.signal.aborted || !this.isUploaded) { |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
const fileNames: Array<string> = []; |
||||||
|
const directories: Array<number> = []; |
||||||
|
// const removes: Array<TaskId> = [];
|
||||||
|
for (const task of this.tasks) { |
||||||
|
fileNames.push(task.name); |
||||||
|
directories.push(task.dirid); |
||||||
|
// removes.push(task.id);
|
||||||
|
} |
||||||
|
await completeUploadApi(this.hash, fileNames, directories, this.signal); |
||||||
|
await this.report("uploaded", {status: "completed"}); |
||||||
|
} catch (error) { |
||||||
|
if (!this.signal.aborted) { |
||||||
|
await this.report("errored", {status: "failed", error}); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
await this.report("uploaded", { |
||||||
|
status: "completed", |
||||||
|
progress: 1, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1 @@ |
|||||||
|
export * from './vTooltip' |
@ -0,0 +1,69 @@ |
|||||||
|
import type {DirectiveBinding, ObjectDirective} from 'vue' |
||||||
|
import {alignWithElement} from '../utils' |
||||||
|
|
||||||
|
interface Elm extends Element { |
||||||
|
v$tooltip?: HTMLDivElement |
||||||
|
} |
||||||
|
|
||||||
|
const getTooltip = (el: Elm): HTMLDivElement => { |
||||||
|
if (el.v$tooltip != null) { |
||||||
|
return el.v$tooltip |
||||||
|
} |
||||||
|
|
||||||
|
const tooltip = document.createElement('div') |
||||||
|
tooltip.style.display = "none" |
||||||
|
tooltip.classList.add('tooltip') |
||||||
|
|
||||||
|
el.addEventListener("mouseenter", () => { |
||||||
|
if (tooltip.parentNode) { |
||||||
|
tooltip.style.display = "block" |
||||||
|
alignWithElement(el, tooltip, "top-center", [0, -6]) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
el.addEventListener("mouseleave", () => { |
||||||
|
if (tooltip.parentNode) { |
||||||
|
tooltip.style.display = "none" |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
el.v$tooltip = tooltip |
||||||
|
|
||||||
|
return tooltip |
||||||
|
} |
||||||
|
|
||||||
|
const stringify = (value: any) => { |
||||||
|
if (value == null) { |
||||||
|
return value |
||||||
|
} else if (typeof value === 'string') { |
||||||
|
return value.trim() |
||||||
|
} else { |
||||||
|
return value.toString().trim() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function destroy(elm: Elm) { |
||||||
|
if (elm.v$tooltip) { |
||||||
|
elm.v$tooltip.remove() |
||||||
|
delete elm.v$tooltip |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function bind(el: Elm, {value}: DirectiveBinding) { |
||||||
|
const text = stringify(value) |
||||||
|
if (text) { |
||||||
|
const tooltip = getTooltip(el) |
||||||
|
if (!tooltip.parentNode) { |
||||||
|
el.closest('[data-ui-barrier]')?.appendChild(tooltip) |
||||||
|
} |
||||||
|
tooltip.innerHTML = text |
||||||
|
} else { |
||||||
|
destroy(el) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const vTooltip: ObjectDirective<Elm> = { |
||||||
|
mounted: bind, |
||||||
|
updated: bind, |
||||||
|
beforeUnmount: destroy, |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export * from './useTheme' |
@ -0,0 +1,91 @@ |
|||||||
|
import { |
||||||
|
computed, |
||||||
|
ComputedRef, |
||||||
|
getCurrentInstance, |
||||||
|
getCurrentScope, |
||||||
|
inject, |
||||||
|
InjectionKey, |
||||||
|
onScopeDispose, |
||||||
|
provide, |
||||||
|
Ref, |
||||||
|
ref, |
||||||
|
watchEffect |
||||||
|
} from 'vue' |
||||||
|
|
||||||
|
export interface UseTheme { |
||||||
|
use: Ref<'dark' | 'light' | 'system'> |
||||||
|
dark: ComputedRef<boolean> |
||||||
|
light: ComputedRef<boolean> |
||||||
|
} |
||||||
|
|
||||||
|
const key: InjectionKey<UseTheme | undefined> = Symbol("useTheme") |
||||||
|
|
||||||
|
export function useTheme(): UseTheme { |
||||||
|
let instance = inject(key, undefined) |
||||||
|
if (instance != null) { |
||||||
|
return instance |
||||||
|
} |
||||||
|
|
||||||
|
const use = ref<'dark' | 'light' | 'system'>('light') |
||||||
|
const matches = ref(false) |
||||||
|
const dark = computed(() => { |
||||||
|
if (use.value === 'dark') return true |
||||||
|
if (use.value === 'light') return false |
||||||
|
return matches.value |
||||||
|
}) |
||||||
|
const light = computed(() => { |
||||||
|
if (use.value === 'light') return true |
||||||
|
if (use.value === 'dark') return false |
||||||
|
return !matches.value |
||||||
|
}) |
||||||
|
|
||||||
|
const handler = (event: MediaQueryListEvent) => { |
||||||
|
matches.value = event.matches |
||||||
|
} |
||||||
|
|
||||||
|
let mediaQuery: MediaQueryList | undefined |
||||||
|
|
||||||
|
const cleanup = () => { |
||||||
|
if (mediaQuery) { |
||||||
|
if ('removeEventListener' in mediaQuery) { |
||||||
|
mediaQuery.removeEventListener('change', handler) |
||||||
|
} else { |
||||||
|
// @ts-expect-error deprecated API
|
||||||
|
mediaQuery.removeListener(handler) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const stopWatch = watchEffect(() => { |
||||||
|
if ('matchMedia' in window && typeof window.matchMedia === 'function') { |
||||||
|
cleanup() |
||||||
|
|
||||||
|
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') |
||||||
|
|
||||||
|
if ('addEventListener' in mediaQuery) { |
||||||
|
mediaQuery.addEventListener('change', handler) |
||||||
|
} else { |
||||||
|
// @ts-expect-error deprecated API
|
||||||
|
mediaQuery.addListener(handler) |
||||||
|
} |
||||||
|
|
||||||
|
matches.value = mediaQuery.matches |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
if (getCurrentScope()) { |
||||||
|
onScopeDispose(() => { |
||||||
|
stopWatch() |
||||||
|
cleanup() |
||||||
|
mediaQuery = undefined |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
instance = {use, dark, light} |
||||||
|
|
||||||
|
if (getCurrentInstance()) { |
||||||
|
provide(key, instance) |
||||||
|
} |
||||||
|
|
||||||
|
return instance |
||||||
|
} |
@ -0,0 +1,225 @@ |
|||||||
|
import {store} from './store' |
||||||
|
import {installIcons} from './widgets/VIcons' |
||||||
|
import UiContainer from './widgets/UiContainer.vue' |
||||||
|
import VDialog from './widgets/UiDialog.vue' |
||||||
|
import { |
||||||
|
type CSSProperties, |
||||||
|
defineCustomElement, |
||||||
|
getCurrentInstance, |
||||||
|
h, |
||||||
|
nextTick, |
||||||
|
onBeforeMount, |
||||||
|
onMounted, |
||||||
|
onUnmounted, |
||||||
|
type Prop, |
||||||
|
reactive, |
||||||
|
ref, |
||||||
|
type VNode, |
||||||
|
VueElementConstructor, |
||||||
|
watch |
||||||
|
} from 'vue' |
||||||
|
import style from './style.css?inline' |
||||||
|
|
||||||
|
let FileManager: VueElementConstructor |
||||||
|
|
||||||
|
if (!customElements.get("file-manager")) { |
||||||
|
FileManager = defineCustomElement({ |
||||||
|
props: { |
||||||
|
select: { |
||||||
|
type: String, |
||||||
|
validator: v => v == null || v === 'single' || v === 'multiple', |
||||||
|
} as Prop<'single' | 'multiple'>, |
||||||
|
mime: { |
||||||
|
type: String, |
||||||
|
validator: v => v == null || v === 'image' || v === 'video' || v === 'all', |
||||||
|
} as Prop<FileMime>, |
||||||
|
apiUrl: { |
||||||
|
type: String, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
artifact: { |
||||||
|
type: String, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
accessToken: { |
||||||
|
type: String, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
dialog: Boolean, |
||||||
|
width: Number, |
||||||
|
height: Number, |
||||||
|
zIndex: Number |
||||||
|
}, |
||||||
|
emits: [ |
||||||
|
'close', |
||||||
|
'select', |
||||||
|
'open', |
||||||
|
'fail', |
||||||
|
], |
||||||
|
setup(props, ctx) { |
||||||
|
const shadowRoot = ref<ShadowRoot | null>(null) |
||||||
|
const visible = ref(false) |
||||||
|
const size = reactive<CSSProperties>({}) |
||||||
|
|
||||||
|
watch(() => props.select, value => { |
||||||
|
store.mode = value ? 'select' : 'manage' |
||||||
|
store.multiple = value === 'multiple' |
||||||
|
}, {immediate: true}) |
||||||
|
|
||||||
|
watch(() => props.mime, value => { |
||||||
|
store.mime = value ?? 'all' |
||||||
|
store.lockMime = value != null |
||||||
|
}, {immediate: true}) |
||||||
|
|
||||||
|
const attachDialog = (root: ShadowRoot) => { |
||||||
|
if (!props.dialog) return |
||||||
|
const style = document.createElement('style') |
||||||
|
style.textContent = `:host{position:fixed;inset:0;z-index:${props.zIndex ?? 100}` |
||||||
|
root.insertBefore(style, root.firstChild) |
||||||
|
} |
||||||
|
|
||||||
|
const handleWindowResize = () => { |
||||||
|
let {width, height} = props |
||||||
|
if (width == null || width < 100) { |
||||||
|
width = window.innerWidth * 0.8 |
||||||
|
} |
||||||
|
if (height == null || height < 100) { |
||||||
|
height = window.innerHeight * 0.8 |
||||||
|
} |
||||||
|
size.width = `${Math.max(width, 875)}px` |
||||||
|
size.height = `${Math.max(height, 640)}px` |
||||||
|
} |
||||||
|
|
||||||
|
onBeforeMount(handleWindowResize) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
const el = getCurrentInstance()?.vnode.el as HTMLElement |
||||||
|
const root = el.parentNode as ShadowRoot |
||||||
|
installIcons(root) |
||||||
|
attachDialog(root) |
||||||
|
shadowRoot.value = root |
||||||
|
void nextTick(() => { |
||||||
|
visible.value = true |
||||||
|
}) |
||||||
|
window.addEventListener('resize', handleWindowResize) |
||||||
|
}) |
||||||
|
|
||||||
|
onUnmounted(() => { |
||||||
|
window.removeEventListener('resize', handleWindowResize) |
||||||
|
}) |
||||||
|
|
||||||
|
const createManager = () => h(UiContainer, { |
||||||
|
setup() { |
||||||
|
return { |
||||||
|
apiUrl: props.apiUrl, |
||||||
|
accessToken: props.accessToken, |
||||||
|
artifact: props.artifact, |
||||||
|
} |
||||||
|
}, |
||||||
|
onClose() { |
||||||
|
ctx.emit('close') |
||||||
|
}, |
||||||
|
onSelect(files: CloudFile[]) { |
||||||
|
ctx.emit('select', files) |
||||||
|
}, |
||||||
|
onFail(message: string) { |
||||||
|
ctx.emit('fail', message) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return () => { |
||||||
|
let child: VNode |
||||||
|
if (props.dialog && shadowRoot.value) { |
||||||
|
child = h(VDialog, { |
||||||
|
barrier: shadowRoot.value as unknown as Element, |
||||||
|
visible: visible.value, |
||||||
|
class: 'shadow-2xl', |
||||||
|
style: size, |
||||||
|
rounded: false, |
||||||
|
outside: 'shake', |
||||||
|
onDismissed() { |
||||||
|
ctx.emit('close') |
||||||
|
}, |
||||||
|
onCompleted() { |
||||||
|
ctx.emit('open') |
||||||
|
} |
||||||
|
}, { |
||||||
|
default: createManager, |
||||||
|
}) |
||||||
|
} else { |
||||||
|
child = createManager() |
||||||
|
} |
||||||
|
return h('div', { |
||||||
|
ref: shadowRoot, |
||||||
|
class: ['size-full', {'fixed': props.dialog}] |
||||||
|
}, [child]) |
||||||
|
} |
||||||
|
}, |
||||||
|
styles: [style], |
||||||
|
}) |
||||||
|
customElements.define("file-manager", FileManager) |
||||||
|
} else { |
||||||
|
FileManager = customElements.get('file-manager') as VueElementConstructor |
||||||
|
} |
||||||
|
|
||||||
|
export { |
||||||
|
FileManager |
||||||
|
} |
||||||
|
|
||||||
|
interface ShowFileManagerOptions { |
||||||
|
select?: 'single' | 'multiple', |
||||||
|
mime?: FileMime |
||||||
|
apiUrl: string |
||||||
|
accessToken: string |
||||||
|
artifact: string |
||||||
|
zIndex?: number // with dialog
|
||||||
|
width?: number // with dialog
|
||||||
|
height?: number // with dialog
|
||||||
|
onClose?: VoidFunction // with dialog
|
||||||
|
onSelect?: (files: CloudFile[]) => void // with dialog
|
||||||
|
onOpen?: VoidFunction // with dialog
|
||||||
|
onFail?: (error: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
export function showFileManager(opts: ShowFileManagerOptions) { |
||||||
|
let elm: Element | null = new FileManager({ |
||||||
|
select: opts.select, |
||||||
|
mime: opts.mime, |
||||||
|
apiUrl: opts.apiUrl, |
||||||
|
accessToken: opts.accessToken, |
||||||
|
artifact: opts.artifact, |
||||||
|
dialog: true, |
||||||
|
zIndex: opts.zIndex, |
||||||
|
width: opts.width, |
||||||
|
height: opts.height, |
||||||
|
}) |
||||||
|
|
||||||
|
const dismiss = () => { |
||||||
|
if (elm != null) { |
||||||
|
elm.remove() |
||||||
|
elm = null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
elm.addEventListener('close', () => { |
||||||
|
opts.onClose?.() |
||||||
|
dismiss() |
||||||
|
}) |
||||||
|
|
||||||
|
elm.addEventListener('select', evt => { |
||||||
|
opts.onSelect?.((evt as CustomEvent).detail as CloudFile[]) |
||||||
|
dismiss() |
||||||
|
}) |
||||||
|
|
||||||
|
elm.addEventListener('open', () => { |
||||||
|
opts.onOpen?.() |
||||||
|
}) |
||||||
|
|
||||||
|
elm.addEventListener('fail', evt => { |
||||||
|
opts.onFail?.((evt as CustomEvent).detail) |
||||||
|
}) |
||||||
|
|
||||||
|
document.body.appendChild(elm) |
||||||
|
|
||||||
|
return dismiss |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
import {$on} from '../../shared' |
||||||
|
import {useFile} from './handlers' |
||||||
|
import {store} from './store' |
||||||
|
import {toast} from "../widgets/UiToastController.ts"; |
||||||
|
|
||||||
|
// 接收上传任务的信息同步命令
|
||||||
|
$on("task", "sync", (task: Task): void => { |
||||||
|
const old = store.tasks.find((t) => t.id === task.id); |
||||||
|
if (old == null) { |
||||||
|
store.tasks.push(task); |
||||||
|
} else { |
||||||
|
Object.assign(old, task); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 接收上传任务的删除命令
|
||||||
|
$on("task", "delete", (deletes: Array<TaskId>): void => { |
||||||
|
for (const id of deletes) { |
||||||
|
const i = store.tasks.findIndex((t) => t.id === id); |
||||||
|
i > -1 && store.tasks.splice(i, 1); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
$on('file', 'create', useFile) |
||||||
|
$on('file', 'update', useFile) |
||||||
|
|
||||||
|
let online = false |
||||||
|
|
||||||
|
$on('app', 'status', (status) => { |
||||||
|
if (status === 'online' && !online) { |
||||||
|
online = true |
||||||
|
} else if (status === 'offline' && online) { |
||||||
|
online = false |
||||||
|
toast('warn', '后台不在线,上传文件无法同步显示') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
export {} |
@ -0,0 +1,112 @@ |
|||||||
|
import {dispatch} from '../worker.ts' |
||||||
|
import {coerce, off, on, sleep} from '../../shared' |
||||||
|
import {getCurrentScope, nextTick, onScopeDispose, reactive, watch} from 'vue' |
||||||
|
import {store} from './store' |
||||||
|
|
||||||
|
export interface Filer { |
||||||
|
loading: boolean |
||||||
|
limit: number |
||||||
|
page: number |
||||||
|
total: number |
||||||
|
error?: string |
||||||
|
dirId: number |
||||||
|
view: |
||||||
|
| "error" |
||||||
|
| "files" |
||||||
|
| "empty" |
||||||
|
| "loader" |
||||||
|
} |
||||||
|
|
||||||
|
export function useFiler(directoryId: number): Filer { |
||||||
|
const filer = reactive<Filer>({ |
||||||
|
loading: false, |
||||||
|
limit: 30, |
||||||
|
page: 0, |
||||||
|
total: 0, |
||||||
|
dirId: directoryId, |
||||||
|
error: undefined, |
||||||
|
view: "loader", |
||||||
|
}) |
||||||
|
|
||||||
|
const jump = async (to: number) => { |
||||||
|
if (filer.total < 1 || filer.view === "error") { |
||||||
|
filer.view = "loader" |
||||||
|
} |
||||||
|
|
||||||
|
filer.loading = true |
||||||
|
filer.page = to |
||||||
|
filer.error = undefined |
||||||
|
|
||||||
|
const mime = store.mime === "all" ? undefined : store.mime |
||||||
|
const {limit, page} = filer |
||||||
|
try { |
||||||
|
const res = await dispatch('file', "list", { |
||||||
|
directoryId, |
||||||
|
page, |
||||||
|
limit, |
||||||
|
mime |
||||||
|
}) |
||||||
|
if (filer.page === res.page && filer.limit === res.limit) { |
||||||
|
filer.loading = false |
||||||
|
filer.total = res.total |
||||||
|
filer.view = res.items?.length ? 'files' : 'empty' |
||||||
|
store.files[directoryId] = res.items ?? [] // 这里会不会是响应式的
|
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
if (filer.page === page && filer.limit === limit) { |
||||||
|
filer.view = "error" |
||||||
|
filer.error = String(coerce(error)) |
||||||
|
filer.loading = false |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const refresh = async (): Promise<void> => { |
||||||
|
filer.loading = true |
||||||
|
if (filer.page == 0) { |
||||||
|
filer.page = 1 |
||||||
|
} |
||||||
|
await sleep(300) |
||||||
|
await jump(filer.page) |
||||||
|
} |
||||||
|
|
||||||
|
const reload = async (): Promise<void> => { |
||||||
|
filer.loading = true |
||||||
|
filer.view = "loader" |
||||||
|
await sleep(300) |
||||||
|
await jump(filer.page) |
||||||
|
} |
||||||
|
|
||||||
|
const reset = async (): Promise<void> => { |
||||||
|
filer.page = 0 |
||||||
|
filer.total = 0 |
||||||
|
delete store.files[directoryId] |
||||||
|
await jump(1) |
||||||
|
} |
||||||
|
|
||||||
|
watch([ |
||||||
|
() => filer.limit, |
||||||
|
() => store.mime, |
||||||
|
], reset) |
||||||
|
|
||||||
|
if (getCurrentScope()) { |
||||||
|
on(`files:${directoryId}:refresh`, refresh) |
||||||
|
on(`files:${directoryId}:reload`, reload) |
||||||
|
on(`files:${directoryId}:reset`, reset) |
||||||
|
on(`files:${directoryId}:jump`, jump) |
||||||
|
on("mime:change", reset) |
||||||
|
|
||||||
|
void nextTick(reset) |
||||||
|
|
||||||
|
onScopeDispose(() => { |
||||||
|
off(`files:${directoryId}:refresh`, refresh) |
||||||
|
off(`files:${directoryId}:reload`, reload) |
||||||
|
off(`files:${directoryId}:reset`, reset) |
||||||
|
off(`files:${directoryId}:jump`, jump) |
||||||
|
off("mime:change", reset) |
||||||
|
delete store.files[directoryId] |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return filer |
||||||
|
} |
@ -0,0 +1,204 @@ |
|||||||
|
import {toast} from '../widgets/UiToastController' |
||||||
|
import {dispatch} from '../worker' |
||||||
|
import {store} from './store' |
||||||
|
|
||||||
|
export function getDirectoryEntry(id: number): CloudDirectory | undefined { |
||||||
|
if (id === 0) return store.rootDirectory; |
||||||
|
return store.directories.get(id); |
||||||
|
} |
||||||
|
|
||||||
|
export function hasChildDirectoryEntries(id: number) { |
||||||
|
return id > 0 && Array.from(store.directories.values()).some((d) => d.pid === id); |
||||||
|
} |
||||||
|
|
||||||
|
export async function refreshDirectories() { |
||||||
|
const dirs = await dispatch("dir", "all"); |
||||||
|
const set = (dirs: CloudDirectory[] | undefined) => { |
||||||
|
if (dirs == null) return; |
||||||
|
for (const {children, ...dir} of dirs) { |
||||||
|
store.directories.set(dir.id, dir); |
||||||
|
set(children); |
||||||
|
} |
||||||
|
}; |
||||||
|
store.directories.clear() |
||||||
|
set(dirs); |
||||||
|
if (store.activeDirectoryId > 0 && !store.directories.has(store.activeDirectoryId)) { |
||||||
|
store.activeDirectoryId = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function reload(): Promise<void> { |
||||||
|
return new Promise<void>((resolve) => { |
||||||
|
store.activeDirectoryId = 0; |
||||||
|
store.files = {}; |
||||||
|
store.directories.clear(); |
||||||
|
queueMicrotask(() => { |
||||||
|
refreshDirectories() |
||||||
|
.catch(err => toast("error", err)) |
||||||
|
.then(resolve) |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export async function uploadFiles(dirid: number, files: FileList | File[] | File): Promise<void> { |
||||||
|
if (files instanceof File) { |
||||||
|
await dispatch('task', 'create', {dirid, file: files}) |
||||||
|
} else { |
||||||
|
for (const file of files) { |
||||||
|
await dispatch('task', "create", {dirid, file}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function deleteFile(file: CloudFile): void { |
||||||
|
const list = store.files[file.directoryId] |
||||||
|
const index = list?.findIndex(f => f.id === file.id) |
||||||
|
if (list && index != null && index > -1) { |
||||||
|
list.splice(index, 1) |
||||||
|
} |
||||||
|
removeSelected(file.id) |
||||||
|
} |
||||||
|
|
||||||
|
export function updateFile(file: CloudFile): void { |
||||||
|
const list = store.files[file.directoryId] |
||||||
|
const index = list?.findIndex(f => f.id === file.id) |
||||||
|
if (list && index != null && index > -1) { |
||||||
|
list.splice(index, 1, file) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function useFile(list: CloudFile | Array<CloudFile>) { |
||||||
|
if (!Array.isArray(list)) { |
||||||
|
list = [list] |
||||||
|
} |
||||||
|
for (let file of list) { |
||||||
|
console.log(file.id, file.object.status, file.name) |
||||||
|
const files = store.files[file.directoryId] ?? [] |
||||||
|
const index = files.findIndex(f => f.id === file.id) |
||||||
|
if (index === -1) { |
||||||
|
files.push(file) |
||||||
|
} else if (files[index].object.status < file.object.status) { |
||||||
|
files.splice(index, 1, file) |
||||||
|
} |
||||||
|
store.files[file.directoryId] = files |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function toggleSelect(file: CloudFile): void { |
||||||
|
if (store.mode === 'manage' || store.multiple) { |
||||||
|
if (!removeSelected(file.id)) { |
||||||
|
store.selected.push({...file}) |
||||||
|
} |
||||||
|
} else if (store.selected.length && store.selected[0].id === file.id) { |
||||||
|
store.selected = [] |
||||||
|
} else { |
||||||
|
store.selected = [file] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function removeSelected(id: number): boolean { |
||||||
|
const index = store.selected.findIndex(f => f.id === id) |
||||||
|
if (index > -1) { |
||||||
|
store.selected.splice(index, 1) |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Feature detection. The API needs to be supported
|
||||||
|
// and the app not run in an iframe.
|
||||||
|
const supportsFileSystemAccess = |
||||||
|
"showOpenFilePicker" in window && |
||||||
|
(() => { |
||||||
|
try { |
||||||
|
return window.self === window.top; |
||||||
|
} catch { |
||||||
|
return false; |
||||||
|
} |
||||||
|
})(); |
||||||
|
|
||||||
|
export async function pickFiles(directoryId: number) { |
||||||
|
// If the File System Access API is supported…
|
||||||
|
if (supportsFileSystemAccess) { |
||||||
|
try { |
||||||
|
// Show the file picker, optionally allowing multiple files.
|
||||||
|
// @ts-ignore
|
||||||
|
const handles = await self.showOpenFilePicker({ |
||||||
|
multiple: true, |
||||||
|
excludeAcceptAllOption: true, |
||||||
|
types: [ |
||||||
|
{ |
||||||
|
description: "图片和视频", |
||||||
|
accept: { |
||||||
|
"image/*": [ |
||||||
|
".apng", |
||||||
|
".avif", |
||||||
|
".gif", |
||||||
|
".jpeg", ".jpg", ".jfif", ".pjpeg", ".pjp", |
||||||
|
".png", |
||||||
|
".webp", |
||||||
|
".bmp", |
||||||
|
".ico", ".cur", |
||||||
|
".tif", ".tiff", |
||||||
|
], |
||||||
|
"video/*": [ |
||||||
|
".3gp", |
||||||
|
".mpeg", ".mpg", |
||||||
|
".mp4", ".m4v", ".m4p", |
||||||
|
".ogv", ".ogg", |
||||||
|
".mov", ".qt", |
||||||
|
".webm", |
||||||
|
".avi", |
||||||
|
], |
||||||
|
} |
||||||
|
} |
||||||
|
], |
||||||
|
}); |
||||||
|
|
||||||
|
const files = await Promise.all( |
||||||
|
handles.map(async (handle: FileSystemFileHandle) => { |
||||||
|
const file = await handle.getFile(); |
||||||
|
// Add the `FileSystemFileHandle` as `.handle`.
|
||||||
|
// @ts-ignore
|
||||||
|
file.handle = handle; |
||||||
|
return file; |
||||||
|
}) |
||||||
|
); |
||||||
|
|
||||||
|
await uploadFiles(directoryId, files) |
||||||
|
|
||||||
|
} catch (err) { |
||||||
|
// Fail silently if the user has simply canceled the dialog.
|
||||||
|
if ((err as Error).name !== 'AbortError') { |
||||||
|
// console.error(err.name, err.message);
|
||||||
|
throw err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Fallback if the File System Access API is not supported.
|
||||||
|
return new Promise<void>((resolve) => { |
||||||
|
const input = document.createElement('input'); |
||||||
|
input.style.display = 'none'; |
||||||
|
input.type = 'file'; |
||||||
|
input.accept = "image/*,video/*" |
||||||
|
document.body.append(input); |
||||||
|
input.multiple = true; |
||||||
|
input.addEventListener('change', () => { |
||||||
|
input.remove(); |
||||||
|
if (!input.files) { |
||||||
|
return; |
||||||
|
} |
||||||
|
uploadFiles(directoryId, input.files) |
||||||
|
resolve() |
||||||
|
}); |
||||||
|
// Show the picker.
|
||||||
|
if ('showPicker' in HTMLInputElement.prototype) { |
||||||
|
input.showPicker(); |
||||||
|
} else { |
||||||
|
input.click(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
export * from './events' |
||||||
|
export * from './filter' |
||||||
|
export * from './handlers' |
||||||
|
export * from './store' |
@ -0,0 +1,52 @@ |
|||||||
|
import {computed, reactive} from 'vue' |
||||||
|
|
||||||
|
export interface Store { |
||||||
|
showSidebar: boolean; |
||||||
|
showTasksPanel: boolean; |
||||||
|
isDragging: boolean; |
||||||
|
mime: FileMime |
||||||
|
lockMime: boolean |
||||||
|
mode: FileMode |
||||||
|
multiple: boolean |
||||||
|
tasks: Array<Task>; |
||||||
|
rootDirectory: CloudDirectory; |
||||||
|
directories: Map<number, CloudDirectory>; |
||||||
|
activeDirectoryId: number; |
||||||
|
// files: Map<number, CloudFile[]>;
|
||||||
|
files: Record<number, CloudFile[]>; |
||||||
|
selected: CloudFile[]; |
||||||
|
} |
||||||
|
|
||||||
|
export const store = reactive<Store>({ |
||||||
|
showSidebar: true, |
||||||
|
showTasksPanel: false, |
||||||
|
isDragging: false, |
||||||
|
mime: "all", |
||||||
|
lockMime: false, |
||||||
|
tasks: [], |
||||||
|
mode: 'manage', |
||||||
|
multiple: true, |
||||||
|
directories: new Map(), |
||||||
|
rootDirectory: rootDirectoryEntry(), |
||||||
|
activeDirectoryId: 0, |
||||||
|
files: {}, |
||||||
|
selected: [], |
||||||
|
}); |
||||||
|
|
||||||
|
/** 当前文件视图的关联目录 */ |
||||||
|
export const activeDirectory = computed(() => { |
||||||
|
if (store.activeDirectoryId === 0) { |
||||||
|
return store.rootDirectory; |
||||||
|
} |
||||||
|
return store.directories.get(store.activeDirectoryId); |
||||||
|
}); |
||||||
|
|
||||||
|
export function rootDirectoryEntry(): CloudDirectory { |
||||||
|
return { |
||||||
|
id: 0, |
||||||
|
pid: -1, |
||||||
|
title: "根目录", |
||||||
|
createdAt: "", |
||||||
|
sort: Number.MAX_SAFE_INTEGER, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
:host, |
||||||
|
:root { |
||||||
|
font-size: 14px; |
||||||
|
} |
||||||
|
|
||||||
|
@tailwind base; |
||||||
|
@tailwind components; |
||||||
|
@tailwind utilities; |
||||||
|
|
||||||
|
@layer utilities { |
||||||
|
.flex-center { |
||||||
|
@apply flex flex-col justify-center items-center ; |
||||||
|
} |
||||||
|
|
||||||
|
.shadow-pop { |
||||||
|
@apply shadow-[0_2px_11px_rgba(0,0,0,0.1),0_3px_6px_rgba(0,0,0,0.05)]; |
||||||
|
} |
||||||
|
|
||||||
|
.pseudo-border { |
||||||
|
@apply before:absolute before:z-10 before:inset-0 ring-1 ring-gray-900/[0.18] before:rounded-2xl before:pointer-events-none; |
||||||
|
} |
||||||
|
|
||||||
|
.popup-panel { |
||||||
|
@apply rounded-lg overflow-hidden shadow-pop bg-[#f2f1f1]/[0.9] backdrop-blur-[8px] pseudo-border; |
||||||
|
} |
||||||
|
|
||||||
|
.tooltip { |
||||||
|
@apply text-xs bg-white px-1 py-0.5 fixed top-0 left-0 leading-tight transition-opacity pointer-events-none; |
||||||
|
@apply rounded overflow-hidden shadow-pop bg-[#fff]/[0.8] backdrop-blur-[8px] pseudo-border z-[1234]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.spinner { |
||||||
|
position: relative; |
||||||
|
transform: translate3d(50%, 50%, 0); |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes v-loading-spin { |
||||||
|
0% { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
100% { |
||||||
|
opacity: 0.15; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar { |
||||||
|
animation: v-loading-spin 1.2s linear infinite; |
||||||
|
border-radius: 6px; |
||||||
|
height: 8%; |
||||||
|
left: -10%; |
||||||
|
position: absolute; |
||||||
|
top: -3.9%; |
||||||
|
width: 24%; |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(1) { |
||||||
|
animation-delay: -1.2s; |
||||||
|
transform: rotate(0.0001deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(2) { |
||||||
|
animation-delay: -1.1s; |
||||||
|
transform: rotate(30deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(3) { |
||||||
|
animation-delay: -1s; |
||||||
|
transform: rotate(60deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(4) { |
||||||
|
animation-delay: -0.9s; |
||||||
|
transform: rotate(90deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(5) { |
||||||
|
animation-delay: -0.8s; |
||||||
|
transform: rotate(120deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(6) { |
||||||
|
animation-delay: -0.7s; |
||||||
|
transform: rotate(150deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(7) { |
||||||
|
animation-delay: -0.6s; |
||||||
|
transform: rotate(180deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(8) { |
||||||
|
animation-delay: -0.5s; |
||||||
|
transform: rotate(210deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(9) { |
||||||
|
animation-delay: -0.4s; |
||||||
|
transform: rotate(240deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(10) { |
||||||
|
animation-delay: -0.3s; |
||||||
|
transform: rotate(270deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(11) { |
||||||
|
animation-delay: -0.2s; |
||||||
|
transform: rotate(300deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.loading-bar:nth-child(12) { |
||||||
|
animation-delay: -0.1s; |
||||||
|
transform: rotate(330deg) translate(146%); |
||||||
|
} |
||||||
|
|
||||||
|
.v-snake { |
||||||
|
--v-snake: 102%; |
||||||
|
animation: v-snake 100ms ease-out normal; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes v-snake { |
||||||
|
50% { |
||||||
|
transform: scale(var(--v-snake, 102%)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,105 @@ |
|||||||
|
import domAlign, {alignPoint} from "dom-align"; |
||||||
|
|
||||||
|
export type Placement = |
||||||
|
| 'top-left' |
||||||
|
| 'top-center' |
||||||
|
| 'top-right' |
||||||
|
| 'bottom-left' |
||||||
|
| 'bottom-center' |
||||||
|
| 'bottom-right' |
||||||
|
| 'left-top' |
||||||
|
| 'left-center' |
||||||
|
| 'left-bottom' |
||||||
|
| 'right-top' |
||||||
|
| 'right-center' |
||||||
|
| 'right-bottom' |
||||||
|
|
||||||
|
// position-align
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// | top-left top-center top-right |
|
||||||
|
// -------------+------------------------------------------+--------------
|
||||||
|
// left-top | | right-top
|
||||||
|
// | |
|
||||||
|
// left-center | | right-center
|
||||||
|
// | |
|
||||||
|
// left-bottom | | right-bottom
|
||||||
|
// -------------+------------------------------------------+--------------
|
||||||
|
// | bottom-left bottom-center bottom-right |
|
||||||
|
//
|
||||||
|
export const positions: Record<Placement, Placement> = { |
||||||
|
// trigger => popup
|
||||||
|
'top-left': 'bottom-left', |
||||||
|
'top-center': 'bottom-center', |
||||||
|
'top-right': 'bottom-right', |
||||||
|
'bottom-left': 'top-left', |
||||||
|
'bottom-center': 'top-center', |
||||||
|
'bottom-right': 'top-right', |
||||||
|
'left-top': 'right-top', |
||||||
|
'left-center': 'right-center', |
||||||
|
'left-bottom': 'right-bottom', |
||||||
|
'right-top': 'left-top', |
||||||
|
'right-center': 'left-center', |
||||||
|
'right-bottom': 'left-bottom', |
||||||
|
} |
||||||
|
|
||||||
|
export function alignWithElement( |
||||||
|
trigger: Element, |
||||||
|
popup: HTMLElement, |
||||||
|
placement: Placement, |
||||||
|
offset: [number, number] |
||||||
|
) { |
||||||
|
let triggerPoints: string[] = placement.split('-') |
||||||
|
let popupPoints: string[] = positions[placement].split('-') |
||||||
|
switch (triggerPoints[0]) { |
||||||
|
case 'left': |
||||||
|
case 'right': |
||||||
|
triggerPoints = triggerPoints.reverse() |
||||||
|
popupPoints = popupPoints.reverse() |
||||||
|
break |
||||||
|
} |
||||||
|
domAlign(popup, trigger, { |
||||||
|
points: [ |
||||||
|
popupPoints.map(c => c[0]).join(''), |
||||||
|
triggerPoints.map(c => c[0]).join(''), |
||||||
|
], |
||||||
|
offset: offset, |
||||||
|
overflow: { |
||||||
|
adjustX: true, |
||||||
|
adjustY: true, |
||||||
|
}, |
||||||
|
useCssTransform: true, |
||||||
|
ignoreShake: true, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function alignWithPoint( |
||||||
|
popup: HTMLElement, |
||||||
|
clientX: number, |
||||||
|
clientY: number, |
||||||
|
placement: Placement, |
||||||
|
offset: [number, number] |
||||||
|
) { |
||||||
|
let triggerPoints: string[] = placement.split('-') |
||||||
|
let popupPoints: string[] = positions[placement].split('-') |
||||||
|
switch (triggerPoints[0]) { |
||||||
|
case 'left': |
||||||
|
case 'right': |
||||||
|
triggerPoints = triggerPoints.reverse() |
||||||
|
popupPoints = popupPoints.reverse() |
||||||
|
break |
||||||
|
} |
||||||
|
alignPoint(popup, {clientX, clientY}, { |
||||||
|
points: [ |
||||||
|
popupPoints.map(c => c[0]).join(''), |
||||||
|
triggerPoints.map(c => c[0]).join(''), |
||||||
|
], |
||||||
|
offset: offset, |
||||||
|
overflow: { |
||||||
|
adjustX: true, |
||||||
|
adjustY: true, |
||||||
|
}, |
||||||
|
useCssTransform: true, |
||||||
|
ignoreShake: true, |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
export * from './align' |
||||||
|
export * from './preview' |
||||||
|
export * from './scroll' |
@ -0,0 +1,106 @@ |
|||||||
|
import Hls from "hls.js" |
||||||
|
import Viewer from 'viewerjs' |
||||||
|
import viewerStyle from "viewerjs/dist/viewer.css?inline" |
||||||
|
|
||||||
|
const viewerStyleId = "wfs-viewer-style" |
||||||
|
|
||||||
|
let video: HTMLVideoElement | undefined |
||||||
|
|
||||||
|
interface PreviewOptions { |
||||||
|
path: string |
||||||
|
mime: string |
||||||
|
thumb: string |
||||||
|
name: string |
||||||
|
} |
||||||
|
|
||||||
|
// https://juejin.cn/post/7146947008015089672
|
||||||
|
const startVideoPreview = (path: string, img: HTMLImageElement): VoidFunction => { |
||||||
|
video = document.createElement('video'); |
||||||
|
video.controls = true |
||||||
|
video.className = img.className |
||||||
|
video.style.cssText = img.style.cssText |
||||||
|
.replace("relative", "absolute") |
||||||
|
.replace("margin-left", "left") |
||||||
|
.replace("margin-top", "top") |
||||||
|
|
||||||
|
if (Hls.isSupported()) { |
||||||
|
const hls = new Hls(); |
||||||
|
hls.loadSource(path); |
||||||
|
hls.attachMedia(video); |
||||||
|
hls.on(Hls.Events.MANIFEST_PARSED, function () { |
||||||
|
void video?.play(); |
||||||
|
}); |
||||||
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { |
||||||
|
video.src = 'https://video-dev.github.io/streams/x36xhzz/x36xhzz.m3u8'; |
||||||
|
video.addEventListener('loadedmetadata', function () { |
||||||
|
img.style.opacity = "0" |
||||||
|
void video?.play(); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
alert("浏览器不支持该视频") |
||||||
|
} |
||||||
|
|
||||||
|
img.parentNode!.appendChild(video) |
||||||
|
|
||||||
|
const onResize = () => { |
||||||
|
if (video != null) { |
||||||
|
video.className = img.className |
||||||
|
video.style.cssText = img.style.cssText |
||||||
|
.replace("relative", "absolute") |
||||||
|
.replace("margin-left", "left") |
||||||
|
.replace("margin-top", "top") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onResize() |
||||||
|
|
||||||
|
window.addEventListener("resize", onResize) |
||||||
|
|
||||||
|
return () => { |
||||||
|
video?.pause() |
||||||
|
video?.remove() |
||||||
|
video = undefined |
||||||
|
window.removeEventListener("resize", onResize) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function showPreview(opts: PreviewOptions): void { |
||||||
|
if (!document.querySelector(`#${viewerStyleId}`)) { |
||||||
|
const style = document.createElement("style") |
||||||
|
style.innerHTML = viewerStyle |
||||||
|
style.id = viewerStyleId |
||||||
|
document.head.appendChild(style) |
||||||
|
} |
||||||
|
|
||||||
|
const isVideo = opts.mime.startsWith("video/") |
||||||
|
const img = document.createElement("img") |
||||||
|
img.src = isVideo ? opts.thumb : opts.path |
||||||
|
let dispose: VoidFunction | undefined |
||||||
|
const viewer = new Viewer(img, { |
||||||
|
title: [4, (_: any, imageData: any) => `${opts.name} (${imageData.naturalWidth} × ${imageData.naturalHeight})`], |
||||||
|
navbar: false, |
||||||
|
zoomable: !isVideo, |
||||||
|
movable: !isVideo, |
||||||
|
backdrop: true, |
||||||
|
slideOnTouch: !isVideo, |
||||||
|
keyboard: !isVideo, |
||||||
|
toolbar: isVideo ? {} : { |
||||||
|
flipHorizontal: true, |
||||||
|
flipVertical: true, |
||||||
|
oneToOne: true, |
||||||
|
reset: true, |
||||||
|
rotateLeft: true, |
||||||
|
rotateRight: true, |
||||||
|
zoomIn: true, |
||||||
|
zoomOut: true, |
||||||
|
}, |
||||||
|
viewed(event: CustomEvent) { |
||||||
|
const el = event.detail.image as HTMLImageElement |
||||||
|
isVideo && (dispose = startVideoPreview(opts.path, el)) |
||||||
|
}, |
||||||
|
hide() { |
||||||
|
dispose?.() |
||||||
|
}, |
||||||
|
}) |
||||||
|
viewer.show() |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
export function getScrollParent(el?: HTMLElement, includeHidden = false) { |
||||||
|
while (el) { |
||||||
|
if (includeHidden ? isPotentiallyScrollable(el) : hasScrollbar(el)) return el |
||||||
|
el = el.parentElement! |
||||||
|
} |
||||||
|
|
||||||
|
return document.scrollingElement as HTMLElement |
||||||
|
} |
||||||
|
|
||||||
|
export function getScrollParents(el?: Element | null, stopAt?: Element | null) { |
||||||
|
const elements: HTMLElement[] = [] |
||||||
|
|
||||||
|
if (stopAt && el && !stopAt.contains(el)) return elements |
||||||
|
|
||||||
|
while (el) { |
||||||
|
if (hasScrollbar(el)) elements.push(el as HTMLElement) |
||||||
|
if (el === stopAt) break |
||||||
|
el = el.parentElement! |
||||||
|
} |
||||||
|
|
||||||
|
return elements |
||||||
|
} |
||||||
|
|
||||||
|
export function hasScrollbar(el?: Element | null) { |
||||||
|
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false |
||||||
|
|
||||||
|
const style = window.getComputedStyle(el) |
||||||
|
return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight) |
||||||
|
} |
||||||
|
|
||||||
|
function isPotentiallyScrollable(el?: Element | null) { |
||||||
|
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false |
||||||
|
|
||||||
|
const style = window.getComputedStyle(el) |
||||||
|
return ['scroll', 'auto'].includes(style.overflowY) |
||||||
|
} |
||||||
|
|
||||||
|
export function getScrollBarWidth() { |
||||||
|
const outer = document.createElement('div'); |
||||||
|
outer.style.overflow = 'scroll'; |
||||||
|
outer.style.height = '200px'; |
||||||
|
outer.style.width = '100px'; |
||||||
|
outer.style.position = 'fixed' |
||||||
|
outer.style.opacity = '0' |
||||||
|
outer.style.pointerEvents = 'none' |
||||||
|
|
||||||
|
document.body.appendChild(outer); |
||||||
|
const inner = document.createElement('div'); |
||||||
|
inner.style.width = '100%'; |
||||||
|
outer.appendChild(inner); |
||||||
|
|
||||||
|
const widthNoScroll = outer.offsetWidth; |
||||||
|
const widthWithScroll = inner.offsetWidth; |
||||||
|
|
||||||
|
outer.remove() |
||||||
|
|
||||||
|
return widthNoScroll - widthWithScroll; |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import UiMistake from './UiMistake.vue' |
||||||
|
import VLoading from './VLoading.vue' |
||||||
|
import {coerce, sleep} from '../../shared' |
||||||
|
import {onMounted, ref} from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
setup: () => Promise<void> |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'setup'): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const rootElmRef = ref<HTMLDivElement | null>(null) |
||||||
|
const errorMessage = ref<string>() |
||||||
|
const isInitializing = ref(true) |
||||||
|
|
||||||
|
const initialize = async (): Promise<void> => { |
||||||
|
try { |
||||||
|
errorMessage.value = undefined |
||||||
|
await props.setup() |
||||||
|
emit('setup') |
||||||
|
} catch (error) { |
||||||
|
errorMessage.value = String(coerce(error)) |
||||||
|
} finally { |
||||||
|
isInitializing.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const reinitialize = async () => { |
||||||
|
isInitializing.value = true |
||||||
|
await sleep(500) |
||||||
|
await initialize() |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(initialize) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
ref="rootElmRef" |
||||||
|
class="relative size-full !min-w-[52rem] !min-h-[30rem] backdrop-blur-md rounded-lg" |
||||||
|
data-ui-barrier |
||||||
|
> |
||||||
|
<div |
||||||
|
class="relative z-0 size-full rounded-lg bg-gray-200/95 dark:bg-gray-700/95 overflow-hidden"> |
||||||
|
<v-loading |
||||||
|
v-if="isInitializing" |
||||||
|
class="absolute inset-0" |
||||||
|
/> |
||||||
|
<UiMistake |
||||||
|
v-else-if="errorMessage" |
||||||
|
:message="errorMessage" |
||||||
|
class="absolute inset-0" |
||||||
|
@refresh="reinitialize" |
||||||
|
/> |
||||||
|
<div v-else class="absolute inset-0"> |
||||||
|
<slot/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,103 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {useTheme} from '../hooks' |
||||||
|
import {reload, store, useFiler} from '../store' |
||||||
|
import UiBarrier from './UiBarrier.vue' |
||||||
|
import UiContextMenu from './UiContextMenu.vue' |
||||||
|
import {hideContextMenu, toggleContextMenu} from './UiContextMenuController' |
||||||
|
import UiDirectoryNaming from './UiDirectoryNaming.vue' |
||||||
|
import {hideDirectoryNaming} from './UiDirectoryNamingController' |
||||||
|
import UiDropFeedback from './UiDropFeedback.vue' |
||||||
|
import UiFileNaming from './UiFileNaming.vue' |
||||||
|
import {hideFileNaming} from './UiFileNamingController' |
||||||
|
import UiFiles from './UiFiles.vue' |
||||||
|
import UiNavbar from './UiNavbar.vue' |
||||||
|
import UiSidebar from './UiSidebar.vue' |
||||||
|
import UiSplitter from './UiSplitter.vue' |
||||||
|
import UiTasks from './UiTasks.vue' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
import UiToaster from './UiToaster.vue' |
||||||
|
import {dispatch} from '../worker' |
||||||
|
import {$on, coerce} from '../../shared' |
||||||
|
import {onMounted, onUnmounted} from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
setup: () => Awaitable<ApiConfig> |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'close'): void |
||||||
|
(ev: 'select', files: CloudFile[]): void |
||||||
|
(ev: 'fail', message: string): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const {dark, light} = useTheme() |
||||||
|
|
||||||
|
const setup = async () => { |
||||||
|
try { |
||||||
|
const cfg = await props.setup() |
||||||
|
await dispatch("cfg", "init", cfg) |
||||||
|
await reload() |
||||||
|
useFiler(0) |
||||||
|
} catch (error) { |
||||||
|
emit('fail', String(coerce(error))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
const dispose = $on('app', 'error', (error: string): void => { |
||||||
|
toast('error', error) |
||||||
|
}) |
||||||
|
onUnmounted(() => { |
||||||
|
dispose() |
||||||
|
hideContextMenu() |
||||||
|
hideFileNaming() |
||||||
|
hideDirectoryNaming() |
||||||
|
store.selected = [] |
||||||
|
}) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<UiBarrier |
||||||
|
:class="{ |
||||||
|
'ring-white/80': light, |
||||||
|
'ring-white/30': dark, |
||||||
|
'light': light, |
||||||
|
'dark': dark, |
||||||
|
}" |
||||||
|
:setup="setup" |
||||||
|
class="ring-1" |
||||||
|
@contextmenu.prevent="toggleContextMenu" |
||||||
|
> |
||||||
|
<UiSplitter :max="300" :min="162" :size="256"> |
||||||
|
<template v-slot:secondary> |
||||||
|
<UiSidebar/> |
||||||
|
</template> |
||||||
|
<template v-slot:primary> |
||||||
|
<div |
||||||
|
class="size-full flex flex-col items-stretch bg-white dark:bg-black"> |
||||||
|
<UiNavbar |
||||||
|
class="bg-gradient-to-b from-white to-gray-100 border-b border-gray-200" |
||||||
|
@confirm="$emit('select', store.selected.map(f => ({...f, object: {...f.object}})))" |
||||||
|
@dismiss="$emit('close')" |
||||||
|
/> |
||||||
|
<UiTasks/> |
||||||
|
<UiToaster/> |
||||||
|
<div class="relative flex-1 overflow-hidden"> |
||||||
|
<KeepAlive> |
||||||
|
<UiFiles |
||||||
|
:key="store.activeDirectoryId" |
||||||
|
:directory-id="store.activeDirectoryId" |
||||||
|
class="absolute inset-0" |
||||||
|
/> |
||||||
|
</KeepAlive> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</UiSplitter> |
||||||
|
<UiContextMenu/> |
||||||
|
<UiDirectoryNaming/> |
||||||
|
<UiFileNaming/> |
||||||
|
<UiDropFeedback/> |
||||||
|
</UiBarrier> |
||||||
|
</template> |
@ -0,0 +1,275 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {pickFiles} from '../store' |
||||||
|
import {emit, formatDate, humanSize} from '../../shared' |
||||||
|
import {onMounted, ref, watch} from 'vue' |
||||||
|
import {alignWithPoint, showPreview} from '../utils' |
||||||
|
import VButton from './VButton.vue' |
||||||
|
import { |
||||||
|
canShare, |
||||||
|
controller, |
||||||
|
copyDirName, |
||||||
|
copyFileName, |
||||||
|
copyFilePath, |
||||||
|
file, |
||||||
|
handleNewDir, |
||||||
|
hideContextMenu, |
||||||
|
isTarget, |
||||||
|
removeDir, |
||||||
|
removeFile, |
||||||
|
renameDir, |
||||||
|
share, |
||||||
|
} from './UiContextMenuController' |
||||||
|
import {showFileNaming} from './UiFileNamingController' |
||||||
|
import UiIconButton from './VIconButton.vue' |
||||||
|
import UiText from './VText.vue' |
||||||
|
import {getRootBarrier} from './utils' |
||||||
|
import {onClickOutside} from "@vueuse/core"; |
||||||
|
|
||||||
|
type Status = |
||||||
|
| 'forward' // 开始 -> 结束 |
||||||
|
| 'completed' // 结束位置 |
||||||
|
| 'reverse' // 结束 -> 开始 |
||||||
|
| 'dismissed' // 开始位置 |
||||||
|
|
||||||
|
const barrier = ref<Node | null>() |
||||||
|
const menuElm = ref<HTMLElement | null>(null) |
||||||
|
const status = ref<Status>("dismissed") |
||||||
|
const visible = ref(controller.visible) |
||||||
|
|
||||||
|
const align = (): void => { |
||||||
|
if (menuElm.value) { |
||||||
|
alignWithPoint( |
||||||
|
menuElm.value, |
||||||
|
controller.x, |
||||||
|
controller.y, |
||||||
|
'bottom-left', |
||||||
|
[0, 0], |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleForward = () => { |
||||||
|
emit('contextmenu:shown') |
||||||
|
status.value = 'forward' |
||||||
|
} |
||||||
|
const handleCompleted = () => { |
||||||
|
status.value = "completed" |
||||||
|
} |
||||||
|
const handleReverse = () => { |
||||||
|
status.value = "reverse" |
||||||
|
} |
||||||
|
const handleDismissed = () => { |
||||||
|
status.value = "dismissed" |
||||||
|
if (controller.visible) { |
||||||
|
visible.value = true |
||||||
|
align() |
||||||
|
} else { |
||||||
|
emit('contextmenu:hidden') |
||||||
|
hideContextMenu() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
watch(menuElm, align) |
||||||
|
|
||||||
|
watch([ |
||||||
|
barrier, |
||||||
|
() => controller.x, |
||||||
|
() => controller.y, |
||||||
|
], () => { |
||||||
|
// ??? 检测位置变化 |
||||||
|
if (visible.value) { |
||||||
|
visible.value = false |
||||||
|
} else { |
||||||
|
align() |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
watch(() => controller.visible, (value) => { |
||||||
|
visible.value = value |
||||||
|
}) |
||||||
|
|
||||||
|
// 这里接收的参数实际上是为了忽略参数 |
||||||
|
const handleClick = (_: any): void => { |
||||||
|
hideContextMenu() |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
barrier.value = getRootBarrier() |
||||||
|
}) |
||||||
|
|
||||||
|
onClickOutside(menuElm, hideContextMenu) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Teleport v-if="barrier" :to="barrier"> |
||||||
|
<div |
||||||
|
ref="menuElm" |
||||||
|
class="absolute z-[100] top-0 left-0 text-sm empty:hidden" |
||||||
|
data-role="contextmenu" |
||||||
|
role="menu" |
||||||
|
> |
||||||
|
<transition |
||||||
|
enter-from-class="duration-100 opacity-0 translate-y-4 scale-95 sm:translate-y-0" |
||||||
|
enter-to-class="duration-100 opacity-100 translate-y-0 scale-100" |
||||||
|
leave-from-class="duration-100 opacity-100 translate-y-0 scale-100" |
||||||
|
leave-to-class="duration-100 opacity-0 translate-y-4 scale-95 sm:translate-y-0" |
||||||
|
@enter="align" |
||||||
|
@before-enter="handleForward" |
||||||
|
@after-enter="handleCompleted" |
||||||
|
@before-leave="handleReverse" |
||||||
|
@after-leave="handleDismissed" |
||||||
|
> |
||||||
|
<div |
||||||
|
v-if="visible" |
||||||
|
class="transition-all origin-bottom-left min-w-36 popup-panel relative z-10 flex flex-col items-stretch gap-0.5 p-1.5" |
||||||
|
> |
||||||
|
<v-button |
||||||
|
v-if="isTarget('sidebar', 'directory')" |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="ArrowClockwiseSolid" |
||||||
|
label="刷新" |
||||||
|
@click="handleClick(emit('dirs:refresh'))" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
v-if="isTarget('content')" |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="ArrowClockwiseSolid" |
||||||
|
label="刷新" |
||||||
|
@click="handleClick(emit(`files:${controller.directoryId ?? 0}:reload`))" |
||||||
|
/> |
||||||
|
<div class="my-0.5 mx-2.5 border-t border-slate-300 first:hidden"/> |
||||||
|
<v-button |
||||||
|
v-if="isTarget('directory', 'content')" |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="FileAdd" |
||||||
|
label="上传文件" |
||||||
|
@click="handleClick(pickFiles(controller.directoryId ?? 0))" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
v-if="isTarget('sidebar')" |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="FolderAdd" |
||||||
|
label="新建文件夹" |
||||||
|
@click="handleClick(handleNewDir(false))" |
||||||
|
/> |
||||||
|
<!-- 与文件夹相关的选项 --> |
||||||
|
<template |
||||||
|
v-if="isTarget('directory') && (controller.directoryId ?? 0) > 0"> |
||||||
|
<v-button |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="FolderAdd" |
||||||
|
label="新建子文件夹" |
||||||
|
@click="handleClick(handleNewDir(true))" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="Rename" |
||||||
|
label="重命名" |
||||||
|
@click="handleClick(renameDir())" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
class="hover:bg-indigo-500 hover:text-white" |
||||||
|
icon="Copy" |
||||||
|
label="复制名称" |
||||||
|
@click="handleClick(copyDirName())" |
||||||
|
/> |
||||||
|
<div class="my-0.5 mx-2.5 border-t border-slate-300"/> |
||||||
|
<v-button |
||||||
|
class="hover:bg-red-500 hover:text-white" |
||||||
|
icon="Delete" |
||||||
|
label="删除" |
||||||
|
@click="handleClick(removeDir())" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
<!-- 与文件相关的选项 --> |
||||||
|
<div |
||||||
|
v-if="isTarget('file', 'filename') && file" |
||||||
|
class="w-60 flex flex-col items-stretch" |
||||||
|
> |
||||||
|
<div class="flex items-center gap-1 p-1.5"> |
||||||
|
<UiIconButton |
||||||
|
icon="Eye" |
||||||
|
primary |
||||||
|
tooltip="预览文件" |
||||||
|
@click="handleClick(showPreview({ |
||||||
|
name: file.name, |
||||||
|
mime: file.mime, |
||||||
|
path: file.object.path, |
||||||
|
thumb: file.object.thumb, |
||||||
|
}))" |
||||||
|
/> |
||||||
|
<UiIconButton |
||||||
|
icon="Rename" |
||||||
|
primary |
||||||
|
tooltip="重命名文件" |
||||||
|
@click="handleClick(showFileNaming({ |
||||||
|
dirId: file.directoryId, |
||||||
|
id: file.id, |
||||||
|
}))" |
||||||
|
/> |
||||||
|
<UiIconButton |
||||||
|
icon="Copy" |
||||||
|
primary |
||||||
|
tooltip="复制文件名称" |
||||||
|
@click="handleClick(copyFileName())" |
||||||
|
/> |
||||||
|
<UiIconButton |
||||||
|
v-if="canShare()" |
||||||
|
icon="Share" |
||||||
|
primary |
||||||
|
tooltip="分享文件" |
||||||
|
@click="handleClick(share())" |
||||||
|
/> |
||||||
|
<div class="flex-1"/> |
||||||
|
<UiIconButton |
||||||
|
class="text-red-500 hover:bg-red-500 hover:text-white" |
||||||
|
icon="Delete" |
||||||
|
tooltip="删除文件" |
||||||
|
@click="handleClick(removeFile())" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div class="mt-1 mb-3 mx-2 border-t border-slate-300"/> |
||||||
|
<div class="leading-tight mx-2.5"> |
||||||
|
<div class="text-xs text-gray-500">名称:</div> |
||||||
|
<UiText |
||||||
|
:text="file.name" |
||||||
|
class="mt-1 break-all leading-tight" |
||||||
|
tag="div" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div class="my-3 mx-2 border-t border-slate-300"/> |
||||||
|
<div class="text-xs mx-2.5"> |
||||||
|
<div class="text-xs text-gray-500">详情:</div> |
||||||
|
<div class="mt-1"> |
||||||
|
<span class="inline-block w-16 text-right">大小:</span> |
||||||
|
<span>{{ humanSize(file.object.size) }}</span> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="inline-block w-16 text-right">尺寸:</span> |
||||||
|
<span>{{ file.object.width }}x{{ file.object.height }}</span> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="inline-block w-16 text-right">上传时间:</span> |
||||||
|
<span>{{ formatDate(file.createdAt) }}</span> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="inline-block w-16 text-right">修改时间:</span> |
||||||
|
<span>{{ formatDate(file.updatedAt) }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="my-3 mx-2 border-t border-slate-300"/> |
||||||
|
<div class="text-xs whitespace-pre-wrap mx-2.5 pb-2"> |
||||||
|
<div class="flex items-center text-xs text-gray-500">链接:</div> |
||||||
|
<UiText |
||||||
|
:text="file.object.path" |
||||||
|
class="break-all rounded mt-1 leading-tight" |
||||||
|
tag="div" |
||||||
|
@dblclick="copyFilePath()" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</transition> |
||||||
|
</div> |
||||||
|
</Teleport> |
||||||
|
</template> |
@ -0,0 +1,173 @@ |
|||||||
|
import {copyText} from '../../shared' |
||||||
|
import {computed, nextTick, reactive} from "vue"; |
||||||
|
import {deleteFile, store} from "../store"; |
||||||
|
import {dispatch} from "../worker.ts"; |
||||||
|
import {showDirectoryNaming} from "./UiDirectoryNamingController"; |
||||||
|
import {toast} from "./UiToastController"; |
||||||
|
|
||||||
|
export type ContextmenuTarget = |
||||||
|
| "directory" // 作用在侧边栏文件夹按钮上
|
||||||
|
| "file" // 作用在右侧文件图标上
|
||||||
|
| "filename" // 作用在右侧文件标题图标上
|
||||||
|
| "sidebar" // 作用在侧边栏上
|
||||||
|
| "content"; // 作用在右侧文件容器上
|
||||||
|
|
||||||
|
export interface ContextMenuOptions { |
||||||
|
x: number; |
||||||
|
y: number; |
||||||
|
directoryId?: number; |
||||||
|
fileId?: number; |
||||||
|
target?: ContextmenuTarget; |
||||||
|
} |
||||||
|
|
||||||
|
interface ContextMenuState extends ContextMenuOptions { |
||||||
|
visible: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const controller = reactive<ContextMenuState>({ |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
directoryId: 0, |
||||||
|
fileId: undefined, |
||||||
|
visible: false, |
||||||
|
}); |
||||||
|
|
||||||
|
export const file = computed((): CloudFile | undefined => { |
||||||
|
return controller.fileId |
||||||
|
? store.files[controller.directoryId ?? 0]?.find(f => f.id === controller.fileId) |
||||||
|
: undefined |
||||||
|
}) |
||||||
|
|
||||||
|
export const directory = computed(() => { |
||||||
|
const dirId = controller.directoryId |
||||||
|
return dirId == 0 |
||||||
|
? store.rootDirectory |
||||||
|
: dirId != null |
||||||
|
? store.directories.get(dirId) |
||||||
|
: undefined |
||||||
|
}) |
||||||
|
|
||||||
|
export function isTarget(...targets: ContextmenuTarget[]): boolean { |
||||||
|
return controller.target != null && targets.includes(controller.target); |
||||||
|
} |
||||||
|
|
||||||
|
export function showContextMenu(opts: ContextMenuOptions): void { |
||||||
|
if (opts.target && opts.directoryId != null) { |
||||||
|
controller.visible = true; |
||||||
|
void nextTick(() => { |
||||||
|
controller.x = opts.x; |
||||||
|
controller.y = opts.y; |
||||||
|
controller.fileId = opts.fileId; |
||||||
|
controller.directoryId = opts.directoryId; |
||||||
|
controller.target = opts.target; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
hideContextMenu(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function hideContextMenu() { |
||||||
|
if (controller.visible) { |
||||||
|
controller.visible = false; |
||||||
|
void nextTick(() => { |
||||||
|
if (!controller.visible) { |
||||||
|
controller.directoryId = undefined; |
||||||
|
controller.fileId = undefined; |
||||||
|
controller.target = undefined; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function toggleContextMenu(evt: MouseEvent): void { |
||||||
|
const elm = evt.target as HTMLElement; |
||||||
|
if (elm.closest('[data-role="contextmenu"]')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const dataset = elm.closest<HTMLElement>("[data-contextmenu]")?.dataset; |
||||||
|
if (dataset && dataset.target != null && dataset.directoryId != null) { |
||||||
|
showContextMenu({ |
||||||
|
x: evt.clientX, |
||||||
|
y: evt.clientY, |
||||||
|
fileId: dataset.fileId ? Number(dataset.fileId) : undefined, |
||||||
|
directoryId: Number(dataset.directoryId), |
||||||
|
target: dataset.target as ContextmenuTarget, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
hideContextMenu(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function handleNewDir(child?: boolean) { |
||||||
|
showDirectoryNaming({ |
||||||
|
action: "create", |
||||||
|
pid: child ? controller.directoryId : undefined, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// 重命名文件或文件夹
|
||||||
|
export function renameDir() { |
||||||
|
const {directoryId} = controller |
||||||
|
if (isTarget("directory") && directoryId) { |
||||||
|
showDirectoryNaming({ |
||||||
|
id: directoryId, |
||||||
|
action: "rename", |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function copyDirName() { |
||||||
|
const text = directory.value?.title |
||||||
|
text?.length && copyText(text) |
||||||
|
} |
||||||
|
|
||||||
|
export function copyFileName() { |
||||||
|
const text = file.value?.name |
||||||
|
text?.length && copyText(text) |
||||||
|
} |
||||||
|
|
||||||
|
export function copyFilePath() { |
||||||
|
const text = file.value?.object.path |
||||||
|
text?.length && copyText(text) |
||||||
|
} |
||||||
|
|
||||||
|
export function removeDir() { |
||||||
|
const {directoryId} = controller |
||||||
|
if (isTarget("directory") && directoryId) { |
||||||
|
dispatch("dir", "delete", directoryId) |
||||||
|
.then(() => store.directories.delete(directoryId)) |
||||||
|
.then(() => toast("success", "删除成功")) |
||||||
|
.catch((error) => toast("error", error)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function removeFile() { |
||||||
|
const f = file.value; |
||||||
|
if (isTarget("file", "filename") && f) { |
||||||
|
dispatch("file", "delete", f.id) |
||||||
|
.then(() => deleteFile(f)) |
||||||
|
.then(() => toast("success", "删除成功")) |
||||||
|
.catch((error) => toast("error", error)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function canShare() { |
||||||
|
return typeof navigator.canShare === 'function' && navigator.canShare({ |
||||||
|
title: "MDN", |
||||||
|
text: "Learn web development on MDN!", |
||||||
|
url: "https://developer.mozilla.org", |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function share() { |
||||||
|
if (file.value) { |
||||||
|
navigator |
||||||
|
.share({ |
||||||
|
title: file.value!.name, |
||||||
|
text: file.value!.object.path, |
||||||
|
url: file.value!.object.path, |
||||||
|
}) |
||||||
|
.then(() => toast("success", "分享成功")) |
||||||
|
.catch((error) => toast("error", error)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {getScrollParent} from '../utils' |
||||||
|
import {nextTick, ref, watch} from 'vue' |
||||||
|
import {onClickOutside, useEventListener} from "@vueuse/core"; |
||||||
|
|
||||||
|
type Status = |
||||||
|
| 'forward' // 开始 -> 结束 |
||||||
|
| 'completed' // 结束位置 |
||||||
|
| 'reverse' // 结束 -> 开始 |
||||||
|
| 'dismissed' // 开始位置 |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
inheritAttrs: false, |
||||||
|
}) |
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ |
||||||
|
barrier: Node |
||||||
|
visible?: boolean |
||||||
|
rounded?: boolean |
||||||
|
outside?: 'close' | 'shake' | 'none' |
||||||
|
zIndex?: number |
||||||
|
}>(), { |
||||||
|
rounded() { |
||||||
|
return true |
||||||
|
}, |
||||||
|
outside() { |
||||||
|
return 'none' |
||||||
|
}, |
||||||
|
zIndex() { |
||||||
|
return 100 |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: Status): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const panel = ref<HTMLElement | null>(null) |
||||||
|
const visible = ref(false) |
||||||
|
const isOpen = ref(false) |
||||||
|
const status = ref<Status>('dismissed') |
||||||
|
|
||||||
|
let resumeScrolling: VoidFunction | undefined |
||||||
|
|
||||||
|
const setStatus = (value: Status): void => { |
||||||
|
status.value = value |
||||||
|
} |
||||||
|
|
||||||
|
watch(() => props.visible, (value) => { |
||||||
|
if (value && !visible.value) { |
||||||
|
visible.value = true |
||||||
|
nextTick(() => { |
||||||
|
isOpen.value = value |
||||||
|
}) |
||||||
|
} else { |
||||||
|
isOpen.value = value |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
watch(status, (value) => { |
||||||
|
visible.value = value !== 'dismissed' |
||||||
|
emit(value) |
||||||
|
if (value === 'dismissed' && resumeScrolling) { |
||||||
|
resumeScrolling() |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
watch(panel, el => { |
||||||
|
if (el) { |
||||||
|
const scroll = getScrollParent(el) |
||||||
|
|
||||||
|
// 检测是否存在滚动条 |
||||||
|
const div = document.createElement('div') |
||||||
|
div.style.cssText = 'position:fixed;left:0;right:0' |
||||||
|
scroll.appendChild(div) |
||||||
|
const hasScroll = div.offsetWidth < scroll.offsetWidth |
||||||
|
div.remove() |
||||||
|
if (!hasScroll) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const {overflowY, position, inset} = scroll.style |
||||||
|
scroll.style.overflowY = 'scroll' |
||||||
|
scroll.style.position = 'fixed' |
||||||
|
scroll.style.inset = '0' |
||||||
|
|
||||||
|
const resume = () => { |
||||||
|
if (resume === resumeScrolling) { |
||||||
|
resumeScrolling = undefined |
||||||
|
scroll.style.inset = inset |
||||||
|
scroll.style.overflowY = overflowY |
||||||
|
scroll.style.position = position |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
resumeScrolling = resume |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
useEventListener(panel, 'animationend', () => { |
||||||
|
panel.value?.classList.remove('v-snake') |
||||||
|
}) |
||||||
|
|
||||||
|
onClickOutside(panel, () => { |
||||||
|
if (props.outside === 'close') { |
||||||
|
isOpen.value = false |
||||||
|
} else if (props.outside === 'shake' && panel.value) { |
||||||
|
const w = panel.value.clientWidth |
||||||
|
const v = ((w + 10) / w).toFixed(4) |
||||||
|
panel.value.style.setProperty("--v-snake", v) |
||||||
|
panel.value.classList.add('v-snake') |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Teleport :to="barrier"> |
||||||
|
<div |
||||||
|
v-if="visible" |
||||||
|
:style="{zIndex}" |
||||||
|
class="absolute inset-0 flex-center" |
||||||
|
> |
||||||
|
<Transition |
||||||
|
enter-from-class="opacity-0" |
||||||
|
enter-to-class="backdrop-blur-[2px]" |
||||||
|
leave-from-class="backdrop-blur-[2px]" |
||||||
|
leave-to-class="opacity-0" |
||||||
|
@before-enter="setStatus('forward')" |
||||||
|
@after-enter="setStatus('completed')" |
||||||
|
@before-leave="setStatus('reverse')" |
||||||
|
@after-leave="setStatus('dismissed')" |
||||||
|
> |
||||||
|
<div |
||||||
|
v-if="isOpen" |
||||||
|
:class="{ |
||||||
|
'backdrop-blur-sm': status === 'completed', |
||||||
|
'rounded-lg': rounded, |
||||||
|
}" |
||||||
|
class="transition-all ease-out absolute z-0 inset-0 bg-black/30" |
||||||
|
/> |
||||||
|
</Transition> |
||||||
|
<Transition |
||||||
|
enter-from-class="opacity-0 scale-75" |
||||||
|
leave-to-class="opacity-0 scale-75" |
||||||
|
> |
||||||
|
<div |
||||||
|
v-if="isOpen" |
||||||
|
ref="panel" |
||||||
|
class="transition-all ease-out relative z-10" |
||||||
|
role="dialog" |
||||||
|
style="--v-snake:90%" |
||||||
|
v-bind="$attrs" |
||||||
|
> |
||||||
|
<slot/> |
||||||
|
</div> |
||||||
|
</Transition> |
||||||
|
</div> |
||||||
|
</Teleport> |
||||||
|
</template> |
@ -0,0 +1,85 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {hideContextMenu} from './UiContextMenuController' |
||||||
|
import {once} from '../../shared' |
||||||
|
import {computed, inject, type Ref} from 'vue' |
||||||
|
import {getDirectoryEntry, hasChildDirectoryEntries, store} from '../store' |
||||||
|
import VIcon from "./VIcon.vue"; |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
dir: number |
||||||
|
deep: number |
||||||
|
}>() |
||||||
|
|
||||||
|
const expandedItems = inject<Ref<number[]>>("expandedItems")! |
||||||
|
const highlighting = inject<Ref<number>>("highlighting")! |
||||||
|
|
||||||
|
const isExpanded = computed(() => expandedItems.value.includes(props.dir)) |
||||||
|
const isSelected = computed(() => store.activeDirectoryId === props.dir) |
||||||
|
const hasIndicative = computed(() => hasChildDirectoryEntries(props.dir)) |
||||||
|
const info = computed(() => getDirectoryEntry(props.dir)) |
||||||
|
|
||||||
|
const handleIndicative = (): void => { |
||||||
|
hideContextMenu() |
||||||
|
const i = expandedItems.value.indexOf(props.dir) |
||||||
|
if (i > -1) expandedItems.value.splice(i, 1) |
||||||
|
else expandedItems.value.push(props.dir) |
||||||
|
} |
||||||
|
|
||||||
|
const handleClick = () => (store.activeDirectoryId = props.dir) |
||||||
|
|
||||||
|
const handleContextmenu = () => { |
||||||
|
once("contextmenu:shown", () => { |
||||||
|
highlighting.value = props.dir |
||||||
|
}) |
||||||
|
once("contextmenu:hidden", () => { |
||||||
|
if (highlighting.value === props.dir) { |
||||||
|
highlighting.value = -1 |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button |
||||||
|
v-if="info" |
||||||
|
:class="{ |
||||||
|
'text-indigo-600': isSelected, |
||||||
|
'bg-black/10': isSelected, |
||||||
|
'bg-black/5': !isSelected && highlighting === dir, |
||||||
|
}" |
||||||
|
:data-directory-id="dir" |
||||||
|
class="w-full flex items-center py-1.5 px-1 text-left hover:bg-black/5 rounded-md" |
||||||
|
data-contextmenu="true" |
||||||
|
data-target="directory" |
||||||
|
@click="handleClick" |
||||||
|
@contextmenu="handleContextmenu" |
||||||
|
> |
||||||
|
<span |
||||||
|
v-if="deep > 0" |
||||||
|
:style="{ width: `${deep}em` }" |
||||||
|
class="block pointer-events-none" |
||||||
|
/> |
||||||
|
<span class="size-[1em] scale-110"> |
||||||
|
<span |
||||||
|
v-if="hasIndicative" |
||||||
|
class="size-full flex flex-col justify-center items-center group" |
||||||
|
@click.stop="handleIndicative" |
||||||
|
> |
||||||
|
<span |
||||||
|
:class='{ "rotate-90": isExpanded }' |
||||||
|
class="size-0 border-[3px] border-transparent border-l-gray-600 group-hover:border-l-gray-900 border-r-0 scale-x-125 origin-center" |
||||||
|
/> |
||||||
|
</span> |
||||||
|
</span> |
||||||
|
<span class="flex pointer-events-none ml-1"> |
||||||
|
<v-icon |
||||||
|
:name="dir === 0 ? 'FileBriefcase' : isExpanded ? 'FolderOpen' : 'Folder'" |
||||||
|
size="1.25em" |
||||||
|
/> |
||||||
|
</span> |
||||||
|
<span |
||||||
|
class="flex-1 pointer-events-none line-clamp-1 ml-1" |
||||||
|
v-text="info.title" |
||||||
|
/> |
||||||
|
</button> |
||||||
|
</template> |
@ -0,0 +1,199 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {getRootBarrier} from './utils' |
||||||
|
import {dispatch} from '../worker' |
||||||
|
import {emit} from '../../shared' |
||||||
|
import {computed, nextTick, onMounted, ref, watch} from 'vue' |
||||||
|
import {store} from '../store' |
||||||
|
import UiDialog from './UiDialog.vue' |
||||||
|
import {controller} from './UiDirectoryNamingController' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
|
||||||
|
const barrier = ref<Node | null>() |
||||||
|
const hasError = ref(false) |
||||||
|
const conformClicked = ref(false) |
||||||
|
const isSubmitting = ref(false) |
||||||
|
const content = ref('') |
||||||
|
|
||||||
|
const title = computed(() => { |
||||||
|
if (controller.action === 'rename') { |
||||||
|
return '重命名文件夹' |
||||||
|
} else if (controller.pid) { |
||||||
|
return "创建子文件夹" |
||||||
|
} else { |
||||||
|
return "创建文件夹" |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const parent = computed(() => { |
||||||
|
return controller.pid |
||||||
|
? store.directories.get(controller.pid) |
||||||
|
: undefined |
||||||
|
}) |
||||||
|
|
||||||
|
const directory = computed(() => { |
||||||
|
return controller.id |
||||||
|
? store.directories.get(controller.id) |
||||||
|
: undefined |
||||||
|
}) |
||||||
|
|
||||||
|
const handleCancel = (): void => { |
||||||
|
if (!isSubmitting.value) { |
||||||
|
controller.visible = false |
||||||
|
nextTick(() => { |
||||||
|
hasError.value = false |
||||||
|
conformClicked.value = false |
||||||
|
isSubmitting.value = false |
||||||
|
content.value = '' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleConfirm = (): void => { |
||||||
|
if (isSubmitting.value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (content.value.trim().length < 2) { |
||||||
|
hasError.value = true |
||||||
|
} |
||||||
|
if (hasError.value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!conformClicked.value) { |
||||||
|
conformClicked.value = true |
||||||
|
return |
||||||
|
} |
||||||
|
isSubmitting.value = true |
||||||
|
requestAnimationFrame(async () => { |
||||||
|
try { |
||||||
|
const title = content.value.trim() |
||||||
|
switch (controller.action) { |
||||||
|
case "create": { |
||||||
|
const pid = parent.value?.id ?? 0 |
||||||
|
const result = await dispatch("dir", "create", {title, pid}); |
||||||
|
store.directories.set(result.id, result); |
||||||
|
toast("success", `成功创建文件夹 “${title}”`) |
||||||
|
if (pid && pid === controller.pid) { |
||||||
|
emit("directory:expanded", parent.value!.id) |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
case "rename": { |
||||||
|
const dir = store.directories.get(directory.value!.id); |
||||||
|
if (!dir) { |
||||||
|
toast("error", "文件夹不存在") |
||||||
|
break |
||||||
|
} |
||||||
|
const res = await dispatch("dir", "rename", {id: dir.id, title}); |
||||||
|
Object.assign(dir, res); |
||||||
|
toast("success", `成功将文件夹 “${directory.value!.title}” 重命名为 “${title}”`) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
isSubmitting.value = false |
||||||
|
handleCancel() |
||||||
|
} catch (err) { |
||||||
|
isSubmitting.value = false |
||||||
|
toast('error', err as any) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
watch(() => controller.visible, (value) => { |
||||||
|
if (value && controller.action === 'rename') { |
||||||
|
content.value = directory.value?.title ?? '' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
watch(content, (value) => { |
||||||
|
conformClicked.value = false |
||||||
|
if (value.trim().length > 1) { |
||||||
|
hasError.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
barrier.value = getRootBarrier() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<UiDialog |
||||||
|
v-if="barrier" |
||||||
|
:barrier="barrier" |
||||||
|
:visible="controller.visible" |
||||||
|
class="w-96 rounded-md overflow-hidden shadow-2xl" |
||||||
|
outside="shake" |
||||||
|
@dismissed="handleCancel" |
||||||
|
> |
||||||
|
<div class="bg-white p-6"> |
||||||
|
<h3 |
||||||
|
id="modal-title" |
||||||
|
class="text-base font-semibold leading-6 text-gray-900" |
||||||
|
v-text="title" |
||||||
|
/> |
||||||
|
<p class="text-xs text-gray-500 mt-1"> |
||||||
|
<template v-if="controller.action === 'rename'"> |
||||||
|
您正在修改文件夹 “<b |
||||||
|
class="text-black align-baseline text-sm">{{ directory!.title }}</b>”;<br/> |
||||||
|
</template> |
||||||
|
<template v-else-if="controller.pid"> |
||||||
|
您正在为 “<b |
||||||
|
class="text-black align-baseline text-sm">{{ parent!.title }}</b>” |
||||||
|
创建子文件夹;<br/> |
||||||
|
</template> |
||||||
|
<span :class="{ 'text-red-500': hasError }"> |
||||||
|
文件夹的名称至少 2 个字符,请尽量不要超过 8 个汉字。 |
||||||
|
</span> |
||||||
|
</p> |
||||||
|
<div class="mt-4"> |
||||||
|
<input |
||||||
|
v-model="content" |
||||||
|
:disabled="isSubmitting" autocomplete="off" |
||||||
|
class="block w-full rounded-md border-0 px-4 py-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 disabled:bg-gray-100 disabled:cursor-progress" |
||||||
|
maxlength="20" minlength="2" |
||||||
|
placeholder="请输入文件夹名称" required |
||||||
|
@blur="hasError = content.length < 2"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
class="bg-gray-50 px-6 py-3 flex justify-end gap-4 border-t border-t-slate-200 text-sm font-semibold"> |
||||||
|
<button |
||||||
|
:disabled="isSubmitting" |
||||||
|
class="rounded-md px-4 py-2 select-none transition-all bg-white text-gray-900 shadow-sm hover:bg-gray-50 ring-1 ring-inset ring-gray-300 disabled:bg-gray-100 disabled:hover:bg-gray-100 disabled:cursor-progress" |
||||||
|
type="button" |
||||||
|
@click="handleCancel" |
||||||
|
> |
||||||
|
取消 |
||||||
|
</button> |
||||||
|
<button |
||||||
|
:disabled="isSubmitting" |
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-md transition-all select-none bg-indigo-600 text-white shadow-sm hover:bg-indigo-500 disabled:bg-indigo-400 disabled:hover:bg-indigo-400 disabled:cursor-progress" |
||||||
|
type="button" |
||||||
|
@click="handleConfirm" |
||||||
|
> |
||||||
|
<svg |
||||||
|
v-if="isSubmitting" |
||||||
|
class="animate-spin h-3 w-3 text-white" |
||||||
|
fill="none" |
||||||
|
viewBox="0 0 24 24" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<circle |
||||||
|
class="opacity-25" |
||||||
|
cx="12" |
||||||
|
cy="12" |
||||||
|
r="10" |
||||||
|
stroke="currentColor" |
||||||
|
stroke-width="4" |
||||||
|
/> |
||||||
|
<path |
||||||
|
class="opacity-75" |
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
||||||
|
fill="currentColor" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
{{ conformClicked ? "确定提交?" : "提交" }} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</UiDialog> |
||||||
|
</template> |
@ -0,0 +1,29 @@ |
|||||||
|
import { reactive } from 'vue' |
||||||
|
|
||||||
|
interface DirectoryNamingOptions { |
||||||
|
pid?: number |
||||||
|
id?: number |
||||||
|
action: 'create' | 'rename' |
||||||
|
} |
||||||
|
|
||||||
|
interface DirectoryNamingState extends DirectoryNamingOptions { |
||||||
|
visible: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export const controller = reactive<DirectoryNamingState>({ |
||||||
|
visible: false, |
||||||
|
pid: undefined, |
||||||
|
id: undefined, |
||||||
|
action: 'create', |
||||||
|
}) |
||||||
|
|
||||||
|
export function hideDirectoryNaming() { |
||||||
|
controller.visible = false |
||||||
|
} |
||||||
|
|
||||||
|
export function showDirectoryNaming(opts: DirectoryNamingOptions): void { |
||||||
|
controller.visible = true |
||||||
|
controller.action = opts.action |
||||||
|
controller.pid = opts.pid |
||||||
|
controller.id = opts.id |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {timestamp} from '../../shared'; |
||||||
|
import {computed, inject, type Ref} from 'vue' |
||||||
|
import {store} from '../store' |
||||||
|
import DirectoryTreeItem from './UiDirectoryTreeItem.vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
pid: number |
||||||
|
deep: number |
||||||
|
}>() |
||||||
|
|
||||||
|
const expandedItems = inject<Ref<number[]>>("expandedItems") |
||||||
|
|
||||||
|
const isExpanded = computed(() => { |
||||||
|
return expandedItems?.value.includes(props.pid) |
||||||
|
}) |
||||||
|
|
||||||
|
const children = computed(() => { |
||||||
|
return Array.from(store.directories.values()).filter(d => d.pid === props.pid).sort((a, b) => { |
||||||
|
if (a.sort != b.sort) return a.sort - b.sort |
||||||
|
return timestamp(a.createdAt) - timestamp(b.createdAt) |
||||||
|
}); |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<ul v-if="children.length && (pid === 0 || isExpanded)" class="w-full"> |
||||||
|
<DirectoryTreeItem |
||||||
|
v-for="item in children" |
||||||
|
:key="item.id" |
||||||
|
:deep="deep" |
||||||
|
:dir="item.id" |
||||||
|
/> |
||||||
|
</ul> |
||||||
|
</template> |
@ -0,0 +1,22 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import DirectoryButton from './UiDirectoryButton.vue' |
||||||
|
import DirectoryTree from './UiDirectoryTree.vue' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
dir: number |
||||||
|
deep: number |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<li class="w-full my-0.5"> |
||||||
|
<DirectoryButton |
||||||
|
:deep="deep" |
||||||
|
:dir="dir" |
||||||
|
/> |
||||||
|
<DirectoryTree |
||||||
|
:deep="deep + 1" |
||||||
|
:pid="dir" |
||||||
|
/> |
||||||
|
</li> |
||||||
|
</template> |
@ -0,0 +1,69 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {store, uploadFiles} from '../store' |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
import {onBeforeUnmount, onMounted} from 'vue' |
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => { |
||||||
|
event.preventDefault(); |
||||||
|
// const target = event.target as HTMLElement |
||||||
|
// store.isDragging = !!target.closest('[data-ui-barrier]'); |
||||||
|
store.isDragging = event.composedPath().some(et => (et instanceof Element) ? et.closest('[data-ui-barrier]') : false) |
||||||
|
} |
||||||
|
|
||||||
|
const handleMouseout = () => { |
||||||
|
store.isDragging = false |
||||||
|
} |
||||||
|
|
||||||
|
const handleDrop = (ev: DragEvent): void => { |
||||||
|
// 阻止浏览器的默认释放行为 |
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop |
||||||
|
ev.preventDefault(); |
||||||
|
|
||||||
|
if (store.isDragging) { |
||||||
|
store.isDragging = false |
||||||
|
|
||||||
|
if (store.activeDirectoryId >= 0) { |
||||||
|
const directoryId = store.activeDirectoryId |
||||||
|
if (ev.dataTransfer?.items) { |
||||||
|
for (const item of ev.dataTransfer.items) { |
||||||
|
const file = item.getAsFile() |
||||||
|
file && uploadFiles(directoryId, file) |
||||||
|
} |
||||||
|
} else if (ev.dataTransfer?.files) { |
||||||
|
uploadFiles(directoryId, ev.dataTransfer.files) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
console.log("dragover drop mouseout") |
||||||
|
document.addEventListener('dragover', handleDragOver) |
||||||
|
document.addEventListener('drop', handleDrop) |
||||||
|
document.addEventListener('mouseout', handleMouseout) |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
document.removeEventListener('dragover', handleDragOver) |
||||||
|
document.removeEventListener('drop', handleDrop) |
||||||
|
document.removeEventListener('mouseout', handleMouseout) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Transition enter-from-class="opacity-0" leave-to-class="opacity-0"> |
||||||
|
<div |
||||||
|
v-if="store.isDragging" |
||||||
|
class="absolute z-50 inset-0 overflow-hidden pointer-events-none |
||||||
|
flex flex-col justify-center items-center transition-opacity |
||||||
|
w-full h-full bg-black/30 backdrop-blur-sm" |
||||||
|
data-drop-feedback |
||||||
|
> |
||||||
|
<div |
||||||
|
class="p-3 rounded-md bg-gray-100/90 flex text-sm items-center gap-1"> |
||||||
|
<VIcon name="InfoSolid" size="20"/> |
||||||
|
<span>释放鼠标开始上传文件</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Transition> |
||||||
|
</template> |
@ -0,0 +1,101 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import VText from './VText.vue' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
import {once} from '../../shared' |
||||||
|
import {computed, inject, type Ref, ref} from 'vue' |
||||||
|
import {store, toggleSelect} from '../store' |
||||||
|
import VBadge from './VBadge.vue' |
||||||
|
import UiFileThumbnail from './UiFileThumbnail.vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
file: CloudFile |
||||||
|
}>() |
||||||
|
|
||||||
|
const isSelected = computed((): boolean => { |
||||||
|
return store.selected.some(f => f.id === props.file.id) |
||||||
|
}) |
||||||
|
|
||||||
|
const index = computed((): number => { |
||||||
|
const index = store.selected.findIndex(f => f.id === props.file.id) |
||||||
|
return index > -1 ? index + 1 : 0 |
||||||
|
}) |
||||||
|
|
||||||
|
const elmRef = ref<HTMLDivElement>() |
||||||
|
|
||||||
|
const scrollIntoView = () => { |
||||||
|
elmRef.value?.scrollIntoView({ |
||||||
|
behavior: 'smooth', |
||||||
|
block: 'nearest', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleClick = () => { |
||||||
|
const {status} = props.file.object |
||||||
|
if (status < 3) { |
||||||
|
toast("warn", "文件处理中,暂时不可选") |
||||||
|
} else if (status > 3) { |
||||||
|
toast("error", props.file.object.error ?? '文件错误,无法选择') |
||||||
|
} else { |
||||||
|
scrollIntoView() |
||||||
|
toggleSelect(props.file) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onNameClick = () => { |
||||||
|
scrollIntoView() |
||||||
|
} |
||||||
|
|
||||||
|
const highlighting = inject<Ref<number>>("highlighting")! |
||||||
|
|
||||||
|
const handleContextmenu = () => { |
||||||
|
once("contextmenu:shown", () => { |
||||||
|
highlighting.value = props.file.id |
||||||
|
}) |
||||||
|
once("contextmenu:hidden", () => { |
||||||
|
if (highlighting.value === props.file.id) { |
||||||
|
highlighting.value = -1 |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div ref="elmRef" class="relative w-32 p-2 z-0"> |
||||||
|
<div class="w-28 h-28 p-1 relative rounded-lg"> |
||||||
|
<template v-if="isSelected || highlighting === file.id"> |
||||||
|
<div |
||||||
|
:class="{ |
||||||
|
'ring-2': isSelected, |
||||||
|
'ring-inset': isSelected, |
||||||
|
'ring-indigo-600': isSelected, |
||||||
|
}" |
||||||
|
class="absolute left-0 right-0 top-0 bottom-0 bg-black bg-opacity-5 rounded-lg" |
||||||
|
/> |
||||||
|
<v-badge |
||||||
|
v-if="index > 0 && isSelected && store.multiple" |
||||||
|
:value="index" |
||||||
|
class="bg-red-500 z-10" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
<div class="size-full flex-center p-0.5 select-none"> |
||||||
|
<UiFileThumbnail |
||||||
|
:file="file" |
||||||
|
@click="handleClick" |
||||||
|
@contextmenu="handleContextmenu" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
class="w-28 h-9 px-1 py-1 text-center line-clamp-2 text-ellipsis leading-4 text-xs"> |
||||||
|
<v-text |
||||||
|
:data-directory-id="file.directoryId" |
||||||
|
:data-file-id="file.id" |
||||||
|
:text="file.name" |
||||||
|
data-contextmenu="true" |
||||||
|
data-target="file" |
||||||
|
@click="onNameClick" |
||||||
|
@contextmenu="handleContextmenu" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,157 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {store, updateFile} from '../store' |
||||||
|
import {getRootBarrier} from './utils' |
||||||
|
import {dispatch} from '../worker' |
||||||
|
import {computed, nextTick, onMounted, ref, watch} from 'vue' |
||||||
|
import UiDialog from './UiDialog.vue' |
||||||
|
import {controller} from './UiFileNamingController' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
|
||||||
|
const barrier = ref<Node | null>() |
||||||
|
const hasError = ref(false) |
||||||
|
const conformClicked = ref(false) |
||||||
|
const isSubmitting = ref(false) |
||||||
|
const content = ref('') |
||||||
|
|
||||||
|
const file = computed(() => { |
||||||
|
return controller.dirId != null && controller.id |
||||||
|
? store.files[controller.dirId]?.find((f: CloudFile) => f.id === controller.id) |
||||||
|
: undefined |
||||||
|
}) |
||||||
|
|
||||||
|
const handleCancel = (): void => { |
||||||
|
if (!isSubmitting.value) { |
||||||
|
controller.visible = false |
||||||
|
nextTick(() => { |
||||||
|
hasError.value = false |
||||||
|
conformClicked.value = false |
||||||
|
isSubmitting.value = false |
||||||
|
content.value = '' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleConfirm = (): void => { |
||||||
|
if (isSubmitting.value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (content.value.trim().length < 2) { |
||||||
|
hasError.value = true |
||||||
|
} |
||||||
|
if (hasError.value) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!conformClicked.value) { |
||||||
|
conformClicked.value = true |
||||||
|
return |
||||||
|
} |
||||||
|
isSubmitting.value = true |
||||||
|
requestAnimationFrame(async () => { |
||||||
|
try { |
||||||
|
const id = controller.id! |
||||||
|
const name = content.value.trim() |
||||||
|
const res = await dispatch("file", "rename", {id, name}) |
||||||
|
updateFile(res) |
||||||
|
isSubmitting.value = false |
||||||
|
handleCancel() |
||||||
|
} catch (err) { |
||||||
|
isSubmitting.value = false |
||||||
|
toast('error', err as any) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
watch(() => controller.visible, (value) => { |
||||||
|
if (value) { |
||||||
|
content.value = file.value?.name ?? '' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
watch(content, (value) => { |
||||||
|
conformClicked.value = false |
||||||
|
if (value.trim().length > 1) { |
||||||
|
hasError.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
barrier.value = getRootBarrier() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<UiDialog |
||||||
|
v-if="barrier" |
||||||
|
:barrier="barrier" |
||||||
|
:visible="controller.visible" |
||||||
|
class="w-96 rounded-md overflow-hidden shadow-2xl" |
||||||
|
outside="shake" |
||||||
|
@dismissed="handleCancel" |
||||||
|
> |
||||||
|
<div class="bg-white p-6"> |
||||||
|
<h3 id="modal-title" |
||||||
|
class="text-base font-semibold leading-6 text-gray-900"> |
||||||
|
文件重命名 |
||||||
|
</h3> |
||||||
|
<p class="text-xs text-gray-500 mt-1"> |
||||||
|
您正在重命名文件 “<b |
||||||
|
class="text-black align-baseline text-sm">{{ file!.name }}</b>”;<br/> |
||||||
|
<span :class="{'text-red-500': hasError}"> |
||||||
|
文件夹的名称至少 2 个字符,请尽量不要超过 50 个汉字。 |
||||||
|
</span> |
||||||
|
</p> |
||||||
|
<div class="mt-4"> |
||||||
|
<input |
||||||
|
v-model="content" |
||||||
|
:disabled="isSubmitting" |
||||||
|
autocomplete="off" |
||||||
|
class="block w-full rounded-md border-0 px-4 py-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 disabled:bg-gray-100 disabled:cursor-progress" |
||||||
|
maxlength="20" |
||||||
|
minlength="2" |
||||||
|
placeholder="请输入文件夹名称" |
||||||
|
required |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
class="bg-gray-50 px-6 py-3 flex justify-end gap-4 border-t border-t-slate-200 text-sm font-semibold"> |
||||||
|
<button |
||||||
|
:disabled="isSubmitting" |
||||||
|
class="rounded-md px-4 py-2 select-none transition-all bg-white text-gray-900 shadow-sm hover:bg-gray-50 ring-1 ring-inset ring-gray-300 disabled:bg-gray-100 disabled:hover:bg-gray-100 disabled:cursor-progress" |
||||||
|
type="button" |
||||||
|
@click="handleCancel" |
||||||
|
> |
||||||
|
取消 |
||||||
|
</button> |
||||||
|
<button |
||||||
|
:disabled="isSubmitting" |
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-md transition-all select-none bg-indigo-600 text-white shadow-sm hover:bg-indigo-500 disabled:bg-indigo-400 disabled:hover:bg-indigo-400 disabled:cursor-progress" |
||||||
|
type="button" |
||||||
|
@click="handleConfirm" |
||||||
|
> |
||||||
|
<svg |
||||||
|
v-if="isSubmitting" |
||||||
|
class="animate-spin h-3 w-3 text-white" |
||||||
|
fill="none" |
||||||
|
viewBox="0 0 24 24" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<circle |
||||||
|
class="opacity-25" |
||||||
|
cx="12" |
||||||
|
cy="12" |
||||||
|
r="10" |
||||||
|
stroke="currentColor" |
||||||
|
stroke-width="4" |
||||||
|
/> |
||||||
|
<path |
||||||
|
class="opacity-75" |
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
||||||
|
fill="currentColor" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
{{ conformClicked ? "确定提交?" : "提交" }} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</UiDialog> |
||||||
|
</template> |
@ -0,0 +1,25 @@ |
|||||||
|
import { reactive } from 'vue' |
||||||
|
|
||||||
|
interface FileNamingOptions { |
||||||
|
dirId?: number |
||||||
|
id?: number |
||||||
|
} |
||||||
|
|
||||||
|
interface FileNamingState extends FileNamingOptions { |
||||||
|
visible: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export const controller = reactive<FileNamingState>({ |
||||||
|
visible: false, |
||||||
|
dirId: undefined, |
||||||
|
id: undefined, |
||||||
|
}) |
||||||
|
|
||||||
|
export function hideFileNaming(): void { |
||||||
|
controller.visible = false |
||||||
|
} |
||||||
|
|
||||||
|
export function showFileNaming(opts: Required<FileNamingOptions>): void { |
||||||
|
Object.assign(controller, opts) |
||||||
|
controller.visible = true |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
import {computed} from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
file: CloudFile |
||||||
|
}>() |
||||||
|
|
||||||
|
const classes = computed(() => { |
||||||
|
if (props.file.object.status !== 3) { |
||||||
|
return ["size-full", 'p-2'] |
||||||
|
} else if (props.file.object.width < props.file.object.height) { |
||||||
|
return ["h-full", "w-auto", "min-w-[1em]"] |
||||||
|
} else { |
||||||
|
return ["w-full", "h-auto", "min-h-[1em]"] |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
:class="classes" |
||||||
|
:data-directory-id="file.directoryId" |
||||||
|
:data-file-id="file.id" |
||||||
|
class="p-0.5 relative flex-center bg-white text-xs ring-2 ring-inset ring-white ring-offset-1 drop-shadow" |
||||||
|
data-contextmenu="true" |
||||||
|
data-target="file" |
||||||
|
> |
||||||
|
<template v-if="file.object.status < 3" class="flex items-center"> |
||||||
|
<v-icon class="size-full text-gray-500" name="FileSync"/> |
||||||
|
</template> |
||||||
|
<template v-else-if="file.object.status > 3"> |
||||||
|
<v-icon name="FileError" size="14"/> |
||||||
|
<span class="mt-1 text-gray-500">处理失败</span> |
||||||
|
</template> |
||||||
|
<img |
||||||
|
v-else |
||||||
|
:alt="file.name" |
||||||
|
:class="classes.join(' ')" |
||||||
|
:loading="'lazy'" |
||||||
|
:src="file.object.thumb" |
||||||
|
class="pointer-events-none" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,83 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {store, useFiler} from '../store' |
||||||
|
import UiFile from './UiFile.vue' |
||||||
|
import UiMistake from './UiMistake.vue' |
||||||
|
import UiNoData from './UiNoData.vue' |
||||||
|
import UiPagination from './UiPagination.vue' |
||||||
|
import VLoading from './VLoading.vue' |
||||||
|
import {emit, timestamp} from '../../shared' |
||||||
|
import {computed, provide, ref} from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
directoryId: number |
||||||
|
}>() |
||||||
|
|
||||||
|
const filer = useFiler(props.directoryId) |
||||||
|
const highlighting = ref(-1) |
||||||
|
const maxPage = computed(() => Math.ceil(filer.total / filer.limit)) |
||||||
|
const list = computed(() => (store.files[props.directoryId] ?? []) |
||||||
|
.sort((a, b) => timestamp(a.createdAt) - timestamp(b.createdAt)) |
||||||
|
.reverse()) |
||||||
|
|
||||||
|
provide("highlighting", highlighting) |
||||||
|
|
||||||
|
const reload = (): void => emit(`files:${props.directoryId}:reload`) |
||||||
|
const jump = (to: number): void => emit(`files:${props.directoryId}:jump`, to) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<VLoading |
||||||
|
v-if="filer.view === 'loader'" |
||||||
|
class="w-full h-full" |
||||||
|
/> |
||||||
|
<UiMistake |
||||||
|
v-else-if="filer.view === 'error'" |
||||||
|
:message="filer.error" |
||||||
|
@refresh="reload" |
||||||
|
/> |
||||||
|
<UiNoData |
||||||
|
v-else-if="filer.view === 'empty'" |
||||||
|
:data-directory-id="directoryId" |
||||||
|
data-contextmenu="true" |
||||||
|
data-target="content" |
||||||
|
subtitle="支持单次和批量上传,仅能够上传图片、视频和音频文件,严禁上传公司数据或其他违禁文件" |
||||||
|
title="拖拽文件至此处即可上传" |
||||||
|
@refresh="reload" |
||||||
|
/> |
||||||
|
<div |
||||||
|
v-else |
||||||
|
:data-directory-id="directoryId" |
||||||
|
class="flex-1 flex flex-col items-stretch" |
||||||
|
data-contextmenu="true" |
||||||
|
data-target="content" |
||||||
|
> |
||||||
|
<div class="relative flex-1 overflow-y-auto"> |
||||||
|
<div |
||||||
|
class="flex-1 flex flex-wrap items-start justify-start content-start p-4"> |
||||||
|
<UiFile |
||||||
|
v-for="file in list" |
||||||
|
:key="file.id" |
||||||
|
:file="file" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
class="border-t border-slate-200 w-full flex items-center leading-tight text-sm select-none"> |
||||||
|
<span class="py-1 px-2">共 {{ filer.total }} 条记录</span> |
||||||
|
<span |
||||||
|
v-if="store.mode === 'select' && store.selected.length > 0" |
||||||
|
class="border-l py-1 px-2" |
||||||
|
> |
||||||
|
已选中 {{ store.selected.length }} 项 |
||||||
|
</span> |
||||||
|
<div class="flex-1"/> |
||||||
|
<UiPagination |
||||||
|
v-if="maxPage > 1" |
||||||
|
:current="filer.page" |
||||||
|
:total="maxPage" |
||||||
|
class="mx-2" |
||||||
|
@change="jump" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,64 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type {IconName} from './VIcons' |
||||||
|
import UiButton from './VButton.vue' |
||||||
|
import {hideContextMenu} from './UiContextMenuController' |
||||||
|
import VDropdown from './VDropdown.vue' |
||||||
|
import {store} from '../store' |
||||||
|
|
||||||
|
const items: Array<{ |
||||||
|
id: FileMime |
||||||
|
icon: IconName |
||||||
|
label: string |
||||||
|
}> = [ |
||||||
|
{ |
||||||
|
id: "image", |
||||||
|
icon: "Image", |
||||||
|
label: "只看图片" |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "video", |
||||||
|
icon: "Video", |
||||||
|
label: "只看视频", |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: "all", |
||||||
|
icon: "FileMultiple", |
||||||
|
label: "图片和视频", |
||||||
|
} |
||||||
|
] |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<v-dropdown |
||||||
|
:disabled="store.lockMime" |
||||||
|
:tooltip="store.lockMime ? null : '切换要显示的文件类型'" |
||||||
|
@open="hideContextMenu" |
||||||
|
> |
||||||
|
<template #default> |
||||||
|
<svg |
||||||
|
class="pointer-events-none size-5" viewBox="0 0 24 24" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path |
||||||
|
d="M5.5 5h13a1 1 0 0 1 .5 1.5L14 12v7l-4-3v-4L5 6.5A1 1 0 0 1 5.5 5" |
||||||
|
fill="none" stroke="currentColor" |
||||||
|
stroke-linecap="round" stroke-linejoin="round" |
||||||
|
stroke-width="2"></path> |
||||||
|
</svg> |
||||||
|
{{ items.find(i => i!.id === store.mime)!.label }} |
||||||
|
</template> |
||||||
|
<template #content> |
||||||
|
<UiButton |
||||||
|
v-for="item in items" |
||||||
|
:key="item.id" |
||||||
|
:icon="item.icon" |
||||||
|
:label="item.label" |
||||||
|
class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-indigo-500 hover:text-white text-sm select-none" |
||||||
|
icon-size="1.5em" |
||||||
|
type="button" |
||||||
|
@click="store.mime = item.id" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
</v-dropdown> |
||||||
|
</template> |
||||||
|
|
@ -0,0 +1,30 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
defineProps<{ |
||||||
|
message?: string |
||||||
|
}>() |
||||||
|
|
||||||
|
defineEmits<{ |
||||||
|
(type: "refresh"): void |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="size-full flex-center"> |
||||||
|
<div class="text-sm font-semibold dark:text-white"> |
||||||
|
<slot name="message"> |
||||||
|
{{ message ?? '哦哦,我们遇到了一些问题!' }} |
||||||
|
</slot> |
||||||
|
</div> |
||||||
|
<slot name="button"> |
||||||
|
<button |
||||||
|
class=" |
||||||
|
mx-auto mt-4 px-4 py-2 rounded-lg transition-all text-sm font-semibold |
||||||
|
bg-gray-50 ring-1 ring-inset ring-gray-400/75 focus:ring-2 focus:ring-indigo-500 |
||||||
|
shadow-sm dark:bg-transparent dark:text-white |
||||||
|
" |
||||||
|
@pointerup="$nextTick(() => $emit('refresh'))" |
||||||
|
v-text="'刷新'" |
||||||
|
/> |
||||||
|
</slot> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,65 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {deleteFile, store} from '../store' |
||||||
|
import VButton from './VButton.vue' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
import {dispatch} from '../worker.ts' |
||||||
|
import {computed, ref} from 'vue' |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'confirm'): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const isPending = ref(false) |
||||||
|
|
||||||
|
const hasSelected = computed(() => store.selected.length > 0) |
||||||
|
const isManageMode = computed(() => store.mode === 'manage') |
||||||
|
|
||||||
|
const handleConfirm = async () => { |
||||||
|
if (!hasSelected.value) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (store.mode === 'select') { |
||||||
|
emit('confirm') |
||||||
|
return |
||||||
|
} |
||||||
|
isPending.value = true |
||||||
|
const files = store.selected.slice() |
||||||
|
const deletes = Array.from(new Set(files.map(f => f.id))) |
||||||
|
dispatch('file', "delete", deletes) |
||||||
|
.then(() => { |
||||||
|
store.selected = store.selected.filter(f => deletes.includes(f.id)) |
||||||
|
files.forEach(deleteFile) |
||||||
|
toast('success', '操作成功') |
||||||
|
}) |
||||||
|
.catch(error => toast('error', error)) |
||||||
|
.finally(() => isPending.value = false) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<transition enter-from-class="opacity-0" leave-to-class="opacity-0"> |
||||||
|
<div v-if="hasSelected" class="flex items-center transition-all gap-4"> |
||||||
|
<v-button |
||||||
|
:disabled="isPending" |
||||||
|
class="bg-white border hover:bg-gray-50 text-sm font-semibold h-8 focus:ring-4 transition-all focus:ring-gray-100 shadow-sm" |
||||||
|
label="取消2" |
||||||
|
@click="store.selected = []" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
:class="{ |
||||||
|
'bg-red-500': isManageMode, |
||||||
|
'hover:bg-red-600': isManageMode, |
||||||
|
'focus:ring-red-200': isManageMode, |
||||||
|
'bg-indigo-500': !isManageMode, |
||||||
|
'hover:bg-indigo-600': !isManageMode, |
||||||
|
'focus:ring-indigo-200': !isManageMode, |
||||||
|
}" |
||||||
|
:disabled="isPending" |
||||||
|
:label="isManageMode ? '删除' : '确定'" |
||||||
|
class="text-sm font-semibold h-8 focus:ring-4 transition-all text-white shadow-sm" |
||||||
|
@click="handleConfirm" |
||||||
|
/> |
||||||
|
<div class="w-[1px] h-4 bg-gray-100 mx-2 flex-shrink-0"/> |
||||||
|
</div> |
||||||
|
</transition> |
||||||
|
</template> |
@ -0,0 +1,54 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {store} from '../store' |
||||||
|
import VBadge from './VBadge.vue' |
||||||
|
import VIconButton from './VIconButton.vue' |
||||||
|
import UiMimeSelect from './UiMimeSelect.vue' |
||||||
|
import UiModeTools from './UiModeTools.vue' |
||||||
|
import UiThemeSelect from './UiThemeSelect.vue' |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'dismiss'): void |
||||||
|
(ev: 'confirm'): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const handleDismiss = () => { |
||||||
|
store.showTasksPanel = false |
||||||
|
emit('dismiss') |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="relative z-50 h-12 px-2 flex-shrink-0 flex gap-2 items-center content-center"> |
||||||
|
<div class="flex-1"/> |
||||||
|
<UiModeTools @confirm="$emit('confirm')"/> |
||||||
|
<UiMimeSelect/> |
||||||
|
<UiThemeSelect/> |
||||||
|
<div/> |
||||||
|
<div class="flex relative"> |
||||||
|
<v-icon-button |
||||||
|
:class="{ |
||||||
|
'ring-2': store.showTasksPanel, |
||||||
|
'ring-indigo-500': store.showTasksPanel, |
||||||
|
}" |
||||||
|
data-tasks-trigger |
||||||
|
icon="TaskListSolid" |
||||||
|
tooltip="任务列表" |
||||||
|
@click="store.showTasksPanel = !store.showTasksPanel" |
||||||
|
/> |
||||||
|
<v-badge |
||||||
|
v-if="store.tasks.length > 0" |
||||||
|
:value="store.tasks.length" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<template v-if="store.mode === 'select'"> |
||||||
|
<div class="w-[1px] h-4 bg-gray-100 mx-2 flex-shrink-0"/> |
||||||
|
<v-icon-button |
||||||
|
class="focus:ring-red-500 text-red-500" |
||||||
|
icon="DismissSolid" |
||||||
|
tooltip="关闭" |
||||||
|
@click="handleDismiss" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,40 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
title?: string |
||||||
|
subtitle?: string |
||||||
|
}>() |
||||||
|
|
||||||
|
defineEmits<{ |
||||||
|
(ev: 'refresh'): void |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="w-full h-full flex-center select-none"> |
||||||
|
<div class="max-w-96 text-center pointer-events-none"> |
||||||
|
<v-icon |
||||||
|
class="text-gray-300 transition-colors" |
||||||
|
name="MailInbox" |
||||||
|
size="4em" |
||||||
|
/> |
||||||
|
<h2 v-if="$slots.title || title" class="line-clamp-1 mt-4"> |
||||||
|
<slot name="title">{{ title }}</slot> |
||||||
|
</h2> |
||||||
|
<p v-if="$slots.subtitle || subtitle" class="text-sm text-gray-500 mt-2"> |
||||||
|
<slot name="subtitle">{{ subtitle }}</slot> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<button |
||||||
|
class=" |
||||||
|
flex items-center px-4 py-2 rounded-xl font-semibold mt-4 mx-auto bg-gray-100 text-sm |
||||||
|
hover:text-indigo-600 hover:bg-indigo-100 transition-colors ring-1 ring-inset ring-slate-300/50 |
||||||
|
focus:ring-2 focus:ring-indigo-500 |
||||||
|
" |
||||||
|
@click.stop="$emit('refresh')" |
||||||
|
> |
||||||
|
<span>点我刷新</span> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,21 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
forward: boolean |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div> |
||||||
|
<span |
||||||
|
class="opacity-0 group-hover:opacity-100 transition-colors flex flex-col justify-center items-center absolute inset-0"> |
||||||
|
<v-icon v-if="forward" class="scale-75" name="ChevronDoubleRight"/> |
||||||
|
<v-icon v-else class="scale-75" name="ChevronDoubleLeft"/> |
||||||
|
</span> |
||||||
|
<span |
||||||
|
class="opacity-100 group-hover:opacity-0 flex flex-col justify-center items-center"> |
||||||
|
<v-icon name="MoreHorizontal"/> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,32 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
defineProps<{ |
||||||
|
active?: boolean |
||||||
|
disabled?: boolean |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button |
||||||
|
:class='{ |
||||||
|
"text-indigo-600": active, |
||||||
|
"cursor-default": active, |
||||||
|
"hover:bg-gray-100": !active, |
||||||
|
"hover:text-indigo-600": active, |
||||||
|
}' |
||||||
|
:disabled="disabled" |
||||||
|
class=" |
||||||
|
relative inline-flex flex-col justify-center items-center leading-tight |
||||||
|
px-1 group |
||||||
|
text-sm font-semibold text-gray-900 |
||||||
|
focus:outline-offset-0 |
||||||
|
hover:text-indigo-600 |
||||||
|
disabled:text-inherit disabled:bg-transparent disabled:opacity-40 |
||||||
|
min-w-[2em] min-h-[2em] |
||||||
|
" |
||||||
|
type="button" |
||||||
|
@contextmenu.stop.prevent="" |
||||||
|
> |
||||||
|
<slot /> |
||||||
|
</button> |
||||||
|
</template> |
||||||
|
|
@ -0,0 +1,83 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {type Page, paginate} from '../../shared' |
||||||
|
import {computed} from 'vue' |
||||||
|
import UiPaginateEllipsis from './UiPaginateEllipsis.vue' |
||||||
|
import UiPaginateItem from './UiPaginateItem.vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
total: number // 总页数 |
||||||
|
current: number // 当前页数 |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'change', value: number): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const pages = computed(() => paginate(props.current, props.total)) |
||||||
|
|
||||||
|
const handleClick = (to: number) => { |
||||||
|
if (to !== props.current) { |
||||||
|
emit('change', to) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handlePage = (page: Page): void => { |
||||||
|
if (!page.ellipsis) { |
||||||
|
handleClick(page.value as number) |
||||||
|
} else if (page.forward) { |
||||||
|
handleClick(Math.min(props.current + 5, props.total)) |
||||||
|
} else { |
||||||
|
handleClick(Math.min(props.current - 5, props.total)) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<nav |
||||||
|
aria-label="Pagination" |
||||||
|
class="isolate inline-flex -space-x-px gap-x-1.5" |
||||||
|
> |
||||||
|
<UiPaginateItem |
||||||
|
:disabled="current === 1" |
||||||
|
@click="handleClick(props.current - 1)" |
||||||
|
> |
||||||
|
<svg |
||||||
|
aria-hidden="true" |
||||||
|
class="h-5 w-5" |
||||||
|
fill="currentColor" |
||||||
|
viewBox="0 0 20 20" |
||||||
|
> |
||||||
|
<path |
||||||
|
clip-rule="evenodd" |
||||||
|
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" |
||||||
|
fill-rule="evenodd" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</UiPaginateItem> |
||||||
|
<UiPaginateItem |
||||||
|
v-for="page in pages" |
||||||
|
:active="page.value === current" |
||||||
|
@click="handlePage(page)" |
||||||
|
> |
||||||
|
<UiPaginateEllipsis v-if="page.ellipsis" :forward="page.forward"/> |
||||||
|
<template v-else>{{ page.value }}</template> |
||||||
|
</UiPaginateItem> |
||||||
|
<UiPaginateItem |
||||||
|
:disabled="current === total" |
||||||
|
@click="handleClick(props.current + 1)" |
||||||
|
> |
||||||
|
<svg |
||||||
|
aria-hidden="true" |
||||||
|
class="size-5" |
||||||
|
fill="currentColor" |
||||||
|
viewBox="0 0 20 20" |
||||||
|
> |
||||||
|
<path |
||||||
|
clip-rule="evenodd" |
||||||
|
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" |
||||||
|
fill-rule="evenodd" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</UiPaginateItem> |
||||||
|
</nav> |
||||||
|
</template> |
@ -0,0 +1,90 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import logo from '../assets/logo.png' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
import {getScrollBarWidth} from '../utils' |
||||||
|
import {coerce, off, on, sleep} from '../../shared' |
||||||
|
import {onBeforeUnmount, onMounted, provide, ref} from 'vue' |
||||||
|
import {refreshDirectories, store} from '../store' |
||||||
|
import DirectoryButton from './UiDirectoryButton.vue' |
||||||
|
import DirectoryTree from './UiDirectoryTree.vue' |
||||||
|
|
||||||
|
const isLoading = ref(false) |
||||||
|
const expandedItems = ref<number[]>([]) |
||||||
|
const highlighting = ref(-1) |
||||||
|
const scrollWidth = ref(0) |
||||||
|
|
||||||
|
provide("expandedItems", expandedItems) |
||||||
|
provide("highlighting", highlighting) |
||||||
|
|
||||||
|
const refresh = async (): Promise<void> => { |
||||||
|
if (!isLoading.value) { |
||||||
|
isLoading.value = true |
||||||
|
await sleep(300) |
||||||
|
await refreshDirectories() |
||||||
|
.then(() => toast('success', "刷新侧边栏成功")) |
||||||
|
.catch((err) => toast('error', `刷新侧边栏失败,原因:${coerce(err)}`)) |
||||||
|
.then(() => sleep(300)) |
||||||
|
.finally(() => isLoading.value = false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onContextmenuDismissed = () => (highlighting.value = -1); |
||||||
|
const onExpanded = (id: number): void => { |
||||||
|
while (id > 0) { |
||||||
|
const dir = store.directories.get(id) |
||||||
|
if (!dir) return |
||||||
|
if (!expandedItems.value.includes(id)) { |
||||||
|
expandedItems.value.push(id) |
||||||
|
} |
||||||
|
id = dir.pid |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
on("contextmenu:dismissed", onContextmenuDismissed) |
||||||
|
on("dirs:refresh", refresh) |
||||||
|
on("directory:expanded", onExpanded) |
||||||
|
|
||||||
|
scrollWidth.value = getScrollBarWidth() |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
off("contextmenu:dismissed", onContextmenuDismissed) |
||||||
|
off("dirs:refresh", refresh) |
||||||
|
off("directory:expanded", onExpanded) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="relative h-full flex flex-col items-stretch" |
||||||
|
data-contextmenu="true" |
||||||
|
data-directory-id="0" |
||||||
|
data-target="sidebar" |
||||||
|
> |
||||||
|
<div class="flex content-center items-center px-2 h-12 gap-2 select-none"> |
||||||
|
<span |
||||||
|
class="flex-center size-8 flex-shrink-0 pointer-events-none select-none"> |
||||||
|
<img |
||||||
|
:src="logo" |
||||||
|
alt="资源管理器" |
||||||
|
class="size-8" |
||||||
|
/> |
||||||
|
</span> |
||||||
|
<span class="flex-1">资源管理器</span> |
||||||
|
<span |
||||||
|
v-if="isLoading" |
||||||
|
class="block size-4 border-2 border-gray-300 rounded-full border-l-black animate-spin ease-in-out" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
:style="{marginRight: `${-scrollWidth}px`}" |
||||||
|
class="flex-1 text-sm overflow-y-scroll p-2" |
||||||
|
> |
||||||
|
<div class="mb-0.5"> |
||||||
|
<DirectoryButton :deep="0" :dir="0"/> |
||||||
|
</div> |
||||||
|
<DirectoryTree :deep="0" :pid="0"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,63 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {ref} from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
size: number |
||||||
|
min: number |
||||||
|
max: number |
||||||
|
disabled?: boolean |
||||||
|
}>() |
||||||
|
|
||||||
|
const isDraggingRef = ref(false) |
||||||
|
const dragElmRef = ref<HTMLElement | null>(null) |
||||||
|
const currentSize = ref(256) |
||||||
|
|
||||||
|
const handleMouseMove = (evt: MouseEvent) => { |
||||||
|
evt.preventDefault() |
||||||
|
isDraggingRef.value = true |
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent): void => { |
||||||
|
updateSize(e) |
||||||
|
} |
||||||
|
|
||||||
|
const onMouseUp = (): void => { |
||||||
|
document.removeEventListener('mousemove', onMouseMove) |
||||||
|
document.removeEventListener('mouseup', onMouseUp) |
||||||
|
isDraggingRef.value = false |
||||||
|
document.body.style.cursor = '' |
||||||
|
} |
||||||
|
|
||||||
|
document.body.style.cursor = 'col-resize' |
||||||
|
document.addEventListener('mousemove', onMouseMove) |
||||||
|
document.addEventListener('mouseup', onMouseUp) |
||||||
|
|
||||||
|
updateSize(evt) |
||||||
|
} |
||||||
|
|
||||||
|
const updateSize = (event: MouseEvent): void => { |
||||||
|
const parentRect = dragElmRef.value?.parentElement?.getBoundingClientRect() |
||||||
|
if (!parentRect) return |
||||||
|
let newSize = event.clientX - parentRect.left |
||||||
|
if (props.min > 0) newSize = Math.max(newSize, props.min) |
||||||
|
if (props.max > 0) newSize = Math.min(newSize, 356) |
||||||
|
currentSize.value = Math.max(newSize, 0) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex items-stretch h-full w-full"> |
||||||
|
<div :style="{flexBasis: `${currentSize}px`}" class="relative z-0"> |
||||||
|
<slot name="secondary"/> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
v-if="!disabled" |
||||||
|
ref="dragElmRef" |
||||||
|
:class="{'bg-green-700': isDraggingRef}" |
||||||
|
class="relative z-20 w-1 h-full -mx-0.5 hover:bg-green-700 transition-colors cursor-col-resize" |
||||||
|
@mousedown="handleMouseMove" |
||||||
|
/> |
||||||
|
<div class="relative z-10 flex-1"> |
||||||
|
<slot name="primary"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,134 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {dispatch} from '../worker' |
||||||
|
import { |
||||||
|
humanSize, |
||||||
|
isCompleted, |
||||||
|
isInvalid, |
||||||
|
isValid, |
||||||
|
sleep, |
||||||
|
statusIndex |
||||||
|
} from '../../shared' |
||||||
|
import {ref, watch} from 'vue' |
||||||
|
import {showPreview} from '../utils' |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
import UiTaskAction from './UiTaskAction.vue' |
||||||
|
import {toast} from './UiToastController' |
||||||
|
import VProgress from './VProgress.vue' |
||||||
|
import {vTooltip} from '../directives' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
task: Task |
||||||
|
}>() |
||||||
|
|
||||||
|
const isActing = ref(false) |
||||||
|
const progress = ref(0) |
||||||
|
|
||||||
|
const handleAction = async (action: "cancel" | "remove" | "resume") => { |
||||||
|
const {hash: fileHash, id: taskId} = props.task |
||||||
|
try { |
||||||
|
isActing.value = true |
||||||
|
await sleep(300) |
||||||
|
await dispatch("task", action as "cancel", {fileHash, taskId}) |
||||||
|
} catch (error) { |
||||||
|
toast('error', error as any) |
||||||
|
} |
||||||
|
isActing.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const handlePreview = () => { |
||||||
|
showPreview({ |
||||||
|
name: props.task.name, |
||||||
|
mime: props.task.mime, |
||||||
|
thumb: props.task.path, // todo first frame for video |
||||||
|
path: props.task.path, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
watch(props.task, (t) => { |
||||||
|
if (isInvalid(t)) { |
||||||
|
progress.value = 1 |
||||||
|
} else if (statusIndex(t.status) <= statusIndex("readend")) { |
||||||
|
progress.value = (t.progress ?? 0) / 2 |
||||||
|
} else if (!isCompleted(t)) { |
||||||
|
progress.value = ((t.progress ?? 0) + 1) / 2 |
||||||
|
} |
||||||
|
progress.value = 0.678 |
||||||
|
}, {immediate: true}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="relative w-full text-sm text-gray-500 py-2 px-3 group"> |
||||||
|
<div |
||||||
|
:class="{'text-red-500': isInvalid(task)}" |
||||||
|
class="flex items-center gap-2 mb-1 text-sm" |
||||||
|
> |
||||||
|
<span v-if="isInvalid(task)" class="size-4 flex-center"> |
||||||
|
<v-icon |
||||||
|
v-tooltip="task.error ?? '上传失败'" |
||||||
|
class="text-red-500" |
||||||
|
name="ErrorCircleSolid" |
||||||
|
size="1.5em" |
||||||
|
/> |
||||||
|
</span> |
||||||
|
<span |
||||||
|
v-else-if="isValid(task)" |
||||||
|
class="size-4 border-2 border-gray-300 rounded-full border-l-black animate-spin ease-in-out" |
||||||
|
/> |
||||||
|
<span v-else> |
||||||
|
<v-icon |
||||||
|
class="text-green-500" |
||||||
|
name="CheckmarkCircleSolid" |
||||||
|
size="1.5em" |
||||||
|
/> |
||||||
|
</span> |
||||||
|
<span class="size-4 flex-center"> |
||||||
|
<v-icon :name="task.mime.startsWith('image/') ? 'Image' : 'Video'"/> |
||||||
|
</span> |
||||||
|
<div class="line-clamp-1 flex-1 font-semibold break-all"> |
||||||
|
{{ task.name }} |
||||||
|
</div> |
||||||
|
<v-progress |
||||||
|
:class="{hidden: !isValid(task)}" |
||||||
|
:label="progress.toString()" |
||||||
|
:value="progress" |
||||||
|
class="w-20" |
||||||
|
/> |
||||||
|
<div |
||||||
|
class="w-16 text-right text-gray-600 select-none group-hover:opacity-0"> |
||||||
|
{{ humanSize(task.size) }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div |
||||||
|
class="absolute inset-y-0 right-0 flex gap-2 items-center opacity-0 group-hover:opacity-100"> |
||||||
|
<div class="flex items-center gap-0.5"> |
||||||
|
<UiTaskAction |
||||||
|
icon="Eye" |
||||||
|
tooltip="预览文件" |
||||||
|
@click="handlePreview" |
||||||
|
/> |
||||||
|
<UiTaskAction |
||||||
|
v-if="isValid(task)" |
||||||
|
:disabled="isActing" |
||||||
|
icon="SyncOff" |
||||||
|
tooltip="取消" |
||||||
|
@click="handleAction('cancel')" |
||||||
|
/> |
||||||
|
<UiTaskAction |
||||||
|
v-if="isInvalid(task)" |
||||||
|
:disabled="isActing" |
||||||
|
icon="Sync" |
||||||
|
tooltip="恢复" |
||||||
|
@click="handleAction('resume')" |
||||||
|
/> |
||||||
|
<UiTaskAction |
||||||
|
:disabled="isActing" |
||||||
|
class="focus:text-red-600 hover:text-red-600" |
||||||
|
icon="Delete" |
||||||
|
tooltip="删除" |
||||||
|
@click="handleAction('remove')" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,25 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type {IconName} from './VIcons' |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
import {vTooltip} from '../directives' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
icon: IconName |
||||||
|
tooltip: string |
||||||
|
disabled?: boolean |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button |
||||||
|
v-tooltip="tooltip" |
||||||
|
:disabled="disabled" |
||||||
|
class=" |
||||||
|
flex-shrink-0 inline-flex justify-center items-center text-gray-900 rounded text-xs size-6 opacity-65 |
||||||
|
hover:text-indigo-600 focus:text-indigo-600 hover:opacity-100 focus:opacity-100 |
||||||
|
" |
||||||
|
type="button" |
||||||
|
> |
||||||
|
<v-icon :name="icon"/> |
||||||
|
</button> |
||||||
|
</template> |
@ -0,0 +1,82 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {store} from '../store' |
||||||
|
import UiTask from './UiTask.vue' |
||||||
|
import {dispatch} from '../worker' |
||||||
|
import {isCompleted, isInvalid, isValid} from '../../shared' |
||||||
|
import {computed, ref} from 'vue' |
||||||
|
import VIcon from './VIcon.vue' |
||||||
|
|
||||||
|
const tasks = computed(() => { |
||||||
|
const list = store.tasks.sort((a, b) => b.id - a.id) |
||||||
|
const completes = list.filter(isCompleted).length |
||||||
|
const invalids = list.filter(isInvalid).length |
||||||
|
const pends = list.filter(isValid).length |
||||||
|
return { |
||||||
|
list, |
||||||
|
completes, |
||||||
|
invalids, |
||||||
|
pends, |
||||||
|
length: completes + invalids + pends, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const cleaning = ref(false) |
||||||
|
const clean = async () => { |
||||||
|
cleaning.value = true |
||||||
|
await dispatch('task', 'cleanup') |
||||||
|
cleaning.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Transition |
||||||
|
enter-from-class="-translate-y-1/3 opacity-0" |
||||||
|
leave-to-class="-translate-y-1/3 opacity-0" |
||||||
|
> |
||||||
|
<div |
||||||
|
v-show="store.showTasksPanel" |
||||||
|
class=" |
||||||
|
transition-all |
||||||
|
absolute right-0 top-12 mt-2 mr-2 z-10 |
||||||
|
max-h-[400px] w-[400px] min-w-[260px] bg-white text-sm rounded-lg overflow-hidden |
||||||
|
shadow-pop backdrop-blur-[8px] pseudo-border flex flex-col |
||||||
|
" |
||||||
|
data-tasks-panel |
||||||
|
> |
||||||
|
<div |
||||||
|
class="flex-shrink-0 bg-gray-50 border-b pl-2 flex items-center justify-between"> |
||||||
|
<div class="py-1">任务列表</div> |
||||||
|
<button |
||||||
|
v-if="tasks.completes || tasks.invalids" |
||||||
|
:disabled="cleaning" |
||||||
|
class="hover:bg-gray-200 h-full p-1" |
||||||
|
@click="clean" |
||||||
|
> |
||||||
|
清空 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="flex-1 overflow-y-auto overflow-hidden"> |
||||||
|
<div |
||||||
|
v-if="!tasks.length" |
||||||
|
class="flex justify-center items-center gap-2 p-8 opacity-40 select-none" |
||||||
|
> |
||||||
|
<VIcon name="MailInbox"/> |
||||||
|
<div>没有上传任务</div> |
||||||
|
</div> |
||||||
|
<template v-for="(task, index) in tasks.list" v-else :key="task.id"> |
||||||
|
<div v-if="index > 0" class="border-t border-slate-50"/> |
||||||
|
<UiTask :task="task"/> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
v-if="tasks.length" |
||||||
|
class="border-t px-2 py-1 text-center text-xs text-gray-500 select-none" |
||||||
|
> |
||||||
|
<span>共 {{ tasks.length }} 项</span> |
||||||
|
<span v-if="tasks.completes">,完成 {{ tasks.completes }} 项</span> |
||||||
|
<span v-if="tasks.invalids">,失败{{ tasks.invalids }}项</span> |
||||||
|
<span v-if="tasks.pends">,有 {{ tasks.pends }} 项正在进行中</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Transition> |
||||||
|
</template> |
@ -0,0 +1,40 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {useTheme} from '../hooks' |
||||||
|
import {hideContextMenu} from './UiContextMenuController' |
||||||
|
import VDropdown from './VDropdown.vue' |
||||||
|
import VButton from "./VButton.vue"; |
||||||
|
|
||||||
|
const {light, use} = useTheme() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<v-dropdown |
||||||
|
:icon="light ? 'SunSolid' : 'MoonSolid'" |
||||||
|
tooltip="切换主题" |
||||||
|
@open="hideContextMenu" |
||||||
|
> |
||||||
|
<template #content> |
||||||
|
<v-button |
||||||
|
class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-indigo-500 hover:text-white text-sm select-none" |
||||||
|
icon="SunSolid" |
||||||
|
icon-size="1.5em" |
||||||
|
label="浅色" |
||||||
|
@click="use = 'light'" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-indigo-500 hover:text-white text-sm select-none" |
||||||
|
icon="MoonSolid" |
||||||
|
icon-size="1.5em" |
||||||
|
label="深色" |
||||||
|
@click="use = 'dark'" |
||||||
|
/> |
||||||
|
<v-button |
||||||
|
class="flex items-center gap-2 px-3 py-1.5 rounded hover:bg-indigo-500 hover:text-white text-sm select-none" |
||||||
|
icon="DesktopSolid" |
||||||
|
icon-size="1.5em" |
||||||
|
label="自动" |
||||||
|
@click="use = 'system'" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
</v-dropdown> |
||||||
|
</template> |
@ -0,0 +1,85 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type {ToastType} from './UiToastController' |
||||||
|
import type {IconName} from "./VIcons"; |
||||||
|
import {computed, onMounted, onUnmounted, watch} from 'vue' |
||||||
|
import VIcon from "./VIcon.vue"; |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
type?: ToastType, |
||||||
|
message: string |
||||||
|
duration?: number |
||||||
|
interact?: boolean |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'dismiss'): void |
||||||
|
(ev: 'interact', value: boolean): void |
||||||
|
}>() |
||||||
|
|
||||||
|
type IconInfo = { |
||||||
|
class: string; name: IconName |
||||||
|
} |
||||||
|
|
||||||
|
const icon = computed((): IconInfo => { |
||||||
|
return ({ |
||||||
|
'success': {name: 'CheckmarkCircleSolid', class: 'text-green-500'}, |
||||||
|
'error': {name: 'DismissCircleSolid', class: 'text-red-500'}, |
||||||
|
'warn': {name: 'ErrorCircleSolid', class: 'text-yellow-500'}, |
||||||
|
'info': {name: 'InfoSolid', class: 'text-indigo-500'}, |
||||||
|
} as Record<ToastType, IconInfo>)[props.type ?? 'info'] |
||||||
|
}) |
||||||
|
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null |
||||||
|
|
||||||
|
const stop = () => { |
||||||
|
if (timer) { |
||||||
|
clearTimeout(timer) |
||||||
|
timer = null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const start = () => { |
||||||
|
const {duration = 3000} = props |
||||||
|
if (duration > 0) { |
||||||
|
timer = setTimeout(() => { |
||||||
|
emit('dismiss') |
||||||
|
}, duration) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleMouseEnter = () => { |
||||||
|
emit('interact', true) |
||||||
|
stop() |
||||||
|
} |
||||||
|
|
||||||
|
const handleMouseLeave = () => { |
||||||
|
emit('interact', false) |
||||||
|
start() |
||||||
|
} |
||||||
|
|
||||||
|
watch(() => props.interact, value => { |
||||||
|
stop() |
||||||
|
if (!value) { |
||||||
|
start() |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(start) |
||||||
|
onUnmounted(stop) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class=" |
||||||
|
flex items-center max-w-96 cursor-pointer p-2 mb-2 rounded-lg shadow-lg |
||||||
|
select-none backdrop-blur-sm bg-white/90 ring-1 ring-black/10 |
||||||
|
" |
||||||
|
role="alert" |
||||||
|
@mouseenter="handleMouseEnter" |
||||||
|
@mouseleave="handleMouseLeave" |
||||||
|
@click.stop="$emit('dismiss')" |
||||||
|
> |
||||||
|
<VIcon size="1.5em" v-bind="icon"/> |
||||||
|
<div class="ms-1 text-sm font-normal" v-html="message"/> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,24 @@ |
|||||||
|
import {coerce} from "../../shared"; |
||||||
|
import {ref} from "vue"; |
||||||
|
|
||||||
|
export type ToastType = "success" | "error" | "warn" | "info"; |
||||||
|
|
||||||
|
export interface Toast { |
||||||
|
key: number; |
||||||
|
type: ToastType; |
||||||
|
message: string; |
||||||
|
duration: number |
||||||
|
} |
||||||
|
|
||||||
|
export const items = ref<Array<Toast>>([]); |
||||||
|
|
||||||
|
let nextKey = 0; |
||||||
|
|
||||||
|
export function toast(type: ToastType, message: any, duration: number = 3000): void { |
||||||
|
items.value.unshift({ |
||||||
|
key: ++nextKey, |
||||||
|
type, |
||||||
|
message: String(coerce(message)), |
||||||
|
duration |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {computed, ref} from 'vue' |
||||||
|
import UiToast from './UiToast.vue' |
||||||
|
import {items} from './UiToastController' |
||||||
|
|
||||||
|
const renders = computed(() => items.value.slice(0, 4)) |
||||||
|
const interact = ref(false) |
||||||
|
|
||||||
|
const handleDismiss = (key: number): void => { |
||||||
|
const i = items.value.findIndex(i => i.key === key) |
||||||
|
i > -1 && items.value.splice(i, 1) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="absolute top-0 left-0 right-0 mt-12 z-40"> |
||||||
|
<div class="absolute left-2 top-2 flex flex-col items-center"> |
||||||
|
<TransitionGroup |
||||||
|
appear |
||||||
|
enter-from-class="-translate-y-full opacity-0 -mb-8" |
||||||
|
leave-to-class="-translate-y-full opacity-0 -mb-8" |
||||||
|
move-class="transition-all" |
||||||
|
> |
||||||
|
<UiToast |
||||||
|
v-for="(item, index) in renders" |
||||||
|
:key="item.key" |
||||||
|
:duration="item.duration" |
||||||
|
:interact="interact" |
||||||
|
:message="item.message" |
||||||
|
:style="{zIndex: renders.length - index}" |
||||||
|
:type="item.type" |
||||||
|
class="transition-all duration-200 relative" |
||||||
|
@dismiss="handleDismiss(item.key)" |
||||||
|
@interact="interact = $event" |
||||||
|
/> |
||||||
|
</TransitionGroup> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,14 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
defineProps<{ |
||||||
|
value?: string | number |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<span class=" |
||||||
|
absolute top-0 right-0 inline-flex items-center justify-center rounded-full px-1 min-w-[1.325em] -translate-y-[45%] translate-x-[45%] |
||||||
|
select-none text-xs font-medium text-white bg-indigo-500 ring-2 ring-white ring-opacity-90 drop-shadow |
||||||
|
"> |
||||||
|
<slot>{{ value }}</slot> |
||||||
|
</span> |
||||||
|
</template> |
@ -0,0 +1,37 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type {IconName} from './VIcons' |
||||||
|
import VIcon from "./VIcon.vue"; |
||||||
|
import {vTooltip} from '../directives' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
type?: 'button' | 'submit' | 'reset' |
||||||
|
icon?: IconName |
||||||
|
label?: string |
||||||
|
iconSize?: number | string |
||||||
|
disabled?: boolean |
||||||
|
tooltip?: string | number |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button |
||||||
|
v-tooltip="tooltip" |
||||||
|
:disabled="disabled" |
||||||
|
:type="type" |
||||||
|
class="flex items-center flex-shrink-0 gap-2 py-1 px-3 hover:bg-gray-100 rounded" |
||||||
|
> |
||||||
|
<v-icon |
||||||
|
v-if="icon" |
||||||
|
:name="icon" |
||||||
|
:size="iconSize" |
||||||
|
class="pointer-events-none" |
||||||
|
/> |
||||||
|
<slot> |
||||||
|
<span |
||||||
|
v-if="label != null" |
||||||
|
class="pointer-events-none" |
||||||
|
v-html="label" |
||||||
|
/> |
||||||
|
</slot> |
||||||
|
</button> |
||||||
|
</template> |
@ -0,0 +1,124 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {store} from '../store' |
||||||
|
import type {IconName} from './VIcons' |
||||||
|
import UiButton from './VButton.vue' |
||||||
|
import {getRootBarrier} from './utils' |
||||||
|
import { |
||||||
|
type ComponentPublicInstance, |
||||||
|
computed, |
||||||
|
onMounted, |
||||||
|
ref, |
||||||
|
watch |
||||||
|
} from 'vue' |
||||||
|
import UiIconButton from "./VIconButton.vue"; |
||||||
|
import {onClickOutside, useEventListener} from "@vueuse/core"; |
||||||
|
import type {Placement} from "../utils"; |
||||||
|
import {alignWithElement} from "../utils"; |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
offset?: [number, number], |
||||||
|
placement?: Placement |
||||||
|
disabled?: boolean |
||||||
|
icon?: IconName |
||||||
|
tooltip?: any |
||||||
|
}>() |
||||||
|
|
||||||
|
defineOptions({ |
||||||
|
inheritAttrs: false, |
||||||
|
}) |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'open'): void |
||||||
|
(ev: 'close'): void |
||||||
|
}>() |
||||||
|
|
||||||
|
defineSlots<{ |
||||||
|
default(props: { open: boolean }): any |
||||||
|
content(): any |
||||||
|
}>() |
||||||
|
|
||||||
|
const barrier = ref<Node | null>() |
||||||
|
const trigger = ref<ComponentPublicInstance | null>(null) |
||||||
|
const panel = ref<HTMLElement | null>(null) |
||||||
|
const visible = ref(false) |
||||||
|
|
||||||
|
const button = computed(() => { |
||||||
|
return trigger.value != null |
||||||
|
? trigger.value instanceof Node ? trigger.value : trigger.value.$el |
||||||
|
: null |
||||||
|
}) |
||||||
|
|
||||||
|
const handlePanelClick = (evt: Event) => { |
||||||
|
const el = evt.target as HTMLElement |
||||||
|
if (el.tagName === 'BUTTON') { |
||||||
|
visible.value = false |
||||||
|
emit('close') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
watch(visible, () => { |
||||||
|
if (button.value && panel.value) { |
||||||
|
if (visible.value) { |
||||||
|
store.showTasksPanel = false |
||||||
|
} |
||||||
|
alignWithElement( |
||||||
|
button.value, |
||||||
|
panel.value, |
||||||
|
props.placement ?? 'bottom-left', |
||||||
|
props.offset ?? [0, 6] |
||||||
|
) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
useEventListener( |
||||||
|
computed(() => trigger.value?.$el), |
||||||
|
"click", () => { |
||||||
|
emit('open') |
||||||
|
visible.value = true |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
onClickOutside(panel, () => { |
||||||
|
visible.value = false |
||||||
|
emit('close') |
||||||
|
}, {ignore: [trigger]}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
barrier.value = getRootBarrier() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Component |
||||||
|
:is="!$slots.default ? UiIconButton : UiButton" |
||||||
|
ref="trigger" |
||||||
|
:class="{ |
||||||
|
'ring-indigo-500': visible, |
||||||
|
'ring-2': visible, |
||||||
|
}" |
||||||
|
:disabled="disabled" |
||||||
|
:icon="icon" |
||||||
|
:tooltip="tooltip" |
||||||
|
class="h-8 text-sm font-semibold" |
||||||
|
v-bind="$attrs" |
||||||
|
> |
||||||
|
<slot :open="visible"/> |
||||||
|
</Component> |
||||||
|
|
||||||
|
<Teleport v-if="barrier" :to="barrier"> |
||||||
|
<div ref="panel" class="absolute top-10 left-10 z-50"> |
||||||
|
<transition |
||||||
|
enter-from-class="opacity-0 scale-95 -translate-y-[4px]" |
||||||
|
leave-to-class="opacity-0 scale-95 -translate-y-[4px]" |
||||||
|
> |
||||||
|
<div |
||||||
|
v-if="visible" |
||||||
|
class="transition-all origin-top-left ease-out flex flex-col items-stretch gap-0.5 p-1.5 popup-panel" |
||||||
|
@click="handlePanelClick" |
||||||
|
> |
||||||
|
<slot name="content"/> |
||||||
|
</div> |
||||||
|
</transition> |
||||||
|
</div> |
||||||
|
</Teleport> |
||||||
|
</template> |
@ -0,0 +1,21 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {type IconName} from './VIcons' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
size?: string | number |
||||||
|
name: IconName |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<svg |
||||||
|
:height="size ?? '1.25em'" |
||||||
|
:width="size ?? '1.25em'" |
||||||
|
class="inline-block" |
||||||
|
fill="currentColor" |
||||||
|
viewBox="0 0 20 20" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<use :href="`#${name}`" class="pointer-events-none"/> |
||||||
|
</svg> |
||||||
|
</template> |
@ -0,0 +1,38 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type {IconName} from './VIcons' |
||||||
|
import VIcon from "./VIcon.vue"; |
||||||
|
import {vTooltip} from '../directives' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
icon: IconName |
||||||
|
iconSize?: number | string |
||||||
|
type?: "submit" | "reset" | "button" | undefined |
||||||
|
variant?: "square" | "circle" |
||||||
|
disabled?: boolean |
||||||
|
rounded?: boolean |
||||||
|
primary?: boolean |
||||||
|
tooltip?: string | number |
||||||
|
}>() |
||||||
|
|
||||||
|
defineEmits<{ |
||||||
|
(ev: 'click'): void |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<button |
||||||
|
v-tooltip="tooltip" |
||||||
|
:class="{ |
||||||
|
'rounded-full': variant === 'circle', |
||||||
|
'rounded': variant == null, |
||||||
|
'hover:bg-indigo-500': primary, |
||||||
|
'hover:text-white': primary, |
||||||
|
}" |
||||||
|
:disabled="disabled" |
||||||
|
:type="type ?? 'button'" |
||||||
|
class="inline-flex justify-center items-center w-8 h-8 font-semibold leading-tight hover:bg-gray-100 disabled:bg-transparent disabled:cursor-not-allowed disabled:opacity-75 focus:ring-2 focus:ring-indigo-500" |
||||||
|
@click="$emit('click')" |
||||||
|
> |
||||||
|
<v-icon :name="icon" :size="iconSize"/> |
||||||
|
</button> |
||||||
|
</template> |
@ -0,0 +1,516 @@ |
|||||||
|
export const paths = { |
||||||
|
'Folder': 'M7.167 3c.27 0 .535.073.765.21l.135.09l1.6 1.2H15.5a2.5 2.5 0 0 1 2.479 2.174l.016.162L18 7v7.5a2.5 2.5 0 0 1-2.336 2.495L15.5 17h-11a2.5 2.5 0 0 1-2.495-2.336L2 14.5v-9a2.5 2.5 0 0 1 2.336-2.495L4.5 3h2.667zm.99 4.034a1.5 1.5 0 0 1-.933.458l-.153.008L3 7.499V14.5a1.5 1.5 0 0 0 1.356 1.493L4.5 16h11a1.5 1.5 0 0 0 1.493-1.355L17 14.5V7a1.5 1.5 0 0 0-1.355-1.493L15.5 5.5H9.617l-1.46 1.534zM7.168 4H4.5a1.5 1.5 0 0 0-1.493 1.356L3 5.5v.999l4.071.001a.5.5 0 0 0 .302-.101l.06-.054L8.694 5.02L7.467 4.1a.5.5 0 0 0-.22-.093L7.167 4z', |
||||||
|
'FolderSolid': 'M10.565 4.5H15.5a2.5 2.5 0 0 1 2.479 2.174l.016.162L18 7v7.5a2.5 2.5 0 0 1-2.336 2.495L15.5 17h-11a2.5 2.5 0 0 1-2.495-2.336L2 14.5v-7h5.07l.154-.008a1.5 1.5 0 0 0 .823-.353l.111-.106L10.565 4.5zM7.167 3c.27 0 .535.073.765.21l.135.09l1.318.989l-1.952 2.055l-.06.055a.5.5 0 0 1-.221.094l-.081.007H2v-1a2.5 2.5 0 0 1 2.336-2.495L4.5 3h2.667z', |
||||||
|
'FolderAdd': 'M4.5 3A2.5 2.5 0 0 0 2 5.5v9A2.5 2.5 0 0 0 4.5 17h5.1a5.465 5.465 0 0 1-.393-1H4.5A1.5 1.5 0 0 1 3 14.5v-7h4.071a1.5 1.5 0 0 0 1.087-.466L9.619 5.5H15.5A1.5 1.5 0 0 1 17 7v2.6c.358.183.693.404 1 .657V7a2.5 2.5 0 0 0-2.5-2.5H9.667l-1.6-1.2a1.5 1.5 0 0 0-.9-.3H4.5zM3 5.5A1.5 1.5 0 0 1 4.5 4h2.667a.5.5 0 0 1 .3.1l1.227.92l-1.26 1.325a.5.5 0 0 1-.363.155H3v-1zm16 9a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V14h-1.5a.5.5 0 0 0 0 1H14v1.5a.5.5 0 0 0 1 0V15h1.5a.5.5 0 0 0 0-1H15v-1.5z', |
||||||
|
'FolderAddSolid': 'M9.386 4.29l-1.32-.99a1.5 1.5 0 0 0-.9-.3H4.5A2.5 2.5 0 0 0 2 5.5v1h5.07a.5.5 0 0 0 .363-.156L9.386 4.29zm1.179.21L8.158 7.033a1.5 1.5 0 0 1-1.087.467H2v7A2.5 2.5 0 0 0 4.5 17h5.1a5.5 5.5 0 0 1 8.4-6.743V7l-.005-.164A2.5 2.5 0 0 0 15.5 4.5h-4.935zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V14h-1.5a.5.5 0 0 0 0 1H14v1.5a.5.5 0 0 0 1 0V15h1.5a.5.5 0 0 0 0-1H15v-1.5z', |
||||||
|
'FolderArrowRight': 'M4.5 3A2.5 2.5 0 0 0 2 5.5v9A2.5 2.5 0 0 0 4.5 17h5.1a5.465 5.465 0 0 1-.393-1H4.5A1.5 1.5 0 0 1 3 14.5v-7h4.071a1.5 1.5 0 0 0 1.087-.466L9.619 5.5H15.5A1.5 1.5 0 0 1 17 7v2.6c.358.183.693.404 1 .657V7a2.5 2.5 0 0 0-2.5-2.5H9.667l-1.6-1.2a1.5 1.5 0 0 0-.9-.3H4.5zM3 5.5A1.5 1.5 0 0 1 4.5 4h2.667a.5.5 0 0 1 .3.1l1.227.92l-1.26 1.325a.5.5 0 0 1-.363.155H3v-1zM14.5 10a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9zm2.353 4.854l.003-.003a.499.499 0 0 0 .144-.348v-.006a.5.5 0 0 0-.146-.35l-2-2a.5.5 0 0 0-.708.707L15.293 14H12.5a.5.5 0 0 0 0 1h2.793l-1.147 1.146a.5.5 0 0 0 .708.708l2-2z', |
||||||
|
'FolderArrowRightSolid': 'M9.386 4.29l-1.32-.99a1.5 1.5 0 0 0-.9-.3H4.5A2.5 2.5 0 0 0 2 5.5v1h5.07a.5.5 0 0 0 .363-.156L9.386 4.29zm1.179.21L8.158 7.033a1.5 1.5 0 0 1-1.087.467H2v7A2.5 2.5 0 0 0 4.5 17h5.1a5.5 5.5 0 0 1 8.4-6.743V7l-.005-.164A2.5 2.5 0 0 0 15.5 4.5h-4.935zM14.5 10a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9zm2.353 4.854l.003-.003a.499.499 0 0 0 .144-.348v-.006a.5.5 0 0 0-.146-.35l-2-2a.5.5 0 0 0-.708.707L15.293 14H12.5a.5.5 0 0 0 0 1h2.793l-1.147 1.146a.5.5 0 0 0 .708.708l2-2z', |
||||||
|
'FolderArrowUp': 'M4.5 3h2.667c.324 0 .64.105.9.3l1.6 1.2H15.5A2.5 2.5 0 0 1 18 7v3.257a5.503 5.503 0 0 0-1-.657V7a1.5 1.5 0 0 0-1.5-1.5H9.62L8.157 7.034A1.5 1.5 0 0 1 7.07 7.5H3v7A1.5 1.5 0 0 0 4.5 16h4.707c.099.349.23.683.393 1H4.5A2.5 2.5 0 0 1 2 14.5v-9A2.5 2.5 0 0 1 4.5 3zM3 5.5v1h4.071a.5.5 0 0 0 .363-.155l1.26-1.324L7.467 4.1a.5.5 0 0 0-.3-.1H4.5A1.5 1.5 0 0 0 3 5.5zm16 9a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4.146-2.353l-.003-.003a.497.497 0 0 0-.348-.144h-.006a.498.498 0 0 0-.35.146l-2 2a.5.5 0 0 0 .707.708L14 13.707V16.5a.5.5 0 1 0 1 0v-2.793l1.146 1.147a.5.5 0 1 0 .707-.707l-2-2z', |
||||||
|
'FolderArrowUpSolid': 'M8.067 3.3l1.319.99l-1.953 2.054a.5.5 0 0 1-.362.156H2v-1A2.5 2.5 0 0 1 4.5 3h2.667c.324 0 .64.105.9.3zm.091 3.733L10.565 4.5H15.5a2.5 2.5 0 0 1 2.495 2.336L18 7v3.257A5.5 5.5 0 0 0 9.6 17H4.5A2.5 2.5 0 0 1 2 14.5v-7h5.07a1.5 1.5 0 0 0 1.088-.467zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4.146-2.353l-.003-.003a.497.497 0 0 0-.348-.144h-.006a.498.498 0 0 0-.35.146l-2 2a.5.5 0 0 0 .707.708L14 13.707V16.5a.5.5 0 1 0 1 0v-2.793l1.146 1.147a.5.5 0 1 0 .707-.707l-2-2z', |
||||||
|
'FolderOpen': 'M16.996 7.073V7a2.5 2.5 0 0 0-2.5-2.5H9.664l-1.6-1.2a1.5 1.5 0 0 0-.9-.3H4.5A2.5 2.5 0 0 0 2 5.5l.001 8.998a2.5 2.5 0 0 0 2.201 2.482c.085.014.172.022.26.022H15.18a1.5 1.5 0 0 0 1.472-1.214l1.358-7a1.501 1.501 0 0 0-1.014-1.715zM4.5 4h2.664a.5.5 0 0 1 .3.1l1.734 1.3a.5.5 0 0 0 .3.1h4.998a1.5 1.5 0 0 1 1.5 1.5v.002H5.824a1.5 1.5 0 0 0-1.472 1.214l-1.298 6.676A1.502 1.502 0 0 1 3 14.498L3 5.5A1.5 1.5 0 0 1 4.5 4zm.833 4.407a.5.5 0 0 1 .491-.405h10.713a.5.5 0 0 1 .491.595l-1.357 7a.5.5 0 0 1-.491.405H4.463a.5.5 0 0 1-.49-.595l1.36-7z', |
||||||
|
'FolderOpenSolid': 'M2 5.5A2.5 2.5 0 0 1 4.5 3h2.664c.325 0 .64.105.9.3l1.6 1.2h4.832a2.5 2.5 0 0 1 2.5 2.5v.002H5.824A1.5 1.5 0 0 0 4.35 8.215l-1.577 8.09a2.493 2.493 0 0 1-.773-1.807L2 5.5zm1.773 10.907a.5.5 0 0 0 .491.595H15.18a1.5 1.5 0 0 0 1.472-1.214l1.395-7.19a.5.5 0 0 0-.491-.596H5.824a.5.5 0 0 0-.491.404l-1.56 8z', |
||||||
|
'FolderOpenVertical': 'M4 3.5A1.5 1.5 0 0 1 5.5 2h9A1.5 1.5 0 0 1 16 3.5v3.877c0 .123-.015.245-.045.364L15 11.56V14.5a1.5 1.5 0 0 1-1.5 1.5H12v.742a1.5 1.5 0 0 1-2.04 1.4L5.6 16.46A2.5 2.5 0 0 1 4 14.128V3.5zM7.186 3L10.4 4.24A2.5 2.5 0 0 1 12 6.572V15h1.5a.5.5 0 0 0 .5-.5v-3c0-.04.005-.082.015-.121l.97-3.88A.5.5 0 0 0 15 7.376V3.5a.5.5 0 0 0-.5-.5H7.186zM5 3.958v10.17a1.5 1.5 0 0 0 .96 1.4l4.36 1.681a.5.5 0 0 0 .68-.466V6.572a1.5 1.5 0 0 0-.96-1.4L5.68 3.49a.5.5 0 0 0-.68.467z', |
||||||
|
'FolderOpenVerticalSolid': 'M4.137 2.873C4.049 3.063 4 3.276 4 3.5v10.628a2.5 2.5 0 0 0 1.6 2.332l4.36 1.682c.355.137.72.13 1.04.015V6.568a1.5 1.5 0 0 0-.956-1.398L4.137 2.873zm.797-.763l5.472 2.128A2.5 2.5 0 0 1 12 6.568V16h1.5a1.5 1.5 0 0 0 1.5-1.5v-2.938l.955-3.821c.03-.12.045-.241.045-.364V3.5A1.5 1.5 0 0 0 14.5 2h-9c-.2 0-.391.04-.566.11z', |
||||||
|
'FolderProhibited': 'M2 5.5A2.5 2.5 0 0 1 4.5 3h2.667c.324 0 .64.105.9.3l1.6 1.2H15.5A2.5 2.5 0 0 1 18 7v3.257a5.503 5.503 0 0 0-1-.657V7a1.5 1.5 0 0 0-1.5-1.5H9.62L8.157 7.034A1.5 1.5 0 0 1 7.07 7.5H3v7A1.5 1.5 0 0 0 4.5 16h4.707c.099.349.23.683.393 1H4.5A2.5 2.5 0 0 1 2 14.5v-9zM4.5 4A1.5 1.5 0 0 0 3 5.5v1h4.071a.5.5 0 0 0 .363-.155l1.26-1.324L7.467 4.1a.5.5 0 0 0-.3-.1H4.5zM10 14.5a4.5 4.5 0 1 1 9 0a4.5 4.5 0 0 1-9 0zm4.5-3.5a3.5 3.5 0 0 0-2.803 5.596l4.9-4.9A3.484 3.484 0 0 0 14.5 11zm2.803 1.404l-4.9 4.9a3.5 3.5 0 0 0 4.9-4.9z', |
||||||
|
'FolderProhibitedSolid': 'M9.386 4.29l-1.32-.99a1.5 1.5 0 0 0-.9-.3H4.5A2.5 2.5 0 0 0 2 5.5v1h5.07a.5.5 0 0 0 .363-.156L9.386 4.29zm1.179.21L8.158 7.033a1.5 1.5 0 0 1-1.087.467H2v7A2.5 2.5 0 0 0 4.5 17h5.1a5.5 5.5 0 0 1 8.4-6.743V7l-.005-.164A2.5 2.5 0 0 0 15.5 4.5h-4.935zM10 14.5a4.5 4.5 0 1 1 9 0a4.5 4.5 0 0 1-9 0zm4.5-3.5a3.5 3.5 0 0 0-2.803 5.596l4.9-4.9A3.484 3.484 0 0 0 14.5 11zm0 7a3.5 3.5 0 0 0 2.803-5.596l-4.9 4.9c.585.437 1.31.696 2.097.696z', |
||||||
|
'FolderSwap': 'M7.932 3.21A1.5 1.5 0 0 0 7.167 3H4.5l-.164.005A2.5 2.5 0 0 0 2 5.5v9l.005.164A2.5 2.5 0 0 0 4.5 17h4.377l-.436-.434A1.5 1.5 0 0 1 8.085 16H4.5l-.144-.007A1.5 1.5 0 0 1 3 14.5V7.499l4.071.001l.153-.008a1.5 1.5 0 0 0 .934-.458L9.617 5.5H15.5l.145.007A1.5 1.5 0 0 1 17 7v5.883l1 1V7l-.005-.164l-.016-.162A2.5 2.5 0 0 0 15.5 4.5H9.667l-1.6-1.2l-.135-.09zM4.5 4h2.667l.08.006a.5.5 0 0 1 .22.094l1.227.921l-1.26 1.324l-.061.054a.5.5 0 0 1-.302.101L3 6.499V5.5l.007-.144A1.5 1.5 0 0 1 4.5 4zm7.356 9.859a.5.5 0 0 0-.706-.708l-2.003 1.998a.5.5 0 0 0 0 .708l2.003 1.997a.5.5 0 1 0 .706-.708l-1.147-1.144h5.584l-1.146 1.144a.5.5 0 1 0 .706.708l2-1.996a.5.5 0 0 0 0-.708l-2-1.999a.5.5 0 1 0-.707.707l1.145 1.144H10.71l1.146-1.143z', |
||||||
|
'FolderSwapSolid': 'M10.565 4.5H15.5a2.5 2.5 0 0 1 2.479 2.174l.016.162L18 7v6.883l-1.44-1.44a1.5 1.5 0 0 0-2.475 1.56h-1.166a1.5 1.5 0 0 0-2.475-1.56L8.44 14.44a1.5 1.5 0 0 0 0 2.125l.436.434H4.5a2.5 2.5 0 0 1-2.495-2.336L2 14.5v-7h5.07l.154-.008a1.5 1.5 0 0 0 .823-.353l.111-.106L10.565 4.5zM7.167 3c.27 0 .535.073.765.21l.135.09l1.318.989l-1.952 2.055l-.06.055a.5.5 0 0 1-.221.094l-.081.007H2v-1a2.5 2.5 0 0 1 2.336-2.495L4.5 3h2.667zm4.69 10.859a.5.5 0 0 0-.707-.708l-2.003 1.998a.5.5 0 0 0 0 .708l2.003 1.997a.5.5 0 1 0 .706-.708l-1.147-1.144h5.584l-1.146 1.144a.5.5 0 1 0 .706.708l2-1.996a.5.5 0 0 0 0-.708l-2-1.999a.5.5 0 1 0-.707.707l1.145 1.144H10.71l1.146-1.143z', |
||||||
|
'FolderSync': 'M4.5 3A2.5 2.5 0 0 0 2 5.5v9A2.5 2.5 0 0 0 4.5 17h5.1a5.465 5.465 0 0 1-.393-1H4.5A1.5 1.5 0 0 1 3 14.5v-7h4.071a1.5 1.5 0 0 0 1.087-.466L9.619 5.5H15.5A1.5 1.5 0 0 1 17 7v2.6c.358.183.693.404 1 .657V7a2.5 2.5 0 0 0-2.5-2.5H9.667l-1.6-1.2a1.5 1.5 0 0 0-.9-.3H4.5zM3 5.5A1.5 1.5 0 0 1 4.5 4h2.667a.5.5 0 0 1 .3.1l1.227.92l-1.26 1.325a.5.5 0 0 1-.363.155H3v-1zM14.5 19a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9zm1.5-7v.152a3.011 3.011 0 0 0-1.448-.401a2.999 2.999 0 0 0-2.173.878a.5.5 0 0 0 .707.707A2 2 0 0 1 15.468 13H15a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 .5-.5V12a.5.5 0 0 0-1 0zm-1.552 5.25a2.999 2.999 0 0 0 2.173-.879a.5.5 0 0 0-.707-.707a2 2 0 0 1-2.382.336H14a.5.5 0 0 0 0-1h-1.5a.5.5 0 0 0-.5.5V17a.5.5 0 0 0 1 0v-.152a3.011 3.011 0 0 0 1.448.402z', |
||||||
|
'FolderSyncSolid': 'M9.386 4.29l-1.32-.99a1.5 1.5 0 0 0-.9-.3H4.5A2.5 2.5 0 0 0 2 5.5v1h5.07a.5.5 0 0 0 .363-.156L9.386 4.29zm1.179.21L8.158 7.033a1.5 1.5 0 0 1-1.087.467H2v7A2.5 2.5 0 0 0 4.5 17h5.1a5.5 5.5 0 0 1 8.4-6.743V7l-.005-.164A2.5 2.5 0 0 0 15.5 4.5h-4.935zM14.5 19a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9zm1.5-7v.152a3.011 3.011 0 0 0-1.448-.401a2.999 2.999 0 0 0-2.173.878a.5.5 0 0 0 .707.707A2 2 0 0 1 15.468 13H15a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 .5-.5V12a.5.5 0 0 0-1 0zm-1.552 5.25a2.999 2.999 0 0 0 2.173-.879a.5.5 0 0 0-.707-.707a2 2 0 0 1-2.382.336H14a.5.5 0 0 0 0-1h-1.5a.5.5 0 0 0-.5.5V17a.5.5 0 0 0 1 0v-.152a3.011 3.011 0 0 0 1.448.402z', |
||||||
|
'FolderZip': 'M4.5 3A2.5 2.5 0 0 0 2 5.5v9A2.5 2.5 0 0 0 4.5 17h5.1a5.465 5.465 0 0 1-.393-1H4.5A1.5 1.5 0 0 1 3 14.5v-7h4.071a1.5 1.5 0 0 0 1.087-.466L9.619 5.5H15.5A1.5 1.5 0 0 1 17 7v2.6c.358.183.693.404 1 .657V7a2.5 2.5 0 0 0-2.5-2.5H9.667l-1.6-1.2a1.5 1.5 0 0 0-.9-.3H4.5zM3 5.5A1.5 1.5 0 0 1 4.5 4h2.667a.5.5 0 0 1 .3.1l1.227.92l-1.26 1.325a.5.5 0 0 1-.363.155H3v-1zM14.5 19a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9zm1.5-7v.152a3.011 3.011 0 0 0-1.448-.401a2.999 2.999 0 0 0-2.173.878a.5.5 0 0 0 .707.707A2 2 0 0 1 15.468 13H15a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 .5-.5V12a.5.5 0 0 0-1 0zm-1.552 5.25a2.999 2.999 0 0 0 2.173-.879a.5.5 0 0 0-.707-.707a2 2 0 0 1-2.382.336H14a.5.5 0 0 0 0-1h-1.5a.5.5 0 0 0-.5.5V17a.5.5 0 0 0 1 0v-.152a3.011 3.011 0 0 0 1.448.402z', |
||||||
|
'FolderZipSolid': 'M12.005 4.5h-1.44L8.158 7.033l-.111.106a1.5 1.5 0 0 1-.823.353L7.07 7.5H2v7l.005.164A2.5 2.5 0 0 0 4.5 17h8.504v-1.941a.515.515 0 0 1 0-.117L13.002 14h-.498a.5.5 0 0 1 0-1h.497v-2H12.5a.5.5 0 0 1 0-1h.5V9h-.495a.5.5 0 0 1-.5-.5v-4zm2 0h-1V8h1V4.5zm1 0h.495a2.5 2.5 0 0 1 2.479 2.174l.016.162L18 7v7.5a2.5 2.5 0 0 1-2.336 2.495L15.5 17h-1.496v-1.5h.496a.5.5 0 0 0 0-1h-.497v-.955a.478.478 0 0 0 0-.09V12.5h.502a.5.5 0 1 0 0-1h-.503L14 9h.505a.5.5 0 0 0 .5-.5v-4zM7.932 3.21A1.5 1.5 0 0 0 7.167 3H4.5l-.164.005A2.5 2.5 0 0 0 2 5.5v1h5.07l.082-.007a.5.5 0 0 0 .22-.094l.061-.055L9.385 4.29L8.067 3.3l-.135-.09z', |
||||||
|
|
||||||
|
'Checkmark': 'M3.374 10.168a.5.5 0 0 0-.748.664l4 4.5a.5.5 0 0 0 .728.022l10.5-10.5a.5.5 0 0 0-.707-.708L7.02 14.271l-3.647-4.103z', |
||||||
|
'CheckmarkSolid': 'M7.032 13.907l-3.471-3.905a.75.75 0 0 0-1.122.996l4 4.5a.75.75 0 0 0 1.091.032l10.5-10.5a.75.75 0 0 0-1.06-1.06l-9.938 9.937z', |
||||||
|
'CheckmarkCircle': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14zm3.358 4.646a.5.5 0 0 1 .058.638l-.058.07l-4.004 4.004a.5.5 0 0 1-.638.058l-.07-.058l-2-2a.5.5 0 0 1 .638-.765l.07.058L9 11.298l3.651-3.652a.5.5 0 0 1 .707 0z', |
||||||
|
'CheckmarkCircleSolid': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm3.358 5.646a.5.5 0 0 0-.637-.057l-.07.057L9 11.298L7.354 9.651l-.07-.058a.5.5 0 0 0-.695.696l.057.07l2 2l.07.057a.5.5 0 0 0 .568 0l.07-.058l4.004-4.004l.058-.07a.5.5 0 0 0-.058-.638z', |
||||||
|
'CheckmarkStarburst': 'M8.46 1.897l.99.39a1.5 1.5 0 0 0 1.099 0l.99-.39a2.418 2.418 0 0 1 3.102 1.285l.424.975a1.5 1.5 0 0 0 .777.777l.975.424a2.418 2.418 0 0 1 1.285 3.103l-.39.99a1.5 1.5 0 0 0 0 1.098l.39.99a2.418 2.418 0 0 1-1.285 3.102l-.975.424a1.499 1.499 0 0 0-.777.777l-.424.975a2.418 2.418 0 0 1-3.103 1.285l-.99-.39a1.5 1.5 0 0 0-1.098 0l-.99.39a2.418 2.418 0 0 1-3.102-1.285l-.424-.975a1.5 1.5 0 0 0-.777-.777l-.975-.424a2.418 2.418 0 0 1-1.285-3.103l.39-.99a1.5 1.5 0 0 0 0-1.098l-.39-.99a2.418 2.418 0 0 1 1.285-3.102l.975-.424a1.5 1.5 0 0 0 .777-.777l.424-.975a2.418 2.418 0 0 1 3.103-1.285zm3.445.93l-.99.39a2.5 2.5 0 0 1-1.831 0l-.99-.39a1.418 1.418 0 0 0-1.819.754l-.424.975a2.5 2.5 0 0 1-1.295 1.295l-.975.424a1.418 1.418 0 0 0-.753 1.82l.389.989a2.5 2.5 0 0 1 0 1.831l-.39.99c-.279.71.054 1.514.754 1.819l.975.424a2.5 2.5 0 0 1 1.295 1.295l.424.975a1.418 1.418 0 0 0 1.82.753l.989-.39a2.5 2.5 0 0 1 1.831 0l.99.39c.71.28 1.514-.053 1.819-.753l.424-.975a2.5 2.5 0 0 1 1.295-1.295l.975-.424a1.418 1.418 0 0 0 .753-1.82l-.39-.989a2.5 2.5 0 0 1 0-1.831l.39-.99a1.418 1.418 0 0 0-.753-1.819l-.975-.424a2.5 2.5 0 0 1-1.295-1.295l-.424-.975a1.418 1.418 0 0 0-1.82-.753zm-2.927 8.944l3.648-4.104a.5.5 0 0 1 .8.592l-.053.073l-4 4.5a.5.5 0 0 1-.655.081l-.072-.06l-2-2a.5.5 0 0 1 .638-.765l.069.058l1.625 1.625l3.648-4.104l-3.648 4.104z', |
||||||
|
'CheckmarkStarburstSolid': 'M8.46 1.897l.99.39a1.5 1.5 0 0 0 1.099 0l.99-.39a2.418 2.418 0 0 1 3.102 1.285l.424.975a1.5 1.5 0 0 0 .777.777l.975.424a2.418 2.418 0 0 1 1.285 3.103l-.39.99a1.5 1.5 0 0 0 0 1.098l.39.99a2.418 2.418 0 0 1-1.285 3.102l-.975.424a1.499 1.499 0 0 0-.777.777l-.424.975a2.418 2.418 0 0 1-3.103 1.285l-.99-.39a1.5 1.5 0 0 0-1.098 0l-.99.39a2.418 2.418 0 0 1-3.102-1.285l-.424-.975a1.5 1.5 0 0 0-.777-.777l-.975-.424a2.418 2.418 0 0 1-1.285-3.103l.39-.99a1.5 1.5 0 0 0 0-1.098l-.39-.99a2.418 2.418 0 0 1 1.285-3.102l.975-.424a1.5 1.5 0 0 0 .777-.777l.424-.975a2.418 2.418 0 0 1 3.103-1.285zm4.166 5.77l-3.648 4.104l-1.625-1.625a.5.5 0 0 0-.707.707l2 2a.5.5 0 0 0 .727-.021l4-4.5a.5.5 0 0 0-.747-.665z', |
||||||
|
'ArrowSyncCheckmark': 'M11.414 3.635a.5.5 0 0 0 0-.707L9.293.807a.5.5 0 0 0-.707.707l.997.997a7.5 7.5 0 0 0-4.075 13.495a.5.5 0 0 0 .6-.8a6.5 6.5 0 0 1 5.29-11.554l.016-.017zM8.586 16.363l.016-.016c.408.09.831.14 1.264.15l-.006.006a.502.502 0 0 1 .074-.004a6.5 6.5 0 0 0 3.959-11.706a.5.5 0 1 1 .6-.8a7.5 7.5 0 0 1-4.075 13.495l.996.996a.5.5 0 1 1-.707.707l-2.121-2.12a.5.5 0 0 1 0-.708zm3.768-8.218a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L9 10.792l2.646-2.647a.5.5 0 0 1 .708 0zM5 10a5 5 0 1 1 10 0a5 5 0 0 1-10 0zm5-4a4 4 0 1 0 0 8a4 4 0 0 0 0-8z', |
||||||
|
'ArrowSyncCheckmarkSolid': 'M11.414 3.635a.5.5 0 0 0 0-.707L9.293.807a.5.5 0 0 0-.707.707l.997.997a7.5 7.5 0 0 0-4.075 13.495a.5.5 0 0 0 .6-.8a6.5 6.5 0 0 1 5.29-11.554l.016-.017zM8.586 16.363l.016-.016c.408.09.831.14 1.264.15l-.006.006a.516.516 0 0 1 .074-.004a6.5 6.5 0 0 0 3.959-11.706a.5.5 0 1 1 .6-.8a7.5 7.5 0 0 1-4.075 13.495l.996.996a.5.5 0 1 1-.707.707l-2.121-2.12a.5.5 0 0 1 0-.708zM15 9.999a5 5 0 1 1-10 0a5 5 0 0 1 10 0zm-2.646-1.854a.5.5 0 0 0-.708 0L9 10.792L7.854 9.645a.5.5 0 1 0-.708.708l1.5 1.5a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708z', |
||||||
|
|
||||||
|
'Image': 'M14 7.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0zm-1 0a.5.5 0 1 0-1 0a.5.5 0 0 0 1 0zM3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm3-2a2 2 0 0 0-2 2v8c0 .373.102.722.28 1.02l4.669-4.588a1.5 1.5 0 0 1 2.102 0l4.67 4.588A1.99 1.99 0 0 0 16 14V6a2 2 0 0 0-2-2H6zm0 12h8c.37 0 .715-.1 1.012-.274l-4.662-4.58a.5.5 0 0 0-.7 0l-4.662 4.58A1.99 1.99 0 0 0 6 16z', |
||||||
|
'ImageSolid': 'M12.5 8a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1zM3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8c0 .65-.206 1.25-.557 1.742l-5.39-5.308a1.5 1.5 0 0 0-2.105 0l-5.39 5.308A2.986 2.986 0 0 1 3 14V6zm9.5 3a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3zm-8.235 7.448C4.755 16.796 5.354 17 6 17h8c.646 0 1.245-.204 1.735-.552l-5.384-5.3a.5.5 0 0 0-.702 0l-5.384 5.3z', |
||||||
|
'DrawImage': 'M14 7.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0zm-1 0a.5.5 0 1 0-1 0a.5.5 0 0 0 1 0zM3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v3.003c-.341.016-.68.092-1 .229V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8.826a.2.2 0 0 0 .34.142l.807-.796l-.002-.003l1.048-1.03l.206-.202l2.55-2.505a1.5 1.5 0 0 1 2.102 0l1.745 1.714l-.707.708l-1.739-1.709a.5.5 0 0 0-.7 0l-2.756 2.707l-1.851 1.828c-.758.748-2.043.21-2.043-.854V6zm.4 11.035c.369.184.83.335 1.217.25c.251-.056.577-.193.943-.347c.885-.373 2.003-.843 2.862-.497c.637.256.584.981.405 1.33c-.035.066-.008.16.065.177a4.6 4.6 0 0 0 1.112.088a.917.917 0 0 1 .023-.14l.375-1.498c.096-.386.296-.74.578-1.02l4.83-4.83a1.87 1.87 0 1 1 2.644 2.645l-4.83 4.829a2.197 2.197 0 0 1-1.02.578l-1.222.305c-1.121.328-2.794.222-3.313-.183c-.449-.35-.467-.887-.316-1.244c.034-.08-.026-.183-.111-.17c-.495.07-.9.25-1.3.427c-.585.26-1.156.513-1.976.411c-.711-.088-1.107-.459-1.325-.825c-.122-.204.147-.392.36-.286z', |
||||||
|
'DrawImageSolid': 'M6 3a3 3 0 0 0-3 3v9.076a.51.51 0 0 0 .868.363l1.342-1.325l3.738-3.68a1.5 1.5 0 0 1 2.104 0l1.742 1.715l2.308-2.308A2.86 2.86 0 0 1 17 9.003V6a3 3 0 0 0-3-3H6zm8 4.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0zm-1 0a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0zm-2.727 7.17l1.813-1.814l-1.735-1.709a.5.5 0 0 0-.702 0l-4.224 4.159c-.22.236-.008.587.296.478l.327-.117c.705-.253 1.764-.55 2.747-.154c.286.115.512.28.684.472c.154-.495.426-.947.794-1.315zm.707.707l4.83-4.83a1.87 1.87 0 1 1 2.644 2.646l-4.83 4.829a2.197 2.197 0 0 1-1.02.578l-1.221.305c-1.122.328-2.795.222-3.314-.183c-.449-.35-.467-.887-.316-1.244c.034-.08-.026-.183-.111-.17c-.495.07-.9.25-1.3.427c-.584.26-1.156.513-1.976.411c-.711-.088-1.107-.459-1.325-.825c-.122-.204.147-.392.36-.286c.368.184.829.335 1.216.25c.251-.056.577-.193.943-.347c.885-.373 2.003-.843 2.863-.497c.636.256.583.981.404 1.33c-.035.066-.008.16.065.177a4.6 4.6 0 0 0 1.112.088a.917.917 0 0 1 .023-.14l.375-1.498c.096-.386.296-.74.578-1.02z', |
||||||
|
'ImageCopy': 'M8.498 7.497a.998.998 0 1 0 0-1.995a.998.998 0 0 0 0 1.995zM5 6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V6zm3-2a2 2 0 0 0-2 2v6c0 .37.101.718.277 1.016L9.79 9.502a1.71 1.71 0 0 1 2.418 0l3.514 3.514C15.9 12.718 16 12.371 16 12V6a2 2 0 0 0-2-2H8zm7.016 9.723l-3.514-3.514a.71.71 0 0 0-1.004 0l-3.514 3.514C7.282 13.9 7.629 14 8 14h6c.37 0 .718-.101 1.016-.277zM12 17c.889 0 1.687-.386 2.236-1H7.5A3.5 3.5 0 0 1 4 12.5V5.764C3.386 6.314 3 7.112 3 8v4.5A4.5 4.5 0 0 0 7.5 17H12z', |
||||||
|
'ImageCopySolid': 'M5 6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6c0 .648-.205 1.248-.555 1.738L12.21 9.502a1.71 1.71 0 0 0-2.418 0l-4.236 4.236A2.986 2.986 0 0 1 5 12V6zm3.498 1.497a.998.998 0 1 0 0-1.995a.998.998 0 0 0 0 1.995zm3.004 2.712l4.236 4.236c-.49.35-1.09.555-1.738.555H8a2.986 2.986 0 0 1-1.738-.555l4.236-4.236a.71.71 0 0 1 1.004 0zM14.236 16c-.55.614-1.348 1-2.236 1H7.5A4.5 4.5 0 0 1 3 12.5V8c0-.888.386-1.687 1-2.236V12.5A3.5 3.5 0 0 0 7.5 16h6.736z', |
||||||
|
'ImageEdit': 'M13.999 7.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0zm-1 0a.5.5 0 1 0-1 0a.5.5 0 0 0 1 0zM3 6a3 3 0 0 1 3-3h7.999a3 3 0 0 1 3 3v3.002a2.87 2.87 0 0 0-1 .229V6a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v7.999c0 .372.103.721.28 1.02l4.669-4.588a1.5 1.5 0 0 1 2.102 0l1.745 1.715l-.707.707l-1.738-1.709a.5.5 0 0 0-.701 0l-4.661 4.58A1.99 1.99 0 0 0 6 16h3.474c-.016.05-.03.103-.043.155l-.211.845H6a3 3 0 0 1-3-3v-8zm7.979 9.376l4.829-4.83a1.87 1.87 0 1 1 2.644 2.646l-4.829 4.828a2.197 2.197 0 0 1-1.02.578l-1.498.375a.89.89 0 0 1-1.078-1.079l.374-1.498c.097-.386.296-.739.578-1.02z', |
||||||
|
'ImageEditSolid': 'M12.499 8a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1zM3 6a3 3 0 0 1 3-3h7.999a3 3 0 0 1 3 3v3.002a2.86 2.86 0 0 0-1.898.838l-2.308 2.308l-1.741-1.714a1.5 1.5 0 0 0-2.105 0l-5.39 5.307A2.986 2.986 0 0 1 3 13.998v-8zm9.499 3a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3zm-2.227 5.669l1.813-1.814l-1.735-1.709a.5.5 0 0 0-.702 0l-5.383 5.3c.49.348 1.088.552 1.735.552h3.22l.21-.844c.141-.562.432-1.075.842-1.485zm.707.707l4.829-4.83a1.87 1.87 0 1 1 2.644 2.646l-4.829 4.828a2.197 2.197 0 0 1-1.02.578l-1.498.375a.89.89 0 0 1-1.078-1.079l.374-1.498c.097-.386.296-.739.578-1.02z', |
||||||
|
'ImageMultiple': 'M11.5 7.5a1 1 0 1 0 0-2a1 1 0 0 0 0 2zM3 6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm3-2a2 2 0 0 0-2 2v6c0 .37.101.718.277 1.016l3.309-3.309a2 2 0 0 1 2.828 0l3.31 3.309c.175-.298.276-.645.276-1.016V6a2 2 0 0 0-2-2H6zm3.707 6.414a1 1 0 0 0-1.414 0l-3.309 3.31c.298.175.645.276 1.016.276h6c.37 0 .718-.101 1.016-.277l-3.309-3.309zM8 17a2.992 2.992 0 0 1-2.236-1H12.5a3.5 3.5 0 0 0 3.5-3.5V5.764c.614.55 1 1.348 1 2.236v4.5a4.5 4.5 0 0 1-4.5 4.5H8z', |
||||||
|
'ImageMultipleSolid': 'M6 3a3 3 0 0 0-3 3v6c0 .648.205 1.248.555 1.738l4.03-4.03a2 2 0 0 1 2.83 0l4.03 4.03c.35-.49.555-1.09.555-1.738V6a3 3 0 0 0-3-3H6zm6.5 3.5a1 1 0 1 1-2 0a1 1 0 0 1 2 0zm1.238 7.945l-4.03-4.03a1 1 0 0 0-1.415 0l-4.031 4.03c.49.35 1.09.555 1.738.555h6c.648 0 1.248-.205 1.738-.555zM5.764 16c.55.614 1.348 1 2.236 1h4.5a4.5 4.5 0 0 0 4.5-4.5V8c0-.888-.386-1.687-1-2.236V12.5a3.5 3.5 0 0 1-3.5 3.5H5.764z', |
||||||
|
'ImageOff': 'M2.854 2.146a.5.5 0 1 0-.708.708l1.409 1.408C3.205 4.752 3 5.352 3 6v8a3 3 0 0 0 3 3h8c.648 0 1.248-.205 1.738-.555l1.408 1.409a.5.5 0 0 0 .708-.708l-15-15zm6.56 7.975a1.497 1.497 0 0 0-.465.311l-4.67 4.588A1.99 1.99 0 0 1 4 14V6c0-.37.101-.718.277-1.016l5.137 5.137zM6 16c-.37 0-.715-.1-1.012-.274l4.662-4.58a.5.5 0 0 1 .7 0l4.662 4.58A1.991 1.991 0 0 1 14 16H6zM16 6v7.879l.898.898c.067-.248.102-.508.102-.777V6a3 3 0 0 0-3-3H6c-.269 0-.53.035-.777.102L6.12 4H14a2 2 0 0 1 2 2zm-2 1.5a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0zm-1 0a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0z', |
||||||
|
'ImageOffSolid': 'M2.854 2.146a.5.5 0 1 0-.708.708l1.409 1.408C3.205 4.752 3 5.352 3 6v8c0 .65.206 1.25.557 1.742l5.39-5.308c.14-.136.298-.24.468-.312l.632.633l-.352.352a.51.51 0 0 0-.046.04l-5.384 5.301C4.755 16.796 5.354 17 6 17h8c.597 0 1.154-.174 1.622-.475l.01-.008c.035-.022.069-.045.103-.069l-.003-.003l.003-.003l1.411 1.412a.5.5 0 0 0 .708-.708l-15-15zM13 7.5a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0zM5.223 3.102l11.675 11.675c.067-.248.102-.508.102-.777V6a3 3 0 0 0-3-3H6c-.269 0-.53.035-.777.102zM14 7.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0z', |
||||||
|
'ImageProhibited': 'M5.5 10a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm0-1c-.786 0-1.512-.26-2.096-.697l4.9-4.899A3.5 3.5 0 0 1 5.5 9zm2.096-6.303l-4.9 4.9a3.5 3.5 0 0 1 4.9-4.9zM3 10.4c.317.162.651.294 1 .393V14c0 .373.102.722.28 1.02l4.669-4.588a1.5 1.5 0 0 1 2.102 0l4.67 4.588A1.99 1.99 0 0 0 16 14V6a2 2 0 0 0-2-2h-3.207a5.466 5.466 0 0 0-.393-1H14a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3v-3.6zm1.988 5.326A1.99 1.99 0 0 0 6 16h8c.37 0 .715-.1 1.012-.274l-4.662-4.58a.5.5 0 0 0-.7 0l-4.662 4.58zM14 7.5a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0zm-1 0a.5.5 0 1 0-1 0a.5.5 0 0 0 1 0z', |
||||||
|
'ImageProhibitedSolid': 'M5.5 10a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm0-1c-.786 0-1.512-.26-2.096-.697l4.9-4.899A3.5 3.5 0 0 1 5.5 9zM2.697 7.596a3.5 3.5 0 0 1 4.9-4.9l-4.9 4.9zM13 7.5a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0zM5.5 11a5.5 5.5 0 0 0 4.9-8H14a3 3 0 0 1 3 3v8c0 .65-.206 1.25-.557 1.742l-5.39-5.308a1.5 1.5 0 0 0-2.105 0l-5.39 5.308A2.986 2.986 0 0 1 3 14v-3.6c.75.384 1.6.6 2.5.6zM14 7.5a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0zM6 17a2.987 2.987 0 0 1-1.735-.552l5.384-5.3a.5.5 0 0 1 .702 0l5.384 5.3A2.987 2.987 0 0 1 14 17H6z', |
||||||
|
'ImageSearch': 'M6 3a3 3 0 0 0-3 3v2.758a4.484 4.484 0 0 1 1-.502V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8c0 .373-.102.722-.28 1.02l-4.669-4.588a1.5 1.5 0 0 0-1.71-.278c.173.283.316.587.424.907a.5.5 0 0 1 .585.084l4.662 4.58A1.991 1.991 0 0 1 14 16h-2.879l.44.44c.163.163.281.355.354.56H14a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3H6zm6.5 6a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3zm0-1a.5.5 0 1 1 0-1a.5.5 0 0 1 0 1zm-4.197 6.596a3.5 3.5 0 1 0-.707.707l2.55 2.55a.5.5 0 0 0 .708-.707l-2.55-2.55zM5.5 15a2.5 2.5 0 1 1 0-5a2.5 2.5 0 0 1 0 5z', |
||||||
|
'ImageSearchSolid': 'M12.5 8a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1zM3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8c0 .65-.206 1.25-.557 1.742l-5.39-5.308a1.5 1.5 0 0 0-1.711-.279A4.497 4.497 0 0 0 3 8.758V6zm9.5 3a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3zm-.585 8H14c.646 0 1.245-.204 1.735-.552l-5.384-5.3a.5.5 0 0 0-.586-.086c.152.451.235.935.235 1.438c0 .694-.158 1.352-.439 1.94l2 2c.163.163.281.355.354.56zm-3.612-2.404a3.5 3.5 0 1 0-.707.707l2.55 2.55a.5.5 0 0 0 .708-.707l-2.55-2.55zM5.5 15a2.5 2.5 0 1 1 0-5a2.5 2.5 0 0 1 0 5z', |
||||||
|
|
||||||
|
'Video': 'M4.5 4A2.5 2.5 0 0 0 2 6.5v7A2.5 2.5 0 0 0 4.5 16h7a2.5 2.5 0 0 0 2.5-2.5v-1l2.4 1.8a1 1 0 0 0 1.6-.8v-7a1 1 0 0 0-1.6-.8L14 7.5v-1A2.5 2.5 0 0 0 11.5 4h-7zM14 8.75l3-2.25v7l-3-2.25v-2.5zM13 6.5v7a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 3 13.5v-7A1.5 1.5 0 0 1 4.5 5h7A1.5 1.5 0 0 1 13 6.5z', |
||||||
|
'VideoSolid': 'M13 6.5A2.5 2.5 0 0 0 10.5 4h-6A2.5 2.5 0 0 0 2 6.5v7A2.5 2.5 0 0 0 4.5 16h6a2.5 2.5 0 0 0 2.5-2.5v-7zm1 1.43v4.152l2.764 2.35A.75.75 0 0 0 18 13.86V6.193a.75.75 0 0 0-1.23-.575L14 7.93z', |
||||||
|
'VideoAdd': 'M4.5 4A2.5 2.5 0 0 0 2 6.5v3.757A5.504 5.504 0 0 1 3 9.6V6.5A1.5 1.5 0 0 1 4.5 5h7A1.5 1.5 0 0 1 13 6.5v7a1.5 1.5 0 0 1-1.5 1.5h-.522a5.489 5.489 0 0 1-.185 1h.707a2.5 2.5 0 0 0 2.5-2.5v-1l2.4 1.8a1 1 0 0 0 1.6-.8v-7a1 1 0 0 0-1.6-.8L14 7.5v-1A2.5 2.5 0 0 0 11.5 4h-7zM14 8.75l3-2.25v7l-3-2.25v-2.5zm-4 5.75a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V14H3.5a.5.5 0 0 0 0 1H5v1.5a.5.5 0 0 0 1 0V15h1.5a.5.5 0 0 0 0-1H6v-1.5z', |
||||||
|
'VideoAddSolid': 'M13 6.5A2.5 2.5 0 0 0 10.5 4h-6A2.5 2.5 0 0 0 2 6.5v3.757a5.5 5.5 0 0 1 8.798 5.725A2.5 2.5 0 0 0 13 13.5v-7zm1 1.43v4.152l2.764 2.35A.75.75 0 0 0 18 13.86V6.193a.75.75 0 0 0-1.23-.575L14 7.93zm-4 6.57a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V14H3.5a.5.5 0 0 0 0 1H5v1.5a.5.5 0 0 0 1 0V15h1.5a.5.5 0 0 0 0-1H6v-1.5z', |
||||||
|
'VideoOff': 'M2.854 2.146a.5.5 0 1 0-.708.708l1.355 1.354A2.5 2.5 0 0 0 2 6.5v7A2.5 2.5 0 0 0 4.5 16h7a2.5 2.5 0 0 0 2.292-1.5l3.354 3.354a.5.5 0 0 0 .708-.708l-15-15zm10.133 11.549A1.5 1.5 0 0 1 11.5 15h-7A1.5 1.5 0 0 1 3 13.5v-7a1.5 1.5 0 0 1 1.305-1.488l8.683 8.683zM13 10.879l3.47 3.469A1 1 0 0 0 18 13.5v-7a1 1 0 0 0-1.6-.8L14 7.5v-1A2.5 2.5 0 0 0 11.5 4H6.121l1 1H11.5A1.5 1.5 0 0 1 13 6.5v4.379zm1-2.129l3-2.25v7l-3-2.25v-2.5z', |
||||||
|
'VideoOffSolid': 'M2.854 2.146a.5.5 0 1 0-.708.708l1.355 1.354A2.5 2.5 0 0 0 2 6.5v7A2.5 2.5 0 0 0 4.5 16h6a2.5 2.5 0 0 0 2.492-2.3l4.154 4.154a.5.5 0 0 0 .708-.708l-15-15zm13.91 12.286l-1.41-1.199L14 11.88V7.93l2.77-2.314a.75.75 0 0 1 1.23.576v7.667a.75.75 0 0 1-1.236.572zM13 10.879l-6.879-6.88H10.5A2.5 2.5 0 0 1 13 6.5v4.38z', |
||||||
|
'VideoProhibited': 'M2 6.5A2.5 2.5 0 0 1 4.5 4h7A2.5 2.5 0 0 1 14 6.5v1l2.4-1.8a1 1 0 0 1 1.6.8v3.757a5.503 5.503 0 0 0-1-.657V6.5l-3 2.25v.272a5.48 5.48 0 0 0-1 .185V6.5A1.5 1.5 0 0 0 11.5 5h-7A1.5 1.5 0 0 0 3 6.5v7A1.5 1.5 0 0 0 4.5 15h4.522c.031.343.094.678.185 1H4.5A2.5 2.5 0 0 1 2 13.5v-7zm8 8a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm2.404 2.803l4.9-4.9a3.5 3.5 0 0 1-4.9 4.9zm-.707-.707a3.5 3.5 0 0 1 4.9-4.9l-4.9 4.9z', |
||||||
|
'VideoProhibitedSolid': 'M13 6.5A2.5 2.5 0 0 0 10.5 4h-6A2.5 2.5 0 0 0 2 6.5v7A2.5 2.5 0 0 0 4.5 16h4.707A5.502 5.502 0 0 1 13 9.207V6.5zm5-.307v4.064a5.477 5.477 0 0 0-4-1.235V7.931l2.77-2.313a.75.75 0 0 1 1.23.575zM10 14.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm1 0a3.5 3.5 0 0 1 5.596-2.803l-4.9 4.9A3.484 3.484 0 0 1 11 14.5zm3.5 3.5c-.786 0-1.512-.26-2.096-.697l4.9-4.9A3.5 3.5 0 0 1 14.5 18z', |
||||||
|
'VideoSync': 'M4.5 4A2.5 2.5 0 0 0 2 6.5v3.757A5.504 5.504 0 0 1 3 9.6V6.5A1.5 1.5 0 0 1 4.5 5h7A1.5 1.5 0 0 1 13 6.5v7a1.5 1.5 0 0 1-1.5 1.5h-.522a5.489 5.489 0 0 1-.185 1h.707a2.5 2.5 0 0 0 2.5-2.5v-1l2.4 1.8a1 1 0 0 0 1.6-.8v-7a1 1 0 0 0-1.6-.8L14 7.5v-1A2.5 2.5 0 0 0 11.5 4h-7zM14 8.75l3-2.25v7l-3-2.25v-2.5zM1 14.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm6.5-3a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-.5.5H6a.5.5 0 0 1 0-1h.468a1.99 1.99 0 0 0-.933-.25a2 2 0 0 0-1.45.586a.5.5 0 0 1-.706-.707A3 3 0 0 1 7 12.152V12a.5.5 0 0 1 .5-.5zm-.876 5.532A2.999 2.999 0 0 1 4 16.848V17a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5H5a.5.5 0 0 1 0 1h-.468a1.99 1.99 0 0 0 .933.25a2 2 0 0 0 1.45-.586a.5.5 0 0 1 .706.707a3 3 0 0 1-.997.66z', |
||||||
|
'VideoSyncSolid': 'M13 6.5A2.5 2.5 0 0 0 10.5 4h-6A2.5 2.5 0 0 0 2 6.5v3.757a5.5 5.5 0 0 1 8.798 5.725A2.5 2.5 0 0 0 13 13.5v-7zm1 1.43v4.152l2.764 2.35A.75.75 0 0 0 18 13.86V6.193a.75.75 0 0 0-1.23-.575L14 7.93zM1 14.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm6.5-3a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-.5.5H6a.5.5 0 0 1 0-1h.468a1.99 1.99 0 0 0-.933-.25a2 2 0 0 0-1.45.586a.5.5 0 0 1-.706-.707A3 3 0 0 1 7 12.152V12a.5.5 0 0 1 .5-.5zm-.876 5.532A2.999 2.999 0 0 1 4 16.848V17a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5H5a.5.5 0 0 1 0 1h-.468a1.99 1.99 0 0 0 .933.25a2 2 0 0 0 1.45-.586a.5.5 0 0 1 .706.707a3 3 0 0 1-.997.66z', |
||||||
|
|
||||||
|
'File': 'M6 2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zM5 4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4zm9.793 3H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7z', |
||||||
|
'FileSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 16.5v-13A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25z', |
||||||
|
'FileAdd': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V14H3.5a.5.5 0 0 0 0 1H5v1.5a.5.5 0 0 0 1 0V15h1.5a.5.5 0 0 0 0-1H6v-1.5z', |
||||||
|
'FileAddSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V14H3.5a.5.5 0 0 0 0 1H5v1.5a.5.5 0 0 0 1 0V15h1.5a.5.5 0 0 0 0-1H6v-1.5z', |
||||||
|
'FileArrowDown': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm-2.354-4.146a.5.5 0 0 1 .708-.708L5 15.293V12.5a.5.5 0 0 1 1 0v2.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.351.146h-.006a.5.5 0 0 1-.348-.144l-.003-.003l-2-2z', |
||||||
|
'FileArrowDownSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm-2.354-4.146a.5.5 0 0 1 .708-.708L5 15.293V12.5a.5.5 0 0 1 1 0v2.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.351.146h-.006a.5.5 0 0 1-.348-.144l-.003-.003l-2-2z', |
||||||
|
'FileArrowUp': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2z', |
||||||
|
'FileArrowUpSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2z', |
||||||
|
'FileBulletList': 'M6 10.5a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zm.5 1.5a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1zM6 14.5a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zM8.5 10a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM8 12.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm.5 1.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM6 2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zM5 4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4zm9.793 3H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7z', |
||||||
|
'FileBulletListSolid': 'M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v13A1.5 1.5 0 0 0 5.5 18h9a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 10 6.5zm-4 4a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zm0 2a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zm0 2a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zm2-4a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm3-8V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5z', |
||||||
|
'FileBulletListMultiple': 'M6.5 10a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1zM6 12.5a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zm2-2a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5zm.5 1.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4zM6 2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 9.586 2H6zM5 4a1 1 0 0 1 1-1h3v3.5A1.5 1.5 0 0 0 10.5 8H14v6a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4zm5 2.5V3.207L13.793 7H10.5a.5.5 0 0 1-.5-.5zM16 8a1 1 0 0 1 1 1v5.06A3.94 3.94 0 0 1 13.06 18H7a1 1 0 0 1-1-1h7a3 3 0 0 0 3-3V8z', |
||||||
|
'FileBulletListMultipleSolid': 'M9 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v11A1.5 1.5 0 0 0 5.5 16H13a2 2 0 0 0 2-2V8h-4.5A1.5 1.5 0 0 1 9 6.5zm-3 4a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zm.5 2.5a.5.5 0 1 1 0-1a.5.5 0 0 1 0 1zm2-2a.5.5 0 0 1 0-1h4a.5.5 0 0 1 0 1h-4zM8 12.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5zm2-6V2.25L14.75 7H10.5a.5.5 0 0 1-.5-.5zM17 9a1 1 0 0 0-1-1v6a3 3 0 0 1-3 3H6a1 1 0 0 0 1 1h6.06A3.94 3.94 0 0 0 17 14.06V9z', |
||||||
|
'FileBulletListOff': 'M4 4.707L2.146 2.854a.5.5 0 1 1 .708-.708l15 15a.5.5 0 0 1-.708.708l-1.241-1.242A2 2 0 0 1 14 18H6a2 2 0 0 1-2-2V4.707zm11 11l-1.032-1.032A.5.5 0 0 1 13.5 15h-5a.5.5 0 0 1 0-1h4.793l-1-1H8.5a.5.5 0 0 1 0-1h2.793l-1-1H8.5a.5.5 0 0 1 0-1h.793L5 5.707V16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-.293zM12.121 10l1 1h.379a.5.5 0 0 0 0-1h-1.379zM15 8v4.879l1 1V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6c-.521 0-.996.2-1.352.526l.708.709A.996.996 0 0 1 6 3h4v3.5A1.5 1.5 0 0 0 11.5 8H15zm-9 2.5a.5.5 0 1 0 1 0a.5.5 0 0 0-1 0zm.5 1.5a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1zM6 14.5a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0zM14.793 7H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7z', |
||||||
|
'FileBulletListOffSolid': 'M4 4.707L2.146 2.854a.5.5 0 1 1 .708-.708l15 15a.5.5 0 0 1-.708.708l-1.159-1.16A1.5 1.5 0 0 1 14.5 18h-9A1.5 1.5 0 0 1 4 16.5V4.707zM13.293 14H8.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 .468-.325L13.293 14zm-1-1l-1-1H8.5a.5.5 0 0 0 0 1h3.793zm-2-2l-1-1H8.5a.5.5 0 0 0 0 1h1.793zm3.207 0h-.379L16 13.879V8h-4.5A1.5 1.5 0 0 1 10 6.5V2H5.5c-.383 0-.733.144-.998.38l7.62 7.62H13.5a.5.5 0 0 1 0 1zM6 10.5a.5.5 0 1 0 1 0a.5.5 0 0 0-1 0zm0 2a.5.5 0 1 0 1 0a.5.5 0 0 0-1 0zm0 2a.5.5 0 1 0 1 0a.5.5 0 0 0-1 0zm5-8V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5z', |
||||||
|
'FileCatchUp': 'M6 2a2 2 0 0 0-2 2v4.5a.5.5 0 0 0 1 0V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-3H4v3a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM7.462 8.308a.5.5 0 0 0-.91-.032L5.192 11H3.5a.5.5 0 0 0 0 1h2a.5.5 0 0 0 .447-.276L6.96 9.7l2.08 4.991a.5.5 0 0 0 .908.032L11.31 12H12.5a.5.5 0 0 0 0-1H11a.5.5 0 0 0-.447.276L9.54 13.3l-2.08-4.991z', |
||||||
|
'FileCatchUpSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 16.5V13h1.5a1.5 1.5 0 0 0 1.342-.83l.034-.068l1.24 2.975a1.5 1.5 0 0 0 2.726.094L11.927 13h.573a1.5 1.5 0 0 0 0-3H11a1.5 1.5 0 0 0-1.342.83l-.034.068l-1.24-2.975a1.5 1.5 0 0 0-2.726-.094L4.573 10H4V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM7.462 8.308a.5.5 0 0 0-.91-.032L5.192 11H3.5a.5.5 0 0 0 0 1h2a.5.5 0 0 0 .447-.276L6.96 9.7l2.08 4.991a.5.5 0 0 0 .908.032L11.31 12H12.5a.5.5 0 0 0 0-1H11a.5.5 0 0 0-.447.276L9.54 13.3l-2.08-4.991z', |
||||||
|
'FileCheckmark': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.146-1.854a.5.5 0 0 0-.708 0L4.5 15.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708z', |
||||||
|
'FileCheckmarkSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.146-1.854a.5.5 0 0 0-.708 0L4.5 15.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708z', |
||||||
|
'FileCopy': 'M6 4a2 2 0 0 1 2-2h3.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 17 7.414V14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V4zm2-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 11 6.5V3H8zm4 .207V6.5a.5.5 0 0 0 .5.5h3.293L12 3.207zM4 5a1 1 0 0 1 1-1v10a3 3 0 0 0 3 3h7a1 1 0 0 1-1 1H7.94A3.94 3.94 0 0 1 4 14.06V5z', |
||||||
|
'FileCopySolid': 'M11 6.5V2H7.5A1.5 1.5 0 0 0 6 3.5v11A1.5 1.5 0 0 0 7.5 16h8a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 11 6.5zm1 0V2.25L16.75 7H12.5a.5.5 0 0 1-.5-.5zM4 5a1 1 0 0 1 1-1v10.5A2.5 2.5 0 0 0 7.5 17H15a1 1 0 0 1-1 1H7.548A3.548 3.548 0 0 1 4 14.452V5z', |
||||||
|
'FileDismiss': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM8.682 17.682a4.5 4.5 0 1 0-6.364-6.364a4.5 4.5 0 0 0 6.364 6.364zm-4.95-4.95a.5.5 0 0 1 .707 0l1.06 1.06l1.062-1.06a.5.5 0 1 1 .707.707L6.207 14.5l1.06 1.06a.5.5 0 1 1-.707.708l-1.06-1.06l-1.06 1.06a.5.5 0 1 1-.708-.708l1.06-1.06l-1.06-1.06a.5.5 0 0 1 0-.708z', |
||||||
|
'FileDismissSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM2.318 17.682a4.5 4.5 0 1 0 6.364-6.364a4.5 4.5 0 0 0-6.364 6.364zm1.414-4.95a.5.5 0 0 1 .707 0l1.06 1.06l1.062-1.06a.5.5 0 1 1 .707.707L6.207 14.5l1.06 1.06a.5.5 0 1 1-.707.708l-1.06-1.06l-1.06 1.06a.5.5 0 1 1-.708-.708l1.06-1.06l-1.06-1.06a.5.5 0 0 1 0-.708z', |
||||||
|
'FileEdit': 'M11.5 8H16v-.586a1.496 1.496 0 0 0-.057-.41L15.942 7a1.5 1.5 0 0 0-.381-.646l-3.915-3.915A1.5 1.5 0 0 0 10.586 2H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h2.221l-.013-.026A1.856 1.856 0 0 1 8.003 17H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8zm0-1a.5.5 0 0 1-.5-.5V3.207L14.793 7H11.5zm3.31 2.548a1.87 1.87 0 1 1 2.644 2.645l-4.83 4.829a2.197 2.197 0 0 1-1.02.578l-1.498.374a.89.89 0 0 1-1.079-1.078l.375-1.498c.096-.386.296-.74.578-1.02l4.83-4.83z', |
||||||
|
'FileEditSolid': 'M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v13A1.5 1.5 0 0 0 5.5 18h2.721c-.21-.39-.285-.86-.164-1.347l.375-1.498c.14-.562.43-1.075.84-1.485l4.83-4.83A2.86 2.86 0 0 1 16 8.004V8h-4.5A1.5 1.5 0 0 1 10 6.5zm1 0V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5zm6.454 3.048a1.87 1.87 0 0 0-2.645 0l-4.83 4.83a2.197 2.197 0 0 0-.577 1.02l-.375 1.498a.89.89 0 0 0 1.079 1.078l1.498-.374c.386-.097.739-.296 1.02-.578l4.83-4.83a1.87 1.87 0 0 0 0-2.644z', |
||||||
|
'FileError': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zM5.5 12a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-2a.5.5 0 0 0-.5-.5zm0 5.125a.625.625 0 1 0 0-1.25a.625.625 0 0 0 0 1.25z', |
||||||
|
'FileErrorSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zM5.5 12a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-2a.5.5 0 0 0-.5-.5zm0 5.125a.625.625 0 1 0 0-1.25a.625.625 0 0 0 0 1.25z', |
||||||
|
'FileLink': 'M6 2a2 2 0 0 0-2 2v7h1V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-2.256a4.483 4.483 0 0 1-.502 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM5 12.5a.5.5 0 0 0-.5-.5l-.192.005A3.5 3.5 0 0 0 4.5 19l.09-.008A.5.5 0 0 0 4.5 18l-.164-.005A2.5 2.5 0 0 1 4.5 13l.09-.008A.5.5 0 0 0 5 12.5zm6 3A3.5 3.5 0 0 0 7.5 12l-.09.008A.5.5 0 0 0 7.5 13l.164.005A2.5 2.5 0 0 1 7.5 18l-.002.005l-.09.008a.5.5 0 0 0 .094.992V19l.192-.005A3.5 3.5 0 0 0 11 15.5zm-3.5-.498L4.5 15l-.09.008A.5.5 0 0 0 4.5 16l3 .002l.09-.008a.5.5 0 0 0-.09-.992z', |
||||||
|
'FileLinkSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5h-3.258A4.5 4.5 0 0 0 7.5 11H4V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM5 12.5a.5.5 0 0 0-.5-.5l-.192.005A3.5 3.5 0 0 0 4.5 19l.09-.008A.5.5 0 0 0 4.5 18l-.164-.005A2.5 2.5 0 0 1 4.5 13l.09-.008A.5.5 0 0 0 5 12.5zm6 3A3.5 3.5 0 0 0 7.5 12l-.09.008A.5.5 0 0 0 7.5 13l.164.005A2.5 2.5 0 0 1 7.5 18l-.002.005l-.09.008a.5.5 0 0 0 .094.992V19l.192-.005A3.5 3.5 0 0 0 11 15.5zm-3.5-.498L4.5 15l-.09.008A.5.5 0 0 0 4.5 16l3 .002l.09-.008a.5.5 0 0 0-.09-.992z', |
||||||
|
'FileLock': 'M6 2a2 2 0 0 0-2 2v5.401a2.98 2.98 0 0 1 1-.36V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM3.5 12v1H3a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-.5v-1a2 2 0 1 0-4 0zm1 1v-1a1 1 0 1 1 2 0v1h-2zm1 2.25a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5z', |
||||||
|
'FileLockSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H10v-4a2 2 0 0 0-1.5-1.937V12A3 3 0 0 0 4 9.401V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM3.5 12v1H3a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-.5v-1a2 2 0 1 0-4 0zm1 1v-1a1 1 0 1 1 2 0v1h-2zm1 2.25a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5z', |
||||||
|
'FileMultiple': 'M4 4a2 2 0 0 1 2-2h3.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 15 7.414V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4zm2-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 9 6.5V3H6zm4 .207V6.5a.5.5 0 0 0 .5.5h3.293L10 3.207zM17 9a1 1 0 0 0-1-1v6a3 3 0 0 1-3 3H6a1 1 0 0 0 1 1h6.06A3.94 3.94 0 0 0 17 14.06V9z', |
||||||
|
'FileMultipleSolid': 'M9 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v11A1.5 1.5 0 0 0 5.5 16h8a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 9 6.5zm1 0V2.25L14.75 7H10.5a.5.5 0 0 1-.5-.5zM17 9a1 1 0 0 0-1-1v6a3 3 0 0 1-3 3H6a1 1 0 0 0 1 1h6.06A3.94 3.94 0 0 0 17 14.06V9z', |
||||||
|
'FilePdf': 'M6.5 11a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-.166h.333a1.167 1.167 0 0 0 0-2.334H6.5zm.833 1.334H7V12h.333a.167.167 0 0 1 0 .334zM12 11.499a.5.5 0 0 1 .5-.499h.999a.5.5 0 0 1 0 1h-.5v.335h.5a.5.5 0 1 1 0 1h-.5l.001.164a.5.5 0 0 1-1 .002L12 12.834L12 11.499zM9.498 11a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 .5.5H10a1.5 1.5 0 0 0 0-3h-.502zm.5 2v-1H10a.5.5 0 0 1 0 1h-.002zM4 4a2 2 0 0 1 2-2h4.585a1.5 1.5 0 0 1 1.061.44l3.914 3.914a1.5 1.5 0 0 1 .44 1.06v1.668a1.5 1.5 0 0 1 .998 1.414v4.003A1.5 1.5 0 0 1 16 15.913V16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-.087A1.5 1.5 0 0 1 3 14.5v-4.003A1.5 1.5 0 0 1 4 9.082V4zm11 4h-3.5A1.5 1.5 0 0 1 10 6.5V3H6a1 1 0 0 0-1 1v4.996h10V8zM5 15.999A1 1 0 0 0 6 17h8a1 1 0 0 0 1-1.001H5zm6-12.792V6.5a.5.5 0 0 0 .5.5h3.293L11 3.207zM4.5 9.996a.5.5 0 0 0-.5.5v4.003a.5.5 0 0 0 .5.5h10.997a.5.5 0 0 0 .5-.5v-4.003a.5.5 0 0 0-.5-.5H4.501z', |
||||||
|
'FilePdfSolid': 'M6.5 11a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-.166h.334a1.167 1.167 0 0 0 0-2.334H6.5zm.834 1.334H7V12h.334a.167.167 0 0 1 0 .334zM12 11.499a.5.5 0 0 1 .5-.499h.998a.5.5 0 1 1 0 1h-.498v.335h.498a.5.5 0 1 1 0 1h-.498v.164a.5.5 0 1 1-1 .002L12 12.834l.002-1.335zM9.5 11a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 .5.5h.502a1.5 1.5 0 0 0 0-3h-.502zm.5 2v-1h.002a.5.5 0 0 1 0 1h-.002zm.002-6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v5.582a1.5 1.5 0 0 0-1 1.414v4.003a1.5 1.5 0 0 0 1 1.414v.587A1.5 1.5 0 0 0 5.5 18h9a1.5 1.5 0 0 0 1.5-1.5v-.587a1.5 1.5 0 0 0 .998-1.414v-4.003a1.5 1.5 0 0 0-.998-1.414V8h-4.5A1.5 1.5 0 0 1 10 6.5zM4.502 9.996h10.997a.5.5 0 0 1 .5.5v4.003a.5.5 0 0 1-.5.5H4.502a.5.5 0 0 1-.5-.5v-4.003a.5.5 0 0 1 .5-.5zM11 6.5V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5z', |
||||||
|
'FileProhibited': 'M4 4a2 2 0 0 1 2-2h4.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 16 7.414V16a2 2 0 0 1-2 2H9.743c.253-.307.474-.642.657-1H14a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 10 6.5V3H6a1 1 0 0 0-1 1v5.022a5.48 5.48 0 0 0-1 .185V4zm7-.793V6.5a.5.5 0 0 0 .5.5h3.293L11 3.207zM8.682 17.682a4.5 4.5 0 1 1-6.364-6.364a4.5 4.5 0 0 1 6.364 6.364zm-5.278-.379a3.5 3.5 0 0 0 4.9-4.9l-4.9 4.9zm-.707-.707l4.9-4.9a3.5 3.5 0 0 0-4.9 4.9z', |
||||||
|
'FileProhibitedSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zm-8.682 9.068a4.5 4.5 0 1 0 6.364 6.364a4.5 4.5 0 0 0-6.364-6.364zm5.657 5.657a3.5 3.5 0 0 1-4.571.328l4.9-4.9a3.5 3.5 0 0 1-.33 4.572zm-.379-5.278l-4.9 4.9a3.5 3.5 0 0 1 4.9-4.9z', |
||||||
|
'FileQuestionMark': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM10 14.5a4.5 4.5 0 1 0-9 0a4.5 4.5 0 0 0 9 0zm-4.5 1.88a.625.625 0 1 1 0 1.25a.625.625 0 0 1 0-1.25zm0-4.877c1.031 0 1.853.846 1.853 1.95c0 .586-.214.908-.727 1.319l-.277.214c-.246.194-.329.3-.346.448l-.011.156A.5.5 0 0 1 5 15.5c0-.57.21-.884.716-1.288l.278-.215c.288-.23.36-.342.36-.544c0-.558-.382-.95-.854-.95c-.494 0-.859.366-.853.945a.5.5 0 1 1-1 .01c-.011-1.137.805-1.955 1.853-1.955z', |
||||||
|
'FileQuestionMarkSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM10 14.5a4.5 4.5 0 1 0-9 0a4.5 4.5 0 0 0 9 0zm-4.5 1.88a.625.625 0 1 1 0 1.25a.625.625 0 0 1 0-1.25zm0-4.877c1.031 0 1.853.846 1.853 1.95c0 .586-.214.908-.727 1.319l-.277.214c-.246.194-.329.3-.346.448l-.011.156A.5.5 0 0 1 5 15.5c0-.57.21-.884.716-1.288l.278-.215c.288-.23.36-.342.36-.544c0-.558-.382-.95-.854-.95c-.494 0-.859.366-.853.945a.5.5 0 1 1-1 .01c-.011-1.137.805-1.955 1.853-1.955z', |
||||||
|
'FileSearch': 'M10 12c0 .924-.314 1.775-.84 2.453l3.691 3.692a.5.5 0 1 1-.707.707L8.453 15.16A4 4 0 1 1 10 12zm-4 3a3 3 0 1 0 0-6a3 3 0 0 0 0 6zM5.5 3a.5.5 0 0 0-.5.5v3.6c-.348.07-.683.177-1 .316V3.5A1.5 1.5 0 0 1 5.5 2h5.086a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 16 7.414V16.5a1.5 1.5 0 0 1-1.5 1.5h-.587a1.494 1.494 0 0 0-.354-.563L13.12 17H14.5a.5.5 0 0 0 .5-.5V8h-3.5A1.5 1.5 0 0 1 10 6.5V3H5.5zm5.5.207V6.5a.5.5 0 0 0 .5.5h3.293L11 3.207z', |
||||||
|
'FileSearchSolid': 'M5 2h5v4.5A1.5 1.5 0 0 0 11.5 8H16v9a1 1 0 0 1-1 1h-1.087a1.494 1.494 0 0 0-.354-.563l-3.125-3.124A5 5 0 0 0 4 7.416V3a1 1 0 0 1 1-1zm6 0l5 5h-4.5a.5.5 0 0 1-.5-.5V2zm-1 10c0 .924-.314 1.775-.84 2.453l3.691 3.692a.5.5 0 1 1-.707.707L8.453 15.16A4 4 0 1 1 10 12zm-4 3a3 3 0 1 0 0-6a3 3 0 0 0 0 6z', |
||||||
|
'FileSync': 'M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM1 14.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm6.5-3a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-.5.5H6a.5.5 0 0 1 0-1h.468a1.99 1.99 0 0 0-.933-.25a2 2 0 0 0-1.45.586a.5.5 0 0 1-.706-.707A3 3 0 0 1 7 12.152V12a.5.5 0 0 1 .5-.5zm-.876 5.532A2.999 2.999 0 0 1 4 16.848V17a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5H5a.5.5 0 0 1 0 1h-.468a1.99 1.99 0 0 0 .933.25a2 2 0 0 0 1.45-.586a.5.5 0 0 1 .706.707a3 3 0 0 1-.997.66z', |
||||||
|
'FileSyncSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H9.743A5.5 5.5 0 0 0 4 9.207V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM1 14.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm6.5-3a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-.5.5H6a.5.5 0 0 1 0-1h.468a1.99 1.99 0 0 0-.933-.25a2 2 0 0 0-1.45.586a.5.5 0 0 1-.706-.707A3 3 0 0 1 7 12.152V12a.5.5 0 0 1 .5-.5zm-.876 5.532A2.999 2.999 0 0 1 4 16.848V17a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5H5a.5.5 0 0 1 0 1h-.468a1.99 1.99 0 0 0 .933.25a2 2 0 0 0 1.45-.586a.5.5 0 0 1 .706.707a3 3 0 0 1-.997.66z', |
||||||
|
'FileText': 'M6.5 10a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7zm0 2a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7zm0 2a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7zM4 4a2 2 0 0 1 2-2h4.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 16 7.414V16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4zm2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 10 6.5V3H6zm5.5 4h3.293L11 3.207V6.5a.5.5 0 0 0 .5.5z', |
||||||
|
'FileTextSolid': 'M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v13A1.5 1.5 0 0 0 5.5 18h9a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 10 6.5zM6.5 10h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1zm0 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1zm0 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1zM11 6.5V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5z', |
||||||
|
'FileJs': 'M4 4a2 2 0 0 1 2-2h4.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 16 7.414V16a2 2 0 0 1-2 2H8.5c.219-.29.375-.63.45-1H14a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 10 6.5V3H6a1 1 0 0 0-1 1v7.5c-.081.061-.16.127-.232.198A1.504 1.504 0 0 0 4 11.085V4zm7.5 3h3.293L11 3.207V6.5a.5.5 0 0 0 .5.5zm-8 5a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0V16a.5.5 0 0 0-1 0v.5a1.5 1.5 0 0 0 3 0v-4a.5.5 0 0 0-.5-.5zM5 13.5a1.5 1.5 0 0 1 3 0a.5.5 0 0 1-1 0a.5.5 0 0 0-1 0v.382a.5.5 0 0 0 .276.447l.895.447A1.5 1.5 0 0 1 8 16.118v.382a1.5 1.5 0 0 1-3 0a.5.5 0 0 1 1 0a.5.5 0 0 0 1 0v-.382a.5.5 0 0 0-.276-.447l-.895-.447A1.5 1.5 0 0 1 5 13.882V13.5z', |
||||||
|
'FileJsSolid': 'M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v7.585c.32.113.589.331.768.613A2.5 2.5 0 0 1 9 13.5c0 .444-.193.843-.5 1.118c.319.425.5.949.5 1.5v.382c0 .563-.186 1.082-.5 1.5h6a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 10 6.5zm1 0V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5zM3.5 12a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0V16a.5.5 0 0 0-1 0v.5a1.5 1.5 0 0 0 3 0v-4a.5.5 0 0 0-.5-.5zM5 13.5a1.5 1.5 0 0 1 3 0a.5.5 0 0 1-1 0a.5.5 0 0 0-1 0v.382a.5.5 0 0 0 .276.447l.895.447A1.5 1.5 0 0 1 8 16.118v.382a1.5 1.5 0 0 1-3 0a.5.5 0 0 1 1 0a.5.5 0 0 0 1 0v-.382a.5.5 0 0 0-.276-.447l-.895-.447A1.5 1.5 0 0 1 5 13.882V13.5z', |
||||||
|
'FileCss': 'M4 4a2 2 0 0 1 2-2h4.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 16 7.414V16a2 2 0 0 1-2 2h-.5c.219-.29.375-.63.45-1H14a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 10 6.5V3H6a1 1 0 0 0-1 1v7.764a2.997 2.997 0 0 0-1-.593V4zm7.5 3h3.293L11 3.207V6.5a.5.5 0 0 0 .5.5zM3 12a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0a.5.5 0 0 0-1 0a1 1 0 1 1-2 0v-2a1 1 0 1 1 2 0a.5.5 0 0 0 1 0a2 2 0 0 0-2-2zm8.5 0a1.5 1.5 0 0 0-1.5 1.5v.382a1.5 1.5 0 0 0 .83 1.342l.894.447a.5.5 0 0 1 .276.447v.382a.5.5 0 0 1-1 0a.5.5 0 0 0-1 0a1.5 1.5 0 0 0 3 0v-.382a1.5 1.5 0 0 0-.83-1.342l-.894-.447a.5.5 0 0 1-.276-.447V13.5a.5.5 0 0 1 1 0a.5.5 0 0 0 1 0a1.5 1.5 0 0 0-1.5-1.5zM6 13.5a1.5 1.5 0 0 1 3 0a.5.5 0 0 1-1 0a.5.5 0 0 0-1 0v.382a.5.5 0 0 0 .276.447l.895.447A1.5 1.5 0 0 1 9 16.118v.382a1.5 1.5 0 0 1-3 0a.5.5 0 0 1 1 0a.5.5 0 0 0 1 0v-.382a.5.5 0 0 0-.276-.447l-.895-.447A1.5 1.5 0 0 1 6 13.882V13.5z', |
||||||
|
'FileCssSolid': 'M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v7.67c.552.196 1.03.548 1.38 1.004A2.498 2.498 0 0 1 9.5 12a2.5 2.5 0 0 1 4.5 1.5c0 .444-.193.843-.5 1.118c.319.425.5.949.5 1.5v.382a2.49 2.49 0 0 1-.5 1.5h1a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 10 6.5zm1 0V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5zM3 12a2 2 0 0 0-2 2v2a2 2 0 1 0 4 0a.5.5 0 0 0-1 0a1 1 0 1 1-2 0v-2a1 1 0 1 1 2 0a.5.5 0 0 0 1 0a2 2 0 0 0-2-2zm8.5 0a1.5 1.5 0 0 0-1.5 1.5v.382a1.5 1.5 0 0 0 .83 1.342l.894.447a.5.5 0 0 1 .276.447v.382a.5.5 0 0 1-1 0a.5.5 0 0 0-1 0a1.5 1.5 0 0 0 3 0v-.382a1.5 1.5 0 0 0-.83-1.342l-.894-.447a.5.5 0 0 1-.276-.447V13.5a.5.5 0 0 1 1 0a.5.5 0 0 0 1 0a1.5 1.5 0 0 0-1.5-1.5zM6 13.5a1.5 1.5 0 0 1 3 0a.5.5 0 0 1-1 0a.5.5 0 0 0-1 0v.382a.5.5 0 0 0 .276.447l.895.447A1.5 1.5 0 0 1 9 16.118v.382a1.5 1.5 0 0 1-3 0a.5.5 0 0 1 1 0a.5.5 0 0 0 1 0v-.382a.5.5 0 0 0-.276-.447l-.895-.447A1.5 1.5 0 0 1 6 13.882V13.5z', |
||||||
|
|
||||||
|
'ChevronDoubleRight': 'M8.646 4.147a.5.5 0 0 1 .707-.001l5.484 5.465a.55.55 0 0 1 0 .779l-5.484 5.465a.5.5 0 0 1-.706-.708L13.812 10L8.647 4.854a.5.5 0 0 1-.001-.707zm-4 0a.5.5 0 0 1 .707-.001l5.484 5.465a.55.55 0 0 1 0 .779l-5.484 5.465a.5.5 0 0 1-.706-.708L9.812 10L4.647 4.854a.5.5 0 0 1-.001-.707z', |
||||||
|
'ChevronDoubleRightSolid': 'M8.733 4.207a.75.75 0 0 1 1.06.026l5.001 5.25a.75.75 0 0 1 0 1.035l-5 5.25a.75.75 0 1 1-1.087-1.034L13.215 10L8.707 5.267a.75.75 0 0 1 .026-1.06zm-4 0a.75.75 0 0 1 1.06.026l5.001 5.25a.75.75 0 0 1 0 1.035l-5 5.25a.75.75 0 1 1-1.087-1.034L9.216 10l-4.51-4.734a.75.75 0 0 1 .027-1.06z', |
||||||
|
'ChevronDoubleLeft': 'M11.353 15.854a.5.5 0 0 1-.707.001L5.162 10.39a.55.55 0 0 1 0-.78l5.484-5.464a.5.5 0 1 1 .706.708L6.188 10l5.164 5.147a.5.5 0 0 1 .001.707zm4 0a.5.5 0 0 1-.708.001L9.161 10.39a.55.55 0 0 1 0-.78l5.484-5.464a.5.5 0 1 1 .706.708L10.187 10l5.164 5.147a.5.5 0 0 1 .001.707z', |
||||||
|
'ChevronDoubleLeftSolid': 'M11.269 15.794a.75.75 0 0 1-1.06-.026l-5.002-5.25a.75.75 0 0 1 0-1.035l5.001-5.25a.75.75 0 1 1 1.086 1.034l-4.508 4.734l4.508 4.733a.75.75 0 0 1-.025 1.06zm4 .001a.75.75 0 0 1-1.06-.026l-5.001-5.25a.75.75 0 0 1 0-1.035l5.001-5.25a.75.75 0 1 1 1.086 1.034l-4.508 4.733l4.508 4.734a.75.75 0 0 1-.025 1.06z', |
||||||
|
'MoreHorizontal': 'M6.25 10a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0zm5 0a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0zM15 11.25a1.25 1.25 0 1 0 0-2.5a1.25 1.25 0 0 0 0 2.5z', |
||||||
|
'MoreHorizontalSolid': 'M6.75 10a1.75 1.75 0 1 1-3.5 0a1.75 1.75 0 0 1 3.5 0zm5 0a1.75 1.75 0 1 1-3.5 0a1.75 1.75 0 0 1 3.5 0zM15 11.75a1.75 1.75 0 1 0 0-3.5a1.75 1.75 0 0 0 0 3.5z', |
||||||
|
'MoreVertical': 'M10 6a1.25 1.25 0 1 1 0-2.5A1.25 1.25 0 0 1 10 6zm0 5.25a1.25 1.25 0 1 1 0-2.5a1.25 1.25 0 0 1 0 2.5zm-1.25 4a1.25 1.25 0 1 0 2.5 0a1.25 1.25 0 0 0-2.5 0z', |
||||||
|
'MoreVerticalSolid': 'M10 6.5A1.75 1.75 0 1 1 10 3a1.75 1.75 0 0 1 0 3.5zM10 17a1.75 1.75 0 1 1 0-3.5a1.75 1.75 0 0 1 0 3.5zm-1.75-7a1.75 1.75 0 1 0 3.5 0a1.75 1.75 0 0 0-3.5 0z', |
||||||
|
|
||||||
|
'Delete': 'M11.5 4a1.5 1.5 0 0 0-3 0h-1a2.5 2.5 0 0 1 5 0H17a.5.5 0 0 1 0 1h-.554L15.15 16.23A2 2 0 0 1 13.163 18H6.837a2 2 0 0 1-1.987-1.77L3.553 5H3a.5.5 0 0 1-.492-.41L2.5 4.5A.5.5 0 0 1 3 4h8.5zm3.938 1H4.561l1.282 11.115a1 1 0 0 0 .994.885h6.326a1 1 0 0 0 .993-.885L15.438 5zM8.5 7.5c.245 0 .45.155.492.359L9 7.938v6.125c0 .241-.224.437-.5.437c-.245 0-.45-.155-.492-.359L8 14.062V7.939c0-.242.224-.438.5-.438zm3 0c.245 0 .45.155.492.359l.008.079v6.125c0 .241-.224.437-.5.437c-.245 0-.45-.155-.492-.359L11 14.062V7.939c0-.242.224-.438.5-.438z', |
||||||
|
'DeleteSolid': 'M10 1.25a2.75 2.75 0 0 1 2.739 2.5H17a.75.75 0 0 1 .102 1.493L17 5.25h-.583L15.15 16.23A2 2 0 0 1 13.163 18H6.837a2 2 0 0 1-1.987-1.77L3.582 5.25H3a.75.75 0 0 1-.743-.648L2.25 4.5a.75.75 0 0 1 .648-.743L3 3.75h4.261A2.75 2.75 0 0 1 10 1.25zM8.5 7.5c-.245 0-.45.155-.492.359L8 7.938v6.125l.008.078c.042.204.247.359.492.359s.45-.155.492-.359L9 14.062V7.939l-.008-.08C8.95 7.656 8.745 7.5 8.5 7.5zm3 0c-.245 0-.45.155-.492.359L11 7.938v6.125l.008.078c.042.204.247.359.492.359s.45-.155.492-.359l.008-.079V7.939l-.008-.08c-.042-.203-.247-.358-.492-.358zM10 2.75c-.605 0-1.11.43-1.225 1h2.45c-.116-.57-.62-1-1.225-1z', |
||||||
|
'DeleteDismiss': 'M11.5 4a1.5 1.5 0 0 0-3 0h3zm-4 0a2.5 2.5 0 0 1 5 0H17a.5.5 0 0 1 0 1h-.554l-.484 4.196a5.484 5.484 0 0 0-.987-.176L15.438 5H4.561l1.282 11.115a1 1 0 0 0 .994.885H9.6c.183.358.404.693.657 1h-3.42a2 2 0 0 1-1.987-1.77L3.553 5H3a.5.5 0 0 1-.492-.41L2.5 4.5A.5.5 0 0 1 3 4h4.5zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.646-1.146a.5.5 0 0 0-.708-.708L14.5 13.793l-1.146-1.147a.5.5 0 0 0-.708.708l1.147 1.146l-1.147 1.146a.5.5 0 0 0 .708.708l1.146-1.147l1.146 1.147a.5.5 0 0 0 .708-.708L15.207 14.5l1.147-1.146z', |
||||||
|
'DeleteDismissSolid': 'M10 1.25a2.75 2.75 0 0 1 2.739 2.5H17a.75.75 0 0 1 .102 1.493L17 5.25h-.583l-.455 3.946A5.5 5.5 0 0 0 10.258 18H6.836a2 2 0 0 1-1.987-1.77L3.582 5.25H3a.75.75 0 0 1-.743-.648L2.25 4.5a.75.75 0 0 1 .648-.743L3 3.75h4.261A2.75 2.75 0 0 1 10 1.25zm0 1.5c-.605 0-1.11.43-1.225 1h2.45c-.116-.57-.62-1-1.225-1zm9 11.75a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.646-1.146a.5.5 0 0 0-.708-.708L14.5 13.793l-1.146-1.147a.5.5 0 0 0-.708.708l1.147 1.146l-1.147 1.146a.5.5 0 0 0 .708.708l1.146-1.147l1.146 1.147a.5.5 0 0 0 .708-.708L15.207 14.5l1.147-1.146z', |
||||||
|
'DeleteOff': 'M3 4h.293L2.146 2.854a.5.5 0 1 1 .708-.708L8.26 7.553l9.594 9.593a.5.5 0 0 1-.708.708l-1.958-1.958l-.038.333A2 2 0 0 1 13.163 18H6.837a2 2 0 0 1-1.987-1.77L3.553 5H3a.5.5 0 0 1-.492-.41L2.5 4.5A.5.5 0 0 1 3 4zm11.286 10.993L12 12.707v1.355c0 .242-.224.438-.5.438c-.245 0-.45-.155-.492-.359L11 14.062v-2.355l-2-2v4.355c0 .242-.224.438-.5.438c-.245 0-.45-.155-.492-.359L8 14.062V8.707L4.596 5.303l1.247 10.812a1 1 0 0 0 .994.885h6.326a1 1 0 0 0 .993-.885l.13-1.122zm1.195-1.633l-.903-.903l.86-7.457H7.121l-1-1H7.5a2.5 2.5 0 0 1 5 0H17a.5.5 0 0 1 0 1h-.554l-.965 8.36zM11.5 4a1.5 1.5 0 0 0-3 0h3zm.5 5.879l-1-1v-.941c0-.242.224-.438.5-.438c.245 0 .45.155.492.359l.008.079v1.94z', |
||||||
|
'DeleteOffSolid': 'M15.188 15.896l-.038.333A2 2 0 0 1 13.163 18H6.837a2 2 0 0 1-1.987-1.77L3.582 5.25H3a.75.75 0 0 1-.743-.648L2.25 4.5a.75.75 0 0 1 .648-.743L3 3.75h.043l-.897-.896a.5.5 0 1 1 .708-.708L8.26 7.553l9.594 9.593a.5.5 0 0 1-.708.708l-1.958-1.958zM8 8.707v5.355l.008.08c.042.203.247.358.492.358s.45-.155.492-.359L9 14.062V9.707l-1-1zm3 3v2.355l.008.08c.042.203.247.358.492.358s.45-.155.492-.359l.008-.079v-1.355l-1-1zm0-2.828L5.871 3.75h1.39a2.75 2.75 0 0 1 5.478 0H17a.75.75 0 0 1 .102 1.493L17 5.25h-.583l-.936 8.11L12 9.879V7.938l-.008-.08c-.042-.203-.247-.358-.492-.358s-.45.155-.492.359L11 7.938v.94zM10 2.75c-.605 0-1.11.43-1.225 1h2.45c-.116-.57-.62-1-1.225-1z', |
||||||
|
'DeleteArrowBack': 'M11.5 4a1.5 1.5 0 0 0-3 0h3zm-4 0a2.5 2.5 0 0 1 5 0H17a.5.5 0 0 1 0 1h-.554l-.484 4.196a5.484 5.484 0 0 0-.987-.176L15.438 5H4.561l1.282 11.115a1 1 0 0 0 .994.885H9.6c.183.358.404.693.657 1h-3.42a2 2 0 0 1-1.987-1.77L3.553 5H3a.5.5 0 0 1-.492-.41L2.5 4.5A.5.5 0 0 1 3 4h4.5zm7 15a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm-.896-6.396l-.897.896h1.543A2.75 2.75 0 0 1 17 16.25v.25a.5.5 0 0 1-1 0v-.25a1.75 1.75 0 0 0-1.75-1.75h-1.543l.897.896a.5.5 0 0 1-.708.708l-1.752-1.753a.499.499 0 0 1 .002-.705l1.75-1.75a.5.5 0 0 1 .708.708z', |
||||||
|
'DeleteArrowBackSolid': 'M10 1.25a2.75 2.75 0 0 1 2.739 2.5H17a.75.75 0 0 1 .102 1.493L17 5.25h-.583l-.455 3.946A5.5 5.5 0 0 0 10.258 18H6.836a2 2 0 0 1-1.987-1.77L3.582 5.25H3a.75.75 0 0 1-.743-.648L2.25 4.5a.75.75 0 0 1 .648-.743L3 3.75h4.261A2.75 2.75 0 0 1 10 1.25zm0 1.5c-.605 0-1.11.43-1.225 1h2.45c-.116-.57-.62-1-1.225-1zM14.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm-.896-6.396l-.897.896h1.543A2.75 2.75 0 0 1 17 16.25v.25a.5.5 0 0 1-1 0v-.25a1.75 1.75 0 0 0-1.75-1.75h-1.543l.897.896a.5.5 0 0 1-.708.708l-1.752-1.753a.499.499 0 0 1 .002-.705l1.75-1.75a.5.5 0 0 1 .708.708z', |
||||||
|
'DeleteLines': 'M7.5 4a2.5 2.5 0 0 1 5 0H17a.5.5 0 0 1 0 1h-.554l-.923 8h-1.007l.922-8H4.561l1.282 11.115a1 1 0 0 0 .994.885h5.248c.066.186.168.356.297.5c-.13.144-.23.314-.297.5H6.837a2 2 0 0 1-1.987-1.77L3.553 5H3a.5.5 0 0 1-.492-.41L2.5 4.5A.5.5 0 0 1 3 4h4.5zm4 0a1.5 1.5 0 0 0-3 0h3zm2 12a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zm0-2a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zm-.5 4.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z', |
||||||
|
'DeleteLinesSolid': 'M10 1.25a2.75 2.75 0 0 1 2.739 2.5H17a.75.75 0 0 1 .102 1.493L17 5.25h-.583L15.523 13H13.5a1.5 1.5 0 0 0-1.118 2.5a1.494 1.494 0 0 0-.382 1c0 .384.144.735.382 1c-.13.144-.23.314-.297.5H6.837a2 2 0 0 1-1.987-1.77L3.582 5.25H3a.75.75 0 0 1-.743-.648L2.25 4.5a.75.75 0 0 1 .648-.743L3 3.75h4.261A2.75 2.75 0 0 1 10 1.25zm0 1.5c-.605 0-1.11.43-1.225 1h2.45c-.116-.57-.62-1-1.225-1zm3 11.75a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z', |
||||||
|
|
||||||
|
'Eye': 'M3.26 11.602C3.942 8.327 6.793 6 10 6c3.206 0 6.057 2.327 6.74 5.602a.5.5 0 0 0 .98-.204C16.943 7.673 13.693 5 10 5c-3.693 0-6.943 2.673-7.72 6.398a.5.5 0 0 0 .98.204zM10 8a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7zm-2.5 3.5a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0z', |
||||||
|
'EyeSolid': 'M3.26 11.602C3.942 8.327 6.793 6 10 6c3.206 0 6.057 2.327 6.74 5.602a.5.5 0 0 0 .98-.204C16.943 7.673 13.693 5 10 5c-3.693 0-6.943 2.673-7.72 6.398a.5.5 0 0 0 .98.204zM9.99 8a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7z', |
||||||
|
'EyeOff': 'M2.854 2.146a.5.5 0 1 0-.708.708l3.5 3.498a8.097 8.097 0 0 0-3.366 5.046a.5.5 0 1 0 .98.204a7.09 7.09 0 0 1 3.107-4.528L7.953 8.66a3.5 3.5 0 1 0 4.886 4.886l4.307 4.308a.5.5 0 0 0 .708-.708l-15-15zm9.265 10.68A2.5 2.5 0 1 1 8.673 9.38l3.446 3.447zm-1.995-4.824l3.374 3.374a3.5 3.5 0 0 0-3.374-3.374zM10 6c-.57 0-1.129.074-1.666.213l-.803-.803A7.648 7.648 0 0 1 10 5c3.693 0 6.942 2.673 7.72 6.398a.5.5 0 0 1-.98.204C16.058 8.327 13.207 6 10 6z', |
||||||
|
'EyeOffSolid': 'M2.854 2.146a.5.5 0 1 0-.708.708l3.5 3.498a8.097 8.097 0 0 0-3.366 5.046a.5.5 0 1 0 .979.204a7.09 7.09 0 0 1 3.108-4.528L7.95 8.656a3.5 3.5 0 1 0 4.884 4.884l4.313 4.314a.5.5 0 0 0 .708-.708l-15-15zm7.27 5.857l3.363 3.363a3.5 3.5 0 0 0-3.363-3.363zM7.53 5.41l.803.803A6.632 6.632 0 0 1 10 6c3.206 0 6.057 2.327 6.74 5.602a.5.5 0 1 0 .98-.204C16.943 7.673 13.693 5 10 5c-.855 0-1.687.143-2.469.41z', |
||||||
|
'EyeTracking': 'M3 4.5A1.5 1.5 0 0 1 4.5 3h3a.5.5 0 0 0 0-1h-3A2.5 2.5 0 0 0 2 4.5v3a.5.5 0 0 0 1 0v-3zm0 11A1.5 1.5 0 0 0 4.5 17h3a.5.5 0 0 1 0 1h-3A2.5 2.5 0 0 1 2 15.5v-3a.5.5 0 0 1 1 0v3zM15.5 3A1.5 1.5 0 0 1 17 4.5v3a.5.5 0 0 0 1 0v-3A2.5 2.5 0 0 0 15.5 2h-3a.5.5 0 0 0 0 1h3zM17 15.5a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 0 0 1h3a2.5 2.5 0 0 0 2.5-2.5v-3a.5.5 0 0 0-1 0v3zm-10-4a3 3 0 1 1 6 0a3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4a2 2 0 0 0 0-4zm-5.052.223v.001a.5.5 0 0 1-.895-.448L4.5 9.5a24.558 24.558 0 0 1-.447-.225l.001-.001l.002-.004l.005-.01a2.106 2.106 0 0 1 .082-.145a5.14 5.14 0 0 1 .249-.377A6.49 6.49 0 0 1 5.425 7.62C6.375 6.805 7.863 6 10 6s3.624.805 4.575 1.62c.473.406.812.812 1.034 1.119a5.13 5.13 0 0 1 .33.521l.005.01l.002.004l.001.002l-.447.224l.447-.224a.5.5 0 0 1-.893.45v-.002l-.002-.001l-.009-.018a4.133 4.133 0 0 0-.245-.381a5.487 5.487 0 0 0-.873-.944C13.125 7.695 11.863 7 10 7s-3.125.695-3.924 1.38a5.49 5.49 0 0 0-.874.944a4.14 4.14 0 0 0-.245.381l-.01.018z', |
||||||
|
'EyeTrackingSolid': 'M4.5 3A1.5 1.5 0 0 0 3 4.5v3a.5.5 0 0 1-1 0v-3A2.5 2.5 0 0 1 4.5 2h3a.5.5 0 0 1 0 1h-3zm0 14A1.5 1.5 0 0 1 3 15.5v-3a.5.5 0 0 0-1 0v3A2.5 2.5 0 0 0 4.5 18h3a.5.5 0 0 0 0-1h-3zM17 4.5A1.5 1.5 0 0 0 15.5 3h-3a.5.5 0 0 1 0-1h3A2.5 2.5 0 0 1 18 4.5v3a.5.5 0 0 1-1 0v-3zM15.5 17a1.5 1.5 0 0 0 1.5-1.5v-3a.5.5 0 0 1 1 0v3a2.5 2.5 0 0 1-2.5 2.5h-3a.5.5 0 0 1 0-1h3zM7 11.5a3 3 0 1 1 6 0a3 3 0 0 1-6 0zM4.948 9.723v.001a.5.5 0 0 1-.895-.448L4.5 9.5a24.558 24.558 0 0 1-.447-.225l.001-.001l.002-.004l.005-.01a2.106 2.106 0 0 1 .082-.145a5.14 5.14 0 0 1 .249-.377A6.49 6.49 0 0 1 5.425 7.62C6.375 6.805 7.863 6 10 6s3.624.805 4.575 1.62c.473.406.812.812 1.034 1.119a5.13 5.13 0 0 1 .33.521l.005.01l.002.004l.001.002l-.447.224l.447-.224a.5.5 0 0 1-.893.45v-.002l-.002-.001l-.009-.018a4.133 4.133 0 0 0-.245-.381a5.487 5.487 0 0 0-.873-.944C13.125 7.695 11.863 7 10 7s-3.125.695-3.924 1.38a5.49 5.49 0 0 0-.874.944a4.14 4.14 0 0 0-.245.381l-.01.018z', |
||||||
|
'EyeTrackingOff': 'M2.414 3.121C2.152 3.517 2 3.991 2 4.5v3a.5.5 0 0 0 1 0v-3c0-.232.052-.45.146-.647l3.141 3.141A6.592 6.592 0 0 0 4.392 8.74a5.14 5.14 0 0 0-.33.521l-.006.01l-.002.004v.001s-.001.001.446.225l-.447-.224a.5.5 0 0 0 .894.448v-.001l.01-.018l.045-.078a4.14 4.14 0 0 1 .2-.303A5.582 5.582 0 0 1 7.02 7.726l1.293 1.293a3 3 0 1 0 4.168 4.168l3.667 3.667A1.494 1.494 0 0 1 15.5 17h-3a.5.5 0 0 0 0 1h3c.51 0 .983-.152 1.379-.414l.267.268a.5.5 0 0 0 .708-.707l-.268-.268l-.732-.732l-3.938-3.938L9.29 8.584L8.007 7.3l-.78-.78l-3.374-3.374l-.732-.732l-.267-.268a.5.5 0 1 0-.708.708l.268.267zm9.34 9.34A2 2 0 1 1 9.04 9.746l2.715 2.715zm6.221 3.393c.016-.116.025-.234.025-.354v-3a.5.5 0 0 0-1 0v2.379l.975.975zM9.17 7.048C9.432 7.017 9.709 7 10 7c1.863 0 3.126.695 3.925 1.38c.402.344.688.688.873.944a4.133 4.133 0 0 1 .245.381l.01.018v.002a.5.5 0 0 0 .894-.449v-.002l-.003-.004l-.005-.01a5.13 5.13 0 0 0-.33-.522a6.491 6.491 0 0 0-1.034-1.118C13.626 6.805 12.137 6 10 6a7.68 7.68 0 0 0-1.695.183l.865.865zm6.777 2.228l-.058.03l-.387.193l.445-.223zM5.121 3H7.5a.5.5 0 0 0 0-1h-3c-.12 0-.238.008-.354.025L5.121 3zM4.5 17A1.5 1.5 0 0 1 3 15.5v-3a.5.5 0 0 0-1 0v3A2.5 2.5 0 0 0 4.5 18h3a.5.5 0 0 0 0-1h-3zm11-14A1.5 1.5 0 0 1 17 4.5v3a.5.5 0 0 0 1 0v-3A2.5 2.5 0 0 0 15.5 2h-3a.5.5 0 0 0 0 1h3z', |
||||||
|
'EyeTrackingOffSolid': 'M2.414 3.121C2.152 3.517 2 3.991 2 4.5v3a.5.5 0 0 0 1 0v-3c0-.232.052-.45.146-.647l3.141 3.141A6.592 6.592 0 0 0 4.392 8.74a5.14 5.14 0 0 0-.33.521l-.006.01l-.002.004v.001s-.001.001.446.225l-.447-.224a.5.5 0 0 0 .894.448v-.001l.01-.018l.045-.078a4.14 4.14 0 0 1 .2-.303A5.582 5.582 0 0 1 7.02 7.726l1.293 1.293a3 3 0 1 0 4.168 4.168l3.667 3.667A1.494 1.494 0 0 1 15.5 17h-3a.5.5 0 0 0 0 1h3c.51 0 .983-.152 1.379-.414l.267.268a.5.5 0 0 0 .708-.707l-.268-.268l-.732-.732l-3.938-3.938L9.29 8.584L8.007 7.3l-.78-.78l-3.374-3.374l-.732-.732l-.267-.268a.5.5 0 1 0-.708.708l.268.267zm15.561 12.733c.016-.116.025-.234.025-.354v-3a.5.5 0 0 0-1 0v2.379l.975.975zM9.17 7.048C9.432 7.017 9.709 7 10 7c1.863 0 3.126.695 3.925 1.38c.402.344.688.688.873.944a4.133 4.133 0 0 1 .245.381l.01.018v.002a.5.5 0 0 0 .894-.449v-.002l-.003-.004l-.005-.01a5.13 5.13 0 0 0-.33-.522a6.491 6.491 0 0 0-1.034-1.118C13.626 6.805 12.137 6 10 6a7.68 7.68 0 0 0-1.695.183l.865.865zm6.777 2.228l-.058.03l-.387.193l.445-.223zM5.121 3H7.5a.5.5 0 0 0 0-1h-3c-.12 0-.238.008-.354.025L5.121 3zM3 15.5A1.5 1.5 0 0 0 4.5 17h3a.5.5 0 0 1 0 1h-3A2.5 2.5 0 0 1 2 15.5v-3a.5.5 0 0 1 1 0v3zm14-11A1.5 1.5 0 0 0 15.5 3h-3a.5.5 0 0 1 0-1h3A2.5 2.5 0 0 1 18 4.5v3a.5.5 0 0 1-1 0v-3z', |
||||||
|
|
||||||
|
'Share': 'M13.33 12.838l4.497-4.423l.057-.065a.587.587 0 0 0-.057-.767L13.33 3.162l-.062-.053c-.36-.27-.89-.01-.89.469v2.13l-.225.015c-3.563.282-5.65 2.537-6.148 6.627c-.064.525.538.854.928.506c1.431-1.278 2.91-2.072 4.445-2.39c.246-.051.493-.09.742-.117l.258-.023v2.096l.005.082c.06.453.609.666.947.334zM12.226 6.72l1.152-.077V4.61l3.446 3.388l-3.446 3.39V9.231l-1.356.122h-.008c-1.703.183-3.31.865-4.827 2.002c.298-1.339.807-2.346 1.476-3.067c.83-.895 1.99-1.443 3.563-1.569zM5.5 4A2.5 2.5 0 0 0 3 6.5v8A2.5 2.5 0 0 0 5.5 17h8a2.5 2.5 0 0 0 2.5-2.5v-1a.5.5 0 0 0-1 0v1a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5h3a.5.5 0 0 0 0-1h-3z', |
||||||
|
'ShareSolid': 'M12.378 5.708v-2.13c0-.48.53-.738.89-.47l.062.054l4.497 4.42c.21.207.229.539.057.768l-.057.065l-4.497 4.423c-.338.332-.887.119-.947-.334l-.005-.082v-2.096l-.258.023c-1.8.193-3.526 1.024-5.187 2.507c-.39.348-.992.02-.928-.506c.498-4.09 2.585-6.345 6.148-6.627l.225-.015zM5.5 4A2.5 2.5 0 0 0 3 6.5v8A2.5 2.5 0 0 0 5.5 17h8a2.5 2.5 0 0 0 2.5-2.5v-1a.5.5 0 0 0-1 0v1a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5h3a.5.5 0 0 0 0-1h-3z', |
||||||
|
|
||||||
|
'Alert': 'M9.998 2c3.149 0 5.744 2.335 5.984 5.355l.014.223l.004.224l-.001 3.596l.925 2.222c.023.054.04.11.053.167l.016.086l.008.132a1 1 0 0 1-.749.963l-.116.027l-.135.01l-3.501-.001l-.005.161a2.5 2.5 0 0 1-4.99 0l-.005-.161H3.999a.998.998 0 0 1-.26-.034l-.124-.042a1 1 0 0 1-.603-1.052l.021-.128l.043-.128l.923-2.219L4 7.793l.004-.225C4.127 4.451 6.771 2 9.998 2zM11.5 15.004h-3l.007.141a1.5 1.5 0 0 0 1.349 1.348L10 16.5a1.5 1.5 0 0 0 1.493-1.355l.007-.141zM9.998 3c-2.623 0-4.77 1.924-4.98 4.385l-.014.212L5 7.802V11.5l-.038.192l-.963 2.313l11.958.002l.045-.002l-.964-2.313L15 11.5V7.812l-.004-.204C14.891 5.035 12.695 3 9.998 3z', |
||||||
|
'AlertSolid': 'M12.45 16.002a2.5 2.5 0 0 1-4.9 0h4.9zM9.998 2c3.149 0 5.744 2.335 5.984 5.355l.013.223l.005.224l-.001 3.606l.954 2.587l.025.085l.016.086l.005.089c0 .315-.196.59-.522.707l-.114.033l-.114.01H3.751a.75.75 0 0 1-.259-.047c-.287-.105-.476-.372-.482-.716l.004-.117l.034-.13l.95-2.584L4 7.793l.004-.225C4.127 4.451 6.771 2 9.998 2z', |
||||||
|
'AlertOff': 'M4.004 7.568a5.62 5.62 0 0 1 .58-2.277L2.146 2.854a.5.5 0 1 1 .708-.708l15 15a.5.5 0 0 1-.708.708l-2.849-2.85H12.5l-.005.161a2.5 2.5 0 0 1-4.99 0l-.005-.161H3.999a.998.998 0 0 1-.26-.034l-.124-.042a1 1 0 0 1-.603-1.052l.021-.128l.043-.128l.923-2.219L4 7.793l.004-.225zm9.295 6.438l-7.96-7.96c-.171.42-.282.87-.322 1.339l-.013.212L5 7.802V11.5l-.038.192l-.963 2.313l9.3.001zm-1.8.998h-3l.008.141a1.5 1.5 0 0 0 1.349 1.348L10 16.5a1.5 1.5 0 0 0 1.493-1.355l.007-.141zm3.54-3.312l.874 2.1l.852.852a.977.977 0 0 0 .236-.64l-.008-.13l-.016-.087a.996.996 0 0 0-.053-.167L16 11.398L16 7.802l-.005-.224l-.013-.223C15.742 4.335 13.147 2 9.998 2c-1.64 0-3.128.633-4.213 1.664l.707.707A5.1 5.1 0 0 1 9.998 3c2.697 0 4.893 2.035 4.998 4.608l.004.204V11.5l.038.192z', |
||||||
|
'AlertOffSolid': 'M4.004 7.568a5.62 5.62 0 0 1 .58-2.277L2.146 2.854a.5.5 0 1 1 .708-.708l15 15a.5.5 0 0 1-.708.708l-2.849-2.85H3.752a.75.75 0 0 1-.259-.046c-.287-.105-.476-.372-.482-.716l.004-.117l.034-.13l.95-2.584L4 7.793l.004-.225zM17 14.255a.72.72 0 0 1-.163.46L5.786 3.663A6.095 6.095 0 0 1 9.997 2c3.149 0 5.744 2.335 5.984 5.355l.013.223l.005.224l-.001 3.606l.954 2.587l.025.085l.016.086l.005.089zm-4.55 1.747a2.5 2.5 0 0 1-4.899 0h4.9z', |
||||||
|
'AlertOn': 'M1.796 2.098a.5.5 0 1 0-.6.8L3.198 4.4a.5.5 0 1 0 .6-.8L1.796 2.098zM1 7a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 0-1H1zm8.998-5c3.149 0 5.744 2.334 5.984 5.355l.014.222l.004.225l-.001 3.596l.925 2.222a1 1 0 0 1 .053.167l.016.086l.008.131a1 1 0 0 1-.749.963l-.116.027l-.135.01H12.5l-.005.16a2.5 2.5 0 0 1-4.99 0l-.005-.16H3.999c-.088 0-.175-.011-.26-.034l-.124-.043a1 1 0 0 1-.603-1.052l.021-.127l.043-.128l.923-2.22L4 7.793l.004-.224C4.127 4.45 6.771 2 9.998 2zM11.5 15.004h-3l.007.141a1.5 1.5 0 0 0 1.349 1.348L10 16.5a1.5 1.5 0 0 0 1.493-1.356l.007-.14zM9.998 3c-2.623 0-4.77 1.923-4.98 4.385l-.014.212L5 7.802V11.5l-.038.192l-.963 2.312l11.958.002l.045-.002l-.964-2.312L15 11.5V7.812l-.004-.204C14.891 5.034 12.695 3 9.998 3zm8.906-.802a.5.5 0 0 0-.7-.1L16.202 3.6a.5.5 0 0 0 .6.8l2.002-1.502a.5.5 0 0 0 .1-.7zM19.5 7.5A.5.5 0 0 0 19 7h-1.5a.5.5 0 0 0 0 1H19a.5.5 0 0 0 .5-.5z', |
||||||
|
'AlertOnSolid': 'M1.796 2.098a.5.5 0 1 0-.6.8L3.198 4.4a.5.5 0 1 0 .6-.8L1.796 2.098zM1 7a.5.5 0 0 0 0 1h1.5a.5.5 0 0 0 0-1H1zM12.45 16a2.501 2.501 0 0 1-4.9 0h4.9zM9.998 2c3.149 0 5.744 2.334 5.984 5.355l.014.222l.004.225l-.001 3.606l.954 2.587l.025.084l.016.087l.005.088c0 .315-.196.59-.522.707l-.113.033l-.115.01H3.751a.75.75 0 0 1-.259-.046c-.287-.106-.476-.372-.482-.716l.004-.118l.034-.13l.951-2.583L4 7.792l.004-.224C4.127 4.45 6.771 2 9.998 2zm8.906.198a.5.5 0 0 0-.7-.1L16.202 3.6a.5.5 0 0 0 .6.8l2.002-1.502a.5.5 0 0 0 .1-.7zM19.5 7.5A.5.5 0 0 0 19 7h-1.5a.5.5 0 0 0 0 1H19a.5.5 0 0 0 .5-.5z', |
||||||
|
'AlertSnooze': 'M5 11.5V8.055A.505.505 0 0 0 5.003 8a5 5 0 0 1 6.36-4.813a.5.5 0 1 0 .272-.962A6 6 0 0 0 4.004 7.94A.504.504 0 0 0 4 7.998V11.4l-.923 2.216A1 1 0 0 0 4 15h3.5a2.5 2.5 0 0 0 5 0H16a1 1 0 0 0 .923-1.384L16 11.4V9.998a.5.5 0 0 0-1 0V11.5a.5.5 0 0 0 .039.192L16 14H4l.962-2.308A.5.5 0 0 0 5 11.5zM8.5 15h3a1.5 1.5 0 0 1-3 0zM14 2h3.5a.5.5 0 0 1 .452.714l-.043.073L14.96 7h2.54a.5.5 0 0 1 .09.992L17.5 8H14a.5.5 0 0 1-.452-.714l.042-.073L16.54 3H14a.5.5 0 0 1-.09-.992L14 2zM9.5 6h2.5a.5.5 0 0 1 .432.753l-.048.067L10.57 9H12a.5.5 0 0 1 .09.992L12 10h-2.5a.5.5 0 0 1-.432-.753l.048-.067L10.933 7H9.501a.5.5 0 0 1-.09-.992L9.501 6z', |
||||||
|
'AlertSnoozeOff': 'M9.998 2c.891 0 1.738.187 2.5.524A1.5 1.5 0 0 0 13.998 4h.627l-1.286 1.826A1.475 1.475 0 0 0 11.999 5H9.454l-.18.016l-.044.008a1.5 1.5 0 0 0-.33 2.852l-.578.694l-.094.131l-.02.034C7.63 9.705 8.305 11 9.498 11h2.546l.179-.016l.044-.008a1.5 1.5 0 0 0 1.088-2.117c.191.09.407.141.643.141H16v2.408l.953 2.587l.026.085l.015.086l.005.089c0 .315-.195.59-.522.707l-.113.033l-.115.01H3.752a.75.75 0 0 1-.26-.047c-.287-.105-.475-.372-.482-.716l.004-.117l.034-.13l.951-2.584L4 7.793l.005-.225C4.127 4.451 6.77 2 9.998 2zm2.452 14.002a2.501 2.501 0 0 1-4.9 0h4.9zM13.998 2h3.5a.5.5 0 0 1 .452.714l-.042.073L14.958 7h2.54a.5.5 0 0 1 .09.992l-.09.008h-3.5a.5.5 0 0 1-.452-.714l.042-.073L16.538 3h-2.54a.5.5 0 0 1-.09-.992l.09-.008zM9.499 6h2.5a.5.5 0 0 1 .432.753l-.048.067L10.567 9h1.432a.5.5 0 0 1 .09.992l-.09.008h-2.5a.5.5 0 0 1-.432-.753l.048-.067L10.93 7H9.5a.5.5 0 0 1-.09-.992L9.5 6z', |
||||||
|
'AlertUrgent': 'M13.264 2.078a.5.5 0 1 0-.523.852c2.258 1.384 4.12 3.414 4.26 7.09A.5.5 0 0 0 18 9.982c-.157-4.099-2.278-6.398-4.736-7.904zm-1.178 2.65a.5.5 0 0 1 .694-.134c1.607 1.085 2.715 2.638 2.888 4.424c.016.16.024.323.024.487a.5.5 0 0 1-1 0c0-.132-.007-.262-.02-.39c-.136-1.418-1.024-2.728-2.452-3.693a.5.5 0 0 1-.134-.694zm-7.006.71a5.158 5.158 0 0 0-2.614 6.811l1.223 2.749l.09 2.32a.75.75 0 0 0 1.054.656l9.727-4.33a.75.75 0 0 0 .218-1.223l-1.664-1.619l-1.224-2.749a5.158 5.158 0 0 0-6.81-2.614zm-1.7 6.404a4.158 4.158 0 0 1 7.596-3.382l1.302 2.925l1.538 1.495l-9.052 4.03l-.083-2.143l-1.302-2.925zm7.298 6.034a1.49 1.49 0 0 1-1.848-.54l2.685-1.194a1.49 1.49 0 0 1-.837 1.734z', |
||||||
|
'AlertUrgentSolid': 'M2.466 12.25a5.158 5.158 0 0 1 9.424-4.197l1.224 2.749l1.664 1.619a.75.75 0 0 1-.218 1.222l-9.727 4.331a.75.75 0 0 1-1.054-.656l-.09-2.32l-1.223-2.749zm6.364 5.087a1.49 1.49 0 0 0 2.685-1.195L8.83 17.337zm3.256-12.609a.5.5 0 0 1 .694-.134c1.607 1.085 2.715 2.638 2.888 4.424c.016.16.024.323.024.487a.5.5 0 1 1-1 0a4.04 4.04 0 0 0-.02-.39c-.136-1.418-1.024-2.728-2.452-3.693a.5.5 0 0 1-.134-.694zm.49-2.485a.5.5 0 0 1 .688-.165c2.458 1.506 4.58 3.805 4.736 7.904a.5.5 0 0 1-1 .038C16.86 6.344 15 4.314 12.741 2.93a.5.5 0 0 1-.165-.687z', |
||||||
|
|
||||||
|
'ArrowClockwise': 'M3.066 9.05a7 7 0 0 1 12.557-3.22l.126.17H12.5a.5.5 0 1 0 0 1h4a.5.5 0 0 0 .5-.5V2.502a.5.5 0 0 0-1 0v2.207a8 8 0 1 0 1.986 4.775a.5.5 0 0 0-.998.064A7 7 0 1 1 3.066 9.05z', |
||||||
|
'ArrowClockwiseSolid': 'M10.628 2.025a8 8 0 1 0 7.367 7.714a.75.75 0 1 0-1.5.045a6.5 6.5 0 1 1-1.573-4.029l.204.248h-2.379l-.101.008a.75.75 0 0 0 0 1.486l.101.007h4l.102-.007a.75.75 0 0 0 .641-.641l.007-.102v-4l-.007-.102a.75.75 0 0 0-.641-.641l-.102-.007l-.102.007a.75.75 0 0 0-.641.641l-.007.102l.001 1.953a7.977 7.977 0 0 0-5.37-2.682z', |
||||||
|
'ArrowClockwiseDashes': 'M8.132 2.22a8.02 8.02 0 0 1 3.736 0a.5.5 0 0 1-.233.972a7.02 7.02 0 0 0-3.27 0a.5.5 0 1 1-.233-.973zM6.507 3.342a.5.5 0 0 1-.165.687A7.039 7.039 0 0 0 4.03 6.342a.5.5 0 0 1-.852-.523A8.039 8.039 0 0 1 5.82 3.18a.5.5 0 0 1 .688.164zm7.674-.165a.5.5 0 1 0-.523.852A7.04 7.04 0 0 1 15.745 6H12.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 0-1 0v2.208a8.035 8.035 0 0 0-1.82-1.53zM2.822 7.762a.5.5 0 0 1 .37.603a7.02 7.02 0 0 0 0 3.27a.5.5 0 0 1-.973.233a8.02 8.02 0 0 1 0-3.736a.5.5 0 0 1 .603-.37zM18 10v-.5a.5.5 0 0 0-1 0v.5a7.02 7.02 0 0 1-.192 1.635a.5.5 0 1 0 .973.233c.143-.6.219-1.225.219-1.868zM3.343 13.493a.5.5 0 0 1 .687.165a7.038 7.038 0 0 0 2.312 2.312a.5.5 0 1 1-.523.852a8.038 8.038 0 0 1-2.64-2.641a.5.5 0 0 1 .164-.688zm13.479.688a.5.5 0 0 0-.852-.523a7.037 7.037 0 0 1-2.313 2.312a.5.5 0 0 0 .524.852a8.037 8.037 0 0 0 2.64-2.641zm-9.06 2.997a.5.5 0 0 1 .603-.37a7.02 7.02 0 0 0 3.27 0a.5.5 0 1 1 .233.973a8.02 8.02 0 0 1-3.736 0a.5.5 0 0 1-.37-.603z', |
||||||
|
'ArrowClockwiseDashesSolid': 'M8.44 2.152a8.035 8.035 0 0 1 3.12 0a.75.75 0 0 1-.29 1.472a6.536 6.536 0 0 0-2.54 0a.75.75 0 0 1-.29-1.472zm4.965 1.402a.75.75 0 0 1 1.04-.206A8.04 8.04 0 0 1 16 4.708V2.75a.75.75 0 0 1 1.5 0v4a.75.75 0 0 1-.75.75h-4a.75.75 0 0 1 0-1.5h2.374a6.541 6.541 0 0 0-1.513-1.406a.75.75 0 0 1-.206-1.04zm-7.016 1.04a.75.75 0 0 0-.834-1.246a8.04 8.04 0 0 0-2.207 2.207a.75.75 0 0 0 1.246.834A6.54 6.54 0 0 1 6.39 4.594zM3.034 7.85a.75.75 0 0 1 .59.882a6.535 6.535 0 0 0 0 2.538a.75.75 0 0 1-1.472.291a8.035 8.035 0 0 1 0-3.12a.75.75 0 0 1 .882-.59zM18 10v-.25a.75.75 0 0 0-1.5 0V10c0 .435-.043.86-.124 1.27a.75.75 0 1 0 1.472.29c.1-.505.152-1.027.152-1.56zM3.554 13.405a.75.75 0 0 1 1.04.206a6.54 6.54 0 0 0 1.795 1.795a.75.75 0 0 1-.834 1.246a8.042 8.042 0 0 1-2.207-2.207a.75.75 0 0 1 .206-1.04zm13.098 1.04a.75.75 0 0 0-1.246-.834a6.54 6.54 0 0 1-1.795 1.795a.75.75 0 0 0 .834 1.246a8.043 8.043 0 0 0 2.207-2.207zM7.85 16.966a.75.75 0 0 1 .882-.59a6.535 6.535 0 0 0 2.538 0a.75.75 0 1 1 .291 1.472a8.033 8.033 0 0 1-3.12 0a.75.75 0 0 1-.59-.881z', |
||||||
|
'ArrowHookDownLeft': 'M6 4.5a.5.5 0 0 1 .5-.5H11c1.636 0 2.9.618 3.749 1.574C15.59 6.521 16 7.768 16 9c0 1.232-.41 2.48-1.251 3.426C13.899 13.382 12.636 14 11 14H5.707l2.647 2.646a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 1 1 .708.708L5.707 13H11c1.364 0 2.35-.507 3.001-1.238C14.66 11.02 15 10.018 15 9s-.34-2.02-.999-2.762C13.351 5.507 12.364 5 11 5H6.5a.5.5 0 0 1-.5-.5z', |
||||||
|
'ArrowHookDownLeftSolid': 'M6 4.75A.75.75 0 0 1 6.75 4h4.5c1.586 0 2.696.621 3.53 1.588C15.6 6.54 16 7.784 16 9c0 1.216-.3 2.46-1.12 3.412c-.834.967-2.044 1.588-3.63 1.588H6.56l2.22 2.22a.75.75 0 1 1-1.06 1.06l-3.5-3.5a.75.75 0 0 1 .02-1.08l3.5-3.25a.75.75 0 0 1 1.02 1.1l-2.1 1.95h4.59c1.164 0 1.86-.441 2.4-1.068c.554-.642.85-1.523.85-2.432s-.296-1.79-.85-2.432c-.54-.627-1.236-1.068-2.4-1.068h-4.5A.75.75 0 0 1 6 4.75z', |
||||||
|
'ArrowHookDownRight': 'M4 9a5 5 0 0 1 5-5h4.5a.5.5 0 0 1 0 1H9a4 4 0 1 0 0 8h5.293l-2.7-2.7a.5.5 0 1 1 .708-.706l3.539 3.539a.5.5 0 0 1 .125.497a.499.499 0 0 1-.135.247l-3.533 3.533a.5.5 0 0 1-.707-.707L14.293 14H9a5 5 0 0 1-5-5z', |
||||||
|
'ArrowHookDownRightSolid': 'M9 14c.06 0-.06.002 0 0c.023.002.227 0 .25 0h4.393l-2.268 2.268a.75.75 0 1 0 1.061 1.06l3.353-3.352a.749.749 0 0 0 .212-.639a.747.747 0 0 0-.215-.444L12.54 9.646a.75.75 0 1 0-1.06 1.061L13.27 12.5H9a3.5 3.5 0 1 1 0-7h4.25a.75.75 0 0 0 0-1.5H9a5 5 0 0 0 0 10z', |
||||||
|
|
||||||
|
'Bookmark': 'M4 4.5A2.5 2.5 0 0 1 6.5 2h7A2.5 2.5 0 0 1 16 4.5v13a.5.5 0 0 1-.794.404L10 14.118l-5.206 3.786A.5.5 0 0 1 4 17.5v-13zM6.5 3A1.5 1.5 0 0 0 5 4.5v12.018l4.706-3.422a.5.5 0 0 1 .588 0L15 16.518V4.5A1.5 1.5 0 0 0 13.5 3h-7z', |
||||||
|
'BookmarkSolid': 'M4 4.5A2.5 2.5 0 0 1 6.5 2h7A2.5 2.5 0 0 1 16 4.5v13a.5.5 0 0 1-.794.404L10 14.118l-5.206 3.786A.5.5 0 0 1 4 17.5v-13z', |
||||||
|
'BookmarkAdd': 'M19 5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V5h-1.5a.5.5 0 0 0 0 1H14v1.5a.5.5 0 0 0 1 0V6h1.5a.5.5 0 0 0 0-1H15V3.5zm0 13.018v-5.54a5.489 5.489 0 0 0 1-.185V17.5a.5.5 0 0 1-.794.404L10 14.118l-5.206 3.786A.5.5 0 0 1 4 17.5v-13A2.5 2.5 0 0 1 6.5 2h3.757A5.504 5.504 0 0 0 9.6 3H6.5A1.5 1.5 0 0 0 5 4.5v12.018l4.706-3.422a.5.5 0 0 1 .588 0L15 16.518z', |
||||||
|
'BookmarkAddSolid': 'M19 5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-4-2a.5.5 0 0 0-1 0V5h-1.5a.5.5 0 0 0 0 1H14v1.5a.5.5 0 0 0 1 0V6h1.5a.5.5 0 0 0 0-1H15V3.5zm-.5 7.5c.52 0 1.023-.072 1.5-.207V17.5a.5.5 0 0 1-.794.404L10 14.118l-5.206 3.786A.5.5 0 0 1 4 17.5v-13A2.5 2.5 0 0 1 6.5 2h3.757a5.5 5.5 0 0 0 4.243 9z', |
||||||
|
'BookmarkMultiple': 'M6.268 3A2 2 0 0 1 8 2h4.5A3.5 3.5 0 0 1 16 5.5v10a.5.5 0 0 1-.777.416L15 15.768V5.5A2.5 2.5 0 0 0 12.5 3H6.268zM6 4a2 2 0 0 0-2 2v11.5a.5.5 0 0 0 .777.416L9 15.101l4.223 2.815A.5.5 0 0 0 14 17.5V6a2 2 0 0 0-2-2H6zM5 6a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v10.566l-3.723-2.482a.5.5 0 0 0-.554 0L5 16.566V6z', |
||||||
|
'BookmarkMultipleSolid': 'M6.268 3A2 2 0 0 1 8 2h4.5A3.5 3.5 0 0 1 16 5.5v10a.5.5 0 0 1-.777.416L15 15.768V5.5A2.5 2.5 0 0 0 12.5 3H6.268zM6 4a2 2 0 0 0-2 2v11.5a.5.5 0 0 0 .777.416L9 15.101l4.223 2.815A.5.5 0 0 0 14 17.5V6a2 2 0 0 0-2-2H6z', |
||||||
|
'BookmarkSearch': 'M15.596 7.303a3.5 3.5 0 1 1 .707-.707l2.55 2.55a.5.5 0 0 1-.707.708l-2.55-2.55zM16 4.5a2.5 2.5 0 1 0-5 0a2.5 2.5 0 0 0 5 0zm0 4.621V17.5a.5.5 0 0 1-.794.404L10 14.118l-5.206 3.786A.5.5 0 0 1 4 17.5v-13A2.5 2.5 0 0 1 6.5 2h3.258a4.484 4.484 0 0 0-.502 1H6.5A1.5 1.5 0 0 0 5 4.5v12.018l4.706-3.422a.5.5 0 0 1 .588 0L15 16.518V8.744c.15-.053.297-.114.44-.183l.56.56z', |
||||||
|
'BookmarkSearchSearch': 'M15.596 7.303a3.5 3.5 0 1 1 .707-.707l2.55 2.55a.5.5 0 0 1-.707.708l-2.55-2.55zM16 4.5a2.5 2.5 0 1 0-5 0a2.5 2.5 0 0 0 5 0zm0 4.621V17.5a.5.5 0 0 1-.794.404L10 14.118l-5.206 3.786A.5.5 0 0 1 4 17.5v-13A2.5 2.5 0 0 1 6.5 2h3.258a4.5 4.5 0 0 0 5.682 6.561l.56.56z', |
||||||
|
|
||||||
|
'Clipboard': 'M7.085 3A1.5 1.5 0 0 1 8.5 2h3a1.5 1.5 0 0 1 1.415 1H14.5A1.5 1.5 0 0 1 16 4.5v12a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3h1.585zM8.5 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM7.085 4H5.5a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5v-12a.5.5 0 0 0-.5-.5h-1.585A1.5 1.5 0 0 1 11.5 5h-3a1.5 1.5 0 0 1-1.415-1z', |
||||||
|
'ClipboardSolid': 'M7.085 3A1.5 1.5 0 0 1 8.5 2h3a1.5 1.5 0 0 1 1.415 1H14.5A1.5 1.5 0 0 1 16 4.5v12a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3h1.585zM8.5 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3z', |
||||||
|
'ClipboardCheckmark': 'M7.085 3A1.5 1.5 0 0 1 8.5 2h3a1.5 1.5 0 0 1 1.415 1H14.5A1.5 1.5 0 0 1 16 4.5v4.707a5.48 5.48 0 0 0-1-.185V4.5a.5.5 0 0 0-.5-.5h-1.585A1.5 1.5 0 0 1 11.5 5h-3a1.5 1.5 0 0 1-1.415-1H5.5a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h4.1c.183.358.404.693.657 1H5.5A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3h1.585zM8.5 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.146-1.854a.5.5 0 0 0-.708 0L13.5 15.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708z', |
||||||
|
'ClipboardCheckmarkSolid': 'M7.085 3A1.5 1.5 0 0 1 8.5 2h3a1.5 1.5 0 0 1 1.415 1H14.5A1.5 1.5 0 0 1 16 4.5v4.707A5.5 5.5 0 0 0 10.257 18H5.5A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3h1.585zM8.5 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.146-1.854a.5.5 0 0 0-.708 0L13.5 15.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708z', |
||||||
|
'ClipboardError': 'M7.085 3A1.5 1.5 0 0 1 8.5 2h3a1.5 1.5 0 0 1 1.415 1H14.5A1.5 1.5 0 0 1 16 4.5v4.707a5.48 5.48 0 0 0-1-.185V4.5a.5.5 0 0 0-.5-.5h-1.585A1.5 1.5 0 0 1 11.5 5h-3a1.5 1.5 0 0 1-1.415-1H5.5a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h4.1c.183.358.404.693.657 1H5.5A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3h1.585zM8.5 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zM14.5 12a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-2a.5.5 0 0 0-.5-.5zm0 5.125a.625.625 0 1 0 0-1.25a.625.625 0 0 0 0 1.25z', |
||||||
|
'ClipboardErrorSolid': 'M7.085 3A1.5 1.5 0 0 1 8.5 2h3a1.5 1.5 0 0 1 1.415 1H14.5A1.5 1.5 0 0 1 16 4.5v4.707A5.5 5.5 0 0 0 10.257 18H5.5A1.5 1.5 0 0 1 4 16.5v-12A1.5 1.5 0 0 1 5.5 3h1.585zM8.5 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zM14.5 12a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-2a.5.5 0 0 0-.5-.5zm0 5.125a.625.625 0 1 0 0-1.25a.625.625 0 0 0 0 1.25z', |
||||||
|
'ClipboardText': 'M6.5 8a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7zM6 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm.5 2.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zm2-12a1.5 1.5 0 0 0-1.415 1H5.5A1.5 1.5 0 0 0 4 4.5v12A1.5 1.5 0 0 0 5.5 18h9a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 14.5 3h-1.585A1.5 1.5 0 0 0 11.5 2h-3zm3 1a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1h3zm-6 1h1.585A1.5 1.5 0 0 0 8.5 5h3a1.5 1.5 0 0 0 1.415-1H14.5a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-12a.5.5 0 0 1 .5-.5z', |
||||||
|
'ClipboardTextSolid': 'M8.5 2a1.5 1.5 0 0 0-1.415 1H5.5A1.5 1.5 0 0 0 4 4.5v12A1.5 1.5 0 0 0 5.5 18h9a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 14.5 3h-1.585A1.5 1.5 0 0 0 11.5 2h-3zm3 1a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1h3zm-5 5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1zm0 3h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zM6 14.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z', |
||||||
|
|
||||||
|
'Clock': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14zm-.5 2a.5.5 0 0 1 .492.41L10 5.5V10h2.5a.5.5 0 0 1 .09.992L12.5 11h-3a.5.5 0 0 1-.492-.41L9 10.5v-5a.5.5 0 0 1 .5-.5z', |
||||||
|
'ClockFill': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm-.5 3a.5.5 0 0 0-.5.5v5l.008.09A.5.5 0 0 0 9.5 11h3l.09-.008A.5.5 0 0 0 12.5 10H10V5.5l-.008-.09A.5.5 0 0 0 9.5 5z', |
||||||
|
'ClockAlarm': 'M10 6.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 .5.5h3a.5.5 0 1 0 0-1H10V6.5zM3.353 7.8A3.19 3.19 0 0 1 2 5.187C2 3.431 3.414 2 5.166 2c1.077 0 2.026.542 2.597 1.365A6.992 6.992 0 0 1 10 3c.78 0 1.529.127 2.23.362A3.164 3.164 0 0 1 14.83 2A3.172 3.172 0 0 1 18 5.175c0 1.08-.538 2.033-1.359 2.607c.233.697.359 1.443.359 2.218a6.973 6.973 0 0 1-1.71 4.584l1.564 1.562a.5.5 0 0 1-.708.707l-1.562-1.562A6.973 6.973 0 0 1 10 17a6.973 6.973 0 0 1-4.584-1.71l-1.562 1.564a.5.5 0 1 1-.708-.707l1.563-1.563A6.973 6.973 0 0 1 3 10c0-.769.124-1.508.353-2.2zM3 5.187c0 .662.291 1.255.75 1.656a7.03 7.03 0 0 1 3.062-3.077A2.152 2.152 0 0 0 5.166 3A2.176 2.176 0 0 0 3 5.187zm13.242 1.64c.464-.399.758-.99.758-1.652A2.172 2.172 0 0 0 14.83 3c-.66 0-1.251.295-1.65.763a7.03 7.03 0 0 1 3.06 3.065zM4 10a6 6 0 1 0 12 0a6 6 0 0 0-12 0z', |
||||||
|
'ClockAlarmSolid': 'M7.763 3.365A3.156 3.156 0 0 0 5.166 2C3.414 2 2 3.43 2 5.187A3.19 3.19 0 0 0 3.353 7.8A6.993 6.993 0 0 0 3 10c0 1.753.644 3.356 1.71 4.584l-1.564 1.563a.5.5 0 0 0 .708.707l1.562-1.563A6.973 6.973 0 0 0 10 17a6.973 6.973 0 0 0 4.584-1.71l1.562 1.563a.5.5 0 0 0 .708-.707l-1.563-1.562A6.973 6.973 0 0 0 17 10c0-.775-.126-1.521-.359-2.218A3.174 3.174 0 0 0 18 5.175A3.172 3.172 0 0 0 14.83 2c-1.078 0-2.03.54-2.602 1.362A6.992 6.992 0 0 0 10 3c-.782 0-1.534.128-2.237.365zM5.166 3c.657 0 1.248.296 1.646.766a7.03 7.03 0 0 0-3.061 3.077A2.19 2.19 0 0 1 3 5.187C3 3.975 3.973 3 5.166 3zm8.015.763c.399-.468.99-.763 1.65-.763C16.028 3 17 3.973 17 5.175c0 .661-.294 1.253-.758 1.653a7.03 7.03 0 0 0-3.06-3.065zM9.5 6a.5.5 0 0 1 .5.5V10h2.5a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5z', |
||||||
|
|
||||||
|
'Cloud': 'M10 4c2.817 0 4.415 1.923 4.647 4.246h.07c1.814 0 3.283 1.512 3.283 3.377C18 13.488 16.53 15 14.718 15H5.282C3.469 15 2 13.488 2 11.623C2 9.82 3.373 8.347 5.102 8.251l.251-.005C5.587 5.908 7.183 4 10 4zm0 1C7.886 5 6.551 6.316 6.348 8.345a1 1 0 0 1-.995.901h-.07C4.027 9.246 3 10.304 3 11.623C3 12.943 4.028 14 5.282 14h9.436C15.972 14 17 12.942 17 11.623c0-1.32-1.028-2.377-2.282-2.377h-.071a1 1 0 0 1-.995-.9C13.45 6.325 12.109 5 10 5z', |
||||||
|
'CloudSolid': 'M10 4c2.817 0 4.415 1.923 4.647 4.246h.07c1.814 0 3.283 1.512 3.283 3.377C18 13.488 16.53 15 14.718 15H5.282C3.469 15 2 13.488 2 11.623c0-1.865 1.47-3.377 3.282-3.377h.071C5.587 5.908 7.183 4 10 4z', |
||||||
|
'CloudCheckmark': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283a5.782 5.782 0 0 0-1.114-1.062c-.31-.933-1.163-1.598-2.157-1.598h-.071a1 1 0 0 1-.995-.9C13.45 4.325 12.109 3 10 3C7.886 3 6.551 4.316 6.348 6.345a1 1 0 0 1-.995.901h-.07C4.027 7.246 3 8.304 3 9.623C3 10.943 4.028 12 5.282 12h2.666a5.733 5.733 0 0 0-.177 1H5.282C3.469 13 2 11.488 2 9.623C2 7.82 3.373 6.347 5.102 6.251l.251-.005C5.587 3.908 7.183 2 10 2zm8 11.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.854-1.854L12.5 14.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708z', |
||||||
|
'CloudCheckmarkSolid': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283A5.75 5.75 0 0 0 7.772 13h-2.49C3.469 13 2 11.488 2 9.623c0-1.865 1.47-3.377 3.282-3.377h.071C5.587 3.908 7.183 2 10 2zm8 11.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.146-1.854a.5.5 0 0 0-.708 0L12.5 14.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708z', |
||||||
|
'CloudAdd': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283a5.782 5.782 0 0 0-1.114-1.062c-.31-.933-1.163-1.598-2.157-1.598h-.071a1 1 0 0 1-.995-.9C13.45 4.325 12.109 3 10 3C7.886 3 6.551 4.316 6.348 6.345a1 1 0 0 1-.995.901h-.07C4.027 7.246 3 8.304 3 9.623C3 10.943 4.028 12 5.282 12h2.666a5.733 5.733 0 0 0-.177 1H5.282C3.469 13 2 11.488 2 9.623C2 7.82 3.373 6.347 5.102 6.251l.251-.005C5.587 3.908 7.183 2 10 2zm3.5 16a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm0-7a.5.5 0 0 1 .5.5V13h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V14h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z', |
||||||
|
'CloudAddSolid': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283A5.75 5.75 0 0 0 7.772 13h-2.49C3.469 13 2 11.488 2 9.623c0-1.865 1.47-3.377 3.282-3.377h.071C5.587 3.908 7.183 2 10 2zm3.5 16a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9zm0-7a.5.5 0 0 1 .5.5V13h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V14h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z', |
||||||
|
'CloudDismiss': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283a5.782 5.782 0 0 0-1.114-1.062c-.31-.933-1.163-1.598-2.157-1.598h-.071a1 1 0 0 1-.995-.9C13.45 4.325 12.109 3 10 3C7.886 3 6.551 4.316 6.348 6.345a1 1 0 0 1-.995.901h-.07C4.027 7.246 3 8.304 3 9.623C3 10.943 4.028 12 5.282 12h2.666a5.733 5.733 0 0 0-.177 1H5.282C3.469 13 2 11.488 2 9.623C2 7.82 3.373 6.347 5.102 6.251l.251-.005C5.587 3.908 7.183 2 10 2zm8 11.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-3.793 0l1.147-1.146a.5.5 0 0 0-.708-.708L13.5 12.793l-1.146-1.147a.5.5 0 0 0-.708.708l1.147 1.146l-1.147 1.146a.5.5 0 0 0 .708.708l1.146-1.147l1.146 1.147a.5.5 0 0 0 .708-.708L14.207 13.5z', |
||||||
|
'CloudDismissSolid': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283A5.75 5.75 0 0 0 7.772 13h-2.49C3.469 13 2 11.488 2 9.623c0-1.865 1.47-3.377 3.282-3.377h.071C5.587 3.908 7.183 2 10 2zm8 11.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0zm-2.646-1.146a.5.5 0 0 0-.708-.708L13.5 12.793l-1.146-1.147a.5.5 0 0 0-.708.708l1.147 1.146l-1.147 1.146a.5.5 0 0 0 .708.708l1.146-1.147l1.146 1.147a.5.5 0 0 0 .708-.708L14.207 13.5l1.147-1.146z', |
||||||
|
'CloudEdit': 'M14.647 8.246C14.415 5.923 12.817 4 10 4S5.587 5.908 5.353 8.246l-.251.005C3.373 8.347 2 9.821 2 11.623C2 13.488 3.47 15 5.282 15h3.193c.11-.361.283-.7.51-1H5.282C4.028 14 3 12.942 3 11.623c0-1.32 1.028-2.377 2.282-2.377h.071a1 1 0 0 0 .995-.9C6.551 6.315 7.886 5 10 5c2.108 0 3.45 1.325 3.652 3.346c.025.25.14.471.313.632l.137-.137c.252-.253.54-.448.847-.587a3.242 3.242 0 0 0-.231-.008h-.071zm.162 1.302l-4.83 4.83a2.197 2.197 0 0 0-.577 1.02l-.375 1.498a.89.89 0 0 0 1.079 1.078l1.498-.374c.386-.097.739-.296 1.02-.578l4.83-4.83a1.87 1.87 0 0 0-2.645-2.644z', |
||||||
|
'CloudEditSolid': 'M14.647 8.246C14.415 5.923 12.817 4 10 4S5.587 5.908 5.353 8.246h-.07C3.468 8.246 2 9.758 2 11.623C2 13.488 3.47 15 5.282 15h3.193c.152-.501.426-.958.798-1.33l4.829-4.83c.252-.252.54-.447.847-.586a3.242 3.242 0 0 0-.231-.008h-.071zm.162 1.302l-4.83 4.83a2.197 2.197 0 0 0-.577 1.02l-.375 1.498a.89.89 0 0 0 1.079 1.078l1.498-.374c.386-.097.739-.296 1.02-.578l4.83-4.83a1.87 1.87 0 0 0-2.645-2.644z', |
||||||
|
'CloudOff': 'M2.854 2.146a.5.5 0 1 0-.708.708l3.67 3.668a5.326 5.326 0 0 0-.463 1.724l-.251.005C3.373 8.347 2 9.821 2 11.623C2 13.488 3.47 15 5.282 15h9.01l2.854 2.854a.5.5 0 0 0 .708-.708l-15-15zM13.293 14h-8.01c-1.255 0-2.283-1.058-2.283-2.377c0-1.32 1.028-2.377 2.282-2.377h.071a1 1 0 0 0 .995-.9c.038-.38.116-.735.23-1.06L13.293 14zM17 11.623c0 .898-.477 1.675-1.176 2.08l.724.724A3.4 3.4 0 0 0 18 11.623c0-1.865-1.47-3.377-3.282-3.377h-.071C14.415 5.923 12.817 4 10 4c-1.209 0-2.193.352-2.941.938l.715.715C8.36 5.233 9.112 5 10 5c2.108 0 3.45 1.325 3.652 3.346a1 1 0 0 0 .995.9h.071c1.254 0 2.282 1.058 2.282 2.377z', |
||||||
|
'CloudOffSolid': 'M2.854 2.146a.5.5 0 1 0-.708.708l3.67 3.668a5.326 5.326 0 0 0-.463 1.724h-.07C3.468 8.246 2 9.758 2 11.623C2 13.488 3.47 15 5.282 15h9.01l2.854 2.854a.5.5 0 0 0 .708-.708l-15-15zM18 11.623a3.4 3.4 0 0 1-1.452 2.804l-9.49-9.49C7.808 4.353 8.792 4 10 4c2.817 0 4.415 1.923 4.647 4.246h.07c1.814 0 3.283 1.512 3.283 3.377z', |
||||||
|
'CloudSync': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283a5.782 5.782 0 0 0-1.114-1.062c-.31-.933-1.163-1.598-2.157-1.598h-.071a1 1 0 0 1-.995-.9C13.45 4.325 12.109 3 10 3C7.886 3 6.551 4.316 6.348 6.345a1 1 0 0 1-.995.901h-.07C4.027 7.246 3 8.304 3 9.623C3 10.943 4.028 12 5.282 12h2.666a5.733 5.733 0 0 0-.177 1H5.282C3.469 13 2 11.488 2 9.623C2 7.82 3.373 6.347 5.102 6.251l.251-.005C5.587 3.908 7.183 2 10 2zM9 13.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm6.5-3a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-.5.5H14a.5.5 0 0 1 0-1h.468a1.999 1.999 0 0 0-2.383.336a.5.5 0 0 1-.706-.707A3.001 3.001 0 0 1 15 11.152V11a.5.5 0 0 1 .5-.5zm-.876 5.532A2.999 2.999 0 0 1 12 15.848V16a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5H13a.5.5 0 0 1 0 1h-.468a1.999 1.999 0 0 0 2.383-.336a.5.5 0 0 1 .706.707c-.285.285-.624.51-.997.66z', |
||||||
|
'CloudSyncSolid': 'M10 2c2.817 0 4.415 1.923 4.647 4.246h.07C16.532 6.246 18 7.758 18 9.623c0 .095-.004.19-.011.283A5.75 5.75 0 0 0 7.772 13h-2.49C3.469 13 2 11.488 2 9.623c0-1.865 1.47-3.377 3.282-3.377h.071C5.587 3.908 7.183 2 10 2zM9 13.5a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0zm6.5-3a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-.5.5H14a.5.5 0 0 1 0-1h.468a1.999 1.999 0 0 0-2.383.336a.5.5 0 0 1-.706-.707A3.001 3.001 0 0 1 15 11.152V11a.5.5 0 0 1 .5-.5zm-.876 5.532A2.999 2.999 0 0 1 12 15.848V16a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5H13a.5.5 0 0 1 0 1h-.468a1.999 1.999 0 0 0 2.383-.336a.5.5 0 0 1 .706.707c-.285.285-.624.51-.997.66z', |
||||||
|
|
||||||
|
'Copy': 'M8 2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8zM7 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V4zM4 6a2 2 0 0 1 1-1.732V14.5A2.5 2.5 0 0 0 7.5 17h6.232A2 2 0 0 1 12 18H7.5A3.5 3.5 0 0 1 4 14.5V6z', |
||||||
|
'CopySolid': 'M6 4a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V4zM4 6a2 2 0 0 1 1-1.732V14.5A2.5 2.5 0 0 0 7.5 17h6.232A2 2 0 0 1 12 18H7.5A3.5 3.5 0 0 1 4 14.5V6z', |
||||||
|
|
||||||
|
'Rename': 'M8.5 2a.5.5 0 0 0 0 1h1v14h-1a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-1V3h1a.5.5 0 0 0 0-1h-3zm-4 2h4v1h-4A1.5 1.5 0 0 0 3 6.5v7A1.5 1.5 0 0 0 4.5 15h4v1h-4A2.5 2.5 0 0 1 2 13.5v-7A2.5 2.5 0 0 1 4.5 4zm11 11h-4v1h4a2.5 2.5 0 0 0 2.5-2.5v-7A2.5 2.5 0 0 0 15.5 4h-4v1h4A1.5 1.5 0 0 1 17 6.5v7a1.5 1.5 0 0 1-1.5 1.5z', |
||||||
|
'RenameSolid': 'M8.5 2a.5.5 0 0 0 0 1h1v14h-1a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-1V3h1a.5.5 0 0 0 0-1h-3zm-4 2h4v12h-4A2.5 2.5 0 0 1 2 13.5v-7A2.5 2.5 0 0 1 4.5 4zm11 12h-4V4h4A2.5 2.5 0 0 1 18 6.5v7a2.5 2.5 0 0 1-2.5 2.5z', |
||||||
|
|
||||||
|
'Desktop': 'M4 2a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3v2H5.5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1H13v-2h3a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm8 13v2H8v-2h4zM3 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z', |
||||||
|
'DesktopSolid': 'M3.5 2A1.5 1.5 0 0 0 2 3.5v10A1.5 1.5 0 0 0 3.5 15H7v2H5.5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1H13v-2h3.5a1.5 1.5 0 0 0 1.5-1.5v-10A1.5 1.5 0 0 0 16.5 2h-13zM12 15v2H8v-2h4z', |
||||||
|
|
||||||
|
'Dismiss': 'M4.089 4.216l.057-.07a.5.5 0 0 1 .638-.057l.07.057L10 9.293l5.146-5.147a.5.5 0 0 1 .638-.057l.07.057a.5.5 0 0 1 .057.638l-.057.07L10.707 10l5.147 5.146a.5.5 0 0 1 .057.638l-.057.07a.5.5 0 0 1-.638.057l-.07-.057L10 10.707l-5.146 5.147a.5.5 0 0 1-.638.057l-.07-.057a.5.5 0 0 1-.057-.638l.057-.07L9.293 10L4.146 4.854a.5.5 0 0 1-.057-.638l.057-.07l-.057.07z', |
||||||
|
'DismissSolid': 'M3.897 4.054l.073-.084a.75.75 0 0 1 .976-.073l.084.073L10 8.939l4.97-4.97a.75.75 0 0 1 .976-.072l.084.073a.75.75 0 0 1 .073.976l-.073.084L11.061 10l4.97 4.97a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L10 11.061l-4.97 4.97a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L8.939 10l-4.97-4.97a.75.75 0 0 1-.072-.976l.073-.084l-.073.084z', |
||||||
|
'DismissCircle': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14zM7.81 7.114l.069.058L10 9.292l2.121-2.12a.5.5 0 0 1 .638-.058l.07.058a.5.5 0 0 1 .057.637l-.058.07L10.708 10l2.12 2.121a.5.5 0 0 1 .058.638l-.058.07a.5.5 0 0 1-.637.057l-.07-.058L10 10.708l-2.121 2.12a.5.5 0 0 1-.638.058l-.07-.058a.5.5 0 0 1-.057-.637l.058-.07L9.292 10l-2.12-2.121a.5.5 0 0 1-.058-.638l.058-.07a.5.5 0 0 1 .637-.057z', |
||||||
|
'DismissCircleSolid': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zM7.81 7.114a.5.5 0 0 0-.638.058l-.058.069a.5.5 0 0 0 .058.638L9.292 10l-2.12 2.121l-.058.07a.5.5 0 0 0 .058.637l.069.058a.5.5 0 0 0 .638-.058L10 10.708l2.121 2.12l.07.058a.5.5 0 0 0 .637-.058l.058-.069a.5.5 0 0 0-.058-.638L10.708 10l2.12-2.121l.058-.07a.5.5 0 0 0-.058-.637l-.069-.058a.5.5 0 0 0-.638.058L10 9.292l-2.121-2.12l-.07-.058z', |
||||||
|
'DismissSquare': 'M7.146 7.146a.5.5 0 0 1 .708 0L10 9.293l2.146-2.147a.5.5 0 0 1 .708.708L10.707 10l2.147 2.146a.5.5 0 0 1-.708.708L10 10.707l-2.146 2.147a.5.5 0 0 1-.708-.708L9.293 10L7.146 7.854a.5.5 0 0 1 0-.708zM3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm3-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z', |
||||||
|
'DismissSquareSolid': 'M3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm4.146 1.146a.5.5 0 0 0 0 .708L9.293 10l-2.147 2.146a.5.5 0 0 0 .708.708L10 10.707l2.146 2.147a.5.5 0 0 0 .708-.708L10.707 10l2.147-2.146a.5.5 0 0 0-.708-.708L10 9.293L7.854 7.146a.5.5 0 0 0-.708 0z', |
||||||
|
|
||||||
|
'Edit': 'M13.245 2.817a2.783 2.783 0 0 1 4.066 3.796l-.13.14l-9.606 9.606a2.001 2.001 0 0 1-.723.462l-.165.053l-4.055 1.106a.5.5 0 0 1-.63-.535l.016-.08l1.106-4.054c.076-.28.212-.54.398-.76l.117-.128l9.606-9.606zm-.86 2.275L4.346 13.13a1 1 0 0 0-.215.321l-.042.123l-.877 3.21l3.212-.875a1 1 0 0 0 .239-.1l.107-.072l.098-.085l8.038-8.04l-2.521-2.52zm4.089-1.568a1.783 1.783 0 0 0-2.402-.11l-.12.11l-.86.86l2.52 2.522l.862-.86a1.783 1.783 0 0 0 .11-2.402l-.11-.12z', |
||||||
|
'EditSolid': 'M11.677 4.384l3.936 3.936l-8.038 8.039a2.001 2.001 0 0 1-.723.462l-.165.053l-4.055 1.106a.5.5 0 0 1-.63-.535l.016-.08l1.106-4.054c.076-.28.212-.54.398-.76l.117-.128l8.038-8.04zm1.568-1.567a2.783 2.783 0 0 1 4.066 3.796l-.13.14l-.861.86l-3.936-3.936l.861-.86z', |
||||||
|
'EditOff': 'M2.854 2.146a.5.5 0 1 0-.707.708l5.53 5.53l-4.038 4.04l-.117.127a2 2 0 0 0-.398.76l-1.106 4.055l-.015.08a.5.5 0 0 0 .63.534l4.054-1.106l.165-.053a2 2 0 0 0 .723-.462l4.038-4.039l5.534 5.534a.5.5 0 0 0 .707-.708l-15-15zm8.052 9.467l-4.038 4.039l-.098.086l-.107.072a1 1 0 0 1-.24.1l-3.21.875l.876-3.21l.042-.124a1 1 0 0 1 .215-.32l4.039-4.039l2.521 2.521zm4-4L12.32 10.2l.708.707l4.153-4.153l.13-.14a2.783 2.783 0 0 0-4.066-3.796L9.092 6.971l.707.707l2.586-2.586l2.52 2.521zm1.568-4.089l.11.12c.584.7.547 1.744-.11 2.402l-.861.86l-2.521-2.52l.86-.862l.12-.11a1.783 1.783 0 0 1 2.402.11z', |
||||||
|
'EditOffSolid': 'M2.854 2.146a.5.5 0 1 0-.707.708l5.53 5.53l-4.038 4.04l-.117.127a2 2 0 0 0-.398.76l-1.106 4.055l-.015.08a.5.5 0 0 0 .63.534l4.054-1.106l.165-.053a2 2 0 0 0 .723-.462l4.038-4.039l5.534 5.534a.5.5 0 0 0 .707-.708l-15-15zM15.613 8.32l-2.586 2.586L9.092 6.97l2.585-2.586l3.936 3.936zm-2.368-5.503a2.783 2.783 0 0 1 4.066 3.797l-.13.14l-.861.86l-3.936-3.937l.861-.86z', |
||||||
|
|
||||||
|
'ErrorCircle': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14zm0 9.5a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5zM10 6a.5.5 0 0 1 .492.41l.008.09V11a.5.5 0 0 1-.992.09L9.5 11V6.5A.5.5 0 0 1 10 6z', |
||||||
|
'ErrorCircleSolid': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 10.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5zM10 6a.5.5 0 0 0-.492.41L9.5 6.5V11l.008.09a.5.5 0 0 0 .984 0L10.5 11V6.5l-.008-.09A.5.5 0 0 0 10 6z', |
||||||
|
|
||||||
|
'Info': 'M10.492 8.91A.5.5 0 0 0 9.5 9v4.502l.008.09a.5.5 0 0 0 .992-.09V9l-.008-.09zm.307-2.16a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0zM18 10a8 8 0 1 0-16 0a8 8 0 0 0 16 0zM3 10a7 7 0 1 1 14 0a7 7 0 0 1-14 0z', |
||||||
|
'InfoSolid': 'M18 10a8 8 0 1 0-16 0a8 8 0 0 0 16 0zM9.508 8.91a.5.5 0 0 1 .984 0L10.5 9v4.502l-.008.09a.5.5 0 0 1-.984 0l-.008-.09V9l.008-.09zM9.25 6.75a.75.75 0 1 1 1.5 0a.75.75 0 0 1-1.5 0z', |
||||||
|
|
||||||
|
'Link': 'M8 6a.5.5 0 0 1 .09.992L8 7H6a3 3 0 0 0-.197 5.994L6 13h2a.5.5 0 0 1 .09.992L8 14H6a4 4 0 0 1-.22-7.994L6 6h2zm6 0a4 4 0 0 1 .22 7.994L14 14h-2a.5.5 0 0 1-.09-.992L12 13h2a3 3 0 0 0 .197-5.994L14 7h-2a.5.5 0 0 1-.09-.992L12 6h2zM6 9.5h8a.5.5 0 0 1 .09.992L14 10.5H6a.5.5 0 0 1-.09-.992L6 9.5h8h-8z', |
||||||
|
'LinkSolid': 'M14 6a4 4 0 0 1 .2 7.995L14 14h-2a.75.75 0 0 1-.102-1.493L12 12.5h2a2.5 2.5 0 0 0 .164-4.995L14 7.5h-2a.75.75 0 0 1-.102-1.493L12 6h2zM8 6a.75.75 0 0 1 .102 1.493L8 7.5H6a2.5 2.5 0 0 0-.164 4.995L6 12.5h2a.75.75 0 0 1 .102 1.493L8 14H6a4 4 0 0 1-.2-7.995L6 6h2zM6.25 9.25h7.5a.75.75 0 0 1 .102 1.493l-.102.007h-7.5a.75.75 0 0 1-.102-1.493l.102-.007h7.5h-7.5z', |
||||||
|
|
||||||
|
'MailInbox': 'M6 3a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3H6zm10 7h-3.5a.5.5 0 0 0-.5.5v.011l-.004.06a2.57 2.57 0 0 1-.256.955a1.694 1.694 0 0 1-.572.667c-.26.174-.63.307-1.168.307c-.538 0-.907-.133-1.168-.307a1.694 1.694 0 0 1-.572-.667A2.572 2.572 0 0 1 8 10.511V10.5A.5.5 0 0 0 7.5 10H4V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v4zM4 11h3.05c.047.264.137.616.315.974c.186.371.473.758.912 1.051c.443.295 1.01.475 1.723.475c.713 0 1.28-.18 1.723-.475c.44-.293.726-.68.912-1.051c.178-.358.268-.71.315-.974H16v3a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-3z', |
||||||
|
'MailInboxSolid': 'M3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm1 4h3.5a.5.5 0 0 1 .5.5v.011l.004.06a2.572 2.572 0 0 0 .256.955c.126.254.308.492.572.667c.26.174.63.307 1.168.307c.537 0 .907-.133 1.168-.307c.264-.175.446-.413.572-.667a2.57 2.57 0 0 0 .26-1.015V10.498a.5.5 0 0 1 .5-.498H16V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4zm4 .5v-.002z', |
||||||
|
|
||||||
|
'Navigation': 'M2 4.5a.5.5 0 0 1 .5-.5h15a.5.5 0 0 1 0 1h-15a.5.5 0 0 1-.5-.5zm0 5a.5.5 0 0 1 .5-.5h15a.5.5 0 0 1 0 1h-15a.5.5 0 0 1-.5-.5zm.5 4.5a.5.5 0 0 0 0 1h15a.5.5 0 0 0 0-1h-15z', |
||||||
|
'NavigationSolid': 'M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75zm0 5A.75.75 0 0 1 2.75 9h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 9.75zM2.75 14a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5H2.75z', |
||||||
|
|
||||||
|
'Open': 'M6 4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2.5a.5.5 0 0 1 1 0V14a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h2.5a.5.5 0 0 1 0 1H6zm5-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V4.707l-4.146 4.147a.5.5 0 0 1-.708-.708L15.293 4H11.5a.5.5 0 0 1-.5-.5z', |
||||||
|
'OpenSolid': 'M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v7.5c0 .966.784 1.75 1.75 1.75h7.5a1.75 1.75 0 0 0 1.75-1.75v-2a.75.75 0 0 1 1.5 0v2A3.25 3.25 0 0 1 13.75 17h-7.5A3.25 3.25 0 0 1 3 13.75v-7.5A3.25 3.25 0 0 1 6.25 3h2a.75.75 0 0 1 0 1.5h-2zm4.25-.75a.75.75 0 0 1 .75-.75h5a.75.75 0 0 1 .75.75v5a.75.75 0 0 1-1.5 0V5.56l-3.72 3.72a.75.75 0 1 1-1.06-1.06l3.72-3.72h-3.19a.75.75 0 0 1-.75-.75z', |
||||||
|
|
||||||
|
'Question': 'M10 3C7.794 3 6 4.794 6 7a.5.5 0 0 0 1 0c0-1.654 1.346-3 3-3s3 1.346 3 3c0 1.249-.692 1.863-1.575 2.62l-.032.027C10.534 10.384 9.5 11.27 9.5 13v.5a.5.5 0 0 0 1 0V13c0-1.249.692-1.863 1.575-2.62l.032-.027C12.966 9.615 14 8.731 14 7c0-2.206-1.794-4-4-4zm0 14a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5z', |
||||||
|
'QuestionSolid': 'M10 3C7.796 3 6 4.796 6 7a.75.75 0 0 0 1.5 0c0-1.376 1.124-2.5 2.5-2.5s2.5 1.124 2.5 2.5c0 .597-.156.975-.368 1.27c-.232.325-.547.58-.969.92l-.01.008c-.4.323-.893.724-1.27 1.288c-.391.588-.633 1.313-.633 2.264v.5a.75.75 0 0 0 1.5 0v-.5c0-.674.164-1.105.382-1.432c.233-.349.552-.62.964-.953l.068-.055c.374-.302.834-.672 1.188-1.167C13.75 8.588 14 7.903 14 7c0-2.204-1.796-4-4-4zm0 14a1 1 0 1 0 0-2a1 1 0 0 0 0 2z', |
||||||
|
'QuestionCircle': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14zm0 10.5a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5zm0-8a2.5 2.5 0 0 1 1.651 4.377l-.154.125l-.219.163l-.087.072a1.968 1.968 0 0 0-.156.149c-.339.36-.535.856-.535 1.614a.5.5 0 0 1-1 0c0-1.012.293-1.752.805-2.298a3.11 3.11 0 0 1 .356-.323l.247-.185l.118-.1A1.5 1.5 0 1 0 8.5 8a.5.5 0 0 1-1 .001A2.5 2.5 0 0 1 10 5.5z', |
||||||
|
'QuestionCircleSolid': 'M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 11.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5zm0-8A2.5 2.5 0 0 0 7.5 8a.5.5 0 0 0 1 0a1.5 1.5 0 1 1 2.632.984l-.106.11l-.118.1l-.247.185a3.11 3.11 0 0 0-.356.323C9.793 10.248 9.5 10.988 9.5 12a.5.5 0 0 0 1 0c0-.758.196-1.254.535-1.614l.075-.076l.08-.073l.088-.072l.219-.163l.154-.125A2.5 2.5 0 0 0 10 5.5z', |
||||||
|
|
||||||
|
'Warning': 'M10 7a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0v-4A.5.5 0 0 1 10 7zm0 7.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5zM8.686 2.852a1.5 1.5 0 0 1 2.628 0l6.56 11.925A1.5 1.5 0 0 1 16.558 17H3.44a1.5 1.5 0 0 1-1.314-2.223L8.686 2.852zm1.752.482a.5.5 0 0 0-.876 0L3.003 15.26a.5.5 0 0 0 .438.741H16.56a.5.5 0 0 0 .438-.74L10.438 3.333z', |
||||||
|
'WarningSolid': 'M8.686 2.852L2.127 14.777A1.5 1.5 0 0 0 3.441 17H16.56a1.5 1.5 0 0 0 1.314-2.223L11.314 2.852a1.5 1.5 0 0 0-2.628 0zM10 6.75a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75zm.75 7a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0z', |
||||||
|
|
||||||
|
'Moon': 'M15.493 13.497a6.981 6.981 0 0 1-11.483.892c2.831-1.087 4.558-2.42 5.593-4.397c1.048-2 1.337-4.16.76-6.909a6.981 6.981 0 0 1 5.13 10.414zM5.457 16.918A7.981 7.981 0 1 0 9.88 2.035a.599.599 0 0 0-.614.74c.688 2.819.434 4.876-.55 6.753c-.934 1.784-2.544 3.031-5.55 4.107a.599.599 0 0 0-.292.903a7.952 7.952 0 0 0 2.582 2.38z', |
||||||
|
'MoonSolid': 'M16.36 13.997a7.981 7.981 0 0 1-13.485.541a.599.599 0 0 1 .292-.903c3.006-1.076 4.616-2.323 5.55-4.107c.984-1.877 1.238-3.934.55-6.753a.599.599 0 0 1 .614-.74a7.981 7.981 0 0 1 6.478 11.962z', |
||||||
|
'Sun': 'M10 2a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 10 2zm0 12a4 4 0 1 0 0-8a4 4 0 0 0 0 8zm0-1a3 3 0 1 1 0-6a3 3 0 0 1 0 6zm7.5-2.5a.5.5 0 0 0 0-1h-1a.5.5 0 0 0 0 1h1zM10 16a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5zm-6.5-5.5a.5.5 0 0 0 0-1H2.463a.5.5 0 0 0 0 1H3.5zm.646-6.354a.5.5 0 0 1 .708 0l1 1a.5.5 0 1 1-.708.708l-1-1a.5.5 0 0 1 0-.708zm.708 11.708a.5.5 0 0 1-.708-.708l1-1a.5.5 0 0 1 .708.708l-1 1zm11-11.708a.5.5 0 0 0-.708 0l-1 1a.5.5 0 0 0 .708.708l1-1a.5.5 0 0 0 0-.708zm-.708 11.708a.5.5 0 0 0 .708-.708l-1-1a.5.5 0 0 0-.708.708l1 1z', |
||||||
|
'SunSolid': 'M10 2a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 10 2zm4 8a4 4 0 1 1-8 0a4 4 0 0 1 8 0zm3.5.5a.5.5 0 0 0 0-1h-1a.5.5 0 0 0 0 1h1zM10 16a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1a.5.5 0 0 1 .5-.5zm-6.5-5.5a.5.5 0 0 0 0-1H2.463a.5.5 0 0 0 0 1H3.5zm.646-6.354a.5.5 0 0 1 .708 0l1 1a.5.5 0 1 1-.708.708l-1-1a.5.5 0 0 1 0-.708zm.708 11.708a.5.5 0 0 1-.708-.708l1-1a.5.5 0 0 1 .708.708l-1 1zm11-11.708a.5.5 0 0 0-.708 0l-1 1a.5.5 0 0 0 .708.708l1-1a.5.5 0 0 0 0-.708zm-.708 11.708a.5.5 0 0 0 .708-.708l-1-1a.5.5 0 0 0-.708.708l1 1z', |
||||||
|
|
||||||
|
'TaskList': 'M5.854 4.354a.5.5 0 1 0-.708-.708L3.5 5.293l-.646-.647a.5.5 0 1 0-.708.708l1 1a.5.5 0 0 0 .708 0l2-2zM8.5 5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1h-9zm0 5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1h-9zM8 15.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM5.854 9.854a.5.5 0 1 0-.708-.708L3.5 10.793l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l2-2zm0 4.292a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708 0l-1-1a.5.5 0 0 1 .708-.708l.646.647l1.646-1.647a.5.5 0 0 1 .708 0z', |
||||||
|
'TaskListSolid': 'M5.854 4.354a.5.5 0 1 0-.708-.708L3.5 5.293l-.646-.647a.5.5 0 1 0-.708.708l1 1a.5.5 0 0 0 .708 0l2-2zM8.75 4.5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5zm0 5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5zM8 15.25a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1-.75-.75zM5.854 9.854a.5.5 0 1 0-.708-.708L3.5 10.793l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l2-2zm0 4.292a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708 0l-1-1a.5.5 0 0 1 .708-.708l.646.647l1.646-1.647a.5.5 0 0 1 .708 0z', |
||||||
|
|
||||||
|
'FileBriefcase': 'M6 2a2 2 0 0 0-2 2v5h1V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-2v1h2a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7zM4 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V12h1a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h1v-1.5zm3 .5H5v1h2v-1zm-4 2a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1H3z', |
||||||
|
'FileBriefcaseSolid': 'M10 2v4.5A1.5 1.5 0 0 0 11.5 8H16v8.5a1.5 1.5 0 0 1-1.5 1.5H12v-4.5A2.5 2.5 0 0 0 9.5 11H9v-1a1 1 0 0 0-1-1H4V3.5A1.5 1.5 0 0 1 5.5 2H10zm1 .25V6.5a.5.5 0 0 0 .5.5h4.25L11 2.25zM4 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V12h1a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h1v-1.5zm3 .5H5v1h2v-1zm-4 2a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1H3z', |
||||||
|
|
||||||
|
'Sync': 'M11.414 3.635a.5.5 0 0 0 0-.707L9.293.807a.5.5 0 0 0-.707.707l.997.997a7.5 7.5 0 0 0-4.075 13.495a.5.5 0 0 0 .6-.8A6.5 6.5 0 0 1 10.066 3.5c.024 0 .05-.002.073-.005L8.586 5.049a.5.5 0 0 0 .707.707l2.121-2.121zM8.586 16.363a.5.5 0 0 0 0 .707l2.121 2.121a.5.5 0 0 0 .707-.707l-.997-.997a7.5 7.5 0 0 0 4.075-13.495a.5.5 0 1 0-.6.8a6.5 6.5 0 0 1-3.959 11.706a.502.502 0 0 0-.073.005l1.554-1.554a.5.5 0 1 0-.707-.707l-2.121 2.121z', |
||||||
|
'SyncSolid': 'M9.885 3.75a6.25 6.25 0 0 0-3.628 11.256a.75.75 0 0 1-.9 1.2a7.75 7.75 0 0 1 3.99-13.93l-.584-.586A.75.75 0 0 1 9.823.63l2.122 2.121a.75.75 0 0 1 0 1.06L9.823 5.934a.75.75 0 0 1-1.06-1.06L9.885 3.75zm.23 12.498a6.25 6.25 0 0 0 3.628-11.256a.75.75 0 0 1 .9-1.2a7.75 7.75 0 0 1-3.99 13.93l.584.585a.75.75 0 1 1-1.06 1.061l-2.122-2.121a.75.75 0 0 1 0-1.06l2.122-2.122a.75.75 0 1 1 1.06 1.06l-1.122 1.123z', |
||||||
|
'SyncOff': 'M11.414 3.635a.5.5 0 0 0 0-.707L9.293.807a.5.5 0 0 0-.707.707l.997.997a7.48 7.48 0 0 0-3.72 1.23l.724.724a6.49 6.49 0 0 1 3.48-.966c.024 0 .05-.001.073-.004L8.586 5.049a.5.5 0 0 0 .707.707l2.121-2.121zm-7.06 1.426a7.5 7.5 0 0 0 1.154 10.945a.5.5 0 0 0 .6-.8A6.5 6.5 0 0 1 5.063 5.77l9.165 9.165A6.479 6.479 0 0 1 9.934 16.5a.502.502 0 0 0-.074.004l1.554-1.554a.5.5 0 1 0-.707-.707l-2.121 2.121a.5.5 0 0 0 0 .707l2.121 2.121a.5.5 0 0 0 .707-.707l-.997-.997a7.471 7.471 0 0 0 4.521-1.843l2.208 2.209a.5.5 0 0 0 .708-.708l-15-15a.5.5 0 1 0-.708.708L4.355 5.06zm10.95-.365a7.503 7.503 0 0 1 .954 9.44l-.724-.724a6.503 6.503 0 0 0-1.641-8.62a.5.5 0 1 1 .6-.8c.282.212.553.447.81.704z', |
||||||
|
'SyncOffSolid': 'M9.885 3.75a6.236 6.236 0 0 0-3.116.897L5.683 3.56a7.725 7.725 0 0 1 3.665-1.285l-.585-.586A.75.75 0 0 1 9.823.63l2.122 2.121a.75.75 0 0 1 0 1.06L9.823 5.934a.75.75 0 0 1-1.06-1.06L9.885 3.75zM4.178 4.884a7.75 7.75 0 0 0 1.18 11.322a.75.75 0 1 0 .9-1.2a6.25 6.25 0 0 1-1.016-9.059l8.81 8.811a6.225 6.225 0 0 1-3.937 1.49l1.122-1.123a.75.75 0 0 0-1.06-1.06l-2.122 2.121a.75.75 0 0 0 0 1.06l2.122 2.122a.75.75 0 1 0 1.06-1.06l-.585-.586a7.718 7.718 0 0 0 4.463-1.9l2.031 2.03a.5.5 0 0 0 .708-.707l-15-15a.5.5 0 1 0-.708.708l2.032 2.03zm11.174 8.346l1.086 1.086a7.753 7.753 0 0 0-1.796-10.524a.75.75 0 0 0-.9 1.2a6.253 6.253 0 0 1 1.61 8.237z', |
||||||
|
'SyncCircle': 'M10 3a7 7 0 1 1 0 14a7 7 0 0 1 0-14zm8 7a8 8 0 1 0-16 0a8 8 0 0 0 16 0zm-8-2.5A2.5 2.5 0 0 1 12.292 9H11.5a.5.5 0 1 0 0 1h2a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 0-1 0v.696a3.498 3.498 0 0 0-5.609-.53a.5.5 0 0 0 .746.667A2.493 2.493 0 0 1 10 7.5zm-3 4.304v.696a.5.5 0 0 1-1 0v-2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-.792a2.5 2.5 0 0 0 4.156.666a.5.5 0 0 1 .745.668A3.498 3.498 0 0 1 7 11.804z', |
||||||
|
'SyncCircleSolid': 'M10 18a8 8 0 1 1 0-16a8 8 0 0 1 0 16zm3.5-8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 0-1 0v.696a3.498 3.498 0 0 0-5.609-.53a.5.5 0 1 0 .746.667A2.5 2.5 0 0 1 12.293 9H11.5a.5.5 0 1 0 0 1h2zm-7.5.5v2a.5.5 0 0 0 1 0v-.696a3.498 3.498 0 0 0 5.609.53a.5.5 0 0 0-.745-.668A2.5 2.5 0 0 1 7.708 11H8.5a.5.5 0 0 0 0-1h-2a.5.5 0 0 0-.5.5z', |
||||||
|
} |
||||||
|
|
||||||
|
export type IconName = |
||||||
|
| 'Folder' |
||||||
|
// | 'FolderSolid'
|
||||||
|
| 'FolderAdd' |
||||||
|
// | 'FolderAddSolid'
|
||||||
|
// | 'FolderArrowRight'
|
||||||
|
// | 'FolderArrowRightSolid'
|
||||||
|
// | 'FolderArrowUp'
|
||||||
|
// | 'FolderArrowUpSolid'
|
||||||
|
| 'FolderOpen' |
||||||
|
// | 'FolderOpenSolid'
|
||||||
|
// | 'FolderOpenVertical'
|
||||||
|
// | 'FolderOpenVerticalSolid'
|
||||||
|
// | 'FolderProhibited'
|
||||||
|
// | 'FolderProhibitedSolid'
|
||||||
|
// | 'FolderSwap'
|
||||||
|
// | 'FolderSwapSolid'
|
||||||
|
// | 'FolderSync'
|
||||||
|
// | 'FolderSyncSolid'
|
||||||
|
// | 'FolderZip'
|
||||||
|
// | 'FolderZipSolid'
|
||||||
|
// | 'Checkmark'
|
||||||
|
// | 'CheckmarkSolid'
|
||||||
|
// | 'CheckmarkCircle'
|
||||||
|
| 'CheckmarkCircleSolid' |
||||||
|
// | 'CheckmarkStarburst'
|
||||||
|
// | 'CheckmarkStarburstSolid'
|
||||||
|
// | 'ArrowSyncCheckmark'
|
||||||
|
// | 'ArrowSyncCheckmarkSolid'
|
||||||
|
| 'Image' |
||||||
|
// | 'ImageSolid'
|
||||||
|
// | 'DrawImage'
|
||||||
|
// | 'DrawImageSolid'
|
||||||
|
// | 'ImageCopy'
|
||||||
|
// | 'ImageCopySolid'
|
||||||
|
// | 'ImageEdit'
|
||||||
|
// | 'ImageEditSolid'
|
||||||
|
// | 'ImageMultiple'
|
||||||
|
// | 'ImageMultipleSolid'
|
||||||
|
// | 'ImageOff'
|
||||||
|
// | 'ImageOffSolid'
|
||||||
|
// | 'ImageProhibited'
|
||||||
|
// | 'ImageProhibitedSolid'
|
||||||
|
// | 'ImageSearch'
|
||||||
|
// | 'ImageSearchSolid'
|
||||||
|
| 'Video' |
||||||
|
// | 'VideoSolid'
|
||||||
|
// | 'VideoAdd'
|
||||||
|
// | 'VideoAddSolid'
|
||||||
|
// | 'VideoOff'
|
||||||
|
// | 'VideoOffSolid'
|
||||||
|
// | 'VideoProhibited'
|
||||||
|
// | 'VideoProhibitedSolid'
|
||||||
|
// | 'VideoSync'
|
||||||
|
// | 'VideoSyncSolid'
|
||||||
|
// | 'File'
|
||||||
|
// | 'FileSolid'
|
||||||
|
| 'FileAdd' |
||||||
|
// | 'FileAddSolid'
|
||||||
|
// | 'FileArrowDown'
|
||||||
|
// | 'FileArrowDownSolid'
|
||||||
|
// | 'FileArrowUp'
|
||||||
|
// | 'FileArrowUpSolid'
|
||||||
|
// | 'FileBulletList'
|
||||||
|
// | 'FileBulletListSolid'
|
||||||
|
// | 'FileBulletListMultiple'
|
||||||
|
// | 'FileBulletListMultipleSolid'
|
||||||
|
// | 'FileBulletListOff'
|
||||||
|
// | 'FileBulletListOffSolid'
|
||||||
|
// | 'FileCatchUp'
|
||||||
|
// | 'FileCatchUpSolid'
|
||||||
|
// | 'FileCheckmark'
|
||||||
|
// | 'FileCheckmarkSolid'
|
||||||
|
// | 'FileCopy'
|
||||||
|
// | 'FileCopySolid'
|
||||||
|
// | 'FileDismiss'
|
||||||
|
// | 'FileDismissSolid'
|
||||||
|
// | 'FileEdit'
|
||||||
|
// | 'FileEditSolid'
|
||||||
|
| 'FileError' |
||||||
|
// | 'FileErrorSolid'
|
||||||
|
// | 'FileLink'
|
||||||
|
// | 'FileLinkSolid'
|
||||||
|
// | 'FileLock'
|
||||||
|
// | 'FileLockSolid'
|
||||||
|
| 'FileMultiple' |
||||||
|
// | 'FileMultipleSolid'
|
||||||
|
// | 'FilePdf'
|
||||||
|
// | 'FilePdfSolid'
|
||||||
|
| 'FileProhibited' |
||||||
|
// | 'FileProhibitedSolid'
|
||||||
|
// | 'FileQuestionMark'
|
||||||
|
// | 'FileQuestionMarkSolid'
|
||||||
|
// | 'FileSearch'
|
||||||
|
// | 'FileSearchSolid'
|
||||||
|
| 'FileSync' |
||||||
|
// | 'FileSyncSolid'
|
||||||
|
// | 'FileText'
|
||||||
|
// | 'FileTextSolid'
|
||||||
|
// | 'FileJs'
|
||||||
|
// | 'FileJsSolid'
|
||||||
|
// | 'FileCss'
|
||||||
|
// | 'FileCssSolid'
|
||||||
|
| 'FileBriefcase' |
||||||
|
| 'FileBriefcaseSolid' |
||||||
|
| 'ChevronDoubleRight' |
||||||
|
// | 'ChevronDoubleRightSolid'
|
||||||
|
| 'ChevronDoubleLeft' |
||||||
|
// | 'ChevronDoubleLeftSolid'
|
||||||
|
| 'MoreHorizontal' |
||||||
|
// | 'MoreHorizontalSolid'
|
||||||
|
// | 'MoreVertical'
|
||||||
|
// | 'MoreVerticalSolid'
|
||||||
|
| 'Delete' |
||||||
|
// | 'DeleteSolid'
|
||||||
|
// | 'DeleteDismiss'
|
||||||
|
// | 'DeleteDismissSolid'
|
||||||
|
// | 'DeleteOff'
|
||||||
|
// | 'DeleteOffSolid'
|
||||||
|
// | 'DeleteArrowBack'
|
||||||
|
// | 'DeleteArrowBackSolid'
|
||||||
|
// | 'DeleteLines'
|
||||||
|
// | 'DeleteLinesSolid'
|
||||||
|
| 'Eye' |
||||||
|
// | 'EyeSolid'
|
||||||
|
// | 'EyeOff'
|
||||||
|
// | 'EyeOffSolid'
|
||||||
|
// | 'EyeTracking'
|
||||||
|
// | 'EyeTrackingSolid'
|
||||||
|
// | 'EyeTrackingOff'
|
||||||
|
// | 'EyeTrackingOffSolid'
|
||||||
|
| 'Share' |
||||||
|
// | 'ShareSolid'
|
||||||
|
// | 'Alert'
|
||||||
|
// | 'AlertSolid'
|
||||||
|
// | 'AlertOff'
|
||||||
|
// | 'AlertOffSolid'
|
||||||
|
// | 'AlertOn'
|
||||||
|
// | 'AlertOnSolid'
|
||||||
|
// | 'AlertSnooze'
|
||||||
|
// | 'AlertSnoozeOff'
|
||||||
|
// | 'AlertUrgent'
|
||||||
|
// | 'AlertUrgentSolid'
|
||||||
|
// | 'ArrowClockwise'
|
||||||
|
| 'ArrowClockwiseSolid' |
||||||
|
// | 'ArrowClockwiseDashes'
|
||||||
|
// | 'ArrowClockwiseDashesSolid'
|
||||||
|
// | 'ArrowHookDownLeft'
|
||||||
|
// | 'ArrowHookDownLeftSolid'
|
||||||
|
// | 'ArrowHookDownRight'
|
||||||
|
// | 'ArrowHookDownRightSolid'
|
||||||
|
// | 'Bookmark'
|
||||||
|
// | 'BookmarkSolid'
|
||||||
|
// | 'BookmarkAdd'
|
||||||
|
// | 'BookmarkAddSolid'
|
||||||
|
// | 'BookmarkMultiple'
|
||||||
|
// | 'BookmarkMultipleSolid'
|
||||||
|
// | 'BookmarkSearch'
|
||||||
|
// | 'BookmarkSearchSearch'
|
||||||
|
// | 'Clipboard'
|
||||||
|
// | 'ClipboardSolid'
|
||||||
|
// | 'ClipboardCheckmark'
|
||||||
|
// | 'ClipboardCheckmarkSolid'
|
||||||
|
// | 'ClipboardError'
|
||||||
|
// | 'ClipboardErrorSolid'
|
||||||
|
// | 'ClipboardText'
|
||||||
|
// | 'ClipboardTextSolid'
|
||||||
|
// | 'Clock'
|
||||||
|
// | 'ClockFill'
|
||||||
|
// | 'ClockAlarm'
|
||||||
|
// | 'ClockAlarmSolid'
|
||||||
|
// | 'Cloud'
|
||||||
|
// | 'CloudSolid'
|
||||||
|
// | 'CloudCheckmark'
|
||||||
|
// | 'CloudCheckmarkSolid'
|
||||||
|
// | 'CloudAdd'
|
||||||
|
// | 'CloudAddSolid'
|
||||||
|
// | 'CloudDismiss'
|
||||||
|
// | 'CloudDismissSolid'
|
||||||
|
// | 'CloudEdit'
|
||||||
|
// | 'CloudEditSolid'
|
||||||
|
// | 'CloudOff'
|
||||||
|
// | 'CloudOffSolid'
|
||||||
|
// | 'CloudSync'
|
||||||
|
// | 'CloudSyncSolid'
|
||||||
|
| 'Copy' |
||||||
|
// | 'CopySolid'
|
||||||
|
| 'Rename' |
||||||
|
// | 'RenameSolid'
|
||||||
|
// | 'Desktop'
|
||||||
|
| 'DesktopSolid' |
||||||
|
// | 'Dismiss'
|
||||||
|
| 'DismissSolid' |
||||||
|
// | 'DismissCircle'
|
||||||
|
| 'DismissCircleSolid' |
||||||
|
// | 'DismissSquare'
|
||||||
|
// | 'DismissSquareSolid'
|
||||||
|
// | 'Edit'
|
||||||
|
// | 'EditSolid'
|
||||||
|
// | 'EditOff'
|
||||||
|
// | 'EditOffSolid'
|
||||||
|
// | 'ErrorCircle'
|
||||||
|
| 'ErrorCircleSolid' |
||||||
|
// | 'Info'
|
||||||
|
| 'InfoSolid' |
||||||
|
// | 'Link'
|
||||||
|
// | 'LinkSolid'
|
||||||
|
| 'MailInbox' |
||||||
|
// | 'MailInboxSolid'
|
||||||
|
// | 'Navigation'
|
||||||
|
// | 'NavigationSolid'
|
||||||
|
// | 'Open'
|
||||||
|
// | 'OpenSolid'
|
||||||
|
// | 'Question'
|
||||||
|
// | 'QuestionSolid'
|
||||||
|
// | 'QuestionCircle'
|
||||||
|
// | 'QuestionCircleSolid'
|
||||||
|
// | 'Warning'
|
||||||
|
| 'WarningSolid' |
||||||
|
// | 'Moon'
|
||||||
|
| 'MoonSolid' |
||||||
|
// | 'Sun'
|
||||||
|
| 'SunSolid' |
||||||
|
// | 'TaskList'
|
||||||
|
| 'TaskListSolid' |
||||||
|
| 'Sync' |
||||||
|
| 'SyncSolid' |
||||||
|
| 'SyncOff' |
||||||
|
| 'SyncOffSolid' |
||||||
|
| 'SyncCircle' |
||||||
|
| 'SyncCircleSolid' |
||||||
|
|
||||||
|
export function installIcons(mount: ParentNode): void { |
||||||
|
if (mount.querySelector("#wfs-icon-symbols")) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const symbols = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
${Object.entries(paths).map( |
||||||
|
([key, path]) => ` |
||||||
|
<symbol id="${key}" viewBox="0 0 20 20"> |
||||||
|
<path d="${path}" fill="currentColor" /> |
||||||
|
</symbol>`,
|
||||||
|
).join('')} |
||||||
|
</svg>`;
|
||||||
|
const temp = document.createElement("div"); |
||||||
|
temp.innerHTML = symbols; |
||||||
|
const svgElm = temp.firstElementChild! as SVGElement; |
||||||
|
temp.innerHTML = ""; |
||||||
|
svgElm.style.cssText = "display:none"; |
||||||
|
svgElm.id = "wfs-icon-symbols"; |
||||||
|
mount.insertBefore(svgElm, mount.firstChild); |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import {defineComponent, h, Prop, reactive} from "vue"; |
||||||
|
import {useImage, UseImageOptions} from "@vueuse/core"; |
||||||
|
|
||||||
|
export default defineComponent({ |
||||||
|
props: { |
||||||
|
src: { |
||||||
|
type: String, |
||||||
|
required: true, |
||||||
|
} as Prop<UseImageOptions['src']>, |
||||||
|
srcset: String as Prop<UseImageOptions['srcset']>, |
||||||
|
sizes: String as Prop<UseImageOptions['sizes']>, |
||||||
|
alt: String as Prop<UseImageOptions['alt']>, |
||||||
|
class: String as Prop<UseImageOptions['class']>, |
||||||
|
loading: String as Prop<UseImageOptions['loading']>, |
||||||
|
crossorigin: String as Prop<UseImageOptions['crossorigin']>, |
||||||
|
referrerPolicy: String as Prop<UseImageOptions['referrerPolicy']> |
||||||
|
}, |
||||||
|
setup(props, {slots}) { |
||||||
|
const data = reactive(useImage(props as UseImageOptions)) |
||||||
|
|
||||||
|
return () => { |
||||||
|
if (data.isLoading && slots.loading) { |
||||||
|
return slots.loading(data) |
||||||
|
} else if (data.error && slots.error) { |
||||||
|
return slots.error(data.error) |
||||||
|
} else if (slots.default) { |
||||||
|
return slots.default(data) |
||||||
|
} else { |
||||||
|
return h('img', props) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
</script> |
@ -0,0 +1,24 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
defineProps<{ text?: string }>() |
||||||
|
const bars = Array(12).fill(0).map((_, i) => i) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex-center"> |
||||||
|
<div class=" |
||||||
|
flex items-center py-2 px-3 bg-gray-300/80 backdrop-blur-md rounded-lg pointer-events-none |
||||||
|
dark:bg-gray-700/80 dark:text-white ring-1 ring-white/10 |
||||||
|
"> |
||||||
|
<div class="size-4 me-2 relative spinner"> |
||||||
|
<div |
||||||
|
v-for="bar in bars" |
||||||
|
:key="bar" |
||||||
|
class="loading-bar bg-black dark:bg-white" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<span class="select-none text-sm"> |
||||||
|
{{ text ?? '正在加载...' }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,24 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
defineProps<{ |
||||||
|
value?: number |
||||||
|
disabled?: boolean; |
||||||
|
label?: string; |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div> |
||||||
|
<div class="relative h-0.5 w-full bg-gray-300"> |
||||||
|
<div |
||||||
|
v-if="value != null" |
||||||
|
:style="{ width: `${Math.min(Math.round(value! * 100), 100)}%` }" |
||||||
|
class="h-full overflow-hidden bg-indigo-500" |
||||||
|
> |
||||||
|
<div |
||||||
|
v-if="value != null && value < 1 && !disabled" |
||||||
|
class="animate-marquee h-full w-full bg-white opacity-50" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,50 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import {ref, watch} from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
text: string | number |
||||||
|
tag?: string |
||||||
|
highlight?: boolean |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(ev: 'dblclick', event: Event): void |
||||||
|
}>() |
||||||
|
|
||||||
|
const el = ref<HTMLElement | null>(null) |
||||||
|
|
||||||
|
const select = (node: Node) => { |
||||||
|
const range = new Range() |
||||||
|
range.selectNode(node) |
||||||
|
const s = document.getSelection()! |
||||||
|
s.empty() |
||||||
|
s.addRange(range) |
||||||
|
} |
||||||
|
|
||||||
|
const dblclick = (evt: Event) => { |
||||||
|
select(evt.target as Node) |
||||||
|
emit('dblclick', evt) |
||||||
|
} |
||||||
|
|
||||||
|
watch(() => props.highlight, value => { |
||||||
|
if (value && el.value) { |
||||||
|
select(el.value) |
||||||
|
} else if (!value && el.value) { |
||||||
|
const range = new Range() |
||||||
|
range.selectNode(el.value) |
||||||
|
const s = document.getSelection()! |
||||||
|
s.removeRange(range) |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<component |
||||||
|
:is="tag ?? 'span'" |
||||||
|
ref="el" |
||||||
|
class="selection:bg-indigo-500 break-all selection:text-white" |
||||||
|
@dblclick="dblclick" |
||||||
|
> |
||||||
|
{{ text }} |
||||||
|
</component> |
||||||
|
</template> |
@ -0,0 +1,9 @@ |
|||||||
|
import {getCurrentInstance} from 'vue' |
||||||
|
|
||||||
|
export function getRootBarrier(): Node | undefined { |
||||||
|
let el = getCurrentInstance()!.vnode.el as Node |
||||||
|
while (el.nodeType === Node.TEXT_NODE || el.nodeType === Node.COMMENT_NODE) { |
||||||
|
el = el.parentNode! |
||||||
|
} |
||||||
|
return (el as Element).closest('[data-ui-barrier]') as Node |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
import BackendWorker from "../backend?sharedworker&inline"; |
||||||
|
import {createReplier, emit, isRecvData, isSendData, uniqueId} from "../shared"; |
||||||
|
|
||||||
|
type Resolver = (value: any) => void; |
||||||
|
type Rejecter = (value: any) => void; |
||||||
|
|
||||||
|
const resolvers: Map<number, [Resolver, Rejecter]> = new Map(); |
||||||
|
|
||||||
|
const worker = new BackendWorker(); |
||||||
|
|
||||||
|
// 接收来自后端的消息
|
||||||
|
worker.port.onmessage = (evt) => { |
||||||
|
if (isSendData(evt.data)) { |
||||||
|
const reply = createReplier(worker.port, evt.data); |
||||||
|
const {scope, action, data} = evt.data; |
||||||
|
// console.log(scope, action, data)
|
||||||
|
emit(`${scope}:${action}`, data, reply); |
||||||
|
} else if (!isRecvData(evt.data)) { |
||||||
|
console.warn("[wfs] unknown message event:", evt); |
||||||
|
} else if (resolvers.has(evt.data.id)) { |
||||||
|
const {id, error, data} = evt.data; |
||||||
|
const [resolve, reject] = resolvers.get(id)!; |
||||||
|
if (error != null) { |
||||||
|
reject(error); |
||||||
|
} else { |
||||||
|
resolve(data); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// 监听页面卸载,通知后端删除关联
|
||||||
|
window.addEventListener("beforeunload", () => { |
||||||
|
void dispatch("app", "close"); |
||||||
|
}); |
||||||
|
|
||||||
|
window.addEventListener("online", () => { |
||||||
|
return dispatch("net", "status", "on"); |
||||||
|
}); |
||||||
|
|
||||||
|
window.addEventListener("offline", () => { |
||||||
|
return dispatch("net", "status", "off"); |
||||||
|
}); |
||||||
|
|
||||||
|
export function dispatch(scope: "cfg", action: "init", config: ApiConfig): Promise<void>; |
||||||
|
|
||||||
|
export function dispatch(scope: "app", action: "close"): Promise<void>; |
||||||
|
|
||||||
|
export function dispatch(scope: "net", action: "status", data: "on" | "off"): Promise<void>; |
||||||
|
|
||||||
|
export function dispatch(scope: "dir", action: "all"): Promise<Array<CloudDirectory>>; |
||||||
|
export function dispatch(scope: "dir", action: "create", data: NewDirEventData): Promise<CloudDirectory>; |
||||||
|
export function dispatch(scope: "dir", action: "delete", dir: number): Promise<boolean>; |
||||||
|
export function dispatch(scope: "dir", action: "rename", data: RenameDirEventData): Promise<CloudDirectory>; |
||||||
|
export function dispatch(scope: "file", action: "rename", data: RenameFileEventData): Promise<CloudFile>; |
||||||
|
export function dispatch(scope: "file", action: "delete", files: number[] | number): Promise<boolean>; |
||||||
|
export function dispatch(scope: "file", action: "list", data: FilesEventData): Promise<FilesEventResult> |
||||||
|
|
||||||
|
export function dispatch(scope: "task", action: "cleanup"): Promise<void>; |
||||||
|
export function dispatch(scope: "task", action: "cancel", data: TaskEventData): Promise<void>; |
||||||
|
export function dispatch(scope: "task", action: "create", data: TaskCreateData): Promise<void>; |
||||||
|
export function dispatch(scope: "task", action: "remove", data: TaskEventData): Promise<void>; |
||||||
|
export function dispatch(scope: "task", action: "resume", data: TaskEventData): Promise<void>; |
||||||
|
|
||||||
|
export function dispatch<T = any>(scope: string, action: string, data?: any): Promise<T> { |
||||||
|
const id = uniqueId(); |
||||||
|
return new Promise<T>((resolve, reject) => { |
||||||
|
resolvers.set(id, [resolve, reject]); |
||||||
|
worker.port.postMessage({id, scope, action, data} as SendData); |
||||||
|
}); |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue