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