From ac74ef9c0b379439a2d9e070d7c9c46ae081ecd0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 29 Sep 2025 12:26:28 +0000 Subject: [PATCH 01/18] [ci skip] Update version to main --- app/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 0ac125b8e..a9135ca7c 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Blinko", - "version": "1.6.3", + "version": "main", "identifier": "com.blinko.app", "build": { "beforeDevCommand": "bun run dev", From 0981288baafcd6ee005c779ca690a8b4f6d58581 Mon Sep 17 00:00:00 2001 From: Blinko Date: Mon, 29 Sep 2025 20:33:03 +0800 Subject: [PATCH 02/18] chore: update cuda path in yml --- .github/workflows/app-release.yml | 12 ++++++++---- .github/workflows/windows-test-release.yml | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 120245fb9..3d6c94e9c 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -134,17 +134,21 @@ jobs: - name: Install CUDA Toolkit (Windows) if: matrix.platform == 'windows-latest' - uses: Jimver/cuda-toolkit@v0.2.15 + uses: Jimver/cuda-toolkit@v0.2.24 + id: cuda-toolkit with: - cuda: '12.1.0' + cuda: '12.5.0' method: 'network' sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev" ]' - name: Set CUDA environment variables (Windows) if: matrix.platform == 'windows-latest' run: | - echo "CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1" >> $GITHUB_ENV - echo "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\bin" >> $GITHUB_PATH + echo "Installed cuda version is: ${{steps.cuda-toolkit.outputs.cuda}}" + echo "Cuda install location: ${{steps.cuda-toolkit.outputs.CUDA_PATH}}" + echo "CUDA_PATH=${{steps.cuda-toolkit.outputs.CUDA_PATH}}" >> $GITHUB_ENV + echo "${{steps.cuda-toolkit.outputs.CUDA_PATH}}\bin" >> $GITHUB_PATH + nvcc -V - name: Fix version format for Windows MSI if: matrix.platform == 'windows-latest' diff --git a/.github/workflows/windows-test-release.yml b/.github/workflows/windows-test-release.yml index a30a836c3..2058adf55 100644 --- a/.github/workflows/windows-test-release.yml +++ b/.github/workflows/windows-test-release.yml @@ -109,16 +109,20 @@ jobs: path: app/src-tauri/ - name: Install CUDA Toolkit (Windows) - uses: Jimver/cuda-toolkit@v0.2.15 + uses: Jimver/cuda-toolkit@v0.2.24 + id: cuda-toolkit with: - cuda: '12.1.0' + cuda: '12.5.0' method: 'network' sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev" ]' - name: Set CUDA environment variables (Windows) run: | - echo "CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1" >> $GITHUB_ENV - echo "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\bin" >> $GITHUB_PATH + echo "Installed cuda version is: ${{steps.cuda-toolkit.outputs.cuda}}" + echo "Cuda install location: ${{steps.cuda-toolkit.outputs.CUDA_PATH}}" + echo "CUDA_PATH=${{steps.cuda-toolkit.outputs.CUDA_PATH}}" >> $GITHUB_ENV + echo "${{steps.cuda-toolkit.outputs.CUDA_PATH}}\bin" >> $GITHUB_PATH + nvcc -V - name: Fix version format for Windows MSI run: | From 551c053c49eff700cdfe504fa4912d1d3ef94983 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 29 Sep 2025 12:34:00 +0000 Subject: [PATCH 03/18] [ci skip] Update version to 1.6.3 --- app/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index a9135ca7c..0ac125b8e 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Blinko", - "version": "main", + "version": "1.6.3", "identifier": "com.blinko.app", "build": { "beforeDevCommand": "bun run dev", From ca41afda7353e0b7e33527f0a46e3556f5907587 Mon Sep 17 00:00:00 2001 From: Blinko Date: Mon, 29 Sep 2025 21:33:48 +0800 Subject: [PATCH 04/18] chore: update yml --- .github/workflows/app-release.yml | 38 ++++++++++++++++++++ .github/workflows/windows-test-release.yml | 42 ++++++++++++++++++++-- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 3d6c94e9c..d16fe7342 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -150,6 +150,43 @@ jobs: echo "${{steps.cuda-toolkit.outputs.CUDA_PATH}}\bin" >> $GITHUB_PATH nvcc -V + - name: Fix CUDA Visual Studio Integration (Windows) + if: matrix.platform == 'windows-latest' + run: | + $cudaPath = "${{steps.cuda-toolkit.outputs.CUDA_PATH}}" + echo "CUDA Path: $cudaPath" + + # Source: CUDA Visual Studio integration files + $sourceDir = "$cudaPath\extras\visual_studio_integration\MSBuildExtensions" + echo "Source: $sourceDir" + + # Find Visual Studio installation + $vsPaths = @( + "C:\Program Files\Microsoft Visual Studio\2022\Enterprise", + "C:\Program Files\Microsoft Visual Studio\2022\Community", + "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" + ) + + foreach ($vsPath in $vsPaths) { + $destDir = "$vsPath\MSBuild\Microsoft\VC\v170\BuildCustomizations" + if (Test-Path $destDir) { + echo "Found VS at: $vsPath" + echo "Destination: $destDir" + + if (Test-Path $sourceDir) { + echo "Copying CUDA integration files..." + Copy-Item "$sourceDir\*" $destDir -Force -Verbose + echo "Successfully copied CUDA integration files to $destDir" + } else { + echo "ERROR: Source directory not found: $sourceDir" + } + break + } + } + + # Set environment variable for CMake to use CUDA toolset + echo "CMAKE_GENERATOR_TOOLSET=cuda=$cudaPath" >> $GITHUB_ENV + - name: Fix version format for Windows MSI if: matrix.platform == 'windows-latest' run: | @@ -194,6 +231,7 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + components: ${{ matrix.platform == 'windows-latest' && 'rustfmt' || '' }} - name: Rust Cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/windows-test-release.yml b/.github/workflows/windows-test-release.yml index 2058adf55..dd1e2f615 100644 --- a/.github/workflows/windows-test-release.yml +++ b/.github/workflows/windows-test-release.yml @@ -114,7 +114,7 @@ jobs: with: cuda: '12.5.0' method: 'network' - sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev" ]' + sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev", "nvrtc", "nvrtc_dev" ]' - name: Set CUDA environment variables (Windows) run: | @@ -124,6 +124,42 @@ jobs: echo "${{steps.cuda-toolkit.outputs.CUDA_PATH}}\bin" >> $GITHUB_PATH nvcc -V + - name: Fix CUDA Visual Studio Integration + run: | + $cudaPath = "${{steps.cuda-toolkit.outputs.CUDA_PATH}}" + echo "CUDA Path: $cudaPath" + + # Source: CUDA Visual Studio integration files + $sourceDir = "$cudaPath\extras\visual_studio_integration\MSBuildExtensions" + echo "Source: $sourceDir" + + # Find Visual Studio installation + $vsPaths = @( + "C:\Program Files\Microsoft Visual Studio\2022\Enterprise", + "C:\Program Files\Microsoft Visual Studio\2022\Community", + "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" + ) + + foreach ($vsPath in $vsPaths) { + $destDir = "$vsPath\MSBuild\Microsoft\VC\v170\BuildCustomizations" + if (Test-Path $destDir) { + echo "Found VS at: $vsPath" + echo "Destination: $destDir" + + if (Test-Path $sourceDir) { + echo "Copying CUDA integration files..." + Copy-Item "$sourceDir\*" $destDir -Force -Verbose + echo "Successfully copied CUDA integration files to $destDir" + } else { + echo "ERROR: Source directory not found: $sourceDir" + } + break + } + } + + # Set environment variable for CMake to use CUDA toolset + echo "CMAKE_GENERATOR_TOOLSET=cuda=$cudaPath" >> $GITHUB_ENV + - name: Fix version format for Windows MSI run: | $versionJson = Get-Content -Path app/src-tauri/tauri.conf.json | ConvertFrom-Json @@ -159,6 +195,8 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Rust Cache uses: Swatinem/rust-cache@v2 @@ -188,4 +226,4 @@ jobs: ../node_modules/.bin/tauri build --no-bundle echo "Windows build completed successfully!" echo "Build artifacts location: src-tauri/target/release/" - ls -la src-tauri/target/release/ \ No newline at end of file + Get-ChildItem src-tauri/target/release/ \ No newline at end of file From 385be77743d87fe1fb92dc23374577af64a87159 Mon Sep 17 00:00:00 2001 From: Blinko Date: Mon, 29 Sep 2025 22:12:18 +0800 Subject: [PATCH 05/18] chore: update yml --- .github/workflows/app-release.yml | 2 +- .github/workflows/windows-test-release.yml | 2 +- app/src-tauri/Cargo.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index d16fe7342..7f1b845c8 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -139,7 +139,7 @@ jobs: with: cuda: '12.5.0' method: 'network' - sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev" ]' + sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev", "visual_studio_integration", "thrust" ]' - name: Set CUDA environment variables (Windows) if: matrix.platform == 'windows-latest' diff --git a/.github/workflows/windows-test-release.yml b/.github/workflows/windows-test-release.yml index dd1e2f615..a1e6fdc98 100644 --- a/.github/workflows/windows-test-release.yml +++ b/.github/workflows/windows-test-release.yml @@ -114,7 +114,7 @@ jobs: with: cuda: '12.5.0' method: 'network' - sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev", "nvrtc", "nvrtc_dev" ]' + sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev", "nvrtc", "nvrtc_dev", "visual_studio_integration", "thrust" ]' - name: Set CUDA environment variables (Windows) run: | diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 1ad111cd0..064777a0b 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -44,10 +44,10 @@ sys-locale = "0.3" [target.'cfg(target_os = "windows")'.dependencies] tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } -whisper-rs = { version = "0.15.1" , features = ["cuda"]} +whisper-rs = { version = "0.15.1", features = ["cuda"] } cpal = "0.16.0" crossbeam-channel = "0.5" parking_lot = "0.12" [target.'cfg(target_os = "macos")'.dependencies] -macos-accessibility-client = "0.0.1" \ No newline at end of file +macos-accessibility-client = "0.0.1" From 0859bfdb2487286879bfaf3b959711ed436a0308 Mon Sep 17 00:00:00 2001 From: Blinko Date: Mon, 29 Sep 2025 22:52:13 +0800 Subject: [PATCH 06/18] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20improve=20w?= =?UTF-8?q?hisper=20transcriber=20code=20formatting=20and=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src-tauri/src/voice/transcriber.rs | 76 +++++++++++++++----------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/app/src-tauri/src/voice/transcriber.rs b/app/src-tauri/src/voice/transcriber.rs index 9443db2d5..520e90f5e 100644 --- a/app/src-tauri/src/voice/transcriber.rs +++ b/app/src-tauri/src/voice/transcriber.rs @@ -19,8 +19,13 @@ impl WhisperTranscriber { } /// Transcribe audio data to text - pub fn transcribe(&self, audio_data: &[f32], language: Option<&str>) -> Result> { - if audio_data.len() < 1600 { // At least 0.1 seconds of audio at 16kHz + pub fn transcribe( + &self, + audio_data: &[f32], + language: Option<&str>, + ) -> Result> { + if audio_data.len() < 1600 { + // At least 0.1 seconds of audio at 16kHz return Ok(String::new()); } @@ -66,17 +71,21 @@ fn detect_cuda_support() -> (bool, String) { match std::process::Command::new("nvidia-smi") .arg("--query-gpu=name") .arg("--format=csv,noheader,nounits") - .output() { + .output() + { Ok(output) if output.status.success() => { let gpu_names = String::from_utf8_lossy(&output.stdout); let gpu_list: Vec<&str> = gpu_names.lines().collect(); if !gpu_list.is_empty() { (true, format!("NVIDIA GPU: {}", gpu_list.join(", "))) } else { - (false, "NVIDIA driver installed but no GPU detected".to_string()) + ( + false, + "NVIDIA driver installed but no GPU detected".to_string(), + ) } } - _ => (false, "NVIDIA GPU or driver not detected".to_string()) + _ => (false, "NVIDIA GPU or driver not detected".to_string()), } } #[cfg(not(target_os = "windows"))] @@ -125,7 +134,11 @@ fn detect_gpu_capabilities() -> (bool, String) { let has_gpu = !gpu_info.is_empty(); let info = if has_gpu { - format!("GPU support detected: {} | {}", gpu_info.join(", "), detailed_info.join(" | ")) + format!( + "GPU support detected: {} | {}", + gpu_info.join(", "), + detailed_info.join(" | ") + ) } else { "No GPU support detected".to_string() }; @@ -134,7 +147,10 @@ fn detect_gpu_capabilities() -> (bool, String) { } /// Create WhisperContext with automatic GPU/CPU fallback -fn create_whisper_context_with_auto_fallback(model_path: &str, prefer_gpu: bool) -> Result<(WhisperContext, String), Box> { +fn create_whisper_context_with_auto_fallback( + model_path: &str, + prefer_gpu: bool, +) -> Result<(WhisperContext, String), Box> { let (has_gpu, gpu_info) = detect_gpu_capabilities(); println!("🔍 {}", gpu_info); @@ -143,35 +159,29 @@ fn create_whisper_context_with_auto_fallback(model_path: &str, prefer_gpu: bool) println!("🚀 GPU support detected, attempting to enable GPU acceleration..."); // Check which GPU features are compiled in - #[cfg(feature = "cuda")] - { - let mut ctx_params = WhisperContextParameters::default(); - ctx_params.use_gpu(true); + let mut ctx_params = WhisperContextParameters::default(); + ctx_params.use_gpu(true); - match WhisperContext::new_with_params(model_path, ctx_params) { - Ok(ctx) => { - println!("✅ GPU mode enabled successfully (CUDA acceleration)"); - return Ok((ctx, "GPU (CUDA)".to_string())); - } - Err(e) => { - println!("⚠️ GPU mode failed: {}", e); - println!("💡 Possible reasons:"); - println!(" - Incompatible CUDA runtime version"); - println!(" - Insufficient GPU memory"); - println!(" - Model file incompatible with GPU version"); - println!("🔄 Auto-fallback to CPU mode"); - } + match WhisperContext::new_with_params(model_path, ctx_params) { + Ok(ctx) => { + println!("✅ GPU mode enabled successfully (CUDA acceleration)"); + return Ok((ctx, "GPU (CUDA)".to_string())); } - } - #[cfg(not(feature = "cuda"))] - { - if prefer_gpu && has_gpu { - println!("⚡ GPU hardware detected, but CUDA feature not enabled"); - println!("💡 To enable GPU acceleration on Windows:"); - println!(" Add 'cuda' feature to build"); - println!("🔄 Using CPU mode"); + Err(e) => { + println!("⚠️ GPU mode failed: {}", e); + println!("💡 Possible reasons:"); + println!(" - Incompatible CUDA runtime version"); + println!(" - Insufficient GPU memory"); + println!(" - Model file incompatible with GPU version"); + println!("🔄 Auto-fallback to CPU mode"); } } + if prefer_gpu && has_gpu { + println!("⚡ GPU hardware detected, but CUDA feature not enabled"); + println!("💡 To enable GPU acceleration on Windows:"); + println!(" Add 'cuda' feature to build"); + println!("🔄 Using CPU mode"); + } } else if prefer_gpu && !has_gpu { println!("🔧 GPU acceleration requested but no GPU support detected, using CPU mode"); } else { @@ -184,4 +194,4 @@ fn create_whisper_context_with_auto_fallback(model_path: &str, prefer_gpu: bool) let ctx = WhisperContext::new_with_params(model_path, ctx_params)?; println!("✅ CPU mode enabled successfully"); Ok((ctx, "CPU".to_string())) -} \ No newline at end of file +} From b2ef73e8fde7f7c2915eb91e4dbd91418ca95b8e Mon Sep 17 00:00:00 2001 From: Blinko Date: Tue, 30 Sep 2025 12:50:32 +0800 Subject: [PATCH 07/18] fix: upload timeout --- server/routerExpress/file/upload.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/routerExpress/file/upload.ts b/server/routerExpress/file/upload.ts index 7f9bd48c3..98ae6b582 100644 --- a/server/routerExpress/file/upload.ts +++ b/server/routerExpress/file/upload.ts @@ -67,6 +67,9 @@ router.options('/', cors({ */ router.post('/', async (req, res) => { try { + req.setTimeout(0); // 0 = no timeout + res.setTimeout(0); // 0 = no timeout + const token = await getTokenFromRequest(req); if (!token) { return res.status(401).json({ error: "Unauthorized" }); @@ -77,7 +80,9 @@ router.post('/', async (req, res) => { return res.status(400).json({ error: "Content type must be multipart/form-data" }); } - const bb = busboy({ headers: req.headers }); + const bb = busboy({ + headers: req.headers + }); let fileInfo: { stream: PassThrough | null, From c3cf5bc5796eae844fd4c74478cd1fd961dbe30c Mon Sep 17 00:00:00 2001 From: Blinko Date: Tue, 30 Sep 2025 15:49:13 +0800 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=A8feat:=20implement=20CUDA/CPU=20f?= =?UTF-8?q?eature=20separation=20with=20conditional=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add feature flags for whisper-cuda and whisper-cpu in Cargo.toml - Create separate tauri.cuda.conf.json with Blinko(CUDA) product name - Implement conditional compilation for voice module based on features - Add is_cuda_available command for frontend CUDA detection - Update GitHub Actions workflows for matrix builds (cpu/cuda variants) - Add CUDA availability check in voice settings UI - Configure conditional voice module loading in lib.rs and setup.rs --- .claude/settings.local.json | 4 +- .github/workflows/app-release.yml | 33 ++++-- .github/workflows/windows-test-release.yml | 32 ++++- app/src-tauri/Cargo.toml | 8 +- app/src-tauri/src/desktop/setup.rs | 98 +++++++++------ app/src-tauri/src/lib.rs | 22 ++-- app/src-tauri/src/voice/commands.rs | 11 ++ app/src-tauri/tauri.cuda.conf.json | 112 ++++++++++++++++++ .../BlinkoSettings/VoiceSetting.tsx | 12 +- 9 files changed, 264 insertions(+), 68 deletions(-) create mode 100644 app/src-tauri/tauri.cuda.conf.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 07d387ba9..14c972f60 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,7 +36,9 @@ "Bash(find:*)", "WebFetch(domain:crates.io)", "WebFetch(domain:lib.rs)", - "Bash(cat:*)" + "Bash(cat:*)", + "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\tauri.cuda.conf.json\")", + "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\wix-template.wxs\")" ], "deny": [], "ask": [] diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 7f1b845c8..0136a408a 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -117,8 +117,14 @@ jobs: args: '--target x86_64-apple-darwin' - platform: 'ubuntu-22.04' # Linux Platform args: '' - - platform: 'windows-latest' # Windows Platform + - platform: 'windows-latest' # Windows Platform (CPU) args: '' + features: '--features whisper-cpu' + variant: 'cpu' + - platform: 'windows-latest' # Windows Platform (CUDA) + args: '' + features: '--features whisper-cuda' + variant: 'cuda' runs-on: ${{ matrix.platform }} steps: @@ -132,8 +138,8 @@ jobs: name: tauri-config path: app/src-tauri/ - - name: Install CUDA Toolkit (Windows) - if: matrix.platform == 'windows-latest' + - name: Install CUDA Toolkit (Windows CUDA) + if: matrix.platform == 'windows-latest' && matrix.variant == 'cuda' uses: Jimver/cuda-toolkit@v0.2.24 id: cuda-toolkit with: @@ -141,8 +147,8 @@ jobs: method: 'network' sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev", "visual_studio_integration", "thrust" ]' - - name: Set CUDA environment variables (Windows) - if: matrix.platform == 'windows-latest' + - name: Set CUDA environment variables (Windows CUDA) + if: matrix.platform == 'windows-latest' && matrix.variant == 'cuda' run: | echo "Installed cuda version is: ${{steps.cuda-toolkit.outputs.cuda}}" echo "Cuda install location: ${{steps.cuda-toolkit.outputs.CUDA_PATH}}" @@ -150,8 +156,9 @@ jobs: echo "${{steps.cuda-toolkit.outputs.CUDA_PATH}}\bin" >> $GITHUB_PATH nvcc -V - - name: Fix CUDA Visual Studio Integration (Windows) - if: matrix.platform == 'windows-latest' + + - name: Fix CUDA Visual Studio Integration (Windows CUDA) + if: matrix.platform == 'windows-latest' && matrix.variant == 'cuda' run: | $cudaPath = "${{steps.cuda-toolkit.outputs.CUDA_PATH}}" echo "CUDA Path: $cudaPath" @@ -253,6 +260,14 @@ jobs: bun install cd app && bun install + + # Copy CUDA config for CUDA builds (with installer checks) + - name: Use CUDA config (Windows CUDA) + if: matrix.platform == 'windows-latest' && matrix.variant == 'cuda' + run: | + Copy-Item "app\src-tauri\tauri.cuda.conf.json" "app\src-tauri\tauri.conf.json" -Force + echo "Using CUDA configuration with installer CUDA detection" + # Using official Tauri Action to build and publish - name: Build and Publish Desktop App uses: tauri-apps/tauri-action@v0 @@ -263,9 +278,9 @@ jobs: with: projectPath: 'app' tauriScript: '../node_modules/.bin/tauri' - args: ${{ matrix.args }} + args: ${{ matrix.args }} ${{ matrix.features || '' }} tagName: ${{ needs.set-version.outputs.version }} - releaseName: Blinko ${{ needs.set-version.outputs.version }} + releaseName: Blinko ${{ needs.set-version.outputs.version }}${{ matrix.variant && format(' ({0})', matrix.variant) || '' }} releaseBody: "Under construction, full changelog will be updated after build completion..." releaseDraft: false prerelease: false diff --git a/.github/workflows/windows-test-release.yml b/.github/workflows/windows-test-release.yml index a1e6fdc98..8a1f8dc64 100644 --- a/.github/workflows/windows-test-release.yml +++ b/.github/workflows/windows-test-release.yml @@ -96,6 +96,14 @@ jobs: needs: [set-version, update-version] permissions: contents: write + strategy: + fail-fast: false + matrix: + include: + - variant: 'cpu' + features: '--features whisper-cpu' + - variant: 'cuda' + features: '--features whisper-cuda' runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -108,7 +116,8 @@ jobs: name: tauri-config path: app/src-tauri/ - - name: Install CUDA Toolkit (Windows) + - name: Install CUDA Toolkit (Windows CUDA) + if: matrix.variant == 'cuda' uses: Jimver/cuda-toolkit@v0.2.24 id: cuda-toolkit with: @@ -116,7 +125,8 @@ jobs: method: 'network' sub-packages: '[ "nvcc", "cudart", "cublas", "cublas_dev", "curand", "curand_dev", "nvrtc", "nvrtc_dev", "visual_studio_integration", "thrust" ]' - - name: Set CUDA environment variables (Windows) + - name: Set CUDA environment variables (Windows CUDA) + if: matrix.variant == 'cuda' run: | echo "Installed cuda version is: ${{steps.cuda-toolkit.outputs.cuda}}" echo "Cuda install location: ${{steps.cuda-toolkit.outputs.CUDA_PATH}}" @@ -124,7 +134,9 @@ jobs: echo "${{steps.cuda-toolkit.outputs.CUDA_PATH}}\bin" >> $GITHUB_PATH nvcc -V + - name: Fix CUDA Visual Studio Integration + if: matrix.variant == 'cuda' run: | $cudaPath = "${{steps.cuda-toolkit.outputs.CUDA_PATH}}" echo "CUDA Path: $cudaPath" @@ -218,12 +230,20 @@ jobs: bun install cd app && bun install + + # Copy CUDA config for CUDA builds (with installer checks) + - name: Use CUDA config (Windows CUDA) + if: matrix.variant == 'cuda' + run: | + Copy-Item "app\src-tauri\tauri.cuda.conf.json" "app\src-tauri\tauri.conf.json" -Force + echo "Using CUDA configuration with installer CUDA detection" + # Build Windows App (without publishing) - - name: Build Windows App + - name: Build Windows App (${{ matrix.variant }}) run: | cd app - echo "Starting Windows build..." - ../node_modules/.bin/tauri build --no-bundle - echo "Windows build completed successfully!" + echo "Starting Windows ${{ matrix.variant }} build..." + ../node_modules/.bin/tauri build --no-bundle ${{ matrix.features }} + echo "Windows ${{ matrix.variant }} build completed successfully!" echo "Build artifacts location: src-tauri/target/release/" Get-ChildItem src-tauri/target/release/ \ No newline at end of file diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 064777a0b..23a3ee226 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -42,12 +42,18 @@ enigo = "0.3" rdev = "0.3" sys-locale = "0.3" +[features] +default = ["whisper-cpu"] +whisper-cuda = ["dep:whisper-rs", "whisper-rs/cuda"] +whisper-cpu = ["dep:whisper-rs"] + [target.'cfg(target_os = "windows")'.dependencies] tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } -whisper-rs = { version = "0.15.1", features = ["cuda"] } cpal = "0.16.0" crossbeam-channel = "0.5" parking_lot = "0.12" +whisper-rs = { version = "0.15.1", optional = true } + [target.'cfg(target_os = "macos")'.dependencies] macos-accessibility-client = "0.0.1" diff --git a/app/src-tauri/src/desktop/setup.rs b/app/src-tauri/src/desktop/setup.rs index 4a113390d..1b4409af2 100644 --- a/app/src-tauri/src/desktop/setup.rs +++ b/app/src-tauri/src/desktop/setup.rs @@ -6,7 +6,7 @@ use tauri::{AppHandle, Manager}; use tauri_plugin_global_shortcut::{ShortcutState, ShortcutEvent}; use crate::desktop::{HotkeyConfig, setup_system_tray, toggle_quicknote_window, toggle_quickai_window, toggle_quicktool_window, restore_main_window_state, setup_window_state_monitoring}; -#[cfg(target_os = "windows")] +#[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] use crate::voice::{load_voice_config, VoiceProcessor, VOICE_STATE}; pub fn setup_app(app: &mut tauri::App) -> Result<(), Box> { @@ -63,50 +63,72 @@ pub fn setup_app(app: &mut tauri::App) -> Result<(), Box> // Initialize voice recognition if enabled (Windows only, non-blocking) #[cfg(target_os = "windows")] { - let voice_config = load_voice_config(&app_handle); - if voice_config.enabled && std::path::Path::new(&voice_config.model_path).exists() { - println!("🎤 Voice recognition enabled, initializing in background..."); + // Check if whisper-rs is available (either CUDA or CPU version) + #[cfg(any(feature = "whisper-cuda", feature = "whisper-cpu"))] + { + let voice_config = load_voice_config(&app_handle); - // Clone voice config for the background thread - let voice_config_clone = voice_config.clone(); + // Print build configuration info + #[cfg(feature = "whisper-cuda")] + println!("🚀 Voice recognition built with CUDA acceleration support"); + #[cfg(all(feature = "whisper-cpu", not(feature = "whisper-cuda")))] + println!("🖥️ Voice recognition built with CPU-only support"); - // Use std::thread::spawn instead of tokio::spawn to avoid runtime issues - std::thread::spawn(move || { - match VoiceProcessor::new(voice_config_clone.clone()) { - Ok(processor) => { - println!("✅ Voice recognition initialized successfully"); + if voice_config.enabled && std::path::Path::new(&voice_config.model_path).exists() { + println!("🎤 Voice recognition enabled, initializing in background..."); - // Update global state - { - let mut state = VOICE_STATE.lock(); - state.processor = Some(std::sync::Arc::new(processor)); - state.is_initialized = true; - *state.config.lock() = voice_config_clone.clone(); - } + // Clone voice config for the background thread + let voice_config_clone = voice_config.clone(); + + // Use std::thread::spawn instead of tokio::spawn to avoid runtime issues + std::thread::spawn(move || { + match VoiceProcessor::new(voice_config_clone.clone()) { + Ok(processor) => { + #[cfg(feature = "whisper-cuda")] + println!("✅ Voice recognition initialized successfully with CUDA support"); + #[cfg(all(feature = "whisper-cpu", not(feature = "whisper-cuda")))] + println!("✅ Voice recognition initialized successfully with CPU support"); + + // Update global state + { + let mut state = VOICE_STATE.lock(); + state.processor = Some(std::sync::Arc::new(processor)); + state.is_initialized = true; + *state.config.lock() = voice_config_clone.clone(); + } - // Start the voice recognition service - if let Some(ref processor) = VOICE_STATE.lock().processor { - if let Err(e) = processor.start() { - eprintln!("❌ Failed to start voice recognition: {}", e); - println!("💡 Voice recognition failed to start, but application will continue normally"); - } else { - println!("🚀 Voice recognition service started successfully"); + // Start the voice recognition service + if let Some(ref processor) = VOICE_STATE.lock().processor { + if let Err(e) = processor.start() { + eprintln!("❌ Failed to start voice recognition: {}", e); + println!("💡 Voice recognition failed to start, but application will continue normally"); + } else { + println!("🚀 Voice recognition service started successfully"); + } } } + Err(e) => { + eprintln!("❌ Failed to initialize voice recognition: {}", e); + #[cfg(feature = "whisper-cuda")] + println!("💡 If you see CUDA errors, try the CPU-only version or install CUDA toolkit"); + println!("💡 Please check model path and configuration in voice settings"); + println!("💡 Application will continue to run normally without voice recognition"); + } } - Err(e) => { - eprintln!("❌ Failed to initialize voice recognition: {}", e); - println!("💡 Please check model path and configuration in voice settings"); - println!("💡 Application will continue to run normally without voice recognition"); - } - } - }); - } else if voice_config.enabled && !std::path::Path::new(&voice_config.model_path).exists() { - println!("⚠️ Voice recognition enabled but model file not found: {}", voice_config.model_path); - println!("💡 Please download a model file and update the path in voice settings"); - println!("💡 Application will continue to run normally without voice recognition"); - } else { - println!("🔇 Voice recognition disabled in configuration"); + }); + } else if voice_config.enabled && !std::path::Path::new(&voice_config.model_path).exists() { + println!("⚠️ Voice recognition enabled but model file not found: {}", voice_config.model_path); + println!("💡 Please download a model file and update the path in voice settings"); + println!("💡 Application will continue to run normally without voice recognition"); + } else { + println!("🔇 Voice recognition disabled in configuration"); + } + } + + // If whisper-rs is not available in this build + #[cfg(not(any(feature = "whisper-cuda", feature = "whisper-cpu")))] + { + println!("🔇 Voice recognition not available in this build (no whisper features enabled)"); } } #[cfg(not(target_os = "windows"))] diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index c1e42b414..051cee1d1 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -1,10 +1,10 @@ #[cfg(not(any(target_os = "android", target_os = "ios")))] mod desktop; -#[cfg(target_os = "windows")] +#[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] mod voice; #[cfg(not(any(target_os = "android", target_os = "ios")))] use desktop::*; -#[cfg(target_os = "windows")] +#[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] use voice::*; use tauri::Manager; @@ -77,19 +77,21 @@ pub fn run() { show_quicktool, set_desktop_theme, set_desktop_colors, - // Voice recognition commands (Windows only) - #[cfg(target_os = "windows")] + // Voice recognition commands (Windows only with whisper features) + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] get_voice_config, - #[cfg(target_os = "windows")] + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] save_voice_config_cmd, - #[cfg(target_os = "windows")] + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] initialize_voice_recognition, - #[cfg(target_os = "windows")] + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] start_voice_recognition, - #[cfg(target_os = "windows")] + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] stop_voice_recognition, - #[cfg(target_os = "windows")] - get_voice_status + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] + get_voice_status, + #[cfg(all(target_os = "windows", any(feature = "whisper-cuda", feature = "whisper-cpu")))] + is_cuda_available ]) .setup(|app| { #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/app/src-tauri/src/voice/commands.rs b/app/src-tauri/src/voice/commands.rs index 3f83d6987..93660bc4b 100644 --- a/app/src-tauri/src/voice/commands.rs +++ b/app/src-tauri/src/voice/commands.rs @@ -152,3 +152,14 @@ pub async fn get_voice_status() -> Result { }) } +/// Check if CUDA support is available in this build +#[tauri::command] +pub async fn is_cuda_available() -> Result { + // Return true if built with CUDA feature, false otherwise + #[cfg(feature = "whisper-cuda")] + return Ok(true); + + #[cfg(not(feature = "whisper-cuda"))] + return Ok(false); +} + diff --git a/app/src-tauri/tauri.cuda.conf.json b/app/src-tauri/tauri.cuda.conf.json new file mode 100644 index 000000000..100793ea0 --- /dev/null +++ b/app/src-tauri/tauri.cuda.conf.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Blinko(CUDA)", + "version": "1.6.3", + "identifier": "com.blinko.app", + "build": { + "beforeDevCommand": "bun run dev", + "devUrl": "http://localhost:1111", + "beforeBuildCommand": "bun run build:no-pwa", + "frontendDist": "../../dist/public" + }, + "app": { + "withGlobalTauri": true, + "security": { + "csp": null + }, + "windows": [ + { + "title": "Blinko", + "width": 1920, + "height": 1080, + "minWidth": 600, + "minHeight": 300, + "fullscreen": false, + "resizable": true, + "focus": true, + "hiddenTitle": true, + "decorations": true, + "visible": false + }, + { + "label": "quicknote", + "title": "Quick Note", + "width": 600, + "height": 125, + "maxHeight": 600, + "fullscreen": false, + "resizable": false, + "focus": true, + "center": true, + "visible": false, + "alwaysOnTop": true, + "skipTaskbar": true, + "titleBarStyle": "Overlay", + "transparent": true, + "hiddenTitle": true, + "decorations": false, + "shadow": true, + "url": "/quicknote" + }, + { + "label": "quickai", + "title": "Quick AI", + "width": 600, + "height": 125, + "maxHeight": 600, + "fullscreen": false, + "resizable": false, + "focus": true, + "center": true, + "visible": false, + "alwaysOnTop": true, + "skipTaskbar": true, + "titleBarStyle": "Overlay", + "transparent": true, + "hiddenTitle": true, + "decorations": false, + "shadow": true, + "url": "/quickai" + }, + { + "label": "quicktool", + "title": "Quick Tool", + "width": 190, + "height": 34, + "fullscreen": false, + "resizable": false, + "focus": false, + "center": false, + "visible": false, + "alwaysOnTop": true, + "skipTaskbar": true, + "titleBarStyle": "Overlay", + "transparent": true, + "hiddenTitle": true, + "decorations": false, + "shadow": true, + "url": "/quicktool" + } + ] + }, + "bundle": { + "createUpdaterArtifacts": true, + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENBNzZBMzZDRTUxQUM4RjcKUldUM3lCcmxiS04yeXYyOGZ0RVVBbE42WDMxUXFiQTI0R3RqT0ZBbkZEcFFRNlZTWVhwZzlwRmkK", + "endpoints": [ + "https://github.com/blinkospace/blinko/releases/latest/download/latest.json" + ] + } + } +} \ No newline at end of file diff --git a/app/src/components/BlinkoSettings/VoiceSetting.tsx b/app/src/components/BlinkoSettings/VoiceSetting.tsx index 7a46c370b..4fa478581 100644 --- a/app/src/components/BlinkoSettings/VoiceSetting.tsx +++ b/app/src/components/BlinkoSettings/VoiceSetting.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { Item, ItemWithTooltip } from './Item'; import { useEffect, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { isDesktop, isInTauri } from '@/lib/tauriHelper'; +import { isDesktop, isInTauri, isWindows } from '@/lib/tauriHelper'; import { CollapsibleCard } from '../Common/CollapsibleCard'; import { ToastPlugin } from '@/store/module/Toast/Toast'; import { VoiceRecognitionConfig } from '@/../../shared/lib/types'; @@ -46,6 +46,7 @@ export const VoiceSetting = observer(() => { const [voiceConfig, setVoiceConfig] = useState(null); const [voiceStatus, setVoiceStatus] = useState(null); const [isVoiceInitializing, setIsVoiceInitializing] = useState(false); + const [isCudaAvailable, setIsCudaAvailable] = useState(false); // Check if running on Tauri desktop const isTauriDesktop = isInTauri() && isDesktop(); @@ -61,6 +62,11 @@ export const VoiceSetting = observer(() => { // Load voice status const status = await invoke('get_voice_status'); setVoiceStatus(status); + + // Check CUDA availability + const cudaAvailable = await invoke('is_cuda_available'); + setIsCudaAvailable(cudaAvailable); + console.log('CUDA support available:', cudaAvailable); } catch (error) { console.error('Failed to load voice config:', error); } @@ -263,8 +269,8 @@ export const VoiceSetting = observer(() => { type="col" /> - {/* CUDA acceleration switch (Windows only) */} - {typeof window !== 'undefined' && navigator.platform.indexOf('Win') > -1 && ( + {/* CUDA acceleration switch (Windows only, when CUDA feature is available) */} + {isWindows() && isCudaAvailable && ( From 53c9f3fa2e907f92149773794d06b8aafafeefa9 Mon Sep 17 00:00:00 2001 From: Blinko Date: Tue, 30 Sep 2025 17:38:00 +0800 Subject: [PATCH 09/18] chore: update doc --- README.md | 11 +++++++++++ README.zh-CN.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/README.md b/README.md index 1ef848e53..94c260627 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,17 @@ Blinko is an AI-powered card note-taking project. Designed for individuals who w - 🔓**Open for Collaboration** :As an open-source project, Blinko invites contributions from the community. All code is transparent and available on GitHub, fostering a spirit of collaboration and constant improvement. +## 🎤 Offline Voice Recognition (Windows) + +The Windows desktop version supports offline voice recognition powered by Whisper, allowing you to convert speech to text without internet connectivity. + +### Available Versions +- **Blinko.exe** - CPU-only version for all systems +- **Blinko(CUDA).exe** - GPU-accelerated version for NVIDIA graphics cards + - **Requires [CUDA Toolkit](https://developer.nvidia.com/cuda-downloads) to be installed, otherwise installation will fail due to missing runtime environment** + - Provides significantly faster transcription performance + - Requires manual download of Whisper models from [Hugging Face](https://huggingface.co/ggerganov/whisper.cpp/tree/main) + ## 📦Start with Docker in seconds ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 64405ac93..6eb8ca49d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -48,6 +48,17 @@ Blinko 是一个AI驱动的卡片笔记项目,专为那些想要快速捕捉 - 🔓**开放协作**:作为开源项目,Blinko 欢迎社区贡献。所有代码都在 GitHub 上公开透明,培养协作和持续改进的精神。 +## 🎤 离线语音识别 (Windows) + +Windows 桌面版支持基于 Whisper 的离线语音识别功能,让您无需网络连接即可将语音转换为文字。 + +### 可用版本 +- **Blinko.exe** - CPU版本,适用于所有系统 +- **Blinko(CUDA).exe** - GPU加速版本,专为NVIDIA显卡优化 + - **必须先安装 [CUDA工具包](https://developer.nvidia.com/cuda-downloads),否则会因缺少运行环境导致安装报错** + - 提供显著更快的转录性能 + - 需要手动从 [Hugging Face](https://huggingface.co/ggerganov/whisper.cpp/tree/main) 下载 Whisper 模型 + ## 🤖 AI 模型支持 ### OpenAI - 支持 OpenAI API From 9c5a8be7cec8769e12625124af666df5d1fb4e70 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 30 Sep 2025 09:43:49 +0000 Subject: [PATCH 10/18] chore: Update version to 1.6.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fbf9a0670..8236b5b47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blinko-monorepo", - "version": "1.6.2", + "version": "1.6.3", "private": true, "packageManager": "bun@1.2.8", "workspaces": [ From 0ceae35ef2835a30d11b5081f6cd285b4b6eaf9c Mon Sep 17 00:00:00 2001 From: Felitendo Date: Tue, 30 Sep 2025 14:33:06 +0200 Subject: [PATCH 11/18] feat: added notes dragging --- app/src/components/BlinkoCard/index.tsx | 33 ++++++- app/src/pages/index.tsx | 94 ++++++++++++++++--- bun.lock | 11 ++- package.json | 3 + .../migration.sql | 2 + prisma/schema.prisma | 1 + server/routerTrpc/note.ts | 32 ++++++- 7 files changed, 151 insertions(+), 25 deletions(-) create mode 100644 prisma/migrations/20250930000000_add_sort_order_to_notes/migration.sql diff --git a/app/src/components/BlinkoCard/index.tsx b/app/src/components/BlinkoCard/index.tsx index 25d5ba3ac..3fda649ce 100644 --- a/app/src/components/BlinkoCard/index.tsx +++ b/app/src/components/BlinkoCard/index.tsx @@ -20,6 +20,8 @@ import { AvatarAccount, SimpleCommentList } from "./commentButton"; import { PluginApiStore } from "@/store/plugin/pluginApiStore"; import { PluginRender } from "@/store/plugin/pluginRender"; import { useLocation } from "react-router-dom"; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; export type BlinkoItem = Note & { @@ -38,15 +40,34 @@ interface BlinkoCardProps { glassEffect?: boolean; withoutHoverAnimation?: boolean; withoutBoxShadow?: boolean; + isDraggable?: boolean; } -export const BlinkoCard = observer(({ blinkoItem, account, isShareMode = false, glassEffect = false, forceBlog = false, withoutBoxShadow = false, withoutHoverAnimation = false, className, defaultExpanded = false }: BlinkoCardProps) => { +export const BlinkoCard = observer(({ blinkoItem, account, isShareMode = false, glassEffect = false, forceBlog = false, withoutBoxShadow = false, withoutHoverAnimation = false, className, defaultExpanded = false, isDraggable = false }: BlinkoCardProps) => { const isPc = useMediaQuery('(min-width: 768px)'); const blinko = RootStore.Get(BlinkoStore); const pluginApi = RootStore.Get(PluginApiStore); const [isExpanded, setIsExpanded] = useState(defaultExpanded); const { pathname } = useLocation(); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id: blinkoItem.id!, + disabled: !isDraggable || isExpanded || blinko.isMultiSelectMode + }); + + const style = isDraggable ? { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + } : {}; + useHistoryBack({ state: isExpanded, onStateChange: () => setIsExpanded(false), @@ -101,6 +122,9 @@ export const BlinkoCard = observer(({ blinkoItem, account, isShareMode = false, {(() => { const cardContent = (
diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index 57d57d644..6de4ef302 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -10,10 +10,13 @@ import { useMediaQuery } from 'usehooks-ts'; import { BlinkoAddButton } from '@/components/BlinkoAddButton'; import { LoadingAndEmpty } from '@/components/Common/LoadingAndEmpty'; import { useSearchParams } from 'react-router-dom'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import dayjs from '@/lib/dayjs'; import { NoteType } from '@shared/lib/types'; import { Icon } from '@/components/Common/Iconify/icons'; +import { DndContext, DragEndEvent, MouseSensor, TouchSensor, useSensor, useSensors, closestCenter } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; +import { api } from '@/lib/trpc'; interface TodoGroup { displayDate: string; @@ -32,6 +35,7 @@ const Home = observer(() => { const isArchivedView = searchParams.get('path') === 'archived'; const isTrashView = searchParams.get('path') === 'trash'; const isAllView = searchParams.get('path') === 'all'; + const [localNotes, setLocalNotes] = useState([]); const currentListState = useMemo(() => { if (isNotesView) { @@ -49,6 +53,55 @@ const Home = observer(() => { } }, [isNotesView, isTodoView, isArchivedView, isTrashView, isAllView, blinko]); + // Update local notes when the list changes + useMemo(() => { + if (currentListState.value) { + setLocalNotes(currentListState.value); + } + }, [currentListState.value]); + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }) + ); + + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + + if (!over || active.id === over.id) { + return; + } + + const oldIndex = localNotes.findIndex((note) => note.id === active.id); + const newIndex = localNotes.findIndex((note) => note.id === over.id); + + const newNotes = arrayMove(localNotes, oldIndex, newIndex); + setLocalNotes(newNotes); + + // Update sort order in database + const updates = newNotes.map((note, index) => ({ + id: note.id, + sortOrder: index, + })); + + try { + await api.note.updateNotesOrder.mutate({ updates }); + } catch (error) { + console.error('Failed to update notes order:', error); + // Revert on error + setLocalNotes(currentListState.value || []); + } + }; + const store = RootStore.Local(() => ({ editorHeight: 30, get showEditor() { @@ -150,20 +203,31 @@ const Home = observer(() => {
) : ( <> - - { - currentListState?.value?.map(i => { - return - }) - } - + + note.id)} + strategy={verticalListSortingStrategy} + > + + { + localNotes?.map(i => { + return + }) + } + + + )} diff --git a/bun.lock b/bun.lock index 0b398604b..ad01ae9f7 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "": { "name": "blinko-monorepo", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@tauri-apps/plugin-process": "^2.2.1", "@tauri-apps/plugin-updater": "^2.7.1", "dotenv": "^16.5.0", @@ -4164,7 +4167,7 @@ "rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="], - "rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], + "rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], @@ -5478,8 +5481,6 @@ "@lobehub/ui/lucide-react": ["lucide-react@0.543.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-fpVfuOQO0V3HBaOA1stIiP/A2fPCXHIleRZL16Mx3HmjTYwNSbimhnFBygs2CAfU1geexMX5ItUcWBGUaqw5CA=="], - "@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], - "@lobehub/ui/url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], "@lobehub/ui/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -5898,6 +5899,8 @@ "antd/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], + "antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], "antd-style/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], @@ -6876,8 +6879,6 @@ "@lobehub/ui/framer-motion/motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], - "@lobehub/ui/rc-collapse/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - "@mastra/core/pino-pretty/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], "@mastra/core/pino-pretty/sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], diff --git a/package.json b/package.json index fbf9a0670..1359dfd87 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "node": ">=20.0.0" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@tauri-apps/plugin-process": "^2.2.1", "@tauri-apps/plugin-updater": "^2.7.1", "dotenv": "^16.5.0", diff --git a/prisma/migrations/20250930000000_add_sort_order_to_notes/migration.sql b/prisma/migrations/20250930000000_add_sort_order_to_notes/migration.sql new file mode 100644 index 000000000..f1749584c --- /dev/null +++ b/prisma/migrations/20250930000000_add_sort_order_to_notes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "notes" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 52bfd41a3..38bac3f85 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -82,6 +82,7 @@ model notes { shareViewCount Int? @default(0) metadata Json? @db.Json accountId Int? + sortOrder Int @default(0) createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) attachments attachments[] diff --git a/server/routerTrpc/note.ts b/server/routerTrpc/note.ts index f1b59ae55..2a8cac5b6 100644 --- a/server/routerTrpc/note.ts +++ b/server/routerTrpc/note.ts @@ -195,7 +195,7 @@ export const noteRouter = router({ const notes = await prisma.notes.findMany({ where, - orderBy: [{ isTop: 'desc' }, timeOrderBy], + orderBy: [{ isTop: 'desc' }, { sortOrder: 'asc' }, timeOrderBy], skip: (page - 1) * size, take: size, include: { @@ -1766,6 +1766,36 @@ export const noteRouter = router({ internalShares: undefined, // Remove this field from the response }))); }), + updateNotesOrder: authProcedure + .meta({ openapi: { method: 'POST', path: '/v1/note/update-order', summary: 'Update notes order', protect: true, tags: ['Note'] } }) + .input( + z.object({ + updates: z.array( + z.object({ + id: z.number(), + sortOrder: z.number(), + }), + ), + }), + ) + .output(z.object({ success: z.boolean() })) + .mutation(async function ({ input, ctx }) { + const { updates } = input; + + await Promise.all( + updates.map(({ id, sortOrder }) => + prisma.notes.updateMany({ + where: { + id, + accountId: Number(ctx.id), + }, + data: { sortOrder }, + }), + ), + ); + + return { success: true }; + }), }); let insertNoteReference = async ({ fromNoteId, toNoteId, accountId }) => { From 29196281f7d6c9175abb50ac39861a1882182960 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Tue, 30 Sep 2025 15:39:59 +0200 Subject: [PATCH 12/18] fix: dragging notes not staying in position --- app/src/pages/index.tsx | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index 6de4ef302..30a6cf251 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -10,7 +10,7 @@ import { useMediaQuery } from 'usehooks-ts'; import { BlinkoAddButton } from '@/components/BlinkoAddButton'; import { LoadingAndEmpty } from '@/components/Common/LoadingAndEmpty'; import { useSearchParams } from 'react-router-dom'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect, useRef } from 'react'; import dayjs from '@/lib/dayjs'; import { NoteType } from '@shared/lib/types'; import { Icon } from '@/components/Common/Iconify/icons'; @@ -36,6 +36,7 @@ const Home = observer(() => { const isTrashView = searchParams.get('path') === 'trash'; const isAllView = searchParams.get('path') === 'all'; const [localNotes, setLocalNotes] = useState([]); + const isDraggingRef = useRef(false); const currentListState = useMemo(() => { if (isNotesView) { @@ -53,10 +54,12 @@ const Home = observer(() => { } }, [isNotesView, isTodoView, isArchivedView, isTrashView, isAllView, blinko]); - // Update local notes when the list changes - useMemo(() => { - if (currentListState.value) { - setLocalNotes(currentListState.value); + // Update local notes when the list changes (but not during drag operations) + useEffect(() => { + if (currentListState.value && !isDraggingRef.current) { + // Sort by sortOrder to maintain the correct order from the database + const sortedNotes = [...currentListState.value].sort((a, b) => a.sortOrder - b.sortOrder); + setLocalNotes(sortedNotes); } }, [currentListState.value]); @@ -81,24 +84,40 @@ const Home = observer(() => { return; } + isDraggingRef.current = true; + const oldIndex = localNotes.findIndex((note) => note.id === active.id); const newIndex = localNotes.findIndex((note) => note.id === over.id); const newNotes = arrayMove(localNotes, oldIndex, newIndex); - setLocalNotes(newNotes); - // Update sort order in database - const updates = newNotes.map((note, index) => ({ - id: note.id, + // Optimistically update the UI with new sortOrder values + const updatedNotes = newNotes.map((note, index) => ({ + ...note, sortOrder: index, })); + setLocalNotes(updatedNotes); + + // Prepare updates for the server + const updates = updatedNotes.map((note) => ({ + id: note.id, + sortOrder: note.sortOrder, + })); try { - await api.note.updateNotesOrder.mutate({ updates }); + await api.notes.updateNotesOrder.mutate({ updates }); + // Wait a bit longer for the store to refresh with updated data + setTimeout(() => { + isDraggingRef.current = false; + }, 2000); } catch (error) { console.error('Failed to update notes order:', error); + isDraggingRef.current = false; // Revert on error - setLocalNotes(currentListState.value || []); + if (currentListState.value) { + const sortedNotes = [...currentListState.value].sort((a, b) => a.sortOrder - b.sortOrder); + setLocalNotes(sortedNotes); + } } }; From 6b64748bedc0ebb45849def817398122799220ea Mon Sep 17 00:00:00 2001 From: Tengxiang Yang Date: Thu, 2 Oct 2025 15:33:08 -0400 Subject: [PATCH 13/18] =?UTF-8?q?Docs:=20update=20French=20translation=20o?= =?UTF-8?q?f=20todo=20to=20T=C3=A2ches=20instead=20of=20Procuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/public/locales/fr/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/public/locales/fr/translation.json b/app/public/locales/fr/translation.json index 06b82a441..0d0241982 100644 --- a/app/public/locales/fr/translation.json +++ b/app/public/locales/fr/translation.json @@ -637,7 +637,7 @@ "import-from-markdown": "Importer à partir d'un fichier Markdown", "import-from-markdown-tip": "Importer à partir d'un simple fichier .md ou d'une archive .zip contenant des fichiers .md", "not-a-markdown-or-zip-file": "Ce n'est pas un fichier Markdown ou zip. Veuillez sélectionner un fichier .md ou .zip.", - "todo": "Procuration", + "todo": "Tâches", "restore": "Rétablissement", "complete": "terminé", "today": "Aujourd'hui", From e031a304131fcc866ff30d589a0bbe86b8a243c2 Mon Sep 17 00:00:00 2001 From: Blinko Date: Thu, 9 Oct 2025 22:26:04 +0800 Subject: [PATCH 14/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20implemen?= =?UTF-8?q?t=20long-press=20drag=20activation=20for=20better=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 250ms delay activation constraint for mouse and touch sensors - Consolidate all drag logic into dedicated useDragCard hook - Remove drag functionality from BlinkoCard component - Implement stable drag with placeholder and insertion line - Add multi-language support for dragging state - Fix text selection conflicts with long-press approach - Improve masonry layout stability during drag operations --- .claude/settings.local.json | 8 +- app/public/locales/ar/translation.json | 3 +- app/public/locales/de/translation.json | 3 +- app/public/locales/en/translation.json | 3 +- app/public/locales/es/translation.json | 3 +- app/public/locales/fr/translation.json | 3 +- app/public/locales/ka/translation.json | 3 +- app/public/locales/kab/translation.json | 3 +- app/public/locales/ko/translation.json | 3 +- app/public/locales/nl/translation.json | 3 +- app/public/locales/pl/translation.json | 3 +- app/public/locales/pt/translation.json | 3 +- app/public/locales/ru/translation.json | 3 +- app/public/locales/tr/translation.json | 3 +- app/public/locales/zh-TW/translation.json | 3 +- app/public/locales/zh/translation.json | 3 +- app/src/components/BlinkoCard/index.tsx | 29 +--- app/src/hooks/useDragCard.tsx | 177 ++++++++++++++++++++++ app/src/pages/index.tsx | 137 ++++++----------- shared/lib/prismaZodType.ts | 1 + 20 files changed, 262 insertions(+), 135 deletions(-) create mode 100644 app/src/hooks/useDragCard.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 14c972f60..e02eba810 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -38,9 +38,13 @@ "WebFetch(domain:lib.rs)", "Bash(cat:*)", "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\tauri.cuda.conf.json\")", - "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\wix-template.wxs\")" + "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\wix-template.wxs\")", + "Bash(del \"E:\\code\\blinko\\app\\src\\hooks\\useDragCard.ts\")", + "Bash(gh pr list)", + "Bash(gh pr view 933)", + "Bash(gh pr comment 933 --body \"$(cat <<''EOF''\nThank you so much for implementing this drag and drop feature! This is a really valuable addition to Blinko that many users have been looking for.\n\nI really appreciate the work you put into this. As we were testing and refining the implementation, we encountered a couple of challenges that we''ve been working to resolve:\n\n1. **Text Selection Conflict**: The original drag functionality interfered with users'' ability to select and copy text from notes. We''ve modified this to use a long-press approach (250ms delay) so users can still select text normally, and only activate drag mode when they press and hold.\n\n2. **Masonry Layout Issues During Drag**: We found that the waterfall/masonry layout had some stability problems during drag operations. We''ve been working on fixes to ensure the layout remains stable and provides good visual feedback during the drag process.\n\nThese changes help maintain the balance between the new drag functionality and the existing user experience for text interaction. The foundation you built here is excellent, and these refinements will make it even better for users.\n\nThanks again for your great work on this feature! 🎉\nEOF\n)\")" ], "deny": [], "ask": [] } -} \ No newline at end of file +} diff --git a/app/public/locales/ar/translation.json b/app/public/locales/ar/translation.json index c83013dc4..d4b518b4a 100644 --- a/app/public/locales/ar/translation.json +++ b/app/public/locales/ar/translation.json @@ -734,5 +734,6 @@ "voice-recognition-hotkey": "مفتاح الإدخال الصوتي السريع", "local-voice-recognition": "التفريغ الصوتي المحلي", "cuda-acceleration": "تسريع CUDA", - "voice-tip": "اضغط مع الاستمرار على مفتاح الاختصار للتحدث، لإجراء التحويل الصوتي إلى نص، وعند تحريره سيتم إدراج المحتوى المُحول في صندوق النص." + "voice-tip": "اضغط مع الاستمرار على مفتاح الاختصار للتحدث، لإجراء التحويل الصوتي إلى نص، وعند تحريره سيتم إدراج المحتوى المُحول في صندوق النص.", + "dragging": "جاري السحب..." } diff --git a/app/public/locales/de/translation.json b/app/public/locales/de/translation.json index 41ab93458..46cee2003 100644 --- a/app/public/locales/de/translation.json +++ b/app/public/locales/de/translation.json @@ -734,5 +734,6 @@ "voice-recognition-hotkey": "Spracheingabe-Hotkey", "local-voice-recognition": "Lokale Sprachtranskription", "cuda-acceleration": "CUDA-Beschleunigung", - "voice-tip": "Halten Sie die Schnelltaste gedrückt, um zu sprechen und eine Sprachtranskription durchzuführen. Wenn Sie loslassen, wird der transkribierte Inhalt in das Textfeld eingefügt." + "voice-tip": "Halten Sie die Schnelltaste gedrückt, um zu sprechen und eine Sprachtranskription durchzuführen. Wenn Sie loslassen, wird der transkribierte Inhalt in das Textfeld eingefügt.", + "dragging": "Wird gerade gezogen..." } diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json index f7f89d12c..8908334c1 100644 --- a/app/public/locales/en/translation.json +++ b/app/public/locales/en/translation.json @@ -791,5 +791,6 @@ "audio-tips": "Press and hold the shortcut key to enter, release to insert into the text box.", "local-voice-recognition": "Local Voice Recognition", "cuda-acceleration": "CUDA Acceleration", - "voice-tip": "Press and hold the shortcut key to speak for voice transcription, and release it to insert the transcribed content into the text box." + "voice-tip": "Press and hold the shortcut key to speak for voice transcription, and release it to insert the transcribed content into the text box.", + "dragging": "Dragging..." } diff --git a/app/public/locales/es/translation.json b/app/public/locales/es/translation.json index abcef41ec..5eb8a3448 100644 --- a/app/public/locales/es/translation.json +++ b/app/public/locales/es/translation.json @@ -736,5 +736,6 @@ "voice-recognition-hotkey": "Teclas de acceso rápido para entrada de voz", "local-voice-recognition": "Transcripción de voz local", "cuda-acceleration": "Aceleración CUDA", - "voice-tip": "Mantén presionada la tecla de acceso rápido para hablar y realizar la transcripción por voz. Al soltar, el contenido transcrito se insertará en el cuadro de texto." + "voice-tip": "Mantén presionada la tecla de acceso rápido para hablar y realizar la transcripción por voz. Al soltar, el contenido transcrito se insertará en el cuadro de texto.", + "dragging": "Arrastrando..." } diff --git a/app/public/locales/fr/translation.json b/app/public/locales/fr/translation.json index 0d0241982..2e243c393 100644 --- a/app/public/locales/fr/translation.json +++ b/app/public/locales/fr/translation.json @@ -736,5 +736,6 @@ "voice-recognition-hotkey": "Raccourci de saisie vocale", "local-voice-recognition": "Transcription vocale locale", "cuda-acceleration": "Accélération CUDA", - "voice-tip": "Maintenez enfoncé le raccourci pour parler et effectuer la transcription vocale. Lorsque vous relâchez, le contenu transcrit sera inséré dans la zone de texte." + "voice-tip": "Maintenez enfoncé le raccourci pour parler et effectuer la transcription vocale. Lorsque vous relâchez, le contenu transcrit sera inséré dans la zone de texte.", + "dragging": "En train de glisser..." } diff --git a/app/public/locales/ka/translation.json b/app/public/locales/ka/translation.json index e621dbd07..9058f822c 100644 --- a/app/public/locales/ka/translation.json +++ b/app/public/locales/ka/translation.json @@ -695,5 +695,6 @@ "voice-recognition-hotkey": "ხმოვანი შეყვანის სოკო клавиши", "local-voice-recognition": "ადგილობრივი ხმოვან տրանսկրիփցիя", "cuda-acceleration": "CUDA აჩქარება", - "voice-tip": "დააჭირე სწრაფ ღილა­­­­­кис қოшოбаs, խօսեք, տեքсті аудио транскрипцияга айналдыру üçün, босатқанда транскрипцияланған мәтіні тексt кутисине енгизиледи." + "voice-tip": "დააჭირე სწრაფ ღილა­­­­­кис қოшოбаs, խօսեք, տեքсті аудио транскрипцияга айналдыру üçün, босатқанда транскрипцияланған мәтіні тексt кутисине енгизиледи.", + "dragging": "მიმდინარეობს გადაწო..." } diff --git a/app/public/locales/kab/translation.json b/app/public/locales/kab/translation.json index 7717e10d7..ce5aac887 100644 --- a/app/public/locales/kab/translation.json +++ b/app/public/locales/kab/translation.json @@ -767,5 +767,6 @@ "voice-recognition-hotkey": "Tansaḍt n usnulfu n tujjut", "local-voice-recognition": "Tutlayt tamezgant n temda", "cuda-acceleration": "CUDA asersi", - "voice-tip": "Sserḥed tamara n umernu ara ad tettalkem, ad d-yeqqen amagrad nniṣnen, ma yella tebdaḍ ad tt-inserteɣ-d aɣbalu yettwakcem deg udrum n tefyar." + "voice-tip": "Sserḥed tamara n umernu ara ad tettalkem, ad d-yeqqen amagrad nniṣnen, ma yella tebdaḍ ad tt-inserteɣ-d aɣbalu yettwakcem deg udrum n tefyar.", + "dragging": "Dduklen..." } diff --git a/app/public/locales/ko/translation.json b/app/public/locales/ko/translation.json index 8b65ffd18..358e7f14c 100644 --- a/app/public/locales/ko/translation.json +++ b/app/public/locales/ko/translation.json @@ -731,5 +731,6 @@ "voice-recognition-hotkey": "음성 입력 단축키", "local-voice-recognition": "현지 음성 전사", "cuda-acceleration": "CUDA 가속", - "voice-tip": "길게 누르고 단축키를 말하면 음성을 텍스트로 변환하고, 손을 떼면 변환된 내용이 텍스트 상자에 삽입됩니다." + "voice-tip": "길게 누르고 단축키를 말하면 음성을 텍스트로 변환하고, 손을 떼면 변환된 내용이 텍스트 상자에 삽입됩니다.", + "dragging": "드래그 중..." } diff --git a/app/public/locales/nl/translation.json b/app/public/locales/nl/translation.json index 688240039..be43b56af 100644 --- a/app/public/locales/nl/translation.json +++ b/app/public/locales/nl/translation.json @@ -743,5 +743,6 @@ "voice-recognition-hotkey": "Spraakopname sneltoets", "local-voice-recognition": "lokale spraaktranscriptie", "cuda-acceleration": "CUDA-versnelling", - "voice-tip": "Houd de sneltoets ingedrukt om te spreken en voer spraak-naar-tekst uit. Wanneer je loslaat, wordt de getranscribeerde inhoud in het tekstvak ingevoegd." + "voice-tip": "Houd de sneltoets ingedrukt om te spreken en voer spraak-naar-tekst uit. Wanneer je loslaat, wordt de getranscribeerde inhoud in het tekstvak ingevoegd.", + "dragging": "Bezig met slepen..." } diff --git a/app/public/locales/pl/translation.json b/app/public/locales/pl/translation.json index 486d77f31..286c2b42a 100644 --- a/app/public/locales/pl/translation.json +++ b/app/public/locales/pl/translation.json @@ -727,5 +727,6 @@ "voice-recognition-hotkey": "Skrót klawiszowy do wprowadzania głosowego", "local-voice-recognition": "Lokalne przepisywanie głosu", "cuda-acceleration": "CUDA przyspieszenie", - "voice-tip": "Przytrzymaj długo skrót klawiszowy, aby mówić i przeprowadzić transkrypcję głosu. Po zwolnieniu zostanie wstawiona zawartość transkrypcji do pola tekstowego." + "voice-tip": "Przytrzymaj długo skrót klawiszowy, aby mówić i przeprowadzić transkrypcję głosu. Po zwolnieniu zostanie wstawiona zawartość transkrypcji do pola tekstowego.", + "dragging": "Trwa przeciąganie..." } diff --git a/app/public/locales/pt/translation.json b/app/public/locales/pt/translation.json index f96c588df..9cc9cbbb6 100644 --- a/app/public/locales/pt/translation.json +++ b/app/public/locales/pt/translation.json @@ -729,5 +729,6 @@ "audio-tips": "Pressione e segure a tecla de atalho para gravar, solte para inserir na caixa de texto.", "voice-recognition-hotkey": "Teclas de atalho para entrada de voz", "local-voice-recognition": "Transcrição de voz local", - "cuda-acceleration": "Aceleração CUDA" + "cuda-acceleration": "Aceleração CUDA", + "dragging": "Arrastando..." } diff --git a/app/public/locales/ru/translation.json b/app/public/locales/ru/translation.json index f1f7c9556..285602e1e 100644 --- a/app/public/locales/ru/translation.json +++ b/app/public/locales/ru/translation.json @@ -729,5 +729,6 @@ "voice-recognition-hotkey": "Горячие клавиши для ввода голосом", "local-voice-recognition": "Местная голосовая транскрипция", "cuda-acceleration": "CUDA ускорение", - "voice-tip": "Долгое нажатие на горячую клавишу для разговора, выполнение голосовой транскрипции, после отпускания содержимое транскрипции будет вставлено в текстовое поле." + "voice-tip": "Долгое нажатие на горячую клавишу для разговора, выполнение голосовой транскрипции, после отпускания содержимое транскрипции будет вставлено в текстовое поле.", + "dragging": "Перетаскивается..." } diff --git a/app/public/locales/tr/translation.json b/app/public/locales/tr/translation.json index 8a5e9408c..df3bdbaa8 100644 --- a/app/public/locales/tr/translation.json +++ b/app/public/locales/tr/translation.json @@ -736,5 +736,6 @@ "voice-recognition-hotkey": "Ses kaydı kısayol tuşu", "local-voice-recognition": "Yerel ses dökümü", "cuda-acceleration": "CUDA hızlandırma", - "voice-tip": "Kısayol tuşuna uzun basarak konuşun, sesli diktat yapın, bıraktığınızda diktat içeriği metin kutusuna eklenecektir." + "voice-tip": "Kısayol tuşuna uzun basarak konuşun, sesli diktat yapın, bıraktığınızda diktat içeriği metin kutusuna eklenecektir.", + "dragging": "Sürükleniyor..." } diff --git a/app/public/locales/zh-TW/translation.json b/app/public/locales/zh-TW/translation.json index 146e59bc5..a4dc456ec 100644 --- a/app/public/locales/zh-TW/translation.json +++ b/app/public/locales/zh-TW/translation.json @@ -736,5 +736,6 @@ "voice-recognition-hotkey": "語音錄入快捷鍵", "local-voice-recognition": "本地語音轉寫", "cuda-acceleration": "CUDA加速", - "voice-tip": "長按快捷鍵說話,進行語音轉寫,鬆開的時候會將轉寫內容插入到文字框中。" + "voice-tip": "長按快捷鍵說話,進行語音轉寫,鬆開的時候會將轉寫內容插入到文字框中。", + "dragging": "正在拖曳..." } diff --git a/app/public/locales/zh/translation.json b/app/public/locales/zh/translation.json index b1f82368e..a8cbbd1fc 100644 --- a/app/public/locales/zh/translation.json +++ b/app/public/locales/zh/translation.json @@ -808,5 +808,6 @@ "audio-tips": "长按快捷键录入,松开插入文本框中", "local-voice-recognition": "本地语音转写", "cuda-acceleration": "CUDA加速", - "voice-tip": "长按快捷键说话,进行语音转写,松开的时候会将转写内容插入到文本框中" + "voice-tip": "长按快捷键说话,进行语音转写,松开的时候会将转写内容插入到文本框中", + "dragging": "正在拖拽..." } diff --git a/app/src/components/BlinkoCard/index.tsx b/app/src/components/BlinkoCard/index.tsx index 3fda649ce..3b7bc2b63 100644 --- a/app/src/components/BlinkoCard/index.tsx +++ b/app/src/components/BlinkoCard/index.tsx @@ -20,8 +20,6 @@ import { AvatarAccount, SimpleCommentList } from "./commentButton"; import { PluginApiStore } from "@/store/plugin/pluginApiStore"; import { PluginRender } from "@/store/plugin/pluginRender"; import { useLocation } from "react-router-dom"; -import { useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; export type BlinkoItem = Note & { @@ -40,34 +38,15 @@ interface BlinkoCardProps { glassEffect?: boolean; withoutHoverAnimation?: boolean; withoutBoxShadow?: boolean; - isDraggable?: boolean; } -export const BlinkoCard = observer(({ blinkoItem, account, isShareMode = false, glassEffect = false, forceBlog = false, withoutBoxShadow = false, withoutHoverAnimation = false, className, defaultExpanded = false, isDraggable = false }: BlinkoCardProps) => { +export const BlinkoCard = observer(({ blinkoItem, account, isShareMode = false, glassEffect = false, forceBlog = false, withoutBoxShadow = false, withoutHoverAnimation = false, className, defaultExpanded = false }: BlinkoCardProps) => { const isPc = useMediaQuery('(min-width: 768px)'); const blinko = RootStore.Get(BlinkoStore); const pluginApi = RootStore.Get(PluginApiStore); const [isExpanded, setIsExpanded] = useState(defaultExpanded); const { pathname } = useLocation(); - - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ - id: blinkoItem.id!, - disabled: !isDraggable || isExpanded || blinko.isMultiSelectMode - }); - - const style = isDraggable ? { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - } : {}; - + useHistoryBack({ state: isExpanded, onStateChange: () => setIsExpanded(false), @@ -122,9 +101,6 @@ export const BlinkoCard = observer(({ blinkoItem, account, isShareMode = false, {(() => { const cardContent = (
diff --git a/app/src/hooks/useDragCard.tsx b/app/src/hooks/useDragCard.tsx new file mode 100644 index 000000000..8f6118975 --- /dev/null +++ b/app/src/hooks/useDragCard.tsx @@ -0,0 +1,177 @@ +import { useState, useRef, useEffect } from 'react'; +import { DragEndEvent, DragStartEvent, MouseSensor, TouchSensor, useSensor, useSensors, closestCenter, useDroppable, useDraggable } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { api } from '@/lib/trpc'; +import { BlinkoCard } from '@/components/BlinkoCard'; +import { useTranslation } from 'react-i18next'; + +interface UseDragCardProps { + notes: any[] | undefined; + onNotesUpdate?: (notes: any[]) => void; + activeId: number | null; + setActiveId: (id: number | null) => void; + insertPosition: number | null; + setInsertPosition: (position: number | null) => void; +} + +export const useDragCard = ({ notes, onNotesUpdate, activeId, setActiveId, insertPosition, setInsertPosition }: UseDragCardProps) => { + const [localNotes, setLocalNotes] = useState([]); + const isDraggingRef = useRef(false); + + // Update local notes when the list changes (but not during drag operations) + useEffect(() => { + if (notes && !isDraggingRef.current) { + // Sort by sortOrder to maintain the correct order from the database + const sortedNotes = [...notes].sort((a, b) => a.sortOrder - b.sortOrder); + setLocalNotes(sortedNotes); + onNotesUpdate?.(sortedNotes); + } + }, [notes]); + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }) + ); + + const handleDragStart = (event: any) => { + setActiveId(event.active.id as number); + }; + + const handleDragEnd = (event: any) => { + const { active, over } = event; + + if (over) { + const dropTargetId = over.id.toString(); + const dragItemId = active.id; + + // Extract the note ID from the droppable ID + const targetNoteId = parseInt(dropTargetId.replace('drop-', '')); + + if (dragItemId !== targetNoteId) { + const oldIndex = localNotes.findIndex((note) => note.id === dragItemId); + const newIndex = localNotes.findIndex((note) => note.id === targetNoteId); + + if (oldIndex !== -1 && newIndex !== -1) { + const newNotes = [...localNotes]; + const [movedNote] = newNotes.splice(oldIndex, 1); + newNotes.splice(newIndex, 0, movedNote); + + // Update sortOrder + const updatedNotes = newNotes.map((note, index) => ({ + ...note, + sortOrder: index, + })); + + // Call the original hook's update logic + setLocalNotes(updatedNotes); + + // Update server + const updates = updatedNotes.map((note) => ({ + id: note.id, + sortOrder: note.sortOrder, + })); + + api.notes.updateNotesOrder.mutate({ updates }); + } + } + } + + setActiveId(null); + setInsertPosition(null); + }; + + const handleDragOver = (event: any) => { + const { over } = event; + if (over) { + const targetNoteId = parseInt(over.id.toString().replace('drop-', '')); + setInsertPosition(targetNoteId); + } + }; + + return { + localNotes, + sensors, + setLocalNotes, + isDraggingRef, + handleDragStart, + handleDragEnd, + handleDragOver + }; +}; + +interface DraggableBlinkoCardProps { + blinkoItem: any; + showInsertLine?: boolean; + insertPosition?: 'top' | 'bottom'; +} + +export const DraggableBlinkoCard = ({ blinkoItem, showInsertLine, insertPosition }: DraggableBlinkoCardProps) => { + const { setNodeRef: setDroppableRef, isOver } = useDroppable({ + id: `drop-${blinkoItem.id}`, + }); + const { t } = useTranslation() + + const { + attributes, + listeners, + setNodeRef: setDraggableRef, + transform, + isDragging, + } = useDraggable({ + id: blinkoItem.id, + }); + + const dragStyle = { + transform: CSS.Transform.toString(transform), + }; + + return ( +
+ {showInsertLine && insertPosition === 'top' && ( +
+ )} + + {/* Droppable area - always visible, shows placeholder when dragging */} +
+ {isDragging ? ( +
+
+
{t('dragging')}
+
+
+ ) : ( + // Draggable area - long press to drag using dnd-kit's activationConstraint +
+ +
+ )} +
+ + {showInsertLine && insertPosition === 'bottom' && ( +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index 30a6cf251..b5eda185a 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -10,13 +10,12 @@ import { useMediaQuery } from 'usehooks-ts'; import { BlinkoAddButton } from '@/components/BlinkoAddButton'; import { LoadingAndEmpty } from '@/components/Common/LoadingAndEmpty'; import { useSearchParams } from 'react-router-dom'; -import { useMemo, useState, useEffect, useRef } from 'react'; +import { useMemo, useState } from 'react'; import dayjs from '@/lib/dayjs'; import { NoteType } from '@shared/lib/types'; import { Icon } from '@/components/Common/Iconify/icons'; -import { DndContext, DragEndEvent, MouseSensor, TouchSensor, useSensor, useSensors, closestCenter } from '@dnd-kit/core'; -import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; -import { api } from '@/lib/trpc'; +import { DndContext, closestCenter, DragOverlay } from '@dnd-kit/core'; +import { useDragCard, DraggableBlinkoCard } from '@/hooks/useDragCard'; interface TodoGroup { displayDate: string; @@ -35,8 +34,8 @@ const Home = observer(() => { const isArchivedView = searchParams.get('path') === 'archived'; const isTrashView = searchParams.get('path') === 'trash'; const isAllView = searchParams.get('path') === 'all'; - const [localNotes, setLocalNotes] = useState([]); - const isDraggingRef = useRef(false); + const [activeId, setActiveId] = useState(null); + const [insertPosition, setInsertPosition] = useState(null); const currentListState = useMemo(() => { if (isNotesView) { @@ -54,72 +53,14 @@ const Home = observer(() => { } }, [isNotesView, isTodoView, isArchivedView, isTrashView, isAllView, blinko]); - // Update local notes when the list changes (but not during drag operations) - useEffect(() => { - if (currentListState.value && !isDraggingRef.current) { - // Sort by sortOrder to maintain the correct order from the database - const sortedNotes = [...currentListState.value].sort((a, b) => a.sortOrder - b.sortOrder); - setLocalNotes(sortedNotes); - } - }, [currentListState.value]); - - const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 10, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 250, - tolerance: 5, - }, - }) - ); - - const handleDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - - if (!over || active.id === over.id) { - return; - } - - isDraggingRef.current = true; - - const oldIndex = localNotes.findIndex((note) => note.id === active.id); - const newIndex = localNotes.findIndex((note) => note.id === over.id); - - const newNotes = arrayMove(localNotes, oldIndex, newIndex); - - // Optimistically update the UI with new sortOrder values - const updatedNotes = newNotes.map((note, index) => ({ - ...note, - sortOrder: index, - })); - setLocalNotes(updatedNotes); - - // Prepare updates for the server - const updates = updatedNotes.map((note) => ({ - id: note.id, - sortOrder: note.sortOrder, - })); - - try { - await api.notes.updateNotesOrder.mutate({ updates }); - // Wait a bit longer for the store to refresh with updated data - setTimeout(() => { - isDraggingRef.current = false; - }, 2000); - } catch (error) { - console.error('Failed to update notes order:', error); - isDraggingRef.current = false; - // Revert on error - if (currentListState.value) { - const sortedNotes = [...currentListState.value].sort((a, b) => a.sortOrder - b.sortOrder); - setLocalNotes(sortedNotes); - } - } - }; + // Use drag card hook only for non-todo views + const { localNotes, sensors, setLocalNotes, handleDragStart, handleDragEnd, handleDragOver } = useDragCard({ + notes: isTodoView ? [] : currentListState.value, + activeId, + setActiveId, + insertPosition, + setInsertPosition + }); const store = RootStore.Local(() => ({ editorHeight: 30, @@ -225,27 +166,41 @@ const Home = observer(() => { - note.id)} - strategy={verticalListSortingStrategy} - > - - { - localNotes?.map(i => { - return - }) - } - - + + { + localNotes?.map((i, index) => { + const showInsertLine = insertPosition === i.id && activeId !== i.id; + return ( + + ); + }) + } + + + {activeId ? ( +
+ n.id === activeId)} + /> +
+ ) : null} +
)} diff --git a/shared/lib/prismaZodType.ts b/shared/lib/prismaZodType.ts index 51b543219..01b7b809d 100644 --- a/shared/lib/prismaZodType.ts +++ b/shared/lib/prismaZodType.ts @@ -78,6 +78,7 @@ export const notesSchema = z.object({ shareMaxView: z.number().nullable().optional(), shareViewCount: z.number().nullable().optional(), metadata: z.any(), + sortOrder: z.number().nullable().optional(), accountId: z.union([z.number().int(), z.null()]), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), From 3fb2f22c2bb308447cae11fd4f7dbd257a76210a Mon Sep 17 00:00:00 2001 From: Blinko Date: Thu, 9 Oct 2025 22:39:05 +0800 Subject: [PATCH 15/18] chore: update --- .claude/settings.local.json | 50 ------------------------------------- .gitignore | 1 + 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index e02eba810..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(where bun)", - "Bash(bun:*)", - "Bash(echo $ANDROID_HOME)", - "Bash(echo $JAVA_HOME)", - "Bash(copy:*)", - "WebSearch", - "Bash(.gradlew.bat:*)", - "Bash(powershell:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(git status:*)", - "Bash(git diff:*)", - "mcp__context7__resolve-library-id", - "mcp__context7__get-library-docs", - "WebFetch(domain:v2.tauri.app)", - "WebFetch(domain:docs.rs)", - "Bash(cargo check:*)", - "Bash(npm run dev:*)", - "WebFetch(domain:github.com)", - "Bash(gh issue list:*)", - "Bash(gh issue view:*)", - "Bash(gh issue comment:*)", - "Bash(gh repo view:*)", - "Bash(gh api:*)", - "Bash(rg:*)", - "Bash(del \"E:\\code\\blinko\\app\\src-tauri\\src\\desktop\\autostart.rs\")", - "Bash(del \"E:\\code\\blinko\\app\\src\\pages\\test-ai-settings.tsx\")", - "Bash(del \"E:\\code\\blinko\\app\\src\\components\\BlinkoSettings\\AiSetting\\ProviderModal.tsx\")", - "WebFetch(domain:icons.lobehub.com)", - "mcp__ide__getDiagnostics", - "WebFetch(domain:raw.githubusercontent.com)", - "Read(//e/code/mastra/packages/mcp/src/server/**)", - "Bash(find:*)", - "WebFetch(domain:crates.io)", - "WebFetch(domain:lib.rs)", - "Bash(cat:*)", - "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\tauri.cuda.conf.json\")", - "Bash(del \"e:\\code\\blinko\\app\\src-tauri\\wix-template.wxs\")", - "Bash(del \"E:\\code\\blinko\\app\\src\\hooks\\useDragCard.ts\")", - "Bash(gh pr list)", - "Bash(gh pr view 933)", - "Bash(gh pr comment 933 --body \"$(cat <<''EOF''\nThank you so much for implementing this drag and drop feature! This is a really valuable addition to Blinko that many users have been looking for.\n\nI really appreciate the work you put into this. As we were testing and refining the implementation, we encountered a couple of challenges that we''ve been working to resolve:\n\n1. **Text Selection Conflict**: The original drag functionality interfered with users'' ability to select and copy text from notes. We''ve modified this to use a long-press approach (250ms delay) so users can still select text normally, and only activate drag mode when they press and hold.\n\n2. **Masonry Layout Issues During Drag**: We found that the waterfall/masonry layout had some stability problems during drag operations. We''ve been working on fixes to ensure the layout remains stable and provides good visual feedback during the drag process.\n\nThese changes help maintain the balance between the new drag functionality and the existing user experience for text interaction. The foundation you built here is excellent, and these refinements will make it even better for users.\n\nThanks again for your great work on this feature! 🎉\nEOF\n)\")" - ], - "deny": [], - "ask": [] - } -} diff --git a/.gitignore b/.gitignore index 802d025df..c47ad3b98 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ keystore.properties dev-dist .claudeconfig .claude/* +.claude/settings.local.json target \ No newline at end of file From d54e5c8c6c8245e372c1eccd4c5c445ac1b20dcc Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 9 Oct 2025 15:09:59 +0000 Subject: [PATCH 16/18] chore: Update version to 1.6.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 946437dfe..4a3c70ac9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blinko-monorepo", - "version": "1.6.3", + "version": "1.6.4", "private": true, "packageManager": "bun@1.2.8", "workspaces": [ From b757f70d888cc030fed3698990be538db47cc61d Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 9 Oct 2025 15:11:42 +0000 Subject: [PATCH 17/18] [ci skip] Update version to 1.6.4 --- app/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 0ac125b8e..325f1424b 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Blinko", - "version": "1.6.3", + "version": "1.6.4", "identifier": "com.blinko.app", "build": { "beforeDevCommand": "bun run dev", From b431f4fd36ccf991392a1bca22ef98324470bcc0 Mon Sep 17 00:00:00 2001 From: weichao7 Date: Wed, 15 Oct 2025 10:53:14 +0800 Subject: [PATCH 18/18] fix: resolve memory leak issues in Tauri app navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed infinite loop in BlinkoStore.updateTicker useEffect by resetting ticker after refresh - Added memory optimization to clear unused PromisePageState instances during navigation - Enhanced clear() method to properly clean up all list states and reset variables - Added proactive list clearing in useQuery() to prevent memory accumulation - Improved refreshData() method to clear unused lists before loading new ones This resolves the memory leak that occurred when switching between flash notes, notes, and analytics sections in the Tauri macOS app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/src/store/blinkoStore.tsx | 77 +++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/app/src/store/blinkoStore.tsx b/app/src/store/blinkoStore.tsx index 13398468f..8e3fa77fc 100644 --- a/app/src/store/blinkoStore.tsx +++ b/app/src/store/blinkoStore.tsx @@ -539,23 +539,49 @@ export class BlinkoStore implements Store { async refreshData() { this.tagList.call() - + const currentPath = new URLSearchParams(window.location.search).get('path'); - + + // Clear unused lists to prevent memory accumulation if (currentPath === 'notes') { + this.todoList.clear(); + this.archivedList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.noteOnlyList.resetAndCall({}); } else if (currentPath === 'todo') { + this.noteOnlyList.clear(); + this.archivedList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.todoList.resetAndCall({}); } else if (currentPath === 'archived') { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.archivedList.resetAndCall({}); } else if (currentPath === 'trash') { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.archivedList.clear(); + this.blinkoList.clear(); this.trashList.resetAndCall({}); } else if (currentPath === 'all') { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.archivedList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.noteList.resetAndCall({}); } else { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.archivedList.clear(); + this.trashList.clear(); this.blinkoList.resetAndCall({}); } - + this.config.call() this.dailyReviewNoteList.call() } @@ -563,6 +589,20 @@ export class BlinkoStore implements Store { private clear() { this.createContentStorage.clear() this.editContentStorage.clear() + // Clear all list states to prevent memory accumulation + this.blinkoList.clear() + this.noteOnlyList.clear() + this.todoList.clear() + this.archivedList.clear() + this.trashList.clear() + this.noteList.clear() + this.offlineNoteStorage.clear() + this.curMultiSelectIds = [] + this.isMultiSelectMode = false + this.curSelectedNote = null + this.noteContent = '' + this.searchText = '' + this.updateTicker = 0 } use() { @@ -577,18 +617,21 @@ export class BlinkoStore implements Store { if (this.updateTicker == 0) return console.log('updateTicker', this.updateTicker) this.refreshData() + // Reset updateTicker to prevent infinite loop + this.updateTicker = 0 }, [this.updateTicker]) } useQuery() { const [searchParams] = useSearchParams(); const location = useLocation(); + useEffect(() => { const tagId = searchParams.get('tagId'); if (tagId && Number(tagId) === this.noteListFilterConfig.tagId) { return; } - + const withoutTag = searchParams.get('withoutTag'); const withFile = searchParams.get('withFile'); const withLink = searchParams.get('withLink'); @@ -596,6 +639,7 @@ export class BlinkoStore implements Store { const hasTodo = searchParams.get('hasTodo'); const path = searchParams.get('path'); + // Reset filter config this.noteListFilterConfig.type = NoteType.BLINKO this.noteTypeDefault = NoteType.BLINKO this.noteListFilterConfig.tagId = null @@ -609,24 +653,49 @@ export class BlinkoStore implements Store { this.noteListFilterConfig.isShare = null this.noteListFilterConfig.hasTodo = false + // Clear unused lists before loading new ones to prevent memory accumulation if (path == 'notes') { + this.todoList.clear(); + this.archivedList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.noteListFilterConfig.type = NoteType.NOTE this.noteOnlyList.resetAndCall({}); } else if (path == 'todo') { + this.noteOnlyList.clear(); + this.archivedList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.noteListFilterConfig.type = NoteType.TODO this.todoList.resetAndCall({}); } else if (path == 'all') { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.archivedList.clear(); + this.trashList.clear(); this.noteListFilterConfig.type = -1 this.noteList.resetAndCall({}); } else if (path == 'archived') { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.trashList.clear(); + this.blinkoList.clear(); this.noteListFilterConfig.type = -1 this.noteListFilterConfig.isArchived = true this.archivedList.resetAndCall({}); } else if (path == 'trash') { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.archivedList.clear(); + this.blinkoList.clear(); this.noteListFilterConfig.type = -1 this.noteListFilterConfig.isRecycle = true this.trashList.resetAndCall({}); } else { + this.noteOnlyList.clear(); + this.todoList.clear(); + this.archivedList.clear(); + this.trashList.clear(); this.blinkoList.resetAndCall({}); }