mirror of
https://github.com/VirtualHotBar/NetMount.git
synced 2026-06-04 10:22:54 +08:00
Add files via upload
This commit is contained in:
284
src/app.tsx
Normal file
284
src/app.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Layout, Menu, Breadcrumb, Button, Message, Grid, ConfigProvider } from '@arco-design/web-react';
|
||||
import "@arco-themes/react-vhbs/css/arco.css";
|
||||
//import "@arco-design/web-react/dist/css/arco.css";
|
||||
import { Routes, Route, Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Test } from './controller/test';
|
||||
import { Routers } from './type/routers';
|
||||
import { Home_page } from './page/home/home';
|
||||
import { Storage_page } from './page/storage/storage';
|
||||
import { AddStorage_page } from './page/storage/add';
|
||||
import { Explorer_page } from './page/storage/explorer';
|
||||
import { Mount_page } from './page/mount/mount';
|
||||
import { Transmit_page } from './page/transmit/transmit';
|
||||
import { Task_page } from './page/task/task';
|
||||
import Setting_page from './page/setting/setting';
|
||||
import AddMount_page from './page/mount/add';
|
||||
import { IconAttachment, IconClose, IconCloud, IconHome, IconLink, IconList, IconMinus, IconSettings, IconStorage, IconSwap } from '@arco-design/web-react/icon';
|
||||
import { windowsHide, windowsMini } from './controller/window';
|
||||
import { rcloneInfo } from './services/rclone';
|
||||
import { AddTask_page } from './page/task/add';
|
||||
import { hooks } from './services/hook';
|
||||
import { getLocale } from './controller/language/language';
|
||||
import { nmConfig } from './services/config';
|
||||
import { getLangCode } from './controller/language/localized';
|
||||
import { Locale } from '@arco-design/web-react/es/locale/interface';
|
||||
|
||||
const { Item: MenuItem, SubMenu } = Menu;
|
||||
const { Sider, Header, Content, Footer } = Layout;
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
|
||||
|
||||
//递归查询对应的路由
|
||||
function searchRoute(
|
||||
path: string,
|
||||
routes: Routers[]
|
||||
): Routers | null {
|
||||
for (let item of routes) {
|
||||
if (item.path === path) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = searchRoute(path, item.children);
|
||||
if (found) {
|
||||
return found; // 当在子路由中找到匹配项时,直接返回
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//生成菜单
|
||||
function mapMenuItem(routes: Routers[]): JSX.Element {
|
||||
return <>{
|
||||
routes.map((item) => {
|
||||
if (item.hide) {
|
||||
return <></>
|
||||
} else if (item.children && item.children.length > 0 && !item.hideChildren) {
|
||||
return (<SubMenu key={item.path} title={item.title} >{mapMenuItem(item.children)}</SubMenu>)
|
||||
} else {
|
||||
return (<MenuItem key={item.path} > {item.title}</MenuItem>)
|
||||
}
|
||||
})
|
||||
}</>
|
||||
|
||||
}
|
||||
|
||||
//生成页面
|
||||
function mapRouters(routes: Routers[]): JSX.Element {
|
||||
return <>{
|
||||
routes.map((item) => { // 添加index作为map方法的第二个参数,用于生成唯一键
|
||||
if (item.children && item.children.length > 0) {
|
||||
return <React.Fragment key={`${item.path}-group`}> {/* 给包含子路由的Fragment添加一个唯一的key */}
|
||||
{mapRouters(item.children)}
|
||||
{item.component ? <Route key={item.path} path={item.path} element={item.component}></Route> : <></>}
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
return <Route key={item.path} path={item.path} element={item.component}></Route>;
|
||||
}
|
||||
})
|
||||
}</>
|
||||
}
|
||||
|
||||
//生成面包屑
|
||||
function generateBreadcrumb(pathname: string, routes: Routers[]): JSX.Element[] {
|
||||
const pathSnippets = pathname.split('/').filter(i => i);
|
||||
const breadcrumbItems: JSX.Element[] = [];
|
||||
|
||||
if (pathSnippets.length == 1) {
|
||||
return [];
|
||||
}
|
||||
// 创建面包屑项(根据是否有子菜单和是否隐藏子菜单决定是否为链接)
|
||||
function createBreadcrumbItem(route: Routers): JSX.Element {
|
||||
if (route.children && route.children.length > 0 && !route.hideChildren) {
|
||||
return <>{route.title}</>;
|
||||
} else {
|
||||
return <Link to={route.path}>{route.title}</Link>;
|
||||
}
|
||||
}
|
||||
|
||||
pathSnippets.reduce((prevPath, pathSnippet) => {
|
||||
const currentPath = `${prevPath}/${pathSnippet}`;
|
||||
const route = searchRoute(currentPath, routes);
|
||||
|
||||
let breadcrumbItem: JSX.Element;
|
||||
|
||||
if (route) {
|
||||
breadcrumbItem = (
|
||||
<Breadcrumb.Item key={currentPath}>
|
||||
{createBreadcrumbItem(route)}
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
} else {
|
||||
breadcrumbItem = (
|
||||
<Breadcrumb.Item key={currentPath}>
|
||||
{pathSnippet}
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
}
|
||||
breadcrumbItems.push(breadcrumbItem);
|
||||
return currentPath;
|
||||
}, '');
|
||||
|
||||
return breadcrumbItems;
|
||||
}
|
||||
|
||||
|
||||
function App() {
|
||||
//const [router, setRouter] = useState<Routers | null>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation()
|
||||
const [localeStr, setLocaleStr] = useState<string>(getLangCode(nmConfig.settings.language!))
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>(['/']);
|
||||
|
||||
|
||||
const routers: Array<Routers> = [
|
||||
{
|
||||
title: <><IconHome />{t('home')}</>,
|
||||
path: '/',
|
||||
component: <Home_page />,
|
||||
},
|
||||
{
|
||||
title: <><IconCloud />{t('storage')}</>,
|
||||
path: '/storage',
|
||||
children: [
|
||||
{
|
||||
title: t('manage'),
|
||||
path: '/storage/manage',
|
||||
component: <Storage_page />,
|
||||
hideChildren: true,
|
||||
children: [
|
||||
{
|
||||
title: t('add'),
|
||||
path: '/storage/manage/add',
|
||||
key: '/storage/manage',//因为父菜单隐藏了子菜单项,在此页面时设置父菜单key以选择父菜单项
|
||||
component: <AddStorage_page />,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('explorer'),
|
||||
path: '/storage/explorer',
|
||||
component: <Explorer_page />
|
||||
}
|
||||
]
|
||||
}, {
|
||||
title: <><IconStorage />{t('mount')}</>,
|
||||
path: '/mount',
|
||||
component: <Mount_page />,
|
||||
hideChildren: true,
|
||||
children: [
|
||||
{
|
||||
title: t('add'),
|
||||
path: '/mount/add',
|
||||
key: '/mount',//因为父菜单隐藏了子菜单项,在此页面时设置父菜单key以选择父菜单项
|
||||
component: <AddMount_page />,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: <><IconSwap style={{ transform: 'rotate(90deg)' }} />{t('transmit')}</> /* +(rcloneInfo.stats.transferring? '(' + rcloneInfo.stats.transferring.length + ')': '') */,
|
||||
path: '/transmit',
|
||||
component: <Transmit_page />,
|
||||
},
|
||||
{
|
||||
title: <><IconList />{t('task')}</>,
|
||||
path: '/task',
|
||||
component: <Task_page />,
|
||||
hideChildren: true,
|
||||
children: [
|
||||
{
|
||||
title: t('add'),
|
||||
path: '/task/add',
|
||||
key: '/task',//因为父菜单隐藏了子菜单项,在此页面时设置父菜单key以选择父菜单项
|
||||
component: <AddTask_page />,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: <><IconSettings />{t('setting')}</>,
|
||||
path: '/setting',
|
||||
component: <Setting_page />,
|
||||
}
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
hooks.setLocaleStr = setLocaleStr
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
hooks.navigate = (path: string) => {
|
||||
if (path != location.pathname) {
|
||||
location.pathname.includes('add') && Message.warning(t('prompt_for_leaving_the_add_or_edit_page'))
|
||||
navigate(path)
|
||||
}
|
||||
}
|
||||
|
||||
//setRouter(searchRoute(location.pathname, routers));
|
||||
const route = searchRoute(location.pathname, routers);
|
||||
if (route) {
|
||||
if (route.key) {
|
||||
setSelectedKeys([route.key]);
|
||||
} else {
|
||||
setSelectedKeys([route.path]);
|
||||
}
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
|
||||
/*
|
||||
<Layout style={{ height: '400px' }}>
|
||||
<Header>Header</Header>
|
||||
<Layout>
|
||||
<Sider>Sider</Sider>
|
||||
<Content>Content</Content>
|
||||
</Layout>
|
||||
<Footer>Footer</Footer>
|
||||
</Layout> */
|
||||
return (
|
||||
<ConfigProvider locale={getLocale(localeStr)}>
|
||||
<Layout style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'var(--color-bg-1)'
|
||||
}}>
|
||||
<Header style={{ width: '100%', height: '2.4rem', backgroundColor: 'var(--color-bg-2)', borderBlockEnd: '1px solid var(--color-border-2)' }}>
|
||||
<Row >
|
||||
<Col flex={'auto'} data-tauri-drag-region style={{ height: '2.4rem', display: 'flex' }}>
|
||||
<img src='/img/color.svg' style={{ width: '1.8rem', height: '1.8rem', marginTop: '0.3rem', marginLeft: '0.6rem' }} data-tauri-drag-region />
|
||||
<span style={{ marginLeft: '0.3rem', fontSize: '1.2rem', marginTop: '0.3rem', color: 'var(--color-text-1)' }} data-tauri-drag-region>NetMount</span>
|
||||
</Col>
|
||||
<Col flex={'5rem'} style={{ textAlign: 'right' }}>
|
||||
<Button onClick={windowsMini} icon={<IconMinus style={{ fontSize: '1.1rem', color: 'var(--color-text-2)' }} />} type='text' style={{ width: '2.5rem', paddingTop: '0.5rem' }} />
|
||||
<Button onClick={windowsHide} icon={<IconClose style={{ fontSize: '1.1rem' }} />} type='text' status='danger' style={{ width: '2.5rem', paddingTop: '0.5rem' }} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
|
||||
<Layout style={{ maxHeight: 'calc(100% - 2.4rem)' }}>
|
||||
<Sider style={{ width: '10rem' }} >
|
||||
<Menu
|
||||
defaultOpenKeys={['/storage']}
|
||||
selectedKeys={selectedKeys}
|
||||
style={{ height: '100%' }}
|
||||
onClickMenuItem={(path) => {
|
||||
hooks.navigate(path)
|
||||
}}
|
||||
>{mapMenuItem(routers)}</Menu>
|
||||
</Sider>
|
||||
<Content style={{ maxHeight: '100%', padding: '1.1rem' }}>
|
||||
{/* <Breadcrumb style={{ margin: '16px 0' }}>{generateBreadcrumb(location.pathname, routers)}</Breadcrumb> */}
|
||||
<Routes>{mapRouters(routers)}</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export { App }
|
||||
BIN
src/assets/font/HarmonyOS_Sans_Bold.ttf
Normal file
BIN
src/assets/font/HarmonyOS_Sans_Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/font/HarmonyOS_Sans_Regular.ttf
Normal file
BIN
src/assets/font/HarmonyOS_Sans_Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/font/seguiemj.woff2
Normal file
BIN
src/assets/font/seguiemj.woff2
Normal file
Binary file not shown.
49
src/controller/errorHandling.ts
Normal file
49
src/controller/errorHandling.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
//日志处理(错误处理)
|
||||
import { Modal } from "@arco-design/web-react";
|
||||
import { t } from "i18next";
|
||||
import { ReactNode } from "react";
|
||||
window.onerror = async function (msg, url, lineNo, columnNo, error) {
|
||||
let message = [
|
||||
'Message: ' + msg,
|
||||
'URL: ' + url,
|
||||
'Line: ' + lineNo,
|
||||
'Column: ' + columnNo,
|
||||
'Error object: ' + JSON.stringify(error)
|
||||
].join(' - ');
|
||||
|
||||
await errorThrowToUser(message)
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', async function (event) {
|
||||
await errorThrowToUser(event.reason)
|
||||
});
|
||||
|
||||
window.addEventListener('error', async (event) => {
|
||||
await errorThrowToUser(event.message)
|
||||
}, true);
|
||||
|
||||
async function errorThrowToUser(message: string) {
|
||||
//排除这个错误
|
||||
if (message.toString().includes('ResizeObserver')) { return }
|
||||
|
||||
let content = t('error_tips') + ',Error:' + message
|
||||
|
||||
//提示错误
|
||||
await errorDialog(t('error'), content)
|
||||
}
|
||||
//错误对话框
|
||||
function errorDialog(title: string, content: ReactNode) {
|
||||
return new Promise((resolve) => {
|
||||
Modal.error(
|
||||
{
|
||||
title: title,
|
||||
content: content,
|
||||
onOk: () => { resolve(true) },
|
||||
onCancel: () => { resolve(false) },
|
||||
maskClosable: false,
|
||||
closable: false
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
45
src/controller/language/language.ts
Normal file
45
src/controller/language/language.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import zhCN from '@arco-design/web-react/es/locale/zh-CN';
|
||||
import enUS from '@arco-design/web-react/es/locale/en-US';
|
||||
import jaJP from '@arco-design/web-react/es/locale/ja-JP';
|
||||
import koKR from '@arco-design/web-react/es/locale/ko-KR';
|
||||
import idID from '@arco-design/web-react/es/locale/id-ID';
|
||||
import thTH from '@arco-design/web-react/es/locale/th-TH';
|
||||
import zhHK from '@arco-design/web-react/es/locale/zh-HK';
|
||||
import frFR from '@arco-design/web-react/es/locale/fr-FR';
|
||||
import esES from '@arco-design/web-react/es/locale/es-ES';
|
||||
import deDE from '@arco-design/web-react/es/locale/de-DE';
|
||||
import itIT from '@arco-design/web-react/es/locale/it-IT';
|
||||
import viVN from '@arco-design/web-react/es/locale/vi-VN';
|
||||
import { Locale } from '@arco-design/web-react/es/locale/interface';
|
||||
|
||||
function getLocale(locale:string):Locale {
|
||||
switch (locale) {
|
||||
case 'zh-cn':
|
||||
return zhCN;
|
||||
case 'en-us':
|
||||
return enUS;
|
||||
case 'ja-jp':
|
||||
return jaJP;
|
||||
case 'ko-kr':
|
||||
return koKR as unknown as Locale;
|
||||
case 'id-id':
|
||||
return idID as unknown as Locale;
|
||||
case 'th-th':
|
||||
return thTH as unknown as Locale;
|
||||
case 'zh-hk':
|
||||
return zhHK;
|
||||
case 'fr-fr':
|
||||
return frFR as unknown as Locale;
|
||||
case 'es-es':
|
||||
return esES as unknown as Locale;
|
||||
case 'de-de':
|
||||
return deDE as unknown as Locale;
|
||||
case 'it-it':
|
||||
return itIT as unknown as Locale;
|
||||
case 'vi-vn':
|
||||
return viVN as unknown as Locale;
|
||||
default:
|
||||
return zhCN;
|
||||
}
|
||||
}
|
||||
export{getLocale}
|
||||
33
src/controller/language/localized.ts
Normal file
33
src/controller/language/localized.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
//本地化
|
||||
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { t } from "i18next";
|
||||
import i18n from "../../services/i18n";
|
||||
import { roConfig } from "../../services/config";
|
||||
import { hooks } from "../../services/hook";
|
||||
|
||||
async function setLocalized(lang: string) {
|
||||
lang = lang.toLowerCase()
|
||||
|
||||
hooks.setLocaleStr(getLangCode(lang))
|
||||
|
||||
i18n.changeLanguage(lang)
|
||||
await invoke('set_localized', {
|
||||
localizedData: {
|
||||
quit: t("quit"),
|
||||
show: t("tray_show"),
|
||||
hide: t("tray_hide")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getLangCode(lang: string): string {
|
||||
for (const value of roConfig.options.setting.language.select) {
|
||||
if (lang === value.value) {
|
||||
return value.langCode
|
||||
}
|
||||
}
|
||||
return roConfig.options.setting.language.select[roConfig.options.setting.language.defIndex].langCode
|
||||
}
|
||||
|
||||
export { setLocalized, getLangCode }
|
||||
265
src/controller/language/zh-cn.json
Normal file
265
src/controller/language/zh-cn.json
Normal file
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"starting": "正在启动",
|
||||
"home": "首页",
|
||||
"name": "名称",
|
||||
"type": "类型",
|
||||
"storage": "存储",
|
||||
"explorer": "浏览",
|
||||
"setting": "设置",
|
||||
"add": "添加",
|
||||
"refresh": "刷新",
|
||||
"delete": "删除",
|
||||
"manage": "管理",
|
||||
"storage_type": "存储类型",
|
||||
"please_select": "请选择",
|
||||
"Webdav_introduce": "WebDAV是一组基于超文本传输协议的技术集合,有利于用户间协同编辑和管理存储在万维网服务器文档。",
|
||||
"url": "地址",
|
||||
"vendor": "提供商",
|
||||
"user": "用户",
|
||||
"pass": "密码",
|
||||
"save": "保存",
|
||||
"step_back": "返回",
|
||||
"step_next": "下一步",
|
||||
"storage_introduce": "存储介绍",
|
||||
"show_advanced_options": "显示高级选项",
|
||||
"please_input": "请输入",
|
||||
"Input_and_press_enter": "输入并回车",
|
||||
"missing_parameter": "缺少参数",
|
||||
"StorageName": "存储名称",
|
||||
"storage_name_illegal": "存储名称不合法",
|
||||
"Storage_added_successfully": "存储添加成功",
|
||||
"Storage_added_failed": "存储添加失败",
|
||||
"edit": "编辑",
|
||||
"please_select_storage": "请选择存储",
|
||||
"actions": "操作",
|
||||
"size": "大小",
|
||||
"modified_time": "修改时间",
|
||||
"mount": "挂载",
|
||||
"task": "任务",
|
||||
"transmit": "传输",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"dev_tips": "正在开发,敬请期待",
|
||||
"confirm_delete_question": "确定要删除吗?",
|
||||
"dir": "目录",
|
||||
"create_directory": "创建目录",
|
||||
"dir_name_cannot_empty": "目录名不能为空",
|
||||
"upload_file": "上传文件",
|
||||
"show_all_options": "显示所有选项",
|
||||
"path": "路径",
|
||||
"mount_path": "挂载路径",
|
||||
"auto_drive_letter": "自动分配盘符",
|
||||
"simulate_hard_drive": "模拟本地硬盘",
|
||||
"read_only": "只读",
|
||||
"mount_options": "挂载选项",
|
||||
"mount_storage_successfully": "挂载存储成功",
|
||||
"mounted_time": "挂载时间",
|
||||
"storage_name": "存储名称",
|
||||
"unmount": "卸载",
|
||||
"mount_path_already_exists": "挂载路径已存在",
|
||||
"auto_mount": "软件启动时挂载",
|
||||
"mounted": "已挂载",
|
||||
"unmounted": "未挂载",
|
||||
"mount_status": "挂载状态",
|
||||
"no_data": "暂无数据",
|
||||
"transferring": "传输中",
|
||||
"overview": "总览",
|
||||
"transferred": "已传输",
|
||||
"used_time": "用时",
|
||||
"time": "时间",
|
||||
"speed": "速度",
|
||||
"eta": "剩余时间",
|
||||
"speed_avg": "平均速度",
|
||||
"target": "目标",
|
||||
"source": "来源",
|
||||
"read_config": "读取配置",
|
||||
"init": "初始化",
|
||||
"transm_task_created": "已创建任务,请到[传输]页查看信息",
|
||||
"clip_board": "剪切板",
|
||||
"paste": "粘贴",
|
||||
"empty_the_clipboard": "清空剪贴板",
|
||||
"parent_directory": "上级目录",
|
||||
"cut": "剪切",
|
||||
"more": "更多",
|
||||
"copy": "复制",
|
||||
"rename": "重命名",
|
||||
"name_cannot_empty": "名称不能为空",
|
||||
"task_name": "任务名称",
|
||||
"state": "状态",
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"cycle": "周期",
|
||||
"run_info": "运行信息",
|
||||
"task_type": "任务类型",
|
||||
"task_run_mode_start": "启动时",
|
||||
"task_run_mode_start_opt": "启动时(软件启动时执行)",
|
||||
"task_run_mode_time": "定时",
|
||||
"task_run_mode_time_opt": "定时(在特定时间执行)",
|
||||
"task_run_mode_interval": "间隔",
|
||||
"task_run_mode_interval_opt": "间隔(每隔一段时间执行)",
|
||||
"task_run_mode_disposable": "一次性",
|
||||
"task_run_mode_disposable_opt": "一次性(添加后立即执行,并自动删除任务)",
|
||||
"task_run_mode": "执行模式",
|
||||
"move": "移动",
|
||||
"sync": "同步",
|
||||
"source_path": "源路径",
|
||||
"target_path": "目标路径",
|
||||
"prompt_for_leaving_the_add_or_edit_page": "离开添加或编辑页面,未保存的设置将丢失。",
|
||||
"explain_for_task_path_format": "路径格式:目录以斜杠结尾,文件以不以斜杠结尾",
|
||||
"interval": "间隔",
|
||||
"day": "天",
|
||||
"week": "周",
|
||||
"month": "月",
|
||||
"hour": "小时",
|
||||
"minute": "分钟",
|
||||
"second": "秒",
|
||||
"the_task_name_is_illegal": "任务名称不合法",
|
||||
"task_added_successfully": "任务添加成功",
|
||||
"the_path_is_illegal": "路径不合法",
|
||||
"same_source_and_target": "源和目标相同",
|
||||
"bisync": "双向同步",
|
||||
"add_storage": "添加存储",
|
||||
"add_mount": "添加挂载",
|
||||
"add_task": "添加任务",
|
||||
"auto_themeMode": "跟随系统",
|
||||
"light_themeMode": "浅色模式",
|
||||
"dark_themeMode": "深色模式",
|
||||
"theme_mode": "主题模式",
|
||||
"tools": "工具",
|
||||
"encoding": "编码",
|
||||
"headers": "标头",
|
||||
"bearer_token_command": "Bearer token命令",
|
||||
"pacer_min_sleep": "最小休眠时间",
|
||||
"nextcloud_chunk_size": "Nextcloud块大小",
|
||||
"owncloud_exclude_shares": "OwnCloud排除共享",
|
||||
"description": "说明",
|
||||
"VolumeName": "卷名",
|
||||
"AllowNonEmpty": "允许非空",
|
||||
"AllowOther": "允许其他",
|
||||
"AllowRoot": "允许Root",
|
||||
"AsyncRead": "异步读取",
|
||||
"AttrTimeout": "属性超时",
|
||||
"Daemon": "守护进程",
|
||||
"DaemonTimeout": "守护进程超时",
|
||||
"DefaultPermissions": "默认权限",
|
||||
"ExtraFlags": "附加参数",
|
||||
"ExtraOptions": "附加选项",
|
||||
"MaxReadAhead": "最大预读",
|
||||
"NoAppleDouble": "无AppleDouble",
|
||||
"NoAppleXattr": "无AppleXattr",
|
||||
"WritebackCache": "写回缓存",
|
||||
"DaemonWait": "守护程序等待",
|
||||
"DeviceName": "设备名",
|
||||
"NetworkMode": "挂载为网络驱动器",
|
||||
"ReadOnly": "只读",
|
||||
"CacheMaxAge": "缓存最大有效期",
|
||||
"CacheMaxSize": "缓存最大大小",
|
||||
"CacheMode": "缓存模式",
|
||||
"CachePollInterval": "缓存轮询间隔",
|
||||
"CaseInsensitive": "不区分大小写",
|
||||
"ChunkSize": "块大小",
|
||||
"ChunkSizeLimit": "块大小限制",
|
||||
"DirCacheTime": "目录缓存时间",
|
||||
"DirPerms": "目录权限",
|
||||
"FilePerms": "文件权限",
|
||||
"NoChecksum": "无校验和",
|
||||
"NoModTime": "忽略文件修改时间",
|
||||
"NoSeek": "禁用文件定位",
|
||||
"PollInterval": "轮询间隔",
|
||||
"ReadAhead": "读取预取大小",
|
||||
"ReadWait": "读取等待时间",
|
||||
"WriteBack": "写回策略",
|
||||
"WriteWait": "写入等待时间",
|
||||
"Refresh": "刷新",
|
||||
"BlockNormDupes": "阻止重复块的规范化",
|
||||
"UsedIsSize": "将已使用空间视为文件大小",
|
||||
"FastFingerprint": "快速指纹计算",
|
||||
"DiskSpaceTotalSize": "磁盘总容量",
|
||||
"UID": "UID",
|
||||
"GID": "GID",
|
||||
"Umask": "文件权限掩码",
|
||||
"about": "关于",
|
||||
"autostart": "开机自启",
|
||||
"install": "安装",
|
||||
"winfsp_not_installed": "需要安装依赖(WinFsp),才能挂载存储。",
|
||||
"install_failed": "安装失败",
|
||||
"install_success": "安装成功",
|
||||
"components": "组件",
|
||||
"log": "日志",
|
||||
"start_hide": "启动时隐藏窗口",
|
||||
"quit": "退出",
|
||||
"tray_show": "显示主窗口",
|
||||
"tray_hide": "隐藏主窗口",
|
||||
"language": "语言",
|
||||
"update_available": "发现新版本",
|
||||
"current_version": "当前版本",
|
||||
"latest_version": "最新版本",
|
||||
"goto_the_website_get_latest_version_ask": "是否前往官网获取最新版?",
|
||||
"netmount_slogan": "统一管理和挂载云存储设施",
|
||||
"please_add_storage_tip": "当前无可用存储,请添加存储",
|
||||
"transmission_overview": "传输概览",
|
||||
"view_more": "详细",
|
||||
"technology_stack": "技术栈",
|
||||
"about_text": "由独立开发者 VirtualHotBar 开发并发布",
|
||||
"licence": "许可证",
|
||||
"error_tips": "请尝试重启程序,并记录控制台错误信息向开发者反馈",
|
||||
"host": "主机",
|
||||
"port": "端口",
|
||||
"tls": "传输加密(TLS)",
|
||||
"explicit_tls": "显式TLS",
|
||||
"client_id": "客户端ID",
|
||||
"client_secret": "客户端秘钥",
|
||||
"region": "区域",
|
||||
"global": "全球版",
|
||||
"us": "美国版",
|
||||
"de": "德国版",
|
||||
"cn": "中国版",
|
||||
"documentLibrary": "SharePoint(文档库)",
|
||||
"personal": "个人版",
|
||||
"business": "商业版",
|
||||
"drive_type": "类型",
|
||||
"token": "Token",
|
||||
"root_folder_id": "根目录ID",
|
||||
"drive_id": "驱动器ID",
|
||||
"local_introduce": "本地存储",
|
||||
"provider": "提供商",
|
||||
"bucket": "桶",
|
||||
"access_key_id": "Access Key ID",
|
||||
"secret_access_key": "Secret Access Key",
|
||||
"endpoint": "Endpoint",
|
||||
"acl": "ACL",
|
||||
"env_auth": "环境认证",
|
||||
"location_constraint": "位置约束",
|
||||
"storage_class": "存储类别",
|
||||
"server_side_encryption": "服务器端加密",
|
||||
"sse_kms_key_id": "SSE-KMS密钥ID",
|
||||
"remote": "存储",
|
||||
"filename_encryption": "文件名加密",
|
||||
"directory_name_encryption": "目录名加密",
|
||||
"password": "密码",
|
||||
"password2": "加盐",
|
||||
"key": "秘钥",
|
||||
|
||||
"ftp_introduce": "FTP(File Transfer Protocol)是一种广泛应用的网络协议,旨在通过客户端-服务器架构实现互联网上可靠、交互式的文件传输、共享与管理。",
|
||||
"onedrive_introduce": "OneDrive 是微软提供的云存储服务,具备跨平台文件同步、分享功能,用户可从任何设备安全访问文档、照片及文件夹,同时提供付费方案以获得更大存储空间。",
|
||||
"s3_introduce": "S3协议是Amazon定义的一套用于在云环境中进行对象存储操作的标准API接口,支持包括上传、下载、检索、管理对象及其元数据在内的多种功能,已被众多云存储提供商广泛采用作为兼容接口。",
|
||||
"googledrive_introduce": "Google Drive是Google提供的云存储服务,集成于Google生态系统,支持文档协作、备份和共享。",
|
||||
"dropbox_introduce": "Dropbox是知名个人与团队文件同步、备份和共享平台,具有跨设备访问和版本控制功能。",
|
||||
"webdav_introduce": "WebDAV是一种基于HTTP协议的网络文件系统标准,允许用户通过标准文件操作(如复制、移动、删除)远程管理服务器上的文件。",
|
||||
"box_introduce": "Box是企业级云内容管理平台,专注于安全文件共享、协作和流程自动化。",
|
||||
"googlecloudstorage_introduce": "Google Cloud Storage是谷歌云平台的云存储服务,提供多种存储级别,适用于不同性能、成本和持久性需求。",
|
||||
"http_introduce": "HTTP在此处可能指通过HTTP协议直接访问存储在Web服务器上的静态文件。",
|
||||
"swift_introduce": "OpenStack Object Storage (Swift)是开源云存储项目,提供大规模、弹性、API可编程的对象存储服务。",
|
||||
"jottacloud_introduce": "Jottacloud是挪威云备份和同步服务,注重数据隐私和安全性,提供无限存储计划。",
|
||||
"mega_introduce": "Mega强调用户隐私保护的云存储服务,提供端到端加密和大容量存储选项。",
|
||||
"opendrive_introduce": "OpenDrive提供文件备份、同步、共享及在线办公套件的全方位云存储解决方案。",
|
||||
"pcloud_introduce": "Pcloud是云存储服务商,提供跨平台文件访问、自动照片备份及音乐播放等功能。",
|
||||
"qingstor_introduce": "Qingstor是青云QingCloud的对象存储服务,具备高性能、高可靠性和丰富的数据处理能力。",
|
||||
"sftp_introduce": "SFTP是安全文件传输协议,基于SSH,提供加密的文件访问、传输和管理功能。",
|
||||
"yandex_introduce": "Yandex Disk是Yandex提供的云存储服务,支持文件同步、备份与分享,以及与Yandex生态系统的集成。",
|
||||
"crypt_introduce": "加密,确保用户数据在云端存储时的安全性。",
|
||||
"alias_introduce": "别名,便于访问和管理。",
|
||||
"alist_introduce":"一个支持多种存储的文件列表程序,使用 Gin 和 Solidjs。",
|
||||
"storage_name_already_exists":"存储名称已存在"
|
||||
|
||||
}
|
||||
82
src/controller/main.ts
Normal file
82
src/controller/main.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { invoke, process } from "@tauri-apps/api"
|
||||
import { nmConfig, readNmConfig, roConfig, saveNmConfig, setNmConfig } from "../services/config"
|
||||
import { rcloneInfo } from "../services/rclone"
|
||||
import { rclone_api_post } from "../utils/rclone/request"
|
||||
import { startUpdateCont } from "./stats/continue"
|
||||
import { reupMount } from "./storage/mount/mount"
|
||||
import { reupStorage } from "./storage/storage"
|
||||
import { listenWindow, windowsHide } from "./window"
|
||||
import { NMConfig } from "../type/config"
|
||||
import { randomString } from "../utils/utils"
|
||||
import { t } from "i18next"
|
||||
import { startRclone, stopRclone } from "../utils/rclone/process"
|
||||
import { getOsInfo } from "../utils/tauri/osInfo"
|
||||
import { startTaskScheduler } from "./task/task"
|
||||
import { autoMount } from "./task/autoMount"
|
||||
import { setThemeMode } from "./setting/setting"
|
||||
import { setLocalized } from "./language/localized"
|
||||
import { checkNotice } from "./update/notice"
|
||||
|
||||
async function init(setStartStr: Function) {
|
||||
|
||||
setStartStr(t('init'))
|
||||
|
||||
listenWindow()
|
||||
|
||||
await getOsInfo()
|
||||
|
||||
setStartStr(t('read_config'))
|
||||
await readNmConfig()
|
||||
|
||||
|
||||
if (nmConfig.settings.startHide) {
|
||||
windowsHide()
|
||||
}
|
||||
|
||||
if (nmConfig.settings.language) {
|
||||
await setLocalized(nmConfig.settings.language);
|
||||
} else {
|
||||
const matchingLang = roConfig.options.setting.language.select.find(
|
||||
(lang) => lang.langCode === navigator.language.toLowerCase()
|
||||
);
|
||||
nmConfig.settings.language = matchingLang?.value || roConfig.options.setting.language.select[roConfig.options.setting.language.defIndex].value;
|
||||
await setLocalized(nmConfig.settings.language);
|
||||
}
|
||||
|
||||
setThemeMode(nmConfig.settings.themeMode)
|
||||
|
||||
await checkNotice()
|
||||
|
||||
await startRclone()
|
||||
|
||||
startUpdateCont()
|
||||
await reupRcloneVersion()
|
||||
await reupStorage()
|
||||
await reupMount()
|
||||
|
||||
//自动挂载
|
||||
await autoMount()
|
||||
|
||||
//开始任务队列
|
||||
await startTaskScheduler()
|
||||
}
|
||||
|
||||
async function reupRcloneVersion() {
|
||||
const ver = await rclone_api_post(
|
||||
'/core/version',
|
||||
)
|
||||
rcloneInfo.version = ver
|
||||
console.log(rcloneInfo.version);
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
}
|
||||
|
||||
async function exit() {
|
||||
await stopRclone()
|
||||
await saveNmConfig()
|
||||
await process.exit();
|
||||
}
|
||||
|
||||
export { init, main, exit }
|
||||
45
src/controller/setting/setting.ts
Normal file
45
src/controller/setting/setting.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { nmConfig } from "../../services/config";
|
||||
|
||||
// 设置颜色模式
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {//监听
|
||||
nmConfig.settings.themeMode === 'auto' && setThemeMode(nmConfig.settings.themeMode)
|
||||
});
|
||||
function setThemeMode(mode: 'dark' | 'light' | 'auto' | string): void {
|
||||
const body = document.body;
|
||||
let isDarkMode: boolean = false;
|
||||
|
||||
switch (mode) {
|
||||
case 'dark':
|
||||
isDarkMode = true;
|
||||
break;
|
||||
case 'light':
|
||||
isDarkMode = false;
|
||||
break;
|
||||
case 'auto':
|
||||
isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// 根据模式设置页面主题和背景颜色
|
||||
if (isDarkMode) {
|
||||
document.body.setAttribute('arco-theme', 'dark');
|
||||
body.style.backgroundColor = "#2E2E2E";
|
||||
} else {
|
||||
document.body.removeAttribute('arco-theme');
|
||||
body.style.backgroundColor = "#FFFFFF";
|
||||
}
|
||||
}
|
||||
|
||||
//获取是否启用自启
|
||||
async function getAutostartState(): Promise<boolean> {
|
||||
return (await invoke('get_autostart_state')) as boolean;
|
||||
}
|
||||
|
||||
//设置自启
|
||||
async function setAutostartState(state: boolean): Promise<boolean> {
|
||||
return (await invoke('set_autostart_state',{enabled:state})) as boolean;
|
||||
}
|
||||
|
||||
export { setThemeMode ,getAutostartState,setAutostartState}
|
||||
18
src/controller/stats/continue.ts
Normal file
18
src/controller/stats/continue.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { reupStats } from "./stats";
|
||||
|
||||
|
||||
function startUpdateCont() {
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
await reupStats();
|
||||
} catch (error) {
|
||||
// 处理错误,例如记录日志或清理状态
|
||||
console.error('Error occurred while updating stats:', error);
|
||||
}
|
||||
}, 500); // 每n毫秒调用一次
|
||||
|
||||
// 返回清除定时器的函数,方便在需要停止更新时调用
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
|
||||
export { startUpdateCont }
|
||||
34
src/controller/stats/stats.ts
Normal file
34
src/controller/stats/stats.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { hooks } from "../../services/hook";
|
||||
import { rcloneInfo, rcloneStatsHistory } from "../../services/rclone";
|
||||
import { RcloneStats } from "../../type/rclone/stats";
|
||||
import { rclone_api_post } from "../../utils/rclone/request";
|
||||
|
||||
async function reupStats() {
|
||||
const stats: RcloneStats = await rclone_api_post(
|
||||
'/core/stats',
|
||||
)
|
||||
|
||||
let realSpeed: number = 0
|
||||
|
||||
if (stats.transferring && stats.transferring.length > 0) {
|
||||
stats.transferring.forEach(item => {
|
||||
realSpeed += item.speed
|
||||
})
|
||||
}
|
||||
|
||||
rcloneInfo.stats = {
|
||||
...stats,
|
||||
realSpeed: realSpeed
|
||||
}
|
||||
|
||||
//历史状态
|
||||
rcloneStatsHistory.push(stats)
|
||||
|
||||
if (rcloneStatsHistory.length > 32) {
|
||||
rcloneStatsHistory.splice(0, rcloneStatsHistory.length - 32);
|
||||
}
|
||||
|
||||
hooks.upStats()
|
||||
}
|
||||
|
||||
export { reupStats }
|
||||
52
src/controller/storage/create.ts
Normal file
52
src/controller/storage/create.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
import { DefaultParams } from "../../type/rclone/storage/defaults";
|
||||
import { rclone_api_post } from "../../utils/rclone/request";
|
||||
import { isEmptyObject } from "../../utils/utils";
|
||||
import { reupStorage } from "./storage";
|
||||
|
||||
|
||||
async function createStorage(name: string, type: string, parameters: object) {
|
||||
|
||||
const back = await rclone_api_post("/config/create", {
|
||||
"name": name,
|
||||
"type": type,
|
||||
"parameters": parameters
|
||||
})
|
||||
|
||||
reupStorage()
|
||||
|
||||
return isEmptyObject(back);
|
||||
}
|
||||
|
||||
//检查必填参数的合法性
|
||||
function checkParams(storageName: string, parameters: { [key: string]: any }, defaultParams: DefaultParams, t?: Function): { isOk: boolean, msg: string } {
|
||||
let isOk = true;
|
||||
let msg = '';
|
||||
|
||||
if (!t) {
|
||||
t = (v: string) => { return v }
|
||||
}
|
||||
|
||||
if (!storageName) {
|
||||
isOk = false;
|
||||
msg += `${t('storage_name_illegal')}`;
|
||||
}
|
||||
|
||||
if (isOk) {
|
||||
for (const param of defaultParams.required) {
|
||||
if (!parameters[param]) {
|
||||
isOk = false;
|
||||
msg += `${t('missing_parameter')}:${t(param)}`;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回结果前清理末尾的换行符,避免多余的空白
|
||||
msg = msg.trimEnd();
|
||||
|
||||
return { isOk: isOk, msg: msg };
|
||||
}
|
||||
|
||||
|
||||
export { createStorage, checkParams }
|
||||
250
src/controller/storage/listAll.ts
Normal file
250
src/controller/storage/listAll.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { StorageListAll } from "../../type/rclone/storage/storageListAll";
|
||||
import { aliasDefaults } from "./parameters/defaults/alias";
|
||||
import { alistDefaults } from "./parameters/defaults/alist";
|
||||
import { boxDefaults } from "./parameters/defaults/box";
|
||||
import { cryptDefaults } from "./parameters/defaults/crypt";
|
||||
import { dropboxDefaults } from "./parameters/defaults/dropbox";
|
||||
import { ftpDefaults } from "./parameters/defaults/ftp";
|
||||
import { googleCloudStorageDefaults } from "./parameters/defaults/googleCloudStorage";
|
||||
import { googleDriveDefaults } from "./parameters/defaults/googledrive";
|
||||
import { httpDefaults } from "./parameters/defaults/http";
|
||||
import { jottacloudDefaults } from "./parameters/defaults/jottacloud";
|
||||
import { localDefaults } from "./parameters/defaults/local";
|
||||
import { megaDefaults } from "./parameters/defaults/mega";
|
||||
import { onedriveDefaults } from "./parameters/defaults/onedrive";
|
||||
import { opendriveDefaults } from "./parameters/defaults/opendrive";
|
||||
import { pcloudDefaults } from "./parameters/defaults/pcloud";
|
||||
import { qingstorDefaults } from "./parameters/defaults/qingstor";
|
||||
import { s3Defaults } from "./parameters/defaults/s3";
|
||||
import { sftpDefaults } from "./parameters/defaults/sftp";
|
||||
import { swiftDefaults } from "./parameters/defaults/swift";
|
||||
import { webdavDefaults } from "./parameters/defaults/webdav";
|
||||
import { yandexDefaults } from "./parameters/defaults/yandex";
|
||||
|
||||
const storageListAll = [
|
||||
{
|
||||
name: 'Alist',
|
||||
type: 'webdav',
|
||||
displayType:'alist',
|
||||
introduce: 'alist_introduce',
|
||||
defaultParams: alistDefaults
|
||||
},
|
||||
{
|
||||
name: 'OneDrive',
|
||||
type: 'onedrive',
|
||||
introduce: 'onedrive_introduce',
|
||||
defaultParams: onedriveDefaults
|
||||
},
|
||||
{
|
||||
name: 'WebDav',
|
||||
type: 'webdav',
|
||||
introduce: 'Webdav_introduce',
|
||||
defaultParams: webdavDefaults
|
||||
}, {
|
||||
name: 'Google Drive',
|
||||
type: 'drive',
|
||||
introduce: 'googledrive_introduce',
|
||||
defaultParams: googleDriveDefaults
|
||||
},
|
||||
{
|
||||
name: 'Dropbox',
|
||||
type: 'dropbox',
|
||||
introduce: 'dropbox_introduce',
|
||||
defaultParams: dropboxDefaults
|
||||
},
|
||||
{
|
||||
name: 'S3 Object Storage',
|
||||
type: 's3',
|
||||
introduce: 's3_introduce',
|
||||
defaultParams: s3Defaults
|
||||
},
|
||||
{
|
||||
name: 'Google Cloud Storage',
|
||||
type: 'google cloud storage',
|
||||
introduce: 'googlecloudstorage_introduce',
|
||||
defaultParams: googleCloudStorageDefaults
|
||||
},
|
||||
{
|
||||
name: 'FTP',
|
||||
type: 'ftp',
|
||||
introduce: 'ftp_introduce',
|
||||
defaultParams: ftpDefaults
|
||||
},
|
||||
{
|
||||
name: 'Local Disk',
|
||||
type: 'local',
|
||||
introduce: 'local_introduce',
|
||||
defaultParams: localDefaults
|
||||
},
|
||||
{
|
||||
name: 'Box',
|
||||
type: 'box',
|
||||
introduce: 'box_introduce',
|
||||
defaultParams: boxDefaults
|
||||
},
|
||||
{
|
||||
name: 'HTTP',
|
||||
type: 'http',
|
||||
introduce: 'http_introduce',
|
||||
defaultParams: httpDefaults
|
||||
},
|
||||
{
|
||||
name: 'OpenStack Object Storage',
|
||||
type: 'swift',
|
||||
introduce: 'swift_introduce',
|
||||
defaultParams: swiftDefaults
|
||||
},
|
||||
{
|
||||
name: 'Pcloud',
|
||||
type: 'pcloud',
|
||||
introduce: 'pcloud_introduce',
|
||||
defaultParams: pcloudDefaults
|
||||
},
|
||||
{
|
||||
name: 'Qingstor',
|
||||
type: 'qingstor',
|
||||
introduce: 'qingstor_introduce',
|
||||
defaultParams: qingstorDefaults
|
||||
},
|
||||
{
|
||||
name: 'SFTP',
|
||||
type: 'sftp',
|
||||
introduce: 'sftp_introduce',
|
||||
defaultParams: sftpDefaults
|
||||
},
|
||||
{
|
||||
name: 'Yandex Disk',
|
||||
type: 'yandex',
|
||||
introduce: 'yandex_introduce',
|
||||
defaultParams: yandexDefaults
|
||||
},
|
||||
{
|
||||
name: 'Mega',
|
||||
type: 'mega',
|
||||
introduce: 'mega_introduce',
|
||||
defaultParams: megaDefaults
|
||||
},
|
||||
{
|
||||
name: 'OpenDrive',
|
||||
type: 'opendrive',
|
||||
introduce: 'opendrive_introduce',
|
||||
defaultParams: opendriveDefaults
|
||||
},
|
||||
{
|
||||
name: 'Jottacloud',
|
||||
type: 'jottacloud',
|
||||
introduce: 'jottacloud_introduce',
|
||||
defaultParams: jottacloudDefaults
|
||||
},
|
||||
{
|
||||
name: 'Crypt',
|
||||
type: 'crypt',
|
||||
introduce: 'crypt_introduce',
|
||||
defaultParams: cryptDefaults
|
||||
},
|
||||
{
|
||||
name: 'Alias',
|
||||
type: 'alias',
|
||||
introduce: 'alias_introduce',
|
||||
defaultParams: aliasDefaults
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
//根据标识返回StorageListAll
|
||||
function searchStorage(v: string | undefined, displayType: boolean=false): StorageListAll {
|
||||
for (const storageItem of storageListAll) {
|
||||
if(!displayType){
|
||||
if (( storageItem.introduce === v|| storageItem.name === v||storageItem.type === v )&&!storageItem.displayType) {
|
||||
return storageItem
|
||||
}
|
||||
}else{
|
||||
if (storageItem.introduce === v|| storageItem.name === v||storageItem.displayType === v ) {
|
||||
return storageItem
|
||||
}
|
||||
}
|
||||
}
|
||||
return storageListAll[0]
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 根据上面内容,生成s3声明及初始值(ts),以下面的格式:
|
||||
|
||||
|
||||
import { ParamsSelectType } from "../defaults";
|
||||
|
||||
interface OneDriveParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
region?: ParamsSelectType;
|
||||
}
|
||||
|
||||
|
||||
interface OneDriveParamsAdvanced {
|
||||
token?: string;
|
||||
auth_url?: string;
|
||||
token_url?: string;
|
||||
chunk_size?: string; // Assuming this to be a string for simplification
|
||||
drive_id?: string;
|
||||
drive_type?: ParamsSelectType;
|
||||
root_folder_id?: string;
|
||||
access_scopes?: string;
|
||||
disable_site_permission?: boolean;
|
||||
expose_onenote_files?: boolean;
|
||||
server_side_across_configs?: boolean;
|
||||
list_chunk?: number;
|
||||
no_versions?: boolean;
|
||||
link_scope?: string;
|
||||
link_type?: string;
|
||||
link_password?: string;
|
||||
hash_type?: string;
|
||||
av_override?: boolean;
|
||||
delta?: boolean;
|
||||
metadata_permissions?: string;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
const standard: OneDriveParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
region: {
|
||||
select: 'global',
|
||||
values: ['global', 'us', 'de', 'cn']
|
||||
},
|
||||
}
|
||||
|
||||
const advanced: OneDriveParamsAdvanced = {
|
||||
drive_type: { select: 'personal', values: ['personal', 'business','documentLibrary'] },
|
||||
token: "",
|
||||
auth_url: "",
|
||||
token_url: "",
|
||||
chunk_size: "10Mi",
|
||||
drive_id: "",
|
||||
root_folder_id: "",
|
||||
access_scopes: "Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access",
|
||||
disable_site_permission: false,
|
||||
expose_onenote_files: false,
|
||||
server_side_across_configs: false,
|
||||
list_chunk: 1000,
|
||||
no_versions: false,
|
||||
link_scope: "anonymous",
|
||||
link_type: "view",
|
||||
link_password: "",
|
||||
hash_type: "auto",
|
||||
av_override: false,
|
||||
delta: false,
|
||||
metadata_permissions: "off",
|
||||
encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,Del,Ctl,LeftSpace,LeftTilde,RightSpace,RightPeriod,InvalidUtf8,Dot",
|
||||
description: ""
|
||||
}
|
||||
|
||||
const onedriveDefaults: DefaultParams = {
|
||||
"name": "OneDrive",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['client_id', 'client_secret']
|
||||
}
|
||||
*/
|
||||
|
||||
export { storageListAll, searchStorage }
|
||||
110
src/controller/storage/mount/mount.ts
Normal file
110
src/controller/storage/mount/mount.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { invoke } from "@tauri-apps/api"
|
||||
import { nmConfig } from "../../../services/config"
|
||||
import { hooks } from "../../../services/hook"
|
||||
import { rcloneInfo } from "../../../services/rclone"
|
||||
import { MountListItem } from "../../../type/config"
|
||||
import { ParametersType } from "../../../type/rclone/storage/defaults"
|
||||
import { rclone_api_post } from "../../../utils/rclone/request"
|
||||
|
||||
|
||||
//列举存储
|
||||
async function reupMount(noRefreshUI?: boolean) {
|
||||
const mountPoints = (await rclone_api_post(
|
||||
'/mount/listmounts',
|
||||
)).mountPoints
|
||||
rcloneInfo.mountList = [];
|
||||
|
||||
if (mountPoints) {
|
||||
mountPoints.forEach((tiem: any) => {
|
||||
const name = tiem.Fs
|
||||
rcloneInfo.mountList.push({
|
||||
storageName: name.substring(0, name.length - 1),
|
||||
mountPath: tiem.MountPoint,
|
||||
mountedTime: new Date(tiem.MountedOn),
|
||||
})
|
||||
});
|
||||
}
|
||||
!noRefreshUI && hooks.upMount()
|
||||
}
|
||||
|
||||
function getMountStorage(mountPath: string): MountListItem | undefined {
|
||||
return nmConfig.mount.lists.find((item) => item.mountPath === mountPath)
|
||||
}
|
||||
|
||||
function isMounted(mountPath: string): boolean {
|
||||
return rcloneInfo.mountList.findIndex((item) => item.mountPath === mountPath) !== -1
|
||||
}
|
||||
|
||||
async function addMountStorage(storageName: string, mountPath: string, parameters: ParametersType, autoMount?: boolean) {
|
||||
|
||||
if (getMountStorage(mountPath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const mountInfo: MountListItem = {
|
||||
storageName: storageName,
|
||||
mountPath: mountPath,
|
||||
parameters: parameters,
|
||||
autoMount: (autoMount || false),
|
||||
}
|
||||
nmConfig.mount.lists.push(mountInfo)
|
||||
|
||||
await reupMount()
|
||||
}
|
||||
|
||||
async function delMountStorage(mountPath: string) {
|
||||
if (isMounted(mountPath)) {
|
||||
await unmountStorage(mountPath)
|
||||
}
|
||||
|
||||
nmConfig.mount.lists.forEach((item, index) => {
|
||||
if (item.mountPath === mountPath) {
|
||||
nmConfig.mount.lists.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
await reupMount()
|
||||
}
|
||||
|
||||
async function editMountStorage(mountInfo: MountListItem) {
|
||||
|
||||
await reupMount()
|
||||
rcloneInfo.mountList.forEach((item) => {
|
||||
if (item.mountPath === mountInfo.mountPath) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const index = nmConfig.mount.lists.findIndex((item) => item.mountPath === mountInfo.mountPath)
|
||||
|
||||
if (index !== -1) {
|
||||
nmConfig.mount.lists[index] = mountInfo
|
||||
}
|
||||
}
|
||||
|
||||
async function mountStorage(mountInfo: MountListItem) {
|
||||
|
||||
const back = await rclone_api_post('/mount/mount', {
|
||||
fs: mountInfo.storageName + ":",
|
||||
mountPoint: mountInfo.mountPath,
|
||||
...(mountInfo.parameters)
|
||||
})
|
||||
|
||||
await reupMount()
|
||||
return back
|
||||
}
|
||||
|
||||
async function unmountStorage(mountPath: string) {
|
||||
await rclone_api_post('/mount/unmount', {
|
||||
mountPoint: mountPath,
|
||||
})
|
||||
|
||||
await reupMount()
|
||||
}
|
||||
|
||||
async function getAvailableDriveLetter(): Promise<string> {
|
||||
return await invoke('get_available_drive_letter')//Z:
|
||||
}
|
||||
|
||||
|
||||
export { reupMount, mountStorage, unmountStorage, addMountStorage, delMountStorage, editMountStorage, getMountStorage, isMounted,getAvailableDriveLetter }
|
||||
67
src/controller/storage/mount/parameters/defaults.ts
Normal file
67
src/controller/storage/mount/parameters/defaults.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { MountOptions, VfsOptions } from "../../../../type/rclone/storage/mount/parameters";
|
||||
|
||||
|
||||
// 示例:初始化VfsOptions和MountOptions的默认值
|
||||
const defaultVfsConfig: VfsOptions = {
|
||||
ReadOnly: false,
|
||||
CacheMaxAge: 3600000000000,
|
||||
CacheMaxSize: -1,
|
||||
CacheMode: {
|
||||
select: 'full',
|
||||
values: [
|
||||
'off',
|
||||
'minimal',
|
||||
'writes',
|
||||
'full',
|
||||
]
|
||||
},
|
||||
CachePollInterval: 60000000000,
|
||||
CaseInsensitive: false,
|
||||
ChunkSize: 67108864,
|
||||
ChunkSizeLimit: -1,
|
||||
DirCacheTime: 300000000000,
|
||||
DirPerms: 511,
|
||||
FilePerms: 511,
|
||||
NoChecksum: false,
|
||||
NoModTime: false,
|
||||
NoSeek: false,
|
||||
PollInterval: 60000000000,
|
||||
ReadAhead: 0,
|
||||
ReadWait: 20000000,
|
||||
WriteBack: 5000000000,
|
||||
WriteWait: 1000000000,
|
||||
Refresh: false,
|
||||
BlockNormDupes: false,
|
||||
UsedIsSize: false,
|
||||
FastFingerprint: false,
|
||||
DiskSpaceTotalSize: -1,
|
||||
UID: 4294967295,
|
||||
GID: 4294967295,
|
||||
Umask: 0,
|
||||
|
||||
};
|
||||
|
||||
const defaultMountConfig: MountOptions = {
|
||||
VolumeName: '',
|
||||
AllowNonEmpty: false,
|
||||
AllowOther: false,
|
||||
AllowRoot: false,
|
||||
AsyncRead: true,
|
||||
AttrTimeout: 1000000000,
|
||||
Daemon: false,
|
||||
DaemonTimeout: 0,
|
||||
DebugFUSE: false,
|
||||
DefaultPermissions: true,
|
||||
ExtraFlags: [],
|
||||
ExtraOptions: [],
|
||||
MaxReadAhead: 1048576,
|
||||
NoAppleDouble: true,
|
||||
NoAppleXattr: false,
|
||||
WritebackCache: false,
|
||||
DaemonWait: 0,
|
||||
DeviceName: '',
|
||||
NetworkMode: false, //挂载为网络驱动器
|
||||
//CaseInsensitive: null,
|
||||
};
|
||||
|
||||
export { defaultVfsConfig, defaultMountConfig }
|
||||
21
src/controller/storage/parameters/defaults/alias.ts
Normal file
21
src/controller/storage/parameters/defaults/alias.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface AliasParamsStandard {
|
||||
remote: string;
|
||||
}
|
||||
|
||||
const standard: AliasParamsStandard = {
|
||||
remote: "",
|
||||
}
|
||||
|
||||
const advanced = {
|
||||
|
||||
}
|
||||
|
||||
const aliasDefaults: DefaultParams = {
|
||||
"name": "alias",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['remote']
|
||||
}
|
||||
export{aliasDefaults}
|
||||
24
src/controller/storage/parameters/defaults/alist.ts
Normal file
24
src/controller/storage/parameters/defaults/alist.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
import { webdavDefaults } from "./webdav";
|
||||
|
||||
const alistDefaults: DefaultParams = {
|
||||
...webdavDefaults,
|
||||
standard: {
|
||||
...webdavDefaults.standard,
|
||||
"url": "http://localhost:5244/dav",
|
||||
"user": "admin",
|
||||
},
|
||||
advanced: {
|
||||
...webdavDefaults.advanced,
|
||||
"vendor":webdavDefaults.standard.vendor
|
||||
},
|
||||
required: [
|
||||
...webdavDefaults.required,
|
||||
'user', 'pass'
|
||||
]
|
||||
}
|
||||
|
||||
//Reflect.deleteProperty(alistDefaults.standard, "vendor");
|
||||
delete alistDefaults.standard.vendor;
|
||||
|
||||
export { alistDefaults }
|
||||
30
src/controller/storage/parameters/defaults/box.ts
Normal file
30
src/controller/storage/parameters/defaults/box.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface BoxParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface BoxParamsAdvanced {
|
||||
upload_cutoff?: number;
|
||||
commit_retries?: number;
|
||||
}
|
||||
|
||||
const standard: BoxParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: ""
|
||||
}
|
||||
|
||||
const advanced: BoxParamsAdvanced = {
|
||||
upload_cutoff: 52428800,
|
||||
commit_retries: 100
|
||||
}
|
||||
|
||||
const boxDefaults: DefaultParams = {
|
||||
"name": "Box",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
required:['client_id','client_secret']
|
||||
}
|
||||
|
||||
export{boxDefaults}
|
||||
35
src/controller/storage/parameters/defaults/crypt.ts
Normal file
35
src/controller/storage/parameters/defaults/crypt.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
import { CryptParamsAdvanced, CryptParamsStandard } from "../../../../type/rclone/storage/parameters/crypt";
|
||||
|
||||
const standard: CryptParamsStandard = {
|
||||
remote: "",
|
||||
filename_encryption: {
|
||||
select:"standard",
|
||||
values:["standard","obfuscate","off"]
|
||||
},
|
||||
directory_name_encryption: true,
|
||||
password: "",
|
||||
password2: ""
|
||||
}
|
||||
|
||||
const advanced: CryptParamsAdvanced = {
|
||||
server_side_across_configs: false,
|
||||
show_mapping: false,
|
||||
no_data_encryption: false,
|
||||
pass_bad_blocks: false,
|
||||
strict_names: false,
|
||||
filename_encoding: {
|
||||
select:'base32',
|
||||
values:['base32','base64','base32768']
|
||||
},
|
||||
suffix: ".bin"
|
||||
}
|
||||
|
||||
const cryptDefaults: DefaultParams = {
|
||||
"name": "New_Storage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['remote', 'password']
|
||||
}
|
||||
|
||||
export{cryptDefaults}
|
||||
30
src/controller/storage/parameters/defaults/dropbox.ts
Normal file
30
src/controller/storage/parameters/defaults/dropbox.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface DropboxParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface DropboxParamsAdvanced {
|
||||
chunk_size?: number;
|
||||
impersonate?: string;
|
||||
}
|
||||
|
||||
const standard: DropboxParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
}
|
||||
|
||||
const advanced: DropboxParamsAdvanced = {
|
||||
chunk_size: 50331648, // 48MB
|
||||
impersonate: "",
|
||||
}
|
||||
|
||||
const dropboxDefaults: DefaultParams = {
|
||||
"name": "Dropbox",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
required:["client_id", "client_secret"]
|
||||
}
|
||||
|
||||
export{dropboxDefaults}
|
||||
40
src/controller/storage/parameters/defaults/ftp.ts
Normal file
40
src/controller/storage/parameters/defaults/ftp.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults"
|
||||
import { FtpParamsAdvanced, FtpParamsStandard } from "../../../../type/rclone/storage/parameters/ftp"
|
||||
|
||||
const standard: FtpParamsStandard = {
|
||||
host: "",
|
||||
user: "",
|
||||
port: 21,
|
||||
pass: "",
|
||||
tls: false,
|
||||
explicit_tls: false,
|
||||
}
|
||||
|
||||
const advanced: FtpParamsAdvanced = {
|
||||
concurrency: 4,
|
||||
no_check_certificate: false,
|
||||
disable_epsv: false,
|
||||
disable_mlsd: false,
|
||||
disable_utf8: false,
|
||||
writing_mdtm: false,
|
||||
force_list_hidden: false,
|
||||
idle_timeout: "1m",
|
||||
close_timeout: "1m",
|
||||
tls_cache_size: 16,
|
||||
disable_tls13: false,
|
||||
shut_timeout: "1m",
|
||||
ask_password: false,
|
||||
socks_proxy: "",
|
||||
encoding: "Slash,Del,Ctl,RightSpace,Dot", // rclone的编码设置根据主要的FTP服务器进行选择,如ProFTPd, PureFTPd, VsFTPd等
|
||||
description: "",
|
||||
}
|
||||
|
||||
const ftpDefaults: DefaultParams = {
|
||||
"name":"New_Storage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['host','port']
|
||||
}
|
||||
|
||||
|
||||
export {ftpDefaults}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { DefaultParams, ParamsSelectType } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
|
||||
interface GoogleCloudStorageParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
project_number?: string;
|
||||
user_project?: string;
|
||||
service_account_file?: string;
|
||||
service_account_credentials?: string;
|
||||
anonymous?: boolean;
|
||||
object_acl?: ParamsSelectType;
|
||||
bucket_acl?: ParamsSelectType;
|
||||
bucket_policy_only?: boolean;
|
||||
location?: string;
|
||||
storage_class?: ParamsSelectType;
|
||||
env_auth?: boolean;
|
||||
}
|
||||
|
||||
interface GoogleCloudStorageParamsAdvanced {
|
||||
token?: string;
|
||||
auth_url?: string;
|
||||
token_url?: string;
|
||||
directory_markers?: boolean;
|
||||
no_check_bucket?: boolean;
|
||||
decompress?: boolean;
|
||||
endpoint?: string;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: GoogleCloudStorageParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
project_number: "",
|
||||
user_project: "",
|
||||
service_account_file: "",
|
||||
service_account_credentials: "",
|
||||
anonymous: false,
|
||||
object_acl: {
|
||||
select: 'private',
|
||||
values: ['authenticatedRead', 'bucketOwnerFullControl', 'bucketOwnerRead', 'private', 'projectPrivate', 'publicRead']
|
||||
},
|
||||
bucket_acl: {
|
||||
select: 'private',
|
||||
values: ['authenticatedRead', 'private', 'projectPrivate', 'publicRead', 'publicReadWrite']
|
||||
},
|
||||
bucket_policy_only: false,
|
||||
location: "",
|
||||
storage_class: {
|
||||
select: 'STANDARD',
|
||||
values: ['MULTI_REGIONAL', 'REGIONAL', 'NEARLINE', 'COLDLINE', 'ARCHIVE', 'DURABLE_REDUCED_AVAILABILITY', 'STANDARD']
|
||||
},
|
||||
env_auth: false,
|
||||
}
|
||||
|
||||
const advanced: GoogleCloudStorageParamsAdvanced = {
|
||||
token: "",
|
||||
auth_url: "",
|
||||
token_url: "",
|
||||
directory_markers: false,
|
||||
no_check_bucket: false,
|
||||
decompress: false,
|
||||
endpoint: "",
|
||||
encoding: "Slash,CrLf,InvalidUtf8,Dot",
|
||||
description: ""
|
||||
}
|
||||
|
||||
const googleCloudStorageDefaults: DefaultParams = {
|
||||
"name": "GoogleCloudStorage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": []
|
||||
}
|
||||
|
||||
export{googleCloudStorageDefaults}
|
||||
74
src/controller/storage/parameters/defaults/googledrive.ts
Normal file
74
src/controller/storage/parameters/defaults/googledrive.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface GoogleDriveParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
scope?: string;
|
||||
root_folder_id?: string;
|
||||
service_account_file?: string;
|
||||
}
|
||||
|
||||
interface GoogleDriveParamsAdvanced {
|
||||
service_account_credentials?: string;
|
||||
team_drive?: string;
|
||||
auth_owner_only?: boolean;
|
||||
use_trash?: boolean;
|
||||
skip_gdocs?: boolean;
|
||||
shared_with_me?: boolean;
|
||||
trashed_only?: boolean;
|
||||
export_formats?: string;
|
||||
import_formats?: string;
|
||||
allow_import_name_change?: boolean;
|
||||
use_created_date?: boolean;
|
||||
list_chunk?: number;
|
||||
impersonate?: string;
|
||||
alternate_export?: boolean;
|
||||
upload_cutoff?: number;
|
||||
chunk_size?: number;
|
||||
acknowledge_abuse?: boolean;
|
||||
keep_revision_forever?: boolean;
|
||||
v2_download_min_size?: number;
|
||||
pacer_min_sleep?: number;
|
||||
pacer_burst?: number;
|
||||
}
|
||||
|
||||
const standard: GoogleDriveParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
scope: "",
|
||||
root_folder_id: "",
|
||||
service_account_file: ""
|
||||
}
|
||||
|
||||
const advanced: GoogleDriveParamsAdvanced = {
|
||||
service_account_credentials: "",
|
||||
team_drive: "",
|
||||
auth_owner_only: false,
|
||||
use_trash: true,
|
||||
skip_gdocs: false,
|
||||
shared_with_me: false,
|
||||
trashed_only: false,
|
||||
export_formats: "docx,xlsx,pptx,svg",
|
||||
import_formats: "",
|
||||
allow_import_name_change: false,
|
||||
use_created_date: false,
|
||||
list_chunk: 1000,
|
||||
impersonate: "",
|
||||
alternate_export: false,
|
||||
upload_cutoff: 8388608,
|
||||
chunk_size: 8388608,
|
||||
acknowledge_abuse: false,
|
||||
keep_revision_forever: false,
|
||||
v2_download_min_size: -1,
|
||||
pacer_min_sleep: 100000000,
|
||||
pacer_burst: 100
|
||||
}
|
||||
|
||||
const googleDriveDefaults: DefaultParams = {
|
||||
"name": "Google Drive",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
required:["client_id", "client_secret"]
|
||||
}
|
||||
|
||||
export{googleDriveDefaults}
|
||||
33
src/controller/storage/parameters/defaults/http.ts
Normal file
33
src/controller/storage/parameters/defaults/http.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
|
||||
interface HTTPParamsStandard {
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface HTTPParamsAdvanced {
|
||||
headers?: string; // Assuming this to be a string for simplification
|
||||
no_slash?: boolean;
|
||||
no_head?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: HTTPParamsStandard = {
|
||||
url: "", // This should be provided by the user since it's required
|
||||
}
|
||||
|
||||
const advanced: HTTPParamsAdvanced = {
|
||||
headers: "",
|
||||
no_slash: false,
|
||||
no_head: false,
|
||||
description: ""
|
||||
}
|
||||
|
||||
const httpDefaults: DefaultParams = {
|
||||
"name": "http",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['url']
|
||||
}
|
||||
|
||||
export{httpDefaults}
|
||||
46
src/controller/storage/parameters/defaults/jottacloud.ts
Normal file
46
src/controller/storage/parameters/defaults/jottacloud.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface JottacloudParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface JottacloudParamsAdvanced {
|
||||
token?: string;
|
||||
auth_url?: string;
|
||||
token_url?: string;
|
||||
md5_memory_limit?: string;
|
||||
trashed_only?: boolean;
|
||||
hard_delete?: boolean;
|
||||
upload_resume_limit?: string;
|
||||
no_versions?: boolean;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: JottacloudParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
}
|
||||
|
||||
const advanced: JottacloudParamsAdvanced = {
|
||||
token: "",
|
||||
auth_url: "",
|
||||
token_url: "",
|
||||
md5_memory_limit: "10Mi",
|
||||
trashed_only: false,
|
||||
hard_delete: false,
|
||||
upload_resume_limit: "10Mi",
|
||||
no_versions: false,
|
||||
encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,Del,Ctl,InvalidUtf8,Dot",
|
||||
description: "",
|
||||
}
|
||||
|
||||
const jottacloudDefaults: DefaultParams = {
|
||||
"name": "Jottacloud",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": [] // 根据实际情况调整必须的参数
|
||||
};
|
||||
|
||||
export{jottacloudDefaults}
|
||||
33
src/controller/storage/parameters/defaults/local.ts
Normal file
33
src/controller/storage/parameters/defaults/local.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
import { LocalParamsAdvanced, LocalParamsStandard } from "../../../../type/rclone/storage/parameters/local";
|
||||
|
||||
const standard: LocalParamsStandard = {
|
||||
// 对于本地文件系统,标准参数通常不需要
|
||||
}
|
||||
|
||||
const advanced: LocalParamsAdvanced = {
|
||||
nounc: false,
|
||||
copy_links: false,
|
||||
links: false,
|
||||
skip_links: false,
|
||||
zero_size_links: false,
|
||||
unicode_normalization: false,
|
||||
no_check_updated: false,
|
||||
one_file_system: false,
|
||||
case_sensitive: false,
|
||||
case_insensitive: false,
|
||||
no_preallocate: false,
|
||||
no_sparse: false,
|
||||
no_set_modtime: false,
|
||||
encoding: "Slash,Dot",
|
||||
description: "",
|
||||
}
|
||||
|
||||
const localDefaults: DefaultParams = {
|
||||
"name": "New_Storage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": []
|
||||
}
|
||||
|
||||
export { localDefaults };
|
||||
37
src/controller/storage/parameters/defaults/mega.ts
Normal file
37
src/controller/storage/parameters/defaults/mega.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface MegaParamsStandard {
|
||||
user?: string;
|
||||
pass?: string;
|
||||
}
|
||||
|
||||
interface MegaParamsAdvanced {
|
||||
debug?: boolean;
|
||||
hard_delete?: boolean;
|
||||
use_https?: boolean;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: MegaParamsStandard = {
|
||||
user: "",
|
||||
pass: ""
|
||||
};
|
||||
|
||||
const advanced: MegaParamsAdvanced = {
|
||||
debug: false,
|
||||
hard_delete: false,
|
||||
use_https: false,
|
||||
encoding: "Slash,InvalidUtf8,Dot",
|
||||
description: ""
|
||||
};
|
||||
|
||||
|
||||
const megaDefaults: DefaultParams = {
|
||||
"name": "Mega",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['user', 'pass']
|
||||
};
|
||||
|
||||
export{megaDefaults}
|
||||
47
src/controller/storage/parameters/defaults/onedrive.ts
Normal file
47
src/controller/storage/parameters/defaults/onedrive.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults"
|
||||
import { OneDriveParamsAdvanced, OneDriveParamsStandard } from "../../../../type/rclone/storage/parameters/onedrive"
|
||||
|
||||
|
||||
const standard: OneDriveParamsStandard = {
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
region: {
|
||||
select: 'global',
|
||||
values: ['global', 'us', 'de', 'cn']
|
||||
},
|
||||
}
|
||||
|
||||
const advanced: OneDriveParamsAdvanced = {
|
||||
drive_type: { select: 'personal', values: ['personal', 'business','documentLibrary'] },
|
||||
token: "",
|
||||
auth_url: "",
|
||||
token_url: "",
|
||||
chunk_size: "10Mi",
|
||||
drive_id: "",
|
||||
root_folder_id: "",
|
||||
access_scopes: "Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access",
|
||||
disable_site_permission: false,
|
||||
expose_onenote_files: false,
|
||||
server_side_across_configs: false,
|
||||
list_chunk: 1000,
|
||||
no_versions: false,
|
||||
link_scope: "anonymous",
|
||||
link_type: "view",
|
||||
link_password: "",
|
||||
hash_type: "auto",
|
||||
av_override: false,
|
||||
delta: false,
|
||||
metadata_permissions: "off",
|
||||
encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,Del,Ctl,LeftSpace,LeftTilde,RightSpace,RightPeriod,InvalidUtf8,Dot",
|
||||
description: ""
|
||||
}
|
||||
|
||||
const onedriveDefaults: DefaultParams = {
|
||||
"name": "New_Storage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['client_id', 'client_secret']
|
||||
}
|
||||
|
||||
|
||||
export { onedriveDefaults }
|
||||
33
src/controller/storage/parameters/defaults/opendrive.ts
Normal file
33
src/controller/storage/parameters/defaults/opendrive.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface OpenDriveParamsStandard {
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface OpenDriveParamsAdvanced {
|
||||
encoding?: string;
|
||||
chunk_size?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: OpenDriveParamsStandard = {
|
||||
username: "",
|
||||
password: ""
|
||||
};
|
||||
|
||||
const advanced: OpenDriveParamsAdvanced = {
|
||||
encoding: "Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,BackSlash,LeftSpace,LeftCrLfHtVt,RightSpace,RightCrLfHtVt,InvalidUtf8,Dot",
|
||||
chunk_size: "10Mi",
|
||||
description: ""
|
||||
};
|
||||
|
||||
|
||||
const opendriveDefaults: DefaultParams = {
|
||||
"name": "OpenDrive",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['username', 'password']
|
||||
};
|
||||
|
||||
export{opendriveDefaults}
|
||||
45
src/controller/storage/parameters/defaults/pcloud.ts
Normal file
45
src/controller/storage/parameters/defaults/pcloud.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface PcloudParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface PcloudParamsAdvanced {
|
||||
token?: string;
|
||||
auth_url?: string;
|
||||
token_url?: string;
|
||||
encoding?: string;
|
||||
root_folder_id?: string;
|
||||
hostname?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: PcloudParamsStandard = {
|
||||
client_id: '',
|
||||
client_secret: ''
|
||||
};
|
||||
|
||||
const advanced: PcloudParamsAdvanced = {
|
||||
token: '',
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
encoding: "Slash,BackSlash,Del,Ctl,InvalidUtf8,Dot",
|
||||
root_folder_id: "d0",
|
||||
hostname: "api.pcloud.com",
|
||||
username: '',
|
||||
password: '',
|
||||
description: ''
|
||||
};
|
||||
|
||||
|
||||
const pcloudDefaults: DefaultParams = {
|
||||
"name": "Pcloud",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
required: ["client_id", "client_secret"]
|
||||
};
|
||||
|
||||
export { pcloudDefaults };
|
||||
45
src/controller/storage/parameters/defaults/qingstor.ts
Normal file
45
src/controller/storage/parameters/defaults/qingstor.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface QingStorParamsStandard {
|
||||
env_auth?: boolean;
|
||||
access_key_id?: string;
|
||||
secret_access_key?: string;
|
||||
endpoint?: string;
|
||||
zone?: string;
|
||||
}
|
||||
|
||||
interface QingStorParamsAdvanced {
|
||||
connection_retries?: number;
|
||||
upload_cutoff?: string; // 假设 SizeSuffix 可以被表示为 string
|
||||
chunk_size?: string; // 假设 SizeSuffix 可以被表示为 string
|
||||
upload_concurrency?: number;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: QingStorParamsStandard = {
|
||||
env_auth: false,
|
||||
access_key_id: '',
|
||||
secret_access_key: '',
|
||||
endpoint: "https://qingstor.com:443",
|
||||
zone: "pek3a"
|
||||
};
|
||||
|
||||
const advanced: QingStorParamsAdvanced = {
|
||||
connection_retries: 3,
|
||||
upload_cutoff: "200Mi",
|
||||
chunk_size: "4Mi",
|
||||
upload_concurrency: 1,
|
||||
encoding: "Slash,Ctl,InvalidUtf8",
|
||||
description: ''
|
||||
};
|
||||
|
||||
|
||||
const qingstorDefaults: DefaultParams = {
|
||||
"name": "QingStor",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
required: ["access_key_id", "secret_access_key"]
|
||||
};
|
||||
|
||||
export { qingstorDefaults }
|
||||
80
src/controller/storage/parameters/defaults/s3.ts
Normal file
80
src/controller/storage/parameters/defaults/s3.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults"
|
||||
import { S3ParamsAdvanced, S3ParamsStandard } from "../../../../type/rclone/storage/parameters/s3"
|
||||
|
||||
|
||||
const standard: S3ParamsStandard = {
|
||||
provider: {
|
||||
select: 'Alibaba',
|
||||
values: [
|
||||
"AWS", "Alibaba", "ArvanCloud", "Ceph", "ChinaMobile", "Cloudflare",
|
||||
"DigitalOcean", "Dreamhost", "GCS", "HuaweiOBS", "IBMCOS", "IDrive",
|
||||
"IONOS", "LyveCloud", "Leviia", "Liara", "Linode", "Minio", "Netease",
|
||||
"Petabox", "RackCorp", "Rclone", "Scaleway", "SeaweedFS", "StackPath",
|
||||
"Storj", "Synology", "TencentCOS", "Wasabi", "Qiniu", "Other"
|
||||
]
|
||||
},
|
||||
env_auth: false,
|
||||
access_key_id: "",
|
||||
secret_access_key: "",
|
||||
region: "",
|
||||
endpoint: "",
|
||||
location_constraint: "",
|
||||
acl: "private",
|
||||
server_side_encryption: "",
|
||||
sse_kms_key_id: "",
|
||||
storage_class: "STANDARD",
|
||||
}
|
||||
|
||||
const advanced: S3ParamsAdvanced = {
|
||||
bucket_acl: "private",
|
||||
requester_pays: false,
|
||||
sse_customer_algorithm: "",
|
||||
sse_customer_key: "",
|
||||
sse_customer_key_base64: "",
|
||||
sse_customer_key_md5: "",
|
||||
upload_cutoff: "200Mi",
|
||||
chunk_size: "5Mi",
|
||||
max_upload_parts: 10000,
|
||||
copy_cutoff: "4.656Gi",
|
||||
disable_checksum: false,
|
||||
shared_credentials_file: "",
|
||||
profile: "",
|
||||
session_token: "",
|
||||
upload_concurrency: 4,
|
||||
force_path_style: true,
|
||||
v2_auth: false,
|
||||
use_dual_stack: false,
|
||||
use_accelerate_endpoint: false,
|
||||
leave_parts_on_error: false,
|
||||
list_chunk: 1000,
|
||||
list_version: 0,
|
||||
list_url_encode: { select: 'unset', values: ['true', 'false', 'unset'] },
|
||||
no_check_bucket: false,
|
||||
no_head: false,
|
||||
no_head_object: false,
|
||||
encoding: "Slash,InvalidUtf8,Dot",
|
||||
disable_http2: false,
|
||||
download_url: "",
|
||||
directory_markers: false,
|
||||
use_multipart_etag: { select: 'unset', values: ['true', 'false', 'unset'] },
|
||||
use_presigned_request: false,
|
||||
versions: false,
|
||||
version_at: "",
|
||||
version_deleted: false,
|
||||
decompress: false,
|
||||
might_gzip: { select: 'unset', values: ['true', 'false', 'unset'] },
|
||||
use_accept_encoding_gzip: { select: 'unset', values: ['true', 'false', 'unset'] },
|
||||
no_system_metadata: false,
|
||||
sts_endpoint: "",
|
||||
use_already_exists: { select: 'unset', values: ['true', 'false', 'unset'] },
|
||||
use_multipart_uploads: { select: 'unset', values: ['true', 'false', 'unset'] },
|
||||
description: "",
|
||||
}
|
||||
|
||||
const s3Defaults: DefaultParams = {
|
||||
"name": "New_Storage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['provider','access_key_id','secret_access_key']
|
||||
}
|
||||
export{s3Defaults}
|
||||
89
src/controller/storage/parameters/defaults/sftp.ts
Normal file
89
src/controller/storage/parameters/defaults/sftp.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface SFTPParamsStandard {
|
||||
host: string;
|
||||
user?: string;
|
||||
port?: number;
|
||||
pass?: string;
|
||||
key_pem?: string;
|
||||
key_file?: string;
|
||||
key_file_pass?: string;
|
||||
pubkey_file?: string;
|
||||
key_use_agent?: boolean;
|
||||
use_insecure_cipher?: boolean;
|
||||
disable_hashcheck?: boolean;
|
||||
ssh?: string;
|
||||
}
|
||||
|
||||
interface SFTPParamsAdvanced {
|
||||
known_hosts_file?: string;
|
||||
ask_password?: boolean;
|
||||
path_override?: string;
|
||||
set_modtime?: boolean;
|
||||
shell_type?: string;
|
||||
md5sum_command?: string;
|
||||
sha1sum_command?: string;
|
||||
skip_links?: boolean;
|
||||
subsystem?: string;
|
||||
server_command?: string;
|
||||
use_fstat?: boolean;
|
||||
disable_concurrent_reads?: boolean;
|
||||
disable_concurrent_writes?: boolean;
|
||||
idle_timeout?: string; // 假设 Duration 可以被表示为 string
|
||||
chunk_size?: string; // 假设 SizeSuffix 可以被表示为 string
|
||||
concurrency?: number;
|
||||
set_env?: string; // SpaceSepList 可以被表示为 string
|
||||
ciphers?: string; // SpaceSepList 可以被表示为 string
|
||||
key_exchange?: string; // SpaceSepList 可以被表示为 string
|
||||
macs?: string; // SpaceSepList 可以被表示为 string
|
||||
host_key_algorithms?: string; // SpaceSepList 可以被表示为 string
|
||||
socks_proxy?: string;
|
||||
copy_is_hardlink?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: SFTPParamsStandard = {
|
||||
host: "",
|
||||
user: "",
|
||||
port: 22,
|
||||
key_use_agent: false,
|
||||
use_insecure_cipher: false,
|
||||
disable_hashcheck: false
|
||||
};
|
||||
|
||||
const advanced: SFTPParamsAdvanced = {
|
||||
known_hosts_file: '',
|
||||
ask_password: false,
|
||||
path_override: '',
|
||||
set_modtime: true,
|
||||
shell_type: '',
|
||||
md5sum_command: '',
|
||||
sha1sum_command: '',
|
||||
skip_links: false,
|
||||
subsystem: "sftp",
|
||||
server_command: '',
|
||||
use_fstat: false,
|
||||
disable_concurrent_reads: false,
|
||||
disable_concurrent_writes: false,
|
||||
idle_timeout: "1m0s",
|
||||
chunk_size: "32Ki",
|
||||
concurrency: 64,
|
||||
set_env: '',
|
||||
ciphers: '',
|
||||
key_exchange: '',
|
||||
macs: '',
|
||||
host_key_algorithms: '',
|
||||
socks_proxy: '',
|
||||
copy_is_hardlink: false,
|
||||
description: ''
|
||||
};
|
||||
|
||||
|
||||
const sftpDefaults: DefaultParams = {
|
||||
"name": "SFTP",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['host']
|
||||
};
|
||||
|
||||
export{sftpDefaults}
|
||||
70
src/controller/storage/parameters/defaults/swift.ts
Normal file
70
src/controller/storage/parameters/defaults/swift.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface SwiftParamsStandard {
|
||||
env_auth?: boolean;
|
||||
user?: string;
|
||||
key?: string;
|
||||
auth?: string;
|
||||
user_id?: string;
|
||||
domain?: string;
|
||||
tenant?: string;
|
||||
tenant_id?: string;
|
||||
tenant_domain?: string;
|
||||
region?: string;
|
||||
storage_url?: string;
|
||||
auth_token?: string;
|
||||
application_credential_id?: string;
|
||||
application_credential_name?: string;
|
||||
application_credential_secret?: string;
|
||||
auth_version?: number;
|
||||
endpoint_type?: string;
|
||||
storage_policy?: string;
|
||||
}
|
||||
|
||||
interface SwiftParamsAdvanced {
|
||||
leave_parts_on_error?: boolean;
|
||||
chunk_size?: string;
|
||||
no_chunk?: boolean;
|
||||
no_large_objects?: boolean;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: SwiftParamsStandard = {
|
||||
env_auth: false,
|
||||
user: "",
|
||||
key: "",
|
||||
auth: "",
|
||||
user_id: "",
|
||||
domain: "",
|
||||
tenant: "",
|
||||
tenant_id: "",
|
||||
tenant_domain: "",
|
||||
region: "",
|
||||
storage_url: "",
|
||||
auth_token: "",
|
||||
application_credential_id: "",
|
||||
application_credential_name: "",
|
||||
application_credential_secret: "",
|
||||
auth_version: 0,
|
||||
endpoint_type: "public",
|
||||
storage_policy: "",
|
||||
};
|
||||
|
||||
const advanced: SwiftParamsAdvanced = {
|
||||
leave_parts_on_error: false,
|
||||
chunk_size: "5Gi",
|
||||
no_chunk: false,
|
||||
no_large_objects: false,
|
||||
encoding: "Slash,InvalidUtf8",
|
||||
description: "",
|
||||
};
|
||||
|
||||
const swiftDefaults: DefaultParams= {
|
||||
"name": "Swift",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": [] // 假设 'auth' 是必需的,具体根据实际情况调整
|
||||
};
|
||||
|
||||
export{swiftDefaults}
|
||||
32
src/controller/storage/parameters/defaults/webdav.ts
Normal file
32
src/controller/storage/parameters/defaults/webdav.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { DefaultParams, ParamsSelectType } from "../../../../type/rclone/storage/defaults";
|
||||
import { WebdavParamsAdvanced, WebdavParamsStandard } from "../../../../type/rclone/storage/parameters/webdav";
|
||||
|
||||
const standard: WebdavParamsStandard = {
|
||||
url: "",
|
||||
vendor: {
|
||||
select: "other",
|
||||
values: ["other", "fastmail", "nextcloud", "owncloud", "sharepoint", "sharepoint-ntlm", "rclone"]
|
||||
},
|
||||
user: "",
|
||||
pass: "",
|
||||
}
|
||||
|
||||
const advanced: WebdavParamsAdvanced = {
|
||||
bearer_token_command: "",
|
||||
encoding: "",
|
||||
headers: [],
|
||||
pacer_min_sleep: "",
|
||||
nextcloud_chunk_size: "",
|
||||
owncloud_exclude_shares: false,
|
||||
description: "",
|
||||
}
|
||||
|
||||
const webdavDefaults: DefaultParams = {
|
||||
"name":"New_Storage",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
"required": ['url']
|
||||
}
|
||||
|
||||
export { webdavDefaults }
|
||||
42
src/controller/storage/parameters/defaults/yandex.ts
Normal file
42
src/controller/storage/parameters/defaults/yandex.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DefaultParams } from "../../../../type/rclone/storage/defaults";
|
||||
|
||||
interface YandexParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
interface YandexParamsAdvanced {
|
||||
token?: string;
|
||||
auth_url?: string;
|
||||
token_url?: string;
|
||||
hard_delete?: boolean;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const standard: YandexParamsStandard = {
|
||||
client_id: '',
|
||||
client_secret: ''
|
||||
};
|
||||
|
||||
const advanced: YandexParamsAdvanced = {
|
||||
token: '',
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
hard_delete: false,
|
||||
encoding: "Slash,Del,Ctl,InvalidUtf8,Dot",
|
||||
description: ''
|
||||
};
|
||||
|
||||
|
||||
const yandexDefaults: DefaultParams = {
|
||||
"name": "Yandex",
|
||||
"standard": standard,
|
||||
"advanced": advanced,
|
||||
required: [
|
||||
"client_id",
|
||||
"client_secret"
|
||||
],
|
||||
};
|
||||
|
||||
export{yandexDefaults}
|
||||
165
src/controller/storage/storage.ts
Normal file
165
src/controller/storage/storage.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { invoke } from "@tauri-apps/api"
|
||||
import { hooks } from "../../services/hook"
|
||||
import { rcloneInfo } from "../../services/rclone"
|
||||
import { FileInfo } from "../../type/rclone/rcloneInfo"
|
||||
import { ParametersType } from "../../type/rclone/storage/defaults"
|
||||
import { rclone_api_post } from "../../utils/rclone/request"
|
||||
|
||||
//列举存储
|
||||
async function reupStorage() {
|
||||
const dump = await rclone_api_post(
|
||||
'/config/dump',
|
||||
)
|
||||
rcloneInfo.storageList = []
|
||||
for (const storageName in dump) {
|
||||
rcloneInfo.storageList.push({
|
||||
name: storageName,
|
||||
type: dump[storageName].type,
|
||||
})
|
||||
}
|
||||
hooks.upStorage()
|
||||
}
|
||||
|
||||
|
||||
//删除存储
|
||||
async function delStorage(name: string) {
|
||||
const del = await rclone_api_post(
|
||||
'/config/delete', {
|
||||
name: name
|
||||
})
|
||||
console.log(del);
|
||||
reupStorage()
|
||||
}
|
||||
|
||||
//获取存储
|
||||
async function getStorageParams(name: string): Promise<ParametersType> {
|
||||
const get = await rclone_api_post(
|
||||
'/config/get', {
|
||||
name: name
|
||||
})
|
||||
return get
|
||||
}
|
||||
|
||||
|
||||
//获取文件列表
|
||||
async function getFileList(storageName: string, path: string): Promise<FileInfo[]> {
|
||||
|
||||
const fileList = await rclone_api_post(
|
||||
'/operations/list', {
|
||||
fs: storageName + ':',
|
||||
remote: formatPathRclone(path, false)
|
||||
})
|
||||
return fileList.list
|
||||
}
|
||||
|
||||
//删除存储
|
||||
async function delFile(storageName: string, path: string, refreshCallback?: Function) {
|
||||
if (path.substring(0, 1) == '/') {
|
||||
path = path.substring(1, path.length)
|
||||
}
|
||||
const backData = await rclone_api_post(
|
||||
'/operations/deletefile', {
|
||||
fs: storageName + ':',
|
||||
remote: formatPathRclone(path, false)
|
||||
})
|
||||
if (refreshCallback) {
|
||||
refreshCallback()
|
||||
}
|
||||
}
|
||||
|
||||
async function delDir(storageName: string, path: string, refreshCallback?: Function) {
|
||||
|
||||
const backData = await rclone_api_post(
|
||||
'/operations/purge', {
|
||||
fs: storageName + ':',
|
||||
remote: formatPathRclone(path, true)
|
||||
})
|
||||
if (refreshCallback) {
|
||||
refreshCallback()
|
||||
}
|
||||
}
|
||||
|
||||
//创建目录
|
||||
async function mkDir(storageName: string, path: string, refreshCallback?: Function) {
|
||||
|
||||
|
||||
const backData = await rclone_api_post(
|
||||
'/operations/mkdir', {
|
||||
fs: storageName + ':',
|
||||
remote: formatPathRclone(path, true)
|
||||
})
|
||||
if (refreshCallback) {
|
||||
refreshCallback()
|
||||
}
|
||||
}
|
||||
|
||||
function formatPathRclone(path: string, isDir?: boolean): string {
|
||||
if (path.substring(0, 1) == '/') {
|
||||
path = path.substring(1, path.length)
|
||||
}
|
||||
if (isDir) {
|
||||
if (path.substring(path.length - 1, path.length) == '/') {
|
||||
path = path.substring(0, path.length - 1)
|
||||
} else {
|
||||
path = path + '/'
|
||||
}
|
||||
}
|
||||
|
||||
path = path.replace(/\/+/g, '/');
|
||||
return path;
|
||||
}
|
||||
|
||||
//copyFile
|
||||
async function copyFile(storageName: string, path: string, destStoragename: string, destPath: string, pathF2f: boolean = false) {//pathF2f:destPath为文件时需要设置为true。(默认false时为文件夹,文件名来自srcPath)
|
||||
const backData = await rclone_api_post(
|
||||
'/operations/copyfile', {
|
||||
srcFs: storageName + ':',
|
||||
srcRemote: formatPathRclone(path),
|
||||
dstFs: destStoragename + ':',
|
||||
dstRemote: formatPathRclone(destPath, !pathF2f) + (!pathF2f && getFileName(path))
|
||||
}, true)
|
||||
}
|
||||
|
||||
async function moveFile(storageName: string, path: string, destStoragename: string, destPath: string, newNmae?: string,pathF2f: boolean = false) {
|
||||
|
||||
const backData = await rclone_api_post(
|
||||
'/operations/movefile', {
|
||||
srcFs: storageName + ':',
|
||||
srcRemote: formatPathRclone(path),
|
||||
dstFs: destStoragename + ':',
|
||||
dstRemote: formatPathRclone(destPath, !pathF2f) + (!pathF2f && newNmae ? newNmae : getFileName(path))
|
||||
}, true)
|
||||
}
|
||||
|
||||
function getFileName(path: string): string {
|
||||
const pathArr = path.split('/')
|
||||
return pathArr[pathArr.length - 1]
|
||||
}
|
||||
|
||||
//copyDir
|
||||
async function copyDir(storageName: string, path: string, destStoragename: string, destPath: string) {
|
||||
const backData = await rclone_api_post(
|
||||
'/sync/copy', {
|
||||
srcFs: storageName + ':' + formatPathRclone(path, true),
|
||||
dstFs: destStoragename + ':' + formatPathRclone(destPath, true) + getFileName(path)
|
||||
}, true)
|
||||
}
|
||||
|
||||
async function moveDir(storageName: string, path: string, destStoragename: string, destPath: string, newNmae?: string) {
|
||||
const backData = await rclone_api_post(
|
||||
'/sync/move', {
|
||||
srcFs: storageName + ':' + formatPathRclone(path, true),
|
||||
dstFs: destStoragename + ':' + formatPathRclone(destPath, true) + (newNmae ? newNmae : getFileName(path))
|
||||
}, true)
|
||||
}
|
||||
|
||||
//sync,需完整path(pathF2f)
|
||||
async function sync(storageName: string, path: string, destStoragename: string, destPath: string, bisync?: boolean) {//bisync:双向同步
|
||||
const backData = await rclone_api_post(
|
||||
!bisync?'/sync/sync':'/sync/bisync', {
|
||||
srcFs: storageName + ':' + formatPathRclone(path, true),
|
||||
dstFs: destStoragename + ':' + formatPathRclone(destPath, true)
|
||||
}, true)
|
||||
}
|
||||
|
||||
export { reupStorage, delStorage, getStorageParams, getFileList, delFile, delDir, mkDir, formatPathRclone, copyFile, copyDir, moveFile, moveDir,sync }
|
||||
11
src/controller/task/autoMount.ts
Normal file
11
src/controller/task/autoMount.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { nmConfig } from "../../services/config";
|
||||
import { mountStorage } from "../storage/mount/mount";
|
||||
|
||||
async function autoMount() {
|
||||
nmConfig.mount.lists.forEach(async (item) => {
|
||||
item.autoMount && await mountStorage(item)
|
||||
})
|
||||
}
|
||||
|
||||
export{autoMount}
|
||||
|
||||
92
src/controller/task/runner.ts
Normal file
92
src/controller/task/runner.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { TaskListItem } from "../../type/config";
|
||||
import { copyDir, copyFile, delDir, delFile, moveDir, moveFile, sync } from "../storage/storage";
|
||||
|
||||
async function runTask(task: TaskListItem): Promise<TaskListItem> {
|
||||
let taskMsg = ''
|
||||
|
||||
const executeTask = (task: TaskListItem) => {
|
||||
console.log(`Executing ${task.taskType} task: ${task.name}`);
|
||||
|
||||
const srcIsDir = task.source.path.endsWith('/');
|
||||
const targetIsDir = task.target.path.endsWith('/');
|
||||
|
||||
/* if (item.isMove) {
|
||||
if (item.isDir) {
|
||||
moveDir(item.storageName, item.path, storageName!, path!);
|
||||
} else {
|
||||
moveFile(item.storageName, item.path, storageName!, path!);
|
||||
}
|
||||
} else {
|
||||
if (item.isDir) {
|
||||
copyDir(item.storageName, item.path, storageName!, path!);
|
||||
} else {
|
||||
copyFile(item.storageName, item.path, storageName!, path!)
|
||||
}
|
||||
} */
|
||||
|
||||
switch (task.taskType) {
|
||||
case 'copy': {//复制
|
||||
if (srcIsDir && targetIsDir) {//复制目录
|
||||
copyDir(task.source.storageName, task.source.path, task.target.storageName, task.target.path)
|
||||
} else if (!srcIsDir && !targetIsDir) {//复制文件
|
||||
copyFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path, true)
|
||||
} else if (!srcIsDir && targetIsDir) {//复制文件到目录
|
||||
copyFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path)
|
||||
} else {
|
||||
throw new Error('The directory cannot be copied/moved to a file');
|
||||
}
|
||||
break;
|
||||
};
|
||||
case 'move': {//移动
|
||||
if (srcIsDir && targetIsDir) {//移动目录
|
||||
moveDir(task.source.storageName, task.source.path, task.target.storageName, task.target.path)
|
||||
} else if (!srcIsDir && targetIsDir) {//移动文件到目录
|
||||
moveFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path)
|
||||
} else if (!srcIsDir && !targetIsDir) {//移动文件
|
||||
moveFile(task.source.storageName, task.source.path, task.target.storageName, task.target.path, undefined, true)
|
||||
} else {
|
||||
throw new Error('The directory cannot be copied/moved to a file');
|
||||
}
|
||||
break;
|
||||
};
|
||||
case 'delete': {//删除
|
||||
if (srcIsDir) {//删除目录
|
||||
delDir(task.source.storageName, task.source.path)
|
||||
} else {//删除文件
|
||||
delFile(task.source.storageName, task.source.path)
|
||||
}
|
||||
break;
|
||||
};
|
||||
case 'sync': {//同步
|
||||
sync(task.source.storageName, task.source.path, task.target.storageName, task.target.path)
|
||||
break;
|
||||
};
|
||||
case 'bisync': {//双向同步
|
||||
sync(task.source.storageName, task.source.path, task.target.storageName, task.target.path, true)
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Invalid task type');
|
||||
}
|
||||
}
|
||||
|
||||
//一次性任务,执行完毕后禁用
|
||||
if (task.run.mode === 'disposable') {
|
||||
task.enable = false;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (task.enable) {
|
||||
executeTask(task);
|
||||
task.runInfo = { ...task.runInfo, error: false, msg: taskMsg };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error executing task ${task.name}:`, error);
|
||||
task.runInfo = { ...task.runInfo, error: true, msg: taskMsg + (error instanceof Error ? error.message : String(error)) };
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
export { runTask }
|
||||
77
src/controller/task/scheduler.ts
Normal file
77
src/controller/task/scheduler.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { TaskListItem } from "../../type/config";
|
||||
import { runTask } from "./runner";
|
||||
import { delTask } from "./task";
|
||||
|
||||
class TaskScheduler {
|
||||
tasks: TaskListItem[];
|
||||
|
||||
constructor() {
|
||||
this.tasks = [];
|
||||
}
|
||||
|
||||
public async addTask(task: TaskListItem) {
|
||||
if (task.enable) {
|
||||
this.tasks.push(task);
|
||||
this.scheduleTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
private async scheduleTask(task: TaskListItem) {
|
||||
switch (task.run.mode) {
|
||||
case 'start':
|
||||
await this.executeTask(task);
|
||||
this.cancelTask(task.name);
|
||||
break;
|
||||
case 'disposable':
|
||||
await this.executeTask(task);
|
||||
this.cancelTask(task.name);
|
||||
delTask(task.name);
|
||||
break;
|
||||
case 'time':
|
||||
const executeTaskInterval = () => {
|
||||
const now = new Date();
|
||||
const scheduledTime = new Date(now);
|
||||
scheduledTime.setDate(scheduledTime.getDate() + task.run.time.intervalDays);
|
||||
scheduledTime.setHours(task.run.time.h, task.run.time.m, task.run.time.s);
|
||||
|
||||
// 如果设置的时间比现在的时间早,则表示下一次执行是明天
|
||||
if (scheduledTime < now) {
|
||||
scheduledTime.setDate(scheduledTime.getDate() + 1);
|
||||
}
|
||||
|
||||
const timeout = scheduledTime.getTime() - now.getTime();
|
||||
if (timeout >= 0) {
|
||||
task.run.runId = window.setTimeout(async () => {
|
||||
await this.executeTask(task);
|
||||
// 完成执行后,重新计划下一次执行
|
||||
executeTaskInterval();
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
executeTaskInterval();
|
||||
break;
|
||||
case 'interval':
|
||||
task.run.runId = window.setInterval(async () => await this.executeTask(task), task.run.interval);
|
||||
break;
|
||||
default:
|
||||
console.error('Invalid task mode:', task.run.mode);
|
||||
}
|
||||
}
|
||||
|
||||
public async executeTask(task: TaskListItem) {
|
||||
const updatedTask = await runTask(task)
|
||||
this.tasks = this.tasks.map(t => t.name === updatedTask.name ? updatedTask : t);
|
||||
}
|
||||
|
||||
cancelTask(taskName: string) {
|
||||
const task = this.tasks.find(t => t.name === taskName);
|
||||
if (task && task.run.runId !== undefined) {
|
||||
window.clearInterval(task.run.runId);
|
||||
window.clearTimeout(task.run.runId);
|
||||
task.run.runId = undefined
|
||||
console.log(`${taskName} task cancelled.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { TaskScheduler }
|
||||
39
src/controller/task/task.ts
Normal file
39
src/controller/task/task.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { nmConfig } from "../../services/config";
|
||||
import { TaskListItem } from "../../type/config";
|
||||
import { TaskScheduler } from "./scheduler";
|
||||
|
||||
const taskScheduler = new TaskScheduler()
|
||||
|
||||
function saveTask(taskInfo: TaskListItem) {
|
||||
const existingTaskIndex = nmConfig.task.findIndex(
|
||||
(task) => task.name === taskInfo.name
|
||||
);
|
||||
|
||||
if (existingTaskIndex !== -1) {
|
||||
// 存在同名任务,更新已有任务
|
||||
if(taskInfo.run.runId){taskScheduler.cancelTask(taskInfo.name)}
|
||||
nmConfig.task[existingTaskIndex] = taskInfo;
|
||||
} else {
|
||||
// 不存在同名任务,直接添加新任务
|
||||
nmConfig.task.push(taskInfo);
|
||||
}
|
||||
if(taskInfo.run.mode!=='start'){
|
||||
taskScheduler.addTask(taskInfo)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function delTask(taskName: string) {
|
||||
taskScheduler.cancelTask(taskName)
|
||||
nmConfig.task = nmConfig.task.filter((task) => task.name !== taskName);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function startTaskScheduler() {
|
||||
for (let task of nmConfig.task) {
|
||||
await taskScheduler.addTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
export { saveTask, delTask, taskScheduler, startTaskScheduler }
|
||||
62
src/controller/test.ts
Normal file
62
src/controller/test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { rclone_api_post } from "../utils/rclone/request";
|
||||
import { createStorage } from "./storage/create";
|
||||
import { getFileList, reupStorage } from "./storage/storage";
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { app } from "@tauri-apps/api";
|
||||
import { nmConfig, osInfo } from "../services/config";
|
||||
import { Aria2 } from "../utils/aria2/aria2";
|
||||
import { checkUpdate } from "./update/update";
|
||||
import { getWinFspInstallState, installWinFsp } from "../utils/utils";
|
||||
import { t } from "i18next";
|
||||
|
||||
export async function Test() {
|
||||
|
||||
|
||||
console.log(await rclone_api_post('/options/get'));
|
||||
|
||||
console.log(nmConfig);
|
||||
|
||||
|
||||
/* let data = await invoke('read_config_file') as any;
|
||||
console.log(data);
|
||||
console.log(await invoke('write_config_file', {
|
||||
configData: data
|
||||
})); */
|
||||
|
||||
|
||||
/* let taskids = (await rclone_api_post('/job/list')).jobids as Array<number>
|
||||
taskids.forEach(async (taskid) => {
|
||||
console.log(await rclone_api_post('/job/status', {
|
||||
jobid: taskid
|
||||
}));
|
||||
}) */
|
||||
/* console.log(await rclone_api_post('/operations/copyurl',{
|
||||
remote:'/hpm-od/QQNT_VirtualHotBar_9.9.7.21453_QQNT.HPM',
|
||||
fs:'Webdav:'
|
||||
})); */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* const aria2Test = new Aria2('https://down.hotpe.top/d/Package/HotPE-V2.7.240201.exe',
|
||||
'F:/',
|
||||
'test1.7z',
|
||||
8, (back) => {
|
||||
console.log(back);
|
||||
})
|
||||
|
||||
|
||||
|
||||
aria2Test.start() */
|
||||
//console.log(await runCmd('curl', [url,'-o', path]));
|
||||
|
||||
console.log(osInfo);
|
||||
|
||||
//await installWinFsp()
|
||||
//console.log(await getWinFspInstallState());
|
||||
|
||||
}
|
||||
14
src/controller/update/notice.ts
Normal file
14
src/controller/update/notice.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { nmConfig } from "../../services/config";
|
||||
import { Notice } from "../../type/controller/update";
|
||||
|
||||
|
||||
async function checkNotice() {
|
||||
const notice: Notice = (await (await fetch(nmConfig.api.url + '/GetNotice/?lang=' + nmConfig.settings.language)).json())
|
||||
if (notice.state === 'success') {
|
||||
if (nmConfig.notice === undefined || (nmConfig.notice && notice.data.content !== nmConfig.notice.data.content)) {
|
||||
nmConfig.notice = notice
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { checkNotice }
|
||||
23
src/controller/update/update.ts
Normal file
23
src/controller/update/update.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { fs } from "@tauri-apps/api";
|
||||
import { downloadFile, takeRightStr } from "../../utils/utils";
|
||||
import { ResItem, ResList } from "../../type/controller/update";
|
||||
import { nmConfig, osInfo } from "../../services/config";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { Modal } from "@arco-design/web-react";
|
||||
|
||||
|
||||
async function checkUpdate(updateCall: (resList: ResItem,localUpdateId: number) => void) {
|
||||
const localUpdateId = await getUpdateId()
|
||||
|
||||
const resList: ResItem = (await (await fetch(nmConfig.api.url + '/GetUpdate/?arch=' + osInfo.arch + '&osType=' + osInfo.osType)).json()).data
|
||||
|
||||
if (resList.id && Number(resList.id) < localUpdateId) {
|
||||
updateCall(resList,localUpdateId)
|
||||
}
|
||||
}
|
||||
|
||||
async function getUpdateId() {
|
||||
return Number(takeRightStr(await getVersion(), '-'))
|
||||
}
|
||||
|
||||
export { checkUpdate }
|
||||
38
src/controller/window.ts
Normal file
38
src/controller/window.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { appWindow } from "@tauri-apps/api/window"
|
||||
import { exit } from "./main";
|
||||
|
||||
function listenWindow() {
|
||||
appWindow.listen('tauri://close-requested', () => {
|
||||
windowsHide()
|
||||
})
|
||||
|
||||
// 阻止F5或Ctrl+R(Windows/Linux)和Command+R(Mac)刷新页面
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'F5' || (event.ctrlKey && event.key === 'r') || (event.metaKey && event.key === 'r')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
//禁止右键
|
||||
document.oncontextmenu = () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function windowsHide() {
|
||||
appWindow.hide()
|
||||
}
|
||||
|
||||
function windowsMini() {
|
||||
appWindow.minimize()
|
||||
}
|
||||
|
||||
listen('exit_app', async () => {
|
||||
await exit()
|
||||
});
|
||||
|
||||
export { listenWindow, windowsHide, windowsMini }
|
||||
|
||||
60
src/index.css
Normal file
60
src/index.css
Normal file
@@ -0,0 +1,60 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
p,h1,h2,h3,h4,h5,h6 {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.singe-line {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 字体 */
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("./assets/font/HarmonyOS_Sans_Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter-Bold";
|
||||
src: url("./assets/font/HarmonyOS_Sans_Bold.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'emoji';
|
||||
src: url('./assets/font/seguiemj.woff2') format('woff2');
|
||||
}
|
||||
|
||||
|
||||
/* 美化滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
width: 6px;
|
||||
background: rgba(#101F1C, 0.1);
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 147, 153, .5);
|
||||
background-clip: padding-box;
|
||||
min-height: 28px;
|
||||
-webkit-border-radius: 2em;
|
||||
-moz-border-radius: 2em;
|
||||
border-radius: 2em;
|
||||
transition: background-color .3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(144, 147, 153, .3);
|
||||
}
|
||||
47
src/main.tsx
Normal file
47
src/main.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './services/i18n'
|
||||
import './index.css'
|
||||
import { App } from './app'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { init } from './controller/main';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ConfigProvider, Spin } from '@arco-design/web-react';
|
||||
import { hooks } from './services/hook';
|
||||
import './controller/errorHandling'
|
||||
|
||||
|
||||
function StartPage() {
|
||||
const { t } = useTranslation()
|
||||
const [startStr, setStartStr] = useState('loading')
|
||||
|
||||
useEffect(() => {
|
||||
appStart(setStartStr)
|
||||
})
|
||||
|
||||
return <div style={{ textAlign: 'center', width: '100%', height: '100%', margin: '0px', padding: '0px', backgroundColor: 'var(--color-bg-1)' }} data-tauri-drag-region>
|
||||
<p style={{ paddingTop: '30%' }} data-tauri-drag-region>
|
||||
<Spin size={30} />
|
||||
<br />
|
||||
{t('starting') + ':' + startStr}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!)
|
||||
reactRoot.render(
|
||||
<StartPage></StartPage>
|
||||
)
|
||||
|
||||
let appStarting = false
|
||||
async function appStart(setStartStr: Function) {
|
||||
if (appStarting) { return }//避免重新执行
|
||||
appStarting = true
|
||||
1
|
||||
await init(setStartStr)//初始化功能
|
||||
|
||||
reactRoot.render(<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App></App>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>)//React.StrictMode:严格模式检查组件副作用
|
||||
}
|
||||
207
src/page/home/home.tsx
Normal file
207
src/page/home/home.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, { useEffect, useReducer, useState } from 'react'
|
||||
|
||||
import { Alert, Avatar, Button, Card, Descriptions, Grid, Link, Modal, Notification, Space, Typography } from "@arco-design/web-react"
|
||||
import { Test } from "../../controller/test"
|
||||
import { rcloneInfo } from '../../services/rclone'
|
||||
import { hooks } from '../../services/hook';
|
||||
import { checkUpdate } from '../../controller/update/update';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { shell } from '@tauri-apps/api';
|
||||
import { formatETA, formatSize } from '../../utils/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { nmConfig } from '../../services/config';
|
||||
import { IconCloud, IconList, IconSelectAll, IconStorage, IconSwap } from '@arco-design/web-react/icon';
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
const { Meta } = Card;
|
||||
|
||||
let checkedUpdate: boolean = false;
|
||||
|
||||
//checkedUpdate = true;
|
||||
|
||||
function Home_page() {
|
||||
const { t } = useTranslation()
|
||||
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [notification, noticeContextHolder] = Notification.useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
hooks.upStats = forceUpdate;
|
||||
console.log(nmConfig.notice);
|
||||
|
||||
if (nmConfig.notice && !nmConfig.notice.displayed && nmConfig.notice.data.content) {
|
||||
notification.info!({
|
||||
...(nmConfig.notice.data.title && { title: nmConfig.notice.data.title }),
|
||||
content: nmConfig.notice.data.content,
|
||||
...{ duration: nmConfig.notice.manual_close ? 1000*60*60*24*365 : 3000 },
|
||||
})
|
||||
nmConfig.notice.displayed = true
|
||||
}
|
||||
|
||||
if (!checkedUpdate) {
|
||||
checkUpdate(async (info) => {
|
||||
modal.confirm!({
|
||||
title: t('update_available'),
|
||||
content: <>
|
||||
{`${t('current_version')}:${await getVersion()} , ${t('latest_version')}:${info.name}`}
|
||||
<br />
|
||||
{t('goto_the_website_get_latest_version_ask')}
|
||||
</>,
|
||||
onOk: () => {
|
||||
shell.open(info.website!)
|
||||
},
|
||||
})
|
||||
})
|
||||
checkedUpdate = true;
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}{noticeContextHolder}
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
{/* <h1 style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>欢迎使用,统一管理和挂载云存储设施。</h1> */}
|
||||
<div style={{ textAlign: 'center', width: '100%' }}>
|
||||
<h1 style={{ fontSize: '2.0rem', fontWeight: 'bold', marginBottom: '1.0rem', marginTop: '0.8rem' }}>NetMount</h1>
|
||||
<span style={{ color: 'var(--color-text-2)', fontSize: '1.1rem' }}>{t('netmount_slogan')}</span>
|
||||
</div>
|
||||
{/*<Row >
|
||||
<Col flex={'auto'}style={{ paddingLeft: '0rem', paddingRight: '0rem' }} >
|
||||
<Card style={{padding:'1.5rem',textAlign:'center'}} bordered={false}>
|
||||
<span style={{fontSize:'4.5rem',fontFamily:'emoji'}}>🧐</span>
|
||||
<p style={{fontSize:'1rem',fontWeight:'bold'}}>初次使用,请点击下方按钮进行配置</p>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row> */}
|
||||
{/* <Card title='状态概览' size='small'>
|
||||
运行时间:{formatETA(rcloneInfo.stats.elapsedTime)}
|
||||
</Card> */}
|
||||
<div style={{ height: '1.5rem' }} />
|
||||
{rcloneInfo.storageList && !(rcloneInfo.storageList.length > 0) &&
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Alert style={{ maxWidth: '20rem', marginBottom: '1.0rem' }} type='info' content={
|
||||
<Row >
|
||||
<Col flex={'auto'} >
|
||||
<Typography.Ellipsis>{t('please_add_storage_tip')}</Typography.Ellipsis>
|
||||
</Col>
|
||||
<Col flex={'4rem'} style={{ textAlign: 'right' }}>
|
||||
<Link type='text' onClick={() => { hooks.navigate('/storage/manage/add') }}> {t('add')} </Link>
|
||||
</Col>
|
||||
</Row>
|
||||
} />
|
||||
</div>
|
||||
}
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Space style={{ height: '100%' }}>
|
||||
<Card style={{ width: '10rem', height: '6rem' }} hoverable >
|
||||
<strong ><IconCloud /> {t('storage')}</strong>({rcloneInfo.storageList.length})<br />
|
||||
<div style={{ paddingTop: '1.3rem', width: '100%', textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/storage/manage/add') }}> {t('add')} </Button>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/storage/manage') }}> {t('manage')} </Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ width: '10rem', height: '6rem' }} hoverable>
|
||||
<strong ><IconStorage /> {t('mount')}</strong>({rcloneInfo.mountList.length})
|
||||
<div style={{ paddingTop: '1.3rem', width: '100%', textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/mount/add') }} > {t('add')} </Button>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/mount') }} > {t('manage')} </Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ width: '10rem', height: '6rem' }} hoverable>
|
||||
<strong ><IconList /> {t('task')}</strong>({nmConfig.task.length})
|
||||
<div style={{ paddingTop: '1.3rem', width: '100%', textAlign: 'center' }}>
|
||||
<Space>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/task/add') }} >{t('add')} </Button>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/task') }}> {t('manage')} </Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
<br /><br />
|
||||
{/* <Card>
|
||||
存储和挂载概览
|
||||
<br />
|
||||
存储数:
|
||||
<br />
|
||||
挂载数:{nmConfig.mount.lists.length}
|
||||
<br />
|
||||
已挂载:{rcloneInfo.mountList.length}
|
||||
</Card> */}
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Card hoverable style={{ maxWidth: '40rem', width: '100%', marginLeft: '1rem', marginRight: '1rem', marginTop: '1rem' }}>
|
||||
|
||||
|
||||
|
||||
<Row >
|
||||
<Col flex={'1'} >
|
||||
<IconSwap style={{ transform: 'rotate(90deg)' }} /> {t('transmission_overview')}
|
||||
</Col>
|
||||
<Col flex={'1'} style={{ textAlign: 'right' }}>
|
||||
<Button type='text' onClick={() => { hooks.navigate('/transmit') }} >{t('view_more')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
|
||||
|
||||
<Descriptions style={{ marginTop: '0.8rem' }} colon=' :' data={[
|
||||
{
|
||||
label: t('speed'),
|
||||
value: `${formatSize(rcloneInfo.stats.realSpeed!)}/s`
|
||||
},
|
||||
|
||||
{
|
||||
label: t('size'),
|
||||
value: `${formatSize(rcloneInfo.stats.bytes)}/${formatSize(rcloneInfo.stats.totalBytes)}`
|
||||
},
|
||||
|
||||
...(rcloneInfo.stats.transferTime > 0 ? [
|
||||
{
|
||||
label: t('used_time'),
|
||||
value: formatETA(rcloneInfo.stats.transferTime)
|
||||
}
|
||||
] : []),
|
||||
...(Number(rcloneInfo.stats.eta) > 0 ? [
|
||||
{
|
||||
label: t('eta'),
|
||||
value: formatETA(rcloneInfo.stats.eta!)
|
||||
}
|
||||
] : []),
|
||||
...(rcloneInfo.stats.transferring && Number(rcloneInfo.stats.transferring.length) > 0 ? [
|
||||
{
|
||||
label: t('transferring'),
|
||||
value: rcloneInfo.stats.transferring.length
|
||||
}
|
||||
] : []),
|
||||
...(Number(rcloneInfo.stats.totalTransfers) > 0 ? [
|
||||
{
|
||||
label: t('transferred'),
|
||||
value: rcloneInfo.stats.totalTransfers
|
||||
}
|
||||
] : []),
|
||||
|
||||
]} />
|
||||
</Card>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
/* 软件名称:NetMount
|
||||
软件功能:挂载云存储到本地
|
||||
|
||||
主菜单(位于左边):首页(待实现),存储(添加存储,编辑存储,浏览和管理存储内文件),挂载存储(挂载为本地路径或盘符),传输(当前在传输的文件信息、速度、剩余时间等),任务(定时或间隔,可执行存储的文件同步、文件复制、文件删除、挂载等)
|
||||
|
||||
软件整体布局为左:主菜单,右:对应页面
|
||||
|
||||
现在就还有软件首页没有写了,请你为我的软件设计一个首页 */
|
||||
|
||||
export { Home_page }
|
||||
145
src/page/mount/add.tsx
Normal file
145
src/page/mount/add.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Button, Checkbox, Collapse, Form, Input, Notification, Select, Space, Switch } from '@arco-design/web-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ParametersType } from '../../type/rclone/storage/defaults';
|
||||
import { getProperties, getURLSearchParam } from '../../utils/utils';
|
||||
import { defaultMountConfig, defaultVfsConfig } from '../../controller/storage/mount/parameters/defaults';
|
||||
import { InputItem_module } from '../other/inputItem';
|
||||
import { rcloneInfo } from '../../services/rclone';
|
||||
import { addMountStorage, getAvailableDriveLetter, getMountStorage, mountStorage } from '../../controller/storage/mount/mount';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
const CollapseItem = Collapse.Item;
|
||||
|
||||
export default function AddMount_page() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate();
|
||||
const [storageName, setStorageName] = useState<string>()
|
||||
const [showAllOptions, setShowAllOptions] = useState(false)
|
||||
const [mountPath, setMountPath] = useState<string>('')
|
||||
const [autoMount, setAutoMount] = useState(true)
|
||||
//const [autoMountPath, setAutoMountPath] = useState(true)//自动分配盘符
|
||||
|
||||
const isWindows = rcloneInfo.version.os.toLowerCase().includes('windows');
|
||||
|
||||
|
||||
let parameters: ParametersType = { mountOpt: {}, vfsOpt: {} }
|
||||
|
||||
const setMountParams = (key: string, value: any) => {
|
||||
parameters.mountOpt[key] = value;
|
||||
};
|
||||
|
||||
const setVfsParams = (key: string, value: any) => {
|
||||
parameters.vfsOpt[key] = value;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (getURLSearchParam('name')) {
|
||||
setStorageName(getURLSearchParam('name'))
|
||||
} else if (!storageName && rcloneInfo.storageList.length > 0) {
|
||||
setStorageName(rcloneInfo.storageList[0].name)
|
||||
}
|
||||
|
||||
|
||||
if (isWindows) {
|
||||
setMountPath('*')
|
||||
} else {
|
||||
setMountPath('/netmount/' + storageName)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ fontSize: '1.5rem', marginBottom: '2rem', marginLeft: '1.8rem' }}>{t('add_mount')}</h2>
|
||||
<Form>
|
||||
<FormItem label={t('storage')}>
|
||||
<Select /* bordered={false} */ value={storageName} placeholder={t('please_select')} onChange={(value) =>
|
||||
setStorageName(value)
|
||||
}>
|
||||
{
|
||||
rcloneInfo.storageList.map((item) => {
|
||||
return (
|
||||
<Select.Option key={item.name} value={item.name}>{item.name}({item.type})</Select.Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label={t('mount_path')}>
|
||||
{
|
||||
isWindows ?
|
||||
<>
|
||||
{mountPath != '*' && <Input value={mountPath} onChange={(value) => setMountPath(value)} style={{ width: '12rem' }} placeholder={t('please_input')} />}
|
||||
<Checkbox checked={mountPath == '*'} onChange={(checked) => { checked ? setMountPath('*') : setMountPath('Z:') }} >{t('auto_drive_letter')}</Checkbox>
|
||||
</> : <>
|
||||
<Input value={mountPath} onChange={(value) => setMountPath(value)} placeholder={t('please_input')} />
|
||||
</>
|
||||
}
|
||||
</FormItem>
|
||||
|
||||
{!showAllOptions &&
|
||||
<FormItem label={t('mount_options')}>
|
||||
{isWindows && <Checkbox defaultChecked={!defaultMountConfig.NetworkMode} onChange={(checked) => { setMountParams('NetworkMode', !checked) }} >{t('simulate_hard_drive')}</Checkbox>}
|
||||
<Checkbox defaultChecked={defaultVfsConfig.ReadOnly} onChange={(checked) => { setVfsParams('ReadOnly', checked) }} >{t('read_only')}</Checkbox>
|
||||
</FormItem>
|
||||
}
|
||||
|
||||
{
|
||||
<div style={{ display: showAllOptions ? 'block' : 'none' }}>
|
||||
{
|
||||
getProperties(defaultMountConfig).map((item) => {
|
||||
return (
|
||||
<InputItem_module key={item.key} data={item} setParams={setMountParams} />
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
getProperties(defaultVfsConfig).map((item) => {
|
||||
return (
|
||||
<InputItem_module key={item.key} data={item} setParams={setVfsParams} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* 按钮 */}
|
||||
<div style={{ width: '100%', textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Checkbox defaultChecked={autoMount} onChange={(checked) => { setAutoMount(checked) }} >{t('auto_mount')}</Checkbox>
|
||||
{!showAllOptions && <Button onClick={() => { setShowAllOptions(!showAllOptions) }} type='text'>{t('show_all_options')}</Button>}
|
||||
<Button onClick={() => { navigate('/mount') }} >{t('step_back')}</Button>
|
||||
<Button disabled={!storageName || !mountPath} onClick={async () => {
|
||||
if (getMountStorage(mountPath)) {
|
||||
Notification.error({
|
||||
title: t('error'),
|
||||
content: t('mount_path_already_exists'),
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
let mountPathTemp = mountPath
|
||||
if (mountPath === "*") {
|
||||
mountPathTemp = await getAvailableDriveLetter()
|
||||
}
|
||||
|
||||
await addMountStorage(storageName!, mountPathTemp, parameters, autoMount)
|
||||
|
||||
if (await mountStorage(getMountStorage(mountPathTemp)!)) {
|
||||
Notification.success({
|
||||
title: t('success'),
|
||||
content: t('mount_storage_successfully'),
|
||||
})
|
||||
navigate('/mount')
|
||||
}
|
||||
|
||||
}} type='primary'>{t('mount')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
src/page/mount/mount.tsx
Normal file
103
src/page/mount/mount.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Alert, Button, Grid, Message, Space, Table, TableColumnProps, Typography } from '@arco-design/web-react'
|
||||
import React, { useEffect, useReducer, useState } from 'react'
|
||||
import { rcloneInfo } from '../../services/rclone'
|
||||
import { delMountStorage, isMounted, mountStorage, reupMount, unmountStorage } from '../../controller/storage/mount/mount'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { hooks } from '../../services/hook'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { nmConfig, osInfo } from '../../services/config'
|
||||
import { NoData_module } from '../other/noData'
|
||||
import { getWinFspInstallState, installWinFsp } from '../../utils/utils'
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
|
||||
function Mount_page() {
|
||||
const { t } = useTranslation()
|
||||
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件
|
||||
const navigate = useNavigate();
|
||||
const [winFspInstallState, setWinFspInstallState] = useState<boolean>();
|
||||
const [winFspInstalling, setWinFspInstalling] = useState<boolean>();
|
||||
|
||||
const columns: TableColumnProps[] = [
|
||||
{
|
||||
title: t('storage_name'),
|
||||
dataIndex: 'storageName',
|
||||
},
|
||||
{
|
||||
title: t('mount_path'),
|
||||
dataIndex: 'mountPath',
|
||||
},
|
||||
{
|
||||
title: t('mount_status'),
|
||||
dataIndex: 'mounted',
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
dataIndex: 'actions',
|
||||
align: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
const getWinFspState = async () => {
|
||||
console.log(await getWinFspInstallState());
|
||||
|
||||
setWinFspInstallState(await getWinFspInstallState())
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
hooks.upMount = forceUpdate
|
||||
if (osInfo.osType === 'Windows_NT' && rcloneInfo.endpoint.isLocal && winFspInstallState === undefined) {
|
||||
getWinFspState()
|
||||
}
|
||||
}, [ignored])
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%", }}>
|
||||
<div style={{ width: "100%", height: "2rem", }}>
|
||||
<Space>
|
||||
<Button onClick={() => { navigate('./add') }} type='primary'>{t('add')}</Button>
|
||||
<Button onClick={() => { reupMount() }}>{t('refresh')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ height: "calc(100% - 2rem)" }}>
|
||||
<br />
|
||||
{
|
||||
winFspInstallState !== undefined && !winFspInstallState && <>
|
||||
<Alert type='warning' content={t('winfsp_not_installed')} action={<>
|
||||
<Button type='primary' onClick={async ()=>{
|
||||
setWinFspInstalling(true)
|
||||
await installWinFsp().catch(()=>{
|
||||
Message.error(t('install_failed'))
|
||||
}).then(()=>{
|
||||
Message.success(t('install_success'))
|
||||
})
|
||||
setWinFspInstalling(false)
|
||||
await getWinFspState()
|
||||
}} loading={winFspInstalling}>{t('install')}</Button>
|
||||
</>}/>
|
||||
<br />
|
||||
</>
|
||||
}
|
||||
<Table style={{ height: "100%" }} noDataElement={<NoData_module />} columns={columns} pagination={false} data={
|
||||
nmConfig.mount.lists.map((item) => {
|
||||
const mounted = isMounted(item.mountPath)
|
||||
return {
|
||||
...item,
|
||||
mounted: mounted ? t('mounted') : t('unmounted'),
|
||||
actions: <Space>
|
||||
{
|
||||
mounted ? <>
|
||||
<Button onClick={() => { unmountStorage(item.mountPath) }} status='danger' >{t('unmount')}</Button>
|
||||
</> :
|
||||
<>
|
||||
<Button onClick={() => { delMountStorage(item.mountPath) }} status='danger' >{t('delete')}</Button>
|
||||
<Button onClick={() => { mountStorage(item) }} type='primary' >{t('mount')}</Button></>
|
||||
}
|
||||
</Space>
|
||||
}
|
||||
})} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export { Mount_page }
|
||||
16
src/page/other/devTips.tsx
Normal file
16
src/page/other/devTips.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Result } from '@arco-design/web-react'
|
||||
import { IconCodeBlock } from '@arco-design/web-react/icon'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function DevTips_module() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Result
|
||||
icon={<IconCodeBlock />}
|
||||
title={t('dev_tips')}
|
||||
></Result>
|
||||
)
|
||||
}
|
||||
export { DevTips_module }
|
||||
112
src/page/other/inputItem.tsx
Normal file
112
src/page/other/inputItem.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Checkbox, Form, Input, InputNumber, InputTag, Link, Message, Notification, Select, Space, Switch, Typography } from "@arco-design/web-react";
|
||||
import { rcloneInfo } from "../../services/rclone";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface InputItemProps {
|
||||
data: { key: any, value: any };
|
||||
setParams: (key: any, value: any) => void;
|
||||
}
|
||||
|
||||
function InputItem_module(props: InputItemProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let valueType: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' = typeof props.data.value;
|
||||
|
||||
if (valueType === 'string' && (props.data.value === 'true' || props.data.value === 'false')) {
|
||||
valueType = 'boolean';
|
||||
} else if (props.data.value == '[]' || (valueType === 'object' && props.data.value instanceof Array)) {
|
||||
valueType = 'array';
|
||||
}
|
||||
|
||||
const setParams = (value: any) => {
|
||||
console.log(value);
|
||||
|
||||
if (valueType == 'array' && props.data.key == 'headers') {
|
||||
props.setParams(props.data.key, value.join(','))
|
||||
} else {
|
||||
props.setParams(props.data.key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if (valueType == 'object' && props.data.value.select) {
|
||||
setParams(props.data.value.select)
|
||||
} else {
|
||||
setParams(props.data.value)
|
||||
}
|
||||
|
||||
const style: CSSProperties = {
|
||||
width: '85%'
|
||||
}
|
||||
|
||||
return <FormItem label={<div className="singe-line">{t(props.data.key)}</div>} title={props.data.key}>
|
||||
|
||||
{/* 输入框,string */
|
||||
valueType === 'string' &&
|
||||
<>{props.data.key.includes('pass') ?//密码
|
||||
<Input.Password style={style} allowClear key={props.data.key} defaultValue={props.data.value} onChange={(value) => setParams(value)} placeholder={t('please_input')} />
|
||||
: props.data.key === 'remote' ?//选择存储(remote:)
|
||||
<Select
|
||||
defaultValue={props.data.value}
|
||||
placeholder={t('please_select')}
|
||||
onChange={(value) => setParams(value)}
|
||||
style={style}
|
||||
>
|
||||
{rcloneInfo.storageList.map((item) => (
|
||||
<Select.Option key={item.name} value={item.name+':'}>
|
||||
{item.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
: <Input style={style} allowClear key={props.data.key} defaultValue={props.data.value} onChange={(value) => setParams(value)} placeholder={t('please_input')} />
|
||||
}</>
|
||||
}
|
||||
|
||||
{valueType === 'object' && props.data.value.select != null &&/* 选择器 */
|
||||
<Select
|
||||
style={style}
|
||||
defaultValue={props.data.value.select}
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
setParams(value)
|
||||
}}
|
||||
>
|
||||
{props.data.value.values.map((item: string, index: number) => (
|
||||
<Select.Option key={index} value={item}>{t(item)}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
|
||||
{valueType === 'number' &&/* 数字输入框 */
|
||||
<InputNumber
|
||||
mode='button'
|
||||
defaultValue={props.data.value}
|
||||
onChange={(value) => setParams(value)}
|
||||
style={style}
|
||||
/>}
|
||||
|
||||
{valueType === 'boolean' &&/* 开关 */
|
||||
<Switch defaultChecked={props.data.value} onChange={(value) => setParams(value)} />
|
||||
}
|
||||
|
||||
{valueType === 'array' && (
|
||||
<InputTag
|
||||
defaultValue={props.data.value}
|
||||
allowClear
|
||||
tokenSeparators={[',']}
|
||||
placeholder={t('Input_and_press_enter')}
|
||||
onChange={(value) => {
|
||||
setParams(value);
|
||||
}}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
|
||||
<br />
|
||||
|
||||
</FormItem>
|
||||
}
|
||||
|
||||
export { InputItem_module }
|
||||
18
src/page/other/noData.tsx
Normal file
18
src/page/other/noData.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Result } from '@arco-design/web-react'
|
||||
import { IconCodeBlock } from '@arco-design/web-react/icon'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
interface NoDataProps {
|
||||
tips?: string
|
||||
}
|
||||
|
||||
function NoData_module(props:NoDataProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Result
|
||||
title={t(props.tips||'no_data')}
|
||||
></Result>
|
||||
)
|
||||
}
|
||||
export { NoData_module }
|
||||
125
src/page/setting/setting.tsx
Normal file
125
src/page/setting/setting.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useEffect, useReducer, useState } from 'react'
|
||||
import { DevTips_module } from '../other/devTips'
|
||||
import { Button, Card, Collapse, Divider, Form, Grid, Link, Modal, Select, Space, Switch, Typography } from '@arco-design/web-react'
|
||||
import { Test } from '../../controller/test'
|
||||
import { nmConfig, roConfig } from '../../services/config';
|
||||
import { getAutostartState, setAutostartState, setThemeMode } from '../../controller/setting/setting';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { shell } from '@tauri-apps/api';
|
||||
import { rcloneInfo } from '../../services/rclone';
|
||||
import { setLocalized } from '../../controller/language/localized';
|
||||
const CollapseItem = Collapse.Item;
|
||||
const FormItem = Form.Item;
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
|
||||
export default function Setting_page() {
|
||||
const { t } = useTranslation()
|
||||
const [autostart, setAutostart] = useState<boolean>()
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件
|
||||
|
||||
const getAutostart = async () => {
|
||||
setAutostart(await getAutostartState());
|
||||
}
|
||||
|
||||
const showLog = (log: string) => {
|
||||
modal.info!({
|
||||
|
||||
title: t('log'),
|
||||
content: <div style={{ width: '100%', height: '100%', overflow: 'auto' }}>
|
||||
{log}
|
||||
</div>
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAutostart()
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{contextHolder}
|
||||
<Space direction='vertical' size='large' style={{ width: '100%' }}>
|
||||
<Card title={t('setting')} style={{}} size='small'>
|
||||
<Form autoComplete='off'>
|
||||
<FormItem label={t('language')}>
|
||||
<Select
|
||||
defaultValue={nmConfig.settings.language}
|
||||
onChange={async (value) => {
|
||||
nmConfig.settings.language = value
|
||||
await setLocalized(nmConfig.settings.language!)
|
||||
}}
|
||||
style={{ width: '8rem' }}
|
||||
>
|
||||
{roConfig.options.setting.language.select.map((item, index) => {
|
||||
return (
|
||||
<Select.Option key={index} value={item.value}>{item.name}</Select.Option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label={t('theme_mode')}>
|
||||
<Select
|
||||
defaultValue={nmConfig.settings.themeMode}
|
||||
onChange={(value) => {
|
||||
nmConfig.settings.themeMode = value;
|
||||
setThemeMode(value);
|
||||
}}
|
||||
style={{ width: '8rem' }}
|
||||
>
|
||||
{roConfig.options.setting.themeMode.select.map((item, index) => {
|
||||
return (
|
||||
<Select.Option key={index} value={item}>{t(`${item}_themeMode`)}</Select.Option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label={t('autostart')}>
|
||||
<Switch checked={autostart} onChange={async (value) => {
|
||||
await setAutostartState(value);
|
||||
setAutostart(value)
|
||||
}} />
|
||||
|
||||
</FormItem>
|
||||
<FormItem label={t('start_hide')}>
|
||||
<Switch checked={nmConfig.settings.startHide} onChange={async (value) => {
|
||||
nmConfig.settings.startHide = value
|
||||
forceUpdate()
|
||||
}} /></FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card title={t('about')} style={{}} size='small'>
|
||||
<Row >
|
||||
<Col flex={'auto'} >
|
||||
{t('about_text')}
|
||||
<br />
|
||||
{t('technology_stack')}:Tauri,TypeScript,Vite,React,Arco Design,Rust
|
||||
<br />
|
||||
Copyright © 2024-Present VirtualHotBar
|
||||
</Col>
|
||||
<Col flex={'10rem'} style={{ textAlign: 'right' }}>
|
||||
<Link onClick={() => { shell.open(roConfig.url.website) }}> NetMount </Link>
|
||||
<br />
|
||||
<Link onClick={() => { open(roConfig.url.website + 'page/license') }}> {t('licence')} </Link>
|
||||
<br />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Card title={t('components')} style={{}} size='small'>
|
||||
<Link onClick={() => { shell.open(roConfig.url.rclone) }}>Rclone</Link>(<Link onClick={() => {
|
||||
rcloneInfo.process.log && showLog(rcloneInfo.process.log)
|
||||
}}>{t('log')}</Link>): {rcloneInfo.version.version}
|
||||
<br />
|
||||
</Card>
|
||||
<Card title={t('tools')} style={{}} size='small'>
|
||||
<Button onClick={Test}>Test</Button>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
197
src/page/storage/add.tsx
Normal file
197
src/page/storage/add.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { Button, Checkbox, Form, Input, InputNumber, InputTag, Link, Message, Notification, Select, Space, Switch, Typography } from "@arco-design/web-react";
|
||||
import { DefaultParams, ParametersType } from "../../type/rclone/storage/defaults";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { searchStorage, storageListAll } from "../../controller/storage/listAll";
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { checkParams, createStorage } from "../../controller/storage/create";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getProperties, getURLSearchParam } from "../../utils/utils";
|
||||
import { getStorageParams } from "../../controller/storage/storage";
|
||||
import { InputItem_module } from "../other/inputItem";
|
||||
import { rcloneInfo } from "../../services/rclone";
|
||||
const FormItem = Form.Item;
|
||||
|
||||
|
||||
|
||||
function AddStorage_page() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate();
|
||||
|
||||
//const [selectStorage, setSelectStorage] = useState<string>()
|
||||
const [storageTypeName, setStorageTypeName] = useState<string>()
|
||||
const [defaultParams, setDefaultParams] = useState<DefaultParams>()
|
||||
const [step, setStep] = useState(0)//0:选择类型,1:填写参数
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
|
||||
const [storageName, setStorageName] = useState('')//存储名称
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
let parameters: ParametersType = {};
|
||||
|
||||
const setParams = (key: string, value: any) => {
|
||||
parameters[key] = value;
|
||||
};
|
||||
|
||||
const editMode = async () => {
|
||||
const type = getURLSearchParam('type')
|
||||
const name = getURLSearchParam('name')
|
||||
const storage = await getStorageParams(name)
|
||||
|
||||
//setSelectStorage(type)
|
||||
setStorageTypeName(searchStorage(type).name)
|
||||
setStorageName(name)
|
||||
|
||||
let defaultParamsEdit = searchStorage(type).defaultParams
|
||||
|
||||
|
||||
const overwriteParams = (params: ParametersType) => {
|
||||
getProperties(params).forEach((paramsItem) => {
|
||||
if (storage[paramsItem.key]) {
|
||||
const valueType = typeof params[paramsItem.key]
|
||||
if (valueType === 'object' && !(params[paramsItem.key] instanceof Array)) {
|
||||
if (params[paramsItem.key].values.includes(storage[paramsItem.key])) {
|
||||
params[paramsItem.key].select = storage[paramsItem.key]
|
||||
}
|
||||
} else {
|
||||
params[paramsItem.key] = storage[paramsItem.key];
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
overwriteParams(defaultParamsEdit.standard)
|
||||
overwriteParams(defaultParamsEdit.advanced)
|
||||
|
||||
setDefaultParams(defaultParamsEdit)
|
||||
|
||||
setStep(1)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditMode(Boolean(getURLSearchParam('edit')))
|
||||
if (isEditMode) {
|
||||
editMode()
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
return <>
|
||||
<h2 style={{ fontSize: '1.5rem', marginBottom: '2rem', marginLeft: '1.8rem' }}>{t('add_storage')}</h2>
|
||||
{step == 0 ?/* 选择类型 */
|
||||
<div className=" w-full h-full">
|
||||
<Form autoComplete='off'>
|
||||
<FormItem label={t('storage_type')}>
|
||||
<Select
|
||||
placeholder={t('please_select')}
|
||||
style={{ width: '15rem' }}
|
||||
value={storageTypeName && searchStorage(storageTypeName).name}
|
||||
onChange={(value) => {
|
||||
setStorageTypeName(value)
|
||||
const storageInfo = searchStorage(value)
|
||||
//setSelectStorage(storageInfo.type)
|
||||
setDefaultParams(storageInfo.defaultParams)
|
||||
setStorageName(storageInfo.defaultParams.name)
|
||||
}}
|
||||
>
|
||||
{storageListAll.map((storageItem, index) => (
|
||||
<Select.Option key={index} value={storageItem.name}>
|
||||
{t(storageItem.name)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
|
||||
{/* 存储介绍 */}
|
||||
{storageTypeName ? <FormItem label={t('storage_introduce')}>
|
||||
<Typography.Text>{t(searchStorage(storageTypeName).introduce!)}</Typography.Text>
|
||||
</FormItem> : ''}
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
{/* 按钮 */}
|
||||
<div style={{ width: '100%', textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => { navigate('/storage/manage') }} >{t('step_back')}</Button>
|
||||
<Button onClick={() => { setStep(1) }} disabled={!storageTypeName} type='primary'>{t('step_next')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
: step == 1 ?/* 填写参数 */
|
||||
<div className=" w-full h-full">
|
||||
<Form autoComplete='off'>
|
||||
<InputItem_module data={{ key: 'StorageName', value: storageName }} setParams={(key: any, value: any) => { key && setStorageName(value) }} />
|
||||
|
||||
{
|
||||
getProperties(defaultParams!.standard).map((paramsItem) => {
|
||||
return (
|
||||
<InputItem_module key={paramsItem.key} data={paramsItem} setParams={setParams} />
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div style={{ display: showAdvanced ? 'block' : 'none' }}>
|
||||
|
||||
{//高级选项
|
||||
getProperties(defaultParams!.advanced).map((paramsItem) => {
|
||||
return (
|
||||
<InputItem_module key={paramsItem.key} data={paramsItem} setParams={setParams} />
|
||||
)
|
||||
})}
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
<br />
|
||||
<div style={{ width: '100%', textAlign: 'right' }}>
|
||||
<Space>
|
||||
{
|
||||
//高级选项
|
||||
!showAdvanced &&
|
||||
<Button onClick={() => setShowAdvanced(true)} type='text'>{t('show_advanced_options')} </Button>
|
||||
}
|
||||
<Button onClick={() => { getURLSearchParam('edit') ? navigate('/storage/manage') : setStep(0) }}>{t('step_back')}</Button>
|
||||
<Button onClick={async () => {
|
||||
console.log(storageName, parameters);
|
||||
if (!isEditMode) {
|
||||
for (const storage of rcloneInfo.storageList) {
|
||||
if (storage.name === storageName) {
|
||||
Message.error(t('storage_name_already_exists'))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const { isOk, msg } = checkParams(storageName, parameters, searchStorage(storageTypeName).defaultParams, t)
|
||||
if (isOk) {
|
||||
if (await createStorage(storageName, searchStorage(storageTypeName).type, parameters)) {
|
||||
Notification.success({
|
||||
title: t('success'),
|
||||
content: t('Storage_added_successfully'),
|
||||
})
|
||||
navigate('/storage/manage')
|
||||
} else {
|
||||
Notification.error({
|
||||
title: t('error'),
|
||||
content: t('Storage_added_failed'),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Message.error(msg)
|
||||
}
|
||||
}
|
||||
} type='primary'>{t('save')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
: ''
|
||||
}</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { AddStorage_page }
|
||||
370
src/page/storage/explorer.tsx
Normal file
370
src/page/storage/explorer.tsx
Normal file
@@ -0,0 +1,370 @@
|
||||
import React, { CSSProperties, useEffect, useReducer, useState } from 'react'
|
||||
import { BackTop, Badge, Button, Divider, Dropdown, Grid, Input, Link, List, Menu, Message, Modal, Notification, Popconfirm, Select, Space, Spin, Table, TableColumnProps, Tabs, Tooltip, Typography, Upload } from '@arco-design/web-react';
|
||||
import { IconCopy, IconDelete, IconEdit, IconFolderAdd, IconLeft, IconMore, IconPaste, IconRefresh, IconScissor, IconUpCircle, IconUpload } from '@arco-design/web-react/icon';
|
||||
import { rcloneInfo } from '../../services/rclone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copyDir, copyFile, delDir, delFile, formatPathRclone, getFileList, mkDir, moveDir, moveFile } from '../../controller/storage/storage';
|
||||
import { FileInfo } from '../../type/rclone/rcloneInfo';
|
||||
import { formatSize, getURLSearchParam } from '../../utils/utils';
|
||||
import { rcloneApiHeaders } from '../../utils/rclone/request';
|
||||
import { RequestOptions } from '@arco-design/web-react/es/Upload';
|
||||
import { NoData_module } from '../other/noData';
|
||||
import { clipListItem } from '../../type/page/storage/explorer';
|
||||
import { searchStorage } from '../../controller/storage/listAll';
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
const TabPane = Tabs.TabPane;
|
||||
const tipsStyle: CSSProperties = {
|
||||
textAlign: 'center',
|
||||
paddingTop: '6rem',
|
||||
fontSize: '1rem'
|
||||
};
|
||||
|
||||
function Explorer_page() {
|
||||
return (
|
||||
<>
|
||||
{/* <Tabs defaultActiveTab='1'>
|
||||
<TabPane key='1' title='Tab 1'>
|
||||
<ExplorerItem />
|
||||
</TabPane>
|
||||
</Tabs> */}
|
||||
<ExplorerItem />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// 规范路径
|
||||
const sanitizePath = (newPath: string): string => {
|
||||
if (!newPath.startsWith('/')) {
|
||||
newPath = '/' + newPath;
|
||||
}
|
||||
|
||||
// 确保路径不以 / 结尾,如果不是根路径
|
||||
if (newPath !== '/' && newPath.endsWith('/')) {
|
||||
newPath = newPath.slice(0, -1);
|
||||
}
|
||||
|
||||
return newPath;
|
||||
};
|
||||
|
||||
//取父目录
|
||||
const getParentPath = (currentPath: string): string => {
|
||||
// 如果路径为空或者只有一个"/",则无上级目录
|
||||
if (currentPath === '/' || currentPath === '') {
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
// 找到最后一个"/"出现的位置
|
||||
const lastSlashIndex = currentPath.lastIndexOf('/');
|
||||
|
||||
// 返回截取到倒数第二个"/"之前的路径作为上级目录
|
||||
return currentPath.substring(0, lastSlashIndex);
|
||||
};
|
||||
|
||||
|
||||
function ExplorerItem() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [storageName, setStorageName] = useState<string>()
|
||||
const [path, setPath] = useState<string>()
|
||||
const [pathTemp, setPathTemp] = useState<string>('')
|
||||
//const [selectedRowKeys, setSelectedRowKeys] = useState<Array<string | number>>([]);
|
||||
|
||||
const [fileList, setFileInfo] = useState<Array<FileInfo>>()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [clipList, setClipList] = useState<Array<clipListItem>>([])
|
||||
|
||||
|
||||
const columns: TableColumnProps[] = [
|
||||
{
|
||||
title: t('name'),
|
||||
dataIndex: 'fileName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: t('modified_time'),
|
||||
dataIndex: 'fileModTime',
|
||||
ellipsis: true,
|
||||
width: '10.5rem',
|
||||
},
|
||||
{
|
||||
title: t('size'),
|
||||
dataIndex: 'fileSize',
|
||||
ellipsis: true,
|
||||
width: '7rem',
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
dataIndex: 'actions',
|
||||
align: 'right',
|
||||
width: '10rem',
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
//刷新文件列表
|
||||
async function fileInfo() {
|
||||
setLoading(true)
|
||||
const l = await getFileList(storageName!, path!)
|
||||
setLoading(false)
|
||||
setFileInfo(l)
|
||||
}
|
||||
|
||||
// 创建一个自定义函数用于更新路径,确保路径始终符合规范
|
||||
const updatePath = (newPath: string) => {
|
||||
const sanitizedPath = sanitizePath(newPath);
|
||||
setPath(sanitizedPath);
|
||||
setPathTemp(sanitizedPath)
|
||||
};
|
||||
|
||||
//剪贴板去重
|
||||
const isClipHave = (storageName_: string, path: string): boolean => {
|
||||
return clipList.findIndex(v => v.storageName === storageName_ && v.path === path) !== -1;
|
||||
};
|
||||
const addCilp = (clip: clipListItem) => {
|
||||
if (isClipHave(clip.storageName, clip.path)) return;
|
||||
setClipList([...clipList, { isMove: clip.isMove, storageName: clip.storageName, path: clip.path, isDir: clip.isDir }])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
//页面加载时,从URL中获取存储名称和路径
|
||||
if (getURLSearchParam('name')) {
|
||||
setStorageName(getURLSearchParam('name'))
|
||||
/* if (getURLSearchParam('path')) {
|
||||
setPath(getURLSearchParam('path'))
|
||||
} */
|
||||
}
|
||||
|
||||
|
||||
if (!storageName && !getURLSearchParam('name')&& rcloneInfo.storageList.length > 0) {
|
||||
setStorageName(rcloneInfo.storageList[0].name)
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (storageName && path) {
|
||||
fileInfo();
|
||||
}
|
||||
}, [path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (storageName && !path) {
|
||||
updatePath('/')
|
||||
setPathTemp('/')
|
||||
}
|
||||
|
||||
if (storageName && path) {
|
||||
fileInfo();
|
||||
}
|
||||
}, [storageName])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
}, [clipList])
|
||||
|
||||
function MakeDir() {
|
||||
let dirNameTemp = ''
|
||||
if (storageName && path) {
|
||||
modal.info!({
|
||||
title: t('create_directory'),
|
||||
icon: null,
|
||||
content: <Input placeholder={t('please_input')} defaultValue={dirNameTemp} onChange={(value) => dirNameTemp = value} />,
|
||||
onOk: async () => {
|
||||
dirNameTemp ? await mkDir(storageName, path + '/' + dirNameTemp, fileInfo) : Message.error(t('dir_name_cannot_empty'))
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function UploadFile() {
|
||||
|
||||
const customRequest = (option: RequestOptions) => {
|
||||
const { onProgress, onError, onSuccess, file } = option;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.onprogress = ({ lengthComputable, loaded, total }) => {
|
||||
if (lengthComputable) {
|
||||
console.log(Math.round(loaded / total * 100));
|
||||
onProgress(Math.round(loaded / total * 100));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
xhr.status === 200 ? onSuccess() : onError(xhr);
|
||||
};
|
||||
|
||||
xhr.onerror = () => onError(xhr);
|
||||
|
||||
xhr.open('POST', `${rcloneInfo.endpoint.url}/operations/uploadfile?fs=${storageName}:&remote=${formatPathRclone(path!, false)}`, true);
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${rcloneApiHeaders.Authorization}`);
|
||||
xhr.send(formData);
|
||||
};
|
||||
|
||||
if (storageName && path) {
|
||||
modal.info!({
|
||||
title: t('upload_file'),
|
||||
icon: null,
|
||||
content: <>
|
||||
<Upload drag customRequest={customRequest} ></Upload></>,
|
||||
onOk: fileInfo,
|
||||
onCancel: fileInfo
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function fileRename(filePath: string, isDir: boolean) {
|
||||
console.log(getParentPath(filePath));
|
||||
|
||||
let nameTemp = filePath.split('/').pop()!;
|
||||
modal.info!({
|
||||
title: t('rename'),
|
||||
icon: null,
|
||||
content: <Input placeholder={t('please_input')} defaultValue={nameTemp} onChange={(value) => nameTemp = value} />,
|
||||
onOk: async () => {
|
||||
if (nameTemp) {
|
||||
isDir ? await moveDir(storageName!, filePath, storageName!, getParentPath(filePath), nameTemp) :
|
||||
await moveFile(storageName!, filePath, storageName!, getParentPath(filePath), nameTemp);
|
||||
fileInfo();
|
||||
} else {
|
||||
Message.error(t('name_cannot_empty'))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<div style={{ width: "100%", height: "2rem", }}>
|
||||
{contextHolder}
|
||||
<Row >
|
||||
<Col flex='2rem'>
|
||||
<Button /* type='secondary' */ icon={<IconLeft />} onClick={() => { updatePath(getParentPath(path!)) }} disabled={!storageName} type='text' title={t('parent_directory')} />
|
||||
</Col>
|
||||
<Col flex='2rem'>
|
||||
<Button /* type='secondary' */ icon={<IconRefresh />} onClick={fileInfo} disabled={!storageName} type='text' title={t('refresh')} />
|
||||
</Col>
|
||||
<Col style={{ paddingLeft: '1rem', paddingRight: '0.2rem' }} flex='10rem'>
|
||||
<Select /* bordered={false} */ value={storageName} placeholder={t('please_select')} onChange={(value) => {
|
||||
if (value !== storageName) {
|
||||
setStorageName(value)
|
||||
setPathTemp('/')
|
||||
setPath('/')
|
||||
}
|
||||
}
|
||||
}>
|
||||
{
|
||||
rcloneInfo.storageList.map((item) => {
|
||||
return (
|
||||
<Select.Option key={item.name} value={item.name}>{item.name}({searchStorage(item.type).name})</Select.Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col style={{ paddingLeft: '0.2rem', paddingRight: '1rem' }} flex='auto'>
|
||||
<Input disabled={!storageName} value={pathTemp} normalize={() => { return path! }} onChange={(value) => { setPathTemp(value) }} onPressEnter={() => { updatePath(pathTemp) }} />
|
||||
</Col>
|
||||
|
||||
<Col flex='2rem'>
|
||||
<Button icon={<IconFolderAdd />} onClick={MakeDir} disabled={!storageName && !path} type='text' title={t('create_directory')} />
|
||||
</Col>
|
||||
<Col flex='2rem'>
|
||||
<Button icon={<IconUpload />} onClick={UploadFile} disabled={!storageName && !path} type='text' title={t('upload_file')} />
|
||||
</Col>
|
||||
<Col flex='2rem' >
|
||||
<Badge count={clipList.length} maxCount={9} title={t('clip_board')}>
|
||||
<Dropdown disabled={clipList.length == 0} droplist={
|
||||
<Menu>
|
||||
<Menu.Item onClick={() => {
|
||||
clipList.forEach((item) => {
|
||||
if (item.isMove) {
|
||||
if (item.isDir) {
|
||||
moveDir(item.storageName, item.path, storageName!, path!);
|
||||
} else {
|
||||
moveFile(item.storageName, item.path, storageName!, path!);
|
||||
}
|
||||
} else {
|
||||
if (item.isDir) {
|
||||
copyDir(item.storageName, item.path, storageName!, path!);
|
||||
} else {
|
||||
copyFile(item.storageName, item.path, storageName!, path!)
|
||||
}
|
||||
}
|
||||
})
|
||||
setClipList([])
|
||||
Notification.success({
|
||||
title: t('success'),
|
||||
content: t('transm_task_created'),
|
||||
})
|
||||
}} key='p' disabled={!storageName && !path}>{t('paste')}({clipList.length})</Menu.Item>
|
||||
<Menu.Item onClick={() => setClipList([])} key='q'>{t('empty_the_clipboard')}</Menu.Item>
|
||||
</Menu>} position='bl'>
|
||||
<Button icon={<IconPaste />} type='text' />
|
||||
</Dropdown>
|
||||
</Badge>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 'calc(100% - 2rem)', marginTop:'1rem',overflow: 'auto' }}>
|
||||
{storageName ?
|
||||
<>{
|
||||
fileList ?
|
||||
<Table columns={columns}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
rowKey='Path'
|
||||
size='small'
|
||||
noDataElement={<NoData_module />}
|
||||
data={
|
||||
fileList.map((item) => {
|
||||
return {
|
||||
...item, fileName: <Link style={{ width: '100%' }} onClick={() => { item.IsDir && updatePath(item.Path) }}><Typography.Ellipsis showTooltip>{item.Name}</Typography.Ellipsis></Link>,
|
||||
fileSize: (item.Size != -1 ? formatSize(item.Size) : t('dir')),
|
||||
fileModTime: (new Date(item.ModTime)).toLocaleString(),
|
||||
actions: <Space size={'mini'}>
|
||||
<Button onClick={() => { addCilp({ isMove: false, storageName: storageName!, path: item.Path, isDir: item.IsDir }) }} type='text' icon={<IconCopy />} title={t('copy')} />
|
||||
<Button onClick={() => { addCilp({ isMove: true, storageName: storageName!, path: item.Path, isDir: item.IsDir }) }} type='text' icon={<IconScissor />} title={t('cut')} />
|
||||
<Dropdown unmountOnExit={false} droplist={
|
||||
<Menu>
|
||||
<Menu.Item key='rename' style={{ color: 'var(primary-4)' }} onClick={() => fileRename(item.Path, item.IsDir)}><IconEdit /> {t('rename')}</Menu.Item>
|
||||
<Menu.Item key='del' /* style={{ color: 'var(danger-4)' }} */>
|
||||
<Popconfirm
|
||||
focusLock
|
||||
title={t('confirm_delete_question')}
|
||||
onOk={() => {
|
||||
item.IsDir ? delDir(storageName!, item.Path, fileInfo) :
|
||||
delFile(storageName!, item.Path, fileInfo)
|
||||
}}
|
||||
>
|
||||
<IconDelete /> {t('delete')}
|
||||
</Popconfirm></Menu.Item>
|
||||
</Menu>} position='bl'>
|
||||
<Button icon={<IconMore />} type='text' title={t('more')} />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
}
|
||||
})} />
|
||||
: ''
|
||||
}
|
||||
</> :
|
||||
!storageName && <Typography.Paragraph style={tipsStyle}>{t('please_select_storage')}</Typography.Paragraph>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { Explorer_page }
|
||||
87
src/page/storage/storage.tsx
Normal file
87
src/page/storage/storage.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Button, Popconfirm, Space } from "@arco-design/web-react"
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { delStorage, reupStorage } from "../../controller/storage/storage"
|
||||
import { rcloneInfo } from "../../services/rclone"
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { hooks } from "../../services/hook";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Table, TableColumnProps } from '@arco-design/web-react';
|
||||
import { NoData_module } from "../other/noData";
|
||||
import { searchStorage } from "../../controller/storage/listAll";
|
||||
|
||||
|
||||
function Storage_page() {
|
||||
const { t } = useTranslation()
|
||||
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
hooks.upStorage = forceUpdate
|
||||
}, [ignored])
|
||||
|
||||
const columns: TableColumnProps[] = [
|
||||
{
|
||||
title: t('name'),
|
||||
dataIndex: 'name',
|
||||
|
||||
},
|
||||
{
|
||||
title: t('type'),
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
dataIndex: 'actions',
|
||||
align: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%", }}>
|
||||
<div style={{ width: "100%", height: "2rem", }}>
|
||||
<Space>
|
||||
<Button onClick={() => { navigate('./add') }} type='primary'>{t('add')}</Button>
|
||||
<Button onClick={reupStorage} >{t('refresh')}</Button>
|
||||
</Space>
|
||||
|
||||
</div>
|
||||
<div style={{ height: "calc(100% - 2rem)" }}>
|
||||
<br />
|
||||
<Table style={{ height: "100%" }} noDataElement={<NoData_module />} columns={columns} pagination={false} data={
|
||||
rcloneInfo.storageList.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
type: searchStorage(item.type).name,
|
||||
actions: <Space>
|
||||
<Popconfirm
|
||||
focusLock
|
||||
title={t('confirm_delete_question')}
|
||||
onOk={() => {
|
||||
delStorage(item.name)
|
||||
}}
|
||||
>
|
||||
<Button status='danger' type='secondary'>{t('delete')}</Button>
|
||||
</Popconfirm>
|
||||
|
||||
<Button onClick={() => navigate('./add?edit=true&name=' + item.name + '&type=' + item.type)}>{t('edit')}</Button>
|
||||
<Button onClick={() => navigate('/storage/explorer?name=' + item.name)} >{t('explorer')}</Button>
|
||||
<Button onClick={() => navigate('/mount/add?name=' + item.name)} type='primary'>{t('mount')}</Button>
|
||||
</Space>
|
||||
|
||||
}
|
||||
})} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export { Storage_page }
|
||||
309
src/page/task/add.tsx
Normal file
309
src/page/task/add.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import { Button, Divider, Form, Grid, Input, InputNumber, Notification, Select, Space, Tooltip } from '@arco-design/web-react';
|
||||
import React, { useReducer, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { nmConfig, roConfig } from '../../services/config';
|
||||
import { TaskListItem } from '../../type/config';
|
||||
import { rcloneInfo } from '../../services/rclone';
|
||||
import { IconQuestionCircle } from '@arco-design/web-react/icon';
|
||||
import { formatPathRclone } from '../../controller/storage/storage';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { saveTask } from '../../controller/task/task';
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
|
||||
const formatPath = (path: string) => {
|
||||
path = path.replace(/\\/g, '/');
|
||||
path = path.replace(/\/+/g, '/');
|
||||
|
||||
if (path.substring(0, 1) != '/') {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// 定义状态和 action 类型
|
||||
type TaskInfoState = TaskListItem;
|
||||
|
||||
type Action =
|
||||
| { type: 'setName'; payload: string }
|
||||
| { type: 'setRunTypeMode'; payload: string }
|
||||
| { type: 'setTaskType'; payload: string }
|
||||
| { type: 'setSourceStorageName'; payload: string }
|
||||
| { type: 'setSourcePath'; payload: string }
|
||||
| { type: 'setTargetStorageName'; payload: string }
|
||||
| { type: 'setTargetPath'; payload: string }
|
||||
| { type: 'setIntervalDays'; payload: number }
|
||||
| { type: 'setRunTime'; payload: { h: number, m: number, s: number } };
|
||||
|
||||
// 定义 reducer 函数
|
||||
const reducer = (state: TaskInfoState, action: Action): TaskInfoState => {
|
||||
switch (action.type) {
|
||||
case 'setName':
|
||||
return { ...state, name: action.payload };
|
||||
case 'setRunTypeMode':
|
||||
return { ...state, run: { ...state.run, mode: action.payload } };
|
||||
case 'setTaskType':
|
||||
return { ...state, taskType: action.payload };
|
||||
case 'setSourceStorageName':
|
||||
return { ...state, source: { ...state.source, storageName: action.payload } };
|
||||
case 'setSourcePath':
|
||||
return { ...state, source: { ...state.source, path: formatPath(action.payload) } };
|
||||
case 'setTargetStorageName':
|
||||
return { ...state, target: { ...state.target, storageName: action.payload } };
|
||||
case 'setTargetPath':
|
||||
return { ...state, target: { ...state.target, path: formatPath(action.payload) } };
|
||||
case 'setIntervalDays':
|
||||
return { ...state, run: { ...state.run, time: { ...state.run.time, intervalDays: action.payload } } };
|
||||
case 'setRunTime':
|
||||
return { ...state, run: { ...state.run, time: { ...state.run.time, ...action.payload } } };
|
||||
default:
|
||||
throw new Error('Invalid action');
|
||||
}
|
||||
};
|
||||
|
||||
function AddTask_page() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [taskInfo, dispatch] = useReducer(reducer, {
|
||||
name: 'task_' + (nmConfig.task ? nmConfig.task.length + 1 : 1),
|
||||
taskType: roConfig.options.task.taskType.select[roConfig.options.task.taskType.defIndex],
|
||||
source: {
|
||||
storageName:
|
||||
rcloneInfo.storageList && rcloneInfo.storageList.length > 0
|
||||
? rcloneInfo.storageList[0].name
|
||||
: '',
|
||||
path: '/',
|
||||
},
|
||||
target: {
|
||||
storageName:
|
||||
rcloneInfo.storageList && rcloneInfo.storageList.length > 0
|
||||
? (rcloneInfo.storageList.length > 1
|
||||
? rcloneInfo.storageList[1].name
|
||||
: rcloneInfo.storageList[0].name)
|
||||
: '',
|
||||
path: '/',
|
||||
},
|
||||
run: {
|
||||
mode: roConfig.options.task.runMode.select[roConfig.options.task.runMode.defIndex], time: {
|
||||
intervalDays: 1,
|
||||
h: 10,
|
||||
m: 30,
|
||||
s: 0,
|
||||
}
|
||||
},
|
||||
enable: true,
|
||||
});
|
||||
|
||||
const [timeMultiplier, setTimeMultiplier] = useState({
|
||||
...roConfig.options.task.dateMultiplier.select[roConfig.options.task.dateMultiplier.defIndex],
|
||||
multiplicand: 1
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (taskInfo.run.mode === 'time') {
|
||||
setTimeMultiplier({ ...roConfig.options.task.dateMultiplier.select[roConfig.options.task.dateMultiplier.defIndex], multiplicand: 1 })
|
||||
} else if (taskInfo.run.mode === 'interval') {
|
||||
setTimeMultiplier({ ...roConfig.options.task.intervalMultiplier.select[roConfig.options.task.intervalMultiplier.defIndex], multiplicand: 1 })
|
||||
}
|
||||
}, [taskInfo.run.mode])
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', marginBottom: '2rem', marginLeft: '1.8rem' }}>{t('add_task')}</h2>
|
||||
<Form style={{ paddingRight: '10%' }}>
|
||||
<Form.Item label={t('task_name')}>
|
||||
<Input
|
||||
value={taskInfo.name}
|
||||
onChange={(value) => dispatch({ type: 'setName', payload: value })}
|
||||
placeholder={t('please_input')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('task_run_mode')}>
|
||||
<Select
|
||||
onChange={(value) => dispatch({ type: 'setRunTypeMode', payload: value })}
|
||||
value={taskInfo.run.mode}
|
||||
>
|
||||
{roConfig.options.task.runMode.select.map((item) => (
|
||||
<Select.Option value={item}>{t(`task_run_mode_${item}_opt`)}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{taskInfo.run.mode != 'start' && taskInfo.run.mode !== 'disposable' &&
|
||||
<>
|
||||
<Form.Item label={t('interval')}>
|
||||
<Row>
|
||||
<Col flex={'6rem'}>
|
||||
<Select value={timeMultiplier.value} onChange={(value) => {
|
||||
setTimeMultiplier({ ...(taskInfo.run.mode === 'time' ? roConfig.options.task.dateMultiplier.select.find(item => item.value === value)! : roConfig.options.task.intervalMultiplier.select.find(item => item.value === value)!), multiplicand: timeMultiplier.multiplicand });
|
||||
}}>
|
||||
{(taskInfo.run.mode === 'time' ? roConfig.options.task.dateMultiplier.select : roConfig.options.task.intervalMultiplier.select).map((item) => (
|
||||
<Select.Option value={item.value}>{t(item.name)}(*{item.value})</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col flex={'auto'}>
|
||||
<InputNumber mode='button' min={1} max={10000} value={timeMultiplier.multiplicand} precision={0}
|
||||
onChange={
|
||||
(value) => {
|
||||
setTimeMultiplier({ ...timeMultiplier, multiplicand: value });
|
||||
}
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
|
||||
{
|
||||
taskInfo.run.mode === 'time' && <>
|
||||
<Form.Item label={t('time')}>
|
||||
<Row gutter={10}>
|
||||
<Col flex={'1'}>
|
||||
<InputNumber
|
||||
min={0} max={23} precision={0}
|
||||
value={taskInfo.run.time.h}
|
||||
suffix={t('hour')}
|
||||
onChange={(value) => dispatch({ type: 'setRunTime', payload: { ...taskInfo.run.time, h: value } })}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex={'1'}>
|
||||
<InputNumber
|
||||
min={0} max={59} precision={0}
|
||||
value={taskInfo.run.time.m}
|
||||
suffix={t('minute')}
|
||||
onChange={(value) => dispatch({ type: 'setRunTime', payload: { ...taskInfo.run.time, m: value } })}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex={'1'}>
|
||||
<InputNumber
|
||||
min={0} max={59} precision={0}
|
||||
value={taskInfo.run.time.s}
|
||||
suffix={t('second')}
|
||||
onChange={(value) => dispatch({ type: 'setRunTime', payload: { ...taskInfo.run.time, s: value } })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
<Form.Item label={t('task_type')}>
|
||||
<Select
|
||||
onChange={(value) => dispatch({ type: 'setTaskType', payload: value })}
|
||||
value={taskInfo.taskType}
|
||||
>
|
||||
{roConfig.options.task.taskType.select.map((item) => (
|
||||
<Select.Option value={item}>{t(`${item}`)}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('source_path')}>
|
||||
<Row>
|
||||
<Col flex={'7rem'}>
|
||||
<Select
|
||||
value={taskInfo.source.storageName}
|
||||
placeholder={t('please_select')}
|
||||
onChange={(value) => dispatch({ type: 'setSourceStorageName', payload: value })}
|
||||
>
|
||||
{rcloneInfo.storageList.map((item) => (
|
||||
<Select.Option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col flex={'auto'}>
|
||||
<Input
|
||||
value={taskInfo.source.path}
|
||||
onChange={(value) => dispatch({ type: 'setSourcePath', payload: value })}
|
||||
disabled={!taskInfo.source.storageName}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex={'2rem'}>
|
||||
<Tooltip content={t('explain_for_task_path_format')}>
|
||||
<Button icon={<IconQuestionCircle />}></Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
|
||||
{taskInfo.taskType !== 'delete' && (
|
||||
<Form.Item label={t('target_path')}>
|
||||
<Row>
|
||||
<Col flex={'7rem'}>
|
||||
<Select
|
||||
value={taskInfo.target.storageName}
|
||||
placeholder={t('please_select')}
|
||||
onChange={(value) => dispatch({ type: 'setTargetStorageName', payload: value })}
|
||||
>
|
||||
{rcloneInfo.storageList.map((item) => (
|
||||
<Select.Option key={item.name} value={item.name}>
|
||||
{item.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col flex={'auto'}>
|
||||
<Input
|
||||
value={taskInfo.target.path}
|
||||
onChange={(value) => dispatch({ type: 'setTargetPath', payload: value })}
|
||||
disabled={!taskInfo.target.storageName}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex={'2rem'}>
|
||||
<Tooltip content={t('explain_for_task_path_format')}>
|
||||
<Button icon={<IconQuestionCircle />}></Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
<div style={{ marginTop: '20px', textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => {
|
||||
navigate('/task/')
|
||||
}}>{t('step_back')}</Button>
|
||||
<Button type='primary' onClick={() => {
|
||||
if (taskInfo.run.mode === 'time') {
|
||||
taskInfo.run.time.intervalDays = timeMultiplier.multiplicand * timeMultiplier.value;
|
||||
} else if (taskInfo.run.mode === 'interval') {
|
||||
taskInfo.run.interval = timeMultiplier.multiplicand * timeMultiplier.value;
|
||||
}
|
||||
if (nmConfig.task && nmConfig.task.forEach(item => item.name == taskInfo.name)! || !taskInfo.name) {
|
||||
Notification.error({
|
||||
title: t('error'),
|
||||
content: t('the_task_name_is_illegal'),
|
||||
})
|
||||
} else if(!taskInfo.source.storageName || !taskInfo.source.path|| (taskInfo.taskType!== 'delete' &&(!taskInfo.target.storageName || !taskInfo.target.path))){
|
||||
Notification.error({
|
||||
title: t('error'),
|
||||
content: t('the_path_is_illegal'),
|
||||
})
|
||||
}else if(taskInfo.taskType!== 'delete' && taskInfo.source.path===taskInfo.target.path&&taskInfo.source.storageName===taskInfo.target.storageName){
|
||||
Notification.error({
|
||||
title: t('error'),
|
||||
content: t('same_source_and_target'),
|
||||
})
|
||||
} else {
|
||||
if (saveTask(taskInfo)) {
|
||||
Notification.success({
|
||||
title: t('success'),
|
||||
content: t('task_added_successfully'),
|
||||
})
|
||||
navigate('/task/')
|
||||
}
|
||||
}
|
||||
}}>{t('add')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { AddTask_page };
|
||||
64
src/page/task/task.tsx
Normal file
64
src/page/task/task.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useReducer } from 'react'
|
||||
import { DevTips_module } from '../other/devTips'
|
||||
import { Button, Space, Table, TableColumnProps } from '@arco-design/web-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { nmConfig } from '../../services/config'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { delTask } from '../../controller/task/task'
|
||||
import { NoData_module } from '../other/noData'
|
||||
|
||||
function Task_page() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate();
|
||||
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);//刷新组件
|
||||
|
||||
const columns: TableColumnProps[] = [
|
||||
{
|
||||
title: t('task_name'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: t('state'),
|
||||
dataIndex: 'state',
|
||||
}, {
|
||||
title: t('cycle'),
|
||||
dataIndex: 'cycle',
|
||||
}
|
||||
, {
|
||||
title: t('run_info'),
|
||||
dataIndex: 'runInfo',
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
dataIndex: 'actions',
|
||||
align:'right'
|
||||
}
|
||||
];
|
||||
console.log(nmConfig.task);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<div style={{ width: '100%', height: '2rem' }}>
|
||||
<Space>
|
||||
<Button type='primary' onClick={() => { navigate('/task/add') }}>{t('add')}</Button>
|
||||
<Button onClick={() => { forceUpdate() }}>{t('refresh')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ height: "calc(100% - 3rem)", marginTop: "1rem" }}>
|
||||
<Table columns={columns} noDataElement={ <NoData_module />} data={nmConfig.task.map((taskItem) => {
|
||||
return {
|
||||
...taskItem,
|
||||
state: taskItem.enable ? t('enabled') : t('disabled'),
|
||||
cycle: t('task_run_mode_' + taskItem.run.mode),
|
||||
runInfo:taskItem.runInfo?.msg,
|
||||
actions: <>
|
||||
<Button onClick={()=>{delTask(taskItem.name);forceUpdate()}}>{t('delete')}</Button>
|
||||
</>
|
||||
}
|
||||
})} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Task_page }
|
||||
135
src/page/transmit/transmit.tsx
Normal file
135
src/page/transmit/transmit.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { rcloneInfo, rcloneStatsHistory } from '../../services/rclone'
|
||||
import { hooks } from '../../services/hook'
|
||||
import { RcloneTransferItem } from '../../type/rclone/stats'
|
||||
import { Card, Descriptions, List, Progress, Space, Statistic, Grid, Typography } from '@arco-design/web-react'
|
||||
import { formatETA, formatSize } from '../../utils/utils'
|
||||
import { Area } from '@ant-design/charts'
|
||||
import { NoData_module } from '../other/noData'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
const Row = Grid.Row;
|
||||
const Col = Grid.Col;
|
||||
|
||||
function Transmit_page() {
|
||||
const { t } = useTranslation()
|
||||
const [transmitList, setTransmitList] = useState<RcloneTransferItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
hooks.upStats = () => {
|
||||
if (rcloneInfo.stats.transferring) {
|
||||
setTransmitList(rcloneInfo.stats.transferring)
|
||||
} else {
|
||||
setTransmitList([])
|
||||
}
|
||||
}
|
||||
hooks.upStats()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{margin:'0'}}>
|
||||
<Card style={{}}
|
||||
title={t('overview')}
|
||||
bordered={false}
|
||||
>
|
||||
<Space direction='vertical' style={{ width: '100%' }}>
|
||||
|
||||
{rcloneInfo.stats.bytes > 0 && <Progress percent={~~(rcloneInfo.stats.bytes / rcloneInfo.stats.totalBytes * 100)} />}
|
||||
<Descriptions colon=' :' data={[
|
||||
{
|
||||
label:t('speed'),
|
||||
value: `${formatSize(rcloneInfo.stats.realSpeed!)}/s`
|
||||
},
|
||||
|
||||
{
|
||||
label: t('size'),
|
||||
value: `${formatSize(rcloneInfo.stats.bytes)}/${formatSize(rcloneInfo.stats.totalBytes)}`
|
||||
},
|
||||
|
||||
...(rcloneInfo.stats.transferTime > 0 ? [
|
||||
{
|
||||
label:t('used_time'),
|
||||
value: formatETA(rcloneInfo.stats.transferTime)
|
||||
}
|
||||
] : []),
|
||||
...(Number(rcloneInfo.stats.eta) > 0 ? [
|
||||
{
|
||||
label: t('eta'),
|
||||
value: formatETA(rcloneInfo.stats.eta!)
|
||||
}
|
||||
] : []),
|
||||
...(Number(rcloneInfo.stats.totalTransfers) > 0 ? [
|
||||
{
|
||||
label: t('transferred'),
|
||||
value: rcloneInfo.stats.totalTransfers
|
||||
}
|
||||
] : []),
|
||||
|
||||
]} />
|
||||
|
||||
|
||||
</Space>
|
||||
</Card>
|
||||
{/* <Area data={rcloneStatsHistory} xField='elapsedTime' yField='speed' height={200} /> */}
|
||||
|
||||
<Card style={{}}
|
||||
title={t('transferring')}
|
||||
bordered={false}
|
||||
>
|
||||
|
||||
<List noDataElement={ <NoData_module />}>
|
||||
{
|
||||
transmitList.map((item, index) => {
|
||||
return <List.Item key={index}>
|
||||
<Row >
|
||||
<Col flex={'3.5rem'}>
|
||||
<Progress type={'circle'} percent={item.percentage} style={{ marginTop: '0.5rem' }} size='small' />
|
||||
</Col>
|
||||
<Col flex={'auto'}>
|
||||
<Typography.Ellipsis >{item.name}</Typography.Ellipsis>
|
||||
<Descriptions
|
||||
size='small'
|
||||
labelStyle={{ textAlign: 'right' }}
|
||||
colon=' :'
|
||||
/* layout='inline-vertical' */
|
||||
data={[
|
||||
{
|
||||
label: t('speed'),
|
||||
value: `${formatSize(item.speed)}/s`
|
||||
},
|
||||
{
|
||||
label: t('size'), value: `${formatSize(item.bytes)}/${formatSize(item.size)}`
|
||||
},
|
||||
{
|
||||
label: t('source'),
|
||||
value: item.srcFs
|
||||
},
|
||||
{
|
||||
label: t('speed_avg'),
|
||||
value: `${formatSize(item.speedAvg)}/s`
|
||||
},
|
||||
...(Number(item.eta) > 0 ? [
|
||||
{
|
||||
label: t('eta'),
|
||||
value: formatETA(item.eta!)
|
||||
}
|
||||
] : []),
|
||||
...(item.dstFs ? [
|
||||
{
|
||||
label: t('target'),
|
||||
value: item.dstFs
|
||||
}
|
||||
] : []),
|
||||
]} />
|
||||
</Col>
|
||||
</Row>
|
||||
</List.Item>
|
||||
})
|
||||
}
|
||||
</List>
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Transmit_page }
|
||||
94
src/services/config.ts
Normal file
94
src/services/config.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { invoke } from "@tauri-apps/api"
|
||||
import { NMConfig, OSInfo } from "../type/config"
|
||||
|
||||
const roConfig = {
|
||||
url: {
|
||||
website: 'https://www.netmount.cn/',
|
||||
rclone: 'https://github.com/rclone/rclone',
|
||||
},
|
||||
options: {
|
||||
task: {
|
||||
runMode: {
|
||||
defIndex: 0,
|
||||
select: ['start', 'time', 'interval', 'disposable']
|
||||
},
|
||||
taskType: {
|
||||
defIndex: 3,
|
||||
select: ['copy', 'move', 'delete', 'sync', 'bisync']
|
||||
},
|
||||
dateMultiplier: {
|
||||
defIndex: 0,
|
||||
select: [{ name: 'day', value: 1 }, { name: 'week', value: 7 }, { name: 'month', value: 30 }]
|
||||
},
|
||||
intervalMultiplier: {
|
||||
defIndex: 0,
|
||||
select: [{ name: 'hour', value: 60 * 60 }, { name: 'minute', value: 60 }, { name: 'second', value: 1 }]
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
themeMode: {
|
||||
defIndex: 0,
|
||||
select: ['auto', 'light', 'dark']
|
||||
}, language: {
|
||||
defIndex: 0,
|
||||
select: [
|
||||
{ name: '简体中文', value: 'cn', langCode: 'zh-cn' },
|
||||
{ name: 'English', value: 'en', langCode: 'en-us' },
|
||||
/* { name: '繁體中文(臺灣)', value: 'cht', langCode: 'zh-tw' },
|
||||
{ name: '繁體中文(香港)', value: 'cht', langCode: 'zh-hk' },
|
||||
{ name: 'Русский язык', value: 'ru', langCode: 'ru-RU' }, */
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nmConfig: NMConfig = {
|
||||
mount: {
|
||||
lists: [],
|
||||
},
|
||||
task: [],
|
||||
api: {
|
||||
url: 'https://api.hotpe.top/test/NetMount',
|
||||
},
|
||||
settings: {
|
||||
themeMode: roConfig.options.setting.themeMode.select[roConfig.options.setting.themeMode.defIndex],
|
||||
startHide: false,
|
||||
},
|
||||
}
|
||||
|
||||
const setNmConfig = (config: NMConfig) => {
|
||||
nmConfig = config
|
||||
}
|
||||
|
||||
const readNmConfig = async () => {
|
||||
await invoke('read_config_file').then(configData => {
|
||||
setNmConfig(configData as NMConfig)
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
}
|
||||
const saveNmConfig = async () => {
|
||||
await invoke('write_config_file', {
|
||||
configData: nmConfig
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
let osInfo: OSInfo = {
|
||||
arch: 'unknown',
|
||||
osType: 'unknown',
|
||||
platform: 'unknown',
|
||||
tempDir: '',
|
||||
osVersion: ''
|
||||
}
|
||||
|
||||
const setOsInfo = (osinfo: OSInfo) => {
|
||||
osInfo = osinfo
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export { nmConfig, setNmConfig, osInfo, setOsInfo, roConfig, readNmConfig, saveNmConfig }
|
||||
12
src/services/hook.ts
Normal file
12
src/services/hook.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Hooks } from "../type/hook";
|
||||
import i18n from "./i18n";
|
||||
|
||||
let hooks:Hooks = {
|
||||
upStats:()=>{},
|
||||
upStorage:()=>{},
|
||||
upMount:()=>{},
|
||||
navigate:()=>{},
|
||||
setLocaleStr:()=>{}
|
||||
}
|
||||
|
||||
export {hooks}
|
||||
27
src/services/i18n.ts
Normal file
27
src/services/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// i18n.js 文件
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
// 引入语言文件
|
||||
import cn from '../controller/language/zh-cn.json';
|
||||
|
||||
// 初始化资源文件,即各种语言的json文件
|
||||
const resources = {
|
||||
cn: {
|
||||
translation: cn
|
||||
}
|
||||
};
|
||||
|
||||
i18n
|
||||
// 连接react-i18next与i18next的插件配置
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
/*lng: "cn", // 初始语言 */
|
||||
keySeparator: false, // 是否允许keys使用点分隔符
|
||||
interpolation: {
|
||||
escapeValue: false // 转义字符
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
61
src/services/rclone.ts
Normal file
61
src/services/rclone.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import { t } from "i18next"
|
||||
import { RcloneInfo } from "../type/rclone/rcloneInfo"
|
||||
import { RcloneStats } from "../type/rclone/stats"
|
||||
|
||||
let rcloneInfo: RcloneInfo = {
|
||||
process:{
|
||||
|
||||
},
|
||||
endpoint: {
|
||||
url: '',
|
||||
isLocal: true,
|
||||
auth: {
|
||||
user: '',
|
||||
pass: '',
|
||||
},
|
||||
localhost: {
|
||||
port: 5572,
|
||||
}
|
||||
},
|
||||
storageList: [],
|
||||
mountList: [],
|
||||
version: {
|
||||
arch: "",
|
||||
decomposed: [],
|
||||
goTags: "",
|
||||
goVersion: "",
|
||||
isBeta: false,
|
||||
isGit: false,
|
||||
linking: "",
|
||||
os: "",
|
||||
version: ""
|
||||
},
|
||||
stats: {
|
||||
bytes: 0,
|
||||
checks: 0,
|
||||
deletedDirs: 0,
|
||||
deletes: 0,
|
||||
elapsedTime: 0,
|
||||
errors: 0,
|
||||
eta: null,
|
||||
fatalError: false,
|
||||
renames: 0,
|
||||
retryError: false,
|
||||
serverSideCopies: 0,
|
||||
serverSideCopyBytes: 0,
|
||||
serverSideMoveBytes: 0,
|
||||
serverSideMoves: 0,
|
||||
speed: 0,
|
||||
totalBytes: 0,
|
||||
totalChecks: 0,
|
||||
totalTransfers: 0,
|
||||
transferTime: 0,
|
||||
lastError: '',
|
||||
transferring: []
|
||||
}
|
||||
}
|
||||
|
||||
let rcloneStatsHistory: RcloneStats[] = []
|
||||
|
||||
export { rcloneInfo, rcloneStatsHistory }
|
||||
67
src/type/config.d.ts
vendored
Normal file
67
src/type/config.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Arch, OsType, Platform } from "@tauri-apps/api/os"
|
||||
import { ParametersType } from "./rclone/storage/defaults"
|
||||
import { Notice } from "./controller/update"
|
||||
|
||||
interface NMConfig {
|
||||
mount: {
|
||||
lists: MountListItem[]
|
||||
},
|
||||
task: TaskListItem[],
|
||||
api: {
|
||||
url: string
|
||||
},
|
||||
settings: {
|
||||
themeMode: 'dark' | 'light' | 'auto' | string,
|
||||
startHide:boolean,
|
||||
language?:string
|
||||
},
|
||||
notice?:Notice
|
||||
}
|
||||
|
||||
|
||||
interface MountListItem {
|
||||
storageName: string,
|
||||
mountPath: string,
|
||||
parameters: ParametersType,//挂载配置
|
||||
autoMount: boolean,//软件启动自动挂载
|
||||
}
|
||||
|
||||
interface TaskListItem {
|
||||
name: string,
|
||||
taskType: 'copy' | 'move' | 'delete' | 'sync' |'bisync'| string,
|
||||
source: {
|
||||
storageName: string,
|
||||
path: string,
|
||||
},
|
||||
target: {
|
||||
storageName: string,
|
||||
path: string,
|
||||
},
|
||||
parameters?: ParametersType,
|
||||
enable: boolean
|
||||
run: {
|
||||
runId?: number,//任务id,setTimeout或setInterval的返回值
|
||||
mode: 'time' | 'interval' | 'start' |'disposable'| string,//start:软件启动时执行,time:定时执行,interval:间隔执行 , disposable:一次性执行(执行后删除任务)
|
||||
time: {
|
||||
intervalDays: number,//间隔天数
|
||||
h: number,//小时
|
||||
m: number,//分钟
|
||||
s: number,//秒
|
||||
},
|
||||
interval?: number,//周期执行,单位ms
|
||||
},
|
||||
runInfo?: {
|
||||
error:boolean
|
||||
msg: string,
|
||||
}
|
||||
}
|
||||
|
||||
interface OSInfo {
|
||||
arch: Arch | 'unknown',
|
||||
osType: OsType | 'unknown',
|
||||
platform: Platform | 'unknown',
|
||||
tempDir: string,
|
||||
osVersion: string
|
||||
}
|
||||
|
||||
export { NMConfig, MountListItem, TaskListItem, OSInfo }
|
||||
30
src/type/controller/update.d.ts
vendored
Normal file
30
src/type/controller/update.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface ResList {
|
||||
[key: string]: ResItem;
|
||||
}
|
||||
|
||||
export interface ResItem {
|
||||
id: string;
|
||||
name: string;
|
||||
pushTime: string;
|
||||
body?: string;
|
||||
assets: ResAsset[];
|
||||
website?: string;
|
||||
download_url?: string;
|
||||
}
|
||||
|
||||
export interface ResAsset {
|
||||
name: string;
|
||||
size: number;
|
||||
download_url: string;
|
||||
}
|
||||
|
||||
export interface Notice {
|
||||
state: 'success' | string; // 假设状态还有 'failure' 等其他可能值,这里仅作示例
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
manual_close: boolean;
|
||||
language: string; // 添加更多可能的语言选项,此处仅为示例
|
||||
displayed:boolean
|
||||
}
|
||||
9
src/type/hook.d.ts
vendored
Normal file
9
src/type/hook.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
interface Hooks{
|
||||
upStats:Function;
|
||||
upStorage:Function;
|
||||
upMount:Function;
|
||||
navigate:(path:string)=>void;
|
||||
setLocaleStr:(localeStr:string)=>void|string;
|
||||
}
|
||||
|
||||
export {Hooks}
|
||||
8
src/type/page/storage/explorer.d.ts
vendored
Normal file
8
src/type/page/storage/explorer.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
interface clipListItem{
|
||||
isMove:boolean;
|
||||
storageName:string;
|
||||
path:string;
|
||||
isDir:boolean
|
||||
}
|
||||
|
||||
export {clipListItem}
|
||||
0
src/type/page/storage/pageMark.d.ts
vendored
Normal file
0
src/type/page/storage/pageMark.d.ts
vendored
Normal file
60
src/type/rclone/rcloneInfo.d.ts
vendored
Normal file
60
src/type/rclone/rcloneInfo.d.ts
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Child, Command } from "@tauri-apps/api/shell";
|
||||
import { RcloneStats } from "./stats";
|
||||
|
||||
interface RcloneInfo {
|
||||
process:{
|
||||
command?:Command,
|
||||
child?:Child,
|
||||
log?:string
|
||||
},
|
||||
endpoint: {
|
||||
url: string,
|
||||
isLocal: boolean,// 是否为本地地址
|
||||
auth: {
|
||||
user: string,
|
||||
pass: string,
|
||||
},
|
||||
localhost: {
|
||||
port: number,
|
||||
}
|
||||
},
|
||||
version: RcloneVersion,
|
||||
stats: RcloneStats,
|
||||
storageList: Array<StorageList>,
|
||||
mountList: Array<MountList>
|
||||
}
|
||||
|
||||
// 定义 RcloneVersion 接口
|
||||
interface RcloneVersion {
|
||||
arch: string; // CPU 架构,例如:"386"
|
||||
decomposed: number[]; // 版本号数组,例如:[1, 66, 0]
|
||||
goTags: string; // Go 语言标签,例如:"cmount"
|
||||
goVersion: string; // Go 语言版本,例如:"go1.22.1"
|
||||
isBeta: boolean; // 是否为 Beta 版本,例如:false
|
||||
isGit: boolean; // 是否为 Git 构建版本,例如:false
|
||||
linking: string; // 链接类型,例如:"static"
|
||||
os: string; // 目标操作系统,例如:"windows"
|
||||
version: string; // Rclone 版本字符串,例如:"v1.66.0"
|
||||
}
|
||||
|
||||
interface StorageList {
|
||||
name: string,
|
||||
type: 'webdav' | string
|
||||
}
|
||||
|
||||
interface MountList {
|
||||
storageName: string,
|
||||
mountPath: string,
|
||||
mountedTime: Date,
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
Path: string;
|
||||
Name: string;
|
||||
Size: number;
|
||||
MimeType: string;
|
||||
ModTime: Date;
|
||||
IsDir: boolean;
|
||||
}
|
||||
|
||||
export { RcloneInfo, FileInfo }
|
||||
62
src/type/rclone/stats.d.ts
vendored
Normal file
62
src/type/rclone/stats.d.ts
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/* {
|
||||
"bytes": 0,
|
||||
"checks": 0,
|
||||
"deletedDirs": 0,
|
||||
"deletes": 0,
|
||||
"elapsedTime": 9.6440363,
|
||||
"errors": 0,
|
||||
"eta": null,
|
||||
"fatalError": false,
|
||||
"renames": 0,
|
||||
"retryError": false,
|
||||
"serverSideCopies": 0,
|
||||
"serverSideCopyBytes": 0,
|
||||
"serverSideMoveBytes": 0,
|
||||
"serverSideMoves": 0,
|
||||
"speed": 0,
|
||||
"totalBytes": 0,
|
||||
"totalChecks": 0,
|
||||
"totalTransfers": 0,
|
||||
"transferTime": 0,
|
||||
"transfers": 0
|
||||
} */
|
||||
|
||||
interface RcloneTransferItem {
|
||||
bytes: number; // 已处理的字节数
|
||||
eta: null | number; // 预计剩余时间(如果可用,否则为null)
|
||||
group: string; // 通常是"global_stats"
|
||||
name: string; // 文件或目录名称
|
||||
percentage: number; // 完成百分比
|
||||
size: number; // 文件大小(字节)
|
||||
speed: number; // 当前速度(字节/秒)
|
||||
speedAvg: number; // 平均速度(字节/秒)
|
||||
srcFs: string; // 源文件系统的标识符(如"Webdav:")
|
||||
dstFs?: string; // 目标文件系统的标识符(如"Webdav:")
|
||||
}
|
||||
|
||||
interface RcloneStats {
|
||||
bytes: number; // 已处理的字节数
|
||||
checks: number; // 完成的校验数
|
||||
deletedDirs: number; // 删除的目录数量
|
||||
deletes: number; // 删除的文件数量
|
||||
elapsedTime: number; // 运行总时间(秒)
|
||||
errors: number; // 出现的错误总数
|
||||
eta: null | number; // 预计剩余时间(如果可用,否则为null)
|
||||
fatalError: boolean; // 是否出现致命错误
|
||||
lastError: string; // 最近发生的错误信息
|
||||
renames: number; // 重命名的数量
|
||||
retryError: boolean; // 是否有重试错误发生
|
||||
serverSideCopies: number; // 服务器端复制操作的数量
|
||||
serverSideCopyBytes: number; // 通过服务器端复制传输的字节数
|
||||
serverSideMoveBytes: number; // 通过服务器端移动传输的字节数
|
||||
serverSideMoves: number; // 服务器端移动操作的数量
|
||||
speed: number; // 当前速度(字节/秒)
|
||||
realSpeed?: number; // 实时速度(字节/秒)
|
||||
totalBytes: number; // 总共处理的字节数
|
||||
totalChecks: number; // 总共完成的校验数
|
||||
totalTransfers: number; // 总共完成的传输操作数
|
||||
transferTime: number; // 执行传输操作所花费的时间(秒)
|
||||
transferring: RcloneTransferItem[]; // 正在进行的传输项列表
|
||||
}
|
||||
|
||||
export { RcloneStats, RcloneTransferItem }
|
||||
17
src/type/rclone/storage/defaults.d.ts
vendored
Normal file
17
src/type/rclone/storage/defaults.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
interface ParametersType {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface DefaultParams {
|
||||
"name": string,//存储名称
|
||||
"standard": ParametersType,
|
||||
"advanced": ParametersType,
|
||||
"required": Array<string>
|
||||
}
|
||||
|
||||
interface ParamsSelectType {
|
||||
select: string | number,
|
||||
values: Array<string | number> | { [key: string | number]: string | number }
|
||||
}
|
||||
|
||||
export { DefaultParams, ParametersType, ParamsSelectType }
|
||||
71
src/type/rclone/storage/mount/parameters.d.ts
vendored
Normal file
71
src/type/rclone/storage/mount/parameters.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
//https://rclone.org/rc/#data-types
|
||||
//https://rclone.org/rc/#mount-mount
|
||||
//https://p0.hotpe.top/i/p/1/66080cb6e9a4c.png
|
||||
|
||||
//https://github.com/rclone/rclone-webui-react/blob/master/src/utils/MountOptions.js
|
||||
//http://localhost:5572/options/get
|
||||
//https://rclone.org/commands/rclone_mount/#options-opt
|
||||
import { ParametersType, ParamsSelectType } from "../defaults";
|
||||
|
||||
interface VfsOptions {
|
||||
CacheMaxAge: number;
|
||||
CacheMaxSize: number;
|
||||
CacheMode: ParamsSelectType;
|
||||
CachePollInterval: number;
|
||||
CaseInsensitive: boolean;
|
||||
ChunkSize: number;
|
||||
ChunkSizeLimit: number;
|
||||
DirCacheTime: number;
|
||||
DirPerms: number;
|
||||
FilePerms: number;
|
||||
GID: number;
|
||||
NoChecksum: boolean;
|
||||
NoModTime: boolean;
|
||||
NoSeek: boolean;
|
||||
PollInterval: number;
|
||||
ReadAhead: number;
|
||||
ReadOnly: boolean;
|
||||
ReadWait: number;
|
||||
UID: number;
|
||||
Umask: number;
|
||||
WriteBack: number;
|
||||
WriteWait: number;
|
||||
Refresh?: boolean;
|
||||
BlockNormDupes?: boolean;
|
||||
UsedIsSize?: boolean;
|
||||
FastFingerprint?: boolean;
|
||||
DiskSpaceTotalSize?: number;
|
||||
UID?: number;
|
||||
GID?: number;
|
||||
Umask?: number;
|
||||
}
|
||||
|
||||
interface MountOptions {
|
||||
AllowNonEmpty: boolean;
|
||||
AllowOther: boolean;
|
||||
AllowRoot: boolean;
|
||||
AsyncRead: boolean;
|
||||
AttrTimeout: number;
|
||||
Daemon: boolean;
|
||||
DaemonTimeout: number;
|
||||
DebugFUSE: boolean;
|
||||
DefaultPermissions: boolean;
|
||||
ExtraFlags: string[];
|
||||
ExtraOptions: string[];
|
||||
MaxReadAhead: number;
|
||||
NoAppleDouble: boolean;
|
||||
NoAppleXattr: boolean;
|
||||
VolumeName: string;
|
||||
WritebackCache: boolean;
|
||||
DaemonWait?: number;
|
||||
DeviceName?: string;
|
||||
NetworkMode?: boolean;
|
||||
CaseInsensitive?: boolean | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { VfsOptions, MountOptions }
|
||||
|
||||
|
||||
|
||||
22
src/type/rclone/storage/parameters/crypt.d.ts
vendored
Normal file
22
src/type/rclone/storage/parameters/crypt.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ParamsSelectType } from "../defaults";
|
||||
|
||||
|
||||
interface CryptParamsStandard {
|
||||
remote?: string;
|
||||
filename_encryption?: ParamsSelectType;
|
||||
directory_name_encryption?: boolean;
|
||||
password?: string;
|
||||
password2?: string;
|
||||
}
|
||||
|
||||
interface CryptParamsAdvanced {
|
||||
server_side_across_configs?: boolean;
|
||||
show_mapping?: boolean;
|
||||
no_data_encryption?: boolean;
|
||||
pass_bad_blocks?: boolean;
|
||||
strict_names?: boolean;
|
||||
filename_encoding?: ParamsSelectType;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
export{CryptParamsStandard, CryptParamsAdvanced}
|
||||
31
src/type/rclone/storage/parameters/ftp.d.ts
vendored
Normal file
31
src/type/rclone/storage/parameters/ftp.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
interface FtpParamsStandard {
|
||||
// 标准选项
|
||||
host: string;
|
||||
user?: string;
|
||||
port?: number;
|
||||
pass?: string;
|
||||
tls?: boolean;
|
||||
explicit_tls?: boolean;
|
||||
}
|
||||
|
||||
interface FtpParamsAdvanced{
|
||||
// 高级选项
|
||||
concurrency?: number;
|
||||
no_check_certificate?: boolean;
|
||||
disable_epsv?: boolean;
|
||||
disable_mlsd?: boolean;
|
||||
disable_utf8?: boolean;
|
||||
writing_mdtm?: boolean;
|
||||
force_list_hidden?: boolean;
|
||||
idle_timeout?: string; // Duration 类型,此处简化为字符串
|
||||
close_timeout?: string; // Duration 类型,此处简化为字符串
|
||||
tls_cache_size?: number;
|
||||
disable_tls13?: boolean;
|
||||
shut_timeout?: string; // Duration 类型,此处简化为字符串
|
||||
ask_password?: boolean;
|
||||
socks_proxy?: string;
|
||||
encoding?: string; // 在 TypeScript 中,可能需要定义一个枚举类型来表示支持的编码格式
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export {FtpParamsStandard,FtpParamsAdvanced}
|
||||
25
src/type/rclone/storage/parameters/local.d.ts
vendored
Normal file
25
src/type/rclone/storage/parameters/local.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
// 定义 LocalParamsStandard 类型,这里暂无具体参数
|
||||
interface LocalParamsStandard {
|
||||
// 这里可以添加未来可能会有的标准参数
|
||||
}
|
||||
|
||||
// 定义 LocalParamsAdvanced 类型,包含你给出的所有高级选项
|
||||
interface LocalParamsAdvanced {
|
||||
nounc: boolean,
|
||||
copy_links: boolean,
|
||||
links: boolean,
|
||||
skip_links: boolean,
|
||||
zero_size_links: boolean,
|
||||
unicode_normalization: boolean,
|
||||
no_check_updated: boolean,
|
||||
one_file_system: boolean,
|
||||
case_sensitive: boolean,
|
||||
case_insensitive: boolean,
|
||||
no_preallocate: boolean,
|
||||
no_sparse: boolean,
|
||||
no_set_modtime: boolean,
|
||||
encoding: string,
|
||||
description: string
|
||||
}
|
||||
|
||||
export{LocalParamsStandard, LocalParamsAdvanced}
|
||||
35
src/type/rclone/storage/parameters/onedrive.d.ts
vendored
Normal file
35
src/type/rclone/storage/parameters/onedrive.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ParamsSelectType } from "../defaults";
|
||||
|
||||
interface OneDriveParamsStandard {
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
region?: ParamsSelectType;
|
||||
}
|
||||
|
||||
|
||||
interface OneDriveParamsAdvanced {
|
||||
token?: string;
|
||||
auth_url?: string;
|
||||
token_url?: string;
|
||||
chunk_size?: string; // Assuming this to be a string for simplification
|
||||
drive_id?: string;
|
||||
drive_type?: ParamsSelectType;
|
||||
root_folder_id?: string;
|
||||
access_scopes?: string;
|
||||
disable_site_permission?: boolean;
|
||||
expose_onenote_files?: boolean;
|
||||
server_side_across_configs?: boolean;
|
||||
list_chunk?: number;
|
||||
no_versions?: boolean;
|
||||
link_scope?: string;
|
||||
link_type?: string;
|
||||
link_password?: string;
|
||||
hash_type?: string;
|
||||
av_override?: boolean;
|
||||
delta?: boolean;
|
||||
metadata_permissions?: string;
|
||||
encoding?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export{OneDriveParamsStandard, OneDriveParamsAdvanced}
|
||||
63
src/type/rclone/storage/parameters/s3.d.ts
vendored
Normal file
63
src/type/rclone/storage/parameters/s3.d.ts
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ParamsSelectType } from "../defaults";
|
||||
|
||||
interface S3ParamsStandard {
|
||||
provider?: ParamsSelectType;
|
||||
env_auth?: boolean;
|
||||
access_key_id?: string;
|
||||
secret_access_key?: string;
|
||||
region?: string;
|
||||
endpoint?: string;
|
||||
location_constraint?: string;
|
||||
acl?: string;
|
||||
server_side_encryption?: string;
|
||||
sse_kms_key_id?: string;
|
||||
storage_class?: string;
|
||||
}
|
||||
|
||||
interface S3ParamsAdvanced {
|
||||
bucket_acl?: string;
|
||||
requester_pays?: boolean;
|
||||
sse_customer_algorithm?: string;
|
||||
sse_customer_key?: string;
|
||||
sse_customer_key_base64?: string;
|
||||
sse_customer_key_md5?: string;
|
||||
upload_cutoff?: string; // Assuming this to be a string for simplification
|
||||
chunk_size?: string; // Assuming this to be a string for simplification
|
||||
max_upload_parts?: number;
|
||||
copy_cutoff?: string; // Assuming this to be a string for simplification
|
||||
disable_checksum?: boolean;
|
||||
shared_credentials_file?: string;
|
||||
profile?: string;
|
||||
session_token?: string;
|
||||
upload_concurrency?: number;
|
||||
force_path_style?: boolean;
|
||||
v2_auth?: boolean;
|
||||
use_dual_stack?: boolean;
|
||||
use_accelerate_endpoint?: boolean;
|
||||
leave_parts_on_error?: boolean;
|
||||
list_chunk?: number;
|
||||
list_version?: number;
|
||||
list_url_encode?: ParamsSelectType;
|
||||
no_check_bucket?: boolean;
|
||||
no_head?: boolean;
|
||||
no_head_object?: boolean;
|
||||
encoding?: string;
|
||||
disable_http2?: boolean;
|
||||
download_url?: string;
|
||||
directory_markers?: boolean;
|
||||
use_multipart_etag?: ParamsSelectType;
|
||||
use_presigned_request?: boolean;
|
||||
versions?: boolean;
|
||||
version_at?: string;
|
||||
version_deleted?: boolean;
|
||||
decompress?: boolean;
|
||||
might_gzip?: ParamsSelectType;
|
||||
use_accept_encoding_gzip?: ParamsSelectType;
|
||||
no_system_metadata?: boolean;
|
||||
sts_endpoint?: string;
|
||||
use_already_exists?: ParamsSelectType;
|
||||
use_multipart_uploads?: ParamsSelectType;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export{S3ParamsStandard, S3ParamsAdvanced}
|
||||
24
src/type/rclone/storage/parameters/webdav.d.ts
vendored
Normal file
24
src/type/rclone/storage/parameters/webdav.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ParamsSelectType } from "../defaults";
|
||||
|
||||
interface WebdavParamsStandard {
|
||||
// 标准选项
|
||||
url: string;
|
||||
vendor?: ParamsSelectType;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
bearer_token?: string;
|
||||
}
|
||||
|
||||
|
||||
interface WebdavParamsAdvanced {
|
||||
// 高级选项
|
||||
bearer_token_command?: string;
|
||||
encoding?: string;
|
||||
headers?: Array<{ key: string, value: string }>; // 也可以使用 Map<string, string> 类型
|
||||
pacer_min_sleep?: string; // 使用 Duration 类型,但此处为了简化表示,我们暂时将其作为字符串
|
||||
nextcloud_chunk_size?: string; // 使用 SizeSuffix 类型,此处同样简化为字符串
|
||||
owncloud_exclude_shares?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export { WebdavParamsStandard, WebdavParamsAdvanced }
|
||||
10
src/type/rclone/storage/storageListAll.d.ts
vendored
Normal file
10
src/type/rclone/storage/storageListAll.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DefaultParams } from "./defaults";
|
||||
|
||||
interface StorageListAll{
|
||||
name: string;
|
||||
type: string;
|
||||
introduce?: string
|
||||
defaultParams:DefaultParams
|
||||
}
|
||||
|
||||
export {StorageListAll}
|
||||
11
src/type/routers.d.ts
vendored
Normal file
11
src/type/routers.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
interface Routers {
|
||||
title: string | ReactNode;
|
||||
path: string;//菜单:key未定义时,key=path
|
||||
key?:string;//菜单:在隐藏子项的情况下,父子设置相同key,则可选择实现选择父项
|
||||
hide?: boolean;
|
||||
hideChildren?:boolean;
|
||||
children?: Routers[];
|
||||
component?: JSX.Element;
|
||||
}
|
||||
|
||||
export { Routers }
|
||||
13
src/type/utils/aria2.d.ts
vendored
Normal file
13
src/type/utils/aria2.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// 定义Aria2的属性
|
||||
interface Aria2Attrib {
|
||||
state: 'doing' | 'done' | 'request' | 'error',//状态,错误:error,发送请求:request,下载中:doing,完成:done
|
||||
speed: string,//速度
|
||||
percentage: number,//进度百分比
|
||||
eta: string,//剩余时间
|
||||
size: string,//总大小
|
||||
newSize: string,//已下载大小
|
||||
message: string,//当前的Aria2返回
|
||||
|
||||
}
|
||||
|
||||
export {Aria2Attrib}
|
||||
108
src/utils/aria2/aria2.ts
Normal file
108
src/utils/aria2/aria2.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Child, Command } from '@tauri-apps/api/shell';
|
||||
import { takeMidStr, takeRightStr } from '../utils';
|
||||
import { Aria2Attrib } from '../../type/utils/aria2';
|
||||
|
||||
class Aria2 {
|
||||
private filePath: string = '';
|
||||
private command: Command;
|
||||
private process: Child | null = null;
|
||||
|
||||
constructor(url: string, saveDir: string, saveName: string, thread: number = 8, callback: (attrib: Aria2Attrib) => void) {
|
||||
this.filePath = saveDir + saveName;
|
||||
const args = [
|
||||
'-d', saveDir,
|
||||
'-o', saveName,
|
||||
'-s', thread.toString(),
|
||||
'-x', thread.toString(),
|
||||
'--file-allocation=none',
|
||||
'-c',
|
||||
'--check-certificate=false',
|
||||
'--force-save=false',
|
||||
url
|
||||
]
|
||||
|
||||
this.command = new Command('ria2c', args);
|
||||
|
||||
this.command.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
if (output.includes('NOTICE') || output.includes('#')) {
|
||||
callback(this.parseOutput(output));
|
||||
}
|
||||
});
|
||||
|
||||
this.command.on('close', (data) => {
|
||||
this.process = null;
|
||||
const attrib: Aria2Attrib = this.parseOutput('');
|
||||
if (data.code === 0) {
|
||||
attrib.state = 'done';
|
||||
} else {
|
||||
attrib.state = 'error';
|
||||
}
|
||||
callback(attrib);
|
||||
});
|
||||
}
|
||||
|
||||
// 解析aria2的命令行输出
|
||||
private parseOutput(output: string): Aria2Attrib {
|
||||
let tempAria2Attrib: Aria2Attrib = {
|
||||
state: 'request',
|
||||
speed: '',
|
||||
percentage: 0,
|
||||
eta: '',
|
||||
size: '',
|
||||
newSize: '',
|
||||
message: output
|
||||
}
|
||||
|
||||
if (output.includes('DL:')) {//正在下载,[#46fea8 210MiB/583MiB(36%) CN:4 DL:10MiB ETA:35s]
|
||||
tempAria2Attrib.state = 'doing';
|
||||
|
||||
//速度speed,str.substring(str.indexOf("DL:") + 3, str.indexOf("iB ETA")) + 'B/S'
|
||||
|
||||
|
||||
//进度百分比,Number(str.substring(str.indexOf("B(") + 2, str.indexOf("%)"))))
|
||||
tempAria2Attrib.percentage = Number(takeMidStr(output, 'B(', '%)'))
|
||||
|
||||
|
||||
if (output.includes('ETA')) {
|
||||
tempAria2Attrib.speed = takeMidStr(output, 'DL:', 'iB ETA') + 'B/s'
|
||||
//剩余时间 eta
|
||||
tempAria2Attrib.eta = takeMidStr(output, 'ETA:', ']')
|
||||
} else {
|
||||
tempAria2Attrib.speed = takeMidStr(output, 'DL:', 'iB]') + 'B/s'
|
||||
}
|
||||
|
||||
//总大小,size
|
||||
tempAria2Attrib.size = takeMidStr(output, '/', 'iB(') + 'B'
|
||||
|
||||
//已下载大小,newSize
|
||||
tempAria2Attrib.newSize = takeRightStr(takeMidStr(output, '[#', 'iB/'), ' ') + 'B'
|
||||
} else {
|
||||
tempAria2Attrib.state = 'request'
|
||||
}
|
||||
return tempAria2Attrib
|
||||
}
|
||||
|
||||
// 启动aria2下载
|
||||
async start(): Promise<void> {
|
||||
this.process = await this.command.spawn()
|
||||
}
|
||||
|
||||
// 停止aria2下载
|
||||
async stop(): Promise<boolean> {
|
||||
if (this.process) {
|
||||
try {
|
||||
await this.process.kill();
|
||||
this.process = null;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { Aria2 };
|
||||
47
src/utils/rclone/process.ts
Normal file
47
src/utils/rclone/process.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { Command } from "@tauri-apps/api/shell";
|
||||
import { rcloneInfo } from "../../services/rclone";
|
||||
import { rclone_api_post } from "./request";
|
||||
|
||||
|
||||
async function startRclone() {
|
||||
if (rcloneInfo.process.child) {
|
||||
await stopRclone()
|
||||
}
|
||||
|
||||
//rcloneInfo.endpoint.auth.user = randomString(32)
|
||||
//rcloneInfo.endpoint.auth.pass = randomString(128)
|
||||
|
||||
rcloneInfo.endpoint.url = 'http://localhost:' + rcloneInfo.endpoint.localhost.port.toString()
|
||||
|
||||
const args = [
|
||||
'rcd',
|
||||
`--rc-addr=:${rcloneInfo.endpoint.localhost.port.toString()}`,
|
||||
`--rc-user=${rcloneInfo.endpoint.auth.user}`,
|
||||
`--rc-pass=${rcloneInfo.endpoint.auth.pass}`,
|
||||
'--rc-allow-origin=*',
|
||||
'--rc-no-auth'
|
||||
];
|
||||
|
||||
rcloneInfo.process.command = new Command('rclone', args)
|
||||
|
||||
rcloneInfo.process.log=''
|
||||
const addLog= (data:string) => { rcloneInfo.process.log += data ;
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
rcloneInfo.process.command.stdout.on('data',(data) => addLog(data))
|
||||
rcloneInfo.process.command.stderr.on('data', (data) => addLog(data))
|
||||
|
||||
rcloneInfo.process.child = await rcloneInfo.process.command.spawn()
|
||||
|
||||
}
|
||||
|
||||
async function stopRclone() {
|
||||
await rclone_api_post('/core/quit')
|
||||
if (rcloneInfo.process.child) {
|
||||
await rcloneInfo.process.child.kill()
|
||||
}
|
||||
}
|
||||
|
||||
export { startRclone, stopRclone }
|
||||
66
src/utils/rclone/request.ts
Normal file
66
src/utils/rclone/request.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Message } from "@arco-design/web-react";
|
||||
import { rcloneInfo } from "../../services/rclone";
|
||||
|
||||
let rcloneApiHeaders = {
|
||||
Authorization: `Basic ${btoa(`${rcloneInfo.endpoint.auth.user}:${rcloneInfo.endpoint.auth.pass}`)}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
|
||||
function rclone_api_post(path: string, data?: object, ignoreError?: boolean) {
|
||||
|
||||
|
||||
if (!data) data = {}
|
||||
|
||||
// 以 base64 编码的方式来设置账密字符串
|
||||
const base64Credentials = btoa(`${rcloneInfo.endpoint.auth.user}:${rcloneInfo.endpoint.auth.pass}`);
|
||||
|
||||
// 定义请求头部,包括授权头部
|
||||
rcloneApiHeaders = {
|
||||
Authorization: `Basic ${base64Credentials}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
return fetch(rcloneInfo.endpoint.url + path, {
|
||||
method: 'POST',
|
||||
headers: rcloneApiHeaders,
|
||||
body: JSON.stringify(data)
|
||||
}).then((response) => {
|
||||
if (!response.ok && !ignoreError) {
|
||||
printError(response);
|
||||
}
|
||||
return response.json();
|
||||
}).then((jsonResponse) => {
|
||||
return jsonResponse;
|
||||
}).catch((error) => {
|
||||
if (ignoreError) { return }
|
||||
printError(error);
|
||||
});
|
||||
}
|
||||
|
||||
async function printError(error: Response) {
|
||||
console.log(error);
|
||||
|
||||
let str = ''
|
||||
|
||||
if (error.status) {
|
||||
str += `HTTP ${error.status} - ${error.statusText}\n`
|
||||
}
|
||||
if (error.body) {
|
||||
str += "\n" + (await error.json()).error;
|
||||
}
|
||||
if (str) {
|
||||
Message.error('Error:' + str);
|
||||
}
|
||||
}
|
||||
|
||||
/* export function rclone_api_get(path:string){
|
||||
return fetch(rcloneApiEndpoint + path,{
|
||||
method: 'GET',
|
||||
headers
|
||||
}).then((res)=>{
|
||||
return res.json()
|
||||
})
|
||||
} */
|
||||
|
||||
export { rclone_api_post, rcloneApiHeaders }
|
||||
30
src/utils/tauri/cmd.ts
Normal file
30
src/utils/tauri/cmd.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Command } from '@tauri-apps/api/shell';
|
||||
|
||||
async function runCmd(cmd: string, args: string[]): Promise<string> {
|
||||
// Create a new Command instance with the provided command and arguments.
|
||||
const commandInstance = new Command(cmd, args);
|
||||
|
||||
try {
|
||||
// Execute the command and wait for its completion.
|
||||
const result = await commandInstance.execute();
|
||||
console.log(result);
|
||||
|
||||
// Ensure the command execution was successful.
|
||||
if (result.code === 0) {
|
||||
// Return the command's output as a string.
|
||||
return result.stdout;
|
||||
} else {
|
||||
throw new Error(`Command failed with exit code ${result.code}: ${cmd} ${args.join(' ')}\nError: ${result.stderr}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to execute command: ${cmd} ${args.join(' ')}\n${error.message}`);
|
||||
} else {
|
||||
throw new Error(`Unknown error occurred while executing command: ${cmd} ${args.join(' ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default runCmd;
|
||||
|
||||
export { runCmd }
|
||||
15
src/utils/tauri/osInfo.ts
Normal file
15
src/utils/tauri/osInfo.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { os } from "@tauri-apps/api";
|
||||
import { OSInfo } from "../../type/config";
|
||||
import { setOsInfo } from "../../services/config";
|
||||
|
||||
async function getOsInfo() {
|
||||
setOsInfo({
|
||||
arch: await os.arch(),
|
||||
osType: await os.type(),
|
||||
platform: await os.platform(),
|
||||
tempDir: await os.tempdir(),
|
||||
osVersion: await os.version(),
|
||||
})
|
||||
}
|
||||
|
||||
export { getOsInfo }
|
||||
105
src/utils/utils.ts
Normal file
105
src/utils/utils.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { fs, invoke } from "@tauri-apps/api";
|
||||
import { runCmd } from "./tauri/cmd";
|
||||
|
||||
export function isEmptyObject(back: any): boolean {
|
||||
return Object.keys(back).length === 0 && back.constructor === Object;
|
||||
}
|
||||
|
||||
export function getURLSearchParam(name: string): string {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
return searchParams.get(name) || '';
|
||||
}
|
||||
|
||||
export function getProperties(obj: Record<string, any>) {
|
||||
|
||||
let result: Array<{ key: any, value: any }> = []
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
result.push({ key: key, value: obj[key] })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function formatSize(v: number) {
|
||||
let UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'ZB'];
|
||||
let prev = 0, i = 0;
|
||||
while (Math.floor(v) > 0 && i < UNITS.length) {
|
||||
prev = v;
|
||||
v /= 1024;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (i > 0 && i < UNITS.length) {
|
||||
v = prev;
|
||||
i -= 1;
|
||||
}
|
||||
return Math.round(v * 100) / 100 + ' ' + UNITS[i];
|
||||
}
|
||||
|
||||
//格式化剩余时间
|
||||
export function formatETA(etaInSeconds: number): string {
|
||||
if (isNaN(etaInSeconds) || etaInSeconds <= 0) {
|
||||
return '未知';
|
||||
}
|
||||
|
||||
const hours = Math.floor(etaInSeconds / 3600);
|
||||
const minutes = Math.floor((etaInSeconds % 3600) / 60);
|
||||
const seconds = Math.floor(etaInSeconds % 60);
|
||||
|
||||
let formattedETA = '';
|
||||
|
||||
if (hours > 0) {
|
||||
formattedETA += `${hours.toString().padStart(2, '0')}h `;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
formattedETA += `${minutes.toString().padStart(2, '0')}m `;
|
||||
}
|
||||
|
||||
formattedETA += `${seconds.toString().padStart(2, '0')}s`;
|
||||
|
||||
return formattedETA;
|
||||
}
|
||||
|
||||
export function randomString(length: number): string {
|
||||
const alphanumericChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
//const specialChars = '!@#$%^&*()_+~`|}{[]:;?><,./-=';
|
||||
const specialChars = '';
|
||||
const getRandomChar = (chars: string): string => chars[Math.floor(Math.random() * chars.length)];
|
||||
|
||||
const randomString = Array.from({ length }, () =>
|
||||
Math.random() < 0.8 ? getRandomChar(alphanumericChars) : getRandomChar(specialChars)
|
||||
).join('');
|
||||
|
||||
return randomString;
|
||||
}
|
||||
|
||||
export function takeMidStr(input: string, startMarker: string, endMarker: string): string {
|
||||
const startIndex = input.indexOf(startMarker) + startMarker.length;
|
||||
const endIndex = input.indexOf(endMarker, startIndex);
|
||||
return input.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
//取字符串右边
|
||||
export function takeRightStr(str: string, taggedStr: string) {
|
||||
return str.substring(str.indexOf(taggedStr) + taggedStr.length, str.length)
|
||||
}
|
||||
|
||||
//下载文件
|
||||
export async function downloadFile(url: string, path: string) {
|
||||
await invoke('download_file', {
|
||||
url: url,
|
||||
outPath: path
|
||||
})
|
||||
return await fs.exists(path)
|
||||
}
|
||||
|
||||
export async function getWinFspInstallState() {
|
||||
return await invoke('get_winfsp_install_state') as boolean
|
||||
}
|
||||
|
||||
export async function installWinFsp() {
|
||||
await runCmd('msiexec',['/i', 'res\\bin\\winfsp.msi','/qn'])
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user