├── src-tauri ├── migrations │ ├── .keep │ ├── 2024-06-13-142820_ignore │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-06-13-142831_backup │ │ ├── down.sql │ │ └── up.sql │ ├── 2024-06-13-142808_mission │ │ ├── down.sql │ │ └── up.sql │ └── 2024-06-13-142827_procedure │ │ ├── down.sql │ │ └── up.sql ├── .env ├── build.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 32x32.png │ ├── icon.icns │ └── 128x128@2x.png ├── src │ ├── core │ │ ├── mod.rs │ │ ├── tray.rs │ │ ├── setup.rs │ │ └── window.rs │ ├── utils │ │ ├── mod.rs │ │ ├── common.rs │ │ └── crypto.rs │ ├── plugins │ │ └── mod.rs │ ├── config │ │ ├── watcher.rs │ │ ├── screensaver.rs │ │ ├── notify.rs │ │ ├── system.rs │ │ └── mod.rs │ ├── main.rs │ └── db │ │ ├── schema.rs │ │ └── utils.rs ├── .gitignore ├── diesel.toml ├── tauri.conf.json └── Cargo.toml ├── src ├── assets │ ├── style │ │ ├── light.less │ │ ├── reset.css │ │ └── route.less │ ├── vue.svg │ └── icons │ │ ├── index.ts │ │ └── logo.svg ├── types.ts ├── store │ ├── watcher │ │ ├── types.ts │ │ └── index.ts │ ├── screensaver │ │ ├── types.ts │ │ └── index.ts │ ├── types.ts │ ├── notify │ │ ├── types.ts │ │ └── index.ts │ ├── status │ │ ├── types.ts │ │ └── index.ts │ ├── index.ts │ ├── system │ │ ├── types.ts │ │ └── index.ts │ └── mission │ │ └── index.ts ├── vite-env.d.ts ├── locales │ ├── index.ts │ └── langs │ │ ├── zh_CN.json │ │ └── en_US.json ├── utils │ ├── error │ │ └── index.ts │ ├── event │ │ └── index.ts │ ├── cmd │ │ └── types.ts │ └── common │ │ └── index.ts ├── main.ts ├── views │ ├── Config │ │ ├── Config.vue │ │ └── components │ │ │ ├── NotifyConfig.vue │ │ │ ├── WatcherConfig.vue │ │ │ ├── SystemConfig.vue │ │ │ └── ScreensaverConfig.vue │ ├── Toolbox │ │ ├── Toolbox.vue │ │ └── components │ │ │ ├── DatabaseTool.vue │ │ │ ├── LogTool.vue │ │ │ └── MigrateTool.vue │ ├── Screensaver │ │ └── Screensaver.vue │ ├── Statistic │ │ ├── components │ │ │ └── BackupChart.vue │ │ └── Statistic.vue │ ├── Backup │ │ └── Backup.vue │ ├── Procedure │ │ └── Procedure.vue │ └── Mission │ │ └── Mission.vue ├── router │ └── index.ts ├── components │ ├── SideMenu.vue │ ├── StatusDot.vue │ ├── CloseDialog.vue │ └── TitleBar.vue ├── styles.css └── App.vue ├── .lintstagedrc ├── eslint.config.js ├── .husky └── pre-commit ├── .vscode └── extensions.json ├── auto-imports.d.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── vite.config.ts ├── public ├── vite.svg ├── tauri.svg └── splashscreen.html ├── README.md ├── package.json ├── .github └── workflows │ └── release.yml ├── components.d.ts ├── .commitlintrc.cjs └── splashscreen.html /src-tauri/migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/style/light.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=debug_db.db3 -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/*.{js,ts,vue, json}": "eslint --fix" 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu() 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hellager/mission-backup/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hellager/mission-backup/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hellager/mission-backup/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hellager/mission-backup/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hellager/mission-backup/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tray; 2 | pub mod window; 3 | pub mod setup; 4 | pub mod cmd; 5 | pub mod state; 6 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hellager/mission-backup/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142820_ignore/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE ignore; -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142831_backup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE backup; -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142808_mission/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE mission; -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142827_procedure/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE procedure; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | enum DialogMode { 2 | None, 3 | Create, 4 | Edit, 5 | } 6 | 7 | export { 8 | DialogMode, 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Custom ignores 6 | debug_db.db3 7 | -------------------------------------------------------------------------------- /src-tauri/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | pub mod compress; 3 | pub mod crypto; 4 | pub mod explorer; 5 | pub mod logger; 6 | pub mod migrate; 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/store/watcher/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the configuration for the watcher. 3 | */ 4 | export interface WatcherConfig { 5 | timeout: number 6 | } 7 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | 6 | const component: DefineComponent 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] 7 | 8 | [migrations_directory] 9 | dir = "D:\\Project\\Github\\mission-backup\\src-tauri\\migrations" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/store/screensaver/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the configuration for the screensaver. 3 | */ 4 | export interface ScreensaverConfig { 5 | /** 6 | * Indicates if the screensaver is enabled. 7 | */ 8 | enable: boolean 9 | /** 10 | * The password for the screensaver. 11 | */ 12 | password: string 13 | /** 14 | * Indicates if the screensaver is locked. 15 | */ 16 | isLocked: boolean 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + Vue + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/mod.rs: -------------------------------------------------------------------------------- 1 | use tauri::{AppHandle, Manager}; 2 | use log::error; 3 | 4 | pub fn on_another_instance(app: &AppHandle, _argv: Vec, _cwd: String) { 5 | let windows = app.windows(); 6 | 7 | if let Some(_) = windows.get("main") { 8 | if let Err(error) = app.emit_to("main", "instance", {}) { 9 | error!("Failed to send event about another instance, errMsg: {:?}", error); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import type { SystemConfig } from './system/types' 2 | import type { ScreensaverConfig } from './screensaver/types' 3 | import type { NotifyConfig } from './notify/types' 4 | import type { WatcherConfig } from './watcher/types' 5 | 6 | /** 7 | * Represents the application configuration. 8 | */ 9 | export interface AppConfig { 10 | system: SystemConfig 11 | notify: NotifyConfig 12 | watcher: WatcherConfig 13 | screensaver: ScreensaverConfig 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/notify/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the NotifyConfig interface. 3 | */ 4 | export interface NotifyConfig { 5 | /** 6 | * Indicates if the notification is granted. 7 | */ 8 | isGranted: boolean 9 | /** 10 | * Indicates if the notification is enabled. 11 | */ 12 | enable: boolean 13 | /** 14 | * Indicates if `when create` notifications are enabled. 15 | */ 16 | whenCreate: boolean 17 | /** 18 | * Indicates if `when failed` notifications are enabled. 19 | */ 20 | whenFailed: boolean 21 | } 22 | -------------------------------------------------------------------------------- /src-tauri/src/config/watcher.rs: -------------------------------------------------------------------------------- 1 | //! # Watcher 2 | //! 3 | //! `watcher` module contains all configuration about app's wacher handler related. 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | /// Configuration for watcher 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct WatcherConfig { 10 | /// Watcher timeout in secs 11 | pub timeout: u64, 12 | } 13 | 14 | impl Default for WatcherConfig { 15 | fn default() -> Self { 16 | WatcherConfig { 17 | timeout: 3 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142820_ignore/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE "ignore" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY, 4 | "ignore_id" TEXT NOT NULL UNIQUE, 5 | "procedure_id" TEXT NOT NULL, 6 | "keyword" TEXT NOT NULL, 7 | "reserved_0" TEXT NOT NULL, 8 | "reserved_1" TEXT NOT NULL, 9 | "reserved_2" TEXT NOT NULL, 10 | "create_at" TIMESTAMP NOT NULL, 11 | "update_at" TIMESTAMP NOT NULL, 12 | "is_deleted" SMALLINT NOT NULL, 13 | "delete_at" TIMESTAMP NOT NULL 14 | ); 15 | -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142831_backup/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE "backup" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY, 4 | "backup_id" TEXT NOT NULL UNIQUE, 5 | "mission_id" TEXT NOT NULL, 6 | "save_path" TEXT NOT NULL, 7 | "backup_size" BIGINT NOT NULL, 8 | "reserved_0" TEXT NOT NULL, 9 | "reserved_1" TEXT NOT NULL, 10 | "reserved_2" TEXT NOT NULL, 11 | "create_at" TIMESTAMP NOT NULL, 12 | "update_at" TIMESTAMP NOT NULL, 13 | "is_deleted" SMALLINT NOT NULL, 14 | "delete_at" TIMESTAMP NOT NULL 15 | ); 16 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import enUS from './langs/en_US.json' 3 | import zhCN from './langs/zh_CN.json' 4 | 5 | /** 6 | * Represents the message schema type based on the 'zhCN' constant. 7 | */ 8 | type MessageSchema = typeof zhCN 9 | 10 | /** 11 | * Creates the i18n instance with message schema and language options. 12 | */ 13 | const i18n = createI18n<[MessageSchema], 'zh-CN' | 'en-US'>({ 14 | legacy: false, 15 | locale: 'zh-CN', 16 | fallbackLocale: 'en-US', 17 | messages: { 18 | 'zh-CN': zhCN, 19 | 'en-US': enUS, 20 | }, 21 | }) 22 | 23 | export default i18n 24 | -------------------------------------------------------------------------------- /src/utils/error/index.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | import { isResponse } from '../cmd/types' 3 | import type { Response } from '../cmd/types' 4 | import { Command, execute } from '../cmd' 5 | 6 | /** 7 | * Handles errors that occur during execution. 8 | * @param error - The error that occurred. 9 | * @param _vm - The first optional argument. 10 | * @param _info - Additional information. 11 | */ 12 | export async function errorHandler(error: any, _vm: any, _info: any) { 13 | if (isResponse(error)) { 14 | const response = error as Response 15 | ElMessage.error(response.msg) 16 | } 17 | else { 18 | await execute(Command.WebLog, 'error', `Web: ${error}`) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src-tauri/src/config/screensaver.rs: -------------------------------------------------------------------------------- 1 | //! # Screen 2 | //! 3 | //! `screen` module contains all configuration about app's screen related. 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | /// Configuration for screen 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct ScreensaverConfig { 10 | /// Whether enable screensaver 11 | pub enable: bool, 12 | 13 | /// Password for screensaver 14 | pub password: String, 15 | 16 | /// Whether has been locked 17 | pub is_locked: bool, 18 | } 19 | 20 | impl Default for ScreensaverConfig { 21 | fn default() -> Self { 22 | ScreensaverConfig { 23 | enable: false, 24 | password: "".to_string(), 25 | is_locked: false, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src-tauri/src/config/notify.rs: -------------------------------------------------------------------------------- 1 | //! # Notify 2 | //! 3 | //! `notify` module contains all configuration about app's notification. 4 | 5 | use serde::{Serialize, Deserialize}; 6 | 7 | /// Configuration for notify 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct NotifyConfig { 10 | /// Whether able to notify 11 | pub is_granted: bool, 12 | 13 | /// Whether enable notify 14 | pub enable: bool, 15 | 16 | /// Whether enable create backup notify 17 | pub when_create: bool, 18 | 19 | /// Whether enable failed backup notify 20 | pub when_failed: bool 21 | } 22 | 23 | impl Default for NotifyConfig { 24 | fn default() -> Self { 25 | NotifyConfig { 26 | is_granted: false, 27 | enable: false, 28 | when_create: false, 29 | when_failed: false 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "types": [ 13 | "element-plus/global", 14 | "unplugin-icons/types/vue" 15 | ], 16 | "allowImportingTsExtensions": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noEmit": true, 24 | 25 | "isolatedModules": true, 26 | "skipLibCheck": true 27 | }, 28 | "references": [{ "path": "./tsconfig.node.json" }], 29 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142808_mission/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE "mission" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY, 4 | "mission_id" TEXT NOT NULL UNIQUE, 5 | "procedure_id" TEXT NOT NULL, 6 | "name" TEXT NOT NULL, 7 | "status" SMALLINT NOT NULL, 8 | "description" TEXT NOT NULL, 9 | "src_path" TEXT NOT NULL, 10 | "dst_path" TEXT NOT NULL, 11 | "path_type" SMALLINT NOT NULL, 12 | "next_runtime" TIMESTAMP NOT NULL, 13 | "last_trigger" TIMESTAMP NOT NULL, 14 | "reserved_0" TEXT NOT NULL, 15 | "reserved_1" TEXT NOT NULL, 16 | "reserved_2" TEXT NOT NULL, 17 | "create_at" TIMESTAMP NOT NULL, 18 | "update_at" TIMESTAMP NOT NULL, 19 | "is_deleted" SMALLINT NOT NULL, 20 | "delete_at" TIMESTAMP NOT NULL 21 | ); 22 | -------------------------------------------------------------------------------- /src/assets/icons/index.ts: -------------------------------------------------------------------------------- 1 | import Logo from '~icons/custom-icons/logo' 2 | import Minimize from '~icons/mdi/minus' 3 | import Maxmize from '~icons/mdi/border-all-variant' 4 | import Close from '~icons/mdi/window-close' 5 | import Locked from '~icons/mdi/lock-outline' 6 | import Create from '~icons/mdi/plus' 7 | import CreateBox from '~icons/mdi/plus-box-outline' 8 | import Delete from '~icons/mdi/delete-outline' 9 | import Edit from '~icons/mdi/square-edit-outline' 10 | import Search from '~icons/mdi/search' 11 | import Open from '~icons/mdi/folder-open-outline' 12 | import Start from '~icons/mdi/play-circle-outline' 13 | import Stop from '~icons/mdi/pause-circle-outline' 14 | import Copy from '~icons/mdi/content-copy' 15 | 16 | export { 17 | Logo, 18 | Minimize, 19 | Maxmize, 20 | Close, 21 | Locked, 22 | Create, 23 | CreateBox, 24 | Delete, 25 | Edit, 26 | Search, 27 | Open, 28 | Start, 29 | Stop, 30 | Copy, 31 | } 32 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod utils; 5 | mod plugins; 6 | mod config; 7 | mod core; 8 | mod db; 9 | 10 | use tauri_plugin_autostart::MacosLauncher; 11 | use plugins::on_another_instance; 12 | 13 | fn main() { 14 | tauri::Builder::default() 15 | .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![]))) 16 | .plugin(tauri_plugin_single_instance::init(on_another_instance)) 17 | .setup(crate::core::setup::setup_handler) 18 | .system_tray(core::tray::create_system_tray()) 19 | .on_system_tray_event(core::tray::on_system_tray_event) 20 | .on_window_event(core::window::on_window_event) 21 | .invoke_handler(crate::core::setup::setup_command()) 22 | .run(tauri::generate_context!()) 23 | .expect("error while running tauri application"); 24 | } 25 | -------------------------------------------------------------------------------- /src/store/status/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the status of different handlers. 3 | */ 4 | export interface HandlerStatus { 5 | /** 6 | * Indicates the status of the log handler. 7 | */ 8 | log: boolean 9 | 10 | /** 11 | * Indicates the status of the app handler. 12 | */ 13 | app: boolean 14 | 15 | /** 16 | * Indicates the status of the cron handler. 17 | */ 18 | cron: boolean 19 | 20 | /** 21 | * Indicates the status of the watcher handler. 22 | */ 23 | watcher: boolean 24 | 25 | /** 26 | * Indicates the status of the config handler. 27 | */ 28 | config: boolean 29 | 30 | /** 31 | * Indicates the status of the database handler. 32 | */ 33 | database: boolean 34 | } 35 | 36 | /** 37 | * Represents the overall status. 38 | */ 39 | export interface Status { 40 | /** 41 | * The status of different handlers. 42 | */ 43 | handler: HandlerStatus 44 | // /** 45 | // * The status of the window. 46 | // */ 47 | // window: WindowStatus; 48 | } 49 | -------------------------------------------------------------------------------- /src-tauri/migrations/2024-06-13-142827_procedure/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE "procedure" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY, 4 | "procedure_id" TEXT NOT NULL UNIQUE, 5 | "name" TEXT NOT NULL, 6 | "has_ignores" BOOL NOT NULL, 7 | "ignore_method" SMALLINT NOT NULL, 8 | "is_compress" BOOL NOT NULL, 9 | "compress_format" SMALLINT NOT NULL, 10 | "trigger" SMALLINT NOT NULL, 11 | "cron_expression" TEXT NOT NULL, 12 | "restrict" SMALLINT NOT NULL, 13 | "restrict_days" SMALLINT NOT NULL, 14 | "restrict_size" BIGINT NOT NULL, 15 | "reserved_0" TEXT NOT NULL, 16 | "reserved_1" TEXT NOT NULL, 17 | "reserved_2" TEXT NOT NULL, 18 | "create_at" TIMESTAMP NOT NULL, 19 | "update_at" TIMESTAMP NOT NULL, 20 | "is_deleted" SMALLINT NOT NULL, 21 | "delete_at" TIMESTAMP NOT NULL 22 | ); 23 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { useStatusStore } from './status' 3 | import type { AppConfig } from './types' 4 | import { defaultSystemConfig, useSystemStore } from './system' 5 | import { defaultScreensaverConfig, useScreensaverStore } from './screensaver' 6 | import { defaultNotifyConfig, useNotifyStore } from './notify' 7 | import { defaultWatcherConfig, useWatcherStore } from './watcher' 8 | import { useMissionStore } from './mission' 9 | 10 | const pinia = createPinia() 11 | 12 | export { 13 | useStatusStore, 14 | useSystemStore, 15 | useWatcherStore, 16 | useNotifyStore, 17 | useScreensaverStore, 18 | useMissionStore, 19 | } 20 | 21 | /** 22 | * Generates the default application configuration. 23 | * @returns The default application configuration. 24 | */ 25 | export function defaultAppConfig(): AppConfig { 26 | return { 27 | system: defaultSystemConfig(), 28 | notify: defaultNotifyConfig(), 29 | watcher: defaultWatcherConfig(), 30 | screensaver: defaultScreensaverConfig(), 31 | } 32 | } 33 | 34 | export default pinia 35 | -------------------------------------------------------------------------------- /src/store/system/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the options for closing. 3 | */ 4 | export enum ThemeOption { 5 | Scoped, 6 | FollowSystem, 7 | } 8 | 9 | /** 10 | * Represents the options for closing. 11 | */ 12 | export enum CloseOption { 13 | Exit, 14 | Hide, 15 | } 16 | 17 | /** 18 | * Represents the system configuration. 19 | */ 20 | export interface SystemConfig { 21 | /** 22 | * The theme of the app. 23 | */ 24 | theme: string 25 | 26 | /** 27 | * The theme of the system. 28 | */ 29 | sysTheme: string 30 | 31 | /** 32 | * The option for theme. 33 | */ 34 | themeOption: ThemeOption 35 | 36 | /** 37 | * Indicates whether the system should auto-start. 38 | */ 39 | autoStart: boolean 40 | 41 | /** 42 | * The option for closing the system. 43 | */ 44 | closeOption: CloseOption 45 | 46 | /** 47 | * The count for closing. 48 | */ 49 | closeCnt: number 50 | 51 | /** 52 | * The limit for closing. 53 | */ 54 | closeLimit: number 55 | 56 | /** 57 | * The language of the system. 58 | */ 59 | language: string 60 | } 61 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import dayjs from 'dayjs' 3 | import utc from 'dayjs/plugin/utc.js' 4 | import App from './App.vue' 5 | import router from './router' 6 | import i18n from './locales' 7 | import pinia from './store' 8 | import { errorHandler } from './utils/error' 9 | 10 | import 'element-plus/theme-chalk/dark/css-vars.css' 11 | import 'element-plus/theme-chalk/el-message.css' 12 | import 'element-plus/theme-chalk/el-message-box.css' 13 | 14 | /** 15 | * Creates the Vue application instance. 16 | */ 17 | const app = createApp(App) 18 | 19 | /** 20 | * Extends dayjs with UTC plugin. 21 | */ 22 | dayjs.extend(utc) 23 | 24 | /** 25 | * Uses the router plugin. 26 | */ 27 | app.use(router) 28 | 29 | /** 30 | * Uses the i18n plugin. 31 | */ 32 | app.use(i18n) 33 | 34 | /** 35 | * Uses the Pinia store plugin. 36 | */ 37 | app.use(pinia) 38 | 39 | /** 40 | * Sets the error handler for the application configuration. 41 | */ 42 | app.config.errorHandler = errorHandler 43 | 44 | /** 45 | * Mounts the application to the specified element with id 'app'. 46 | */ 47 | app.mount('#app') 48 | -------------------------------------------------------------------------------- /src/utils/event/index.ts: -------------------------------------------------------------------------------- 1 | import { listen } from '@tauri-apps/api/event' 2 | import { ElMessage } from 'element-plus' 3 | import { appWindow } from '@tauri-apps/api/window' 4 | import i18n from '../../locales/index.ts' 5 | import { useSystemStore } from '../../store' 6 | 7 | /** 8 | * Listens to the 'instance' event and shows a warning message. 9 | */ 10 | async function listenToInstanceEvent(): Promise { 11 | await listen('instance', async () => { 12 | await appWindow.show() 13 | await appWindow.setFocus() 14 | ElMessage({ 15 | message: i18n.global.t('warning.anotherInstance'), 16 | type: 'warning', 17 | }) 18 | }) 19 | } 20 | 21 | /** 22 | * Listens to system theme update event 23 | */ 24 | async function listenToSysThemeEvent(): Promise { 25 | await listen('sys_theme', (event: any) => { 26 | if (event.payload.code === 200) { 27 | const store = useSystemStore() 28 | 29 | store.updateSysTheme(event.payload.data) 30 | } 31 | }) 32 | } 33 | 34 | /** 35 | * Listens to multiple events by calling individual event listeners. 36 | */ 37 | async function listenToEvents(): Promise { 38 | await listenToInstanceEvent() 39 | await listenToSysThemeEvent() 40 | } 41 | 42 | export { 43 | listenToEvents, 44 | } 45 | -------------------------------------------------------------------------------- /src/assets/style/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /src/views/Config/Config.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | 37 | 43 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw, Router } from 'vue-router' 2 | import { createRouter, createWebHashHistory } from 'vue-router' 3 | 4 | import Backup from '../views/Backup/Backup.vue' 5 | import Config from '../views/Config/Config.vue' 6 | import Mission from '../views/Mission/Mission.vue' 7 | import Statistic from '../views/Statistic/Statistic.vue' 8 | import Procedure from '../views/Procedure/Procedure.vue' 9 | import Screensaver from '../views/Screensaver/Screensaver.vue' 10 | import ToolBox from '../views/Toolbox/Toolbox.vue' 11 | 12 | /** 13 | * Router record array 14 | */ 15 | const routes: RouteRecordRaw[] = [ 16 | { path: '/', component: Config }, 17 | { path: '/backup', component: Backup }, 18 | { path: '/config', component: Config }, 19 | { path: '/mission', component: Mission }, 20 | { path: '/statistic', component: Statistic }, 21 | { path: '/procedure', component: Procedure }, 22 | { path: '/screensaver', component: Screensaver }, 23 | { path: '/toolbox', component: ToolBox }, 24 | ] 25 | 26 | /** 27 | * Create router instance 28 | */ 29 | const router: Router = createRouter({ 30 | history: createWebHashHistory(), 31 | routes, 32 | scrollBehavior(_to: any, _from: any, _savedPosition: any) { 33 | return { 34 | top: 0, 35 | behavior: 'smooth', 36 | } 37 | }, 38 | }) 39 | 40 | export default router 41 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import Icons from 'unplugin-icons/vite' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 7 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(async () => ({ 11 | plugins: [ 12 | vue(), 13 | AutoImport({ 14 | resolvers: [ElementPlusResolver()], 15 | }), 16 | Components({ 17 | resolvers: [ElementPlusResolver()], 18 | }), 19 | Icons({ 20 | compiler: 'vue3', 21 | autoInstall: true, 22 | customCollections: { 23 | 'custom-icons': FileSystemIconLoader( 24 | 'src/assets/icons', 25 | svg => svg.replace(/^ -------------------------------------------------------------------------------- /src/store/status/index.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import type { HandlerStatus, Status } from './types' 4 | 5 | export const useStatusStore = defineStore('status', () => { 6 | /** 7 | * The reactive object for handler status. 8 | */ 9 | let handlerStatus = reactive({ 10 | log: false, 11 | app: false, 12 | cron: false, 13 | watcher: false, 14 | config: false, 15 | database: false, 16 | }) 17 | 18 | /** 19 | * Gets the current handler status. 20 | * @returns The current handler status. 21 | */ 22 | function getHandlerStatus(): HandlerStatus { 23 | return handlerStatus 24 | } 25 | 26 | /** 27 | * Sets the handler status. 28 | * @param status - The handler status to set. 29 | */ 30 | function setHandlerStatus(status: HandlerStatus): void { 31 | handlerStatus = status 32 | } 33 | 34 | /** 35 | * Gets the overall status. 36 | * @returns The overall status. 37 | */ 38 | function getStatus(): Status { 39 | return { 40 | handler: getHandlerStatus(), 41 | } 42 | } 43 | 44 | /** 45 | * Sets the overall status. 46 | * @param status - The overall status to set. 47 | */ 48 | function setStatus(status: Status): void { 49 | handlerStatus = status.handler 50 | } 51 | 52 | return { 53 | handlerStatus, 54 | getHandlerStatus, 55 | setHandlerStatus, 56 | getStatus, 57 | setStatus, 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/components/SideMenu.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 64 | 65 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tauri + Vue 3 + TypeScript 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 44 | 45 | 51 | 52 | 87 | -------------------------------------------------------------------------------- /src/utils/cmd/types.ts: -------------------------------------------------------------------------------- 1 | import { compareKeys } from '../common' 2 | import type { AppConfig } from '../../store/types' 3 | import type { Backup, Ignore, Mission, Procedure } from '../../store/mission/types' 4 | 5 | /** 6 | * Represents various commands for execution. 7 | */ 8 | enum Command { 9 | InitApp, 10 | ShutdownApp, 11 | WebLog, 12 | ShowInExplorer, 13 | InitConfig, 14 | UpdateConfig, 15 | CreateRecord, 16 | UpdateRecord, 17 | QueryRecord, 18 | DeleteRecord, 19 | ClearRecord, 20 | DeleteBackup, 21 | SetMissionStatus, 22 | CreateMission, 23 | DeleteMission, 24 | QueryStatistic, 25 | QueryDBInfo, 26 | CleanDatabase, 27 | QueryLogInfo, 28 | CleanAppLog, 29 | MigrateFromOld, 30 | } 31 | 32 | /** 33 | * Represents a response object with a code, data, and message. 34 | * @template T - The type of data in the response. 35 | */ 36 | interface Response { 37 | code: number 38 | data: T 39 | msg: string 40 | } 41 | 42 | interface DBInfo { 43 | path: string 44 | deleted: number 45 | } 46 | 47 | interface LogInfo { 48 | path: string 49 | size: number 50 | } 51 | 52 | interface MigratedData { 53 | config: AppConfig 54 | ignores: Ignore[] 55 | procedures: Procedure[] 56 | missions: Mission[] 57 | backups: Backup[] 58 | } 59 | 60 | /** 61 | * Generates a default response object with a boolean data value. 62 | * 63 | * @returns A Response object with code 200, data set to true, and an empty message. 64 | */ 65 | function defaultResponse(): Response { 66 | return { 67 | code: 200, 68 | data: true, 69 | msg: '', 70 | } 71 | } 72 | 73 | /** 74 | * Checks if the provided data object matches the keys of the default response object. 75 | * 76 | * @param data - The data object to compare with the default response object 77 | * @returns Returns true if the keys of the data object match the keys of the default response object, otherwise returns false. 78 | */ 79 | function isResponse(data: any): boolean { 80 | return compareKeys(data, defaultResponse()) 81 | } 82 | 83 | export { Command, defaultResponse, isResponse } 84 | export type { Response, DBInfo, LogInfo, MigratedData } 85 | -------------------------------------------------------------------------------- /src/views/Toolbox/Toolbox.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /src/views/Config/components/NotifyConfig.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color: #0f0f0f; 8 | background-color: #f6f6f6; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | .container { 18 | margin: 0; 19 | padding-top: 10vh; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: center; 23 | text-align: center; 24 | } 25 | 26 | .logo { 27 | height: 6em; 28 | padding: 1.5em; 29 | will-change: filter; 30 | transition: 0.75s; 31 | } 32 | 33 | .logo.tauri:hover { 34 | filter: drop-shadow(0 0 2em #24c8db); 35 | } 36 | 37 | .row { 38 | display: flex; 39 | justify-content: center; 40 | } 41 | 42 | a { 43 | font-weight: 500; 44 | color: #646cff; 45 | text-decoration: inherit; 46 | } 47 | 48 | a:hover { 49 | color: #535bf2; 50 | } 51 | 52 | h1 { 53 | text-align: center; 54 | } 55 | 56 | input, 57 | button { 58 | border-radius: 8px; 59 | border: 1px solid transparent; 60 | padding: 0.6em 1.2em; 61 | font-size: 1em; 62 | font-weight: 500; 63 | font-family: inherit; 64 | color: #0f0f0f; 65 | background-color: #ffffff; 66 | transition: border-color 0.25s; 67 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 68 | } 69 | 70 | button { 71 | cursor: pointer; 72 | } 73 | 74 | button:hover { 75 | border-color: #396cd8; 76 | } 77 | button:active { 78 | border-color: #396cd8; 79 | background-color: #e8e8e8; 80 | } 81 | 82 | input, 83 | button { 84 | outline: none; 85 | } 86 | 87 | #greet-input { 88 | margin-right: 5px; 89 | } 90 | 91 | @media (prefers-color-scheme: dark) { 92 | :root { 93 | color: #f6f6f6; 94 | background-color: #2f2f2f; 95 | } 96 | 97 | a:hover { 98 | color: #24c8db; 99 | } 100 | 101 | input, 102 | button { 103 | color: #ffffff; 104 | background-color: #0f0f0f98; 105 | } 106 | button:active { 107 | background-color: #0f0f0f69; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/store/watcher/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import { relaunch } from '@tauri-apps/api/process' 4 | import type { AppConfig } from '../types' 5 | import { Command, execute } from '../../utils/cmd' 6 | import type { WatcherConfig } from './types' 7 | 8 | export const useWatcherStore = defineStore('watcher', () => { 9 | const timeout = ref(0) 10 | 11 | /** 12 | * Gets the current watcher configuration. 13 | * @returns The current watcher configuration. 14 | */ 15 | function getConfig(): WatcherConfig { 16 | return { 17 | timeout: timeout.value, 18 | } 19 | } 20 | 21 | /** 22 | * Sets the watcher configuration. 23 | * @param config - The watcher configuration to set. 24 | */ 25 | function setConfig(config: WatcherConfig): void { 26 | timeout.value = config.timeout 27 | } 28 | 29 | /** 30 | * Initializes the watcher with the provided data or fetches the configuration if data is undefined. 31 | * @param data - The optional watcher configuration data. 32 | */ 33 | async function init(data: WatcherConfig | undefined): Promise { 34 | if (data === undefined) { 35 | await execute(Command.InitConfig, 'watcher') 36 | .then((config: AppConfig) => { 37 | setConfig(config.watcher) 38 | }) 39 | } 40 | else { 41 | setConfig(data) 42 | } 43 | } 44 | 45 | /** 46 | * Updates the timeout value for the watcher. 47 | * @param value - The new timeout value. 48 | * @param reboot - Whether to reboot after updating the timeout. 49 | */ 50 | async function updateTimeout(value: number, reboot: boolean): Promise { 51 | const config = getConfig() 52 | config.timeout = value 53 | 54 | await execute(Command.UpdateConfig, 'watcher', config) 55 | .then((config: AppConfig) => { 56 | setConfig(config.watcher) 57 | }) 58 | 59 | if (reboot) 60 | await relaunch() 61 | } 62 | 63 | return { 64 | timeout, 65 | init, 66 | updateTimeout, 67 | } 68 | }) 69 | 70 | /** 71 | * Generates the default watcher configuration. 72 | * @returns The default watcher configuration. 73 | */ 74 | export function defaultWatcherConfig(): WatcherConfig { 75 | return { 76 | timeout: 3, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "pnpm dev", 4 | "beforeBuildCommand": "pnpm build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist" 7 | }, 8 | "package": { 9 | "productName": "mission-backup", 10 | "version": "0.0.0" 11 | }, 12 | "tauri": { 13 | "allowlist": { 14 | "all": false, 15 | "shell": { 16 | "all": false, 17 | "open": true 18 | }, 19 | "window": { 20 | "all": false, 21 | "close": true, 22 | "hide": true, 23 | "show": true, 24 | "setFocus": true, 25 | "maximize": true, 26 | "minimize": true, 27 | "unmaximize": true, 28 | "unminimize": true, 29 | "startDragging": true 30 | }, 31 | "path": { 32 | "all": true 33 | }, 34 | "dialog": { 35 | "all": false, 36 | "ask": false, 37 | "confirm": false, 38 | "message": false, 39 | "open": true, 40 | "save": true 41 | }, 42 | "clipboard": { 43 | "all": true, 44 | "writeText": true, 45 | "readText": true 46 | } 47 | }, 48 | "windows": [ 49 | { 50 | "label": "main", 51 | "fullscreen": false, 52 | "resizable": true, 53 | "title": "MissionBackup", 54 | "width": 640, 55 | "height": 380, 56 | "minWidth": 640, 57 | "minHeight": 380, 58 | "visible": true, 59 | "center": true, 60 | "fileDropEnabled": true, 61 | "decorations": false 62 | }, 63 | { 64 | "width": 500, 65 | "height": 300, 66 | "center": true, 67 | "decorations": false, 68 | "url": "splashscreen.html", 69 | "label": "splashscreen", 70 | "title": "Loading", 71 | "transparent": true 72 | } 73 | ], 74 | "systemTray": { 75 | "iconPath": "icons/icon.png", 76 | "iconAsTemplate": true 77 | }, 78 | "security": { 79 | "csp": null 80 | }, 81 | "bundle": { 82 | "active": true, 83 | "targets": "all", 84 | "identifier": "com.tauri.dev", 85 | "icon": [ 86 | "icons/32x32.png", 87 | "icons/128x128.png", 88 | "icons/128x128@2x.png", 89 | "icons/icon.icns", 90 | "icons/icon.ico" 91 | ] 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src-tauri/src/db/schema.rs: -------------------------------------------------------------------------------- 1 | // @generated automatically by Diesel CLI. 2 | 3 | diesel::table! { 4 | backup (id) { 5 | id -> Integer, 6 | backup_id -> Text, 7 | mission_id -> Text, 8 | save_path -> Text, 9 | backup_size -> BigInt, 10 | reserved_0 -> Text, 11 | reserved_1 -> Text, 12 | reserved_2 -> Text, 13 | create_at -> Timestamp, 14 | update_at -> Timestamp, 15 | is_deleted -> SmallInt, 16 | delete_at -> Timestamp, 17 | } 18 | } 19 | 20 | diesel::table! { 21 | ignore (id) { 22 | id -> Integer, 23 | ignore_id -> Text, 24 | procedure_id -> Text, 25 | keyword -> Text, 26 | reserved_0 -> Text, 27 | reserved_1 -> Text, 28 | reserved_2 -> Text, 29 | create_at -> Timestamp, 30 | update_at -> Timestamp, 31 | is_deleted -> SmallInt, 32 | delete_at -> Timestamp, 33 | } 34 | } 35 | 36 | diesel::table! { 37 | mission (id) { 38 | id -> Integer, 39 | mission_id -> Text, 40 | procedure_id -> Text, 41 | name -> Text, 42 | status -> SmallInt, 43 | description -> Text, 44 | src_path -> Text, 45 | dst_path -> Text, 46 | path_type -> SmallInt, 47 | next_runtime -> Timestamp, 48 | last_trigger -> Timestamp, 49 | reserved_0 -> Text, 50 | reserved_1 -> Text, 51 | reserved_2 -> Text, 52 | create_at -> Timestamp, 53 | update_at -> Timestamp, 54 | is_deleted -> SmallInt, 55 | delete_at -> Timestamp, 56 | } 57 | } 58 | 59 | diesel::table! { 60 | procedure (id) { 61 | id -> Integer, 62 | procedure_id -> Text, 63 | name -> Text, 64 | has_ignores -> Bool, 65 | ignore_method -> SmallInt, 66 | is_compress -> Bool, 67 | compress_format -> SmallInt, 68 | trigger -> SmallInt, 69 | cron_expression -> Text, 70 | restrict -> SmallInt, 71 | restrict_days -> SmallInt, 72 | restrict_size -> BigInt, 73 | reserved_0 -> Text, 74 | reserved_1 -> Text, 75 | reserved_2 -> Text, 76 | create_at -> Timestamp, 77 | update_at -> Timestamp, 78 | is_deleted -> SmallInt, 79 | delete_at -> Timestamp, 80 | } 81 | } 82 | 83 | diesel::allow_tables_to_appear_in_same_query!( 84 | backup, 85 | ignore, 86 | mission, 87 | procedure, 88 | ); 89 | -------------------------------------------------------------------------------- /src/utils/common/index.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, isArray, isObject, snakeCase, transform } from 'lodash' 2 | 3 | // https://stackoverflow.com/questions/59769649/recursively-convert-an-object-fields-from-snake-case-to-camelcase 4 | 5 | /** 6 | * Converts keys of an object to snake_case. 7 | * @param data - The object to convert. 8 | * @returns The object with keys in snake_case. 9 | */ 10 | export function toSnakeCase(data: any): unknown { 11 | return transform(data, (acc: any, value: unknown, key: string, target) => { 12 | const snakeKey = isArray(target) ? key : snakeCase(key) 13 | 14 | acc[snakeKey] = isObject(value) ? toSnakeCase(value) : value 15 | }) 16 | } 17 | 18 | /** 19 | * Converts keys of an object to camelCase. 20 | * @param data - The object to convert. 21 | * @returns The object with keys in camelCase. 22 | */ 23 | export function toCamelCase(data: any): unknown { 24 | return transform(data, (acc: any, value: unknown, key: string, target) => { 25 | const camelKey = isArray(target) ? key : camelCase(key) 26 | 27 | acc[camelKey] = isObject(value) ? toCamelCase(value) : value 28 | }) 29 | } 30 | 31 | /** 32 | * Compares the keys of two objects to determine if they are the same. 33 | * 34 | * @param a - The first object 35 | * @param b - The second object 36 | * @returns Returns true if the two objects have the same keys, otherwise returns false. 37 | */ 38 | export function compareKeys(a: any, b: any) { 39 | const aKeys = Object.keys(a).sort() 40 | const bKeys = Object.keys(b).sort() 41 | return JSON.stringify(aKeys) === JSON.stringify(bKeys) 42 | } 43 | 44 | /** 45 | * Function that count occurrences of a substring in a string; 46 | * @param src - The string 47 | * @param sub - The subString The sub string to search for 48 | * @param allowOverlapping - Optional. (Default:false) 49 | * 50 | * @author Vitim.us https://gist.github.com/victornpb/7736865 51 | * @see Unit Test https://jsfiddle.net/Victornpb/5axuh96u/ 52 | * @see https://stackoverflow.com/a/7924240/938822 53 | */ 54 | export function subStringOccurrences(src: string, sub: string, allowOverlapping?: boolean) { 55 | src += '' 56 | sub += '' 57 | if (sub.length <= 0) 58 | return (src.length + 1) 59 | 60 | let n = 0 61 | let pos = 0 62 | const step = allowOverlapping ? 1 : sub.length 63 | 64 | while (true) { 65 | pos = src.indexOf(sub, pos) 66 | if (pos >= 0) { 67 | ++n 68 | pos += step 69 | } 70 | else { 71 | break 72 | } 73 | } 74 | return n 75 | } 76 | -------------------------------------------------------------------------------- /src/views/Screensaver/Screensaver.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 71 | 72 | 92 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src-tauri/src/core/tray.rs: -------------------------------------------------------------------------------- 1 | //! # Tray 2 | //! 3 | //! `tray` module contains functions about tauri system tray. 4 | 5 | use tauri::{ Wry, Manager, AppHandle, 6 | SystemTray, CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent 7 | }; 8 | 9 | /// Create app system tray. 10 | /// 11 | /// # Arguments 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// use core::tray::create_system_tray; 17 | /// 18 | /// fn main() { 19 | /// tauri::Builder::default() 20 | /// .system_tray(create_system_tray()) 21 | /// .run(tauri::generate_context!()) 22 | /// .expect("error while running tauri application"); 23 | /// } 24 | /// ``` 25 | pub fn create_system_tray() -> SystemTray { 26 | let quit = CustomMenuItem::new("quit".to_string(), "Quit Program"); 27 | let hide = CustomMenuItem::new("hide".to_string(), "Close to tray"); 28 | let tray_menu = SystemTrayMenu::new() 29 | .add_item(hide) 30 | .add_native_item(SystemTrayMenuItem::Separator) 31 | .add_item(quit); 32 | SystemTray::new().with_menu(tray_menu) 33 | } 34 | 35 | #[allow(dead_code)] 36 | /// Handle tauri app system tray event. 37 | /// 38 | /// # Arguments 39 | /// 40 | /// * `app` - A handle for current tauri app 41 | /// * `event` - An event from system tray 42 | /// 43 | /// # Examples 44 | /// 45 | /// ``` 46 | /// use core::tray::{create_system_tray, on_system_tray_event}; 47 | /// 48 | /// fn main() { 49 | /// tauri::Builder::default() 50 | /// .system_tray(create_system_tray()) 51 | /// .on_system_tray_event(on_system_tray_event) 52 | /// .run(tauri::generate_context!()) 53 | /// .expect("error while running tauri application"); 54 | /// } 55 | /// ``` 56 | pub fn on_system_tray_event(app: &AppHandle, event: SystemTrayEvent) { 57 | match event { 58 | SystemTrayEvent::DoubleClick { position: _ , size: _, .. } => { 59 | let window = app.get_window("main").unwrap(); 60 | window.unminimize().unwrap(); 61 | window.show().unwrap(); 62 | window.set_focus().unwrap(); 63 | }, 64 | SystemTrayEvent::MenuItemClick { id, ..} => { 65 | match id.as_str() { 66 | "quit" => { 67 | app.get_window("main").unwrap().hide().unwrap(); 68 | 69 | std::process::exit(0); 70 | } 71 | "hide" => { 72 | app.get_window("main").unwrap().hide().unwrap(); 73 | } 74 | _ => {} 75 | } 76 | } 77 | _ => {} 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mission-backup" 3 | version = "0.0.0" 4 | description = "A cross-platform application helps you keeping local backups for your data." 5 | authors = ["stein"] 6 | edition = "2021" 7 | # documentation = "https://mission-backup.hellagur.com" 8 | homepage = "https://mission-backup.hellagur.com" 9 | repository = "https://github.com/Hellager/mission-backup" 10 | license = "Apache-2.0" 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [build-dependencies] 14 | tauri-build = { version = "1", features = [] } 15 | 16 | [dependencies] 17 | tauri = { version = "1", features = [ "clipboard-all", "window-set-focus", "dialog-open", "dialog-save", "path-all", "window-minimize", "window-close", "window-unmaximize", "window-maximize", "window-unminimize", "window-hide", "window-show", "window-start-dragging", "system-tray", "shell-open"] } 18 | serde = { version = "1", features = ["derive"] } 19 | serde_json = "1" 20 | chrono = { version = "0.4.35", features = ["serde"] } 21 | sys-locale = "0.3.1" 22 | base64 = "0.22.0" 23 | sha2 = "0.10.8" 24 | url = "2.5.0" 25 | zip = "0.6.6" 26 | tar = "0.4.40" 27 | flate2 = "1.0.28" 28 | bzip2 = "0.4.4" 29 | xz2 = "0.1.7" 30 | sevenz-rust= {version="0.5.4", features= ["compress"] } 31 | walkdir = "2.5.0" 32 | ignore = "0.4.22" 33 | fs_extra = "1.3.0" 34 | dark-light = "1.1.1" 35 | wry = "0.24.6" 36 | rand = "0.8.5" 37 | byte-unit = "5.1.4" 38 | log = "0.4.20" 39 | time = "0.3.34" 40 | simplelog = "0.12.2" 41 | directories = "5.0.1" 42 | log4rs = {version="1.3.0", features= ["rolling_file_appender"] } 43 | tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 44 | tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 45 | toml = "0.8.12" 46 | window-shadows = "0.2.2" 47 | diesel = { version = "2.2.1", features = ["sqlite", "chrono", "returning_clauses_for_sqlite_3_35"] } 48 | libsqlite3-sys = { version = "^0", features = ["bundled"] } 49 | diesel_migrations = { version = "2.2.0", features = ["sqlite"] } 50 | dotenvy = "0.15.7" 51 | tokio = "1.38.0" 52 | tokio-cron-scheduler = "0.10.2" 53 | notify = "6.1.1" 54 | notify-debouncer-full = "0.3.1" 55 | path-absolutize = "3.1.1" 56 | 57 | [dependencies.uuid] 58 | version = "1.8.0" 59 | features = [ 60 | "v4", # Lets you generate random UUIDs 61 | "fast-rng", # Use a faster (but still sufficiently random) RNG 62 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 63 | ] 64 | 65 | [features] 66 | # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! 67 | custom-protocol = ["tauri/custom-protocol"] 68 | 69 | [dev-dependencies] 70 | same-file = "1.0.6" 71 | -------------------------------------------------------------------------------- /src/components/CloseDialog.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 101 | 102 | 104 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CloseDialog: typeof import('./src/components/CloseDialog.vue')['default'] 11 | ElAside: typeof import('element-plus/es')['ElAside'] 12 | ElButton: typeof import('element-plus/es')['ElButton'] 13 | ElCard: typeof import('element-plus/es')['ElCard'] 14 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 15 | ElCol: typeof import('element-plus/es')['ElCol'] 16 | ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] 17 | ElContainer: typeof import('element-plus/es')['ElContainer'] 18 | ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] 19 | ElDialog: typeof import('element-plus/es')['ElDialog'] 20 | ElDivider: typeof import('element-plus/es')['ElDivider'] 21 | ElForm: typeof import('element-plus/es')['ElForm'] 22 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 23 | ElHeader: typeof import('element-plus/es')['ElHeader'] 24 | ElIcon: typeof import('element-plus/es')['ElIcon'] 25 | ElInput: typeof import('element-plus/es')['ElInput'] 26 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] 27 | ElMain: typeof import('element-plus/es')['ElMain'] 28 | ElMenu: typeof import('element-plus/es')['ElMenu'] 29 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 30 | ElOption: typeof import('element-plus/es')['ElOption'] 31 | ElPopover: typeof import('element-plus/es')['ElPopover'] 32 | ElRadio: typeof import('element-plus/es')['ElRadio'] 33 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 34 | ElRow: typeof import('element-plus/es')['ElRow'] 35 | ElSelect: typeof import('element-plus/es')['ElSelect'] 36 | ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] 37 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 38 | ElTable: typeof import('element-plus/es')['ElTable'] 39 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 40 | ElTabPane: typeof import('element-plus/es')['ElTabPane'] 41 | ElTabs: typeof import('element-plus/es')['ElTabs'] 42 | ElText: typeof import('element-plus/es')['ElText'] 43 | ElTooltip: typeof import('element-plus/es')['ElTooltip'] 44 | RouterLink: typeof import('vue-router')['RouterLink'] 45 | RouterView: typeof import('vue-router')['RouterView'] 46 | SideMenu: typeof import('./src/components/SideMenu.vue')['default'] 47 | StatusDot: typeof import('./src/components/StatusDot.vue')['default'] 48 | TitleBar: typeof import('./src/components/TitleBar.vue')['default'] 49 | } 50 | export interface ComponentCustomProperties { 51 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/views/Config/components/WatcherConfig.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 93 | 94 | 107 | -------------------------------------------------------------------------------- /src-tauri/src/core/setup.rs: -------------------------------------------------------------------------------- 1 | //! # Setup 2 | //! 3 | //! `setup` module contains functions about setup tauri app itself and its' commands. 4 | use tauri::{App, Manager}; 5 | use std::collections::HashMap; 6 | 7 | /// Setup tauri app. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `app` - An app instance 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// use core::setup::setup_handler; 17 | /// 18 | /// fn main() { 19 | /// tauri::Builder::default() 20 | /// .setup(crate::core::setup::setup_handler) 21 | /// .run(tauri::generate_context!()) 22 | /// .expect("error while running tauri application"); 23 | /// } 24 | /// ``` 25 | pub fn setup_handler(app: &mut App) -> Result<(), Box> { 26 | use super::state::{ MissionHandler, HandlerStatus, MissionHandlerState }; 27 | use super::window; 28 | use crate::config::AppConfig; 29 | use tokio::sync::Mutex; 30 | use log::error; 31 | 32 | if let Some(main_window) = app.get_window("main") { 33 | // add window shadows to app, not available on linux now 34 | #[cfg(not(target_os = "linux"))] 35 | window::init_window_shadow(&main_window, true); 36 | } else { 37 | error!("failed to init window shadow"); 38 | } 39 | 40 | let state = MissionHandlerState(Mutex::new(MissionHandler { 41 | is_set: false, 42 | status: HandlerStatus::default(), 43 | config: AppConfig::default(), 44 | 45 | app_handler: Some(app.handle().clone()), 46 | log_handler: None, 47 | db_handler: None, 48 | cron_handler: None, 49 | watcher_handler: None, 50 | watcher_receiver: None, 51 | cron_jobs: HashMap::new(), 52 | monitor_jobs: HashMap::new() 53 | })); 54 | 55 | app.manage(state); 56 | 57 | Ok(()) 58 | } 59 | 60 | /// Setup tauri commands. 61 | /// 62 | /// # Arguments 63 | /// 64 | /// # Examples 65 | /// 66 | /// ``` 67 | /// use core::setup::setup_command; 68 | /// 69 | /// fn main() { 70 | /// tauri::Builder::default() 71 | /// .invoke_handler(crate::core::setup::setup_command()) 72 | /// .run(tauri::generate_context!()) 73 | /// .expect("error while running tauri application"); 74 | /// } 75 | /// ``` 76 | pub fn setup_command() -> Box) + Send + Sync> { 77 | use super::cmd::*; 78 | 79 | Box::new(tauri::generate_handler![ 80 | init_app, 81 | shutdown_app, 82 | web_log, 83 | show_item_in_explorer, 84 | sync_config, 85 | create_record, 86 | update_record, 87 | query_record, 88 | delete_record, 89 | clear_record, 90 | delete_backup, 91 | set_mission_status, 92 | create_mission, 93 | delete_mission, 94 | query_statistic_record, 95 | query_db_info, 96 | clean_database, 97 | query_log_info, 98 | clean_app_log, 99 | migrate_from_old 100 | ]) 101 | } 102 | -------------------------------------------------------------------------------- /src/views/Toolbox/components/DatabaseTool.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 99 | 100 | 108 | -------------------------------------------------------------------------------- /src/views/Toolbox/components/LogTool.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 101 | 102 | 110 | -------------------------------------------------------------------------------- /.commitlintrc.cjs: -------------------------------------------------------------------------------- 1 | // .commitlintrc.js 2 | /** @type {import('cz-git').UserConfig} */ 3 | module.exports = { 4 | rules: { 5 | // @see: https://commitlint.js.org/#/reference-rules 6 | }, 7 | prompt: { 8 | alias: { fd: 'docs: fix typos' }, 9 | messages: { 10 | type: 'Select the type of change that you\'re committing:', 11 | scope: 'Denote the SCOPE of this change (optional):', 12 | customScope: 'Denote the SCOPE of this change:', 13 | subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', 14 | body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', 15 | breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n', 16 | footerPrefixesSelect: 'Select the ISSUES type of changeList by this change (optional):', 17 | customFooterPrefix: 'Input ISSUES prefix:', 18 | footer: 'List any ISSUES by this change. E.g.: #31, #34:\n', 19 | generatingByAI: 'Generating your AI commit subject...', 20 | generatedSelectByAI: 'Select suitable subject by AI generated:', 21 | confirmCommit: 'Are you sure you want to proceed with the commit above?', 22 | }, 23 | types: [ 24 | { value: 'feat', name: 'feat: A new feature', emoji: ':sparkles:' }, 25 | { value: 'fix', name: 'fix: A bug fix', emoji: ':bug:' }, 26 | { value: 'docs', name: 'docs: Documentation only changes', emoji: ':memo:' }, 27 | { value: 'style', name: 'style: Changes that do not affect the meaning of the code', emoji: ':lipstick:' }, 28 | { value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature', emoji: ':recycle:' }, 29 | { value: 'perf', name: 'perf: A code change that improves performance', emoji: ':zap:' }, 30 | { value: 'test', name: 'test: Adding missing tests or correcting existing tests', emoji: ':white_check_mark:' }, 31 | { value: 'build', name: 'build: Changes that affect the build system or external dependencies', emoji: ':package:' }, 32 | { value: 'ci', name: 'ci: Changes to our CI configuration files and scripts', emoji: ':ferris_wheel:' }, 33 | { value: 'chore', name: 'chore: Other changes that don\'t modify src or test files', emoji: ':hammer:' }, 34 | { value: 'revert', name: 'revert: Reverts a previous commit', emoji: ':rewind:' }, 35 | ], 36 | useEmoji: false, 37 | emojiAlign: 'center', 38 | useAI: false, 39 | aiNumber: 1, 40 | themeColorCode: '', 41 | scopes: [], 42 | allowCustomScopes: true, 43 | allowEmptyScopes: true, 44 | customScopesAlign: 'bottom', 45 | customScopesAlias: 'custom', 46 | emptyScopesAlias: 'empty', 47 | upperCaseSubject: false, 48 | markBreakingChangeMode: false, 49 | allowBreakingChanges: ['feat', 'fix'], 50 | breaklineNumber: 100, 51 | breaklineChar: '|', 52 | skipQuestions: [], 53 | issuePrefixes: [{ value: 'closed', name: 'closed: ISSUES has been processed' }], 54 | customIssuePrefixAlign: 'top', 55 | emptyIssuePrefixAlias: 'skip', 56 | customIssuePrefixAlias: 'custom', 57 | allowCustomIssuePrefix: true, 58 | allowEmptyIssuePrefix: true, 59 | confirmColorize: true, 60 | scopeOverrides: undefined, 61 | defaultBody: '', 62 | defaultIssues: '', 63 | defaultScope: '', 64 | defaultSubject: '', 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /splashscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 36 | 37 | 38 |
39 | 40 | Loading... 41 |
42 | 43 | 93 | 94 | -------------------------------------------------------------------------------- /public/splashscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 36 | 37 | 38 |
39 | 40 | Loading... 41 |
42 | 43 | 93 | 94 | -------------------------------------------------------------------------------- /src/views/Statistic/components/BackupChart.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | 133 | 134 | 145 | -------------------------------------------------------------------------------- /src/views/Config/components/SystemConfig.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 112 | 113 | 124 | -------------------------------------------------------------------------------- /src/views/Config/components/ScreensaverConfig.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 123 | 124 | 131 | -------------------------------------------------------------------------------- /src/store/screensaver/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import type { AppConfig } from '../types' 4 | import { Command, execute } from '../../utils/cmd' 5 | import type { ScreensaverConfig } from './types' 6 | 7 | export const useScreensaverStore = defineStore('screensaver', () => { 8 | const enable = ref(false) 9 | const password = ref('') 10 | const isLocked = ref(false) 11 | 12 | /** 13 | * Gets the current screensaver configuration. 14 | * @returns The current screensaver configuration. 15 | */ 16 | function getConfig(): ScreensaverConfig { 17 | return { 18 | enable: enable.value, 19 | password: password.value, 20 | isLocked: isLocked.value, 21 | } 22 | } 23 | 24 | /** 25 | * Sets the screensaver configuration. 26 | * @param config - The screensaver configuration to set. 27 | */ 28 | function setConfig(config: ScreensaverConfig): void { 29 | enable.value = config.enable 30 | password.value = config.password 31 | isLocked.value = config.isLocked 32 | } 33 | 34 | /** 35 | * Initializes the screensaver with the provided data or fetches the configuration if data is undefined. 36 | * @param data - The optional screensaver configuration data. 37 | */ 38 | async function init(data: ScreensaverConfig | undefined): Promise { 39 | if (data === undefined) { 40 | await execute(Command.InitConfig, 'screensaver') 41 | .then((config: AppConfig) => { 42 | setConfig(config.screensaver) 43 | }) 44 | } 45 | else { 46 | setConfig(data) 47 | } 48 | } 49 | 50 | /** 51 | * Enables or disables the screensaver. 52 | * @param value - The value to set for enabling the screensaver. 53 | */ 54 | async function enableScreensaver(value: boolean): Promise { 55 | const config = getConfig() 56 | config.enable = value 57 | 58 | await execute(Command.UpdateConfig, 'screensaver', config) 59 | .then((config: AppConfig) => { 60 | setConfig(config.screensaver) 61 | }) 62 | } 63 | 64 | /** 65 | * Updates the password for the screensaver. 66 | * @param value - The new password for the screensaver. 67 | */ 68 | async function updatePassword(value: string): Promise { 69 | const config = getConfig() 70 | config.password = value 71 | 72 | await execute(Command.UpdateConfig, 'screensaver', config) 73 | .then((config: AppConfig) => { 74 | setConfig(config.screensaver) 75 | }) 76 | } 77 | 78 | /** 79 | * Updates the lock status of the screensaver. 80 | * @param value - The new lock status for the screensaver. 81 | */ 82 | async function updateLockStatus(value: boolean): Promise { 83 | isLocked.value = value 84 | } 85 | 86 | /** 87 | * Tries to unlock the screensaver with the provided password. 88 | * @param value - The password to try for unlocking the screensaver. 89 | * @returns A boolean indicating whether the unlock attempt was successful. 90 | */ 91 | async function tryUnlock(value: string): Promise { 92 | if (password.value !== value) { 93 | await execute(Command.WebLog, 'warn', `Unlock screensaver failed, invalid password: ${value}`) 94 | return false 95 | } 96 | else { 97 | await execute(Command.WebLog, 'info', `Unlock screensaver success`) 98 | } 99 | 100 | return true 101 | } 102 | 103 | return { 104 | enable, 105 | password, 106 | isLocked, 107 | init, 108 | enableScreensaver, 109 | updatePassword, 110 | updateLockStatus, 111 | tryUnlock, 112 | } 113 | }) 114 | 115 | /** 116 | * Get the default screensaver configuration. 117 | * @returns The default ScreensaverConfig object. 118 | */ 119 | export function defaultScreensaverConfig(): ScreensaverConfig { 120 | return { 121 | enable: false, 122 | password: '', 123 | isLocked: false, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src-tauri/src/core/window.rs: -------------------------------------------------------------------------------- 1 | //! # Window 2 | //! 3 | //! `window` module contains functions about tauri app window. 4 | 5 | use tauri::{ Wry, GlobalWindowEvent, WindowEvent, FileDropEvent, Theme, Window, Manager }; 6 | 7 | /// Handle app window event. 8 | /// 9 | /// # Arguments 10 | /// 11 | /// - `event` - An event from app window 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// use core::window::on_window_event; 17 | /// 18 | /// fn main() { 19 | /// tauri::Builder::default() 20 | /// .on_window_event(on_window_event) 21 | /// .run(tauri::generate_context!()) 22 | /// .expect("error while running tauri application"); 23 | /// } 24 | /// ``` 25 | pub fn on_window_event(event: GlobalWindowEvent) { 26 | use super::cmd::Response; 27 | use log::{info, error}; 28 | 29 | match event.event() { 30 | WindowEvent::CloseRequested { api, .. } => { 31 | let window = event.window(); 32 | window.hide().unwrap(); 33 | api.prevent_close(); 34 | println!("window {} try to close", window.label()); 35 | 36 | std::process::exit(0); 37 | }, 38 | WindowEvent::FileDrop(drop_event) => { 39 | match drop_event { 40 | FileDropEvent::Hovered(files) => { 41 | println!("files: {:?} hovered", files); 42 | }, 43 | FileDropEvent::Dropped(files) => { 44 | println!("files: {:?} dropped", files); 45 | }, 46 | FileDropEvent::Cancelled => { 47 | 48 | }, 49 | _ => {} 50 | } 51 | }, 52 | WindowEvent::ThemeChanged(theme) => { 53 | match theme { 54 | Theme::Light => { 55 | let window = event.window(); 56 | let payload = Response::success("light"); 57 | match window.emit_all("sys_theme", payload) { 58 | Ok(()) => { 59 | info!("Detect system theme update to light"); 60 | }, 61 | Err(error) => { 62 | error!("Failed to send sys_theme update event, errMsg: {:?}", error); 63 | } 64 | } 65 | }, 66 | Theme::Dark => { 67 | let window = event.window(); 68 | let payload = Response::success("dark"); 69 | match window.emit_all("sys_theme", payload) { 70 | Ok(()) => { 71 | info!("Detect system theme update to dark"); 72 | }, 73 | Err(error) => { 74 | error!("Failed to send sys_theme update event, errMsg: {:?}", error); 75 | } 76 | } 77 | }, 78 | _ => {} 79 | } 80 | } 81 | _ => {} 82 | } 83 | } 84 | 85 | /// Initialize app window shadow, not available for linux for now. 86 | /// 87 | /// # Arguments 88 | /// 89 | /// * `window` - A webview window 90 | /// * `is_enable` - Whether enable window shadow 91 | /// 92 | /// # Examples 93 | /// 94 | /// ``` 95 | /// use core::window::init_window_shadow; 96 | /// 97 | /// fn main() { 98 | /// tauri::Builder::default() 99 | /// .setup(move |app| { 100 | /// let main_window = app.get_window("main").unwrap(); 101 | /// 102 | /// #[cfg(not(target_os = "linux"))] 103 | /// init_window_shadow(&main_window, true); 104 | /// }) 105 | /// .run(tauri::generate_context!()) 106 | /// .expect("error while running tauri application"); 107 | /// } 108 | /// ``` 109 | pub fn init_window_shadow(window: &Window, is_enable: bool) { 110 | use window_shadows::set_shadow; 111 | 112 | if let Err(e) = set_shadow(window, is_enable) { 113 | println!("Failed to add native window shadow, errMsg: {:?}", e); 114 | } 115 | } -------------------------------------------------------------------------------- /src/assets/style/route.less: -------------------------------------------------------------------------------- 1 | .route-slide-in-right-enter-active, 2 | .route-slide-in-right-leave-active { 3 | transition: all 0.85s ease-in-out; 4 | } 5 | 6 | .route-slide-in-right-enter-to { 7 | position: absolute; 8 | right: 0; 9 | top: 25px; 10 | } 11 | 12 | .route-slide-in-right-enter-from { 13 | position: absolute; 14 | right: -100%; 15 | top: 25px; 16 | } 17 | 18 | .route-slide-in-right-leave-to { 19 | opacity: 0; 20 | } 21 | 22 | .route-slide-in-right-leave-from { 23 | opacity: 1; 24 | } 25 | 26 | .route-slide-in-left-enter-active, 27 | .route-slide-in-left-leave-active { 28 | transition: all 0.85s ease-in-out; 29 | } 30 | 31 | .route-slide-in-left-enter-to { 32 | position: absolute; 33 | left: 140px; 34 | top: 25px; 35 | } 36 | 37 | .route-slide-in-left-enter-from { 38 | position: absolute; 39 | left: calc(-100% + 140px); 40 | top: 25px; 41 | } 42 | 43 | .route-slide-in-left-leave-to { 44 | opacity: 0; 45 | } 46 | 47 | .route-slide-in-left-leave-from { 48 | opacity: 1; 49 | } 50 | 51 | .route-slide-out-left-enter-active, 52 | .route-slide-out-left-leave-active { 53 | transition: all 0.85s ease-in-out; 54 | } 55 | 56 | .route-slide-out-left-enter-to { 57 | opacity: 1; 58 | } 59 | 60 | .route-slide-out-left-enter-from { 61 | opacity: 0; 62 | } 63 | 64 | .route-slide-out-left-leave-to { 65 | position: absolute; 66 | left: calc(-100% + 140px); 67 | top: 25px; 68 | } 69 | 70 | .route-slide-out-left-leave-from { 71 | position: absolute; 72 | left: 140px; 73 | top: 25px; 74 | } 75 | 76 | .route-slide-out-right-enter-active, 77 | .route-slide-out-right-leave-active { 78 | transition: all 0.85s ease-in-out; 79 | } 80 | 81 | .route-slide-out-right-enter-to { 82 | opacity: 1; 83 | } 84 | 85 | .route-slide-out-right-enter-from { 86 | opacity: 0; 87 | } 88 | 89 | .route-slide-out-right-leave-to { 90 | position: absolute; 91 | right: -100%; 92 | top: 25px; 93 | } 94 | 95 | .route-slide-out-right-leave-from { 96 | position: absolute; 97 | right: 0; 98 | top: 25px; 99 | } 100 | 101 | .route-slide-in-up-enter-active, 102 | .route-slide-in-up-leave-active { 103 | transition: all 0.85s ease-in-out; 104 | } 105 | 106 | .route-slide-in-up-enter-to { 107 | position: absolute; 108 | top: 25px; 109 | left: 0; 110 | } 111 | 112 | .route-slide-in-up-enter-from { 113 | position: absolute; 114 | top: -100%; 115 | left: 0; 116 | } 117 | 118 | .route-slide-in-up-leave-to { 119 | opacity: 0; 120 | } 121 | 122 | .route-slide-in-up-leave-from { 123 | opacity: 1; 124 | } 125 | 126 | .route-slide-in-down-enter-active, 127 | .route-slide-in-down-leave-active { 128 | transition: all 0.85s ease-in-out; 129 | } 130 | 131 | .route-slide-in-down-enter-to { 132 | position: absolute; 133 | top: 25px; 134 | left: 0; 135 | } 136 | 137 | .route-slide-in-down-enter-from { 138 | position: absolute; 139 | top: 100%; 140 | left: 0; 141 | } 142 | 143 | .route-slide-in-down-leave-to { 144 | opacity: 0; 145 | } 146 | 147 | .route-slide-in-down-leave-from { 148 | opacity: 1; 149 | } 150 | 151 | .route-slide-out-up-enter-active, 152 | .route-slide-out-up-leave-active { 153 | transition: all 0.85s ease-in-out; 154 | } 155 | 156 | .route-slide-out-up-enter-to { 157 | opacity: 1; 158 | } 159 | 160 | .route-slide-out-up-enter-from { 161 | opacity: 0; 162 | } 163 | 164 | .route-slide-out-up-leave-to { 165 | position: absolute; 166 | top: -100%; 167 | left: 0; 168 | } 169 | 170 | .route-slide-out-up-leave-from { 171 | position: absolute; 172 | top: 25px; 173 | left: 0; 174 | } 175 | 176 | .route-slide-out-down-enter-active, 177 | .route-slide-out-down-leave-active { 178 | transition: all 0.85s ease-in-out; 179 | } 180 | 181 | .route-slide-out-down-enter-to { 182 | opacity: 1; 183 | } 184 | 185 | .route-slide-out-down-enter-from { 186 | opacity: 0; 187 | } 188 | 189 | .route-slide-out-down-leave-to { 190 | position: absolute; 191 | top: 100%; 192 | left: 0; 193 | } 194 | 195 | .route-slide-out-down-leave-from { 196 | position: absolute; 197 | top: 25px; 198 | left: 0; 199 | } 200 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 112 | 113 | 146 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 13 | 15 | 16 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 33 | 35 | 36 | 43 | 45 | 47 | 48 | -------------------------------------------------------------------------------- /src/store/notify/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification' 4 | import type { AppConfig } from '../types' 5 | import { Command, execute } from '../../utils/cmd' 6 | import type { NotifyConfig } from './types' 7 | 8 | export const useNotifyStore = defineStore('notify', () => { 9 | const isGranted = ref(false) 10 | const enable = ref(false) 11 | const whenCreate = ref(false) 12 | const whenFailed = ref(false) 13 | 14 | /** 15 | * Get the current notification configuration. 16 | * @returns The current NotifyConfig object. 17 | */ 18 | function getConfig(): NotifyConfig { 19 | return { 20 | isGranted: isGranted.value, 21 | enable: enable.value, 22 | whenCreate: whenCreate.value, 23 | whenFailed: whenFailed.value, 24 | } 25 | } 26 | 27 | /** 28 | * Set the notification configuration. 29 | * @param config - The NotifyConfig object to set. 30 | */ 31 | function setConfig(config: NotifyConfig) { 32 | isGranted.value = config.isGranted 33 | enable.value = config.enable 34 | whenCreate.value = config.whenCreate 35 | whenFailed.value = config.whenFailed 36 | } 37 | 38 | /** 39 | * Initialize the notification configuration. 40 | * @param data - The initial NotifyConfig data. 41 | */ 42 | async function init(data: NotifyConfig | undefined) { 43 | if (data === undefined) { 44 | await execute(Command.InitConfig, 'notify') 45 | .then((config: AppConfig) => { 46 | setConfig(config.notify) 47 | }) 48 | } 49 | else { 50 | setConfig(data) 51 | } 52 | } 53 | 54 | /** 55 | * Enable or disable notifications. 56 | * @param value - The value to set for notification enablement. 57 | */ 58 | async function enableNotify(value: boolean) { 59 | const config = getConfig() 60 | config.enable = value 61 | await execute(Command.UpdateConfig, 'notify', config) 62 | .then((config: AppConfig) => { 63 | setConfig(config.notify) 64 | }) 65 | } 66 | 67 | /** 68 | * Update the create backup notification setting. 69 | * @param value - The value to set for create backup notification. 70 | */ 71 | async function updatewhenCreateNotify(value: boolean) { 72 | const config = getConfig() 73 | config.whenCreate = value 74 | await execute(Command.UpdateConfig, 'notify', config) 75 | .then((config: AppConfig) => { 76 | setConfig(config.notify) 77 | }) 78 | } 79 | 80 | /** 81 | * Update the failed backup notification setting. 82 | * @param value - The value to set for failed backup notification. 83 | */ 84 | async function updatewhenFailedNotify(value: boolean) { 85 | const config = getConfig() 86 | config.whenFailed = value 87 | await execute(Command.UpdateConfig, 'notify', config) 88 | .then((config: AppConfig) => { 89 | setConfig(config.notify) 90 | }) 91 | } 92 | 93 | /** 94 | * Update the notification granted status. 95 | * @param value - The value to set for notification granted status. 96 | */ 97 | async function updateNotifyGranted(value: boolean) { 98 | const config = getConfig() 99 | config.isGranted = value 100 | await execute(Command.UpdateConfig, 'notify', config) 101 | .then((config: AppConfig) => { 102 | setConfig(config.notify) 103 | }) 104 | } 105 | 106 | /** 107 | * Attempt to get permission for notifications. 108 | * @returns A boolean indicating if permission was granted. 109 | */ 110 | async function tryGetPermission(): Promise { 111 | const granted = await isPermissionGranted() 112 | if (!granted) { 113 | const permission = await requestPermission() 114 | if (permission !== 'granted') 115 | return false 116 | } 117 | 118 | updateNotifyGranted(true) 119 | return true 120 | } 121 | 122 | return { 123 | isGranted, 124 | enable, 125 | whenCreate, 126 | whenFailed, 127 | init, 128 | enableNotify, 129 | updatewhenCreateNotify, 130 | updatewhenFailedNotify, 131 | tryGetPermission, 132 | } 133 | }) 134 | 135 | /** 136 | * Get the default notify configuration object. 137 | * @returns The default NotifyConfig object. 138 | */ 139 | export function defaultNotifyConfig(): NotifyConfig { 140 | return { 141 | isGranted: false, 142 | enable: false, 143 | whenCreate: false, 144 | whenFailed: false, 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/views/Toolbox/components/MigrateTool.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 145 | 146 | 161 | -------------------------------------------------------------------------------- /src-tauri/src/utils/common.rs: -------------------------------------------------------------------------------- 1 | //! The `common` module contains various commonly used functions. 2 | 3 | #[allow(dead_code)] 4 | /// Get app home directory, not the exe directory 5 | /// 6 | /// # Arguments 7 | /// 8 | /// # Examples 9 | /// 10 | /// ``` 11 | /// use common::get_app_home_dir; 12 | /// 13 | /// let app_dir = get_app_home_dir(); 14 | /// match app_dir { 15 | /// Ok(dir) => println!("app running at: {:?}", dir), 16 | /// Err(error) => println!("get app home dir failed, errMsg: {:?}", error), 17 | /// } 18 | /// ``` 19 | pub fn get_app_home_dir() -> Result { 20 | #[cfg(target_os = "windows")] 21 | return std::env::current_dir(); 22 | 23 | #[cfg(not(target_os = "windows"))] 24 | match std::env::home_dir() { 25 | None => { 26 | Err(ErrorKind::NotFound) 27 | }, 28 | Some(path) => { 29 | Ok(path.join(APP_DIR)) 30 | } 31 | } 32 | } 33 | 34 | #[allow(dead_code)] 35 | /// Gets system locale. 36 | /// 37 | /// # Arguments 38 | /// 39 | /// # Examples 40 | /// 41 | /// ``` 42 | /// use common::get_sys_locale; 43 | /// 44 | /// let locale = get_sys_locale(); 45 | /// println!("current locale: {:?}", locale), 46 | /// ``` 47 | pub fn get_sys_locale() -> String { 48 | use sys_locale::get_locale; 49 | 50 | get_locale().unwrap_or_else(|| String::from("en-US")) 51 | } 52 | 53 | #[allow(dead_code)] 54 | /// Get system theme. 55 | /// 56 | /// # Arguments 57 | /// 58 | /// # Examples 59 | /// 60 | /// ``` 61 | /// use common::get_sys_theme; 62 | /// 63 | /// let theme = get_sys_theme(); 64 | /// println!("current theme: {:?}", theme), 65 | /// ``` 66 | pub fn get_sys_theme() -> String { 67 | let mode = dark_light::detect(); 68 | 69 | if mode == dark_light::Mode::Dark { "dark".to_string() } else { "light".to_string() } 70 | } 71 | 72 | #[allow(dead_code)] 73 | /// Get system webview versoin. 74 | /// 75 | /// # Arguments 76 | /// 77 | /// # Examples 78 | /// 79 | /// ``` 80 | /// use common::get_webview_version; 81 | /// 82 | /// let res = get_webview_version(); 83 | /// 84 | /// match res { 85 | /// Ok(version) => println!("webview version: {:?}", version), 86 | /// Err(error) => println!("get webview version failed, errMsg: {:?}", error), 87 | /// } 88 | /// ``` 89 | pub fn get_webview_version() -> Result { 90 | use wry::webview::webview_version; 91 | 92 | webview_version() 93 | } 94 | 95 | #[allow(dead_code)] 96 | /// Generate a random i32 number 97 | /// 98 | /// # Arguments 99 | /// 100 | /// # Examples 101 | /// 102 | /// ``` 103 | /// use common::rand_number; 104 | /// 105 | /// let number = rand_number(); 106 | /// println!("the random number is {}", number); 107 | /// ``` 108 | pub fn rand_number() -> i32 { 109 | use rand::Rng; 110 | 111 | let mut rng = rand::thread_rng(); 112 | 113 | rng.gen::() 114 | } 115 | 116 | #[cfg(test)] 117 | mod test { 118 | use super::*; 119 | 120 | #[test] 121 | fn test_get_app_home_dir() { 122 | use std::env::current_dir; 123 | 124 | let std_dir = current_dir().expect(""); 125 | let home_dir = get_app_home_dir().expect(""); 126 | 127 | assert_ne!(std_dir.display().to_string(), "".to_string()); 128 | assert_ne!(home_dir.display().to_string(), "".to_string()); 129 | assert_eq!(std_dir, home_dir); 130 | } 131 | 132 | #[test] 133 | fn test_get_sys_locale() { 134 | let sys_locale = sys_locale::get_locale().expect("en-US"); 135 | let locale = get_sys_locale(); 136 | 137 | assert_ne!(locale, "".to_string()); 138 | assert_eq!(locale, sys_locale); 139 | } 140 | 141 | #[test] 142 | fn test_get_sys_theme() { 143 | let sys_theme = dark_light::detect(); 144 | let theme = get_sys_theme(); 145 | 146 | assert_ne!(theme, "".to_string()); 147 | match sys_theme { 148 | dark_light::Mode::Dark => { 149 | assert_eq!(theme, "dark"); 150 | }, 151 | dark_light::Mode::Light => { 152 | assert_eq!(theme, "light"); 153 | }, 154 | dark_light::Mode::Default => { 155 | assert_eq!(theme, "light"); 156 | } 157 | } 158 | } 159 | 160 | #[test] 161 | fn test_get_webview_version() { 162 | let sys_webview_version = wry::webview::webview_version(); 163 | let version = get_webview_version().expect(""); 164 | 165 | if let Ok(webview_version) = sys_webview_version { 166 | assert_eq!(webview_version, version); 167 | } else { 168 | assert_eq!(version, ""); 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /src-tauri/src/db/utils.rs: -------------------------------------------------------------------------------- 1 | //! # Utils 2 | //! 3 | //! `utils` module contains all functions about database utils. 4 | 5 | use diesel::prelude::*; 6 | use diesel::sqlite::SqliteConnection; 7 | use serde::{Serialize, Deserialize}; 8 | 9 | /// Struct Record 10 | #[derive(Debug, Serialize, Deserialize, Clone)] 11 | pub struct DBInfo { 12 | pub path: String, 13 | 14 | pub deleted: u64, 15 | } 16 | 17 | /// Get database path. 18 | /// 19 | /// Will use env variable `DATABASE_URL` as default database path. 20 | /// 21 | /// If `DATABASE_URL` not exists: 22 | /// In debug mode, will create a new database at app home directory. 23 | /// In release mode, will create a new database at app project data directory. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// # Examples 28 | /// 29 | /// ``` 30 | /// use db::get_db_path; 31 | /// 32 | /// if let Ok(db_path) = get_db_path() { 33 | /// println!("db path should be: {:?}", db_path); 34 | /// } 35 | /// ``` 36 | pub fn get_db_path() -> Result { 37 | use std::env; 38 | use dotenvy::dotenv; 39 | use std::path::Path; 40 | use path_absolutize::*; 41 | use log::error; 42 | use crate::utils::explorer::create_all; 43 | #[cfg(debug_assertions)] 44 | use crate::utils::common::get_app_home_dir; 45 | #[cfg(not(debug_assertions))] 46 | use directories::ProjectDirs; 47 | 48 | dotenv().ok(); 49 | 50 | let mut database_url = env::var("DATABASE_URL").unwrap_or("".to_string()); 51 | if database_url.is_empty() { 52 | #[cfg(not(debug_assertions))] 53 | { 54 | let application = option_env!("CARGO_PKG_NAME").unwrap_or("mission-backup"); 55 | if let Some(proj_dirs) = ProjectDirs::from( 56 | "dev", 57 | "", 58 | application 59 | ) { 60 | database_url = proj_dirs.data_dir().join("database").join("mission_backup.db3").display().to_string(); 61 | } 62 | } 63 | 64 | #[cfg(debug_assertions)] 65 | if let Ok(app_dir) = get_app_home_dir() { 66 | database_url = app_dir.join("local_test.db3").display().to_string(); 67 | } 68 | } 69 | 70 | if !Path::new(&database_url).exists() { 71 | if let Err(error) = create_all(&database_url, "file") { 72 | error!("Failed to create database, errMsg: {error}"); 73 | } 74 | } 75 | 76 | if let Ok(abs_res) = Path::new(&database_url).absolutize() { 77 | if let Some(abs_path) = abs_res.to_str() { 78 | database_url = abs_path.to_string(); 79 | } 80 | } 81 | 82 | println!("database url: {}", database_url); 83 | 84 | Ok(database_url) 85 | } 86 | 87 | pub fn get_db_deleted_count(conn: &mut SqliteConnection) -> Result { 88 | use crate::db::schema::{ 89 | backup::dsl::*, 90 | ignore::dsl::*, 91 | mission::dsl::*, 92 | procedure::dsl::* 93 | }; 94 | 95 | let mut count: u64 = 0; 96 | count += backup.filter(super::schema::backup::is_deleted.eq(1)).count().get_result(conn).unwrap_or(0) as u64; 97 | count += ignore.filter(super::schema::ignore::is_deleted.eq(1)).count().get_result(conn).unwrap_or(0) as u64; 98 | count += mission.filter(super::schema::mission::is_deleted.eq(1)).count().get_result(conn).unwrap_or(0) as u64; 99 | count += procedure.filter(super::schema::procedure::is_deleted.eq(1)).count().get_result(conn).unwrap_or(0) as u64; 100 | 101 | Ok(count) 102 | } 103 | 104 | pub fn get_db_info(conn: &mut SqliteConnection) -> Result { 105 | let db_path = get_db_path()?; 106 | let db_deleted = get_db_deleted_count(conn)?; 107 | 108 | Ok(DBInfo { 109 | path: db_path, 110 | deleted: db_deleted, 111 | }) 112 | } 113 | 114 | pub fn clean_database_records(conn: &mut SqliteConnection) -> Result { 115 | use super::clean_db_record; 116 | use log::error; 117 | use std::io::{Error, ErrorKind}; 118 | 119 | let db_path = get_db_path()?; 120 | 121 | let tables = vec!["backup", "ignore", "mission", "procedure"]; 122 | let mut cleaned_cnt: usize = 0; 123 | for item in tables { 124 | match clean_db_record(item, conn) { 125 | Ok(cnt) => { 126 | cleaned_cnt += cnt; 127 | }, 128 | Err(error) => { 129 | error!("failed to clean table {:?} record, errMsg: {:?}", item, error); 130 | return Err(Error::from(ErrorKind::Other)); 131 | } 132 | } 133 | } 134 | 135 | Ok(DBInfo { 136 | path: db_path, 137 | deleted: cleaned_cnt as u64, 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /src/locales/langs/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "appName": "有备", 4 | "confirm": "确认", 5 | "cancel": "取消" 6 | }, 7 | "sidemenu": { 8 | "mission": "任务", 9 | "procedure": "程序", 10 | "backup": "备份", 11 | "statistic": "统计", 12 | "config": "设置", 13 | "toolbox": "工具" 14 | }, 15 | "status": { 16 | "error": "错误", 17 | "stopped": "停止", 18 | "running": "运行中", 19 | "backuping": "备份中", 20 | "default": "默认" 21 | }, 22 | "closeDialog": { 23 | "title": "退出选项", 24 | "remember": "记住选项" 25 | }, 26 | "screensaver": { 27 | "inputPwd": "请输入密码" 28 | }, 29 | "mission": { 30 | "name": "名称", 31 | "status": "状态", 32 | "operation": "操作", 33 | "newTitle": "新建任务", 34 | "editTitle": "编辑任务", 35 | "description": "描述", 36 | "pathType": "类型", 37 | "srcPath": "源目标", 38 | "dstPath": "保存至", 39 | "procedure": "程序", 40 | "select": "选择", 41 | "file": "文件", 42 | "directory": "目录" 43 | }, 44 | "procedure": { 45 | "newTitle": "新建程序", 46 | "editTitle": "编辑程序", 47 | "name": "名称", 48 | "hasIgnore": "启用忽略", 49 | "ignoreMethod": "忽略策略", 50 | "customIgnore": "自定义", 51 | "edit": "编辑", 52 | "gitIgnore": ".gitignore", 53 | "isCompress": "启用压缩", 54 | "compressFormat": "压缩格式", 55 | "zip": "zip", 56 | "targz": "tar.gz", 57 | "tarbz2": "tar.bz2", 58 | "tarxz": "tar.xz", 59 | "sevenz": "7z", 60 | "trigger": "触发方式", 61 | "cron": "定时", 62 | "monitor": "监控", 63 | "cronExpression": "Cron 表达式", 64 | "restrict": "限制备份", 65 | "restrictNone": "无", 66 | "restrictDays": "时间", 67 | "restrictSize": "大小", 68 | "restrictDaysAndSize": "时间和大小", 69 | "applied": "应用", 70 | "operation": "操作", 71 | "day": "天", 72 | "size": "字节" 73 | }, 74 | "ignore": { 75 | "keyword": "关键词", 76 | "operation": "操作", 77 | "createTitle": "新建忽略项", 78 | "editTitle": "编辑忽略项" 79 | }, 80 | "backup": { 81 | "date": "日期", 82 | "mission": "任务", 83 | "size": "大小", 84 | "operation": "操作" 85 | }, 86 | "statistic": { 87 | "selectMission": "选择任务", 88 | "startDate": "起始日期", 89 | "endDate": "截止日期", 90 | "dateSeperator": "到" 91 | }, 92 | "config": { 93 | "general": { 94 | "notify": "通知", 95 | "screensaver": "屏保", 96 | "watcher": "监控", 97 | "system": "系统" 98 | }, 99 | "notify": { 100 | "enable": "启用通知", 101 | "whenCreate": "当创建备份", 102 | "whenFailed": "当备份失败" 103 | }, 104 | "screensaver": { 105 | "enable": "启用屏保", 106 | "password": "锁屏密码", 107 | "edit": "修改", 108 | "title": "修改屏保密码", 109 | "oldPwd": "旧密码", 110 | "newPwd": "新密码" 111 | }, 112 | "watcher": { 113 | "title": "修改监控延时", 114 | "edit": "修改", 115 | "timeout": "监控延时", 116 | "relunch": "重启以生效" 117 | }, 118 | "system": { 119 | "theme": "主题", 120 | "themeLight": "明亮", 121 | "themeDark": "暗黑", 122 | "themeOption": "跟随系统", 123 | "autoStart": "开机启动", 124 | "closeOption": "关闭选项", 125 | "closeExit": "退出程序", 126 | "closeTray": "最小化至托盘", 127 | "language": "语言", 128 | "langCN": "中文", 129 | "langEN": "英文" 130 | } 131 | }, 132 | "toolbox": { 133 | "crontab": { 134 | "title": "Cron 编辑器", 135 | "next": "下次", 136 | "second": "秒", 137 | "minute": "分钟", 138 | "hour": "小时", 139 | "dayOfMonth": "日期", 140 | "month": "月份", 141 | "dayOfWeek": "周", 142 | "asterisk": "任意时刻", 143 | "comma": "分隔不同时间点", 144 | "hyphen": "一段时间区间", 145 | "slash": "每隔一段时间", 146 | "valueRange": "可设定的数值范围", 147 | "alterValues": "可替代英文简称" 148 | }, 149 | "database": { 150 | "title": "数据库工具", 151 | "check": "查看", 152 | "clean": "清理", 153 | "preCleanHint": "尝试清理数据库", 154 | "sufCleanHint": "条数据将被清理, 点击 '确认' 以继续", 155 | "cleanedHint": "条数据已被清理!" 156 | }, 157 | "log": { 158 | "title": "日志工具", 159 | "check": "查看", 160 | "clean": "清理", 161 | "preCleanHint": "尝试清理日志", 162 | "sufCleanHint": "字节日志数据将被清理, 点击 '确认' 以继续", 163 | "cleanedHint": "字节日志数据已被清理!" 164 | }, 165 | "migrate": { 166 | "title": "数据迁移", 167 | "selectFile": "选择数据文件", 168 | "select": "选择", 169 | "migrate": "迁移", 170 | "data": "数据", 171 | "count": "数量", 172 | "config": "设置", 173 | "missionRelated": "任务相关", 174 | "action": "操作" 175 | } 176 | }, 177 | "info": { 178 | "valueCopied": "复制成功" 179 | }, 180 | "warning": { 181 | "anotherInstance": "程序已在运行" 182 | }, 183 | "error": { 184 | "invalidCron": "无效的 cron 表达式!" 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/components/TitleBar.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 142 | 143 | 212 | -------------------------------------------------------------------------------- /src/locales/langs/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "appName": "Mission Backup", 4 | "confirm": "Confirm", 5 | "cancel": "Cancel" 6 | }, 7 | "sidemenu": { 8 | "mission": "Mission", 9 | "procedure": "Procedure", 10 | "backup": "Backup", 11 | "statistic": "Statistic", 12 | "config": "Config", 13 | "toolbox": "Toolbox" 14 | }, 15 | "status": { 16 | "error": "error", 17 | "stopped": "stopped", 18 | "running": "running", 19 | "backuping": "backuping", 20 | "default": "default" 21 | }, 22 | "closeDialog": { 23 | "title": "Close Option", 24 | "remember": "Remember Choice" 25 | }, 26 | "screensaver": { 27 | "inputPwd": "Input Password" 28 | }, 29 | "mission": { 30 | "name": "Name", 31 | "status": "Status", 32 | "operation": "Action", 33 | "newTitle": "New Mission", 34 | "editTitle": "Edit Mission", 35 | "description": "Description", 36 | "pathType": "Type", 37 | "srcPath": "From", 38 | "dstPath": "Save to", 39 | "procedure": "Procedure", 40 | "select": "Select", 41 | "file": "File", 42 | "directory": "Directory" 43 | }, 44 | "procedure": { 45 | "newTitle": "New Procedure", 46 | "editTitle": "Edit Procedure", 47 | "name": "Name", 48 | "hasIgnore": "Has Ignore", 49 | "ignoreMethod": "Ignore Method", 50 | "customIgnore": "Custom", 51 | "edit": "edit", 52 | "gitIgnore": ".gitignore", 53 | "isCompress": "Comperss", 54 | "compressFormat": "Format", 55 | "zip": "zip", 56 | "targz": "tar.gz", 57 | "tarbz2": "tar.bz2", 58 | "tarxz": "tar.xz", 59 | "sevenz": "7z", 60 | "trigger": "Trigger", 61 | "cron": "Cron", 62 | "monitor": "Monitor", 63 | "cronExpression": "Cron", 64 | "restrict": "Restrict", 65 | "restrictNone": "None", 66 | "restrictDays": "Days", 67 | "restrictSize": "Size", 68 | "restrictDaysAndSize": "Days and Size", 69 | "applied": "Applied", 70 | "operation": "Operation", 71 | "day": "Days", 72 | "size": "Bytes" 73 | }, 74 | "ignore": { 75 | "keyword": "Keyword", 76 | "operation": "Operation", 77 | "createTitle": "Create Ignores", 78 | "editTitle": "Edit Ignores" 79 | }, 80 | "backup": { 81 | "date": "Date", 82 | "mission": "Mission", 83 | "size": "Size", 84 | "operation": "Operation" 85 | }, 86 | "statistic": { 87 | "selectMission": "Select Mission", 88 | "startDate": "Start Date", 89 | "endDate": "End Date", 90 | "dateSeperator": "To" 91 | }, 92 | "config": { 93 | "general": { 94 | "notify": "Notify", 95 | "screensaver": "Screensaver", 96 | "watcher": "Watcher", 97 | "system": "System" 98 | }, 99 | "notify": { 100 | "enable": "Enable", 101 | "whenCreate": "When create", 102 | "whenFailed": "When failed" 103 | }, 104 | "screensaver": { 105 | "enable": "Enable", 106 | "password": "Password", 107 | "edit": "Edit", 108 | "title": "Edit password", 109 | "oldPwd": "Old password", 110 | "newPwd": "New password" 111 | }, 112 | "watcher": { 113 | "title": "Edit timeout", 114 | "edit": "Edit", 115 | "timeout": "Timeout", 116 | "relunch": "Relunch to effect" 117 | }, 118 | "system": { 119 | "theme": "Theme", 120 | "themeLight": "Light", 121 | "themeDark": "Dark", 122 | "themeOption": "Follow System", 123 | "autoStart": "Auto Start", 124 | "closeOption": "Close Option", 125 | "closeExit": "Exit", 126 | "closeTray": "Minimize", 127 | "language": "Language", 128 | "langCN": "Chinese", 129 | "langEN": "English" 130 | } 131 | }, 132 | "toolbox": { 133 | "crontab": { 134 | "title": "Crontab", 135 | "next": "Next", 136 | "second": "second", 137 | "minute": "minute", 138 | "hour": "hour", 139 | "dayOfMonth": "day", 140 | "month": "month", 141 | "dayOfWeek": "week", 142 | "asterisk": "any value", 143 | "comma": "value list separator", 144 | "hyphen": "range of values", 145 | "slash": "step values", 146 | "valueRange": "allowed values", 147 | "alterValues": "alternative single values" 148 | }, 149 | "database": { 150 | "title": "Database", 151 | "check": "Check", 152 | "clean": "Clean", 153 | "preCleanHint": "Try clean database", 154 | "sufCleanHint": "records will be cleaned, confirm to continue", 155 | "cleanedHint": "records have been cleaned!" 156 | }, 157 | "log": { 158 | "title": "Log", 159 | "check": "Check", 160 | "clean": "Clean", 161 | "preCleanHint": "Try clean log", 162 | "sufCleanHint": "bytes of log data will be cleaned, confirm to continue", 163 | "cleanedHint": "bytes log data have been cleaned!" 164 | }, 165 | "migrate": { 166 | "title": "Migrate", 167 | "selectFile": "Select data file", 168 | "select": "Select", 169 | "migrate": "Migrate", 170 | "data": "Data", 171 | "count": "Count", 172 | "config": "config", 173 | "missionRelated": "mission related", 174 | "action": "Action" 175 | } 176 | }, 177 | "info": { 178 | "valueCopied": "Value Copied" 179 | }, 180 | "warning": { 181 | "anotherInstance": "App is already running" 182 | }, 183 | "error": { 184 | "invalidCron": "Invalid cron expression!" 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src-tauri/src/utils/crypto.rs: -------------------------------------------------------------------------------- 1 | //! The `crypto` module contains functions about crypto. 2 | 3 | #[allow(dead_code)] 4 | /// Encodes str to base64 string. 5 | /// Takes care if the input data is url, see [URL-safe alphabet](https://docs.rs/base64/latest/base64/#url-safe-alphabet). 6 | /// 7 | /// # Arguments 8 | /// 9 | /// * `data` - A string that holds the text data 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// use crypto::encoding_base64; 15 | /// 16 | /// assert_eq!(encode_base64("Hello world!").expect("Error!"), "SGVsbG8gd29ybGQh"); 17 | /// ``` 18 | pub fn encode_base64(data: &str) -> Result { 19 | use base64::{engine::general_purpose::STANDARD, engine::general_purpose::URL_SAFE,Engine as _}; 20 | use url::Url; 21 | 22 | match Url::parse(data) { 23 | Ok(_) => Ok(STANDARD.encode(data)), 24 | Err(_) => Ok(URL_SAFE.encode(data)), 25 | } 26 | } 27 | 28 | #[allow(dead_code)] 29 | /// Decodes base64 data to normal string. 30 | /// Takes care if the origin data is url, see [URL-safe alphabet](https://docs.rs/base64/latest/base64/#url-safe-alphabet). 31 | /// 32 | /// # Arguments 33 | /// 34 | /// * `data` - A string that holds the encoded data. 35 | /// * `is_url` - Option if the origin data is url. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ``` 40 | /// use crypto::decode_base64; 41 | /// 42 | /// assert_eq!(decode_base64("SGVsbG8gd29ybGQh", None).expect("Error!"), "Hello world!"); 43 | /// ``` 44 | pub fn decode_base64(data: &str, is_url: Option) -> Result { 45 | use base64::{engine::general_purpose::STANDARD, engine::general_purpose::URL_SAFE,Engine as _, DecodeError}; 46 | 47 | if let Some(true) = is_url { 48 | match URL_SAFE.decode(data) { 49 | Ok(arr) => { 50 | if let Ok(res) = String::from_utf8(arr) { 51 | return Ok(res); 52 | } else { 53 | return Err(DecodeError::from(DecodeError::InvalidPadding)); 54 | } 55 | }, 56 | Err(error) => { 57 | // println!("decode error, errMsg: {:?}", error); 58 | return Err(error); 59 | } 60 | } 61 | } else { 62 | match STANDARD.decode(data) { 63 | Ok(arr) => { 64 | if let Ok(res) = String::from_utf8(arr) { 65 | return Ok(res); 66 | } else { 67 | return Err(DecodeError::from(DecodeError::InvalidPadding)); 68 | } 69 | }, 70 | Err(error) => { 71 | // println!("decode error, errMsg: {:?}", error); 72 | return Err(error); 73 | } 74 | } 75 | } 76 | } 77 | 78 | #[allow(dead_code)] 79 | /// Encodes file to Sha256 string. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `path` - A string that holds the file path. 84 | /// 85 | /// # Examples 86 | /// 87 | /// ``` 88 | /// use crypto::encode_sha2_file; 89 | /// 90 | /// let path = "path\\to\\encoded"; 91 | /// 92 | /// match encode_sha2_file(path) { 93 | /// Ok(data) => { 94 | /// println!("File {} encoded data: {}", path, data); 95 | /// }, 96 | /// Err(error) => { 97 | /// println!("Failed to encode file {}, errMsg: {:?}", path, error); 98 | /// } 99 | /// } 100 | /// ``` 101 | pub fn encode_sha2_file(path: &str) -> Result { 102 | use sha2::{Sha256, Digest}; 103 | use std::io::{Error, ErrorKind}; 104 | use std::fs::read; 105 | use std::path::Path; 106 | 107 | let input = Path::new(path); 108 | if !input.exists() { 109 | return Err(Error::from(ErrorKind::NotFound)); 110 | } 111 | 112 | let mut hasher = Sha256::new(); 113 | 114 | match read(path) { 115 | Ok(data) => { 116 | hasher.update(data); 117 | return Ok(format!("{:X}", hasher.finalize())); 118 | }, 119 | Err(_error) => { 120 | // println!("encode sha2 file for {} failed, errMsg: {:?}", path, _error); 121 | return Err(Error::from(ErrorKind::Other)); 122 | } 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod test { 128 | use super::*; 129 | 130 | #[test] 131 | fn test_encode_base64() { 132 | let normal_text = encode_base64("Hello world!").expect(""); 133 | assert_eq!(normal_text, "SGVsbG8gd29ybGQh".to_string()); 134 | } 135 | 136 | #[test] 137 | fn test_decode_base64() { 138 | let normal_text = decode_base64("SGVsbG8gd29ybGQh", None).expect(""); 139 | assert_eq!(normal_text, "Hello world!".to_string()); 140 | } 141 | 142 | #[test] 143 | fn test_encode_sha2_file() { 144 | use std::env::current_dir; 145 | use std::fs::{OpenOptions, remove_file}; 146 | use std::io::Write; 147 | 148 | if let Ok(path) = current_dir() { 149 | let file_path = path.join("test_file.txt").display().to_string(); 150 | let mut test_file = OpenOptions::new().write(true).create_new(true).open(file_path.clone().as_str()).unwrap(); 151 | test_file.write_all("Hello world!".as_bytes()).unwrap(); 152 | 153 | let encoded_data = encode_sha2_file(file_path.as_str()).expect(""); 154 | assert_eq!(encoded_data, "C0535E4BE2B79FFD93291305436BF889314E4A3FAEC05ECFFCBB7DF31AD9E51A".to_string()); 155 | 156 | remove_file(file_path).expect(""); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src-tauri/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Config 2 | //! 3 | //! `config` module contains all configuration about app. 4 | 5 | pub mod notify; 6 | pub mod screensaver; 7 | pub mod system; 8 | pub mod watcher; 9 | 10 | use serde::{Serialize, Deserialize}; 11 | use notify::NotifyConfig; 12 | use screensaver::ScreensaverConfig; 13 | use system::SystemConfig; 14 | use watcher::WatcherConfig; 15 | 16 | /// Configuration for app 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct AppConfig { 19 | /// App system config, like `theme`, `language`... 20 | pub system: SystemConfig, 21 | 22 | /// App notify config, like `enable_notify`... 23 | pub notify: NotifyConfig, 24 | 25 | /// App watcher Config, like `timeout`... 26 | pub watcher: WatcherConfig, 27 | 28 | /// App screensaver config, like `enable`... 29 | pub screensaver: ScreensaverConfig, 30 | } 31 | 32 | impl Default for AppConfig { 33 | fn default() -> Self { 34 | AppConfig { 35 | system: system::SystemConfig::default(), 36 | notify: notify::NotifyConfig::default(), 37 | watcher: watcher::WatcherConfig::default(), 38 | screensaver: screensaver::ScreensaverConfig::default(), 39 | } 40 | } 41 | } 42 | 43 | /// Get config file path in system. 44 | /// This will not create config file, but will create the parent directory if not exists. 45 | /// 46 | /// # Arguments 47 | /// 48 | /// # Examples 49 | /// 50 | /// ``` 51 | /// use config::get_config_file_path; 52 | /// 53 | /// match get_config_file_path() { 54 | /// Ok(path) => { 55 | /// println!("the config file path is: {}", path); 56 | /// }, 57 | /// Err(error) => { 58 | /// println!("failed to get config file path, errMsg: {:?}", error); 59 | /// } 60 | /// } 61 | /// ``` 62 | pub fn get_config_file_path() -> Result { 63 | use directories::ProjectDirs; 64 | use std::env::current_dir; 65 | use std::path::PathBuf; 66 | use std::fs::create_dir_all; 67 | 68 | 69 | // Where to get/how to generate config instance (to use with tauri::api::path::app_data_dir)? 70 | // https://github.com/tauri-apps/tauri/discussions/6583 71 | 72 | let application = option_env!("CARGO_PKG_NAME").unwrap_or("mission-backup"); 73 | let mut config_dir: PathBuf = current_dir()?; 74 | if let Some(proj_dirs) = ProjectDirs::from( 75 | "dev", 76 | "", 77 | application 78 | ) { 79 | config_dir = proj_dirs.config_dir().to_path_buf(); 80 | create_dir_all(config_dir.clone())?; 81 | } 82 | 83 | let file_path = config_dir.join(format!("{}.toml", application)); 84 | return Ok(file_path.display().to_string()); 85 | } 86 | 87 | /// Load app configuration. 88 | /// If config file not exists or file incorrect, will return default app config. 89 | /// 90 | /// # Arguments 91 | /// 92 | /// # Examples 93 | /// 94 | /// ``` 95 | /// use config::load_app_config; 96 | /// 97 | /// match load_app_config() { 98 | /// Ok(config) => { 99 | /// println!("cur config: {:?}", config); 100 | /// }, 101 | /// Err(error) => { 102 | /// println!("failed to load config, errMsg: {:?}", error); 103 | /// } 104 | /// } 105 | /// ``` 106 | pub fn load_app_config() -> Result { 107 | use std::path::Path; 108 | use std::fs::read_to_string; 109 | use std::io::{Error, ErrorKind}; 110 | 111 | if let Ok(path) = get_config_file_path() { 112 | if Path::new(&path).exists() { 113 | let stored_config = read_to_string(path)?; 114 | match toml::from_str(stored_config.as_str()) { 115 | Ok(config) => { 116 | return Ok(config); 117 | }, 118 | Err(_error) => { 119 | return Err(Error::from(ErrorKind::InvalidData)); 120 | } 121 | } 122 | } 123 | } 124 | 125 | return Err(Error::from(ErrorKind::NotFound)); 126 | } 127 | 128 | /// Save app configuration to config file. 129 | /// 130 | /// # Arguments 131 | /// 132 | /// * `config` - A configuration for app. 133 | /// 134 | /// # Examples 135 | /// 136 | /// ``` 137 | /// use config::{AppConfig, load_app_config}; 138 | /// 139 | /// let mut config: AppConfig = AppConfig::Default(); 140 | /// config.watcher.timeout = 6; 141 | /// 142 | /// if let Ok(()) = save_app_config(&config) { 143 | /// println!("save app config success"); 144 | /// let cur_config = load_app_config()?; 145 | /// println!("cur config is: {:?}", cur_config); 146 | /// } 147 | /// ``` 148 | pub fn save_app_config(config: &AppConfig) -> Result<(), std::io::Error> { 149 | use std::io::Write; 150 | use std::fs::File; 151 | 152 | if let Ok(path) = get_config_file_path() { 153 | let mut toml_file = File::options().write(true).create(true).truncate(true).open(path)?; 154 | if let Ok(toml) = toml::to_string(config) { 155 | toml_file.write_all(toml.as_bytes())?; 156 | } 157 | } 158 | 159 | Ok(()) 160 | } 161 | 162 | #[cfg(test)] 163 | mod test { 164 | use super::*; 165 | 166 | #[test] 167 | fn test_app_config() { 168 | if let Ok(config) = load_app_config() { 169 | let config_file_path = get_config_file_path().unwrap(); 170 | 171 | save_app_config(&config).unwrap(); 172 | 173 | assert_eq!(std::path::Path::new(&config_file_path).exists(), true); 174 | 175 | std::fs::remove_file(config_file_path).unwrap(); 176 | } else { 177 | panic!("failed to load app config"); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/views/Backup/Backup.vue: -------------------------------------------------------------------------------- 1 | 141 | 142 | 170 | 171 | 187 | -------------------------------------------------------------------------------- /src/views/Statistic/Statistic.vue: -------------------------------------------------------------------------------- 1 | 158 | 159 | 197 | 198 | 216 | -------------------------------------------------------------------------------- /src/views/Procedure/Procedure.vue: -------------------------------------------------------------------------------- 1 | 150 | 151 | 192 | 193 | 217 | -------------------------------------------------------------------------------- /src/views/Mission/Mission.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | 206 | 207 | 223 | -------------------------------------------------------------------------------- /src/store/mission/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import dayjs from 'dayjs' 4 | import { Command, execute } from '../../utils/cmd' 5 | import type { Backup, Ignore, Mission, Procedure, Record } from './types' 6 | 7 | export const useMissionStore = defineStore('mission', () => { 8 | const ignores = ref([]) 9 | const procedures = ref([]) 10 | const missions = ref([]) 11 | const backups = ref([]) 12 | 13 | /** 14 | * Get the list of ignores. 15 | * @returns The list of ignores. 16 | */ 17 | function getIgnores(): Ignore[] { 18 | return ignores.value 19 | } 20 | 21 | /** 22 | * Set the list of ignores. 23 | * @param data - The list of ignores to set. 24 | */ 25 | function setIgnores(data: Ignore[]) { 26 | ignores.value = data 27 | } 28 | 29 | /** 30 | * Create a record. 31 | * @param table - The table name. 32 | * @param data - The record data. 33 | * @returns The created record or false. 34 | */ 35 | async function createRecord(table: string, data: Record): Promise { 36 | const res = await execute(Command.CreateRecord, table, data) 37 | return res 38 | } 39 | 40 | /** 41 | * Update a record in the specified table. 42 | * @param table - The table name. 43 | * @param data - The record data to update. 44 | * @returns The updated record or false. 45 | */ 46 | async function updateRecord(table: string, data: Record): Promise { 47 | const res = await execute(Command.UpdateRecord, table, data) 48 | return res 49 | } 50 | 51 | /** 52 | * Delete a record from the specified table. 53 | * @param table - The table name. 54 | * @param uuid_0 - The first UUID for table record. 55 | * @param uuid_1 - The second UUID optional for table `ignore` and `backup`. 56 | * once set, `procedure` related ignores or `mission` related backups will be all deleted. 57 | * @returns True if the record is deleted successfully, otherwise the error data. 58 | */ 59 | async function deleteRecord(table: string, uuid_0: string | undefined, uuid_1: string | undefined): Promise { 60 | const res = await execute(Command.DeleteRecord, table, uuid_0, uuid_1) 61 | return res 62 | } 63 | 64 | /** 65 | * Synchronize records for the specified table. 66 | * @param table - The table name. 67 | * @returns An array of records. 68 | */ 69 | async function syncRecords(table: string): Promise { 70 | const records: Record[] = await execute(Command.QueryRecord, table) 71 | switch (table) { 72 | case 'ignore': 73 | ignores.value = records.map(r => r.ignore) 74 | break 75 | 76 | case 'procedure': 77 | procedures.value = records.map(r => r.procedure) 78 | break 79 | 80 | case 'mission': 81 | missions.value = records.map(r => r.mission) 82 | break 83 | 84 | case 'backup': 85 | backups.value = records.map(r => r.backup) 86 | break 87 | 88 | default: { 89 | throw new Error(`No match table for syncRecords`) 90 | } 91 | } 92 | 93 | return records 94 | } 95 | 96 | /** 97 | * Set the status of a mission by UUID. 98 | * @param uuid - The UUID of the mission. 99 | * @param status - The status to set. 100 | * @returns True if the status is set successfully, otherwise false. 101 | */ 102 | async function setMissionStatus(uuid: string, status: number): Promise { 103 | const res = await execute(Command.SetMissionStatus, uuid, status) 104 | return res 105 | } 106 | 107 | /** 108 | * Create a new mission. 109 | * @param mission - The mission data to create. 110 | * @returns True if the mission is created successfully, otherwise false. 111 | */ 112 | async function createMission(mission: Mission): Promise { 113 | const res = await execute(Command.CreateMission, mission) 114 | return res 115 | } 116 | 117 | /** 118 | * Delete a mission by UUID. 119 | * @param uuid - The UUID of the mission to delete. 120 | * @returns True if the mission is deleted successfully, otherwise false. 121 | */ 122 | async function deleteMission(uuid: string): Promise { 123 | const res = await execute(Command.DeleteMission, uuid) 124 | return res 125 | } 126 | 127 | return { 128 | ignores, 129 | procedures, 130 | missions, 131 | backups, 132 | getIgnores, 133 | setIgnores, 134 | createRecord, 135 | updateRecord, 136 | deleteRecord, 137 | syncRecords, 138 | setMissionStatus, 139 | createMission, 140 | deleteMission, 141 | } 142 | }) 143 | 144 | /** 145 | * Get the default ignore object. 146 | * @returns The default Ignore object. 147 | */ 148 | function defaultIgnore(): Ignore { 149 | return { 150 | id: 0, 151 | ignoreId: '', 152 | procedureId: '', 153 | keyword: '', 154 | reserved0: '', 155 | reserved1: '', 156 | reserved2: '', 157 | createAt: dayjs.utc().format().slice(0, -1), 158 | updateAt: dayjs.utc().format().slice(0, -1), 159 | isDeleted: 0, 160 | deleteAt: dayjs.utc().format().slice(0, -1), 161 | } 162 | } 163 | 164 | /** 165 | * Get the default procedure object. 166 | * @returns The default Procedure object. 167 | */ 168 | function defaultProcedure(): Procedure { 169 | return { 170 | id: 0, 171 | procedureId: '', 172 | name: '', 173 | hasIgnores: false, 174 | ignoreMethod: 1, 175 | isCompress: false, 176 | compressFormat: 1, 177 | trigger: 2, 178 | cronExpression: '', 179 | restrict: 0, 180 | restrictDays: 3, 181 | restrictSize: 1024, 182 | reserved0: '', 183 | reserved1: '', 184 | reserved2: '', 185 | createAt: dayjs.utc().format().slice(0, -1), 186 | updateAt: dayjs.utc().format().slice(0, -1), 187 | isDeleted: 0, 188 | deleteAt: dayjs.utc().format().slice(0, -1), 189 | } 190 | } 191 | 192 | /** 193 | * Get the default mission object. 194 | * @returns The default Mission object. 195 | */ 196 | function defaultMission(): Mission { 197 | return { 198 | id: 0, 199 | missionId: '', 200 | procedureId: '', 201 | name: '', 202 | status: 0, 203 | description: '', 204 | pathType: 1, 205 | srcPath: '', 206 | dstPath: '', 207 | nextRuntime: dayjs.utc().format().slice(0, -1), 208 | lastTrigger: dayjs.utc().format().slice(0, -1), 209 | reserved0: '', 210 | reserved1: '', 211 | reserved2: '', 212 | createAt: dayjs.utc().format().slice(0, -1), 213 | updateAt: dayjs.utc().format().slice(0, -1), 214 | isDeleted: 0, 215 | deleteAt: dayjs.utc().format().slice(0, -1), 216 | } 217 | } 218 | 219 | /** 220 | * Get the default backup object. 221 | * @returns The default Backup object. 222 | */ 223 | function defaultBackup(): Backup { 224 | return { 225 | id: 0, 226 | backupId: '', 227 | missionId: '', 228 | savePath: '', 229 | backupSize: 0, 230 | reserved0: '', 231 | reserved1: '', 232 | reserved2: '', 233 | createAt: dayjs.utc().format().slice(0, -1), 234 | updateAt: dayjs.utc().format().slice(0, -1), 235 | isDeleted: 0, 236 | deleteAt: dayjs.utc().format().slice(0, -1), 237 | } 238 | } 239 | 240 | /** 241 | * Get the default record object. 242 | * @returns The default Record object. 243 | */ 244 | function defaultRecord(): Record { 245 | return { 246 | ignore: defaultIgnore(), 247 | procedure: defaultProcedure(), 248 | mission: defaultMission(), 249 | backup: defaultBackup(), 250 | } 251 | } 252 | 253 | export { 254 | defaultBackup, 255 | defaultIgnore, 256 | defaultMission, 257 | defaultProcedure, 258 | defaultRecord, 259 | } 260 | -------------------------------------------------------------------------------- /src/store/system/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import { useI18n } from 'vue-i18n' 4 | import { useDark, useToggle } from '@vueuse/core' 5 | import { disable, enable } from 'tauri-plugin-autostart-api' 6 | import { appWindow } from '@tauri-apps/api/window' 7 | import { Command, execute } from '../../utils/cmd' 8 | import type { AppConfig } from '../types' 9 | import { CloseOption } from './types' 10 | import type { SystemConfig } from './types' 11 | 12 | export const useSystemStore = defineStore('system', () => { 13 | const { locale } = useI18n({ useScope: 'global' }) 14 | 15 | const theme = ref('light') 16 | const sysTheme = ref('light') 17 | const themeOption = ref(0) 18 | const autoStart = ref(false) 19 | const closeOption = ref(0) 20 | const closeCnt = ref(0) 21 | const closeLimit = ref(50) 22 | const language = ref('zh-CN') 23 | 24 | /** 25 | * Gets the current system configuration. 26 | * @returns The current system configuration. 27 | */ 28 | function getConfig(): SystemConfig { 29 | return { 30 | theme: theme.value, 31 | sysTheme: sysTheme.value, 32 | themeOption: themeOption.value, 33 | autoStart: autoStart.value, 34 | closeOption: closeOption.value, 35 | closeCnt: closeCnt.value, 36 | closeLimit: closeLimit.value, 37 | language: language.value, 38 | } 39 | } 40 | 41 | /** 42 | * Sets the system configuration. 43 | * @param config - The system configuration to set. 44 | */ 45 | function setConfig(config: SystemConfig): void { 46 | theme.value = config.theme 47 | sysTheme.value = config.sysTheme 48 | themeOption.value = config.themeOption 49 | autoStart.value = config.autoStart 50 | closeOption.value = config.closeOption 51 | closeCnt.value = config.closeCnt 52 | closeLimit.value = config.closeLimit 53 | language.value = config.language 54 | 55 | // udpate theme 56 | const isDark = useDark() 57 | if (config.themeOption) 58 | isDark.value = config.sysTheme !== 'light' 59 | else 60 | isDark.value = config.theme !== 'light' 61 | useToggle(isDark) 62 | 63 | // update lang 64 | locale.value = config.language 65 | } 66 | 67 | /** 68 | * Initializes the system with the provided data or fetches the configuration if data is undefined. 69 | * @param data - The optional system configuration data. 70 | */ 71 | async function init(data: SystemConfig | undefined): Promise { 72 | if (data === undefined) { 73 | await execute(Command.InitConfig, 'system') 74 | .then((config: AppConfig) => { 75 | setConfig(config.system) 76 | }) 77 | } 78 | else { 79 | setConfig(data) 80 | } 81 | } 82 | 83 | /** 84 | * Shuts down the system. 85 | */ 86 | async function shutdown(): Promise { 87 | await execute(Command.ShutdownApp) 88 | } 89 | 90 | /** 91 | * Updates the theme of the app. 92 | * @param theme - The new theme for the app. 93 | */ 94 | async function updateTheme(theme: string): Promise { 95 | const config = getConfig() 96 | config.theme = theme 97 | await execute(Command.UpdateConfig, 'system', config) 98 | .then((config: AppConfig) => { 99 | setConfig(config.system) 100 | const isDark = useDark() 101 | isDark.value = config.system.theme !== 'light' 102 | useToggle(isDark) 103 | }) 104 | } 105 | 106 | /** 107 | * Updates the theme of the system in config. 108 | * @param theme - The new theme for the system. 109 | */ 110 | async function updateSysTheme(theme: string): Promise { 111 | const config = getConfig() 112 | config.sysTheme = theme 113 | await execute(Command.UpdateConfig, 'system', config) 114 | .then((config: AppConfig) => { 115 | setConfig(config.system) 116 | 117 | if (config.system.themeOption) { 118 | const isDark = useDark() 119 | isDark.value = config.system.sysTheme !== 'light' 120 | useToggle(isDark) 121 | } 122 | }) 123 | } 124 | 125 | /** 126 | * Updates the theme option of the app. 127 | * @param option - The new theme option for the app. 128 | */ 129 | async function updateThemeOption(option: number): Promise { 130 | const config = getConfig() 131 | config.themeOption = option 132 | await execute(Command.UpdateConfig, 'system', config) 133 | .then((config: AppConfig) => { 134 | setConfig(config.system) 135 | }) 136 | } 137 | 138 | /** 139 | * Updates the language of the system. 140 | * @param lang - The new language for the system. 141 | */ 142 | async function updateLanguage(lang: string): Promise { 143 | const config = getConfig() 144 | config.language = lang 145 | await execute(Command.UpdateConfig, 'system', config) 146 | .then((config: AppConfig) => { 147 | setConfig(config.system) 148 | locale.value = config.system.language 149 | }) 150 | } 151 | 152 | /** 153 | * Updates the auto-start setting of the system. 154 | * @param start - The new auto-start setting for the system. 155 | */ 156 | async function updateAutoStart(start: boolean): Promise { 157 | const config = getConfig() 158 | config.autoStart = start 159 | await execute(Command.UpdateConfig, 'system', config) 160 | .then(async (config: AppConfig) => { 161 | setConfig(config.system) 162 | if (config.system.autoStart) 163 | await enable() 164 | else 165 | await disable() 166 | }) 167 | } 168 | 169 | /** 170 | * Updates the close option of the system. 171 | * @param option - The new close option for the system. 172 | */ 173 | async function updateCloseOption(option: number): Promise { 174 | const config = getConfig() 175 | config.closeOption = option 176 | await execute(Command.UpdateConfig, 'system', config) 177 | .then(async (config: AppConfig) => { 178 | setConfig(config.system) 179 | }) 180 | } 181 | 182 | /** 183 | * Checks if a close confirmation is needed. 184 | * @returns A boolean indicating whether a close confirmation is needed. 185 | */ 186 | function closeConfirm(): boolean { 187 | return closeCnt.value + 1 >= closeLimit.value 188 | } 189 | 190 | /** 191 | * Tries to close the system with the specified option and remember setting. 192 | * @param option - The close option to use. 193 | * @param remember - Whether to remember the close option. 194 | */ 195 | async function tryClose(option: number | undefined, remember: boolean | undefined): Promise { 196 | const config = getConfig() 197 | config.closeCnt++ 198 | 199 | if (option === undefined || remember === undefined) { 200 | option = config.closeOption 201 | remember = false 202 | } 203 | 204 | if (remember) { 205 | config.closeOption = option 206 | config.closeCnt = 0 207 | } 208 | await execute(Command.UpdateConfig, 'system', config) 209 | .then((config: AppConfig) => { 210 | setConfig(config.system) 211 | }) 212 | 213 | if (option === 0) 214 | await shutdown() 215 | else 216 | await appWindow.hide() 217 | } 218 | 219 | return { 220 | theme, 221 | themeOption, 222 | autoStart, 223 | closeOption, 224 | language, 225 | init, 226 | closeConfirm, 227 | tryClose, 228 | updateTheme, 229 | updateSysTheme, 230 | updateThemeOption, 231 | updateLanguage, 232 | updateAutoStart, 233 | updateCloseOption, 234 | } 235 | }) 236 | 237 | /** 238 | * Generates the default system configuration. 239 | * @returns The default system configuration. 240 | */ 241 | export function defaultSystemConfig(): SystemConfig { 242 | return { 243 | theme: 'light', 244 | sysTheme: 'light', 245 | themeOption: 0, 246 | autoStart: false, 247 | closeOption: CloseOption.Exit, 248 | closeCnt: 0, 249 | closeLimit: 50, 250 | language: 'zh-CN', 251 | } 252 | } 253 | --------------------------------------------------------------------------------