Merge branch 'main' into backup-restore-system

This commit is contained in:
Sergey Kurdin 2025-06-12 11:51:51 -04:00
commit 1aa8e5d28c
48 changed files with 4354 additions and 1338 deletions

View File

@ -0,0 +1,5 @@
---
'pastebar-app-ui': patch
---
Added option to disable capturing and storing images from clipboard

View File

@ -0,0 +1,5 @@
---
'pastebar-app-ui': minor
---
Added custom data location to store application data in a folder of your choice instead of the default location.

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ safelist.txt
clipboard-images/**/*
clip-images/**/*
src-tauri/http-cacache/**/*
pastebar_settings.yaml
node_modules
packages/dist-ui/**/*

131
custom_db_location_plan.md Normal file
View File

@ -0,0 +1,131 @@
# Plan: Implement Custom Data Location Feature
This document outlines the plan to implement the feature allowing users to specify a custom location for the PasteBar application's data.
## 1. Goals
* Allow users to specify a custom parent directory for application data via the settings UI.
* The application will create and manage a `pastebar-data` subdirectory within the user-specified location.
* This `pastebar-data` directory will contain the database file (`pastebar-db.data`), the `clip-images` folder, and the `clipboard-images` folder.
* Provide options to either **move** the existing data, **copy** it, or **use the new location without moving/copying**.
* Ensure the application uses the data from the new location after a restart.
* Handle potential errors gracefully and inform the user.
* Update the application state and backend configuration accordingly.
## 2. Backend (Rust - `src-tauri`)
### 2.1. Configuration (`user_settings_service.rs`)
* The `UserConfig` struct's `custom_db_path: Option<String>` will now be repurposed to store the path to the **user-selected parent directory**. The application logic will handle appending the `/pastebar-data/` segment. This requires no change to the struct itself, only to how the path is interpreted.
### 2.2. Path Logic (`db.rs` and new helpers)
* We will introduce new helper functions to consistently resolve data paths, whether default or custom.
* `get_data_dir() -> PathBuf`: This will be the core helper. It checks for a `custom_db_path` in the settings.
* If present, it returns `PathBuf::from(custom_path)`.
* If `None`, it returns the default application data directory.
* `get_db_path()`: This function will be refactored to use `get_data_dir().join("pastebar-db.data")`.
* `get_clip_images_dir()`: A new helper that returns `get_data_dir().join("clip-images")`.
* `get_clipboard_images_dir()`: A new helper that returns `get_data_dir().join("clipboard-images")`.
### 2.3. New & Updated Tauri Commands (`user_settings_command.rs`)
* **`cmd_validate_custom_db_path(path: String) -> Result<bool, String>`**
* **No change in purpose.** This command will still check if the user-selected directory is valid and writable.
* **`cmd_check_custom_data_path(path: String) -> Result<PathStatus, String>`**
* A new command to check the status of a selected directory. It returns one of the following statuses: `Empty`, `NotEmpty`, `IsPastebarDataAndNotEmpty`.
* **`cmd_set_and_relocate_data(new_parent_dir_path: String, operation: String) -> Result<String, String>`** (renamed from `set_and_relocate_db`)
* `new_parent_dir_path`: The new directory path selected by the user.
* `operation`: Either "move", "copy", or "none".
* **Updated Steps:**
1. Get the source paths:
* Current DB file path.
* Current `clip-images` directory path.
* Current `clipboard-images` directory path.
2. Define the new data directory: `let new_data_dir = Path::new(&new_parent_dir_path);`
3. Create the new data directory: `fs::create_dir_all(&new_data_dir)`.
4. Perform file/directory operations for each item (DB file, `clip-images` dir, `clipboard-images` dir):
* If "move": `fs::rename(source, destination)`.
* If "copy": `fs::copy` for the file, and a recursive copy function for the directories.
* If "none", do nothing.
* Handle cases where source items might not exist (e.g., `clip-images` folder hasn't been created yet) by skipping them gracefully.
5. If successful, call `user_settings_service::set_custom_db_path(&new_parent_dir_path)`.
6. Return a success or error message.
* **`cmd_revert_to_default_data_location() -> Result<String, String>`** (renamed and simplified)
* **Updated Steps:**
1. Call `user_settings_service::remove_custom_db_path()` to clear the custom data path setting.
2. Return a success message indicating the setting has been removed.
## 3. Frontend (React)
* The UI has been updated to refer to "Custom Application Data Location" instead of "Custom Database Location".
* A third radio button option, "Use new location", has been added.
* The `handleBrowse` function now calls the `cmd_check_custom_data_path` command to analyze the selected directory and prompts the user accordingly.
* The `settingsStore.ts` has been updated to support the "none" operation.
## 4. User Interaction Flow (Mermaid Diagram)
```mermaid
graph TD
subgraph User Flow
A[User navigates to User Preferences] --> B{Custom Data Path Set?};
B -- Yes --> C[Display Current Custom Path];
B -- No --> D[Display Current Path: Default];
C --> E[Show "Revert to Default" Button];
D --> F[User Selects New Parent Directory];
F --> G{Path Status?};
G -- Empty --> H[Set Path];
G -- Not Empty --> I{Confirm "pastebar-data" subfolder};
I -- Yes --> J[Append "pastebar-data" to path];
J --> H;
I -- No --> H;
G -- Is 'pastebar-data' and Not Empty --> K[Alert user existing data will be used];
K --> H;
H --> L[User Selects Operation: Move/Copy/None];
L --> M[User Clicks "Apply and Restart"];
end
subgraph Backend Logic
M --> N[Frontend calls `cmd_set_and_relocate_data`];
N -- Success --> O[1. Create new data dir if needed];
O --> P[2. Move/Copy/Skip data];
P --> Q[3. Update `custom_db_path` in settings];
Q --> R[Show Success Toast & Relaunch App];
N -- Error --> S[Show Error Toast];
E --> T[User Clicks "Revert"];
T --> U[Frontend calls `cmd_revert_to_default_data_location`];
U -- Success --> V[Move/Copy data back to default app dir & clear setting];
V --> W[Show Success Toast & Relaunch App];
U -- Error --> X[Show Error Toast];
end
D -- "Browse..." --> F;
```
## 5. Implementation Summary
The following changes have been implemented:
* **`packages/pastebar-app-ui/src/pages/settings/UserPreferences.tsx`**:
* Renamed "Custom Database Location" to "Custom Application Data Location".
* Added a third radio button for the "Use new location" option.
* Updated the `handleBrowse` function to call the new `cmd_check_custom_data_path` command and handle the different path statuses with user prompts.
* **`packages/pastebar-app-ui/src/store/settingsStore.ts`**:
* Updated the `applyCustomDbPath` function to accept the "none" operation.
* Updated the `revertToDefaultDbPath` function to call the renamed backend command.
* **`src-tauri/src/commands/user_settings_command.rs`**:
* Added the `cmd_check_custom_data_path` command.
* Renamed `cmd_set_and_relocate_db` to `cmd_set_and_relocate_data` and updated its logic to handle the "none" operation and the new data directory structure.
* Renamed `cmd_revert_to_default_db_location` to `cmd_revert_to_default_data_location` and updated its logic.
* **`src-tauri/src/db.rs`**:
* Refactored the `get_data_dir` function to no longer automatically append `pastebar-data`.
* Added `get_clip_images_dir` and `get_clipboard_images_dir` helper functions.
* **`src-tauri/src/main.rs`**:
* Registered the new and renamed commands in the `invoke_handler`.
* **`src-tauri/Cargo.toml`**:
* Added the `fs_extra` dependency for recursive directory copying.
* **`src-tauri/src/services/items_service.rs`** and **`src-tauri/src/services/history_service.rs`**:
* Updated to use the new `get_clip_images_dir` and `get_clipboard_images_dir` helper functions.

210
package-lock.json generated
View File

@ -135,7 +135,7 @@
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@tailwindcss/line-clamp": "^0.4.4",
"@tauri-apps/cli": "^1.6.0",
"@tauri-apps/cli": "^1.6.3",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/bcryptjs": "^2.4.6",
"@types/codemirror": "^5.60.15",
@ -7000,10 +7000,14 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.0.tgz",
"integrity": "sha512-DBBpBl6GhTzm8ImMbKkfaZ4fDTykWrC7Q5OXP4XqD91recmDEn2LExuvuiiS3HYe7uP8Eb5B9NPHhqJb+Zo7qQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.3.tgz",
"integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"dependencies": {
"semver": ">=7.5.2"
},
"bin": {
"tauri": "tauri.js"
},
@ -7015,26 +7019,27 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "1.6.0",
"@tauri-apps/cli-darwin-x64": "1.6.0",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.0",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.0",
"@tauri-apps/cli-linux-arm64-musl": "1.6.0",
"@tauri-apps/cli-linux-x64-gnu": "1.6.0",
"@tauri-apps/cli-linux-x64-musl": "1.6.0",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.0",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.0",
"@tauri-apps/cli-win32-x64-msvc": "1.6.0"
"@tauri-apps/cli-darwin-arm64": "1.6.3",
"@tauri-apps/cli-darwin-x64": "1.6.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.3",
"@tauri-apps/cli-linux-arm64-musl": "1.6.3",
"@tauri-apps/cli-linux-x64-gnu": "1.6.3",
"@tauri-apps/cli-linux-x64-musl": "1.6.3",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.3",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.3",
"@tauri-apps/cli-win32-x64-msvc": "1.6.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.0.tgz",
"integrity": "sha512-SNRwUD9nqGxY47mbY1CGTt/jqyQOU7Ps7Mx/mpgahL0FVUDiCEY/5L9QfEPPhEgccgcelEVn7i6aQHIkHyUtCA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -7044,13 +7049,14 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.0.tgz",
"integrity": "sha512-g2/uDR/eeH2arvuawA4WwaEOqv/7jDO/ZLNI3JlBjP5Pk8GGb3Kdy0ro1xQzF94mtk2mOnOXa4dMgAet4sUJ1A==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz",
"integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -7060,13 +7066,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.0.tgz",
"integrity": "sha512-EVwf4oRkQyG8BpSrk0gqO7oA0sDM2MdNDtJpMfleYFEgCxLIOGZKNqaOW3M7U+0Y4qikmG3TtRK+ngc8Ymtrjg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -7076,13 +7083,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.0.tgz",
"integrity": "sha512-YdpY17cAySrhK9dX4BUVEmhAxE2o+6skIEFg8iN/xrDwRxhaNPI9I80YXPatUTX54Kx55T5++25VJG9+3iw83A==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -7092,13 +7100,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.0.tgz",
"integrity": "sha512-4U628tuf2U8pMr4tIBJhEkrFwt+46dwhXrDlpdyWSZtnop5RJAVKHODm0KbWns4xGKfTW1F3r6sSv+2ZxLcISA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -7108,13 +7117,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.0.tgz",
"integrity": "sha512-AKRzp76fVUaJyXj5KRJT9bJyhwZyUnRQU0RqIRqOtZCT5yr6qGP8rjtQ7YhCIzWrseBlOllc3Qvbgw3Yl0VQcA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -7124,13 +7134,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.0.tgz",
"integrity": "sha512-0edIdq6aMBTaRMIXddHfyAFL361JqulLLd2Wi2aoOie7DkQ2MYh6gv3hA7NB9gqFwNIGE+xtJ4BkXIP2tSGPlg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -7140,13 +7151,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.0.tgz",
"integrity": "sha512-QwWpWk4ubcwJ1rljsRAmINgB2AwkyzZhpYbalA+MmzyYMREcdXWGkyixWbRZgqc6fEWEBmq5UG73qz5eBJiIKg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -7156,13 +7168,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.0.tgz",
"integrity": "sha512-Vtw0yxO9+aEFuhuxQ57ALG43tjECopRimRuKGbtZYDCriB/ty5TrT3QWMdy0dxBkpDTu3Rqsz30sbDzw6tlP3Q==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -7172,13 +7185,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.0.tgz",
"integrity": "sha512-h54FHOvGi7+LIfRchzgZYSCHB1HDlP599vWXQQJ/XnwJY+6Rwr2E5bOe/EhqoG8rbGkfK0xX3KPAvXPbUlmggg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -7187,6 +7201,19 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -22361,90 +22388,99 @@
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg=="
},
"@tauri-apps/cli": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.0.tgz",
"integrity": "sha512-DBBpBl6GhTzm8ImMbKkfaZ4fDTykWrC7Q5OXP4XqD91recmDEn2LExuvuiiS3HYe7uP8Eb5B9NPHhqJb+Zo7qQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.3.tgz",
"integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==",
"dev": true,
"requires": {
"@tauri-apps/cli-darwin-arm64": "1.6.0",
"@tauri-apps/cli-darwin-x64": "1.6.0",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.0",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.0",
"@tauri-apps/cli-linux-arm64-musl": "1.6.0",
"@tauri-apps/cli-linux-x64-gnu": "1.6.0",
"@tauri-apps/cli-linux-x64-musl": "1.6.0",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.0",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.0",
"@tauri-apps/cli-win32-x64-msvc": "1.6.0"
"@tauri-apps/cli-darwin-arm64": "1.6.3",
"@tauri-apps/cli-darwin-x64": "1.6.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.3",
"@tauri-apps/cli-linux-arm64-musl": "1.6.3",
"@tauri-apps/cli-linux-x64-gnu": "1.6.3",
"@tauri-apps/cli-linux-x64-musl": "1.6.3",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.3",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.3",
"@tauri-apps/cli-win32-x64-msvc": "1.6.3",
"semver": ">=7.5.2"
},
"dependencies": {
"semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true
}
}
},
"@tauri-apps/cli-darwin-arm64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.0.tgz",
"integrity": "sha512-SNRwUD9nqGxY47mbY1CGTt/jqyQOU7Ps7Mx/mpgahL0FVUDiCEY/5L9QfEPPhEgccgcelEVn7i6aQHIkHyUtCA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-darwin-x64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.0.tgz",
"integrity": "sha512-g2/uDR/eeH2arvuawA4WwaEOqv/7jDO/ZLNI3JlBjP5Pk8GGb3Kdy0ro1xQzF94mtk2mOnOXa4dMgAet4sUJ1A==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz",
"integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.0.tgz",
"integrity": "sha512-EVwf4oRkQyG8BpSrk0gqO7oA0sDM2MdNDtJpMfleYFEgCxLIOGZKNqaOW3M7U+0Y4qikmG3TtRK+ngc8Ymtrjg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-arm64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.0.tgz",
"integrity": "sha512-YdpY17cAySrhK9dX4BUVEmhAxE2o+6skIEFg8iN/xrDwRxhaNPI9I80YXPatUTX54Kx55T5++25VJG9+3iw83A==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-arm64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.0.tgz",
"integrity": "sha512-4U628tuf2U8pMr4tIBJhEkrFwt+46dwhXrDlpdyWSZtnop5RJAVKHODm0KbWns4xGKfTW1F3r6sSv+2ZxLcISA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-x64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.0.tgz",
"integrity": "sha512-AKRzp76fVUaJyXj5KRJT9bJyhwZyUnRQU0RqIRqOtZCT5yr6qGP8rjtQ7YhCIzWrseBlOllc3Qvbgw3Yl0VQcA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-linux-x64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.0.tgz",
"integrity": "sha512-0edIdq6aMBTaRMIXddHfyAFL361JqulLLd2Wi2aoOie7DkQ2MYh6gv3hA7NB9gqFwNIGE+xtJ4BkXIP2tSGPlg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-win32-arm64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.0.tgz",
"integrity": "sha512-QwWpWk4ubcwJ1rljsRAmINgB2AwkyzZhpYbalA+MmzyYMREcdXWGkyixWbRZgqc6fEWEBmq5UG73qz5eBJiIKg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-win32-ia32-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.0.tgz",
"integrity": "sha512-Vtw0yxO9+aEFuhuxQ57ALG43tjECopRimRuKGbtZYDCriB/ty5TrT3QWMdy0dxBkpDTu3Rqsz30sbDzw6tlP3Q==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==",
"dev": true,
"optional": true
},
"@tauri-apps/cli-win32-x64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.0.tgz",
"integrity": "sha512-h54FHOvGi7+LIfRchzgZYSCHB1HDlP599vWXQQJ/XnwJY+6Rwr2E5bOe/EhqoG8rbGkfK0xX3KPAvXPbUlmggg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==",
"dev": true,
"optional": true
},

View File

@ -149,7 +149,7 @@
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@tailwindcss/line-clamp": "^0.4.4",
"@tauri-apps/cli": "^1.6.0",
"@tauri-apps/cli": "^1.6.3",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/bcryptjs": "^2.4.6",
"@types/codemirror": "^5.60.15",

View File

