style: eslint config

This commit is contained in:
山田 2022-08-07 12:55:43 +08:00
parent b71a55d83f
commit afa88d0396
19 changed files with 720 additions and 612 deletions

10
.eslintignore Normal file
View File

@ -0,0 +1,10 @@
# .eslintignore
*.svg
*.css
*.less
/dist
*.scss
*.css
/node_modules
/build
vite.config.ts

74
.eslintrc.js Normal file
View File

@ -0,0 +1,74 @@
module.exports = {
env: {
browser: true,
es2021: true,
es6: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'airbnb',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: [
'react',
'react-hooks',
'@typescript-eslint',
'eslint-plugin-import',
],
settings: {
'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
moduleDirectory: ['node_modules', 'src/'],
},
alias: {
map: [['@', './src']],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/jsx-filename-extension': [1, { extensions: ['.ts', '.tsx'] }],
'react-hooks/rules-of-hooks': 'error', // 检查 Hook 的规则
'react-hooks/exhaustive-deps': 'warn', // 检查 effect 的依赖
'no-console': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-unused-vars': 'off',
'import/no-extraneous-dependencies': 'off',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error'],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
};

View File

@ -1 +1 @@
module.exports = {extends: ['@commitlint/config-angular']}
module.exports = { extends: ['@commitlint/config-angular'] };

View File

@ -1,96 +1,101 @@
import { app, BrowserWindow, shell, ipcMain } from 'electron'
import { release } from 'os'
import { join } from 'path'
/* eslint-disable import/prefer-default-export */
import {
app, BrowserWindow, shell, ipcMain,
} from 'electron';
import { release } from 'os';
import { join } from 'path';
import installExtension, {
REACT_DEVELOPER_TOOLS,REDUX_DEVTOOLS
REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS,
} from 'electron-devtools-installer';
// Disable GPU Acceleration for Windows 7
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
if (release().startsWith('6.1')) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
if (process.platform === 'win32') app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
app.quit();
process.exit(0);
}
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
export const ROOT_PATH = {
// /dist
dist: join(__dirname, '../..'),
// /dist or /public
public: join(__dirname, app.isPackaged ? '../..' : '../../../public'),
}
};
let win: BrowserWindow | null = null
let win: BrowserWindow | null = null;
// Here, you can also use other preload
const preload = join(__dirname, '../preload/index.js')
const preload = join(__dirname, '../preload/index.js');
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin
const url = `http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}`
const indexHtml = join(ROOT_PATH.dist, 'index.html')
// eslint-disable-next-line dot-notation
const url = `http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}`;
const indexHtml = join(ROOT_PATH.dist, 'index.html');
async function createWindow() {
win = new BrowserWindow({
title: 'Main window',
icon: join(ROOT_PATH.public, 'favicon.svg'),frame:false,
icon: join(ROOT_PATH.public, 'favicon.svg'),
frame: false,
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false,
},
})
});
if (app.isPackaged) {
win.loadFile(indexHtml)
win.loadFile(indexHtml);
} else {
win.loadURL(url)
win.loadURL(url);
// win.webContents.openDevTools()
}
// Test actively push message to the Electron-Renderer
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString())
})
win?.webContents.send('main-process-message', new Date().toLocaleString());
});
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' }
})
win.webContents.setWindowOpenHandler((event) => {
if (event.url.startsWith('https:')) shell.openExternal(event.url);
return { action: 'deny' };
});
// open devtools
win.webContents.openDevTools()
win.webContents.openDevTools();
}
app.whenReady().then(createWindow).then(()=>{
app.whenReady().then(createWindow).then(() => {
installExtension(REACT_DEVELOPER_TOOLS.id)
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err));
})
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err));
});
app.on('window-all-closed', () => {
win = null
if (process.platform !== 'darwin') app.quit()
})
win = null;
if (process.platform !== 'darwin') app.quit();
});
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore()
win.focus()
if (win.isMinimized()) win.restore();
win.focus();
}
})
});
app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows()
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus()
allWindows[0].focus();
} else {
createWindow()
createWindow();
}
})
});
// new window example arg: new windows url
ipcMain.handle('open-win', (event, arg) => {
@ -98,24 +103,24 @@ ipcMain.handle('open-win', (event, arg) => {
webPreferences: {
preload,
},
})
});
if (app.isPackaged) {
childWindow.loadFile(indexHtml, { hash: arg })
childWindow.loadFile(indexHtml, { hash: arg });
} else {
childWindow.loadURL(`${url}/#${arg}`)
childWindow.loadURL(`${url}/#${arg}`);
// childWindow.webContents.openDevTools({ mode: "undocked", activate: true })
}
})
});
ipcMain.handle('close-win', (event) => {
app.exit()
})
app.exit();
});
ipcMain.handle('minimize-win', (event) => {
win.minimize()
})
win.minimize();
});
ipcMain.handle('maximize-win', (event) => {
win.maximize()
})
win.maximize();
});

View File

