fix window on workspace rendering

This commit is contained in:
2025-10-25 08:09:15 +02:00
parent 9281a85deb
commit cb0c8d71f4
12 changed files with 424 additions and 640 deletions

View File

@ -156,11 +156,11 @@ export const haexDesktopItems = sqliteTable(
.notNull()
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
enum: ['extension', 'file', 'folder'],
enum: ['system', 'extension', 'file', 'folder'],
}).notNull(),
referenceId: text(
tableNames.haex.desktop_items.columns.referenceId,
).notNull(), // extensionId für extensions, filePath für files/folders
).notNull(), // systemId für system windows, extensionId für extensions, filePath für files/folders
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
.notNull()
.default(0),

View File

@ -88,30 +88,64 @@ impl ExtensionManager {
reason: format!("Cannot extract ZIP: {}", e),
})?;
// Check if manifest.json is directly in temp or in a subdirectory
let manifest_path = temp.join("manifest.json");
let actual_dir = if manifest_path.exists() {
temp.clone()
} else {
// manifest.json is in a subdirectory - find it
let mut found_dir = None;
for entry in fs::read_dir(&temp)
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?
{
let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
let path = entry.path();
if path.is_dir() && path.join("manifest.json").exists() {
found_dir = Some(path);
break;
}
// Read haextension_dir from config if it exists, otherwise use default
let config_path = temp.join("haextension.config.json");
let haextension_dir = if config_path.exists() {
let config_content = std::fs::read_to_string(&config_path)
.map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read haextension.config.json: {}", e),
})?;
let config: serde_json::Value = serde_json::from_str(&config_content)
.map_err(|e| ExtensionError::ManifestError {
reason: format!("Invalid haextension.config.json: {}", e),
})?;
let dir = config
.get("dev")
.and_then(|dev| dev.get("haextension_dir"))
.and_then(|dir| dir.as_str())
.unwrap_or("haextension")
.to_string();
// Security: Validate that haextension_dir doesn't contain ".." for path traversal
if dir.contains("..") {
return Err(ExtensionError::ManifestError {
reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(),
});
}
found_dir.ok_or_else(|| ExtensionError::ManifestError {
reason: "manifest.json not found in extension archive".to_string(),
})?
dir
} else {
"haextension".to_string()
};
let manifest_path = actual_dir.join("manifest.json");
// Build the manifest path
let manifest_path = temp.join(&haextension_dir).join("manifest.json");
// Ensure the resolved path is still within temp directory (safety check against path traversal)
let canonical_temp = temp.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// Only check if manifest_path parent exists to avoid errors
if let Some(parent) = manifest_path.parent() {
if let Ok(canonical_manifest_dir) = parent.canonicalize() {
if !canonical_manifest_dir.starts_with(&canonical_temp) {
return Err(ExtensionError::ManifestError {
reason: "Security violation: manifest path outside extension directory".to_string(),
});
}
}
}
// Check if manifest exists
if !manifest_path.exists() {
return Err(ExtensionError::ManifestError {
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
});
}
let actual_dir = temp.clone();
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
@ -119,7 +153,7 @@ impl ExtensionManager {
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| {
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
ExtensionError::SignatureVerificationFailed {
reason: e.to_string(),
}

View File

@ -4,28 +4,13 @@ use std::{
};
// src-tauri/src/extension/crypto.rs
use crate::extension::error::ExtensionError;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
pub struct ExtensionCrypto;
impl ExtensionCrypto {
/// Berechnet Hash vom Public Key (wie im SDK)
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
let public_key_bytes =
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
.map_err(|e| format!("Invalid public key: {}", e))?;
let mut hasher = Sha256::new();
hasher.update(public_key.as_bytes());
let result = hasher.finalize();
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
Ok(hex::encode(&result[..10]))
}
/// Verifiziert Extension-Signatur
pub fn verify_signature(
public_key_hex: &str,
@ -50,26 +35,48 @@ impl ExtensionCrypto {
}
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
pub fn hash_directory(dir: &Path) -> Result<String, String> {
pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result<String, ExtensionError> {
// 1. Alle Dateipfade rekursiv sammeln
let mut all_files = Vec::new();
Self::collect_files_recursively(dir, &mut all_files)
.map_err(|e| format!("Failed to collect files: {}", e))?;
all_files.sort();
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// 2. Konvertiere zu relativen Pfaden für konsistente Sortierung (wie im SDK)
let mut relative_files: Vec<(String, PathBuf)> = all_files
.into_iter()
.map(|path| {
let relative = path.strip_prefix(dir)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
(relative, path)
})
.collect();
// 3. Sortiere nach relativen Pfaden
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
println!("=== Files to hash ({}): ===", relative_files.len());
for (rel, _) in &relative_files {
println!(" - {}", rel);
}
let mut hasher = Sha256::new();
let manifest_path = dir.join("manifest.json");
// 2. Inhalte der sortierten Dateien hashen
for file_path in all_files {
// 4. Inhalte der sortierten Dateien hashen
for (_relative, file_path) in relative_files {
if file_path == manifest_path {
// FÜR DIE MANIFEST.JSON:
let content_str = fs::read_to_string(&file_path)
.map_err(|e| format!("Cannot read manifest file: {}", e))?;
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// Parse zu einem generischen JSON-Wert
let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
.map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
let mut manifest: serde_json::Value =
serde_json::from_str(&content_str).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Cannot parse manifest JSON: {}", e),
}
})?;
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
if let Some(obj) = manifest.as_object_mut() {
@ -80,13 +87,19 @@ impl ExtensionCrypto {
}
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap();
// serde_json sortiert die Keys automatisch alphabetisch
let canonical_manifest_content =
serde_json::to_string_pretty(&manifest).map_err(|e| {
ExtensionError::ManifestError {
reason: format!("Failed to serialize manifest: {}", e),
}
})?;
println!("canonical_manifest_content: {}", canonical_manifest_content);
hasher.update(canonical_manifest_content.as_bytes());
} else {
// FÜR ALLE ANDEREN DATEIEN:
let content = fs::read(&file_path)
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
let content =
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
hasher.update(&content);
}
}

View File

@ -223,6 +223,16 @@ pub fn is_extension_installed(
#[derive(serde::Deserialize, Debug)]
struct HaextensionConfig {
dev: DevConfig,
#[serde(default)]
keys: KeysConfig,
}
#[derive(serde::Deserialize, Debug, Default)]
struct KeysConfig {
#[serde(default)]
public_key_path: Option<String>,
#[serde(default)]
private_key_path: Option<String>,
}
#[derive(serde::Deserialize, Debug)]
@ -231,6 +241,8 @@ struct DevConfig {
port: u16,
#[serde(default = "default_host")]
host: String,
#[serde(default = "default_haextension_dir")]
haextension_dir: String,
}
fn default_port() -> u16 {
@ -241,6 +253,10 @@ fn default_host() -> String {
"localhost".to_string()
}
fn default_haextension_dir() -> String {
"haextension".to_string()
}
/// Check if a dev server is reachable by making a simple HTTP request
async fn check_dev_server_health(url: &str) -> bool {
use tauri_plugin_http::reqwest;
@ -276,29 +292,30 @@ pub async fn load_dev_extension(
let extension_path_buf = PathBuf::from(&extension_path);
// 1. Read haextension.json to get dev server config
let config_path = extension_path_buf.join("haextension.json");
let (host, port) = if config_path.exists() {
// 1. Read haextension.config.json to get dev server config and haextension directory
let config_path = extension_path_buf.join("haextension.config.json");
let (host, port, haextension_dir) = if config_path.exists() {
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Failed to read haextension.json: {}", e),
reason: format!("Failed to read haextension.config.json: {}", e),
}
})?;
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
ExtensionError::ValidationError {
reason: format!("Failed to parse haextension.json: {}", e),
reason: format!("Failed to parse haextension.config.json: {}", e),
}
})?;
(config.dev.host, config.dev.port)
(config.dev.host, config.dev.port, config.dev.haextension_dir)
} else {
// Default values if config doesn't exist
(default_host(), default_port())
(default_host(), default_port(), default_haextension_dir())
};
let dev_server_url = format!("http://{}:{}", host, port);
eprintln!("📡 Dev server URL: {}", dev_server_url);
eprintln!("📁 Haextension directory: {}", haextension_dir);
// 1.5. Check if dev server is running
if !check_dev_server_health(&dev_server_url).await {
@ -311,8 +328,8 @@ pub async fn load_dev_extension(
}
eprintln!("✅ Dev server is reachable");
// 2. Build path to manifest: <extension_path>/haextension/manifest.json
let manifest_path = extension_path_buf.join("haextension").join("manifest.json");
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
let manifest_path = extension_path_buf.join(&haextension_dir).join("manifest.json");
// Check if manifest exists
if !manifest_path.exists() {

View File

@ -37,7 +37,7 @@
:alt="label"
class="w-14 h-14 object-contain transition-transform duration-200"
:class="{ 'scale-110': isSelected }"
>
/>
<UIcon
v-else
name="i-heroicons-puzzle-piece-solid"
@ -69,7 +69,7 @@
<script setup lang="ts">
const props = defineProps<{
id: string
itemType: 'extension' | 'file' | 'folder'
itemType: DesktopItemType
referenceId: string
initialX: number
initialY: number

View File

@ -27,6 +27,8 @@
class="w-full h-full relative isolate"
@click.self.stop="handleDesktopClick"
@mousedown.left.self="handleAreaSelectStart"
@dragover.prevent="handleDragOver"
@drop.prevent="handleDrop($event, workspace.id)"
>
<!-- Grid Pattern Background -->
<div
@ -81,128 +83,113 @@
v-for="window in getWorkspaceWindows(workspace.id)"
:key="window.id"
>
<!-- Desktop container for when overview is closed -->
<div
:id="`desktop-container-${window.id}`"
class="absolute"
/>
<!-- Window with dynamic teleport -->
<!-- Overview Mode: Teleport to window preview -->
<Teleport
:to="
windowManager.showWindowOverview &&
overviewWindowState.has(window.id)
? `#window-preview-${window.id}`
: `#desktop-container-${window.id}`
"
v-if="windowManager.showWindowOverview && overviewWindowState.has(window.id)"
:to="`#window-preview-${window.id}`"
>
<template
v-if="
windowManager.showWindowOverview &&
overviewWindowState.has(window.id)
"
<div
class="absolute origin-top-left"
:style="{
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
width: `${overviewWindowState.get(window.id)!.width}px`,
height: `${overviewWindowState.get(window.id)!.height}px`,
}"
>
<div
class="absolute origin-top-left"
:style="{
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
width: `${overviewWindowState.get(window.id)!.width}px`,
height: `${overviewWindowState.get(window.id)!.height}px`,
}"
<HaexWindow
v-show="
windowManager.showWindowOverview || !window.isMinimized
"
:id="window.id"
v-model:x="overviewWindowState.get(window.id)!.x"
v-model:y="overviewWindowState.get(window.id)!.y"
v-model:width="overviewWindowState.get(window.id)!.width"
v-model:height="overviewWindowState.get(window.id)!.height"
:title="window.title"
:icon="window.icon"
:is-active="windowManager.isWindowActive(window.id)"
:source-x="window.sourceX"
:source-y="window.sourceY"
:source-width="window.sourceWidth"
:source-height="window.sourceHeight"
:is-opening="window.isOpening"
:is-closing="window.isClosing"
class="no-swipe"
@close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)"
@activate="windowManager.activateWindow(window.id)"
@position-changed="
(x, y) =>
windowManager.updateWindowPosition(window.id, x, y)
"
@size-changed="
(width, height) =>
windowManager.updateWindowSize(window.id, width, height)
"
@drag-start="handleWindowDragStart(window.id)"
@drag-end="handleWindowDragEnd"
>
<HaexWindow
v-show="
windowManager.showWindowOverview || !window.isMinimized
"
:id="window.id"
v-model:x="overviewWindowState.get(window.id)!.x"
v-model:y="overviewWindowState.get(window.id)!.y"
v-model:width="overviewWindowState.get(window.id)!.width"
v-model:height="overviewWindowState.get(window.id)!.height"
:title="window.title"
:icon="window.icon"
:is-active="windowManager.isWindowActive(window.id)"
:source-x="window.sourceX"
:source-y="window.sourceY"
:source-width="window.sourceWidth"
:source-height="window.sourceHeight"
:is-opening="window.isOpening"
:is-closing="window.isClosing"
class="no-swipe"
@close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)"
@activate="windowManager.activateWindow(window.id)"
@position-changed="
(x, y) =>
windowManager.updateWindowPosition(window.id, x, y)
"
@size-changed="
(width, height) =>
windowManager.updateWindowSize(window.id, width, height)
"
@drag-start="handleWindowDragStart(window.id)"
@drag-end="handleWindowDragEnd"
>
<!-- System Window: Render Vue Component -->
<component
:is="getSystemWindowComponent(window.sourceId)"
v-if="window.type === 'system'"
/>
<!-- System Window: Render Vue Component -->
<component
:is="getSystemWindowComponent(window.sourceId)"
v-if="window.type === 'system'"
/>
<!-- Extension Window: Render iFrame -->
<HaexDesktopExtensionFrame
v-else
:extension-id="window.sourceId"
:window-id="window.id"
/>
</HaexWindow>
</div>
</template>
<HaexWindow
v-else
v-show="windowManager.showWindowOverview || !window.isMinimized"
:id="window.id"
v-model:x="window.x"
v-model:y="window.y"
v-model:width="window.width"
v-model:height="window.height"
:title="window.title"
:icon="window.icon"
:is-active="windowManager.isWindowActive(window.id)"
:source-x="window.sourceX"
:source-y="window.sourceY"
:source-width="window.sourceWidth"
:source-height="window.sourceHeight"
:is-opening="window.isOpening"
:is-closing="window.isClosing"
class="no-swipe"
@close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)"
@activate="windowManager.activateWindow(window.id)"
@position-changed="
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
"
@size-changed="
(width, height) =>
windowManager.updateWindowSize(window.id, width, height)
"
@drag-start="handleWindowDragStart(window.id)"
@drag-end="handleWindowDragEnd"
>
<!-- System Window: Render Vue Component -->
<component
:is="getSystemWindowComponent(window.sourceId)"
v-if="window.type === 'system'"
/>
<!-- Extension Window: Render iFrame -->
<HaexDesktopExtensionFrame
v-else
:extension-id="window.sourceId"
:window-id="window.id"
/>
</HaexWindow>
<!-- Extension Window: Render iFrame -->
<HaexDesktopExtensionFrame
v-else
:extension-id="window.sourceId"
:window-id="window.id"
/>
</HaexWindow>
</div>
</Teleport>
<!-- Desktop Mode: Render directly in workspace -->
<HaexWindow
v-else
v-show="windowManager.showWindowOverview || !window.isMinimized"
:id="window.id"
v-model:x="window.x"
v-model:y="window.y"
v-model:width="window.width"
v-model:height="window.height"
:title="window.title"
:icon="window.icon"
:is-active="windowManager.isWindowActive(window.id)"
:source-x="window.sourceX"
:source-y="window.sourceY"
:source-width="window.sourceWidth"
:source-height="window.sourceHeight"
:is-opening="window.isOpening"
:is-closing="window.isClosing"
class="no-swipe"
@close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)"
@activate="windowManager.activateWindow(window.id)"
@position-changed="
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
"
@size-changed="
(width, height) =>
windowManager.updateWindowSize(window.id, width, height)
"
@drag-start="handleWindowDragStart(window.id)"
@drag-end="handleWindowDragEnd"
>
<!-- System Window: Render Vue Component -->
<component
:is="getSystemWindowComponent(window.sourceId)"
v-if="window.type === 'system'"
/>
<!-- Extension Window: Render iFrame -->
<HaexDesktopExtensionFrame
v-else
:extension-id="window.sourceId"
:window-id="window.id"
/>
</HaexWindow>
</template>
</div>
</SwiperSlide>
@ -342,6 +329,18 @@ const getWorkspaceIcons = (workspaceId: string) => {
return desktopItems.value
.filter((item) => item.workspaceId === workspaceId)
.map((item) => {
if (item.itemType === 'system') {
const systemWindow = windowManager.getAllSystemWindows().find(
(win) => win.id === item.referenceId,
)
return {
...item,
label: systemWindow?.name || 'Unknown',
icon: systemWindow?.icon || '',
}
}
if (item.itemType === 'extension') {
const extension = availableExtensions.value.find(
(ext) => ext.id === item.referenceId,
@ -416,6 +415,49 @@ const handleDragEnd = async () => {
allowSwipe.value = true // Re-enable Swiper after drag
}
// Handle drag over for launcher items
const handleDragOver = (event: DragEvent) => {
if (!event.dataTransfer) return
// Check if this is a launcher item
if (event.dataTransfer.types.includes('application/haex-launcher-item')) {
event.dataTransfer.dropEffect = 'copy'
}
}
// Handle drop for launcher items
const handleDrop = async (event: DragEvent, workspaceId: string) => {
if (!event.dataTransfer) return
const launcherItemData = event.dataTransfer.getData('application/haex-launcher-item')
if (!launcherItemData) return
try {
const item = JSON.parse(launcherItemData) as {
id: string
name: string
icon: string
type: 'system' | 'extension'
}
// Get drop position relative to desktop
const desktopRect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
const y = Math.max(0, event.clientY - desktopRect.top - 32)
// Create desktop icon on the specific workspace
await desktopStore.addDesktopItemAsync(
item.type as DesktopItemType,
item.id,
x,
y,
workspaceId
)
} catch (error) {
console.error('Failed to create desktop icon:', error)
}
}
const handleDesktopClick = () => {
// Only clear selection if it was a simple click, not an area selection
// Check if we just finished an area selection (box size > threshold)

View File

@ -11,22 +11,29 @@
<template #content>
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
<UiButton
<UContextMenu
v-for="item in launcherItems"
:key="item.id"
square
size="xl"
variant="ghost"
:ui="{
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
leadingIcon: 'size-10',
label: 'w-full',
}"
:icon="item.icon"
:label="item.name"
:tooltip="item.name"
@click="openItem(item)"
/>
:items="getContextMenuItems(item)"
>
<UiButton
square
size="lg"
variant="ghost"
:ui="{
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
leadingIcon: 'size-10',
label: 'w-full',
}"
:icon="item.icon"
:label="item.name"
:tooltip="item.name"
draggable="true"
@click="openItem(item)"
@dragstart="handleDragStart($event, item)"
@dragend="handleDragEnd"
/>
</UContextMenu>
<!-- Disabled Extensions (grayed out) -->
<UiButton
@ -119,14 +126,77 @@ const openItem = async (item: LauncherItem) => {
console.log(error)
}
}
// Uninstall extension
const uninstallExtension = async (item: LauncherItem) => {
try {
const extension = extensionStore.availableExtensions.find(ext => ext.id === item.id)
if (!extension) return
await extensionStore.removeExtensionAsync(
extension.publicKey,
extension.name,
extension.version
)
} catch (error) {
console.error('Failed to uninstall extension:', error)
}
}
// Get context menu items for launcher item
const getContextMenuItems = (item: LauncherItem) => {
const items = [
{
label: t('contextMenu.open'),
icon: 'i-heroicons-arrow-top-right-on-square',
click: () => openItem(item),
}
]
// Add uninstall option for extensions
if (item.type === 'extension') {
items.push({
label: t('contextMenu.uninstall'),
icon: 'i-heroicons-trash',
click: () => uninstallExtension(item),
})
}
return items
}
// Drag & Drop handling
const handleDragStart = (event: DragEvent, item: LauncherItem) => {
if (!event.dataTransfer) return
// Store the launcher item data
event.dataTransfer.effectAllowed = 'copy'
event.dataTransfer.setData('application/haex-launcher-item', JSON.stringify(item))
// Set drag image (optional - uses default if not set)
const dragImage = event.target as HTMLElement
if (dragImage) {
event.dataTransfer.setDragImage(dragImage, 20, 20)
}
}
const handleDragEnd = () => {
// Cleanup if needed
}
</script>
<i18n lang="yaml">
de:
disabled: Deaktiviert
marketplace: Marketplace
contextMenu:
open: Öffnen
uninstall: Deinstallieren
en:
disabled: Disabled
marketplace: Marketplace
contextMenu:
open: Open
uninstall: Uninstall
</i18n>

View File

@ -24,7 +24,7 @@
<!-- Window Thumbnails Flex Layout -->
<div
v-if="windows.length > 0"
class="flex flex-wrap gap-6 justify-start items-start"
class="flex flex-wrap gap-6 justify-center-safe items-start"
>
<div
v-for="window in windows"

View File

@ -1,139 +0,0 @@
<template>
<div>
<div
class="grid grid-rows-2 sm:grid-cols-2 sm:gap-2 p-2 max-w-2xl w-full h-fit"
>
<div class="p-2">{{ t('language') }}</div>
<div><UiDropdownLocale @select="onSelectLocaleAsync" /></div>
<div class="p-2">{{ t('design') }}</div>
<div><UiDropdownTheme @select="onSelectThemeAsync" /></div>
<div class="p-2">{{ t('vaultName.label') }}</div>
<div>
<UiInput
v-model="currentVaultName"
:placeholder="t('vaultName.label')"
@change="onSetVaultNameAsync"
/>
</div>
<div class="p-2">{{ t('notifications.label') }}</div>
<div>
<UiButton
:label="t('notifications.requestPermission')"
@click="requestNotificationPermissionAsync"
/>
</div>
<div class="p-2">{{ t('deviceName.label') }}</div>
<div>
<UiInput
v-model="deviceName"
:placeholder="t('deviceName.label')"
@change="onUpdateDeviceNameAsync"
/>
</div>
</div>
<!-- Child routes (like developer.vue) will be rendered here -->
<NuxtPage />
</div>
</template>
<script setup lang="ts">
import type { Locale } from 'vue-i18n'
definePageMeta({
name: 'settings',
})
const { t, setLocale } = useI18n()
const { currentVaultName } = storeToRefs(useVaultStore())
const { updateVaultNameAsync, updateLocaleAsync, updateThemeAsync } =
useVaultSettingsStore()
const onSelectLocaleAsync = async (locale: Locale) => {
await updateLocaleAsync(locale)
await setLocale(locale)
}
const { currentThemeName } = storeToRefs(useUiStore())
const onSelectThemeAsync = async (theme: string) => {
currentThemeName.value = theme
console.log('onSelectThemeAsync', currentThemeName.value)
await updateThemeAsync(theme)
}
const { add } = useToast()
const onSetVaultNameAsync = async () => {
try {
await updateVaultNameAsync(currentVaultName.value)
add({ description: t('vaultName.update.success'), color: 'success' })
} catch (error) {
console.error(error)
add({ description: t('vaultName.update.error'), color: 'error' })
}
}
const { requestNotificationPermissionAsync } = useNotificationStore()
const { deviceName } = storeToRefs(useDeviceStore())
const { updateDeviceNameAsync, readDeviceNameAsync } = useDeviceStore()
onMounted(async () => {
await readDeviceNameAsync()
})
const onUpdateDeviceNameAsync = async () => {
const check = vaultDeviceNameSchema.safeParse(deviceName.value)
if (!check.success) return
try {
await updateDeviceNameAsync({ name: deviceName.value })
add({ description: t('deviceName.update.success'), color: 'success' })
} catch (error) {
console.log(error)
add({ description: t('deviceName.update.error'), color: 'error' })
}
}
</script>
<i18n lang="yaml">
de:
language: Sprache
design: Design
save: Änderung speichern
notifications:
label: Benachrichtigungen
requestPermission: Benachrichtigung erlauben
vaultName:
label: Vaultname
update:
success: Vaultname erfolgreich aktualisiert
error: Vaultname konnte nicht aktualisiert werden
deviceName:
label: Gerätename
update:
success: Gerätename wurde erfolgreich aktualisiert
error: Gerätename konnte nich aktualisiert werden
en:
language: Language
design: Design
save: save changes
notifications:
label: Notifications
requestPermission: Grant Permission
vaultName:
label: Vault Name
update:
success: Vault Name successfully updated
error: Vault name could not be updated
deviceName:
label: Device name
update:
success: Device name has been successfully updated
error: Device name could not be updated
</i18n>

View File

@ -1,279 +0,0 @@
<template>
<div class="p-4 max-w-4xl mx-auto space-y-6">
<div class="space-y-2">
<h1 class="text-2xl font-bold">{{ t('title') }}</h1>
<p class="text-sm opacity-70">{{ t('description') }}</p>
</div>
<!-- Add Dev Extension Form -->
<UCard class="p-4 space-y-4">
<h2 class="text-lg font-semibold">{{ t('add.title') }}</h2>
<div class="space-y-2">
<label class="text-sm font-medium">{{ t('add.extensionPath') }}</label>
<div class="flex gap-2">
<UiInput
v-model="extensionPath"
:placeholder="t('add.extensionPathPlaceholder')"
class="flex-1"
/>
<UiButton
:label="t('add.browse')"
variant="outline"
@click="browseExtensionPathAsync"
/>
</div>
<p class="text-xs opacity-60">{{ t('add.extensionPathHint') }}</p>
</div>
<UiButton
:label="t('add.loadExtension')"
:loading="isLoading"
:disabled="!extensionPath"
@click="loadDevExtensionAsync"
/>
</UCard>
<!-- List of Dev Extensions -->
<div
v-if="devExtensions.length > 0"
class="space-y-2"
>
<h2 class="text-lg font-semibold">{{ t('list.title') }}</h2>
<UCard
v-for="ext in devExtensions"
:key="ext.id"
class="p-4 flex items-center justify-between"
>
<div class="space-y-1">
<div class="flex items-center gap-2">
<h3 class="font-medium">{{ ext.name }}</h3>
<UBadge color="info">DEV</UBadge>
</div>
<p class="text-sm opacity-70">v{{ ext.version }}</p>
<p class="text-xs opacity-50">{{ ext.publicKey.slice(0, 16) }}...</p>
</div>
<div class="flex gap-2">
<UiButton
:label="t('list.reload')"
variant="outline"
size="sm"
@click="reloadDevExtensionAsync(ext)"
/>
<UiButton
:label="t('list.remove')"
variant="ghost"
size="sm"
color="error"
@click="removeDevExtensionAsync(ext)"
/>
</div>
</UCard>
</div>
<div
v-else
class="text-center py-8 opacity-50"
>
{{ t('list.empty') }}
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
definePageMeta({
name: 'settings-developer',
})
const { t } = useI18n()
const { add } = useToast()
const { loadExtensionsAsync } = useExtensionsStore()
// State
const extensionPath = ref('')
const isLoading = ref(false)
const devExtensions = ref<
Array<{
id: string
publicKey: string
name: string
version: string
enabled: boolean
}>
>([])
// Load dev extensions on mount
onMounted(async () => {
await loadDevExtensionListAsync()
})
// Browse for extension directory
const browseExtensionPathAsync = async () => {
try {
const selected = await open({
directory: true,
multiple: false,
title: t('add.browseTitle'),
})
if (selected && typeof selected === 'string') {
extensionPath.value = selected
}
} catch (error) {
console.error('Failed to browse directory:', error)
add({
description: t('add.errors.browseFailed'),
color: 'error',
})
}
}
// Load a dev extension
const loadDevExtensionAsync = async () => {
if (!extensionPath.value) return
isLoading.value = true
try {
const extensionId = await invoke<string>('load_dev_extension', {
extensionPath: extensionPath.value,
})
add({
description: t('add.success'),
color: 'success',
})
// Reload list
await loadDevExtensionListAsync()
// Reload all extensions in the main extension store so they appear in the launcher
await loadExtensionsAsync()
// Clear input
extensionPath.value = ''
} catch (error: any) {
console.error('Failed to load dev extension:', error)
add({
description: error || t('add.errors.loadFailed'),
color: 'error',
})
} finally {
isLoading.value = false
}
}
// Load all dev extensions (for the list on this page)
const loadDevExtensionListAsync = async () => {
try {
const extensions = await invoke<Array<any>>('get_all_dev_extensions')
devExtensions.value = extensions
} catch (error) {
console.error('Failed to load dev extensions:', error)
}
}
// Reload a dev extension (removes and re-adds)
const reloadDevExtensionAsync = async (ext: any) => {
try {
// Get the extension path from somewhere (we need to store this)
// For now, just show a message
add({
description: t('list.reloadInfo'),
color: 'info',
})
} catch (error: any) {
console.error('Failed to reload dev extension:', error)
add({
description: error || t('list.errors.reloadFailed'),
color: 'error',
})
}
}
// Remove a dev extension
const removeDevExtensionAsync = async (ext: any) => {
try {
await invoke('remove_dev_extension', {
publicKey: ext.publicKey,
name: ext.name,
})
add({
description: t('list.removeSuccess'),
color: 'success',
})
// Reload list
await loadDevExtensionListAsync()
// Reload all extensions store
await loadExtensionsAsync()
} catch (error: any) {
console.error('Failed to remove dev extension:', error)
add({
description: error || t('list.errors.removeFailed'),
color: 'error',
})
}
}
</script>
<i18n lang="yaml">
de:
title: Entwicklereinstellungen
description: Lade Extensions im Entwicklungsmodus für schnelleres Testen mit Hot-Reload.
add:
title: Dev-Extension hinzufügen
extensionPath: Extension-Pfad
extensionPathPlaceholder: /pfad/zu/deiner/extension
extensionPathHint: Pfad zum Extension-Projekt (enthält haextension/ und haextension.json)
browse: Durchsuchen
browseTitle: Extension-Verzeichnis auswählen
loadExtension: Extension laden
success: Dev-Extension erfolgreich geladen
errors:
browseFailed: Verzeichnis konnte nicht ausgewählt werden
loadFailed: Extension konnte nicht geladen werden
list:
title: Geladene Dev-Extensions
empty: Keine Dev-Extensions geladen
reload: Neu laden
remove: Entfernen
reloadInfo: Extension wird beim nächsten Laden automatisch aktualisiert
removeSuccess: Dev-Extension erfolgreich entfernt
errors:
reloadFailed: Extension konnte nicht neu geladen werden
removeFailed: Extension konnte nicht entfernt werden
en:
title: Developer Settings
description: Load extensions in development mode for faster testing with hot-reload.
add:
title: Add Dev Extension
extensionPath: Extension Path
extensionPathPlaceholder: /path/to/your/extension
extensionPathHint: Path to your extension project (contains haextension/ and haextension.json)
browse: Browse
browseTitle: Select Extension Directory
loadExtension: Load Extension
success: Dev extension loaded successfully
errors:
browseFailed: Failed to select directory
loadFailed: Failed to load extension
list:
title: Loaded Dev Extensions
empty: No dev extensions loaded
reload: Reload
remove: Remove
reloadInfo: Extension will be automatically updated on next load
removeSuccess: Dev extension removed successfully
errors:
reloadFailed: Failed to reload extension
removeFailed: Failed to remove extension
</i18n>

View File

@ -7,7 +7,7 @@ import type {
import de from './de.json'
import en from './en.json'
export type DesktopItemType = 'extension' | 'file' | 'folder'
export type DesktopItemType = 'extension' | 'file' | 'folder' | 'system'
export interface IDesktopItem extends SelectHaexDesktopItems {
label?: string
@ -57,18 +57,20 @@ export const useDesktopStore = defineStore('desktopStore', () => {
referenceId: string,
positionX: number = 0,
positionY: number = 0,
workspaceId?: string,
) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
if (!currentWorkspace.value) {
const targetWorkspaceId = workspaceId || currentWorkspace.value?.id
if (!targetWorkspaceId) {
throw new Error('Kein Workspace aktiv')
}
try {
const newItem: InsertHaexDesktopItems = {
workspaceId: currentWorkspace.value.id,
workspaceId: targetWorkspaceId,
itemType: itemType,
referenceId: referenceId,
positionX: positionX,
@ -85,7 +87,19 @@ export const useDesktopStore = defineStore('desktopStore', () => {
return result[0]
}
} catch (error) {
console.error('Fehler beim Hinzufügen des Desktop-Items:', error)
console.error('Fehler beim Hinzufügen des Desktop-Items:', {
error,
itemType,
referenceId,
workspaceId: targetWorkspaceId,
position: { x: positionX, y: positionY }
})
// Log full error details
if (error && typeof error === 'object') {
console.error('Full error object:', JSON.stringify(error, null, 2))
}
throw error
}
}
@ -154,8 +168,23 @@ export const useDesktopStore = defineStore('desktopStore', () => {
referenceId: string,
sourcePosition?: { x: number; y: number; width: number; height: number },
) => {
if (itemType === 'extension') {
const windowManager = useWindowManagerStore()
const windowManager = useWindowManagerStore()
if (itemType === 'system') {
const systemWindow = windowManager.getAllSystemWindows().find(
(win) => win.id === referenceId,
)
if (systemWindow) {
windowManager.openWindowAsync({
sourceId: systemWindow.id,
type: 'system',
icon: systemWindow.icon,
title: systemWindow.name,
sourcePosition,
})
}
} else if (itemType === 'extension') {
const extensionsStore = useExtensionsStore()
const extension = extensionsStore.availableExtensions.find(

View File

@ -1,4 +1,4 @@
import { eq } from 'drizzle-orm'
import { asc, eq } from 'drizzle-orm'
import {
haexWorkspaces,
type SelectHaexWorkspaces,
@ -32,18 +32,18 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
}
try {
/* const items = await currentVault.value.drizzle
const items = await currentVault.value.drizzle
.select()
.from(haexWorkspaces)
.orderBy(asc(haexWorkspaces.position))
console.log('loadWorkspacesAsync', items)
workspaces.value = items */
workspaces.value = items
// Create default workspace if none exist
/* if (items.length === 0) { */
await addWorkspaceAsync('Workspace 1')
/* } */
if (items.length === 0) {
await addWorkspaceAsync('Workspace 1')
}
} catch (error) {
console.error('Fehler beim Laden der Workspaces:', error)
throw error
@ -61,15 +61,12 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
try {
const newIndex = workspaces.value.length + 1
const newWorkspace: SelectHaexWorkspaces = {
id: crypto.randomUUID(),
const newWorkspace = {
name: name || `Workspace ${newIndex}`,
position: workspaces.value.length,
haexTimestamp: '',
}
workspaces.value.push(newWorkspace)
currentWorkspaceIndex.value = workspaces.value.length - 1
/* const result = await currentVault.value.drizzle
const result = await currentVault.value.drizzle
.insert(haexWorkspaces)
.values(newWorkspace)
.returning()
@ -78,7 +75,7 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
workspaces.value.push(result[0])
currentWorkspaceIndex.value = workspaces.value.length - 1
return result[0]
} */
}
} catch (error) {
console.error('Fehler beim Hinzufügen des Workspace:', error)
throw error
@ -106,27 +103,27 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
if (index === -1) return
workspaces.value.splice(index, 1)
workspaces.value.forEach((workspace, index) => (workspace.position = index))
try {
/* await currentVault.value.drizzle.transaction(async (tx) => {
await currentVault.value.drizzle.transaction(async (tx) => {
// Delete workspace
await tx
.delete(haexWorkspaces)
.where(eq(haexWorkspaces.id, workspaceId))
// Update local state
workspaces.value.splice(index, 1)
workspaces.value.forEach(
(workspace, index) => (workspace.position = index),
)
workspaces.value.forEach((workspace, idx) => {
workspace.position = idx
})
// Update positions in database
for (const workspace of workspaces.value) {
await tx
.update(haexWorkspaces)
.set({ position: index })
.where(eq(haexWorkspaces.position, workspace.position))
.set({ position: workspace.position })
.where(eq(haexWorkspaces.id, workspace.id))
}
}) */
})
// Adjust current index if needed
if (currentWorkspaceIndex.value >= workspaces.value.length) {