@ -107,6 +107,11 @@ function App() {
settingsStore.initSettings({
appDataDir: import.meta.env.TAURI_DEBUG ? appDevDataDir : appDataDir,
// Initialize new DB path settings for type conformity; actual value loaded by loadInitialCustomDbPath
customDbPath: null,
isCustomDbPathValid: null,
customDbPathError: null,
dbRelocationInProgress: false,
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,
@ -139,6 +144,7 @@ function App() {
isHistoryEnabled: settings.isHistoryEnabled?.valueBool,
clipTextMinLength: settings.clipTextMinLength?.valueInt,
clipTextMaxLength: settings.clipTextMaxLength?.valueInt,
isImageCaptureDisabled: settings.isImageCaptureDisabled?.valueBool,
isAutoClearSettingsEnabled: settings.isAutoClearSettingsEnabled?.valueBool,
autoClearSettingsDuration: settings.autoClearSettingsDuration?.valueInt,
autoClearSettingsDurationType:
@ -187,6 +193,8 @@ function App() {
settingsStore.initConstants({
APP_DETECT_LANGUAGES_SUPPORTED: appDetectLanguageSupport,
})
// Load the actual custom DB path after basic settings are initialized
settingsStore.loadInitialCustomDbPath()
type().then(osType => {
if (osType === 'Windows_NT' && settings.copyPasteDelay?.valueInt === 0) {
settingsStore.updateSetting('copyPasteDelay', 2)
@ -424,6 +432,9 @@ function App() {
if (name === 'isHistoryEnabled') {
settingsStore.updateSetting('isHistoryEnabled', Boolean(value_bool))
}
if (name === 'isImageCaptureDisabled') {
settingsStore.updateSetting('isImageCaptureDisabled', Boolean(value_bool))
}
})
const listenToMenuUnlisten = listen('menu:add_first_menu_item', () => {

View File

@ -191,6 +191,8 @@ export function NavBar() {
setIsShowDisabledCollectionsOnNavBarMenu,
isShowNavBarItemsOnHoverOnly,
isHideCollectionsOnNavBar,
isImageCaptureDisabled,
setIsImageCaptureDisabled,
} = useAtomValue(settingsStoreAtom)
const {
@ -642,6 +644,23 @@ export function NavBar() {
<Shortcut keys="CTRL+A" />
</MenubarShortcut>
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={!isImageCaptureDisabled}
onClick={e => {
e.preventDefault()
setIsImageCaptureDisabled(!isImageCaptureDisabled)
}}
>
<Text
className={`mr-2 ${
isImageCaptureDisabled
? 'text-slate-900/50'
: 'text-slate-800'
}`}
>
{t('Enable Image Capture', { ns: 'history' })}
</Text>
</MenubarCheckboxItem>
<MenubarSeparator />
<MenubarItem
onClick={() => {

View File

@ -24,6 +24,7 @@ Are you sure you want to delete?: Are you sure you want to delete?
Are you sure?: Are you sure?
Attach History Window: Attach History Window
Back: Back
Browse...: Browse...
Build on {{buildDate}}: Build on {{buildDate}}
Cancel: Cancel
Cancel Reset: Cancel Reset

View File

@ -11,6 +11,7 @@ Confirm Clear All History: Confirm Clear All History
Do you really want to remove ALL clipboard history items?: Do you really want to remove ALL clipboard history items?
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}
Enable Capture History: Enable Capture History
Enable Image Capture: Enable Image Capture
Filters:
App Filters: App Filters
Audio: Audio

View File

@ -12,6 +12,11 @@ Application UI Color Theme: Application UI Color Theme
Application UI Fonts Scale: Application UI Fonts Scale
Application UI Language: Application UI Language
Applications listed below will not have their copy to clipboard action captured in clipboard history. Case insensitive.: Applications listed below will not have their copy to clipboard action captured in clipboard history. Case insensitive.
Apply and Restart: Apply and Restart
Are you sure you want to go back to the default data folder? This will remove the custom location setting.: Are you sure you want to go back to the default data folder? This will remove the custom location setting.
Are you sure you want to revert to the default database location? The application will restart.: Are you sure you want to revert to the default database location? The application will restart.
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: Are you sure you want to set "{{path}}" as the new data folder? The application will restart.
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.
Auto Disable History Capture when Screen Unlocked: Auto Disable History Capture when Screen Unlocked
Auto Lock Application Screen on User Inactivity: Auto Lock Application Screen on User Inactivity
Auto Lock Screen on User Inactivity: Auto Lock Screen on User Inactivity
@ -30,25 +35,42 @@ Auto-delete clipboard history after: Auto-delete clipboard history after
Back: Back
Capture History: Capture History
Capture History Text Length Limits: Capture History Text Length Limits
Change Custom Data Folder...: Change Custom Data Folder...
Change Directory...: Change Directory...
Change Selected Folder...: Change Selected Folder...
Change the application UI font size scale: Change the application UI font size scale
Change the application UI language: Change the application UI language
Change the application user interface color theme: Change the application user interface color theme
Change the application user interface font size scale: Change the application user interface font size scale
Change the application user interface language: Change the application user interface language
Changing the database location requires an application restart to take effect.: Changing the database location requires an application restart to take effect.
Clip Notes Popup Maximum Dimensions: Clip Notes Popup Maximum Dimensions
Clipboard History Settings: Clipboard History Settings
'Complete details:': 'Complete details:'
Configure settings to automatically delete clipboard history items after a specified duration.: Configure settings to automatically delete clipboard history items after a specified duration.
ConfirmRevertToDefaultDbPathMessage: Are you sure you want to revert to the default data location? Your application data will be moved to the default path.
Copy data: Copy data
Copy database file: Copy database file
Create a preview card on link hover in the clipboard history. This allows you to preview the link before opening or pasting it.: Create a preview card on link hover in the clipboard history. This allows you to preview the link before opening or pasting it.
Create an unlimited number of collections to organize your clips and menus.: Create an unlimited number of collections to organize your clips and menus.
Current data folder: Current data folder
Current data location: Current data location
Current database location: Current database location
Custom: Custom
Custom Application Data Location: Custom Application Data Location
Custom Database Location: Custom Database Location
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: Custom data location is active. You can change it or revert to the default location. Changes require an application restart.
Custom themes: Custom themes
Decrease UI Font Size: Decrease UI Font Size
Default: Default
Disable Image Capture: Disable Image Capture
Disable capturing and storing images from clipboard: Disable capturing and storing images from clipboard
? Display clipboard history capture toggle on the locked application screen. This allows you to control history capture settings directly from the lock screen.
: Display clipboard history capture toggle on the locked application screen. This allows you to control history capture settings directly from the lock screen.
Display disabled collections name on the navigation bar collections menu: Display disabled collections name on the navigation bar collections menu
Display disabled collections name on the navigation bar under collections menu: Display disabled collections name on the navigation bar under collections menu
Display full name of selected collection on the navigation bar: Display full name of selected collection on the navigation bar
Do you want to attempt to move the database file from "{{customPath}}" back to the default location?: Do you want to attempt to move the database file from "{{customPath}}" back to the default location?
Drag and drop to prioritize languages for detection. The higher a language is in the list, the higher its detection priority.: Drag and drop to prioritize languages for detection. The higher a language is in the list, the higher its detection priority.
Email: Email
Email is not valid: Email is not valid
@ -61,11 +83,12 @@ Enable auto lock the application screen after a certain period of inactivity, to
Enable auto lock the application screen when user not active: Enable auto lock the application screen when user not active
Enable auto trim spaces on history capture: Enable auto trim spaces on history capture
Enable auto update on capture: Enable auto update on capture
Enable custom data location to store application data in a directory of your choice instead of the default location.: Enable custom data location to store application data in a directory of your choice instead of the default location.
Enable history capture: Enable history capture
Enable programming language detection: Enable programming language detection
Enable screen unlock requirement on app launch for enhanced security, safeguarding data from unauthorized access.: Enable screen unlock requirement on app launch for enhanced security, safeguarding data from unauthorized access.
Enhance security by automatically locking the application screen after a set period of user inactivity.: Enhance security by automatically locking the application screen after a set period of user inactivity.
Enter Passcode length: Enter Passcode length
Enter new directory path or leave empty for default on next revert: Enter new directory path or leave empty for default on next revert
Enter recovery password to reset passcode.: Enter recovery password to reset passcode.
Enter your <strong>{{screenLockPassCodeLength}} digits</strong> passcode: Enter your <strong>{{screenLockPassCodeLength}} digits</strong> passcode
Entered Passcode is invalid: Entered Passcode is invalid
@ -73,12 +96,16 @@ Excluded Apps List: Excluded Apps List
Execute Web Requests: Execute Web Requests
Execute terminal or shell commands directly from PasteBar clip and copy the results to the clipboard.: Execute terminal or shell commands directly from PasteBar clip and copy the results to the clipboard.
'Expires:': 'Expires:'
Failed to revert to default database location.: Failed to revert to default database location.
Failed to select directory: Failed to select directory
Forgot Passcode ? Enter your recovery password to reset the passcode.: Forgot Passcode ? Enter your recovery password to reset the passcode.
Forgot passcode ?: Forgot passcode ?
Forgot?: Forgot?
Forgot? Reset using Password: Forgot? Reset using Password
Get priority email support from us to resolve any issues or questions you may have about PasteBar.: Get priority email support from us to resolve any issues or questions you may have about PasteBar.
'Hint: {{screenLockRecoveryPasswordMasked}}': 'Hint: {{screenLockRecoveryPasswordMasked}}'
? If a database file already exists at the default location, do you want to overwrite it? Choosing "Cancel" will skip moving the file if an existing file is found.
: If a database file already exists at the default location, do you want to overwrite it? Choosing "Cancel" will skip moving the file if an existing file is found.
Incorrect passcode.: Incorrect passcode.
Increase UI Font Size: Increase UI Font Size
Issued: Issued
@ -95,8 +122,13 @@ Medium: Medium
Minimal 4 digits: Minimal 4 digits
Minimize Window: Minimize Window
Minimum number of lines to trigger detection: Minimum number of lines to trigger detection
Move data: Move data
Move database file: Move database file
Name: Name
New Data Directory Path: New Data Directory Path
New Database Directory Path: New Database Directory Path
Open Security Settings: Open Security Settings
Operation when applying new path: Operation when applying new path
Passcode digits remaining: Passcode digits remaining
Passcode is locked.: Passcode is locked.
Passcode is not set: Passcode is not set
@ -132,17 +164,27 @@ Require Screen Unlock at Application Start: Require Screen Unlock at Application
? Require screen unlock at application launch to enhance security. This setting ensures that only authorized users can access the application, protecting your data from unauthorized access right from the start.
: Require screen unlock at application launch to enhance security. This setting ensures that only authorized users can access the application, protecting your data from unauthorized access right from the start.
Reset Font Size: Reset Font Size
Revert to Default: Revert to Default
Revert to Default and Restart: Revert to Default and Restart
Run Terminal or Shell Commands: Run Terminal or Shell Commands
Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard.: Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard.
? 'Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard. '
: 'Scrape and parse websites or API responses using built-in web scraping tools and copy the extracted data to the clipboard. '
Security: Security
Security Settings: Security Settings
Select Custom Data Folder...: Select Custom Data Folder...
Select Data Folder: Select Data Folder
Select Data Folder...: Select Data Folder...
Select a custom location to store your application data instead of the default location.: Select a custom location to store your application data instead of the default location.
Selected data folder: Selected data folder
Selected new data folder: Selected new data folder
Selected new data folder for change: Selected new data folder for change
Send HTTP requests to web APIs or services and copy the response data to the clipboard.: Send HTTP requests to web APIs or services and copy the response data to the clipboard.
Sensitive words or sentences listed below will automatically be masked if found in the copied text. Case insensitive.: Sensitive words or sentences listed below will automatically be masked if found in the copied text. Case insensitive.
Set a passcode to unlock the locked screen and protect your data from unauthorized access.: Set a passcode to unlock the locked screen and protect your data from unauthorized access.
? Set a recovery password to easily reset your lock screen passcode if forgotten. Your password will be securely stored in your device's OS storage.
: Set a recovery password to easily reset your lock screen passcode if forgotten. Your password will be securely stored in your device's OS storage.
Setting a custom database location requires an application restart to take effect.: Setting a custom database location requires an application restart to take effect.
Settings: Settings
Show Clipboard History Capture Control on Lock Screen: Show Clipboard History Capture Control on Lock Screen
Show Disable History Capture When Screen Locked: Show Disable History Capture When Screen Locked
@ -158,6 +200,10 @@ Swap Panels Layout: Swap Panels Layout
Switch the layout position of panels in Clipboard History and Paste Menu views: Switch the layout position of panels in Clipboard History and Paste Menu views
Thank you again for using PasteBar.: Thank you again for using PasteBar.
Thank you for testing! 🙌: Thank you for testing! 🙌
? The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?
: The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?
The selected folder is not empty. Do you want to create a "pastebar-data" subfolder to store the data?: The selected folder is not empty. Do you want to create a "pastebar-data" subfolder to store the data?
This folder already contains PasteBar data. The application will use this existing data after restart.: This folder already contains PasteBar data. The application will use this existing data after restart.
? This option lets you control the display and timing of hover notes on clips. You can choose to show notes instantly or with a delay to prevent unintended popups.
: This option lets you control the display and timing of hover notes on clips. You can choose to show notes instantly or with a delay to prevent unintended popups.
? This option lets you customize the maximum width and height of the popup that displays clip notes, ensuring it fits comfortably within your desired size.
@ -177,8 +223,10 @@ Unlimited Collections: Unlimited Collections
Unlimited Tabs per Collection: Unlimited Tabs per Collection
Unlimited paste history: Unlimited paste history
Use Password: Use Password
Use new location: Use new location
User Preferences: User Preferences
Web Scraping and Parsing: Web Scraping and Parsing
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.
none: none
passcode reset: passcode reset
password reset: password reset

View File

@ -11,6 +11,7 @@ Confirm Clear All History: Confirmar Borrar Todo el Historial
Do you really want to remove ALL clipboard history items?: ¿Realmente quieres eliminar TODOS los elementos del historial?
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: ¿Quieres eliminar los elementos del historial más antiguos que {{olderThen}} {{durationType}}?
Enable Capture History: Habilitar Captura de Historial
Enable Image Capture: Habilitar Captura de Imágenes
Filters:
App Filters: Filtros de App
Audio: Audio

View File

@ -179,3 +179,38 @@ Web Scraping and Parsing: Web Scraping y Análisis
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Las palabras o frases listadas abajo no serán capturadas en el historial del portapapeles si se encuentran en el texto copiado. No distingue mayúsculas y minúsculas.
passcode reset: restablecimiento de código
password reset: restablecimiento de contraseña
Apply and Restart: Aplicar y Reiniciar
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: ¿Estás seguro de que quieres establecer "{{path}}" como la nueva carpeta de datos? La aplicación se reiniciará.
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: ¿Estás seguro de que quieres {{operation}} la base de datos a "{{path}}"? La aplicación se reiniciará.
Change Custom Data Folder...: Cambiar Carpeta de Datos Personalizada...
Change Selected Folder...: Cambiar Carpeta Seleccionada...
Changing the database location requires an application restart to take effect.: Cambiar la ubicación de la base de datos requiere reiniciar la aplicación para que surta efecto.
ConfirmRevertToDefaultDbPathMessage: ¿Estás seguro de que quieres revertir a la ubicación de datos predeterminada? Los datos de tu aplicación se moverán a la ruta predeterminada.
Copy data: Copiar datos
Current data folder: Carpeta de datos actual
Custom Application Data Location: Ubicación de Datos de Aplicación Personalizada
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: La ubicación de datos personalizada está activa. Puedes cambiarla o revertir a la ubicación predeterminada. Los cambios requieren reiniciar la aplicación.
Default: Predeterminado
Enable custom data location to store application data in a directory of your choice instead of the default location.: Habilitar ubicación de datos personalizada para almacenar datos de la aplicación en un directorio de tu elección en lugar de la ubicación predeterminada.
Failed to apply custom database location.: Error al aplicar la ubicación de base de datos personalizada.
Failed to create directory. Please check permissions and try again.: Error al crear directorio. Por favor verifica los permisos e intenta de nuevo.
Failed to revert to default database location.: Error al revertir a la ubicación de base de datos predeterminada.
Found existing "pastebar-data" folder. The application will use this folder to store data.: Se encontró una carpeta "pastebar-data" existente. La aplicación usará esta carpeta para almacenar datos.
Invalid directory selected.: Directorio seleccionado inválido.
Operation when applying new path: Operación al aplicar nueva ruta
Please select a directory first.: Por favor selecciona un directorio primero.
Please select a new directory different from the current one.: Por favor selecciona un directorio nuevo diferente al actual.
Revert to Default: Revertir a Predeterminado
Select Data Folder: Seleccionar Carpeta de Datos
Select Data Folder...: Seleccionar Carpeta de Datos...
Select a custom location to store your application data instead of the default location.: Selecciona una ubicación personalizada para almacenar los datos de tu aplicación en lugar de la ubicación predeterminada.
Selected data folder: Carpeta de datos seleccionada
Selected new data folder for change: Nueva carpeta de datos seleccionada para cambio
Setting a custom database location requires an application restart to take effect.: Establecer una ubicación de base de datos personalizada requiere reiniciar la aplicación para que surta efecto.
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: La carpeta seleccionada no está vacía y no contiene archivos de datos de PasteBar. ¿Quieres crear una subcarpeta "pastebar-data" para almacenar los datos?
This folder already contains PasteBar data. The application will use this existing data after restart.: Esta carpeta ya contiene datos de PasteBar. La aplicación usará estos datos existentes después del reinicio.
Use new location: Usar nueva ubicación
An error occurred during directory processing.: Ocurrió un error durante el procesamiento del directorio.
An error occurred during directory selection or setup.: Ocurrió un error durante la selección o configuración del directorio.
Disable Image Capture: Deshabilitar Captura de Imágenes
Disable capturing and storing images from clipboard: Deshabilitar la captura y almacenamiento de imágenes del portapapeles

View File

@ -11,6 +11,7 @@ Confirm Clear All History: Confirmer l'effacement de tout l'historique
Do you really want to remove ALL clipboard history items?: Voulez-vous vraiment supprimer TOUS les éléments de l'historique du presse-papier ?
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: Voulez-vous supprimer les éléments de l'historique du presse-papier antérieurs à {{olderThen}} {{durationType}}
Enable Capture History: Activer la capture de l'historique
Enable Image Capture: Activer la capture d'images
Filters:
App Filters: Filtres d'applications
Audio: Audio

View File

@ -182,3 +182,38 @@ Web Scraping and Parsing: Extraction et analyse web
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Les mots ou phrases énumérés ci-dessous ne seront pas capturés dans l'historique du presse-papiers s'ils se trouvent dans le texte copié. Insensible à la casse
passcode reset: réinitialisation code d'accès
password reset: réinitialisation mot de passe
Apply and Restart: Appliquer et redémarrer
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: Êtes-vous sûr de vouloir définir "{{path}}" comme nouveau dossier de données ? L'application va redémarrer.
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: Êtes-vous sûr de vouloir {{operation}} la base de données vers "{{path}}" ? L'application va redémarrer.
Change Custom Data Folder...: Changer le dossier de données personnalisé...
Change Selected Folder...: Changer le dossier sélectionné...
Changing the database location requires an application restart to take effect.: Le changement de l'emplacement de la base de données nécessite un redémarrage de l'application pour prendre effet.
ConfirmRevertToDefaultDbPathMessage: Êtes-vous sûr de vouloir revenir à l'emplacement de données par défaut ? Vos données d'application seront déplacées vers le chemin par défaut.
Copy data: Copier les données
Current data folder: Dossier de données actuel
Custom Application Data Location: Emplacement de données d'application personnalisé
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: L'emplacement de données personnalisé est actif. Vous pouvez le modifier ou revenir à l'emplacement par défaut. Les modifications nécessitent un redémarrage de l'application.
Default: Par défaut
Enable custom data location to store application data in a directory of your choice instead of the default location.: Activer l'emplacement de données personnalisé pour stocker les données d'application dans un répertoire de votre choix au lieu de l'emplacement par défaut.
Failed to apply custom database location.: Échec de l'application de l'emplacement de base de données personnalisé.
Failed to create directory. Please check permissions and try again.: Échec de la création du répertoire. Veuillez vérifier les autorisations et réessayer.
Failed to revert to default database location.: Échec du retour à l'emplacement de base de données par défaut.
Found existing "pastebar-data" folder. The application will use this folder to store data.: Dossier "pastebar-data" existant trouvé. L'application utilisera ce dossier pour stocker les données.
Invalid directory selected.: Répertoire sélectionné invalide.
Operation when applying new path: Opération lors de l'application du nouveau chemin
Please select a directory first.: Veuillez d'abord sélectionner un répertoire.
Please select a new directory different from the current one.: Veuillez sélectionner un nouveau répertoire différent de l'actuel.
Revert to Default: Revenir au défaut
Select Data Folder: Sélectionner le dossier de données
Select Data Folder...: Sélectionner le dossier de données...
Select a custom location to store your application data instead of the default location.: Sélectionnez un emplacement personnalisé pour stocker vos données d'application au lieu de l'emplacement par défaut.
Selected data folder: Dossier de données sélectionné
Selected new data folder for change: Nouveau dossier de données sélectionné pour modification
Setting a custom database location requires an application restart to take effect.: La définition d'un emplacement de base de données personnalisé nécessite un redémarrage de l'application pour prendre effet.
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: Le dossier sélectionné n'est pas vide et ne contient pas de fichiers de données PasteBar. Voulez-vous créer un sous-dossier "pastebar-data" pour stocker les données ?
This folder already contains PasteBar data. The application will use this existing data after restart.: Ce dossier contient déjà des données PasteBar. L'application utilisera ces données existantes après le redémarrage.
Use new location: Utiliser le nouvel emplacement
An error occurred during directory processing.: Une erreur s'est produite lors du traitement du répertoire.
An error occurred during directory selection or setup.: Une erreur s'est produite lors de la sélection ou configuration du répertoire.
Disable Image Capture: Désactiver la Capture d'Images
Disable capturing and storing images from clipboard: Désactiver la capture et le stockage d'images depuis le presse-papiers

View File

@ -11,6 +11,7 @@ Confirm Clear All History: Conferma Cancellazione di Tutta la Cronologia
Do you really want to remove ALL clipboard history items?: Vuoi davvero rimuovere TUTTI gli elementi della cronologia degli appunti?
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: Vuoi rimuovere gli elementi della cronologia degli appunti più vecchi di {{olderThen}} {{durationType}}
Enable Capture History: Abilita Cattura Cronologia
Enable Image Capture: Abilita Cattura Immagini
Filters:
App Filters: Filtri App
Audio: Audio

View File

@ -181,3 +181,38 @@ Web Scraping and Parsing: Web Scraping e Parsing
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Le parole o frasi elencate di seguito non verranno catturate nella cronologia degli appunti se trovate nel testo copiato. Non sensibile alle maiuscole/minuscole.
passcode reset: reset del codice di accesso
password reset: reset della password
Apply and Restart: Applica e Riavvia
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: Sei sicuro di voler impostare "{{path}}" come nuova cartella dati? L'applicazione si riavvierà.
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: Sei sicuro di voler {{operation}} il database in "{{path}}"? L'applicazione si riavvierà.
Change Custom Data Folder...: Cambia Cartella Dati Personalizzata...
Change Selected Folder...: Cambia Cartella Selezionata...
Changing the database location requires an application restart to take effect.: Cambiare la posizione del database richiede un riavvio dell'applicazione per avere effetto.
ConfirmRevertToDefaultDbPathMessage: Sei sicuro di voler tornare alla posizione dati predefinita? I dati della tua applicazione verranno spostati nel percorso predefinito.
Copy data: Copia dati
Current data folder: Cartella dati corrente
Custom Application Data Location: Posizione Dati Applicazione Personalizzata
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: La posizione dati personalizzata è attiva. Puoi cambiarla o tornare alla posizione predefinita. Le modifiche richiedono un riavvio dell'applicazione.
Default: Predefinito
Enable custom data location to store application data in a directory of your choice instead of the default location.: Abilita posizione dati personalizzata per memorizzare i dati dell'applicazione in una directory di tua scelta invece della posizione predefinita.
Failed to apply custom database location.: Impossibile applicare la posizione database personalizzata.
Failed to create directory. Please check permissions and try again.: Impossibile creare la directory. Controlla i permessi e riprova.
Failed to revert to default database location.: Impossibile tornare alla posizione database predefinita.
Found existing "pastebar-data" folder. The application will use this folder to store data.: Trovata cartella "pastebar-data" esistente. L'applicazione userà questa cartella per memorizzare i dati.
Invalid directory selected.: Directory selezionata non valida.
Operation when applying new path: Operazione quando si applica il nuovo percorso
Please select a directory first.: Seleziona prima una directory.
Please select a new directory different from the current one.: Seleziona una nuova directory diversa da quella corrente.
Revert to Default: Torna al Predefinito
Select Data Folder: Seleziona Cartella Dati
Select Data Folder...: Seleziona Cartella Dati...
Select a custom location to store your application data instead of the default location.: Seleziona una posizione personalizzata per memorizzare i dati della tua applicazione invece della posizione predefinita.
Selected data folder: Cartella dati selezionata
Selected new data folder for change: Nuova cartella dati selezionata per il cambio
Setting a custom database location requires an application restart to take effect.: Impostare una posizione database personalizzata richiede un riavvio dell'applicazione per avere effetto.
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: La cartella selezionata non è vuota e non contiene file dati di PasteBar. Vuoi creare una sottocartella "pastebar-data" per memorizzare i dati?
This folder already contains PasteBar data. The application will use this existing data after restart.: Questa cartella contiene già dati di PasteBar. L'applicazione userà questi dati esistenti dopo il riavvio.
Use new location: Usa nuova posizione
An error occurred during directory processing.: Si è verificato un errore durante l'elaborazione della directory.
An error occurred during directory selection or setup.: Si è verificato un errore durante la selezione o configurazione della directory.
Disable Image Capture: Disabilita Cattura Immagini
Disable capturing and storing images from clipboard: Disabilita la cattura e memorizzazione di immagini dagli appunti

View File

@ -12,6 +12,7 @@ Do you really want to remove ALL clipboard history items?: Вы действит
Do you want to remove all recent clipboard history older than {{olderThen}} {{durationType}}: Вы хотите удалить всю недавнюю историю буфера обмена за период {{olderThen}} {{durationType}}
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: Вы хотите удалить историю буфера обмена старше чем {{olderThen}} {{durationType}}
Enable Capture History: Включить захват истории
Enable Image Capture: Включить захват изображений
Filters:
App Filters: Фильтры приложений
Audio: Аудио

View File

@ -165,3 +165,38 @@ Web Scraping and Parsing: Веб-скрейпинг и анализ
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Слова или предложения, перечисленные ниже, не будут захватываться в историю буфера обмена, если они будут найдены в скопированном тексте. Без учета регистра.
passcode reset: сброс кода доступа
password reset: сброс пароля
Apply and Restart: Применить и перезапустить
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: Вы уверены, что хотите установить "{{path}}" как новую папку данных? Приложение будет перезапущено.
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: Вы уверены, что хотите {{operation}} базу данных в "{{path}}"? Приложение будет перезапущено.
Change Custom Data Folder...: Изменить пользовательскую папку данных...
Change Selected Folder...: Изменить выбранную папку...
Changing the database location requires an application restart to take effect.: Изменение расположения базы данных требует перезапуска приложения для вступления в силу.
ConfirmRevertToDefaultDbPathMessage: Вы уверены, что хотите вернуться к расположению данных по умолчанию? Данные вашего приложения будут перемещены в путь по умолчанию.
Copy data: Копировать данные
Current data folder: Текущая папка данных
Custom Application Data Location: Пользовательское расположение данных приложения
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: Пользовательское расположение данных активно. Вы можете изменить его или вернуться к расположению по умолчанию. Изменения требуют перезапуска приложения.
Default: По умолчанию
Enable custom data location to store application data in a directory of your choice instead of the default location.: Включить пользовательское расположение данных для хранения данных приложения в выбранной вами директории вместо расположения по умолчанию.
Failed to apply custom database location.: Не удалось применить пользовательское расположение базы данных.
Failed to create directory. Please check permissions and try again.: Не удалось создать директорию. Проверьте разрешения и попробуйте снова.
Failed to revert to default database location.: Не удалось вернуться к расположению базы данных по умолчанию.
Found existing "pastebar-data" folder. The application will use this folder to store data.: Найдена существующая папка "pastebar-data". Приложение будет использовать эту папку для хранения данных.
Invalid directory selected.: Выбрана недопустимая директория.
Operation when applying new path: Операция при применении нового пути
Please select a directory first.: Пожалуйста, сначала выберите директорию.
Please select a new directory different from the current one.: Пожалуйста, выберите новую директорию, отличную от текущей.
Revert to Default: Вернуться к умолчанию
Select Data Folder: Выбрать папку данных
Select Data Folder...: Выбрать папку данных...
Select a custom location to store your application data instead of the default location.: Выберите пользовательское расположение для хранения данных приложения вместо расположения по умолчанию.
Selected data folder: Выбранная папка данных
Selected new data folder for change: Выбранная новая папка данных для изменения
Setting a custom database location requires an application restart to take effect.: Установка пользовательского расположения базы данных требует перезапуска приложения для вступления в силу.
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: Выбранная папка не пуста и не содержит файлов данных PasteBar. Хотите создать подпапку "pastebar-data" для хранения данных?
This folder already contains PasteBar data. The application will use this existing data after restart.: Эта папка уже содержит данные PasteBar. Приложение будет использовать эти существующие данные после перезапуска.
Use new location: Использовать новое расположение
An error occurred during directory processing.: Произошла ошибка при обработке директории.
An error occurred during directory selection or setup.: Произошла ошибка при выборе или настройке директории.
Disable Image Capture: Отключить Захват Изображений
Disable capturing and storing images from clipboard: Отключить захват и сохранение изображений из буфера обмена

View File

@ -11,6 +11,7 @@ Confirm Clear All History: Tüm Geçmişi Temizle'yi Onayla
Do you really want to remove ALL clipboard history items?: Gerçekten TÜM pano geçmişi öğelerini kaldırmak istiyormusunuz?
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: Şu tarihten önceki pano geçmişi öğelerini kaldırmak ister misiniz? {{olderThen}} {{durationType}}
Enable Capture History: Yakalama Geçmişini Etkinleştir
Enable Image Capture: Görüntü Yakalamayı Etkinleştir
Filters:
App Filters: Uygulama Filtreleri
Audio: Ses

View File

@ -182,3 +182,38 @@ Web Scraping and Parsing: Web Kazıma ve Ayrıştırma
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Aşağıda listelenen kelimeler veya cümleler kopyalanan metinde bulunursa pano geçmişine kaydedilmeyecektir. Büyük/küçük harf duyarlı değildir.
passcode reset: parolayı sıfırla
password reset: şifreyi sıfırla
Apply and Restart: Uygula ve Yeniden Başlat
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: '"{{path}}" yeni veri klasörü olarak ayarlamak istediğinizden emin misiniz? Uygulama yeniden başlayacak.'
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: 'Veritabanını "{{path}}" konumuna {{operation}} yapmak istediğinizden emin misiniz? Uygulama yeniden başlayacak.'
Change Custom Data Folder...: Özel Veri Klasörünü Değiştir...
Change Selected Folder...: Seçili Klasörü Değiştir...
Changing the database location requires an application restart to take effect.: Veritabanı konumunu değiştirmek, etkili olması için uygulamanın yeniden başlatılmasını gerektirir.
ConfirmRevertToDefaultDbPathMessage: Varsayılan veri konumuna geri dönmek istediğinizden emin misiniz? Uygulama verileriniz varsayılan yola taşınacak.
Copy data: Veriyi kopyala
Current data folder: Mevcut veri klasörü
Custom Application Data Location: Özel Uygulama Veri Konumu
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: Özel veri konumu aktif. Değiştirebilir veya varsayılan konuma geri dönebilirsiniz. Değişiklikler uygulama yeniden başlatma gerektirir.
Default: Varsayılan
Enable custom data location to store application data in a directory of your choice instead of the default location.: Uygulama verilerini varsayılan konum yerine seçtiğiniz bir dizinde depolamak için özel veri konumunu etkinleştirin.
Failed to apply custom database location.: Özel veritabanı konumu uygulanamadı.
Failed to create directory. Please check permissions and try again.: Dizin oluşturulamadı. Lütfen izinleri kontrol edin ve tekrar deneyin.
Failed to revert to default database location.: Varsayılan veritabanı konumuna geri dönülemedi.
Found existing "pastebar-data" folder. The application will use this folder to store data.: Mevcut "pastebar-data" klasörü bulundu. Uygulama verileri depolamak için bu klasörü kullanacak.
Invalid directory selected.: Geçersiz dizin seçildi.
Operation when applying new path: Yeni yol uygulanırken işlem
Please select a directory first.: Lütfen önce bir dizin seçin.
Please select a new directory different from the current one.: Lütfen mevcut olandan farklı yeni bir dizin seçin.
Revert to Default: Varsayılana Dön
Select Data Folder: Veri Klasörü Seç
Select Data Folder...: Veri Klasörü Seç...
Select a custom location to store your application data instead of the default location.: Uygulama verilerinizi varsayılan konum yerine depolamak için özel bir konum seçin.
Selected data folder: Seçili veri klasörü
Selected new data folder for change: Değişiklik için seçili yeni veri klasörü
Setting a custom database location requires an application restart to take effect.: Özel veritabanı konumu ayarlamak, etkili olması için uygulama yeniden başlatması gerektirir.
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: Seçili klasör boş değil ve PasteBar veri dosyalarını içermiyor. Verileri depolamak için "pastebar-data" alt klasörü oluşturmak istiyor musunuz?
This folder already contains PasteBar data. The application will use this existing data after restart.: Bu klasör zaten PasteBar verileri içeriyor. Uygulama yeniden başlatıldıktan sonra mevcut verileri kullanacak.
Use new location: Yeni konumu kullan
An error occurred during directory processing.: Dizin işleme sırasında bir hata oluştu.
An error occurred during directory selection or setup.: Dizin seçimi veya kurulumu sırasında bir hata oluştu.
Disable Image Capture: Görüntü Yakalamayı Devre Dışı Bırak
Disable capturing and storing images from clipboard: Panodan görüntü yakalama ve depolamayı devre dışı bırak

View File

@ -12,6 +12,7 @@ Do you really want to remove ALL clipboard history items?: Ви дійсно х
Do you want to remove all recent clipboard history older than {{olderThen}} {{durationType}}: Ви хочете видалити всю нещодавню історію буфера обміну за період {{olderThen}} {{durationType}}
Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}: Ви хочете видалити історію буфера обміну старішу за {{olderThen}} {{durationType}}
Enable Capture History: Увімкнути захоплення історії
Enable Image Capture: Увімкнути захоплення зображень
Filters:
App Filters: Фільтри додатків
Audio: Аудіо

View File

@ -165,28 +165,38 @@ Web Scraping and Parsing: Веб-скрейпінг та аналіз
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: Слова або речення, перелічені нижче, не будуть захоплюватися в історію буфера обміну, якщо вони будуть знайдені в скопійованому тексті. Без урахування регістру.
passcode reset: скидання коду доступу
password reset: скидання пароля
Backup and Restore: Резервне копіювання та відновлення
Create Backup: Створити резервну копію
Include images in backup: Включити зображення в резервну копію
Backup Now: Створити резервну копію зараз
Restore Data: Відновити дані
Restore from File...: Відновити з файлу...
Select backup file: Вибрати файл резервної копії
Available Backups: Доступні резервні копії
Total backup space: "{{size}}": Загальний розмір резервних копій: {{size}}
No backups found: Резервні копії не знайдені
Restore: Відновити
Delete: Видалити
Create a backup of your data?: Створити резервну копію ваших даних?
Backup created successfully: Резервну копію успішно створено
Move to another location?: Перемістити в інше місце?
This will replace all current data. Are you sure?: Це замінить всі поточні дані. Ви впевнені?
Restore from "{{filename}}"? This will replace all current data.: Відновити з {{filename}}? Це замінить всі поточні дані.
Delete this backup? This action cannot be undone.: Видалити цю резервну копію? Цю дію неможливо скасувати.
Restore completed. The application will restart.: Відновлення завершено. Додаток перезапуститься.
Creating backup...: Створення резервної копії...
Restoring backup...: Відновлення резервної копії...
Backup deleted successfully: Резервну копію успішно видалено
Failed to delete backup: Не вдалося видалити резервну копію
Invalid backup file: Недійсний файл резервної копії
The selected file is not a valid PasteBar backup: Вибраний файл не є дійсною резервною копією PasteBar
Apply and Restart: Застосувати та перезапустити
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: Ви впевнені, що хочете встановити "{{path}}" як нову папку даних? Додаток буде перезапущено.
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: Ви впевнені, що хочете {{operation}} базу даних до "{{path}}"? Додаток буде перезапущено.
Change Custom Data Folder...: Змінити користувацьку папку даних...
Change Selected Folder...: Змінити вибрану папку...
Changing the database location requires an application restart to take effect.: Зміна розташування бази даних вимагає перезапуску додатка для набрання чинності.
ConfirmRevertToDefaultDbPathMessage: Ви впевнені, що хочете повернутися до розташування даних за замовчуванням? Дані вашого додатка будуть переміщені за шляхом за замовчуванням.
Copy data: Копіювати дані
Current data folder: Поточна папка даних
Custom Application Data Location: Користувацьке розташування даних додатка
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: Користувацьке розташування даних активне. Ви можете змінити його або повернутися до розташування за замовчуванням. Зміни вимагають перезапуску додатка.
Default: За замовчуванням
Enable custom data location to store application data in a directory of your choice instead of the default location.: Увімкнути користувацьке розташування даних для зберігання даних додатка в обраній вами директорії замість розташування за замовчуванням.
Failed to apply custom database location.: Не вдалося застосувати користувацьке розташування бази даних.
Failed to create directory. Please check permissions and try again.: Не вдалося створити директорію. Перевірте дозволи та спробуйте знову.
Failed to revert to default database location.: Не вдалося повернутися до розташування бази даних за замовчуванням.
Found existing "pastebar-data" folder. The application will use this folder to store data.: Знайдено існуючу папку "pastebar-data". Додаток буде використовувати цю папку для зберігання даних.
Invalid directory selected.: Вибрано недійсну директорію.
Operation when applying new path: Операція при застосуванні нового шляху
Please select a directory first.: Будь ласка, спочатку виберіть директорію.
Please select a new directory different from the current one.: Будь ласка, виберіть нову директорію, відмінну від поточної.
Revert to Default: Повернутися до замовчування
Select Data Folder: Вибрати папку даних
Select Data Folder...: Вибрати папку даних...
Select a custom location to store your application data instead of the default location.: Виберіть користувацьке розташування для зберігання даних вашого додатка замість розташування за замовчуванням.
Selected data folder: Вибрана папка даних
Selected new data folder for change: Вибрана нова папка даних для зміни
Setting a custom database location requires an application restart to take effect.: Встановлення користувацького розташування бази даних вимагає перезапуску додатка для набрання чинності.
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: Вибрана папка не порожня і не містить файлів даних PasteBar. Хочете створити підпапку "pastebar-data" для зберігання даних?
This folder already contains PasteBar data. The application will use this existing data after restart.: Ця папка вже містить дані PasteBar. Додаток буде використовувати ці існуючі дані після перезапуску.
Use new location: Використовувати нове розташування
An error occurred during directory processing.: Сталася помилка під час обробки директорії.
An error occurred during directory selection or setup.: Сталася помилка під час вибору або налаштування директорії.
Disable Image Capture: Вимкнути Захоплення Зображень
Disable capturing and storing images from clipboard: Вимкнути захоплення та збереження зображень з буферу обміну

View File

@ -11,6 +11,7 @@ Confirm Clear All History: 确认清除所有历史
Do you really want to remove ALL clipboard history items?: 确定要删除所有剪贴板历史记录吗?
"Do you want to remove clipboard history items older than {{olderThen}} {{durationType}}": 您想删除早于 {{olderThen}} {{durationType}} 的剪贴板历史项目吗
Enable Capture History: 启用捕获历史
Enable Image Capture: 启用图像捕获
Filters:
App Filters: 应用筛选器
Audio: 音频

View File

@ -182,3 +182,38 @@ Web Scraping and Parsing: 网页抓取和解析
Words or sentences listed below will not be captured in clipboard history if found in the copied text. Case insensitive.: 如果在复制的文本中找到以下列出的词或句子,将不会被记录在剪贴板历史中。不区分大小写。
passcode reset: 密码重置
password reset: 密码重置
Apply and Restart: 应用并重启
Are you sure you want to set "{{path}}" as the new data folder? The application will restart.: 您确定要将"{{path}}"设置为新的数据文件夹吗?应用程序将重新启动。
Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.: 您确定要将数据库{{operation}}到"{{path}}"吗?应用程序将重新启动。
Change Custom Data Folder...: 更改自定义数据文件夹...
Change Selected Folder...: 更改选定文件夹...
Changing the database location requires an application restart to take effect.: 更改数据库位置需要重新启动应用程序才能生效。
ConfirmRevertToDefaultDbPathMessage: 您确定要恢复到默认数据位置吗?您的应用程序数据将移动到默认路径。
Copy data: 复制数据
Current data folder: 当前数据文件夹
Custom Application Data Location: 自定义应用程序数据位置
Custom data location is active. You can change it or revert to the default location. Changes require an application restart.: 自定义数据位置已激活。您可以更改它或恢复到默认位置。更改需要重新启动应用程序。
Default: 默认
Enable custom data location to store application data in a directory of your choice instead of the default location.: 启用自定义数据位置,将应用程序数据存储在您选择的目录中,而不是默认位置。
Failed to apply custom database location.: 应用自定义数据库位置失败。
Failed to create directory. Please check permissions and try again.: 创建目录失败。请检查权限并重试。
Failed to revert to default database location.: 恢复到默认数据库位置失败。
Found existing "pastebar-data" folder. The application will use this folder to store data.: 找到现有的"pastebar-data"文件夹。应用程序将使用此文件夹存储数据。
Invalid directory selected.: 选择的目录无效。
Operation when applying new path: 应用新路径时的操作
Please select a directory first.: 请先选择一个目录。
Please select a new directory different from the current one.: 请选择与当前不同的新目录。
Revert to Default: 恢复到默认
Select Data Folder: 选择数据文件夹
Select Data Folder...: 选择数据文件夹...
Select a custom location to store your application data instead of the default location.: 选择一个自定义位置来存储应用程序数据,而不是默认位置。
Selected data folder: 选定的数据文件夹
Selected new data folder for change: 选定的新数据文件夹用于更改
Setting a custom database location requires an application restart to take effect.: 设置自定义数据库位置需要重新启动应用程序才能生效。
The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?: 选定的文件夹不为空且不包含PasteBar数据文件。您想创建一个"pastebar-data"子文件夹来存储数据吗?
This folder already contains PasteBar data. The application will use this existing data after restart.: 此文件夹已包含PasteBar数据。应用程序将在重启后使用这些现有数据。
Use new location: 使用新位置
An error occurred during directory processing.: 目录处理期间发生错误。
An error occurred during directory selection or setup.: 目录选择或设置期间发生错误。
Disable Image Capture: 禁用图像捕获
Disable capturing and storing images from clipboard: 禁用从剪贴板捕获和存储图像

View File

@ -87,6 +87,8 @@ export default function ClipboardHistorySettings() {
setClipTextMinLength,
setClipTextMaxLength,
setIsHistoryEnabled,
isImageCaptureDisabled,
setIsImageCaptureDisabled,
isHistoryAutoUpdateOnCaputureEnabled,
setIsHistoryAutoTrimOnCaputureEnabled,
isHistoryAutoTrimOnCaputureEnabled,
@ -411,6 +413,35 @@ export default function ClipboardHistorySettings() {
</Card>
</Box>
<Box className="max-w-xl animate-in fade-in mt-4">
<Card
className={`${
!isImageCaptureDisabled &&
'opacity-80 bg-gray-100 dark:bg-gray-900/80'
}`}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
<CardTitle className="animate-in fade-in text-md font-medium w-full">
{t('Disable Image Capture', { ns: 'settings' })}
</CardTitle>
<Switch
checked={isImageCaptureDisabled}
className="ml-auto"
onCheckedChange={() => {
setIsImageCaptureDisabled(!isImageCaptureDisabled)
}}
/>
</CardHeader>
<CardContent>
<Text className="text-sm text-muted-foreground">
{t('Disable capturing and storing images from clipboard', {
ns: 'settings',
})}
</Text>
</CardContent>
</Card>
</Box>
<Box className="mt-4 max-w-xl animate-in fade-in">
<Card
className={`${

View File

@ -0,0 +1,799 @@
import { useEffect, useState } from 'react'
import { dialog, invoke } from '@tauri-apps/api'
import { join } from '@tauri-apps/api/path'
import { settingsStore, settingsStoreAtom } from '~/store'
import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import { Icons } from '~/components/icons'
import {
Box,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Flex,
Switch,
Text,
TextNormal,
} from '~/components/ui'
export default function CustomDatabaseLocationSettings() {
const { t } = useTranslation()
const {
customDbPath,
customDbPathError: storeCustomDbPathError,
dbRelocationInProgress,
validateCustomDbPath,
applyCustomDbPath,
revertToDefaultDbPath,
relaunchApp,
} = useAtomValue(settingsStoreAtom)
// States for the "Change" functionality within CardContent
const [selectedPathForChangeDialog, setSelectedPathForChangeDialog] = useState<
string | null
>(null)
const [dbOperationForChangeDialog, setDbOperationForChangeDialog] = useState<
'copy' | 'none'
>('none')
const [isApplyingChange, setIsApplyingChange] = useState(false)
// isRevertingPath state is effectively handled by isProcessing when called from CardContent's revert button
// General states
const [isProcessing, setIsProcessing] = useState(false) // For toggle switch operations and CardContent button operations
const [operationError, setOperationError] = useState<string | null>(null)
const [validationErrorForChangeDialog, setValidationErrorForChangeDialog] = useState<
string | null
>(null)
// Local state to control the expanded/collapsed state of the setup section when no custom path is set
const [isSetupSectionExpanded, setIsSetupSectionExpanded] = useState(false)
useEffect(() => {
// Reset "Change" dialog state if customDbPath changes (e.g., reverted or set)
setValidationErrorForChangeDialog(null)
setSelectedPathForChangeDialog(null)
setDbOperationForChangeDialog('none')
// Reset setup section expansion when custom path is set/unset
if (customDbPath) {
setIsSetupSectionExpanded(false)
}
}, [customDbPath])
// Effect to react to validation errors from the store for the "Change" dialog
useEffect(() => {
const state = settingsStore.getState()
if (
selectedPathForChangeDialog &&
storeCustomDbPathError &&
!state.isCustomDbPathValid
) {
setValidationErrorForChangeDialog(storeCustomDbPathError)
} else if (state.isCustomDbPathValid) {
// Clear validation error if store says path is valid (e.g. after successful validation for the selectedPathForChangeDialog)
setValidationErrorForChangeDialog(null)
}
}, [storeCustomDbPathError, selectedPathForChangeDialog])
// Renamed from handleBrowse, used by "Change" button in CardContent
const handleBrowseForChangeDialog = async () => {
setOperationError(null)
setValidationErrorForChangeDialog(null)
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
})
if (typeof selected === 'string') {
const status: any = await invoke('cmd_check_custom_data_path', {
pathStr: selected,
})
let finalPath = selected
if (status === 'HasPastebarDataSubfolder') {
// Use existing pastebar-data subfolder
finalPath = await join(selected, 'pastebar-data')
await dialog.message(
t(
'Found existing "pastebar-data" folder. The application will use this folder to store data.',
{ ns: 'settings' }
)
)
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t(
'The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?',
{ ns: 'settings' }
)
)
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data')
// Create the directory after user confirmation
try {
await invoke('cmd_create_directory', { pathStr: finalPath })
} catch (error) {
console.error('Failed to create pastebar-data directory:', error)
setOperationError(
t('Failed to create directory. Please check permissions and try again.', {
ns: 'settings',
})
)
return
}
} else {
return
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t(
'This folder already contains PasteBar data. The application will use this existing data after restart.',
{ ns: 'settings' }
)
)
}
setSelectedPathForChangeDialog(finalPath)
if (finalPath !== customDbPath) {
await validateCustomDbPath(finalPath) // Validation result will be reflected in storeCustomDbPathError
}
}
} catch (error) {
console.error('Error handling directory selection for change:', error)
setOperationError(
t('An error occurred during directory processing.', { ns: 'settings' })
)
}
}
// Renamed from handleApply, used by "Apply and Restart" button in CardContent
const handleApplyChangeDialog = async () => {
if (!selectedPathForChangeDialog || selectedPathForChangeDialog === customDbPath) {
setOperationError(
t('Please select a new directory different from the current one.', {
ns: 'settings',
})
)
return
}
setIsApplyingChange(true)
setIsProcessing(true) // General processing state
setOperationError(null)
setValidationErrorForChangeDialog(null)
// Ensure validation is re-checked or use existing validation state
await validateCustomDbPath(selectedPathForChangeDialog)
const currentStoreState = settingsStore.getState()
if (!currentStoreState.isCustomDbPathValid) {
setValidationErrorForChangeDialog(
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
)
setIsApplyingChange(false)
setIsProcessing(false)
return
}
let confirmMessage: string
if (dbOperationForChangeDialog === 'none') {
confirmMessage = t(
'Are you sure you want to set "{{path}}" as the new data folder? The application will restart.',
{
ns: 'settings',
path: selectedPathForChangeDialog,
}
)
} else {
confirmMessage = t(
'Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.',
{
ns: 'settings',
operation: t(dbOperationForChangeDialog, { ns: 'settings' }),
path: selectedPathForChangeDialog,
}
)
}
const confirmed = await dialog.confirm(confirmMessage)
if (confirmed) {
try {
await applyCustomDbPath(selectedPathForChangeDialog, dbOperationForChangeDialog)
relaunchApp()
} catch (error: any) {
// Attempt rollback on failure
try {
await revertToDefaultDbPath()
} catch (_) {
// Ignore rollback errors
}
setOperationError(
error.message ||
t('Failed to apply custom database location.', { ns: 'settings' })
)
} finally {
setIsApplyingChange(false)
setIsProcessing(false)
}
} else {
setIsApplyingChange(false)
setIsProcessing(false)
}
}
// Internal function for reverting, called by toggle or button
const revertToDefaultWithConfirmationInternal = async () => {
setOperationError(null)
const confirmed = await dialog.confirm(
t('ConfirmRevertToDefaultDbPathMessage', { ns: 'settings' })
)
if (!confirmed) {
return false // Indicates cancellation
}
setIsProcessing(true)
try {
await revertToDefaultDbPath()
relaunchApp()
return true // Indicates success
} catch (error: any) {
setOperationError(
error.message ||
t('Failed to revert to default database location.', { ns: 'settings' })
)
return false // Indicates failure
} finally {
setIsProcessing(false)
}
}
// Called by "Revert to Default" button in CardContent
const handleRevertFromContent = async () => {
await revertToDefaultWithConfirmationInternal()
}
// Handle initial setup path selection
const handleSetupPathSelection = async () => {
setOperationError(null)
setValidationErrorForChangeDialog(null)
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
})
if (typeof selected === 'string') {
const status: any = await invoke('cmd_check_custom_data_path', {
pathStr: selected,
})
let finalPath = selected
if (status === 'HasPastebarDataSubfolder') {
// Use existing pastebar-data subfolder
finalPath = await join(selected, 'pastebar-data')
await dialog.message(
t(
'Found existing "pastebar-data" folder. The application will use this folder to store data.',
{ ns: 'settings' }
)
)
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t(
'The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?',
{ ns: 'settings' }
)
)
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data')
// Create the directory after user confirmation
try {
await invoke('cmd_create_directory', { pathStr: finalPath })
} catch (error) {
console.error('Failed to create pastebar-data directory:', error)
setOperationError(
t('Failed to create directory. Please check permissions and try again.', {
ns: 'settings',
})
)
return
}
} else {
return
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t(
'This folder already contains PasteBar data. The application will use this existing data after restart.',
{ ns: 'settings' }
)
)
}
setSelectedPathForChangeDialog(finalPath)
await validateCustomDbPath(finalPath) // Validation result will be reflected in storeCustomDbPathError
}
} catch (error) {
console.error('Error handling directory selection for setup:', error)
setOperationError(
t('An error occurred during directory processing.', { ns: 'settings' })
)
}
}
// Handle applying the initial setup
const handleApplySetup = async () => {
if (!selectedPathForChangeDialog) {
setOperationError(t('Please select a directory first.', { ns: 'settings' }))
return
}
setIsApplyingChange(true)
setIsProcessing(true)
setOperationError(null)
setValidationErrorForChangeDialog(null)
// Ensure validation is re-checked
await validateCustomDbPath(selectedPathForChangeDialog)
const currentStoreState = settingsStore.getState()
if (!currentStoreState.isCustomDbPathValid) {
setValidationErrorForChangeDialog(
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
)
setIsApplyingChange(false)
setIsProcessing(false)
return
}
let confirmMessage: string
if (dbOperationForChangeDialog === 'none') {
confirmMessage = t(
'Are you sure you want to set "{{path}}" as the new data folder? The application will restart.',
{
ns: 'settings',
path: selectedPathForChangeDialog,
}
)
} else {
confirmMessage = t(
'Are you sure you want to {{operation}} the database to "{{path}}"? The application will restart.',
{
ns: 'settings',
operation: t(dbOperationForChangeDialog, { ns: 'settings' }),
path: selectedPathForChangeDialog,
}
)
}
const confirmed = await dialog.confirm(confirmMessage)
if (confirmed) {
try {
await applyCustomDbPath(selectedPathForChangeDialog, dbOperationForChangeDialog)
relaunchApp()
} catch (error: any) {
try {
await revertToDefaultDbPath()
} catch (_) {}
setOperationError(
error.message ||
t('Failed to apply custom database location.', { ns: 'settings' })
)
} finally {
setIsApplyingChange(false)
setIsProcessing(false)
}
} else {
setIsApplyingChange(false)
setIsProcessing(false)
}
}
// Function to choose and set custom path, called when toggle is turned ON
const chooseAndSetCustomPath = async () => {
setOperationError(null)
setIsProcessing(true)
let pathSuccessfullySet = false
try {
const selected = await dialog.open({
directory: true,
multiple: false,
title: t('Select Data Folder', { ns: 'settings' }),
})
if (typeof selected === 'string') {
let finalPath = selected
const status: any = await invoke('cmd_check_custom_data_path', {
pathStr: selected,
})
if (status === 'HasPastebarDataSubfolder') {
// Use existing pastebar-data subfolder
finalPath = await join(selected, 'pastebar-data')
await dialog.message(
t(
'Found existing "pastebar-data" folder. The application will use this folder to store data.',
{ ns: 'settings' }
)
)
} else if (status === 'NotEmpty') {
const confirmSubfolder = await dialog.confirm(
t(
'The selected folder is not empty and does not contain PasteBar data files. Do you want to create a "pastebar-data" subfolder to store the data?',
{ ns: 'settings' }
)
)
if (confirmSubfolder) {
finalPath = await join(selected, 'pastebar-data')
// Create the directory after user confirmation
try {
await invoke('cmd_create_directory', { pathStr: finalPath })
} catch (error) {
console.error('Failed to create pastebar-data directory:', error)
setOperationError(
t('Failed to create directory. Please check permissions and try again.', {
ns: 'settings',
})
)
setIsProcessing(false)
return false
}
} else {
setIsProcessing(false)
return false // User cancelled subfolder creation
}
} else if (status === 'IsPastebarDataAndNotEmpty') {
await dialog.message(
t(
'This folder already contains PasteBar data. The application will use this existing data after restart.',
{ ns: 'settings' }
)
)
}
await validateCustomDbPath(finalPath)
const currentStoreState = settingsStore.getState()
if (!currentStoreState.isCustomDbPathValid) {
setOperationError(
currentStoreState.customDbPathError ||
t('Invalid directory selected.', { ns: 'settings' })
)
setIsProcessing(false)
return false // Validation failed
}
const confirmed = await dialog.confirm(
t(
'Are you sure you want to set "{{path}}" as the new data folder? The application will restart.',
{ ns: 'settings', path: finalPath }
)
)
if (confirmed) {
try {
await applyCustomDbPath(finalPath, 'none') // 'none' for initial setup
relaunchApp()
pathSuccessfullySet = true // Path will be set by store, app restarts
} catch (error) {
try {
await revertToDefaultDbPath()
} catch (_) {}
setOperationError(
(error as any).message ||
t('Failed to apply custom database location.', { ns: 'settings' })
)
}
}
}
} catch (error) {
console.error('Error choosing custom DB path:', error)
setOperationError(
t('An error occurred during directory selection or setup.', {
ns: 'settings',
})
)
} finally {
setIsProcessing(false)
}
return pathSuccessfullySet // This return might not be directly used if app restarts
}
const handleToggle = async (checked: boolean) => {
if (!customDbPath) {
// If no custom path is set, toggle the setup section expansion
setIsSetupSectionExpanded(checked)
if (!checked) {
// When closing, reset any setup-related states
setOperationError(null)
setSelectedPathForChangeDialog(null)
setDbOperationForChangeDialog('none')
}
}
// If customDbPath is already set, the toggle is disabled so this won't be called
}
const isLoading = dbRelocationInProgress || isProcessing || isApplyingChange
const currentPathDisplay = customDbPath || t('Default', { ns: 'settings' })
const isPathUnchangedForChangeDialog = selectedPathForChangeDialog === customDbPath
// Determine switch state: ON if custom path is set OR if setup section is expanded
const isSwitchChecked = !!customDbPath || isSetupSectionExpanded
// Disable switch only when loading or when custom path is already set (to prevent toggling off)
const isSwitchDisabled = isLoading || !!customDbPath
return (
<Box className="animate-in fade-in max-w-xl mt-4">
<Card
className={`${
!customDbPath && !isSetupSectionExpanded
? 'opacity-80 bg-gray-100 dark:bg-gray-900/80'
: ''
}`}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
<CardTitle className="animate-in fade-in text-md font-medium">
{t('Custom Application Data Location', { ns: 'settings' })}
</CardTitle>
{!customDbPath && (
<Switch
checked={isSwitchChecked}
onCheckedChange={handleToggle}
disabled={isSwitchDisabled}
/>
)}
</CardHeader>
{!!customDbPath ? ( // Content visible only if customDbPath is set
<CardContent>
<Text className="text-sm text-muted-foreground">
{t(
'Custom data location is active. You can change it or revert to the default location. Changes require an application restart.',
{ ns: 'settings' }
)}
</Text>
<div className="space-y-4 mt-4">
<div>
<Text className="text-sm text-muted-foreground">
{t('Current data folder', { ns: 'settings' })}:
</Text>
<Text className="text-sm font-semibold text-foreground mt-1 break-all">
{currentPathDisplay}
</Text>
</div>
{selectedPathForChangeDialog && (
<div className="p-3 bg-blue-50 dark:bg-blue-950/20 rounded-md border">
<Text className="text-sm font-medium text-blue-900 dark:text-blue-100">
{t('Selected new data folder for change', { ns: 'settings' })}:{' '}
</Text>
<Text className="text-sm text-blue-700 dark:text-blue-200 break-all">
{selectedPathForChangeDialog}
</Text>
</div>
)}
{validationErrorForChangeDialog && (
<Text className="text-sm text-red-500">
{validationErrorForChangeDialog}
</Text>
)}
<Flex className="gap-3">
<Button
onClick={handleBrowseForChangeDialog}
disabled={isLoading}
variant="outline"
className="flex-1 h-10"
>
{dbRelocationInProgress && !isApplyingChange && !isProcessing ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Change Custom Data Folder...', { ns: 'settings' })}
</Button>
<Button
onClick={handleRevertFromContent}
disabled={isLoading}
variant="secondary"
className="flex-1 h-10 bg-yellow-500 hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700 text-white border-yellow-500 dark:border-yellow-600"
>
{isProcessing && !isApplyingChange ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Revert to Default', { ns: 'settings' })}
</Button>
</Flex>
{selectedPathForChangeDialog && (
<Flex className="items-center space-x-4">
<Text className="text-sm">
{t('Operation when applying new path', { ns: 'settings' })}:
</Text>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="dbOperationForChange"
value="none"
checked={dbOperationForChangeDialog === 'none'}
onChange={() => setDbOperationForChangeDialog('none')}
disabled={isLoading}
className="form-radio accent-primary"
/>
<TextNormal size="sm">
{t('Use new location', { ns: 'settings' })}
</TextNormal>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="dbOperationForChange"
value="copy"
checked={dbOperationForChangeDialog === 'copy'}
onChange={() => setDbOperationForChangeDialog('copy')}
disabled={isLoading}
className="form-radio accent-primary"
/>
<TextNormal size="sm">
{t('Copy data', { ns: 'settings' })}
</TextNormal>
</label>
</Flex>
)}
{operationError && (
<Text className="text-sm text-red-500">{operationError}</Text>
)}
{selectedPathForChangeDialog && (
<Button
onClick={handleApplyChangeDialog}
disabled={
isLoading ||
!selectedPathForChangeDialog ||
isPathUnchangedForChangeDialog ||
!!validationErrorForChangeDialog
}
className="w-full h-10"
>
{isApplyingChange ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Apply and Restart', { ns: 'settings' })}
</Button>
)}
<Text className="text-xs text-muted-foreground pt-2">
{t(
'Changing the database location requires an application restart to take effect.',
{ ns: 'settings' }
)}
</Text>
</div>
</CardContent>
) : isSetupSectionExpanded ? (
// Setup content visible when no custom path is set but section is expanded
<CardContent>
<Text className="text-sm text-muted-foreground mb-4">
{t(
'Select a custom location to store your application data instead of the default location.',
{ ns: 'settings' }
)}
</Text>
<div className="space-y-4">
{selectedPathForChangeDialog && (
<div className="p-3 bg-blue-50 dark:bg-blue-950/20 rounded-md border">
<Text className="text-sm font-medium text-blue-900 dark:text-blue-100">
{t('Selected data folder', { ns: 'settings' })}:{' '}
</Text>
<Text className="text-sm text-blue-700 dark:text-blue-200 break-all">
{selectedPathForChangeDialog}
</Text>
</div>
)}
{validationErrorForChangeDialog && (
<Text className="text-sm text-red-500">
{validationErrorForChangeDialog}
</Text>
)}
<Button
onClick={handleSetupPathSelection}
disabled={isLoading}
variant="outline"
className="w-full h-10"
>
{isProcessing && !isApplyingChange ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{selectedPathForChangeDialog
? t('Change Selected Folder...', { ns: 'settings' })
: t('Select Data Folder...', { ns: 'settings' })}
</Button>
{selectedPathForChangeDialog && (
<Flex className="items-center space-x-4">
<Text className="text-sm">
{t('Operation when applying new path', { ns: 'settings' })}:
</Text>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="dbOperationForSetup"
value="none"
checked={dbOperationForChangeDialog === 'none'}
onChange={() => setDbOperationForChangeDialog('none')}
disabled={isLoading}
className="form-radio accent-primary"
/>
<TextNormal size="sm">
{t('Use new location', { ns: 'settings' })}
</TextNormal>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="radio"
name="dbOperationForSetup"
value="copy"
checked={dbOperationForChangeDialog === 'copy'}
onChange={() => setDbOperationForChangeDialog('copy')}
disabled={isLoading}
className="form-radio accent-primary"
/>
<TextNormal size="sm">
{t('Copy data', { ns: 'settings' })}
</TextNormal>
</label>
</Flex>
)}
{operationError && (
<Text className="text-sm text-red-500">{operationError}</Text>
)}
{selectedPathForChangeDialog && (
<Button
onClick={handleApplySetup}
disabled={
isLoading ||
!selectedPathForChangeDialog ||
!!validationErrorForChangeDialog
}
className="w-full h-10"
>
{isApplyingChange ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t('Apply and Restart', { ns: 'settings' })}
</Button>
)}
<Text className="text-xs text-muted-foreground pt-2">
{t(
'Setting a custom database location requires an application restart to take effect.',
{ ns: 'settings' }
)}
</Text>
</div>
</CardContent>
) : (
// Default collapsed state when no custom path is set
<CardContent>
<Text className="text-sm text-muted-foreground">
{t(
'Enable custom data location to store application data in a directory of your choice instead of the default location.',
{ ns: 'settings' }
)}
</Text>
</CardContent>
)}
</Card>
</Box>
)
}

View File

@ -10,8 +10,9 @@ import {
themeStoreAtom,
uiStoreAtom,
} from '~/store'
import CustomDatabaseLocationSettings from './CustomDatabaseLocationSettings'
import { useAtomValue } from 'jotai'
import { MessageSquare, MessageSquareDashed } from 'lucide-react'
import { ChevronDown, ChevronUp, MessageSquare, MessageSquareDashed } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
@ -83,7 +84,7 @@ export default function UserPreferences() {
if (theme !== mode) {
setMode(theme)
}
}, [theme])
}, [theme, mode, setMode]) // Added mode and setMode to dependency array
useEffect(() => {
invoke('is_autostart_enabled').then(isEnabled => {
@ -107,6 +108,8 @@ export default function UserPreferences() {
setQuickPasteHotkey(hotKeysShowHideQuickPasteWindow)
}
}, [hotKeysShowHideMainAppWindow, hotKeysShowHideQuickPasteWindow])
// Removed mainAppHotkey, quickPasteHotkey from local state dependencies in the original thought process,
// as they are set inside this effect. The effect correctly depends on props.
const handleKeyDown = (
event: KeyboardEvent | React.KeyboardEvent<HTMLInputElement>,
@ -274,6 +277,10 @@ export default function UserPreferences() {
</Card>
</Box>
{/* ------------- Custom Database Location Settings Card ------------- */}
<CustomDatabaseLocationSettings />
{/* ------------------------------------------------------------------ */}
<Box className="animate-in fade-in max-w-xl mt-4">
<Card>
<CardHeader className="flex flex-col items-start justify-between space-y-0 pb-1">

View File

@ -26,7 +26,11 @@ import {
type Settings = {
appLastUpdateVersion: string
appLastUpdateDate: string
appDataDir: string
appDataDir: string // This might be the old customDbPath or a general app data dir. We'll add a specific one.
customDbPath: string | null // Path to the custom database directory
isCustomDbPathValid: boolean | null // Validation status of the entered customDbPath
customDbPathError: string | null // Error message if validation fails or operation fails
dbRelocationInProgress: boolean // True if a DB move/copy/revert operation is ongoing
isAppReady: boolean
isClipNotesHoverCardsEnabled: boolean
clipNotesHoverCardsDelayMS: number
@ -81,6 +85,7 @@ type Settings = {
isScreenLockPassCodeRequireOnStart: boolean
clipTextMinLength: number
clipTextMaxLength: number
isImageCaptureDisabled: boolean
}
type Constants = {
@ -88,6 +93,11 @@ type Constants = {
}
export interface SettingsStoreState {
setCustomDbPath: (path: string | null) => void
validateCustomDbPath: (path: string) => Promise<void>
applyCustomDbPath: (newPath: string, operation: 'copy' | 'none') => Promise<string>
revertToDefaultDbPath: () => Promise<string>
loadInitialCustomDbPath: () => Promise<void>
setIsHistoryEnabled: (isHistoryEnabled: boolean) => void
setIsHistoryAutoUpdateOnCaputureEnabled: (
isHistoryAutoUpdateOnCaputureEnabled: boolean
@ -120,7 +130,7 @@ export interface SettingsStoreState {
setAppToursCompletedList: (words: string[]) => void
setAppToursSkippedList: (words: string[]) => void
setHistoryDetectLanguagesPrioritizedList: (words: string[]) => void
setAppDataDir: (appDataDir: string) => void
setAppDataDir: (appDataDir: string) => void // Keep if used for other general app data
setIsAutoCloseOnCopyPaste: (isEnabled: boolean) => void
setClipNotesHoverCardsDelayMS: (delay: number) => void
setClipNotesMaxWidth: (width: number) => void
@ -150,6 +160,7 @@ export interface SettingsStoreState {
setIsKeepMainWindowClosedOnRestartEnabled: (isEnabled: boolean) => void
setIsHideCollectionsOnNavBar: (isEnabled: boolean) => void
setIsShowNavBarItemsOnHoverOnly: (isEnabled: boolean) => void
setIsImageCaptureDisabled: (isEnabled: boolean) => void
hashPassword: (pass: string) => Promise<string>
isNotTourCompletedOrSkipped: (tourName: string) => boolean
verifyPassword: (pass: string, hash: string) => Promise<boolean>
@ -175,7 +186,11 @@ const initialState: SettingsStoreState & Settings = {
appLastUpdateVersion: '0.0.1',
appLastUpdateDate: '',
isAppReady: false,
appDataDir: '',
appDataDir: '', // Default app data dir if needed for other things
customDbPath: null,
isCustomDbPathValid: null,
customDbPathError: null,
dbRelocationInProgress: false,
isHistoryEnabled: true,
isFirstRun: true,
historyDetectLanguagesEnabledList: [],
@ -230,6 +245,7 @@ const initialState: SettingsStoreState & Settings = {
isFirstRunAfterUpdate: false,
clipTextMinLength: 0,
clipTextMaxLength: 5000,
isImageCaptureDisabled: false,
CONST: {
APP_DETECT_LANGUAGES_SUPPORTED: [],
},
@ -286,8 +302,14 @@ const initialState: SettingsStoreState & Settings = {
setIsShowNavBarItemsOnHoverOnly: () => {},
setClipTextMinLength: () => {},
setClipTextMaxLength: () => {},
setIsImageCaptureDisabled: () => {},
initConstants: () => {},
setAppDataDir: () => {},
setAppDataDir: () => {}, // Keep if used for other general app data
setCustomDbPath: () => {},
validateCustomDbPath: async () => {},
applyCustomDbPath: async () => '',
revertToDefaultDbPath: async () => '',
loadInitialCustomDbPath: async () => {},
updateSetting: () => {},
setIsFirstRun: () => {},
setAppLastUpdateVersion: () => {},
@ -352,7 +374,8 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
if (
name === 'isHistoryEnabled' ||
name === 'userSelectedLanguage' ||
name === 'isAppLocked'
name === 'isAppLocked' ||
name === 'isImageCaptureDisabled'
) {
invoke('build_system_menu')
}
@ -585,6 +608,9 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
setClipTextMaxLength: async (length: number) => {
return get().updateSetting('clipTextMaxLength', length)
},
setIsImageCaptureDisabled: async (isEnabled: boolean) => {
return get().updateSetting('isImageCaptureDisabled', isEnabled)
},
isNotTourCompletedOrSkipped: (tourName: string) => {
const { appToursCompletedList, appToursSkippedList } = get()
return (
@ -738,10 +764,77 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
availableVersionDateISO.value = null
},
initConstants: (CONST: Constants) => set(() => ({ CONST })),
setAppDataDir: (appDataDir: string) =>
setAppDataDir: (
appDataDir: string // Keep if used for other general app data
) =>
set(() => ({
appDataDir,
})),
// Actions for custom DB path
setCustomDbPath: (path: string | null) =>
set({ customDbPath: path, isCustomDbPathValid: null, customDbPathError: null }),
loadInitialCustomDbPath: async () => {
try {
const path = await invoke('cmd_get_custom_db_path')
set({ customDbPath: path as string | null })
} catch (error) {
console.error('Failed to load initial custom DB path:', error)
set({ customDbPathError: 'Failed to load custom DB path setting.' })
}
},
validateCustomDbPath: async (path: string) => {
set({
dbRelocationInProgress: true,
customDbPathError: null,
isCustomDbPathValid: null,
})
try {
await invoke('cmd_validate_custom_db_path', { pathStr: path })
set({ isCustomDbPathValid: true, dbRelocationInProgress: false })
} catch (error) {
console.error('Custom DB path validation failed:', error)
set({
isCustomDbPathValid: false,
customDbPathError: error as string,
dbRelocationInProgress: false,
})
}
},
applyCustomDbPath: async (newPath: string, operation: 'copy' | 'none') => {
set({ dbRelocationInProgress: true, customDbPathError: null })
try {
const message = await invoke('cmd_set_and_relocate_data', {
newParentDirPath: newPath,
operation,
})
set({
customDbPath: newPath,
isCustomDbPathValid: true,
dbRelocationInProgress: false,
})
return message as string
} catch (error) {
console.error('Failed to apply custom DB path:', error)
set({ customDbPathError: error as string, dbRelocationInProgress: false })
throw error
}
},
revertToDefaultDbPath: async () => {
set({ dbRelocationInProgress: true, customDbPathError: null })
try {
const message = await invoke('cmd_revert_to_default_data_location')
set({
customDbPath: null,
isCustomDbPathValid: null,
dbRelocationInProgress: false,
})
return message as string
} catch (error) {
console.error('Failed to revert to default DB path:', error)
set({ customDbPathError: error as string, dbRelocationInProgress: false })
throw error
}
},
setAppLastUpdateVersion: (appLastUpdateVersion: string) => {
return get().updateSetting('appLastUpdateVersion', appLastUpdateVersion)
},

File diff suppressed because one or more lines are too long

2470
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ edition = "2021"
rust-version = "1.75.0"
[build-dependencies.tauri-build]
version = "1.4"
version = "1.5"
features = []
[target.'cfg(target_os = "windows")'.dependencies]
@ -16,6 +16,7 @@ winapi = { version = "0.3", features = ["winuser", "windef"] }
winreg = "0.52.0"
[dependencies]
fs_extra = "1.3.0"
fns = "0"
mouse_position = "0.1.4"
keyring = "2.3.2"
@ -40,7 +41,7 @@ reqwest = "0.11.12"
nanoid = "0.4.0"
anyhow = "1.0.66"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.7.1", features = [ "api-all", "system-tray", "icon-png", "clipboard", "updater", "reqwest-client"] }
tauri = { version = "1.8.3", features = [ "api-all", "system-tray", "icon-png", "clipboard", "updater", "reqwest-client"] }
tokio = { version = "1.28.2", features = ["full"] }
#single-instance = "0.3.3"
@ -65,7 +66,7 @@ chrono = { version = "0.4.24", features = ["serde"] }
uuid = "1.3.1"
once_cell = "1.7.0"
thiserror = "1.0"
arboard = "3.2.1"
arboard = "3.5.0"
image = "0.24.9"
tempfile = "3"
base64 = "0.22.0"
@ -90,7 +91,7 @@ zip = "0.6"
[target.'cfg(target_os = "macos")'.dependencies]
macos-accessibility-client = { git = "https://github.com/kurdin/macos-accessibility-client", branch = "master", version = "0.0.1" }
objc = "0.2.7"
cocoa = "0.25.0"
cocoa = "0.26.1"
[target.'cfg(target_os = "linux")'.dependencies]
inputbotlinux = { path = "libs/inputbotlinux" }

View File

@ -22,6 +22,7 @@ use active_win_pos_rs::get_active_window;
use crate::cron_jobs;
use crate::models::Setting;
use crate::services::history_service;
use crate::services::utils::debug_output;
#[derive(Debug)]
pub struct LanguageDetectOptions {
@ -230,6 +231,19 @@ where
}
}
} else if let Ok(image_binary) = clipboard_manager.get_image_binary() {
// Check if image capturing is disabled
let is_image_capture_disabled = settings_map
.get("isImageCaptureDisabled")
.and_then(|s| s.value_bool)
.unwrap_or(false);
if is_image_capture_disabled {
debug_output(|| {
println!("Image capturing is disabled, skipping image capture!");
});
return CallbackResult::Next;
}
let mut is_app_excluded = false;
if let Some(setting) = settings_map.get("isExclusionAppListEnabled") {
@ -299,58 +313,7 @@ impl ClipboardManager {
clipboard.set_text(text).map_err(|err| err.to_string())
}
pub fn read_image(&self) -> Result<String, String> {
let mut clipboard = Clipboard::new().unwrap();
let image = clipboard.get_image().map_err(|err| err.to_string())?;
let tmp_dir = tempfile::Builder::new()
.prefix("clipboard-img")
.tempdir()
.map_err(|err| err.to_string())?;
let fname = tmp_dir.path().join("clipboard-img.png");
let image2: RgbaImage = ImageBuffer::from_raw(
image.width.try_into().unwrap(),
image.height.try_into().unwrap(),
image.bytes.into_owned(),
)
.unwrap();
image2.save(fname.clone()).map_err(|err| err.to_string())?;
let mut file = File::open(fname.clone()).unwrap();
let mut buffer = vec![];
file.read_to_end(&mut buffer).unwrap();
let base64_str = general_purpose::STANDARD_NO_PAD.encode(buffer);
Ok(base64_str)
}
pub fn get_image_binary(&self) -> Result<ImageData, String> {
let mut clipboard = Clipboard::new().unwrap();
let image_data = clipboard.get_image().map_err(|err| err.to_string())?;
Ok(image_data)
}
pub fn read_image_binary(&self) -> Result<Vec<u8>, String> {
let mut clipboard = Clipboard::new().unwrap();
let image = clipboard.get_image().map_err(|err| err.to_string())?;
let tmp_dir = tempfile::Builder::new()
.prefix("clipboard-img")
.tempdir()
.map_err(|err| err.to_string())?;
let fname = tmp_dir.path().join("clipboard-img.png");
let image2: RgbaImage = ImageBuffer::from_raw(
image.width.try_into().unwrap(),
image.height.try_into().unwrap(),
image.bytes.into_owned(),
)
.unwrap();
image2.save(fname.clone()).map_err(|err| err.to_string())?;
let mut file = File::open(fname.clone()).unwrap();
let mut buffer = vec![];
file.read_to_end(&mut buffer).unwrap();
Ok(buffer)
}
// write_image function remains unchanged as it's writing, not reading
pub fn write_image(&self, base64_image: String) -> Result<(), String> {
let mut clipboard = Clipboard::new().unwrap();
let decoded = general_purpose::STANDARD_NO_PAD
@ -373,6 +336,139 @@ impl ClipboardManager {
.map_err(|err| err.to_string())?;
Ok(())
}
pub fn read_image(&self) -> Result<String, String> {
let mut clipboard = Clipboard::new().unwrap();
let image = clipboard.get_image().map_err(|err| err.to_string())?;
// Handle stride alignment
let bytes_per_pixel = 4; // RGBA
let expected_bytes_per_row = image.width * bytes_per_pixel;
let actual_bytes_per_row = image.bytes.len() / image.height;
let cleaned_bytes = if actual_bytes_per_row != expected_bytes_per_row {
// Remove stride padding
let mut cleaned = Vec::with_capacity(expected_bytes_per_row * image.height);
for row in 0..image.height {
let row_start = row * actual_bytes_per_row;
let row_end = row_start + expected_bytes_per_row;
cleaned.extend_from_slice(&image.bytes[row_start..row_end]);
}
cleaned
} else {
image.bytes.into_owned()
};
// Create image from cleaned bytes
let image2: RgbaImage = ImageBuffer::from_raw(
image.width.try_into().unwrap(),
image.height.try_into().unwrap(),
cleaned_bytes,
)
.ok_or_else(|| "Failed to create image from raw bytes".to_string())?;
// Save to temporary file and encode as base64
let tmp_dir = tempfile::Builder::new()
.prefix("clipboard-img")
.tempdir()
.map_err(|err| err.to_string())?;
let fname = tmp_dir.path().join("clipboard-img.png");
image2.save(&fname).map_err(|err| err.to_string())?;
let mut file = File::open(&fname).map_err(|err| err.to_string())?;
let mut buffer = vec![];
file
.read_to_end(&mut buffer)
.map_err(|err| err.to_string())?;
let base64_str = general_purpose::STANDARD_NO_PAD.encode(buffer);
Ok(base64_str)
}
pub fn get_image_binary(&self) -> Result<ImageData, String> {
let mut clipboard = Clipboard::new().unwrap();
let image_data = clipboard.get_image().map_err(|err| err.to_string())?;
// Only check for stride alignment on Windows
#[cfg(target_os = "windows")]
{
let bytes_per_pixel = 4; // RGBA
let expected_bytes_per_row = image_data.width * bytes_per_pixel;
let actual_bytes_per_row = image_data.bytes.len() / image_data.height;
if actual_bytes_per_row != expected_bytes_per_row {
// We have stride padding, need to remove it
let mut cleaned_bytes = Vec::with_capacity(expected_bytes_per_row * image_data.height);
for row in 0..image_data.height {
let row_start = row * actual_bytes_per_row;
let row_end = row_start + expected_bytes_per_row;
cleaned_bytes.extend_from_slice(&image_data.bytes[row_start..row_end]);
}
return Ok(ImageData {
width: image_data.width,
height: image_data.height,
bytes: Cow::Owned(cleaned_bytes),
});
}
}
// For macOS, Linux, and Windows without padding, return as-is
Ok(image_data)
}
// Function 2: Returns Vec<u8> of PNG file data
pub fn read_image_binary(&self) -> Result<Vec<u8>, String> {
let mut clipboard = Clipboard::new().unwrap();
let image = clipboard.get_image().map_err(|err| err.to_string())?;
// Handle stride alignment
let bytes_per_pixel = 4; // RGBA
let expected_bytes_per_row = image.width * bytes_per_pixel;
let actual_bytes_per_row = image.bytes.len() / image.height;
let cleaned_bytes = if actual_bytes_per_row != expected_bytes_per_row {
// Remove stride padding
let mut cleaned = Vec::with_capacity(expected_bytes_per_row * image.height);
for row in 0..image.height {
let row_start = row * actual_bytes_per_row;
let row_end = row_start + expected_bytes_per_row;
cleaned.extend_from_slice(&image.bytes[row_start..row_end]);
}
cleaned
} else {
image.bytes.into_owned()
};
// Create image from cleaned bytes
let image2: RgbaImage = ImageBuffer::from_raw(
image.width.try_into().unwrap(),
image.height.try_into().unwrap(),
cleaned_bytes,
)
.ok_or_else(|| "Failed to create image from raw bytes".to_string())?;
// Save to temporary file and read back
let tmp_dir = tempfile::Builder::new()
.prefix("clipboard-img")
.tempdir()
.map_err(|err| err.to_string())?;
let fname = tmp_dir.path().join("clipboard-img.png");
image2.save(&fname).map_err(|err| err.to_string())?;
let mut file = File::open(&fname).map_err(|err| err.to_string())?;
let mut buffer = vec![];
file
.read_to_end(&mut buffer)
.map_err(|err| err.to_string())?;
Ok(buffer)
}
}
/// Initializes the plugin.

View File

@ -130,12 +130,16 @@ pub fn copy_history_item(app_handle: AppHandle, history_id: String) -> String {
if let (Some(true), Some(false)) = (history_item.is_image, history_item.is_link) {
let base64_image = match history_item.image_path_full_res {
Some(path) => match std::fs::read(&path) {
Some(path) => {
// Convert relative path to absolute path
let absolute_path = crate::db::to_absolute_image_path(&path);
match std::fs::read(&absolute_path) {
Ok(img_data) => base64::encode(&img_data),
Err(e) => {
eprintln!("Failed to read image from path: {}", e);
IMAGE_NOT_FOUND_BASE64.to_string()
}
}
},
None => IMAGE_NOT_FOUND_BASE64.to_string(),
};
@ -412,12 +416,16 @@ pub async fn copy_clip_item(
}
} else if let (Some(true), Some(false)) = (item.is_image, item.is_link) {
let base64_image = match item.image_path_full_res {
Some(path) => match std::fs::read(&path) {
Some(path) => {
// Convert relative path to absolute path
let absolute_path = crate::db::to_absolute_image_path(&path);
match std::fs::read(&absolute_path) {
Ok(img_data) => base64::encode(&img_data),
Err(e) => {
eprintln!("Failed to read image from path: {}", e);
IMAGE_NOT_FOUND_BASE64.to_string()
}
}
},
None => IMAGE_NOT_FOUND_BASE64.to_string(),
};

View File

@ -272,7 +272,9 @@ pub async fn save_to_file_history_item(
} else if let (Some(image_path), Some(true)) =
(history_item.image_path_full_res, history_item.is_image)
{
img_data = Some(std::fs::read(&image_path).map_err(|e| e.to_string())?);
// Convert relative path to absolute path
let absolute_path = crate::db::to_absolute_image_path(&image_path);
img_data = Some(std::fs::read(&absolute_path).map_err(|e| e.to_string())?);
} else if let Some(true) = history_item.is_link {
if let Some(image_url) = &history_item.value {
let parsed_url = Url::parse(&ensure_url_prefix(image_url)).map_err(|e| e.to_string())?;

View File

@ -538,7 +538,9 @@ pub async fn save_to_file_clip_item(
}
}
} else if let (Some(image_path), Some(true)) = (item.image_path_full_res, item.is_image) {
img_data = Some(std::fs::read(&image_path).map_err(|e| e.to_string())?);
// Convert relative path to absolute path
let absolute_path = crate::db::to_absolute_image_path(&image_path);
img_data = Some(std::fs::read(&absolute_path).map_err(|e| e.to_string())?);
} else if let Some(true) = item.is_link {
if let Some(image_url) = &item.value {
let parsed_url = Url::parse(&ensure_url_prefix(image_url)).map_err(|e| e.to_string())?;

View File

@ -10,3 +10,4 @@ pub(crate) mod security_commands;
pub(crate) mod shell_commands;
pub(crate) mod tabs_commands;
pub(crate) mod translations_commands;
pub(crate) mod user_settings_command;

View File

@ -0,0 +1,258 @@
use serde_yaml::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tauri::command;
use crate::db::{
get_clip_images_dir, get_clipboard_images_dir, get_data_dir, get_db_path, get_default_data_dir,
get_default_db_path_string,
};
use crate::services::user_settings_service::{
self as user_settings_service, get_all_settings, get_custom_db_path, get_setting,
remove_custom_db_path, remove_setting, set_custom_db_path, set_setting,
};
use fs_extra::dir::{copy, CopyOptions};
use std::path::PathBuf;
fn rollback_moves(items: &[(PathBuf, PathBuf)]) {
for (src, dest) in items.iter().rev() {
if dest.exists() {
if dest.is_dir() {
let mut options = CopyOptions::new();
options.overwrite = true;
let _ = copy(dest, src, &options);
let _ = fs::remove_dir_all(dest);
} else {
let _ = fs::copy(dest, src);
let _ = fs::remove_file(dest);
}
}
}
}
#[derive(serde::Serialize)]
pub enum PathStatus {
Empty,
NotEmpty,
IsPastebarDataAndNotEmpty,
HasPastebarDataSubfolder,
}
/// Checks the status of a given path.
#[command]
pub fn cmd_check_custom_data_path(path_str: String) -> Result<PathStatus, String> {
let path = Path::new(&path_str);
if !path.exists() || !path.is_dir() {
return Ok(PathStatus::Empty); // Treat non-existent paths as empty for this purpose
}
// Check if the selected path itself is named "pastebar-data"
if path.file_name().and_then(|n| n.to_str()) == Some("pastebar-data") {
if path.read_dir().map_err(|e| e.to_string())?.next().is_some() {
return Ok(PathStatus::IsPastebarDataAndNotEmpty);
}
}
// Check if directory contains PasteBar database files directly
let has_dev_db = path.join("local.pastebar-db.data").exists();
let has_prod_db = path.join("pastebar-db.data").exists();
if has_dev_db || has_prod_db {
return Ok(PathStatus::IsPastebarDataAndNotEmpty);
}
// Check if there's a "pastebar-data" subfolder in the selected directory
let pastebar_data_subfolder = path.join("pastebar-data");
if pastebar_data_subfolder.exists() && pastebar_data_subfolder.is_dir() {
// Check if the pastebar-data subfolder contains database files
let has_dev_db_in_subfolder = pastebar_data_subfolder
.join("local.pastebar-db.data")
.exists();
let has_prod_db_in_subfolder = pastebar_data_subfolder.join("pastebar-db.data").exists();
if has_dev_db_in_subfolder || has_prod_db_in_subfolder {
return Ok(PathStatus::IsPastebarDataAndNotEmpty);
} else {
return Ok(PathStatus::HasPastebarDataSubfolder);
}
}
if path.read_dir().map_err(|e| e.to_string())?.next().is_some() {
return Ok(PathStatus::NotEmpty);
}
Ok(PathStatus::Empty)
}
/// Returns the current `custom_db_path` (if any).
#[command]
pub fn cmd_get_custom_db_path() -> Option<String> {
get_custom_db_path()
}
// cmd_set_custom_db_path is now part of cmd_set_and_relocate_db
// cmd_remove_custom_db_path is now part of cmd_revert_to_default_db_location
/// Creates a directory at the specified path.
#[command]
pub fn cmd_create_directory(path_str: String) -> Result<(), String> {
let path = Path::new(&path_str);
fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create directory {}: {}", path.display(), e))?;
Ok(())
}
/// Validates if the provided path is a writable directory.
#[command]
pub fn cmd_validate_custom_db_path(path_str: String) -> Result<bool, String> {
let input_path = PathBuf::from(&path_str);
if input_path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err("Path traversal not allowed".to_string());
}
let path = if input_path.exists() {
input_path
.canonicalize()
.map_err(|e| format!("Invalid path: {}", e))?
} else {
input_path
};
if path.exists() && !path.is_dir() {
return Err(format!("Path {} is not a directory.", path.display()));
}
// Check writability by trying to create a temporary file
let temp_file_path = path.join(".tmp_pastebar_writable_check");
match fs::File::create(&temp_file_path) {
Ok(_) => {
fs::remove_file(&temp_file_path)
.map_err(|e| format!("Failed to remove temporary check file: {}", e))?;
Ok(true)
}
Err(e) => Err(format!("Directory {} is not writable: {}", path_str, e)),
}
}
/// Sets the custom data path, and moves/copies the data directory.
#[command]
pub fn cmd_set_and_relocate_data(
new_parent_dir_path: String,
operation: String,
) -> Result<String, String> {
let current_data_dir = get_data_dir();
let new_data_dir = PathBuf::from(&new_parent_dir_path);
fs::create_dir_all(&new_data_dir)
.map_err(|e| format!("Failed to create new data directory: {}", e))?;
let items_to_relocate = vec!["pastebar-db.data", "clip-images", "clipboard-images"];
let mut moved_items: Vec<(PathBuf, PathBuf)> = Vec::new();
for item_name in items_to_relocate {
let source_path = current_data_dir.join(item_name);
let dest_path = new_data_dir.join(item_name);
if !source_path.exists() {
println!(
"Source item {} does not exist, skipping.",
source_path.display()
);
continue;
}
match operation.as_str() {
"move" => {
if source_path.is_dir() {
let mut options = CopyOptions::new();
options.overwrite = true;
copy(&source_path, &dest_path, &options)
.map_err(|e| format!("Failed to copy directory: {}", e))?;
if let Err(e) = fs::remove_dir_all(&source_path) {
let _ = fs_extra::dir::remove(&dest_path);
rollback_moves(&moved_items);
return Err(format!("Failed to remove original directory: {}", e));
}
} else {
fs::copy(&source_path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
if let Err(e) = fs::remove_file(&source_path) {
let _ = fs::remove_file(&dest_path);
rollback_moves(&moved_items);
return Err(format!("Failed to remove original file: {}", e));
}
}
moved_items.push((source_path.clone(), dest_path.clone()));
}
"copy" => {
if source_path.is_dir() {
let mut options = CopyOptions::new();
options.overwrite = true;
copy(&source_path, &dest_path, &options)
.map_err(|e| format!("Failed to copy directory: {}", e))?;
} else {
fs::copy(&source_path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
}
}
"none" => {
// Do nothing, just switch to the new location
}
_ => return Err("Invalid operation specified. Use 'move', 'copy', or 'none'.".to_string()),
}
}
user_settings_service::set_custom_db_path(&new_parent_dir_path)?;
crate::db::reinitialize_connection_pool();
Ok(format!(
"Data successfully {} to {}. Please restart the application.",
operation,
new_data_dir.display()
))
}
/// Clears the custom data path setting.
#[command]
pub fn cmd_revert_to_default_data_location() -> Result<String, String> {
// Simply remove the custom database path setting
remove_custom_db_path()?;
crate::db::reinitialize_connection_pool();
Ok("Custom database location setting removed successfully.".to_string())
}
/// Return all key-value pairs from the `data` map.
#[command]
pub fn cmd_get_all_settings() -> HashMap<String, Value> {
get_all_settings()
}
/// Return a single setting by key (from the `data` map).
#[command]
pub fn cmd_get_setting(key: String) -> Option<Value> {
get_setting(&key)
}
/// Insert (or update) a setting in the `data` map.
/// `value_yaml` is a string containing valid YAML (e.g. `"true"`, `"42"`, `"some string"`).
#[command]
pub fn cmd_set_setting(key: String, value_yaml: String) -> Result<(), String> {
// If your front end only sends strings,
// you could store them directly as `Value::String(value_yaml)`.
// But here we parse the YAML so you can handle booleans, numbers, etc.
match serde_yaml::from_str::<Value>(&value_yaml) {
Ok(val) => set_setting(&key, val),
Err(e) => Err(format!("Failed to parse YAML string: {}", e)),
}
}
/// Remove a setting by key from the `data` map.
#[command]
pub fn cmd_remove_setting(key: String) -> Result<(), String> {
remove_setting(&key)
}

View File

@ -4,12 +4,14 @@ use serde::Serialize;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::RwLock;
use std::time::Duration;
use diesel::connection::SimpleConnection;
use diesel::prelude::*;
use diesel::r2d2 as diesel_r2d2;
use crate::services::user_settings_service::load_user_config;
use diesel::sqlite::SqliteConnection;
// use diesel::connection::{set_default_instrumentation, Instrumentation, InstrumentationEvent};
@ -38,7 +40,7 @@ pub struct ConnectionOptions {
}
lazy_static! {
pub static ref DB_POOL_CONNECTION: Pool = init_connection_pool();
pub static ref DB_POOL_CONNECTION: RwLock<Pool> = RwLock::new(init_connection_pool());
}
impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
@ -61,7 +63,7 @@ impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
}
}
fn adjust_canonicalization<P: AsRef<Path>>(p: P) -> String {
pub fn adjust_canonicalization<P: AsRef<Path>>(p: P) -> String {
const VERBATIM_PREFIX: &str = r#"\\?\"#;
let p = p.as_ref().display().to_string();
if p.starts_with(VERBATIM_PREFIX) {
@ -91,6 +93,12 @@ fn init_connection_pool() -> Pool {
.expect("Failed to create db pool.")
}
pub fn reinitialize_connection_pool() {
let new_pool = init_connection_pool();
let mut pool_lock = DB_POOL_CONNECTION.write().unwrap();
*pool_lock = new_pool;
}
pub fn init(app: &mut tauri::App) {
let config = app.config().clone();
@ -181,6 +189,8 @@ pub fn establish_pool_db_connection(
});
DB_POOL_CONNECTION
.read()
.unwrap()
.get()
.unwrap_or_else(|_| panic!("Error connecting to db pool"))
}
@ -216,40 +226,163 @@ fn db_file_exists() -> bool {
Path::new(&db_path).exists()
}
fn get_db_path() -> String {
if cfg!(debug_assertions) {
let app_dir = APP_CONSTANTS.get().unwrap().app_dev_data_dir.clone();
let path = if cfg!(target_os = "macos") {
format!(
"{}/local.pastebar-db.data",
adjust_canonicalization(app_dir)
)
} else if cfg!(target_os = "windows") {
format!(
"{}\\local.pastebar-db.data",
adjust_canonicalization(app_dir)
)
/// Returns the base directory for application data.
/// This will be a `pastebar-data` subdirectory if a custom path is set.
pub fn get_data_dir() -> PathBuf {
let user_config = load_user_config();
if let Some(custom_path_str) = user_config.custom_db_path {
PathBuf::from(custom_path_str)
} else {
format!(
"{}/local.pastebar-db.data",
adjust_canonicalization(app_dir)
)
get_default_data_dir()
}
}
/// Returns the default application data directory.
pub fn get_default_data_dir() -> PathBuf {
if cfg!(debug_assertions) {
APP_CONSTANTS.get().unwrap().app_dev_data_dir.clone()
} else {
APP_CONSTANTS.get().unwrap().app_data_dir.clone()
}
}
pub fn get_db_path() -> String {
let filename = if cfg!(debug_assertions) {
"local.pastebar-db.data"
} else {
"pastebar-db.data"
};
path
let db_path = get_data_dir().join(filename);
db_path.to_string_lossy().into_owned()
}
/// Returns the path to the `clip-images` directory.
pub fn get_clip_images_dir() -> PathBuf {
get_data_dir().join("clip-images")
}
/// Returns the path to the `clipboard-images` directory.
pub fn get_clipboard_images_dir() -> PathBuf {
get_data_dir().join("clipboard-images")
}
/// Returns the default database file path as a string.
pub fn get_default_db_path_string() -> String {
let db_path = get_default_data_dir().join("pastebar-db.data");
db_path.to_string_lossy().into_owned()
}
/// Converts an absolute image path to a relative path with {{base_folder}} placeholder
pub fn to_relative_image_path(absolute_path: &str) -> String {
let data_dir = get_data_dir();
let data_dir_str = data_dir.to_string_lossy();
if absolute_path.starts_with(&data_dir_str.as_ref()) {
// Remove the data directory prefix and replace with placeholder
let relative_path = absolute_path
.strip_prefix(&data_dir_str.as_ref())
.unwrap_or(absolute_path)
.trim_start_matches('/')
.trim_start_matches('\\');
format!("{{{{base_folder}}}}/{}", relative_path)
} else {
// If path doesn't start with data dir, return as is
absolute_path.to_string()
}
}
/// Converts a relative image path with {{base_folder}} placeholder to absolute path
pub fn to_absolute_image_path(relative_path: &str) -> String {
if relative_path.starts_with("{{base_folder}}") {
let data_dir = get_data_dir();
let path_without_placeholder = relative_path
.strip_prefix("{{base_folder}}")
.unwrap_or(relative_path)
.trim_start_matches('/')
.trim_start_matches('\\');
data_dir
.join(path_without_placeholder)
.to_string_lossy()
.into_owned()
} else {
// If path doesn't have placeholder, return as is
relative_path.to_string()
}
}
fn can_access_or_create(db_path: &str) -> bool {
let path = std::path::Path::new(db_path);
if let Some(parent) = path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!(
"Failed to create parent directory '{}': {}",
parent.display(),
e
);
return false;
}
}
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)
{
Ok(_file) => true,
Err(e) => {
eprintln!("Failed to open custom DB path '{}': {}", db_path, e);
false
}
}
}
pub fn get_config_file_path() -> PathBuf {
if cfg!(debug_assertions) {
let app_dir = APP_CONSTANTS
.get()
.expect("APP_CONSTANTS not initialized")
.app_dev_data_dir
.clone();
if cfg!(target_os = "macos") {
PathBuf::from(format!(
"{}/pastebar_settings.yaml",
adjust_canonicalization(app_dir)
))
} else if cfg!(target_os = "windows") {
PathBuf::from(format!(
"{}\\pastebar_settings.yaml",
adjust_canonicalization(app_dir)
))
} else {
PathBuf::from(format!(
"{}/pastebar_settings.yaml",
adjust_canonicalization(app_dir)
))
}
} else {
// Release mode
let app_data_dir = APP_CONSTANTS.get().unwrap().app_data_dir.clone();
let data_dir = app_data_dir.as_path();
let path = if cfg!(target_os = "macos") {
format!("{}/pastebar-db.data", adjust_canonicalization(data_dir))
if cfg!(target_os = "macos") {
PathBuf::from(format!(
"{}/pastebar_settings.yaml",
adjust_canonicalization(data_dir)
))
} else if cfg!(target_os = "windows") {
format!("{}\\pastebar-db.data", adjust_canonicalization(data_dir))
PathBuf::from(format!(
"{}\\pastebar_settings.yaml",
adjust_canonicalization(data_dir)
))
} else {
format!("{}/pastebar-db.data", adjust_canonicalization(data_dir))
};
path
PathBuf::from(format!(
"{}/pastebar_settings.yaml",
adjust_canonicalization(data_dir)
))
}
}
}

View File

@ -64,6 +64,8 @@ use commands::security_commands;
use commands::shell_commands;
use commands::tabs_commands;
use commands::translations_commands;
use commands::user_settings_command;
use db::AppConstants;
use mouse_position::mouse_position::Mouse;
use std::collections::HashMap;
@ -739,7 +741,9 @@ async fn main() {
None => return (),
};
let img_data = std::fs::read(&image_path).expect("Failed to read image from path");
// Convert relative path to absolute path
let absolute_path = db::to_absolute_image_path(&image_path);
let img_data = std::fs::read(&absolute_path).expect("Failed to read image from path");
let base64_image = base64::encode(&img_data);
write_image_to_clipboard(base64_image).expect("Failed to write image to clipboard");
@ -832,7 +836,9 @@ async fn main() {
None => return (),
};
let img_data = std::fs::read(&image_path).expect("Failed to read image from path");
// Convert relative path to absolute path
let absolute_path = db::to_absolute_image_path(&image_path);
let img_data = std::fs::read(&absolute_path).expect("Failed to read image from path");
let base64_image = base64::encode(&img_data);
write_image_to_clipboard(base64_image).expect("Failed to write image to clipboard");
@ -1189,6 +1195,18 @@ async fn main() {
security_commands::verify_os_password,
security_commands::delete_os_password,
security_commands::get_stored_os_password,
user_settings_command::cmd_get_custom_db_path,
// user_settings_command::cmd_set_custom_db_path, // Replaced by cmd_set_and_relocate_db
// user_settings_command::cmd_remove_custom_db_path, // Replaced by cmd_revert_to_default_db_location
user_settings_command::cmd_create_directory,
user_settings_command::cmd_validate_custom_db_path,
user_settings_command::cmd_check_custom_data_path,
user_settings_command::cmd_set_and_relocate_data,
user_settings_command::cmd_revert_to_default_data_location,
user_settings_command::cmd_get_all_settings,
user_settings_command::cmd_get_setting,
user_settings_command::cmd_set_setting,
user_settings_command::cmd_remove_setting,
open_osx_accessibility_preferences,
check_osx_accessibility_preferences,
open_path_or_app,

View File

@ -331,3 +331,21 @@ pub struct Tabs {
pub tab_layout_split: i32,
pub tab_is_protected: bool,
}
impl Item {
/// Transforms the image_path_full_res field from relative to absolute path for frontend consumption
pub fn transform_image_path_for_frontend(&mut self) {
if let Some(ref mut path) = self.image_path_full_res {
*path = crate::db::to_absolute_image_path(path);
}
}
}
impl ClipboardHistory {
/// Transforms the image_path_full_res field from relative to absolute path for frontend consumption
pub fn transform_image_path_for_frontend(&mut self) {
if let Some(ref mut path) = self.image_path_full_res {
*path = crate::db::to_absolute_image_path(path);
}
}
}

View File

@ -361,9 +361,15 @@ pub fn get_active_collection_with_menu_items() -> Result<CollectionWithItems, Er
))
.load::<AssociatedMenu>(connection)?;
// Transform image paths for frontend consumption
let mut transformed_items = associated_items;
for item in &mut transformed_items {
item.transform_image_path_for_frontend();
}
Ok(CollectionWithItems {
collection,
items: associated_items,
items: transformed_items,
})
}
@ -447,9 +453,15 @@ pub fn get_active_collection_with_clips() -> Result<CollectionWithClips, Error>
))
.load::<AssociatedClips>(connection);
// Transform image paths for frontend consumption
let mut transformed_clips = associated_clips?;
for clip in &mut transformed_clips {
clip.transform_image_path_for_frontend();
}
Ok(CollectionWithClips {
collection,
clips: associated_clips?,
clips: transformed_clips,
tabs: collection_tabs,
})
}
@ -740,3 +752,21 @@ pub fn create_default_board_item(
Ok(new_item.item_id)
}
impl AssociatedClips {
/// Transforms the image_path_full_res field from relative to absolute path for frontend consumption
pub fn transform_image_path_for_frontend(&mut self) {
if let Some(ref mut path) = self.image_path_full_res {
*path = crate::db::to_absolute_image_path(path);
}
}
}
impl AssociatedMenu {
/// Transforms the image_path_full_res field from relative to absolute path for frontend consumption
pub fn transform_image_path_for_frontend(&mut self) {
if let Some(ref mut path) = self.image_path_full_res {
*path = crate::db::to_absolute_image_path(path);
}
}
}

View File

@ -32,7 +32,7 @@ use std::path::{Path, PathBuf};
use std::io::Cursor;
use crate::db::APP_CONSTANTS;
use crate::db::{self, APP_CONSTANTS};
use crate::schema::clipboard_history;
use crate::schema::clipboard_history::dsl::*;
use crate::schema::link_metadata;
@ -187,6 +187,13 @@ impl ClipboardHistoryWithMetaData {
process_history_item(&mut history_with_metadata, auto_mask_words_list);
history_with_metadata
}
/// Transforms the image_path_full_res field from relative to absolute path for frontend consumption
pub fn transform_image_path_for_frontend(&mut self) {
if let Some(ref mut path) = self.image_path_full_res {
*path = crate::db::to_absolute_image_path(path);
}
}
}
pub fn get_source_apps_list() -> Result<Vec<Option<String>>, Error> {
@ -226,11 +233,7 @@ pub fn add_clipboard_history_from_image(
let _history_id = nanoid!().to_string();
let folder_name = &_history_id[..3];
let base_dir = if cfg!(debug_assertions) {
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
} else {
&APP_CONSTANTS.get().unwrap().app_data_dir
};
let base_dir = db::get_clipboard_images_dir();
let (_image_width, _image_height) = image.dimensions();
@ -279,16 +282,21 @@ pub fn add_clipboard_history_from_image(
))
.execute(connection);
} else {
let folder_path = base_dir.join("clipboard-images").join(folder_name);
let folder_path = base_dir.join(folder_name);
ensure_dir_exists(&folder_path);
let image_file_name = folder_path.join(format!("{}.png", &_history_id));
let _ = image.save(&image_file_name);
// Convert absolute path to relative path before storing
let relative_image_path = image_file_name.to_str()
.map(|path| db::to_relative_image_path(path))
.unwrap_or_default();
let new_history = create_new_history(
_history_id,
_image_data_low_res,
image_file_name,
PathBuf::from(relative_image_path),
image_hash_string,
_preview_height.try_into().unwrap(),
_image_height.try_into().unwrap(),
@ -565,19 +573,26 @@ pub fn get_pinned_clipboard_histories(
auto_mask_words_list: Vec<String>,
) -> Result<Vec<ClipboardHistoryWithMetaData>, Error> {
let connection = &mut establish_pool_db_connection();
let histories = clipboard_history
let query_results = clipboard_history
.left_join(
link_metadata_dsl.on(link_metadata::history_id.eq(clipboard_history::history_id.nullable())),
)
.limit(20)
.filter(is_pinned.eq(true))
.load::<(ClipboardHistory, Option<LinkMetadata>)>(connection)?
.load::<(ClipboardHistory, Option<LinkMetadata>)>(connection)?;
let mut histories: Vec<ClipboardHistoryWithMetaData> = query_results
.into_iter()
.map(|(history, link_metadata)| {
ClipboardHistoryWithMetaData::from(history, link_metadata, &auto_mask_words_list)
})
.collect();
// Transform image paths for frontend consumption
for history in &mut histories {
history.transform_image_path_for_frontend();
}
Ok(histories)
}
@ -591,30 +606,42 @@ pub fn get_clipboard_histories(
let connection = &mut establish_pool_db_connection();
let histories: Vec<ClipboardHistoryWithMetaData> = clipboard_history
let query_results = clipboard_history
.left_join(
link_metadata_dsl.on(link_metadata::history_id.eq(clipboard_history::history_id.nullable())),
)
.order(updated_date.desc())
.limit(limit)
.offset(offset)
.load::<(ClipboardHistory, Option<LinkMetadata>)>(connection)?
.load::<(ClipboardHistory, Option<LinkMetadata>)>(connection)?;
let mut histories: Vec<ClipboardHistoryWithMetaData> = query_results
.into_iter()
.map(|(history, link_metadata)| {
ClipboardHistoryWithMetaData::from(history, link_metadata, &auto_mask_words_list)
})
.collect();
// Transform image paths for frontend consumption
for history in &mut histories {
history.transform_image_path_for_frontend();
}
Ok(histories)
}
pub fn get_clipboard_history_by_id(history_id_value: &String) -> Option<ClipboardHistory> {
let connection = &mut establish_pool_db_connection();
clipboard_history
let mut result = clipboard_history
.find(history_id_value)
.first::<ClipboardHistory>(connection)
.ok()
.ok()?;
// Transform image path for frontend consumption
result.transform_image_path_for_frontend();
Some(result)
}
pub fn delete_clipboard_history_older_than(age: Duration) -> Result<String, diesel::result::Error> {
@ -712,13 +739,7 @@ pub fn delete_recent_clipboard_history(
pub fn delete_all_clipboard_histories() -> String {
let connection = &mut establish_pool_db_connection();
let base_dir = if cfg!(debug_assertions) {
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
} else {
&APP_CONSTANTS.get().unwrap().app_data_dir
};
let folder_path = base_dir.join("clipboard-images");
let folder_path = db::get_clipboard_images_dir();
let _ = remove_dir_if_exists(&folder_path);
@ -877,15 +898,22 @@ pub fn find_clipboard_histories_by_value_or_filter(
}
}
let histories = query_builder
let query_results = query_builder
.order(updated_date.desc())
.limit(max_results)
.load::<(ClipboardHistory, Option<LinkMetadata>)>(connection)?
.load::<(ClipboardHistory, Option<LinkMetadata>)>(connection)?;
let mut histories: Vec<ClipboardHistoryWithMetaData> = query_results
.into_iter()
.map(|(history, link_metadata)| {
ClipboardHistoryWithMetaData::from(history, link_metadata, &auto_mask_words_list)
})
.collect::<Vec<ClipboardHistoryWithMetaData>>();
.collect();
// Transform image paths for frontend consumption
for history in &mut histories {
history.transform_image_path_for_frontend();
}
Ok(histories)
}

View File

@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use crate::db::APP_CONSTANTS;
use crate::db::{self, APP_CONSTANTS};
use crate::models::models::UpdatedItemData;
use crate::models::Item;
use crate::services::utils::debug_output;
@ -102,7 +102,11 @@ pub fn get_item_by_id(item_id: String) -> Result<Item, String> {
.first::<Item>(connection);
match found_item {
Ok(item) => Ok(item),
Ok(mut item) => {
// Transform image path for frontend consumption
item.transform_image_path_for_frontend();
Ok(item)
},
Err(e) => Err(format!("Item not found: {}", e)),
}
}
@ -509,13 +513,7 @@ pub fn add_image_to_item(item_id: &str, image_full_path: &str) -> Result<String,
let is_svg = extension == "svg";
let base_dir = if cfg!(debug_assertions) {
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
} else {
&APP_CONSTANTS.get().unwrap().app_data_dir
};
let folder_path = base_dir.join("clip-images").join(&item_id[..3]);
let folder_path = db::get_clip_images_dir().join(&item_id[..3]);
ensure_dir_exists(&folder_path);
let new_image_path = folder_path.join(format!("{}.{}", item_id, extension));
@ -544,9 +542,13 @@ pub fn add_image_to_item(item_id: &str, image_full_path: &str) -> Result<String,
let connection = &mut establish_pool_db_connection();
// Convert absolute path to relative path before storing
let relative_image_path = new_image_path.to_str()
.map(|path| db::to_relative_image_path(path));
diesel::update(items.find(item_id))
.set((
image_path_full_res.eq(new_image_path.to_str()),
image_path_full_res.eq(relative_image_path),
is_image.eq(true),
image_height.eq(_image_height),
image_width.eq(_image_width),
@ -636,13 +638,7 @@ pub fn save_item_image_from_history_item(
) -> Result<String, String> {
let folder_name = &item_id[..3];
let base_dir = if cfg!(debug_assertions) {
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
} else {
&APP_CONSTANTS.get().unwrap().app_data_dir
};
let folder_path = base_dir.join("clip-images").join(folder_name);
let folder_path = db::get_clip_images_dir().join(folder_name);
ensure_dir_exists(&folder_path);
let clip_image_file_name = folder_path.join(format!("{}.png", &item_id));
@ -664,7 +660,11 @@ pub fn save_item_image_from_history_item(
e.to_string()
})?;
Ok(clip_image_file_name.to_str().unwrap().to_string())
// Return relative path instead of absolute path
let relative_path = clip_image_file_name.to_str()
.map(|path| db::to_relative_image_path(path))
.unwrap_or_default();
Ok(relative_path)
}
pub fn upload_image_file_to_item_id(
@ -684,13 +684,7 @@ pub fn upload_image_file_to_item_id(
let file_name = format!("{}.{}", item_id, extension);
let base_dir = if cfg!(debug_assertions) {
&APP_CONSTANTS.get().unwrap().app_dev_data_dir
} else {
&APP_CONSTANTS.get().unwrap().app_data_dir
};
let folder_path = base_dir.join("clip-images").join(&item_id[..3]);
let folder_path = db::get_clip_images_dir().join(&item_id[..3]);
ensure_dir_exists(&folder_path);
let image_path = folder_path.join(&file_name);
@ -723,9 +717,13 @@ pub fn upload_image_file_to_item_id(
let _image_hash_string = chrono::Utc::now().timestamp_millis().to_string();
let connection = &mut establish_pool_db_connection();
// Convert absolute path to relative path before storing
let relative_image_path = image_path.to_str()
.map(|path| db::to_relative_image_path(path));
let _ = diesel::update(items.find(item_id))
.set((
image_path_full_res.eq(image_path.to_str()),
image_path_full_res.eq(relative_image_path),
image_data_url.eq(_image_data_url),
image_height.eq(_image_height),
image_width.eq(_image_width),

View File

@ -7,4 +7,5 @@ pub mod settings_service;
pub mod shell_service;
pub mod tabs_service;
pub mod translations;
pub mod user_settings_service;
pub mod utils;

View File

@ -39,7 +39,11 @@ pub fn insert_or_update_setting_by_name(
setting: &Setting,
app_handle: tauri::AppHandle,
) -> Result<String, Error> {
let connection = &mut DB_POOL_CONNECTION.get().unwrap();
let connection = &mut DB_POOL_CONNECTION
.read()
.expect("Failed to acquire read lock on DB_POOL_CONNECTION")
.get()
.expect("Failed to get a database connection from the pool");
match settings
.filter(name.eq(&setting.name))

View File

@ -0,0 +1,99 @@
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::collections::HashMap;
use crate::db::get_config_file_path;
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct UserConfig {
/// The custom DB path, if user specified one.
pub custom_db_path: Option<String>,
/// General-purpose key-value settings.
#[serde(default)]
pub data: HashMap<String, serde_yaml::Value>,
}
pub fn load_user_config() -> UserConfig {
let path = get_config_file_path();
if !path.exists() {
return UserConfig::default();
}
match std::fs::read_to_string(&path) {
Ok(contents) => match serde_yaml::from_str::<UserConfig>(&contents) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("Error parsing user config YAML: {:#}", e);
UserConfig::default()
}
},
Err(e) => {
eprintln!("Error reading user config file: {:#}", e);
UserConfig::default()
}
}
}
/// Save the `UserConfig` back to `pastebar_settings.yaml`.
pub fn save_user_config(cfg: &UserConfig) -> Result<(), String> {
let path = get_config_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
let yaml_str =
serde_yaml::to_string(cfg).map_err(|e| format!("Failed to serialize config to YAML: {}", e))?;
std::fs::write(&path, yaml_str).map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(())
}
// ===========================
// Custom DB Path Methods
// ===========================
/// Get the current `custom_db_path` (if any).
pub fn get_custom_db_path() -> Option<String> {
load_user_config().custom_db_path
}
/// Insert or update the `custom_db_path`.
pub fn set_custom_db_path(new_path: &str) -> Result<(), String> {
let mut config = load_user_config();
config.custom_db_path = Some(new_path.to_string());
save_user_config(&config)
}
/// Remove (clear) the `custom_db_path`.
pub fn remove_custom_db_path() -> Result<(), String> {
let mut config = load_user_config();
config.custom_db_path = None;
save_user_config(&config)
}
// ===========================
// KeyValue data Methods
// ===========================
pub fn get_setting(key: &str) -> Option<serde_yaml::Value> {
let config = load_user_config();
config.data.get(key).cloned()
}
pub fn set_setting(key: &str, value: serde_yaml::Value) -> Result<(), String> {
let mut config = load_user_config();
config.data.insert(key.to_string(), value);
save_user_config(&config)
}
pub fn remove_setting(key: &str) -> Result<(), String> {
let mut config = load_user_config();
config.data.remove(key);
save_user_config(&config)
}
pub fn get_all_settings() -> HashMap<String, serde_yaml::Value> {
load_user_config().data
}

View File

@ -39,11 +39,32 @@ impl<R: Runtime> WindowToolBar for Window<R> {
let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
window
.standardWindowButton_(NSWindowButton::NSWindowDocumentIconButton)
.setCanHide_(cocoa::base::YES);
// Check if standard buttons exist
if close.is_null() || miniaturize.is_null() || zoom.is_null() {
eprintln!("Warning: Window buttons are null, skipping traffic light positioning");
return;
}
let title_bar_container_view = close.superview().superview();
let document_icon = window.standardWindowButton_(NSWindowButton::NSWindowDocumentIconButton);
if !document_icon.is_null() {
let mut doc_rect: NSRect = NSView::frame(document_icon);
doc_rect.origin.x = -200.0; // Move it off-screen
// document_icon.setFrameOrigin(doc_rect.origin);
// document_icon.setCanHide_(cocoa::base::YES);
}
// Check superviews exist
let superview = close.superview();
if superview.is_null() {
eprintln!("Warning: Close button superview is null");
return;
}
let title_bar_container_view = superview.superview();
if title_bar_container_view.is_null() {
eprintln!("Warning: Title bar container view is null");
return;
}
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;