@ -1,28 +1,32 @@
/* eslint-disable no-undef */
/* eslint-disable no-unused-expressions */
/* eslint-disable react-hooks/rules-of-hooks */
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
return new Promise(resolve => {
return new Promise((resolve) => {
if (condition.includes(document.readyState)) {
resolve(true)
resolve(true);
} else {
document.addEventListener('readystatechange', () => {
if (condition.includes(document.readyState)) {
resolve(true)
resolve(true);
}
})
});
}
})
});
}
const safeDOM = {
append(parent: HTMLElement, child: HTMLElement) {
if (!Array.from(parent.children).find(e => e === child)) {
return parent.appendChild(child)
class safeDOM {
static append(parent: HTMLElement, child: HTMLElement) {
if (!Array.from(parent.children).find((e) => e === child)) {
parent.appendChild(child);
}
},
remove(parent: HTMLElement, child: HTMLElement) {
if (Array.from(parent.children).find(e => e === child)) {
return parent.removeChild(child)
}
static remove(parent: HTMLElement, child: HTMLElement) {
if (Array.from(parent.children).find((e) => e === child)) {
parent.removeChild(child);
}
},
}
}
/**
@ -32,7 +36,7 @@ const safeDOM = {
* https://matejkustec.github.io/SpinThatShit
*/
function useLoading() {
const className = `loaders-css__square-spin`
const className = 'loaders-css__square-spin';
const styleContent = `
@keyframes square-spin {
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
@ -59,34 +63,34 @@ function useLoading() {
background: #282c34;
z-index: 9;
}
`
const oStyle = document.createElement('style')
const oDiv = document.createElement('div')
`;
const oStyle = document.createElement('style');
const oDiv = document.createElement('div');
oStyle.id = 'app-loading-style'
oStyle.innerHTML = styleContent
oDiv.className = 'app-loading-wrap'
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
oStyle.id = 'app-loading-style';
oStyle.innerHTML = styleContent;
oDiv.className = 'app-loading-wrap';
oDiv.innerHTML = `<div class="${className}"><div></div></div>`;
return {
appendLoading() {
safeDOM.append(document.head, oStyle)
safeDOM.append(document.body, oDiv)
safeDOM.append(document.head, oStyle);
safeDOM.append(document.body, oDiv);
},
removeLoading() {
safeDOM.remove(document.head, oStyle)
safeDOM.remove(document.body, oDiv)
safeDOM.remove(document.head, oStyle);
safeDOM.remove(document.body, oDiv);
},
}
};
}
// ----------------------------------------------------------------------
const { appendLoading, removeLoading } = useLoading()
domReady().then(appendLoading)
const { appendLoading, removeLoading } = useLoading();
domReady().then(appendLoading);
window.onmessage = ev => {
ev.data.payload === 'removeLoading' && removeLoading()
}
window.onmessage = (ev) => {
ev.data.payload === 'removeLoading' && removeLoading();
};
setTimeout(removeLoading, 4999)
setTimeout(removeLoading, 4999);

View File

@ -10,31 +10,29 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build && electron-builder",
"commit": "cz"
"commit": "cz",
"prepare": "husky install",
"postinstall": "pnpm prepare",
"lint": "eslint . --ext .js,.ts,.tsx --fix --ignore-path .gitignore"
},
"engines": {
"node": ">=14.17.0"
},
"dependencies": {
"better-scroll": "^2.4.2",
"classnames": "^2.3.1",
"echarts": "^5.3.3",
"electron-store": "^8.0.1",
"normalize.css": "^8.0.1",
"winston": "^3.8.1"
},
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@commitlint/cli": "^17.0.3",
"@commitlint/config-angular": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@iconify/json": "^2.1.81",
"@koale/useworker": "^4.0.2",
"@types/d3": "^7.4.0",
"@types/fs-extra": "^9.0.13",
"@types/node": "^18.0.3",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/winston": "^2.4.4",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@unocss/preset-attributify": "^0.44.5",
"@unocss/preset-icons": "^0.44.5",
"@unocss/preset-uno": "^0.44.5",
@ -42,24 +40,38 @@
"@vitejs/plugin-react": "^1.3.2",
"ahooks": "^3.5.2",
"antd": "^4.21.5",
"better-scroll": "^2.4.2",
"classnames": "^2.3.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"d3": "^7.6.1",
"dayjs": "^1.11.3",
"echarts": "^5.3.3",
"electron": "^19.0.3",
"electron-builder": "^23.0.3",
"electron-devtools-installer": "^3.2.0",
"electron-store": "^8.0.1",
"eslint": "^7.32.0 || ^8.2.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^3.4.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.6.0",
"fs-extra": "^10.1.0",
"husky": "^8.0.1",
"less": "^4.1.3",
"normalize.css": "^8.0.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-router-dom": "6",
"typescript": "^4.7.3",
"unocss": "^0.44.0",
"unocss": "^0.44.1",
"unocss-preset-scrollbar": "^0.2.1",
"vite": "^2.9.10",
"vite-plugin-electron": "^0.6.2"
"vite-plugin-electron": "^0.6.2",
"winston": "^3.8.1"
},
"env": {
"VITE_DEV_SERVER_HOST": "127.0.0.1",

View File

@ -1,25 +1,29 @@
import { useState } from 'react'
import FileManage from '@/pages/fileStore'
/* eslint-disable react/button-has-type */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { useState } from 'react';
import {
SettingOutlined,
RadarChartOutlined,
FileTextOutlined,
MinusOutlined,
FullscreenOutlined,
CloseOutlined,
} from '@ant-design/icons';
import { Layout, Menu, Space } from 'antd';
import type { MenuProps } from 'antd';
import { BrowserRouter, Route, Routes, useNavigate } from 'react-router-dom'
import {
Route, Routes, useNavigate,
} from 'react-router-dom';
import classNames from 'classnames';
import Analysis from '@/pages/analysis';
import Settings from '@/pages/settings';
import styles from "./App.module.less"
import classNames from 'classnames';
const { Header, Footer, Sider, Content } = Layout;
const { ipcRenderer } = require('electron')
import FileManage from '@/pages/fileStore';
import styles from './App.module.less';
const App: React.FC = () => {
let navigate = useNavigate();
const {
Header, Footer, Sider, Content,
} = Layout;
const { ipcRenderer } = require('electron');
function App() {
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
type MenuItem = Required<MenuProps>['items'][number];
function getItem(
@ -45,26 +49,26 @@ const App: React.FC = () => {
];
const closeApp = () => {
ipcRenderer.invoke('close-win')
}
ipcRenderer.invoke('close-win');
};
const minimizeApp = () => {
ipcRenderer.invoke('minimize-win')
}
ipcRenderer.invoke('minimize-win');
};
const maximizeApp = () => {
ipcRenderer.invoke('maximize-win')
}
ipcRenderer.invoke('maximize-win');
};
return (
<Layout>
<Header style={{ height: 45 }} className={classNames('flex justify-between items-center text-white', styles['ant-layout-header'])}>
<div className='text-xl text-black dark:text-white font-bold' >
<div className="text-xl text-black dark:text-white font-bold">
MIB
</div>
<div>
<Space size='middle' className='text-md'>
<button className="opBtn dark:text-white i-carbon-sun dark:i-carbon-moon" />
<button className="opBtn dark:text-white i-codicon:chrome-minimize" onClick={() => minimizeApp()} />
<button className="opBtn dark:text-white i-fluent:full-screen-maximize-20-filled" onClick={() => maximizeApp()} />
<button className="opBtn dark:text-white i-eva:close-fill" onClick={() => closeApp()} />
<Space size="middle" className="text-md">
<button className="opBtn dark:text-white i-carbon-sun dark:i-carbon-moon" />
<button className="opBtn dark:text-white i-codicon:chrome-minimize" onClick={() => minimizeApp()} />
<button className="opBtn dark:text-white i-fluent:full-screen-maximize-20-filled" onClick={() => maximizeApp()} />
<button className="opBtn dark:text-white i-eva:close-fill" onClick={() => closeApp()} />
</Space>
</div>
</Header>
@ -79,15 +83,15 @@ const App: React.FC = () => {
</div>
</Sider>
<Content style={{ padding: '12px', minHeight: 'calc( 100vh - 50px )' }}>
<Routes >
<Route path='/analysis' element={<Analysis />} />
<Route path='/fileManage' element={<FileManage />} />
<Routes>
<Route path="/analysis" element={<Analysis />} />
<Route path="/fileManage" element={<FileManage />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Content>
</Layout>
</Layout>
)
);
}
export default App
export default App;

View File

@ -1,6 +1,5 @@
import { DriverType } from "@/types";
import React, { forwardRef, ReactNode, Ref } from "react";
import styles from './index.module.scss'
import React, { forwardRef, ReactNode, Ref } from 'react';
import { DriverType } from '@/types';
export interface ControlOptionType {
label: string;
@ -13,21 +12,24 @@ interface PropType {
curType: DriverType,
onRow: (e: any) => any
}
const controlPanel = ({ styles, options, curType, onRow }: PropType, ref: any) => {
const controlPanel = ({
styles, options, curType, onRow,
}: PropType, ref: any) => {
// 过滤
const list = options.filter(i => i.show.includes(curType))
const list = options.filter((i) => i.show.includes(curType));
return (
<div ref={ref} style={styles} className='absolute rounded-md shadow-md overflow-hidden m-2 z-10 min-w-130px wa bg-white'>
<div ref={ref} style={styles} className="absolute rounded-md shadow-md overflow-hidden m-2 z-10 min-w-130px wa bg-white">
{
list.length !== 0 ? list.map(({ label, show,key}) => {
const events = onRow({label,show,key})
return (<div className="transition-colors-500 px-6 py-1 last-pb-2 first-pt-2 hover-bg-#eee cursor-pointer" {...events} key={key}>{label}</div>)
list.length !== 0 ? list.map(({ label, show, key }) => {
const events = onRow({ label, show, key });
// eslint-disable-next-line react/jsx-props-no-spreading
return (<div className="transition-colors-500 px-6 py-1 last-pb-2 first-pt-2 hover-bg-#eee cursor-pointer" {...events} key={key}>{label}</div>);
}) : <div className="transition-colors-500 px-6 py-1 last-pb-2 first-pt-2 hover-bg-#eee cursor-pointer"></div>
}
</div>
)
}
export default forwardRef<HTMLDivElement, PropType>(controlPanel)
);
};
export default forwardRef<HTMLDivElement, PropType>(controlPanel);

View File

@ -1,17 +1,18 @@
const { readJsonSync, pathExistsSync, outputJsonSync } =require("fs-extra") ;
import { platform, env } from "process";
import path from "path";
import { ConfigType } from "@/types";
import { platform, env } from 'process';
import path from 'path';
import { ConfigType } from '@/types';
export const home = platform === "win32" ? env.USERPROFILE : env.HOME;
const { readJsonSync, pathExistsSync, outputJsonSync } = require('fs-extra');
const CONFIG_PATH: string = path.join(home || "~/", ".mibrc");
export const home = platform === 'win32' ? env.USERPROFILE : env.HOME;
const CONFIG_PATH: string = path.join(home || '~/', '.mibrc');
const existConf = () => pathExistsSync(CONFIG_PATH);
const createDefaultConfig = (): ConfigType => {
const conf: ConfigType = {
backups: [],
output: "C:/",
output: 'C:/',
};
outputJsonSync(CONFIG_PATH, conf);
return readJsonSync(CONFIG_PATH);

View File

@ -1,20 +1,20 @@
export enum ThemeType {
DARK = "dark",
LIGHT = "light",
DARK = 'dark',
LIGHT = 'light',
}
let theme = localStorage.getItem("theme") as ThemeType;
const theme = localStorage.getItem('theme') as ThemeType;
export async function loadTheme(type:ThemeType=theme ?? ThemeType.LIGHT) {
export async function loadTheme(type:ThemeType = theme ?? ThemeType.LIGHT) {
// 获取主题
if (type === ThemeType.LIGHT) {
import('./light/index.less');
localStorage.setItem('theme',ThemeType.LIGHT)
document.documentElement.classList.add(ThemeType.LIGHT)
localStorage.setItem('theme', ThemeType.LIGHT);
document.documentElement.classList.add(ThemeType.LIGHT);
}
if (type === ThemeType.DARK) {
import('./dark/index.less');
localStorage.setItem('theme',ThemeType.DARK)
document.documentElement.classList.add(ThemeType.DARK)
localStorage.setItem('theme', ThemeType.DARK);
document.documentElement.classList.add(ThemeType.DARK);
}
}

View File

@ -1,15 +1,14 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// import './samples/node-api'
// import 'normalize.css'
import 'antd/dist/antd.less';
await loadTheme()
import 'styles/index.css'
import 'uno.css'
/* eslint-disable import/no-unresolved */
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { loadTheme } from './lib/css/theme'
import App from './App';
import 'antd/dist/antd.less';
import 'styles/index.css';
import 'uno.css';
import { loadTheme } from './lib/css/theme';
await loadTheme();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
@ -18,7 +17,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Route path="/*" element={<App />} />
</Routes>
</BrowserRouter>
</React.StrictMode>
)
</React.StrictMode>,
);
postMessage({ payload: 'removeLoading' }, '*')
postMessage({ payload: 'removeLoading' }, '*');

View File

@ -1,39 +1,39 @@
import { BackItemType, ConfigType, DriverType, FileNodeType } from "@/types";
import { createFileNodeWithADB, diff, execAdb, getFileNodeList, getFileSize, isPath, log, openNotification, pathRepair, speedReg } from "@/utils"
import ignoreFileList from "@/utils/ignoreFileList";
import { useMount } from "ahooks";
import { Button, Card, Modal, Space, Tag } from "antd";
import { useEffect, useRef, useState } from "react";
import {
Button, Card, Modal, Space, Tag,
} from 'antd';
import {
useEffect, useRef, useState, Key,
} from 'react';
import * as echarts from 'echarts/core';
import {
TooltipComponent,
TooltipComponentOption,
LegendComponent,
LegendComponentOption
} from 'echarts/components';
import { BarChart, BarSeriesOption } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import styles from './index.module.less'
import * as d3 from "d3";
import Table, { ColumnsType } from "antd/lib/table";
import * as d3 from 'd3';
import Table, { ColumnsType } from 'antd/lib/table';
import { getConfig } from '@/config';
import {
diff, execAdb, getFileNodeList, isPath, log, openNotification, pathRepair, speedReg,
} from '@/utils';
import {
BackItemType, ConfigType, DriverType, FileNodeType,
} from '@/types';
import styles from './index.module.less';
const { confirm } = Modal;
import { getConfig } from "@/config"
import { ExclamationCircleOutlined } from "@ant-design/icons";
echarts.use([
TooltipComponent,
LegendComponent,
BarChart,
CanvasRenderer,
LabelLayout
LabelLayout,
]);
type EChartsOption = echarts.ComposeOption<
TooltipComponentOption | LegendComponentOption | BarSeriesOption
>;
// 图标实例
let Chart
const backupNodeColumns: ColumnsType<BackItemType> = [
{
title: '节点描述',
@ -53,89 +53,56 @@ const backupNodeColumns: ColumnsType<BackItemType> = [
align: 'center',
render: (_: any, record: BackItemType) => (
<Space>
<a></a>
<a></a>
<Button type="link"></Button>
<Button type="link"></Button>
</Space>
)
),
},
];
export default () => {
const test_path = '/sdcard/DCIM/Camera/'
export default function Analysis() {
const testPath = '/sdcard/DCIM/Camera/';
// const test_path = '/sdcard/MIUI/sound_recorder/app_rec/'
// 文件列表
const [fileNodeList, setFileNodeList] = useState<FileNodeType[]>([])
const [fileNodeList, setFileNodeList] = useState<FileNodeType[]>([]);
// 备份状态
const [backing, setBacking] = useState<boolean>(false)
const [backing, setBacking] = useState<boolean>(false);
// 备份目录
const [config, setConfig] = useState<ConfigType>(getConfig())
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const chartRef = useRef<HTMLDivElement | null>(null)
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
const [config, setConfig] = useState<ConfigType>(getConfig());
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]);
const chartRef = useRef<HTMLDivElement | null>(null);
const onSelectChange = (newSelectedRowKeys: Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
};
useEffect(() => {
// 备份节点更改
console.log(selectedRowKeys)
}, [selectedRowKeys])
console.log(selectedRowKeys);
}, [selectedRowKeys]);
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
// useMount(() => {
// // readDir(test_path)
// var svg = d3.select(chartRef.current)
// .append("svg")
// .attr("width", 300)
// .attr("height", 120);
// let contain = svg
// .append("rect")
// .attr("width", 300)
// .attr("height", 50)
// .attr("stroke", 'white')
// .attr("fill", '#eee')
// .attr("rx", 5)
// .attr("yx", 5)
// var imgRect = svg.append("rect")
// .attr("id", 'img')
// .attr("x", 0)
// .attr("y", 0)
// .attr("width", 50)
// .attr("height", 50)
// .attr("rx", 2)
// .attr("yx", 2)
// .attr("fill", "red")
// var videoRect = svg.append("rect") //添加一个矩形
// .attr("id", 'video')
// .attr("x", 120)
// .attr("y", 0)
// .attr("width", 50)
// .attr("height", 50)
// .attr("rx", 2)
// .attr("yx", 2)
// .attr("fill", "blue")
// })
function move(backupQueue: FileNodeType[], outputDir: string): void {
if (backupQueue.length === 0) {
log("无需备份");
log('无需备份');
return;
}
for (const fileN of backupQueue) {
backupQueue.forEach((fileN) => {
log(`正在备份${fileN.fileName}`);
try {
const out: string = execAdb(
`pull "${fileN.filePath}" "${outputDir + fileN.fileName}"`,
);
const speed: string | null = out.match(speedReg) !== null ? out.match(speedReg)![0] : "读取速度失败";
const speed: string | null = out.match(speedReg) !== null ? out.match(speedReg)![0] : '读取速度失败';
log(`平均传输速度${speed}`);
} catch (e: any) {
log(`备份${fileN.fileName}失败 error:${e.message}`, "error");
log(`备份${fileN.fileName}失败 error:${e.message}`, 'error');
}
}
};
});
}
function backup() {
setBacking(true)
setBacking(true);
// 确定是否备份
// confirm({
// title: '',
@ -149,83 +116,87 @@ export default () => {
// },
// });
// 判断是否有选择的节点
console.log('备份节点', selectedRowKeys)
console.log('备份节点', selectedRowKeys);
// 备份选择的节点
// 获取节点完整信息
let curBackupNode = config.backups.filter(item => selectedRowKeys.includes(item.comment))
let outputRootDir = config.output
const curBackupNode = config.backups.filter((item) => selectedRowKeys.includes(item.comment));
const outputRootDir = config.output;
// 判断根路径
isPath(outputRootDir);
for (const item of curBackupNode) {
curBackupNode.forEach((item) => {
// 依次读取对比
// 获取指定目录下的文件、文件夹列表
let waitBackupFileList: FileNodeType[] = []
let dirPath = item.path
let dirList = execAdb(`shell ls -l ${dirPath}`).toString().split("\r\n").filter(i => i !== '');
const waitBackupFileList: FileNodeType[] = [];
const dirPath = item.path;
const dirList = execAdb(`shell ls -l ${dirPath}`).toString().split('\r\n').filter((i) => i !== '');
// 去掉total
dirList.shift()
dirList.shift();
dirList.forEach((i) => {
const item: string[] = i.split(/\s+/);
const fileName = item.slice(7).join(" ");
const splitItem: string[] = i.split(/\s+/);
const fileName = splitItem.slice(7).join(' ');
const fileNode: FileNodeType = {
fileName,
fileSize: Number(item[4]) ?? 0,
fileSize: Number(splitItem[4]) ?? 0,
filePath: pathRepair(dirPath) + fileName,
isDirectory: item[0].startsWith("d"),
fileMTime: item.slice(5, 7).join(' ')
}
waitBackupFileList.push(fileNode)
isDirectory: splitItem[0].startsWith('d'),
fileMTime: splitItem.slice(5, 7).join(' '),
};
waitBackupFileList.push(fileNode);
});
// 判断导出路径是否存在
const folderName = item.path.split("/").filter((i: string) => i !== "").at(-1);
const folderName = item.path.split('/').filter((i: string) => i !== '').at(-1);
// 判断节点内是否有备份目录 // 拼接导出路径
const itemRootPath = pathRepair(pathRepair(config.output) + folderName);
const outputDir = item.output
? item.output && pathRepair(item.output)
: itemRootPath;
isPath(outputDir)
isPath(outputDir);
// 获取当前目录下的文件
// 获取当前存储空间
const localFileNodeList = getFileNodeList(outputDir, DriverType.LOCAL)
const localFileNodeList = getFileNodeList(outputDir, DriverType.LOCAL);
// 对比文件
const diffList: FileNodeType[] = diff(localFileNodeList, waitBackupFileList);
console.log(localFileNodeList)
console.log(waitBackupFileList)
console.log('diffList', diffList)
console.log(localFileNodeList);
console.log(waitBackupFileList);
console.log('diffList', diffList);
// 备份
move(diffList, outputDir)
}
move(diffList, outputDir);
});
// 完成弹窗
openNotification('提示', '备份完成')
openNotification('提示', '备份完成');
// 全部备份
setBacking(false)
setBacking(false);
}
const hasSelected = selectedRowKeys.length > 0;
function tableFooter() {
return (
<div >
{hasSelected ? <div>
<Space>
{selectedRowKeys.length}
<Button onClick={()=>setSelectedRowKeys([])}></Button>
</Space>
</div> : ''}
<div>
{hasSelected ? (
<div>
<Space>
{' '}
{selectedRowKeys.length}
{' '}
<Button onClick={() => setSelectedRowKeys([])}></Button>
</Space>
</div>
) : ''}
</div>
)
);
}
return (
<div>
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
<Card>
{/* 节点 */}
<Table rowSelection={rowSelection} footer={selectedRowKeys.length!==0?tableFooter:undefined} scroll={{ x: '100%', scrollToFirstRowOnChange: true, y: '300px' }} pagination={false} rowKey='comment' columns={backupNodeColumns} dataSource={config.backups} />
<Table rowSelection={rowSelection} footer={selectedRowKeys.length !== 0 ? tableFooter : undefined} scroll={{ x: '100%', scrollToFirstRowOnChange: true, y: '300px' }} pagination={false} rowKey="comment" columns={backupNodeColumns} dataSource={config.backups} />
{/* 图表 */}
{/* <div className={styles.chartContain} ref={chartRef}>
@ -239,5 +210,5 @@ export default () => {
</Card>
</Space>
</div>
)
);
}

View File

@ -1,18 +1,29 @@
import { useClickAway, useMount, useSetState } from "ahooks"
import path from 'path'
import { AppstoreOutlined, DesktopOutlined, MobileOutlined, RetweetOutlined, RollbackOutlined, SearchOutlined } from '@ant-design/icons'
import { createRef, useEffect, useRef, useState } from "react";
import ignoreFileList from "@/utils/ignoreFileList";
import Table, { ColumnsType } from "antd/lib/table";
const { pathExistsSync, readdirSync, statSync } = require('fs-extra')
import dayjs from 'dayjs'
import { createFileNode, execAdb, openNotification, readablizeBytes } from "@/utils";
import { DriverType, FileNodeType } from "@/types";
import { Breadcrumb, Button, Card, ConfigProvider, Empty, Input, message, Switch } from "antd";
import styles from './index.module.less'
import classnames from "classnames";
import Control, { ControlOptionType } from "@/components/control";
import { exec } from "child_process";
import { useClickAway, useMount } from 'ahooks';
import path from 'path';
import {
DesktopOutlined, MobileOutlined, SearchOutlined,
} from '@ant-design/icons';
import {
useCallback,
useEffect, useRef, useState,
} from 'react';
import Table, { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import {
Breadcrumb, Button, Card, ConfigProvider, Empty, Input, message, Switch,
} from 'antd';
import classnames from 'classnames';
import { exec } from 'child_process';
import ignoreFileList from '@/utils/ignoreFileList';
import {
createFileNode, execAdb, openNotification, readablizeBytes,
} from '@/utils';
import { DriverType, FileNodeType } from '@/types';
import Control, { ControlOptionType } from '@/components/control';
import styles from './index.module.less';
const { pathExistsSync, readdirSync } = require('fs-extra');
const columns: ColumnsType<FileNodeType> = [
{
title: '文件名称',
@ -23,153 +34,159 @@ const columns: ColumnsType<FileNodeType> = [
title: '文件大小',
dataIndex: 'fileSize',
key: 'fileSize',
render: val => readablizeBytes(val),
render: (val) => readablizeBytes(val),
sorter: (a, b) => a.fileSize - b.fileSize,
},
{
title: '修改时间',
dataIndex: 'fileMTime',
key: 'fileMTime',
render: time => <span>{dayjs(time).format("YYYY-MM-DD hh:mm:ss")}</span>
render: (time) => <span>{dayjs(time).format('YYYY-MM-DD hh:mm:ss')}</span>,
},
];
export default function fileManage() {
interface FileListEmptyProps {
turnBack: () => void
}
function FileListEmpty({ turnBack }: FileListEmptyProps) {
return (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前目录没有文件/文件夹哦">
<Button onClick={() => turnBack()}></Button>
</Empty>
);
}
export default function FileManage() {
// 本地文件列表
const [localFileNodeList, setLocalFileNodeList] = useState<FileNodeType[]>([])
const [localFileNodeList, setLocalFileNodeList] = useState<FileNodeType[]>([]);
// 移动设备文件列表
const [mobileFileNodeList, setMobileFileNodeList] = useState<FileNodeType[]>([])
const [mobileFileNodeList, setMobileFileNodeList] = useState<FileNodeType[]>([]);
// loading
const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(true);
// 当前文件路径
const [localPathCollection, setLocalPathCollection] = useState(['d:/'])
const [localPathCollection, setLocalPathCollection] = useState(['d:/']);
// 当前路径
const [MobilePathCollection, setMobilePathCollection] = useState(['sdcard/', 'DCIM', 'Camera'])
const [MobilePathCollection, setMobilePathCollection] = useState(['sdcard/', 'DCIM', 'Camera']);
// 搜索框
const [searchVal, setSearchVal] = useState<string>('')
const [searchVal, setSearchVal] = useState<string>('');
// 显示状态
const [curDriType, setCurDriType] = useState<DriverType>(DriverType.LOCAL)
const [curDriType, setCurDriType] = useState<DriverType>(DriverType.LOCAL);
// 右击
const controlPanelRef = useRef<HTMLDivElement | null>(null)
const controlPanelRef = useRef<HTMLDivElement | null>(null);
// 控制面板坐标
interface ControlPanelStyleType {
left: number;
top: number;
visibility: 'visible' | 'hidden'
}
useClickAway(() => {
setControlPanelStyle({ ...controlPanelStyle, visibility: 'hidden' })
}, controlPanelRef);
const [controlPanelStyle, setControlPanelStyle] = useState<ControlPanelStyleType>({
left: 0,
top: 0,
visibility: 'hidden'
})
visibility: 'hidden',
});
useClickAway(() => {
setControlPanelStyle({ ...controlPanelStyle, visibility: 'hidden' });
}, controlPanelRef);
const rightDownOperations: ControlOptionType[] = [
{
label: '打开',
show: [DriverType.LOCAL, DriverType.MOBILE],
key: 'open'
key: 'open',
},
{
label: '添加到备份节点', show: [DriverType.MOBILE]
, key: 'addBackNodeList'
label: '添加到备份节点',
show: [DriverType.MOBILE],
key: 'addBackNodeList',
},
{
label: '添加到忽略名单', show: [DriverType.LOCAL]
, key: 'addIgnoreList'
label: '添加到忽略名单',
show: [DriverType.LOCAL],
key: 'addIgnoreList',
},
{
label: '刷新',
show: [DriverType.LOCAL, DriverType.MOBILE],
key: 'reload'
key: 'reload',
},
{
label: '重命名',
show: [DriverType.LOCAL, DriverType.MOBILE],
key: 'rename'
key: 'rename',
},
]
];
// 读取移动设备目录
function readMobileDriverDir(target: string) {
// 清空原列表
setMobileFileNodeList([])
setMobileFileNodeList([]);
console.log('trigger readDir');
// todo 判断路径是否存在
// 获取指定目录下的文件、文件夹列表
let dirList = execAdb(`shell ls -l ${target}`).toString().split("\r\n").filter(i => i !== '');
const dirList = execAdb(`shell ls -l ${target}`).toString().split('\r\n').filter((i) => i !== '');
// 去掉total
dirList.shift()
dirList.shift();
dirList.forEach((i) => {
const item: string[] = i.split(/\s+/);
const fileName = item.slice(7).join(" ");
const fileName = item.slice(7).join(' ');
const fileNode: FileNodeType = {
fileName,
fileSize: Number(item[4]) ?? 0,
filePath: target + fileName,
isDirectory: item[0].startsWith("d"),
fileMTime: item.slice(5, 7).join(' ')
}
isDirectory: item[0].startsWith('d'),
fileMTime: item.slice(5, 7).join(' '),
};
setMobileFileNodeList(fileNodeList => [...fileNodeList, fileNode])
setMobileFileNodeList((fileNodeList) => [...fileNodeList, fileNode]);
});
setLoading(false)
setLoading(false);
}
// 读取本地目录
function readDir(target: string) {
// 清空原列表
setLocalFileNodeList([])
setLoading(true)
setLocalFileNodeList([]);
setLoading(true);
console.log('trigger readDir');
if (!pathExistsSync(target)) {
console.log(target);
throw new Error('无效路径')
throw new Error('无效路径');
}
const fileList = readdirSync(target)
for (const item of fileList) {
if (ignoreFileList.includes(item)) {
// 在忽略名单中,跳过
continue;
}
try {
const node = createFileNode(path.join(target, item))
setLocalFileNodeList(fileNodeList => [...fileNodeList, node])
} catch (error) {
openNotification(item, '生成节点出错啦')
}
}
setLoading(false)
// 读取文件名称
const fileList: string[] = readdirSync(target);
// 过滤不必要的文件名
const filterFileList = fileList.filter((name) => !ignoreFileList.includes(name));
const nodeList = filterFileList.map((name) => createFileNode(path.join(target, name)));
setLocalFileNodeList(nodeList);
setLoading(false);
}
useMount(() => {
readDir(localPathCollection.join('/'))
readDir(localPathCollection.join('/'));
try {
readMobileDriverDir(MobilePathCollection.join('/'))
readMobileDriverDir(MobilePathCollection.join('/'));
} catch (error) {
openNotification('error', '读取移动设备文件失败')
openNotification('error', '读取移动设备文件失败');
}
})
});
// 更新本地文件列表
useEffect(() => {
console.log('trigger local path rejoin');
setLoading(true)
readDir(localPathCollection.join('/'))
}, [localPathCollection])
setLoading(true);
readDir(localPathCollection.join('/'));
}, [localPathCollection]);
// 更新移动设备文件列表
useEffect(() => {
console.log('trigger mobile path rejoin');
setLoading(true)
readMobileDriverDir(MobilePathCollection.join('/'))
}, [MobilePathCollection])
setLoading(true);
readMobileDriverDir(MobilePathCollection.join('/'));
}, [MobilePathCollection]);
// 返回上一级
const turnBack = () => {
@ -177,69 +194,61 @@ export default function fileManage() {
// 如果是最上一层,不处理
if (localPathCollection.length === 1) {
message.warning('当前位置处于根目录,无法再返回上一级了');
return
return;
}
setLocalPathCollection(path => path.slice(0, -1))
setLocalPathCollection((cPath) => cPath.slice(0, -1));
}
if (curDriType === DriverType.MOBILE) {
// 如果是最上一层,不处理
if (MobilePathCollection.length === 1) {
message.warning('当前位置处于根目录,无法再返回上一级了');
return
return;
}
setMobilePathCollection(path => path.slice(0, -1))
setMobilePathCollection((cPath) => cPath.slice(0, -1));
}
}
};
const reload = () => {
search()
}
const search = () => {
if (searchVal.trim() === '') {
readDir(localPathCollection.join('/'))
return
const search = (val:string) => {
setSearchVal(val);
if (val.trim() === '') {
readDir(localPathCollection.join('/'));
return;
}
if (curDriType === DriverType.LOCAL) {
const filterList = localFileNodeList.filter(item => item.fileName.includes(searchVal))
setLocalFileNodeList(filterList)
const filterList = localFileNodeList.filter((item) => item.fileName.includes(val));
setLocalFileNodeList(filterList);
}
if (curDriType === DriverType.MOBILE) {
const filterList = mobileFileNodeList.filter(item => item.fileName.includes(searchVal))
setMobileFileNodeList(filterList)
const filterList = mobileFileNodeList.filter((item) => item.fileName.includes(val));
setMobileFileNodeList(filterList);
}
}
};
const reload = () => {
search(searchVal);
};
// 切换状态
const handleDriverStatus = (targetStatus: DriverType) => {
setCurDriType(targetStatus)
}
useEffect(() => {
console.log('search:', searchVal);
search()
}, [searchVal])
function fileListEmpty(){
return (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前目录没有文件/文件夹哦">
<Button onClick={()=>turnBack()}></Button>
</Empty>
)
}
setCurDriType(targetStatus);
};
return (
<Card className="overflow-hidden">
{/* 右击菜单面板 */}
<Control ref={controlPanelRef} options={rightDownOperations} onRow={
({ label, key }) => {
return {
<Control
ref={controlPanelRef}
options={rightDownOperations}
onRow={
({ label, key }) => ({
onClick: () => {
console.log(key, label)
setControlPanelStyle({ ...controlPanelStyle, visibility: 'hidden' })
console.log(key, label);
setControlPanelStyle({ ...controlPanelStyle, visibility: 'hidden' });
},
}
}
})
}
curType={curDriType} styles={controlPanelStyle}></Control>
}
curType={curDriType}
styles={controlPanelStyle}
/>
<div className={classnames('flex', 'mb-4', 'justify-between')}>
<div className={classnames('flex', 'items-center')}>
<div className={styles.operationGroup}>
@ -248,14 +257,21 @@ function fileListEmpty(){
</div>
<Breadcrumb>
{
curDriType === DriverType.LOCAL ?
localPathCollection.map((item, index) => <Breadcrumb.Item key={item}>{
index === 0 ? (item.slice(0, -2)).toUpperCase() : item
}</Breadcrumb.Item>)
:
MobilePathCollection.map((item) => <Breadcrumb.Item key={item}>{
item
}</Breadcrumb.Item>)
curDriType === DriverType.LOCAL
? localPathCollection.map((item, index) => (
<Breadcrumb.Item key={item}>
{
index === 0 ? (item.slice(0, -2)).toUpperCase() : item
}
</Breadcrumb.Item>
))
: MobilePathCollection.map((item) => (
<Breadcrumb.Item key={item}>
{
item
}
</Breadcrumb.Item>
))
}
</Breadcrumb>
</div>
@ -271,66 +287,72 @@ function fileListEmpty(){
allowClear
prefix={<SearchOutlined />}
value={searchVal}
onChange={(event) => setSearchVal(event.target.value)}
onChange={(event) => search(event.target.value)}
style={{ width: 304 }}
/></div>
/>
</div>
</div>
<ConfigProvider renderEmpty={fileListEmpty}>
<Table columns={columns} onRow={({ fileName, isDirectory }, rowIndex) => {
return {
onClick: e => {
console.log('fileName', fileName);
console.log('isDirectory', isDirectory);
},
onDoubleClick: event => {
console.log(fileName);
<ConfigProvider renderEmpty={() => <FileListEmpty turnBack={() => turnBack()} />}>
<Table
columns={columns}
onRow={({ fileName, isDirectory }, rowIndex) => ({
onClick: (e) => {
console.log('fileName', fileName);
console.log('isDirectory', isDirectory);
},
onDoubleClick: (event) => {
console.log(fileName);
if (isDirectory) {
setLoading(true)
if (curDriType === DriverType.LOCAL) setLocalPathCollection(paths => [...paths, fileName])
if (curDriType === DriverType.MOBILE) setMobilePathCollection(paths => [...paths, fileName])
} else {
// 打开文件
console.log((localPathCollection.join('/') + fileName));
// 处理开头 // 盘符为 /
const filePath = [localPathCollection[0].slice(0, -1), ...localPathCollection.slice(1), fileName].join('/')
exec(`start ${filePath}`, (error, stdout, stderr) => {
if (error) {
openNotification('error', error.message)
console.log(`error: ${error.message}`);
return;
if (isDirectory) {
setLoading(true);
if (curDriType === DriverType.LOCAL) {
setLocalPathCollection((paths) => [...paths, fileName]);
}
if (stderr) {
openNotification('error', stderr)
return;
if (curDriType === DriverType.MOBILE) {
setMobilePathCollection((paths) => [...paths, fileName]);
}
})
}
} else {
// 打开文件
console.log((localPathCollection.join('/') + fileName));
// 处理开头 // 盘符为 /
const filePath = [localPathCollection[0].slice(0, -1), ...localPathCollection.slice(1), fileName].join('/');
exec(`start ${filePath}`, (error, stdout, stderr) => {
if (error) {
openNotification('error', error.message);
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
openNotification('error', stderr);
}
});
}
},
onMouseDown: (event) => {
if (event.button === 2) {
// 触发右击
console.log(event);
const { pageX, pageY } = event.nativeEvent;
setControlPanelStyle({
left: pageX - 98,
top: pageY - 80,
visibility: 'visible',
});
}
},
})}
rowKey="fileName"
loading={loading}
scroll={{ x: '100%', scrollToFirstRowOnChange: true, y: '380px' }}
dataSource={curDriType === DriverType.LOCAL ? localFileNodeList : mobileFileNodeList}
/>
},
onMouseDown: event => {
if (event.button === 2) {
// 触发右击
console.log(event)
const { pageX, pageY } = event.nativeEvent
setControlPanelStyle({
left: pageX - 98,
top: pageY - 80,
visibility: 'visible'
})
</ConfigProvider>
}
}
};
}} rowKey='fileName' loading={loading} scroll={{ x: '100%', scrollToFirstRowOnChange: true, y: '380px' }} dataSource={curDriType === DriverType.LOCAL ? localFileNodeList : mobileFileNodeList} /></ConfigProvider>
</ Card >
)
</Card>
);
}

View File

@ -1,42 +1,43 @@
import { Button, Card, List, Radio, RadioChangeEvent, Space, Tag } from "antd"
import { useState } from "react"
import styles from './index.module.less'
import {
Button, Card, List, Radio, RadioChangeEvent, Space, Tag,
} from 'antd';
import { useState } from 'react';
import { PlusCircleOutlined, DeleteOutlined } from '@ant-design/icons';
import { useMount } from 'ahooks';
import Table, { ColumnsType } from 'antd/lib/table';
import { useNavigate } from 'react-router-dom';
import ignoreFileList from "@/utils/ignoreFileList"
import { PlusCircleOutlined, DeleteOutlined } from "@ant-design/icons"
import { useMount } from "ahooks"
import { getConfig } from "@/config"
import Table, { ColumnsType } from "antd/lib/table"
import { BackItemType } from "@/types"
import { loadTheme, ThemeType } from "@/lib/css/theme"
import { useNavigate} from 'react-router-dom'
import ignoreFileList from '@/utils/ignoreFileList';
import { getConfig } from '@/config';
import { BackItemType } from '@/types';
import { loadTheme, ThemeType } from '@/lib/css/theme';
import styles from './index.module.less';
const ignoreHeaderFn = () => {
return (
<div className="flex">
<Button className="mr-3" type="primary" icon={<PlusCircleOutlined />} size='small'></Button>
<Button className="mr-3" type="primary" danger icon={<DeleteOutlined />} size='small'></Button>
<div>{ignoreFileList.length}</div>
const ignoreHeaderFn = () => (
<div className="flex">
<Button className="mr-3" type="primary" icon={<PlusCircleOutlined />} size="small"></Button>
<Button className="mr-3" type="primary" danger icon={<DeleteOutlined />} size="small"></Button>
<div>
{ignoreFileList.length}
</div>
)
}
export default () => {
const navigate =useNavigate()
const [theme,setTheme]=useState<ThemeType>(localStorage.getItem('theme'))
</div>
);
export default function Settings() {
const navigate = useNavigate();
const [theme, setTheme] = useState<ThemeType>(localStorage.getItem('theme') as ThemeType ?? ThemeType.LIGHT);
const [config, setConfig] = useState({
color: 'auto',
backupNode: getConfig()
})
backupNode: getConfig(),
});
const handleColor = (event: RadioChangeEvent) => {
const theme = event.target.value
loadTheme(theme)
navigate(0)
setTheme(theme)
}
const targetTheme = event.target.value;
loadTheme(targetTheme);
navigate(0);
setTheme(targetTheme);
};
const backupNodeColumns: ColumnsType<BackItemType> = [
{
@ -54,15 +55,14 @@ export default () => {
dataIndex: 'output',
key: 'output',
align: 'center',
render: (i) => <Tag color={i ? 'gold' : ''}>{i ? i : '继承父级'}</Tag >
render: (i) => <Tag color={i ? 'gold' : ''}>{i || '继承父级'}</Tag>,
},
{
title: '全量备份',
dataIndex: 'full',
key: 'full',
align: 'center',
render: (i) =>
<Tag color={i ? 'blue' : 'red'}>{i ? '是' : '否'}</Tag >
render: (i) => <Tag color={i ? 'blue' : 'red'}>{i ? '是' : '否'}</Tag>,
},
{
@ -70,13 +70,12 @@ export default () => {
dataIndex: 'actions',
key: 'actions',
align: 'center',
render: (_: any, record: BackItemType) => (
<a>Delete</a>
)
render: (_: any, record: BackItemType) => <Button type="link">Delete</Button>
,
},
];
return (
<Card >
<Card>
<div className={styles.settingItem}>
<div className="text-md font-bold mb-3"></div>
<Radio.Group onChange={(event) => handleColor(event)} value={theme}>
@ -91,10 +90,10 @@ export default () => {
<List
size="small"
header={ignoreHeaderFn()}
footer={<div></div>}
footer={<div />}
bordered
dataSource={ignoreFileList}
renderItem={item => <List.Item>{item}</List.Item>}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
</div>
<div className={styles.settingItem}>
@ -103,5 +102,5 @@ export default () => {
</div>
</Card>
)
);
}

View File

@ -1,13 +1,13 @@
import { lstat } from 'fs/promises'
import { cwd } from 'process'
import { ipcRenderer } from 'electron'
import { lstat } from 'fs/promises';
import { cwd } from 'process';
import { ipcRenderer } from 'electron';
ipcRenderer.on('main-process-message', (_event, ...args) => {
console.log('[Receive Main-process message]:', ...args)
})
console.log('[Receive Main-process message]:', ...args);
});
lstat(cwd()).then(stats => {
console.log('[fs.lstat]', stats)
}).catch(err => {
console.error(err)
})
lstat(cwd()).then((stats) => {
console.log('[fs.lstat]', stats);
}).catch((err) => {
console.error(err);
});

View File

@ -27,7 +27,7 @@ export interface devicesType {
status: string;
}
// 显示数据
// 显示数据
export enum DriverType {
MOBILE,
LOCAL

View File

@ -1,5 +1,5 @@
export default [
'System Volume Information',
'$RECYCLE.BIN',
'Config.Msi'
]
'Config.Msi',
];

View File

@ -1,32 +1,85 @@
import { DriverType, FileNodeType } from "@/types";
import { notification } from "antd";
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-continue */
import { notification } from 'antd';
// import winston, { format } from "winston";
import path from 'path';
import { execSync } from 'child_process';
import { DriverType, FileNodeType, devicesType } from '@/types';
import ignoreFileList from './ignoreFileList';
const {
statSync,
pathExistsSync,
ensureDirSync,
readdirSync,
} = require("fs-extra");
// import winston, { format } from "winston";
import path from "path";
import { execSync } from "child_process";
import { home } from "@/config";
import { devicesType } from "@/types";
import ignoreFileList from "./ignoreFileList";
} = require('fs-extra');
type levelType = 'info' | 'error' | 'warn';
// 记录输出
export const log = (value: string, level: levelType = 'info'): void => {
// logger.log(level, value);
console.log(level, value);
};
export function openNotification(
message: string,
description: string,
onClick?: () => void,
onClose?: () => void,
) {
notification.open({
message,
description,
onClick,
onClose,
});
}
// 执行adb shell命令
const currentDeviceName: string = '';
export const execAdb = (code: string) => {
const command = `adb ${
currentDeviceName ? `-s ${currentDeviceName}` : ''
} ${code}`;
try {
const res = execSync(command).toString();
return res;
} catch (error) {
console.log(error);
openNotification('error', `${code} 执行失败`);
return '';
}
};
// 文件大小后缀转换
export function readablizeBytes(bytes: number): string {
if (bytes === 0) return "";
const s = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
if (bytes === 0) return '';
const s = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const e = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2) + " " + s[e] ?? 0;
return `${(bytes / 1024 ** Math.floor(e)).toFixed(2)} ${s[e]}` ?? 0;
}
// 替换字符
export const replace = (str: string): string => {
const reg = [
{ reg: /[(]/g, val: '\\(' },
{ reg: /[)]/g, val: '\\)' },
];
let res: string = str;
for (let i = 0; i < reg.length; i += 1) {
res = res.replace(reg[i].reg, reg[i].val);
}
return res;
};
// 获取手机中文件的大小
export const getFileSize = (path: string): number => {
export const getFileSize = (adbPath: string): number => {
try {
const res = execAdb(`shell du -k '${replace(path)}'`).toString();
const fileSize: string = res.split("\t")[0];
const res = execAdb(`shell du -k '${replace(adbPath)}'`).toString();
const fileSize: string = res.split('\t')[0];
return Number(fileSize);
} catch (error) {
log(`获取文件大小失败-${path}`, "warn");
log(`获取文件大小失败-${adbPath}`, 'warn');
return 0;
}
};
@ -35,7 +88,7 @@ export function createFileNode(targetFilePath: string): FileNodeType {
const detail = statSync(targetFilePath);
return {
fileSize: detail.size,
fileName: targetFilePath.split("\\").at(-1) ?? "读取文件名错误",
fileName: targetFilePath.split('\\').at(-1) ?? '读取文件名错误',
filePath: targetFilePath,
isDirectory: detail.isDirectory(),
fileMTime: detail.mtime,
@ -45,7 +98,7 @@ export function createFileNode(targetFilePath: string): FileNodeType {
// 获取指定目录下文件节点
export function getFileNodeList(
targetPath: string,
mode: DriverType
mode: DriverType,
): FileNodeType[] {
const fileNodeList: FileNodeType[] = [];
if (mode === DriverType.LOCAL) {
@ -59,14 +112,14 @@ export function getFileNodeList(
const node = createFileNode(path.join(targetPath, item));
fileNodeList.push(node);
} catch (error) {
openNotification(item, "生成节点出错啦");
openNotification(item, '生成节点出错啦');
}
}
return fileNodeList;
}
if (mode === DriverType.MOBILE) {
}
// if (mode === DriverType.MOBILE) {
// }
return fileNodeList;
}
// 通过adb方式获取文件路径生成文件节点
@ -76,47 +129,33 @@ export function createFileNodeWithADB(targetFilePath: string): FileNodeType {
// 文件名称 filename
// 文件大小 filesize
const fileType = execAdb(`shell stat -c '%F' '${targetFilePath}'`).replace(
"\r\n",
""
'\r\n',
'',
);
const fileName = execAdb(`shell stat -c '%n' '${targetFilePath}'`).replace(
"\r\n",
""
'\r\n',
'',
);
const fileSize = Number(
execAdb(`shell stat -c '%s' '${targetFilePath}'`).replace("\r\n", "")
execAdb(`shell stat -c '%s' '${targetFilePath}'`).replace('\r\n', ''),
);
return {
fileName,
fileSize,
filePath: targetFilePath,
isDirectory: fileType === "directory",
isDirectory: fileType === 'directory',
};
}
export function openNotification(
message: string,
description: string,
onClick?: () => void,
onClose?: () => void
) {
notification.open({
message,
description,
onClick,
onClose,
});
}
type levelType = "info" | "error" | "warn";
export const diff = (
localArr: FileNodeType[],
remoteArr: FileNodeType[]
remoteArr: FileNodeType[],
): FileNodeType[] => {
let names: { [key: string]: boolean } = {};
localArr.forEach(i=> (names[i.fileName] = true));
return remoteArr.filter(i =>!names[i.fileName] );
const names: { [key: string]: boolean } = {};
localArr.forEach((i) => {
names[i.fileName] = true;
});
return remoteArr.filter((i) => !names[i.fileName]);
};
// const logger = winston.createLogger({
// format: format.combine(
@ -137,48 +176,29 @@ export const diff = (
// }),
// ],
// });
// 记录输出
export const log = (value: string, level: levelType = "info"): void => {
// logger.log(level, value);
console.log(level, value);
};
// 判断路径
export const isPath = (dirPath: string): void => {
if (!pathExistsSync(dirPath)) {
log(`导出路径不存在-${dirPath}`, "warn");
log(`导出路径不存在-${dirPath}`, 'warn');
// 没有则创建文件夹
ensureDirSync(dirPath);
log(`已自动创建导出路径-${dirPath}`, "warn");
log(`已自动创建导出路径-${dirPath}`, 'warn');
}
};
// 替换字符
export const replace = (str: string): string => {
const reg = [
{ reg: /[(]/g, val: "\\(" },
{ reg: /[)]/g, val: "\\)" },
];
let res: string = str;
for (let i = 0; i < reg.length; i += 1) {
res = res.replace(reg[i].reg, reg[i].val);
}
return res;
};
// 获取设备
export const devices = (): devicesType[] => {
const res = execSync("adb devices").toString();
const res = execSync('adb devices').toString();
const arr = res
.split(/\n/)
.map((line) => line.split("\t"))
.map((line) => line.split('\t'))
.filter((line) => line.length > 1)
.map((device) => ({ name: device[0].trim(), status: device[1].trim() }));
return arr;
};
let currentDeviceName: string = "";
// 指定设备
// export const selectDevice = async (): Promise<string | false> => {
// // 获取设备
@ -207,21 +227,6 @@ let currentDeviceName: string = "";
// return currentDeviceName;
// };
// 执行adb shell命令
export const execAdb = (code: string) => {
const command = `adb ${
currentDeviceName ? `-s ${currentDeviceName}` : ""
} ${code}`;
try {
let res = execSync(command).toString();
return res;
} catch (error) {
console.log(error);
openNotification("error", `${code} 执行失败`);
return "";
}
};
// 判断手机备份路径是否存在
export const isPathAdb = (folderPath: string): boolean => {
try {
@ -235,5 +240,4 @@ export const isPathAdb = (folderPath: string): boolean => {
export const speedReg: RegExp = /[0-9.]+\s(MB\/s)/;
// 路径后面补上斜杠
export const pathRepair = (spath: string): string =>
spath.at(-1) === "/" ? spath : `${spath}/`;
export const pathRepair = (spath: string): string => (spath.at(-1) === '/' ? spath : `${spath}/`);

View File

@ -1,25 +1,26 @@
import { rmSync } from "fs";
import { join } from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import electron from "vite-plugin-electron";
import pkg from "./package.json";
import Unocss from "unocss/vite";
import presetUno from "@unocss/preset-uno";
import presetIcons from "@unocss/preset-icons";
import presetAttributify from "@unocss/preset-attributify";
import transformerDirective from "@unocss/transformer-directives";
import { presetScrollbar } from "unocss-preset-scrollbar";
rmSync(join(__dirname, "dist"), { recursive: true, force: true }); // v14.14.0
import { rmSync } from 'fs';
import { join } from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import electron from 'vite-plugin-electron';
import Unocss from 'unocss/vite';
import presetUno from '@unocss/preset-uno';
import presetIcons from '@unocss/preset-icons';
import presetAttributify from '@unocss/preset-attributify';
import transformerDirective from '@unocss/transformer-directives';
import { presetScrollbar } from 'unocss-preset-scrollbar';
import pkg from './package.json';
rmSync(join(__dirname, 'dist'), { recursive: true, force: true }); // v14.14.0
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": join(__dirname, "src"),
styles: join(__dirname, "src/assets/styles"),
'@': join(__dirname, 'src'),
styles: join(__dirname, 'src/assets/styles'),
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".less"],
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.less'],
},
css: {
@ -33,24 +34,24 @@ export default defineConfig({
react(),
electron({
main: {
entry: "electron/main/index.ts",
entry: 'electron/main/index.ts',
vite: {
build: {
sourcemap: false,
outDir: "dist/electron/main",
outDir: 'dist/electron/main',
},
},
},
preload: {
input: {
// You can configure multiple preload scripts here
index: join(__dirname, "electron/preload/index.ts"),
index: join(__dirname, 'electron/preload/index.ts'),
},
vite: {
build: {
// For debug
sourcemap: "inline",
outDir: "dist/electron/preload",
sourcemap: 'inline',
outDir: 'dist/electron/preload',
},
},
},
@ -60,7 +61,7 @@ export default defineConfig({
Unocss({
presets: [
presetUno({ dark: "class" }),
presetUno({ dark: 'class' }),
presetAttributify(),
presetIcons({
/* options */