Add files via upload

This commit is contained in:
VirtualHotBar
2024-04-27 17:26:47 +08:00
committed by GitHub
parent 50ff1d7a01
commit b64d462e2e
90 changed files with 5936 additions and 0 deletions

284
src/app.tsx Normal file
View 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 }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
}
)
})
}

View 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}

View 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 }

View 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": "FTPFile 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
View 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 }

View 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}

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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}

View 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 }

View 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}

View 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}

View 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}

View 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}

View File

@@ -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}

View 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}

View 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}

View 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}

View 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 };

View 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}

View 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 }

View 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}

View 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 };

View 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 }

View 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}

View 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}

View 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}

View 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 }

View 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}

View 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 }

View 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}

View 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 }

View 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 }

View 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
View 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());
}

View 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 }

View 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
View 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+RWindows/Linux和Command+RMac刷新页面
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
View 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
View 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
View 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
View 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
View 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 }

View 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 }

View 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
View 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 }

View 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
View 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 }

View 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 }

View 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
View 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
View 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 }

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

60
src/type/rclone/rcloneInfo.d.ts vendored Normal file
View 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
View 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
View 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 }

View 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 }

View 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}

View 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}

View 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}

View 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}

View 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}

View 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 }

View 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
View 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
View 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
View 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 };

View 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 }

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />