From c7311010ab45f3ecdf18c3a780d9f24a4171792b Mon Sep 17 00:00:00 2001 From: 1111mp Date: Tue, 10 Sep 2024 19:43:58 +0800 Subject: [PATCH] chore: configration & rebuild system tray Signed-off-by: 1111mp --- .github/workflows/pre-release.yml | 360 +++++++++++ @types/index.d.ts | 7 +- package.json | 6 +- src-tauri/src/cmds.rs | 36 +- src-tauri/src/config/settings.rs | 7 +- src-tauri/src/core/configration.rs | 156 +++++ src-tauri/src/core/group.rs | 10 +- src-tauri/src/core/handle.rs | 14 +- src-tauri/src/core/mod.rs | 1 + src-tauri/src/core/node.rs | 18 +- src-tauri/src/core/project.rs | 7 +- src-tauri/src/core/tray.rs | 12 +- src-tauri/src/main.rs | 3 + src/app-context.tsx | 2 +- src/components/ui/data-table.tsx | 1 - src/locales/en.json | 259 ++++---- src/locales/zh_CN.json | 1 + src/main.tsx | 18 +- src/pages/groups/index.tsx | 2 +- src/pages/home/configration.tsx | 579 +++++++++--------- src/pages/home/updater.tsx | 469 +++++++------- src/pages/installed/index.tsx | 2 +- src/pages/projects/index.tsx | 2 +- src/pages/versions/index.tsx | 939 ++++++++++++++--------------- src/services/cmds.ts | 55 +- 25 files changed, 1784 insertions(+), 1182 deletions(-) create mode 100644 .github/workflows/pre-release.yml create mode 100644 src-tauri/src/core/configration.rs diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000..ee30111 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,360 @@ +name: Alpha Build + +on: workflow_dispatch +permissions: write-all + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +jobs: + alpha: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + - os: windows-latest + target: aarch64-pc-windows-msvc + - os: macos-latest + target: aarch64-apple-darwin + - os: macos-latest + target: x86_64-apple-darwin + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Rust Stable + uses: dtolnay/rust-toolchain@stable + + - name: Add Rust Target + run: rustup target add ${{ matrix.target }} + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + cache-all-crates: true + + - name: Install dependencies (ubuntu only) + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Pnpm install and check + run: | + pnpm i + pnpm check ${{ matrix.target }} + + - name: Tauri Build + uses: tauri-apps/tauri-action@v0 + env: + NODE_OPTIONS: '--max_old_space_size=4096' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tagName: v__VERSION__-alpha + releaseName: 'NVM Desktop v__VERSION__-alpha' + releaseBody: 'More new features are now supported.' + releaseDraft: false + prerelease: true + tauriScript: pnpm + args: --target ${{ matrix.target }} + + alpha-for-linux-arm64: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Rust Stable + uses: dtolnay/rust-toolchain@stable + + - name: Add Rust Target + run: rustup target add ${{ matrix.target }} + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + cache-all-crates: true + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Pnpm install and check + run: | + pnpm i + pnpm check ${{ matrix.target }} + + - name: 'Setup for linux' + run: |- + sudo ls -lR /etc/apt/ + echo ------------- + # sudo sed 's/mirror+file:\/etc\/apt\/apt-mirrors.txt/[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | sudo tee /etc/apt/sources.list.d/ports.list + # echo ------------- + # sudo sed -i 's/mirror+file:\/etc\/apt\/apt-mirrors.txt/[arch=amd64,i386] http:\/\/archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list + cat > /tmp/sources.list << EOF + deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted + deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted + deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted + deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-backports main multiverse universe restricted + + deb [arch-=amd64,i386] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted + deb [arch-=amd64,i386] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted + deb [arch-=amd64,i386] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted + deb [arch-=amd64,i386] http://ports.ubuntu.com/ubuntu-ports jammy-backports main multiverse universe restricted + EOF + sudo mv /etc/apt/sources.list /etc/apt/sources.list.default + sudo mv /tmp/sources.list /etc/apt/sources.list + + echo ------------- + echo /etc/apt/sources.list && cat /etc/apt/sources.list + echo ------------- + echo /etc/apt/apt-mirrors.txt && cat /etc/apt/apt-mirrors.txt + echo ------------- + echo /etc/apt/sources.list.d/ports.list && cat /etc/apt/sources.list.d/ports.list || true + echo ------------- + + sudo dpkg --add-architecture arm64 + sudo apt update + + sudo apt install -y \ + gcc-multilib \ + g++-multilib + + echo ------------- + echo install arm64 dependences ... + + sudo apt install -y \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + libgtk-3-dev \ + patchelf:arm64 \ + libwebkit2gtk-4.1-dev:arm64 \ + libappindicator3-dev:arm64 \ + libssl-dev:arm64 \ + libssl3:arm64 \ + libgtk-3-dev:arm64 \ + librsvg2-dev:arm64 + + echo 'ok' + + - name: Build for Linux + run: | + echo "build native binarys..." + + export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH + export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/ + export PKG_CONFIG_ALLOW_CROSS=1 + pnpm build --target aarch64-unknown-linux-gnu + + echo "build native binarys finished" + env: + NODE_OPTIONS: '--max_old_space_size=4096' + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + + - name: Get Version + run: | + sudo apt-get update + sudo apt-get install jq + echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{env.VERSION}}-alpha + name: 'NVM Desktop v${{env.VERSION}}-alpha' + body: 'More new features are now supported.' + draft: false + prerelease: true + token: ${{ secrets.GITHUB_TOKEN }} + files: | + src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb + src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm + + alpha-for-fixed-webview2: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + arch: x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + arch: arm64 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Add Rust Target + run: rustup target add ${{ matrix.target }} + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - name: Pnpm install and check + run: | + pnpm i + pnpm check ${{ matrix.target }} + + - name: Download WebView2 Runtime + run: | + invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/127.0.2651.105/Microsoft.WebView2.FixedVersionRuntime.127.0.2651.105.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.127.0.2651.105.${{ matrix.arch }}.cab + Expand .\Microsoft.WebView2.FixedVersionRuntime.127.0.2651.105.${{ matrix.arch }}.cab -F:* ./src-tauri + Remove-Item .\src-tauri\tauri.windows.conf.json + Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json + + - name: Tauri build + id: build + uses: tauri-apps/tauri-action@v0 + env: + NODE_OPTIONS: '--max_old_space_size=4096' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tauriScript: pnpm + args: --target ${{ matrix.target }} + + - name: Rename + run: | + Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\NVM Desktop_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'NVM Desktop_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe' + Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\NVM Desktop_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe.sig' 'NVM Desktop_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe.sig' + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{steps.build.outputs.appVersion}}-alpha + name: 'NVM Desktop v${{steps.build.outputs.appVersion}}-alpha' + body: 'More new features are now supported.' + draft: false + prerelease: true + token: ${{ secrets.GITHUB_TOKEN }} + files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* + + update_alpha_notes: + name: Update alpha notes + runs-on: ubuntu-latest + needs: [alpha, alpha-for-linux-arm64, alpha-for-fixed-webview2] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set Env + run: | + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + shell: bash + + - name: Get Version + run: | + sudo apt-get update + sudo apt-get install jq + echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV + + - name: Update Tag + uses: richardsimko/update-tag@v1 + with: + tag_name: v${{ env.VERSION }}-alpha + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - run: | + cat > release.txt << 'EOF' + ## Which version should I download? + + ### MacOS (Prompts that the file is damaged or the developer cannot verify it. Please check [MacOS issues](https://github.com/1111mp/nvm-desktop?tab=readme-ov-file#macos-issues)) + - MacOS Intel chip: x64.dmg + - MacOS apple M chip: aarch64.dmg + + ### Linux + - Linux 64-bit: amd64.deb/amd64.rpm + - Linux arm64 architecture: arm64.deb/aarch64.rpm + + ### Windows (Win7 users please make sure to install and enable webview2) + #### Normal version (recommended) + - 64-bit: x64-setup.exe + - arm64 architecture: arm64-setup.exe + #### Built-in Webview2 version (large in size, only used when the enterprise version system or Win7 cannot install webview2) + - 64-bit: x64_fixed_webview2-setup.exe + - arm64 architecture: arm64_fixed_webview2-setup.exe + + --- + + ## 我应该下载哪个版本? + + ### MacOS (提示文件损坏或开发者无法验证请查看 [MacOS issues](https://github.com/1111mp/nvm-desktop?tab=readme-ov-file#macos-issues)) + - MacOS intel芯片: x64.dmg + - MacOS apple M芯片: aarch64.dmg + + ### Linux + - Linux 64位: amd64.deb/amd64.rpm + - Linux arm64架构: arm64.deb/aarch64.rpm + + ### Windows (Win7 用户请确保安装并启用webview2) + #### 正常版本(推荐) + - 64位: x64-setup.exe + - arm64架构: arm64-setup.exe + #### 内置Webview2版(体积较大,仅在企业版系统或Win7无法安装webview2时使用) + - 64位: x64_fixed_webview2-setup.exe + - arm64架构: arm64_fixed_webview2-setup.exe + + Created at ${{ env.BUILDTIME }}. + EOF + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ env.VERSION }}-alpha + name: 'NVM Desktop v${{env.VERSION}}-alpha' + body_path: release.txt + draft: false + prerelease: true + token: ${{ secrets.GITHUB_TOKEN }} + generate_release_notes: true diff --git a/@types/index.d.ts b/@types/index.d.ts index 2fc775d..d5f451f 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -94,18 +94,15 @@ declare global { interface ConfigrationExport { color?: string; - mirrors?: string | null; - path: string; + mirrors?: string; projects?: boolean; setting?: boolean; } - interface Configration { + interface ConfigrationImport { color?: string; mirrors?: string; setting?: Setting; - projects?: Project[]; - groups?: Group[]; } } } diff --git a/package.json b/package.json index 5413e6f..cc34f92 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:verbose": "tauri build --verbose", "tauri": "tauri", "ui:dev": "vite", - "ui:build": "vite build", + "ui:build": "tsc && vite build", "ui:preview": "vite preview", "check": "node scripts/check.mjs", "updater": "node scripts/updater.mjs", @@ -84,5 +84,5 @@ "typescript": "^5.5.4", "vite": "^5.4.2" }, - "packageManager": "pnpm@9.7.0" -} \ No newline at end of file + "packageManager": "pnpm@9.9.0" +} diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index adf2ed7..ed53bff 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use crate::{ config::{Config, Group, ISettings, NVersion, Project}, - core::{group, node, project}, + core::{configration, group, handle, node, project}, ret_err, wrap_err, }; @@ -42,8 +42,22 @@ pub async fn read_settings() -> CmdResult { /// update settings #[tauri::command] pub async fn update_settings(settings: ISettings) -> CmdResult<()> { + let locale = Config::settings().latest().get_locale(); + let directory = Config::settings().latest().get_directory(); + Config::settings().apply(); - wrap_err!({ Config::settings().data().patch_settings(settings) }) + wrap_err!({ Config::settings().data().patch_settings(settings.clone()) }); + + // refresh data when directory changes + if directory != settings.directory { + wrap_err!(node::get_installed_list(Some(true)).await); + } + // update system tray + if locale != settings.locale { + wrap_err!(handle::Handle::update_systray_part()); + } + + Ok(()) } /// install node @@ -123,6 +137,24 @@ pub async fn update_group_version(name: String, version: String) -> CmdResult<() wrap_err!(group::update_group_version(name, version).await) } +/// configration export +#[tauri::command] +pub async fn configration_export( + output_path: PathBuf, + configration: configration::ConfigrationExport, +) -> CmdResult<()> { + wrap_err!(configration::configration_export(output_path, configration).await) +} + +/// configration import +#[tauri::command] +pub async fn configration_import( + app_handle: tauri::AppHandle, + sync: bool, +) -> CmdResult> { + wrap_err!(configration::configration_import(&app_handle, sync).await) +} + /// restart app #[tauri::command] pub fn restart(app_handle: tauri::AppHandle) { diff --git a/src-tauri/src/config/settings.rs b/src-tauri/src/config/settings.rs index 31ec3f1..69b40b5 100644 --- a/src-tauri/src/config/settings.rs +++ b/src-tauri/src/config/settings.rs @@ -65,7 +65,12 @@ impl ISettings { help::save_json(&dirs::settings_path()?, self, None) } - /// get the value of `directory` + /// get the value of `locale` + pub fn get_locale(&self) -> Option { + self.locale.clone() + } + + /// get the value of `closer` pub fn get_closer(&self) -> Option { self.closer.clone() } diff --git a/src-tauri/src/core/configration.rs b/src-tauri/src/core/configration.rs new file mode 100644 index 0000000..9fc9c93 --- /dev/null +++ b/src-tauri/src/core/configration.rs @@ -0,0 +1,156 @@ +use super::{handle, project::sync_project_version}; +use crate::config::{Config, Group, ISettings, Project}; +use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::{Emitter, Manager}; +use tauri_plugin_dialog::{DialogExt, FilePath}; + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct ConfigrationExport { + /// theme color + color: Option, + + /// export setting data + setting: Option, + + /// export mirrors data + mirrors: Option, + + /// export projects data (include groups) + projects: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct ConfigrationData { + /// theme color + color: Option, + + /// export setting data + setting: Option, + + /// export mirrors data + mirrors: Option, + + /// export projects data + projects: Option>, + + /// export groups data + groups: Option>, +} + +#[derive(Default, Serialize)] +pub struct ConfigrationImport { + /// theme color + color: Option, + + /// export setting data + setting: Option, + + /// export mirrors data + mirrors: Option, +} + +/// configration export +pub async fn configration_export( + output_path: PathBuf, + configration: ConfigrationExport, +) -> Result<()> { + let ConfigrationExport { + color, + setting, + mirrors, + projects, + } = configration; + + let mut output = ConfigrationData::default(); + + // export theme color + if let Some(color) = color { + output.color = Some(color); + } + + // export setting & mirrors data + if setting.unwrap_or(false) { + output.setting = Some(Config::settings().latest().clone()); + output.mirrors = mirrors; + } + + // export projects & groups data + if projects.unwrap_or(false) { + output.projects = Config::projects().latest().get_list(); + output.groups = Config::groups().latest().get_list(); + } + + let output_json = serde_json::to_string_pretty(&output)?; + tokio::fs::write(output_path, output_json).await?; + + Ok(()) +} + +/// configration import +pub async fn configration_import( + app_handle: &tauri::AppHandle, + sync: bool, +) -> Result> { + if let Some(file_path) = app_handle + .dialog() + .file() + .add_filter("Select Json", &["json"]) + .blocking_pick_file() + { + let data = match file_path { + FilePath::Path(path) => tokio::fs::read_to_string(path).await?, + FilePath::Url(_) => bail!("Unsupported URL scheme"), + }; + + let configration: ConfigrationData = serde_json::from_str(&data)?; + let projects = configration.projects.unwrap_or_default(); + let groups = configration.groups.unwrap_or_default(); + + // need sync node version for every project + if sync { + for project in &projects { + let mut version = project.version.clone(); + // If the project's version matches any group's name, use the group's version + if let Some(ref project_version) = version { + if let Some(group) = groups.iter().find(|g| g.name == *project_version) { + version = group.version.clone(); + } + } + + if let Some(version) = version { + sync_project_version(PathBuf::from(&project.path), &version).await?; + } + } + } + + let need_update_projects = !projects.is_empty(); + let need_update_groups = !groups.is_empty(); + // update projects data + if need_update_projects { + Config::projects().apply(); + Config::projects().data().update_and_save_list(projects)?; + } + // update groups data + if need_update_groups { + Config::groups().apply(); + Config::groups().data().update_groups(groups)?; + } + // update system tray & notification page refresh data + if need_update_projects || need_update_groups { + handle::Handle::update_systray_part()?; + if let Some(window) = app_handle.get_webview_window("main") { + window.emit("call-projects-update", ())?; + } + } + + return Ok(Some(ConfigrationImport { + color: configration.color, + setting: configration.setting, + mirrors: configration.mirrors, + })); + } + + Ok(None) +} diff --git a/src-tauri/src/core/group.rs b/src-tauri/src/core/group.rs index f06dc94..dc44b79 100644 --- a/src-tauri/src/core/group.rs +++ b/src-tauri/src/core/group.rs @@ -4,6 +4,8 @@ use crate::{ }; use anyhow::Result; +use super::handle; + /// get project list from `projects.json` pub async fn group_list(fetch: Option) -> Result>> { let fetch = fetch.unwrap_or(false); @@ -23,15 +25,17 @@ pub async fn group_list(fetch: Option) -> Result>> { /// update groups & save pub async fn update_groups(list: Vec) -> Result<()> { - Config::groups().latest().update_groups(list)?; Config::groups().apply(); + Config::groups().data().update_groups(list)?; + + handle::Handle::update_systray_part()?; + Ok(()) } /// update group version pub async fn update_group_version(name: String, version: String) -> Result<()> { - Config::groups().latest().update_version(name, version)?; - Config::groups().apply(); + Config::groups().data().update_version(name, version)?; Config::groups().data().save_file() } diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 88365c3..0b6e523 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -33,12 +33,22 @@ impl Handle { } /// update the system tray state - pub fn update_systray_part(event: &str, version: &str) -> Result<()> { + pub fn update_systray_part() -> Result<()> { let app_handle = Self::global().app_handle.lock(); if app_handle.is_none() { bail!("update_systray unhandled error"); } - Tray::update_part(app_handle.as_ref().unwrap(), event, version)?; + Tray::update_part(app_handle.as_ref().unwrap())?; + Ok(()) + } + + /// update the system tray state & emit event + pub fn update_systray_part_with_emit(event: &str, version: &str) -> Result<()> { + let app_handle = Self::global().app_handle.lock(); + if app_handle.is_none() { + bail!("update_systray unhandled error"); + } + Tray::update_part_with_emit(app_handle.as_ref().unwrap(), event, version)?; Ok(()) } } diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 17b8ec0..5998c98 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod configration; pub mod group; pub mod handle; pub mod node; diff --git a/src-tauri/src/core/node.rs b/src-tauri/src/core/node.rs index 9ccee4e..c408753 100644 --- a/src-tauri/src/core/node.rs +++ b/src-tauri/src/core/node.rs @@ -50,11 +50,13 @@ pub fn get_current(fetch: Option) -> Result> { /// Set the current node version pub async fn set_current(version: Option) -> Result<()> { let version = version.as_deref().unwrap_or(""); - Config::node().latest().update_current(version)?; Config::node().apply(); + Config::node().data().update_current(version)?; Config::node().data().save_current()?; + handle::Handle::update_systray_part()?; + Ok(()) } @@ -63,7 +65,7 @@ pub async fn update_current_from_menu(current: String) -> Result<()> { let ret = { Config::node().draft().update_current(¤t)?; - wrap_err!(handle::Handle::update_systray_part( + wrap_err!(handle::Handle::update_systray_part_with_emit( "call-current-update", ¤t )); @@ -124,12 +126,13 @@ pub async fn get_installed_list(fetch: Option) -> Result) -> Result Result<()> { /// uninstall node pub async fn uninstall_node(version: String, current: Option) -> Result<()> { - let directory = Config::settings().data().get_directory(); + let directory = Config::settings().latest().get_directory(); if let Some(directory) = directory { let current = current.unwrap_or(false); let directory = PathBuf::from(directory).join(&version); diff --git a/src-tauri/src/core/project.rs b/src-tauri/src/core/project.rs index 998319d..85a9670 100644 --- a/src-tauri/src/core/project.rs +++ b/src-tauri/src/core/project.rs @@ -67,6 +67,9 @@ pub async fn update_projects(list: Vec, path: Option) -> Resul Config::projects().apply(); Config::projects().data().update_and_save_list(list)?; + + handle::Handle::update_systray_part()?; + Ok(()) } @@ -111,7 +114,7 @@ pub async fn change_with_version(name: String, version: String) -> Result<()> { sync_project_version(PathBuf::from(&project_path), &version).await?; - log_err!(handle::Handle::update_systray_part( + log_err!(handle::Handle::update_systray_part_with_emit( "call-projects-update", &version )); @@ -152,7 +155,7 @@ pub async fn change_with_group(name: String, group_name: String) -> Result<()> { sync_project_version(PathBuf::from(&project_path), &version).await?; - log_err!(handle::Handle::update_systray_part( + log_err!(handle::Handle::update_systray_part_with_emit( "call-projects-update", &version )); diff --git a/src-tauri/src/core/tray.rs b/src-tauri/src/core/tray.rs index 5cb81e2..826ee2f 100644 --- a/src-tauri/src/core/tray.rs +++ b/src-tauri/src/core/tray.rs @@ -11,7 +11,6 @@ use tauri::{ CheckMenuItemBuilder, IsMenuItem, Menu, MenuBuilder, MenuEvent, MenuItemBuilder, PredefinedMenuItem, Submenu, SubmenuBuilder, }, - tray::TrayIconBuilder, AppHandle, Emitter, Manager, Wry, }; @@ -163,7 +162,16 @@ impl Tray { Ok(()) } - pub fn update_part(app_handle: &AppHandle, event: &str, version: &str) -> Result<()> { + pub fn update_part(app_handle: &AppHandle) -> Result<()> { + if let Some(tray) = app_handle.tray_by_id("main") { + tray.set_menu(Some(Tray::tray_menu(app_handle)?))?; + Ok(()) + } else { + bail!("The system tray menu has not been initialized") + } + } + + pub fn update_part_with_emit(app_handle: &AppHandle, event: &str, version: &str) -> Result<()> { if let Some(tray) = app_handle.tray_by_id("main") { tray.set_menu(Some(Tray::tray_menu(app_handle)?))?; if let Some(window) = app_handle.get_webview_window("main") { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ffc9f00..61d13fc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -64,6 +64,9 @@ fn main() -> tauri::Result<()> { cmds::group_list, cmds::update_groups, cmds::update_group_version, + // configration + cmds::configration_export, + cmds::configration_import, // app cmds::restart, ]); diff --git a/src/app-context.tsx b/src/app-context.tsx index 916c3b6..6d3f81c 100644 --- a/src/app-context.tsx +++ b/src/app-context.tsx @@ -122,7 +122,6 @@ export function AppProvider({ const onUpdateSetting = useMemo( () => async (setting: Nvmd.Setting) => { updateSettings(setting); - // await window.Context.updateSettingData(setting); dispatch({ type: Actions.UpdateSetting, @@ -132,6 +131,7 @@ export function AppProvider({ [ settings.locale, settings.theme, + settings.closer, settings.directory, settings.mirror, settings.proxy, diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index 4a337e9..607f770 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -10,7 +10,6 @@ import { type ColumnFiltersState, type VisibilityState, getFacetedRowModel, - Row, getFacetedUniqueValues, } from '@tanstack/react-table'; import { diff --git a/src/locales/en.json b/src/locales/en.json index 098d7cf..56b7d20 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,130 +1,131 @@ { - "About": "About", - "Services": "Services", - "Hide": "Hide", - "Hide-Others": "Hide Others", - "Show-All": "Show All", - "Quit": "Quit", - "Edit": "Edit", - "Undo": "Undo", - "Redo": "Redo", - "Cut": "Cut", - "Copy": "Copy", - "Paste": "Paste", - "Select-All": "Select All", - "Window": "Window", - "Minimize": "Minimize", - "Close": "Close", - "Help": "Help", - "Learn-More": "Learn More", - "Documentation": "Documentation", - "Search-Issues": "Search-Issues", - "Setting": "Setting", - "Tip": "Tip", - "OK": "OK", - "Cancel": "Cancel", - "Continue": "Continue", - "Language": "Language", - "System-Default": "System Default", - "Light": "Light", - "Dark": "Dark", - "Themes": "Themes", - "Mirror-Url": "Mirror Url", - "Installation-Directory": "Installation Directory", - "Installation-Directory-tip": "Please note that nvm-desktop does not keep track of all your used directories. This means that every time you change the installation directory, you need to re-download Nodejs.", - "Reset": "Reset", - "Install": "Install", - "Installed": "Installed", - "Not-Installed": "Not Installed", - "Supported": "Supported", - "Not-Supported": "Not Supported", - "Current": "Current", - "Version": "Version", - "Release-Date": "Release Date", - "Status": "Status", - "Operation": "Operation", - "More": "More", - "Apply": "Apply", - "Uninstall": "Uninstall", - "latest": "latest", - "Versions": "Versions", - "Finally": "Finally", - "Welcome": "Welcome", - "Welcome-to": "Welcome to the", - "App-Desc": "A desktop application to manage multiple active node.js versions.", - "Tip-Content": "Now add these lines to your ~/.bashrc, ~/.profile, or ~/.zshrc file to have it automatically sourced upon login: (you may have to add to more than one of the above files)", - "Restart-Terminal": "Now using node {{version}}", - "Tip-Uninstall": "The node {{version}} has been successfully uninstalled", - "Tip-Finally": "For more information about this issue and possible workarounds, please", - "Refer": "refer here.", - "Bles": "Have a nice day!", - "Set-by": "Set by", - "Retry": "Retry", - "Start-Install": "Start Install", - "Version-Manager": "Version Manager", - "Install-Tip": "Click the Start Installation button to start the installation.", - "Check-Update": "Check Update", - "Download-Progress": "Download Progress", - "Update-Info": "Update Info", - "Upgrade": "Upgrade", - "Quit-And-Install": "Quit And Install", - "Current-Version": "Current Version", - "New-Version": "New Version", - "Release-Notes": "Release Notes", - "Upgrade-Tip": "Quit the app and install to upgrade to the latest version?", - "Up-to-date": "You’re up to date!", - "Projects": "Projects", - "All-Projects": "All Projects", - "Add-Project": "Add Project", - "Project-Name": "Project Name", - "Project-Path": "Project Path", - "Project-Delete": "Are you sure to delete this project?", - "Remove": "Remove", - "Can-Select": "You can individually select the version of Node you want for your project.", - "Had-File": "A file will be added to the root of the project:", - "Load-Node": ". The content is the version number of Node you choose. The version number of Node that is loaded as a global setting if this file is not added.", - "Project-Select": "Please select your project", - "Directory-Select": "Please select your directory", - "Whats-new": "What's new", - "Refresh-successful": "The latest data has been synchronized", - "Command-Tip-Project": "Or you can also specify the nodejs version for your project through the command line:", - "Command-tools-intro": "Command tools intro", - "Migration-error": "Please close all Node processes and restart the application to complete the update", - "When-Closing": "When Closing", - "Minimize-Window": "Minimize To System Tray", - "Quit-App": "Quit App", - "Theme color": "Theme Color", - "Color-Tip": "Here you can choose your favorite color as the theme color.", - "Tour": "Tour", - "Tour-Text": "Click me to run the onboarding tours", - "Page-Reload": "Page Reload", - "Data-Update": "Data Update", - "Tour-Tip": "The operation is successful. The onboarding tours will be run the next time you open the client", - "Selected": "selected", - "Clear-Filters": "Clear filters", - "No-results": "No results.", - "Asc": "Asc", - "Desc": "Desc", - "Mirror-Tip": "Please select or input the appropriate mirror url according to your region.", - "Default": "Default", - "Custom": "Custom", - "Configration": "Configration", - "Configration-export": "Configration export", - "Configration-export-tip": "Please select the configuration you want to export:", - "Configration-export-setting": "(lanuage, installation directory, mirror url, etc.)", - "Configration-export-success": "The export has been completed, please view it in the {{filename}} file", - "Configration-import": "Configration import", - "Configration-import-tip": "Whether to automatically synchronize the nodejs version (.nvmdrc) for project data when importing it?", - "Import-only": "Import only", - "Import-and-sync": "Import and sync", - "Configration-import-success": "The import has been completed. If you encounter data inconsistency, please restart the application.", - "Groups": "Groups", - "Group-Name": "Group Name", - "Group-Desc": "Group Description", - "Create-Group": "Create Group", - "Group-Delete": "Are you sure to delete this group?", - "Input-To-Search": "input to search", - "Proxy": "Proxy", - "Enabled": "Enabled", - "Disabled": "Disabled" -} + "About": "About", + "Services": "Services", + "Hide": "Hide", + "Hide-Others": "Hide Others", + "Show-All": "Show All", + "Quit": "Quit", + "Edit": "Edit", + "Undo": "Undo", + "Redo": "Redo", + "Cut": "Cut", + "Copy": "Copy", + "Paste": "Paste", + "Select-All": "Select All", + "Window": "Window", + "Minimize": "Minimize", + "Close": "Close", + "Help": "Help", + "Learn-More": "Learn More", + "Documentation": "Documentation", + "Search-Issues": "Search-Issues", + "Setting": "Setting", + "Tip": "Tip", + "OK": "OK", + "Cancel": "Cancel", + "Continue": "Continue", + "Language": "Language", + "System-Default": "System Default", + "Light": "Light", + "Dark": "Dark", + "Themes": "Themes", + "Mirror-Url": "Mirror Url", + "Installation-Directory": "Installation Directory", + "Installation-Directory-tip": "Please note that nvm-desktop does not keep track of all your used directories. This means that every time you change the installation directory, you need to re-download Nodejs.", + "Reset": "Reset", + "Install": "Install", + "Installed": "Installed", + "Not-Installed": "Not Installed", + "Supported": "Supported", + "Not-Supported": "Not Supported", + "Current": "Current", + "Version": "Version", + "Release-Date": "Release Date", + "Status": "Status", + "Operation": "Operation", + "More": "More", + "Apply": "Apply", + "Uninstall": "Uninstall", + "latest": "latest", + "Versions": "Versions", + "Finally": "Finally", + "Welcome": "Welcome", + "Welcome-to": "Welcome to the", + "App-Desc": "A desktop application to manage multiple active node.js versions.", + "Tip-Content": "Now add these lines to your ~/.bashrc, ~/.profile, or ~/.zshrc file to have it automatically sourced upon login: (you may have to add to more than one of the above files)", + "Restart-Terminal": "Now using node {{version}}", + "Tip-Uninstall": "The node {{version}} has been successfully uninstalled", + "Tip-Finally": "For more information about this issue and possible workarounds, please", + "Refer": "refer here.", + "Bles": "Have a nice day!", + "Set-by": "Set by", + "Retry": "Retry", + "Start-Install": "Start Install", + "Version-Manager": "Version Manager", + "Install-Tip": "Click the Start Installation button to start the installation.", + "Check-Update": "Check Update", + "Download-Progress": "Download Progress", + "Update-Info": "Update Info", + "Upgrade": "Upgrade", + "Quit-And-Install": "Quit And Install", + "Current-Version": "Current Version", + "New-Version": "New Version", + "Release-Notes": "Release Notes", + "Upgrade-Tip": "Quit the app and install to upgrade to the latest version?", + "Up-to-date": "You’re up to date!", + "Projects": "Projects", + "All-Projects": "All Projects", + "Add-Project": "Add Project", + "Project-Name": "Project Name", + "Project-Path": "Project Path", + "Project-Delete": "Are you sure to delete this project?", + "Remove": "Remove", + "Can-Select": "You can individually select the version of Node you want for your project.", + "Had-File": "A file will be added to the root of the project:", + "Load-Node": ". The content is the version number of Node you choose. The version number of Node that is loaded as a global setting if this file is not added.", + "Project-Select": "Please select your project", + "Directory-Select": "Please select your directory", + "Whats-new": "What's new", + "Refresh-successful": "The latest data has been synchronized", + "Command-Tip-Project": "Or you can also specify the nodejs version for your project through the command line:", + "Command-tools-intro": "Command tools intro", + "Migration-error": "Please close all Node processes and restart the application to complete the update", + "When-Closing": "When Closing", + "Minimize-Window": "Minimize To System Tray", + "Quit-App": "Quit App", + "Theme color": "Theme Color", + "Color-Tip": "Here you can choose your favorite color as the theme color.", + "Tour": "Tour", + "Tour-Text": "Click me to run the onboarding tours", + "Page-Reload": "Page Reload", + "Data-Update": "Data Update", + "Tour-Tip": "The operation is successful. The onboarding tours will be run the next time you open the client", + "Selected": "selected", + "Clear-Filters": "Clear filters", + "No-results": "No results.", + "Asc": "Asc", + "Desc": "Desc", + "Mirror-Tip": "Please select or input the appropriate mirror url according to your region.", + "Default": "Default", + "Custom": "Custom", + "Configration": "Configration", + "Configration-export": "Configration export", + "Configration-export-tip": "Please select the configuration you want to export:", + "Configration-export-setting": "(lanuage, installation directory, mirror url, etc.)", + "Configration-export-projects": "(include groups)", + "Configration-export-success": "The export has been completed, please view it in the {{filename}} file", + "Configration-import": "Configration import", + "Configration-import-tip": "Whether to automatically synchronize the nodejs version (.nvmdrc) for project data when importing it?", + "Import-only": "Import only", + "Import-and-sync": "Import and sync", + "Configration-import-success": "The import has been completed. If you encounter data inconsistency, please restart the application.", + "Groups": "Groups", + "Group-Name": "Group Name", + "Group-Desc": "Group Description", + "Create-Group": "Create Group", + "Group-Delete": "Are you sure to delete this group?", + "Input-To-Search": "input to search", + "Proxy": "Proxy", + "Enabled": "Enabled", + "Disabled": "Disabled" +} \ No newline at end of file diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json index b525edb..cd6212a 100644 --- a/src/locales/zh_CN.json +++ b/src/locales/zh_CN.json @@ -112,6 +112,7 @@ "Configration-export": "配置导出", "Configration-export-tip": "请选择您要导出的配置:", "Configration-export-setting": "(语言、安装目录、镜像地址等。)", + "Configration-export-projects": "(包括分组)", "Configration-export-success": "导出已完成,请在 {{filename}} 文件中查看", "Configration-import": "配置导入", "Configration-import-tip": "导入项目数据时是否自动同步 nodejs 版本(.nvmdrc)?", diff --git a/src/main.tsx b/src/main.tsx index 59cc074..8b8fb96 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,14 +15,14 @@ import { getSystemCurrentTheme } from '@/services/api'; * The delay is always within `15ms` (in development) */ (async () => { - const [settings, sysTheme] = await Promise.all([ - getSettings(), - getSystemCurrentTheme(), - ]); + const [settings, sysTheme] = await Promise.all([ + getSettings(), + getSystemCurrentTheme(), + ]); - createRoot(document.getElementById('root') as HTMLElement).render( - // - - // - ); + createRoot(document.getElementById('root') as HTMLElement).render( + + + + ); })(); diff --git a/src/pages/groups/index.tsx b/src/pages/groups/index.tsx index a82e781..d84bf94 100644 --- a/src/pages/groups/index.tsx +++ b/src/pages/groups/index.tsx @@ -102,7 +102,7 @@ export const Component: React.FC = () => { useEffect(() => { const fetcher = async () => { - const iVersions = await installedList(true); + const iVersions = await installedList(false); setInstalledVersions(iVersions); }; diff --git a/src/pages/home/configration.tsx b/src/pages/home/configration.tsx index 59a036b..455c53c 100644 --- a/src/pages/home/configration.tsx +++ b/src/pages/home/configration.tsx @@ -1,36 +1,37 @@ import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, } from 'react'; import { - AlertDialog, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - Button, - Checkbox, - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuTrigger, - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + Checkbox, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from '@/components/ui'; import { Share1Icon } from '@radix-ui/react-icons'; +import { open as openDialog } from '@tauri-apps/plugin-dialog'; import { z } from 'zod'; import { toast } from 'sonner'; @@ -38,306 +39,302 @@ import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '@/app-context'; import { zodResolver } from '@hookform/resolvers/zod'; +import { configrationExport, configrationImport } from '@/services/cmds'; const items = [ - { - id: 'color', - label: 'Theme color', - }, - { - id: 'setting', - label: 'Setting', - }, - { - id: 'projects', - label: 'Projects', - }, + { + id: 'color', + label: 'Theme color', + }, + { + id: 'setting', + label: 'Setting', + }, + { + id: 'projects', + label: 'Projects', + }, ] as const; const FormSchema = z.object({ - items: z.array(z.string()).refine((value) => value.some((item) => item), { - message: 'You have to select at least one item.', - }), + items: z.array(z.string()).refine((value) => value.some((item) => item), { + message: 'You have to select at least one item.', + }), }); const Configration: React.FC = () => { - const { t } = useTranslation(); + const { t } = useTranslation(); - const exporter = useRef(null); - const importer = useRef(null); + const exporter = useRef(null); + const importer = useRef(null); - useEffect(() => { - const listener = (evt: KeyboardEvent) => { - if (evt.metaKey && evt.shiftKey && (evt.key === 'e' || evt.key === 'E')) { - exporter.current?.alert(); - } + useEffect(() => { + const listener = (evt: KeyboardEvent) => { + if (evt.metaKey && evt.shiftKey && (evt.key === 'e' || evt.key === 'E')) { + exporter.current?.alert(); + } - if (evt.metaKey && evt.shiftKey && (evt.key === 'i' || evt.key === 'I')) { - importer.current?.alert(); - } - }; - document.addEventListener('keydown', listener); - return () => { - document.removeEventListener('keydown', listener); - }; - }, []); + if (evt.metaKey && evt.shiftKey && (evt.key === 'i' || evt.key === 'I')) { + importer.current?.alert(); + } + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); - const title = t('Configration'), - platform = OS_PLATFORM; + const title = t('Configration'), + platform = OS_PLATFORM; - return ( - <> - - - - - - - ); + const { items } = values; + setLoading(true); + try { + const filename = `${path}${ + OS_PLATFORM === 'win32' ? '\\' : '/' + }configration_${Date.now()}.json`; + const exportSetting = items.includes('setting'); + + await configrationExport(filename, { + color: items.includes('color') ? color : void 0, + projects: items.includes('projects'), + setting: exportSetting, + mirrors: exportSetting + ? localStorage.getItem('nvmd-mirror') || void 0 + : void 0, + }); + toast.success(t('Configration-export-success', { filename }), { + duration: 5000, + }); + setOpen(false); + } catch (err) { + toast.error(err?.message || err.toString()); + } finally { + setLoading(false); + } + }; + + return ( + { + if (!open) form.reset({ items: ['color', 'setting', 'projects'] }); + setOpen(open); + }} + > + + + {t('Configration-export')} + + {t('Configration-export-tip')} + +
+ ( + + {items.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange([...field.value, item.id]) + : field.onChange( + field.value?.filter( + (value) => value !== item.id + ) + ); + }} + /> + + + {t(item.label)} + {item.id === 'setting' ? ( + + {t('Configration-export-setting')} + + ) : item.id === 'projects' ? ( + + {t('Configration-export-projects')} + + ) : null} + + + ); + }} + /> + ))} + + + )} + /> + +
+ + + {t('Cancel')} + + + +
+
+ ); }); type Importer = { - alert: () => void; + alert: () => void; }; const ConfigrationImport = forwardRef(({}, ref) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); + + const { setColor, onUpdateSetting } = useAppContext(); + const { t } = useTranslation(); - // const { setColor, onUpdateSetting } = useAppContext(); - const { t } = useTranslation(); + useImperativeHandle(ref, () => ({ + alert: onAlert, + })); - useImperativeHandle(ref, () => ({ - alert: onAlert, - })); + const onAlert = () => { + setOpen(true); + }; - const onAlert = () => { - setOpen(true); - }; + const onConfigrationImport = async (sync: boolean) => { + try { + const data = await configrationImport(sync); + if (!data) return; - const onConfigrationImport = async (sync: boolean) => { - // try { - // const { canceled, color, mirrors, setting } = - // await window.Context.onConfigrationImport({ - // sync, - // title: t('File-Select'), - // }); - // if (canceled) return; - // toast.success(t('Configration-import-success'), { duration: 5000 }); - // color && setColor(color); - // mirrors && localStorage.setItem('nvmd-mirror', mirrors); - // setting && onUpdateSetting(setting); - // setOpen(false); - // } catch (err) { - // toast.error( - // err.message - // ? err.message - // .split("Error invoking remote method 'configration-import': ") - // .slice(-1) - // : 'Something went wrong' - // ); - // } - }; + console.log(data); + const { color, mirrors, setting } = data; + toast.success(t('Configration-import-success'), { duration: 5000 }); + color && setColor(color); + mirrors && localStorage.setItem('nvmd-mirror', mirrors); + setting && onUpdateSetting(setting); + setOpen(false); + } catch (err) { + toast.error(err?.message || err.toString()); + } + }; - return ( - { - setOpen(open); - }} - > - - - {t('Configration-import')} - - {t('Configration-import-tip')} - - - - {t('Cancel')} - - - - - - ); + return ( + { + setOpen(open); + }} + > + + + {t('Configration-import')} + + {t('Configration-import-tip')} + + + + {t('Cancel')} + + + + + + ); }); export default Configration; diff --git a/src/pages/home/updater.tsx b/src/pages/home/updater.tsx index 0359293..ba43505 100644 --- a/src/pages/home/updater.tsx +++ b/src/pages/home/updater.tsx @@ -2,20 +2,20 @@ import './progress.css'; import { useEffect, useState } from 'react'; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - Button, - Label, - Popover, - PopoverContent, - PopoverTrigger, - Progress, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + Button, + Label, + Popover, + PopoverContent, + PopoverTrigger, + Progress, } from '@/components/ui'; import Markdown from 'react-markdown'; import { GlobeIcon } from '@radix-ui/react-icons'; @@ -28,228 +28,227 @@ import { useTranslation } from 'react-i18next'; import { relaunch } from '@/services/cmds'; enum ModalType { - Check = 'check', - Complete = 'complete', + Check = 'check', + Complete = 'complete', } export const Updater: React.FC = () => { - const [open, setOpen] = useState<{ visible: boolean; type: ModalType }>({ - visible: false, - type: ModalType.Check, - }); - const [pop, setPop] = useState(false); - const [loading, setLoading] = useState(false); - const [percentage, setPercentage] = useState(); - const [updateInfo, setUpdateInfo] = useState(); - - const { t } = useTranslation(); - - useEffect(() => { - const checkUpdate = async () => { - try { - const update = await check(); - console.log(update); - if (update) { - setUpdateInfo(update); - } - } catch (err) { - toast.error(err?.message || err.toString()); - } - }; - - checkUpdate(); - }, []); - - const onCheckUpdates = async () => { - if (updateInfo?.available) { - return setOpen({ visible: true, type: ModalType.Check }); - } - - setLoading(true); - try { - const update = await check(); - if (update) { - setUpdateInfo(update); - setOpen({ visible: true, type: ModalType.Check }); - } else { - toast.success(t('Up-to-date')); - } - } catch (err) { - toast.error(err?.message || err.toString()); - } finally { - setLoading(false); - } - }; - - const onUpgrade = async () => { - if (!updateInfo) return; - - setOpen({ visible: false, type: ModalType.Check }); - - let downloaded = 0; - let contentLength = 0; - const updateProgress = debounce((downloaded, contentLength) => { - const percent = Math.floor((downloaded / contentLength) * 100); - setPercentage(percent); - }, 100); - - await updateInfo.download((progress) => { - switch (progress.event) { - case 'Started': { - contentLength = progress.data.contentLength!; - break; - } - case 'Progress': { - downloaded += progress.data.chunkLength; - updateProgress(downloaded, contentLength); - break; - } - case 'Finished': { - setOpen({ visible: true, type: ModalType.Complete }); - break; - } - } - }); - }; - - const onMakeUpdateNow = async () => { - if (!updateInfo) return; - - try { - await updateInfo.install(); - await relaunch(); - } catch (err) { - toast.error(err?.message || err.toString()); - } - }; - - return ( - <> - {percentage === void 0 ? ( -
-
- ) : ( - setPop(open)}> - - -
- - { - try { - const curVersion = version.slice(1); - await vSetCurrent(curVersion); - setCurrent(curVersion); - toast.success(t('Restart-Terminal', { version })); - } catch (err) { - toast.error(err); - } - }} - > - - {t('Apply')} - - { - try { - await uninstallNode( - version.slice(1), - version.includes(current) - ); - const [currentVersion, versions] = await Promise.all([ - vCurrent(), - installedList(true), - ]); - setCurrent(currentVersion); - setInstalledVersions(versions); - toast.success(t('Tip-Uninstall', { version })); - } catch (err) { - toast.error(err); - } - }} - > - - {t('Uninstall')} - - -
- ); - - return ( - - ); - }, - }, - ]; - }, [locale, current, installedVersions.length, versions.length]); - - const statuses = useMemo( - () => [ - { - label: t('Installed'), - value: 'Installed', - icon: MinusCircledIcon, - }, - { - label: t('Supported'), - value: 'Supported', - icon: CheckCircledIcon, - }, - { - label: t('Not-Supported'), - value: 'UnSupported', - icon: CrossCircledIcon, - }, - ], - [locale] - ); - - const getFacetedUniqueValues: () => ( - table: Table, - columnId: string - ) => () => Map = useMemo(() => { - return function getFacetedUniqueValues() { - return (table, columnId) => - memo( - () => [table.getColumn(columnId)?.getFacetedRowModel()], - (facetedRowModel) => { - if (!facetedRowModel) return new Map(); - - let facetedUniqueValues = new Map(); - - for (let i = 0; i < facetedRowModel.flatRows.length; i++) { - const { version, files } = facetedRowModel.flatRows[i]!.original; - - let key: string | undefined; - if (installedVersions.includes(version.slice(1))) - key = 'Installed'; - - if (key === void 0 && checkSupportive(files)) key = 'Supported'; - - if (key === void 0) key = 'UnSupported'; - - if (facetedUniqueValues.has(key)) { - facetedUniqueValues.set( - key, - (facetedUniqueValues.get(key) ?? 0) + 1 - ); - } else { - facetedUniqueValues.set(key, 1); - } - } - - return facetedUniqueValues; - }, - { - key: - process.env.NODE_ENV === 'development' && - 'getFacetedUniqueValues_' + columnId, - debug: () => table.options.debugAll ?? table.options.debugTable, - onChange: () => {}, - } - ); - }; - }, [installedVersions.length]); - - const onPageReload = async () => { - seLocaltLoading(true); - try { - const [currentVersion, versions, installeds] = await Promise.all([ - vCurrent(), - versionList(), - installedList(), - ]); - setCurrent(currentVersion); - setVersions(versions); - setInstalledVersions(installeds); - toast.success(t('Refresh-successful')); - } finally { - seLocaltLoading(false); - } - }; - - const onDataUpdate = async () => { - setLoading(true); - try { - const [currentVersion, versions, installeds] = await Promise.all([ - vCurrent(true), - versionList(true), - installedList(true), - ]); - setCurrent(currentVersion); - setVersions(versions); - setInstalledVersions(installeds); - toast.success(t('Refresh-successful')); - } catch (err) { - toast.error(err); - } finally { - setLoading(false); - } - }; - - const onInstalledRefresh = async () => { - const versions = await installedList(true); - setInstalledVersions(versions); - }; - - return ( - <> -
- ( -
- -
- - -
-
- )} - getFacetedUniqueValues={getFacetedUniqueValues} - /> -
- - - ); + const versionsData = useAsyncValue() as VersionsResult; + + const [currentVersion, allVersions, allInstalledVersions] = versionsData; + + const [current, setCurrent] = useState(() => currentVersion); + const [versions, setVersions] = useState(() => allVersions); + const [installedVersions, setInstalledVersions] = useState( + () => allInstalledVersions + ); + const [loading, setLoading] = useState(false); + const [localLoading, seLocaltLoading] = useState(false); + + const modal = useRef(null); + + const { settings } = useAppContext(); + const { directory, locale } = settings; + const { t } = useTranslation(); + + useEffect(() => { + const unlisted = getCurrent().listen( + 'call-current-update', + async ({ payload }) => { + if (payload) { + setCurrent(payload); + toast.success(t('Restart-Terminal', { version: `v${payload}` })); + } + } + ); + + return () => { + unlisted.then((fn) => fn()); + }; + }, []); + + useEffect(() => { + const fetcher = async () => { + const iVersions = await installedList(false); + setInstalledVersions(iVersions); + }; + fetcher(); + }, [directory]); + + const columns: ColumnDef[] = useMemo(() => { + const { version: latest } = versions[0] || { version: '' }; + return [ + { + accessorKey: 'version', + header: ({ column }) => ( + + ), + enableHiding: false, + filterFn: (row, _columnId, filterValue: string) => { + const { version, lts } = row.original; + if ('lts'.includes(filterValue.toLocaleLowerCase())) return !!lts; + + return ( + ('lts'.includes(filterValue.toLocaleLowerCase()) ? !!lts : false) || + version + .toString() + .toLowerCase() + .includes(filterValue.toLowerCase()) || + (lts + ? lts.toString().toLowerCase().includes(filterValue.toLowerCase()) + : false) + ); + }, + cell: ({ row }) => { + const { version, lts } = row.original; + return ( +
+ + + + {version} + + + + {t('Whats-new')} + + + {lts ? ( + ({lts}) + ) : latest === version ? ( + + ({t('latest')}) + + ) : null} +
+ ); + }, + }, + { + accessorKey: 'v8', + header: ({ column }) => ( + + ), + enableSorting: false, + }, + { + accessorKey: 'npm', + header: ({ column }) => ( + + ), + enableSorting: false, + }, + { + accessorKey: 'date', + header: ({ column }) => ( + + ), + cell: ({ row }) => dayjs(row.original.date).format('ll'), + }, + { + accessorKey: 'status', + header: t('Status'), + enableSorting: false, + filterFn: (row, _columnId, filterValue: string[]) => { + const { version, files } = row.original; + + const rets = filterValue.map((value) => { + switch (value) { + case 'Installed': { + return !!installedVersions.find((installed) => + version.includes(installed) + ); + } + case 'Supported': { + return checkSupportive(files); + } + case 'UnSupported': { + return !checkSupportive(files); + } + default: + return false; + } + }); + + return rets.includes(true); + }, + cell: ({ row }) => { + const { version, files } = row.original; + const support = checkSupportive(files); + + if (!support) return {t('Not-Supported')}; + + const installed = installedVersions.find((installed) => + version.includes(installed) + ); + + if (installed && current && version.includes(current)) + return {t('Current')}; + + if (installed) return {t('Installed')}; + + return {t('Not-Installed')}; + }, + }, + { + header: t('Operation'), + enableHiding: false, + enableSorting: false, + cell: ({ row }) => { + const { version } = row.original; + + if (installedVersions.find((install) => version.includes(install))) + return ( + + + + + + { + try { + const curVersion = version.slice(1); + await vSetCurrent(curVersion); + setCurrent(curVersion); + toast.success(t('Restart-Terminal', { version })); + } catch (err) { + toast.error(err); + } + }} + > + + {t('Apply')} + + { + try { + await uninstallNode( + version.slice(1), + version.includes(current) + ); + const [currentVersion, versions] = await Promise.all([ + vCurrent(), + installedList(true), + ]); + setCurrent(currentVersion); + setInstalledVersions(versions); + toast.success(t('Tip-Uninstall', { version })); + } catch (err) { + toast.error(err); + } + }} + > + + {t('Uninstall')} + + + + ); + + return ( + + ); + }, + }, + ]; + }, [locale, current, installedVersions.length, versions.length]); + + const statuses = useMemo( + () => [ + { + label: t('Installed'), + value: 'Installed', + icon: MinusCircledIcon, + }, + { + label: t('Supported'), + value: 'Supported', + icon: CheckCircledIcon, + }, + { + label: t('Not-Supported'), + value: 'UnSupported', + icon: CrossCircledIcon, + }, + ], + [locale] + ); + + const getFacetedUniqueValues: () => ( + table: Table, + columnId: string + ) => () => Map = useMemo(() => { + return function getFacetedUniqueValues() { + return (table, columnId) => + memo( + () => [table.getColumn(columnId)?.getFacetedRowModel()], + (facetedRowModel) => { + if (!facetedRowModel) return new Map(); + + let facetedUniqueValues = new Map(); + + for (let i = 0; i < facetedRowModel.flatRows.length; i++) { + const { version, files } = facetedRowModel.flatRows[i]!.original; + + let key: string | undefined; + if (installedVersions.includes(version.slice(1))) + key = 'Installed'; + + if (key === void 0 && checkSupportive(files)) key = 'Supported'; + + if (key === void 0) key = 'UnSupported'; + + if (facetedUniqueValues.has(key)) { + facetedUniqueValues.set( + key, + (facetedUniqueValues.get(key) ?? 0) + 1 + ); + } else { + facetedUniqueValues.set(key, 1); + } + } + + return facetedUniqueValues; + }, + { + key: + process.env.NODE_ENV === 'development' && + 'getFacetedUniqueValues_' + columnId, + debug: () => table.options.debugAll ?? table.options.debugTable, + onChange: () => {}, + } + ); + }; + }, [installedVersions.length]); + + const onPageReload = async () => { + seLocaltLoading(true); + try { + const [currentVersion, versions, installeds] = await Promise.all([ + vCurrent(), + versionList(), + installedList(), + ]); + setCurrent(currentVersion); + setVersions(versions); + setInstalledVersions(installeds); + toast.success(t('Refresh-successful')); + } finally { + seLocaltLoading(false); + } + }; + + const onDataUpdate = async () => { + setLoading(true); + try { + const [currentVersion, versions, installeds] = await Promise.all([ + vCurrent(true), + versionList(true), + installedList(true), + ]); + setCurrent(currentVersion); + setVersions(versions); + setInstalledVersions(installeds); + toast.success(t('Refresh-successful')); + } catch (err) { + toast.error(err); + } finally { + setLoading(false); + } + }; + + const onInstalledRefresh = async () => { + const versions = await installedList(true); + setInstalledVersions(versions); + }; + + return ( + <> +
+ ( +
+ +
+ + +
+
+ )} + getFacetedUniqueValues={getFacetedUniqueValues} + /> +
+ + + ); }; diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 32b6200..11db76d 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -5,7 +5,7 @@ import { invoke } from '@tauri-apps/api/core'; * @return {Promise} Promise-Nvmd.Setting */ export async function getSettings() { - return invoke('read_settings'); + return invoke('read_settings'); } /** @@ -14,7 +14,7 @@ export async function getSettings() { * @return {Promise} Promise-void */ export async function updateSettings(settings: Nvmd.Setting) { - return invoke('update_settings', { settings }); + return invoke('update_settings', { settings }); } /** @@ -23,7 +23,7 @@ export async function updateSettings(settings: Nvmd.Setting) { * @return {Promise} node version */ export function vCurrent(fetch: boolean = false) { - return invoke('current', { fetch }); + return invoke('current', { fetch }); } /** @@ -32,7 +32,7 @@ export function vCurrent(fetch: boolean = false) { * @return {Promise} Promise-void */ export function vSetCurrent(version: string) { - return invoke('set_current', { version }); + return invoke('set_current', { version }); } /** @@ -41,7 +41,7 @@ export function vSetCurrent(version: string) { * @return {Promise} List of all officially released versions of node */ export function versionList(fetch: boolean = false) { - return invoke('version_list', { fetch }); + return invoke('version_list', { fetch }); } /** @@ -50,7 +50,7 @@ export function versionList(fetch: boolean = false) { * @return {Promise>} An array containing the node version number */ export function installedList(fetch: boolean = false) { - return invoke>('installed_list', { fetch }); + return invoke>('installed_list', { fetch }); } /** @@ -60,7 +60,7 @@ export function installedList(fetch: boolean = false) { * @return {Promise} The file path where the downloaded node is saved */ export function installNode(version: string, arch?: string) { - return invoke('install_node', { version, arch }); + return invoke('install_node', { version, arch }); } /** @@ -68,7 +68,7 @@ export function installNode(version: string, arch?: string) { * @return {Promise} Promise-void */ export function installNodeCancel() { - return invoke('install_node_cancel'); + return invoke('install_node_cancel'); } /** @@ -78,7 +78,7 @@ export function installNodeCancel() { * @returns {Promise} Promise */ export function uninstallNode(version: string, current: boolean = false) { - return invoke('uninstall_node', { version, current }); + return invoke('uninstall_node', { version, current }); } /** @@ -87,7 +87,7 @@ export function uninstallNode(version: string, current: boolean = false) { * @return {Promise} */ export function projectList(fetch: boolean = false) { - return invoke('project_list', { fetch }); + return invoke('project_list', { fetch }); } /** @@ -95,7 +95,7 @@ export function projectList(fetch: boolean = false) { * @return {Promise>} */ export function selectProjects() { - return invoke>('select_projects'); + return invoke>('select_projects'); } /** @@ -104,7 +104,7 @@ export function selectProjects() { * @return {Promise} */ export function updateProjects(list: Nvmd.Project[], path?: string) { - return invoke('update_projects', { list, path }); + return invoke('update_projects', { list, path }); } /** @@ -114,7 +114,7 @@ export function updateProjects(list: Nvmd.Project[], path?: string) { * @return {Promise<200 | 404>} */ export function syncProjectVersion(path: string, version: string) { - return invoke<200 | 404>('sync_project_version', { path, version }); + return invoke<200 | 404>('sync_project_version', { path, version }); } /** @@ -124,7 +124,7 @@ export function syncProjectVersion(path: string, version: string) { * @return {Promise} */ export function batchUpdateProjectVersion(paths: string[], version: string) { - return invoke('batch_update_project_version', { paths, version }); + return invoke('batch_update_project_version', { paths, version }); } /** @@ -133,7 +133,7 @@ export function batchUpdateProjectVersion(paths: string[], version: string) { * @return {Promise} */ export function groupList(fetch: boolean = false) { - return invoke('group_list', { fetch }); + return invoke('group_list', { fetch }); } /** @@ -142,7 +142,7 @@ export function groupList(fetch: boolean = false) { * @return {Promise} */ export function updateGroups(list: Nvmd.Group[]) { - return invoke('update_groups', { list }); + return invoke('update_groups', { list }); } /** @@ -152,7 +152,26 @@ export function updateGroups(list: Nvmd.Group[]) { * @return {Promise} */ export function updateGroupVersion(name: string, version: string) { - return invoke('update_group_version', { name, version }); + return invoke('update_group_version', { name, version }); +} + +/** + * @description: Export configration data. + * @param {string} outputPath Output file path + * @param {Nvmd.ConfigrationExport} configration Decide which data to export + * @returns {Promise} + */ +export function configrationExport( + outputPath: string, + configration: Nvmd.ConfigrationExport +) { + return invoke('configration_export', { outputPath, configration }); +} + +export function configrationImport(sync: boolean = false) { + return invoke('configration_import', { + sync, + }); } /** @@ -160,5 +179,5 @@ export function updateGroupVersion(name: string, version: string) { * @returns {Promise} */ export function relaunch() { - return invoke('restart'); + return invoke('restart'); }