Merge branch 'main' into backup-restore-system
This commit is contained in:
commit
1aa8e5d28c
5
.changeset/calm-bags-learn.md
Normal file
5
.changeset/calm-bags-learn.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'pastebar-app-ui': patch
|
||||
---
|
||||
|
||||
Added option to disable capturing and storing images from clipboard
|
5
.changeset/tasty-walls-pull.md
Normal file
5
.changeset/tasty-walls-pull.md
Normal 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
1
.gitignore
vendored
@ -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
131
custom_db_location_plan.md
Normal 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
210
package-lock.json
generated
@ -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
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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', () => {
|
||||
|
@ -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={() => {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: Аудио
|
||||
|
@ -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: Отключить захват и сохранение изображений из буфера обмена
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: Аудіо
|
||||
|
@ -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: Вимкнути захоплення та збереження зображень з буферу обміну
|
||||
|
@ -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: 音频
|
||||
|
@ -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: 禁用从剪贴板捕获和存储图像
|
@ -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={`${
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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">
|
||||
|
@ -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
2470
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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" }
|
||||
|
@ -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.
|
||||
|
@ -130,11 +130,15 @@ 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) {
|
||||
Ok(img_data) => base64::encode(&img_data),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to read image from path: {}", e);
|
||||
IMAGE_NOT_FOUND_BASE64.to_string()
|
||||
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,11 +416,15 @@ 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) {
|
||||
Ok(img_data) => base64::encode(&img_data),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to read image from path: {}", e);
|
||||
IMAGE_NOT_FOUND_BASE64.to_string()
|
||||
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(),
|
||||
|
@ -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())?;
|
||||
|
@ -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())?;
|
||||
|
@ -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;
|
||||
|
258
src-tauri/src/commands/user_settings_command.rs
Normal file
258
src-tauri/src/commands/user_settings_command.rs
Normal 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)
|
||||
}
|
@ -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)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}/local.pastebar-db.data",
|
||||
adjust_canonicalization(app_dir)
|
||||
)
|
||||
};
|
||||
|
||||
path
|
||||
/// 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 {
|
||||
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"
|
||||
};
|
||||
|
||||
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)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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))
|
||||
|
99
src-tauri/src/services/user_settings_service.rs
Normal file
99
src-tauri/src/services/user_settings_service.rs
Normal 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)
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Key–Value 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
|
||||
}
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user