Add context menu for vault actions and trash functionality

- Add UiButtonContext component for context menu support on buttons
- Implement vault trash functionality using trash crate
- Move vaults to system trash on desktop (with fallback to permanent delete on mobile)
- Add context menu to vault list items for better mobile UX
- Keep hover delete button for desktop users
This commit is contained in:
2025-10-31 22:57:56 +01:00
parent 5ea04a80e0
commit dceb49ae90
8 changed files with 185 additions and 22 deletions

108
src-tauri/Cargo.lock generated
View File

@ -1715,6 +1715,7 @@ dependencies = [
"tauri-plugin-persisted-scope",
"tauri-plugin-store",
"thiserror 2.0.17",
"trash",
"ts-rs",
"uhlc",
"url",
@ -4431,7 +4432,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-version",
"x11-dl",
@ -4503,7 +4504,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.1",
]
[[package]]
@ -4687,7 +4688,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.17",
"url",
"windows",
"windows 0.61.1",
"zbus",
]
@ -4763,7 +4764,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.1",
]
[[package]]
@ -4789,7 +4790,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.1",
"wry",
]
@ -4849,7 +4850,7 @@ checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
dependencies = [
"quick-xml 0.37.5",
"thiserror 2.0.17",
"windows",
"windows 0.61.1",
"windows-version",
]
@ -5189,6 +5190,24 @@ dependencies = [
"once_cell",
]
[[package]]
name = "trash"
version = "5.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22746c6b0c6d85d60a8f0d858f7057dfdf11297c132679f452ec908fba42b871"
dependencies = [
"chrono",
"libc",
"log",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"once_cell",
"percent-encoding",
"scopeguard",
"urlencoding",
"windows 0.56.0",
]
[[package]]
name = "tray-icon"
version = "0.21.0"
@ -5353,6 +5372,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"
@ -5638,10 +5663,10 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-implement",
"windows-interface",
"windows-implement 0.60.0",
"windows-interface 0.59.1",
]
[[package]]
@ -5662,7 +5687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.17",
"windows",
"windows 0.61.1",
"windows-core 0.61.0",
]
@ -5712,6 +5737,16 @@ dependencies = [
"windows-version",
]
[[package]]
name = "windows"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
dependencies = [
"windows-core 0.56.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.1"
@ -5743,16 +5778,28 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
dependencies = [
"windows-implement 0.56.0",
"windows-interface 0.56.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
@ -5766,6 +5813,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-implement"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@ -5777,6 +5835,17 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
@ -5810,11 +5879,20 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result",
"windows-result 0.3.2",
"windows-strings 0.3.1",
"windows-targets 0.53.2",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.2"
@ -6226,7 +6304,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-version",
"x11-dl",

View File

@ -49,6 +49,7 @@ tauri-plugin-os = "2.3.2"
tauri-plugin-persisted-scope = "2.3.4"
tauri-plugin-store = "2.4.1"
thiserror = "2.0.17"
trash = "5.2.0"
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
uhlc = "0.8.2"
url = "2.5.7"

View File

@ -20,6 +20,7 @@ use std::time::UNIX_EPOCH;
use std::{fs, sync::Arc};
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
use tauri_plugin_fs::FsExt;
use trash;
use ts_rs::TS;
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
@ -212,7 +213,46 @@ pub fn vault_exists(app_handle: AppHandle, vault_name: String) -> Result<bool, D
Ok(Path::new(&vault_path).exists())
}
/// Deletes a vault database file
/// Moves a vault database file to trash (or deletes permanently if trash is unavailable)
#[tauri::command]
pub fn move_vault_to_trash(
app_handle: AppHandle,
vault_name: String,
) -> Result<String, DatabaseError> {
let vault_path = get_vault_path(&app_handle, &vault_name)?;
let vault_shm_path = format!("{}-shm", vault_path);
let vault_wal_path = format!("{}-wal", vault_path);
if !Path::new(&vault_path).exists() {
return Err(DatabaseError::IoError {
path: vault_path,
reason: "Vault does not exist".to_string(),
});
}
// Try to move to trash first (works on desktop systems)
let moved_to_trash = trash::delete(&vault_path).is_ok();
if moved_to_trash {
// Also try to move auxiliary files to trash (ignore errors as they might not exist)
let _ = trash::delete(&vault_shm_path);
let _ = trash::delete(&vault_wal_path);
Ok(format!(
"Vault '{}' successfully moved to trash",
vault_name
))
} else {
// Fallback: Permanent deletion (e.g., on mobile devices without trash)
println!(
"Trash not available, falling back to permanent deletion for vault '{}'",
vault_name
);
delete_vault(app_handle, vault_name)
}
}
/// Deletes a vault database file permanently (bypasses trash)
#[tauri::command]
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
let vault_path = get_vault_path(&app_handle, &vault_name)?;

View File

@ -68,6 +68,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
database::create_encrypted_database,
database::delete_vault,
database::move_vault_to_trash,
database::list_vaults,
database::open_encrypted_database,
database::sql_execute_with_crdt,

View File

@ -0,0 +1,28 @@
<template>
<UContextMenu :items="contextMenuItems">
<UiButton
v-bind="$attrs"
@click="$emit('click', $event)"
>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
/>
</template>
</UiButton>
</UContextMenu>
</template>
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
defineProps<{
contextMenuItems: ContextMenuItem[]
}>()
defineEmits<{ click: [Event] }>()
</script>

View File

@ -54,10 +54,18 @@
:key="vault.name"
class="flex items-center justify-between group overflow-x-scroll"
>
<UButton
<UiButtonContext
variant="ghost"
color="neutral"
class="flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full px-3"
:context-menu-items="[
{
icon: 'mdi:trash-can-outline',
label: t('remove.button'),
onSelect: () => prepareRemoveVault(vault.name),
color: 'error',
},
]"
@click="
() => {
passwordPromptOpen = true
@ -68,7 +76,7 @@
<span class="block">
{{ vault.name }}
</span>
</UButton>
</UiButtonContext>
<UButton
color="error"
square
@ -129,6 +137,9 @@ const showRemoveDialog = ref(false)
const { lastVaults } = storeToRefs(useLastVaultStore())
const { syncLastVaultsAsync, moveVaultToTrashAsync } = useLastVaultStore()
const { syncDeviceIdAsync } = useDeviceStore()
const vaultToBeRemoved = ref('')
const prepareRemoveVault = (vaultName: string) => {
vaultToBeRemoved.value = vaultName
@ -138,7 +149,7 @@ const prepareRemoveVault = (vaultName: string) => {
const toast = useToast()
const onConfirmRemoveAsync = async () => {
try {
await removeVaultAsync(vaultToBeRemoved.value)
await moveVaultToTrashAsync(vaultToBeRemoved.value)
showRemoveDialog.value = false
await syncLastVaultsAsync()
} catch (error) {
@ -149,9 +160,6 @@ const onConfirmRemoveAsync = async () => {
}
}
const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
const { syncDeviceIdAsync } = useDeviceStore()
onMounted(async () => {
try {
await syncLastVaultsAsync()
@ -168,6 +176,7 @@ de:
lastUsed: 'Zuletzt verwendete Vaults'
sponsors: Supported by
remove:
button: Löschen
title: Vault löschen
description: Möchtest du die Vault {vaultName} wirklich löschen?
@ -176,6 +185,7 @@ en:
lastUsed: 'Last used Vaults'
sponsors: 'Supported by'
remove:
button: Delete
title: Delete Vault
description: Are you sure you really want to delete {vaultName}?
</i18n>

View File

@ -22,9 +22,14 @@ export const useLastVaultStore = defineStore('lastVaultStore', () => {
return await invoke('delete_vault', { vaultName })
}
const moveVaultToTrashAsync = async (vaultName: string) => {
return await invoke('move_vault_to_trash', { vaultName })
}
return {
syncLastVaultsAsync,
lastVaults,
removeVaultAsync,
moveVaultToTrashAsync,
}
})