mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
Add device management and database-backed desktop settings
This update migrates desktop grid settings from localStorage to the database and introduces a comprehensive device management system. Features: - New haex_devices table for device identification and naming - Device-specific settings with foreign key relationships - Preset-based icon sizes (Small, Medium, Large, Extra Large) - Grid positioning improvements to prevent dragging behind PageHeader - Dynamic icon sizing based on database settings Database Changes: - Created haex_devices table with deviceId (UUID) and name columns - Modified haex_settings to include device_id FK and updated unique constraint - Migration 0002_loose_quasimodo.sql for schema changes Technical Improvements: - Replaced arbitrary icon size slider (60-200px) with preset system - Icons use actual measured dimensions for proper grid centering - Settings sync on vault mount for cross-device consistency - Proper bounds checking during icon drag operations Bump version to 0.1.7
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "haex-hub",
|
"name": "haex-hub",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
|
|||||||
13
src-tauri/database/migrations/0002_loose_quasimodo.sql
Normal file
13
src-tauri/database/migrations/0002_loose_quasimodo.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE `haex_devices` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`device_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
`updated_at` integer,
|
||||||
|
`haex_timestamp` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `haex_devices_device_id_unique` ON `haex_devices` (`device_id`);--> statement-breakpoint
|
||||||
|
DROP INDEX `haex_settings_key_type_value_unique`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `haex_settings` ADD `device_id` text REFERENCES haex_devices(id);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `haex_settings_device_id_key_type_unique` ON `haex_settings` (`device_id`,`key`,`type`);
|
||||||
774
src-tauri/database/migrations/meta/0002_snapshot.json
Normal file
774
src-tauri/database/migrations/meta/0002_snapshot.json
Normal file
@ -0,0 +1,774 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "3aedf10c-2266-40f4-8549-0ff8b0588853",
|
||||||
|
"prevId": "10bec43a-4227-483e-b1c1-fd50ae32bb96",
|
||||||
|
"tables": {
|
||||||
|
"haex_crdt_configs": {
|
||||||
|
"name": "haex_crdt_configs",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_crdt_logs": {
|
||||||
|
"name": "haex_crdt_logs",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"table_name": {
|
||||||
|
"name": "table_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"row_pks": {
|
||||||
|
"name": "row_pks",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"op_type": {
|
||||||
|
"name": "op_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"column_name": {
|
||||||
|
"name": "column_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"new_value": {
|
||||||
|
"name": "new_value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"old_value": {
|
||||||
|
"name": "old_value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_haex_timestamp": {
|
||||||
|
"name": "idx_haex_timestamp",
|
||||||
|
"columns": [
|
||||||
|
"haex_timestamp"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_table_row": {
|
||||||
|
"name": "idx_table_row",
|
||||||
|
"columns": [
|
||||||
|
"table_name",
|
||||||
|
"row_pks"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_crdt_snapshots": {
|
||||||
|
"name": "haex_crdt_snapshots",
|
||||||
|
"columns": {
|
||||||
|
"snapshot_id": {
|
||||||
|
"name": "snapshot_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"name": "created",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"epoch_hlc": {
|
||||||
|
"name": "epoch_hlc",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"location_url": {
|
||||||
|
"name": "location_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"file_size_bytes": {
|
||||||
|
"name": "file_size_bytes",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_desktop_items": {
|
||||||
|
"name": "haex_desktop_items",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"item_type": {
|
||||||
|
"name": "item_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"extension_id": {
|
||||||
|
"name": "extension_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"system_window_id": {
|
||||||
|
"name": "system_window_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"position_x": {
|
||||||
|
"name": "position_x",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"position_y": {
|
||||||
|
"name": "position_y",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"haex_desktop_items_workspace_id_haex_workspaces_id_fk": {
|
||||||
|
"name": "haex_desktop_items_workspace_id_haex_workspaces_id_fk",
|
||||||
|
"tableFrom": "haex_desktop_items",
|
||||||
|
"tableTo": "haex_workspaces",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"haex_desktop_items_extension_id_haex_extensions_id_fk": {
|
||||||
|
"name": "haex_desktop_items_extension_id_haex_extensions_id_fk",
|
||||||
|
"tableFrom": "haex_desktop_items",
|
||||||
|
"tableTo": "haex_extensions",
|
||||||
|
"columnsFrom": [
|
||||||
|
"extension_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"item_reference": {
|
||||||
|
"name": "item_reference",
|
||||||
|
"value": "(\"haex_desktop_items\".\"item_type\" = 'extension' AND \"haex_desktop_items\".\"extension_id\" IS NOT NULL AND \"haex_desktop_items\".\"system_window_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'system' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'file' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'folder' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"haex_devices": {
|
||||||
|
"name": "haex_devices",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(CURRENT_TIMESTAMP)"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_devices_device_id_unique": {
|
||||||
|
"name": "haex_devices_device_id_unique",
|
||||||
|
"columns": [
|
||||||
|
"device_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_extension_permissions": {
|
||||||
|
"name": "haex_extension_permissions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"extension_id": {
|
||||||
|
"name": "extension_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"resource_type": {
|
||||||
|
"name": "resource_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"name": "action",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"name": "target",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"name": "constraints",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'denied'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(CURRENT_TIMESTAMP)"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
||||||
|
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
||||||
|
"columns": [
|
||||||
|
"extension_id",
|
||||||
|
"resource_type",
|
||||||
|
"action",
|
||||||
|
"target"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
||||||
|
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
||||||
|
"tableFrom": "haex_extension_permissions",
|
||||||
|
"tableTo": "haex_extensions",
|
||||||
|
"columnsFrom": [
|
||||||
|
"extension_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_extensions": {
|
||||||
|
"name": "haex_extensions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"public_key": {
|
||||||
|
"name": "public_key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"entry": {
|
||||||
|
"name": "entry",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'index.html'"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"name": "homepage",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"name": "signature",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"single_instance": {
|
||||||
|
"name": "single_instance",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_extensions_public_key_name_unique": {
|
||||||
|
"name": "haex_extensions_public_key_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"public_key",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_notifications": {
|
||||||
|
"name": "haex_notifications",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"alt": {
|
||||||
|
"name": "alt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"name": "date",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"read": {
|
||||||
|
"name": "read",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"name": "text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_settings": {
|
||||||
|
"name": "haex_settings",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_settings_device_id_key_type_unique": {
|
||||||
|
"name": "haex_settings_device_id_key_type_unique",
|
||||||
|
"columns": [
|
||||||
|
"device_id",
|
||||||
|
"key",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"haex_settings_device_id_haex_devices_id_fk": {
|
||||||
|
"name": "haex_settings_device_id_haex_devices_id_fk",
|
||||||
|
"tableFrom": "haex_settings",
|
||||||
|
"tableTo": "haex_devices",
|
||||||
|
"columnsFrom": [
|
||||||
|
"device_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"haex_workspaces": {
|
||||||
|
"name": "haex_workspaces",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "device_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"name": "position",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"name": "background",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"haex_timestamp": {
|
||||||
|
"name": "haex_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"haex_workspaces_position_unique": {
|
||||||
|
"name": "haex_workspaces_position_unique",
|
||||||
|
"columns": [
|
||||||
|
"position"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,13 @@
|
|||||||
"when": 1762122405562,
|
"when": 1762122405562,
|
||||||
"tag": "0001_furry_brother_voodoo",
|
"tag": "0001_furry_brother_voodoo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1762263814375,
|
||||||
|
"tag": "0002_loose_quasimodo",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
@ -24,29 +24,25 @@
|
|||||||
<div class="flex flex-col items-center gap-2 p-3 group">
|
<div class="flex flex-col items-center gap-2 p-3 group">
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'w-20 h-20 flex items-center justify-center rounded-2xl transition-all duration-200 ease-out',
|
'flex items-center justify-center rounded-2xl transition-all duration-200 ease-out',
|
||||||
'backdrop-blur-sm border',
|
'backdrop-blur-sm border',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-white/95 dark:bg-gray-800/95 border-blue-500 dark:border-blue-400 shadow-lg scale-105'
|
? 'bg-white/95 dark:bg-gray-800/95 border-blue-500 dark:border-blue-400 shadow-lg scale-105'
|
||||||
: 'bg-white/80 dark:bg-gray-800/80 border-gray-200/50 dark:border-gray-700/50 hover:bg-white/90 dark:hover:bg-gray-800/90 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md hover:scale-105',
|
: 'bg-white/80 dark:bg-gray-800/80 border-gray-200/50 dark:border-gray-700/50 hover:bg-white/90 dark:hover:bg-gray-800/90 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md hover:scale-105',
|
||||||
]"
|
]"
|
||||||
|
:style="{ width: `${containerSize}px`, height: `${containerSize}px` }"
|
||||||
>
|
>
|
||||||
<img
|
<HaexIcon
|
||||||
v-if="icon"
|
:name="icon || 'i-heroicons-puzzle-piece-solid'"
|
||||||
:src="icon"
|
|
||||||
: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"
|
|
||||||
:class="[
|
:class="[
|
||||||
'w-14 h-14 transition-all duration-200',
|
'object-contain transition-all duration-200',
|
||||||
isSelected
|
isSelected && 'scale-110',
|
||||||
? 'text-blue-500 dark:text-blue-400 scale-110'
|
!icon &&
|
||||||
: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-500 dark:group-hover:text-gray-400',
|
(isSelected
|
||||||
|
? 'text-blue-500 dark:text-blue-400'
|
||||||
|
: 'text-gray-400 dark:text-gray-500 group-hover:text-gray-500 dark:group-hover:text-gray-400'),
|
||||||
]"
|
]"
|
||||||
|
:style="{ width: `${innerIconSize}px`, height: `${innerIconSize}px` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
@ -79,15 +75,19 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
positionChanged: [id: string, x: number, y: number]
|
positionChanged: [id: string, x: number, y: number]
|
||||||
dragStart: [id: string, itemType: string, referenceId: string]
|
dragStart: [id: string, itemType: string, referenceId: string, width: number, height: number, x: number, y: number]
|
||||||
|
dragging: [id: string, x: number, y: number]
|
||||||
dragEnd: []
|
dragEnd: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const desktopStore = useDesktopStore()
|
const desktopStore = useDesktopStore()
|
||||||
|
const { effectiveIconSize } = storeToRefs(desktopStore)
|
||||||
const showUninstallDialog = ref(false)
|
const showUninstallDialog = ref(false)
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const isSelected = computed(() => desktopStore.isItemSelected(props.id))
|
const isSelected = computed(() => desktopStore.isItemSelected(props.id))
|
||||||
|
const containerSize = computed(() => effectiveIconSize.value) // Container size
|
||||||
|
const innerIconSize = computed(() => effectiveIconSize.value * 0.7) // Inner icon is 70% of container
|
||||||
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
// Prevent selection during drag
|
// Prevent selection during drag
|
||||||
@ -131,9 +131,40 @@ const isDragging = ref(false)
|
|||||||
const offsetX = ref(0)
|
const offsetX = ref(0)
|
||||||
const offsetY = ref(0)
|
const offsetY = ref(0)
|
||||||
|
|
||||||
// Icon dimensions (approximate)
|
// Track actual icon dimensions dynamically
|
||||||
const iconWidth = 120 // Matches design in template
|
const { width: iconWidth, height: iconHeight } = useElementSize(draggableEl)
|
||||||
const iconHeight = 140
|
|
||||||
|
// Re-center icon position when dimensions are measured
|
||||||
|
watch([iconWidth, iconHeight], async ([width, height]) => {
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
console.log('📐 Icon dimensions measured:', {
|
||||||
|
label: props.label,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
currentPosition: { x: x.value, y: y.value },
|
||||||
|
gridCellSize: desktopStore.gridCellSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Re-snap to grid with actual dimensions to ensure proper centering
|
||||||
|
const snapped = desktopStore.snapToGrid(x.value, y.value, width, height)
|
||||||
|
|
||||||
|
console.log('📍 Snapped position:', {
|
||||||
|
label: props.label,
|
||||||
|
oldPosition: { x: x.value, y: y.value },
|
||||||
|
newPosition: snapped,
|
||||||
|
})
|
||||||
|
|
||||||
|
const oldX = x.value
|
||||||
|
const oldY = y.value
|
||||||
|
x.value = snapped.x
|
||||||
|
y.value = snapped.y
|
||||||
|
|
||||||
|
// Save corrected position to database if it changed
|
||||||
|
if (oldX !== snapped.x || oldY !== snapped.y) {
|
||||||
|
emit('positionChanged', props.id, snapped.x, snapped.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { once: true }) // Only run once when dimensions are first measured
|
||||||
|
|
||||||
const style = computed(() => ({
|
const style = computed(() => ({
|
||||||
position: 'absolute' as const,
|
position: 'absolute' as const,
|
||||||
@ -146,7 +177,7 @@ const handlePointerDown = (e: PointerEvent) => {
|
|||||||
if (!draggableEl.value || !draggableEl.value.parentElement) return
|
if (!draggableEl.value || !draggableEl.value.parentElement) return
|
||||||
|
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
emit('dragStart', props.id, props.itemType, props.referenceId)
|
emit('dragStart', props.id, props.itemType, props.referenceId, iconWidth.value, iconHeight.value, x.value, y.value)
|
||||||
|
|
||||||
// Get parent offset to convert from viewport coordinates to parent-relative coordinates
|
// Get parent offset to convert from viewport coordinates to parent-relative coordinates
|
||||||
const parentRect = draggableEl.value.parentElement.getBoundingClientRect()
|
const parentRect = draggableEl.value.parentElement.getBoundingClientRect()
|
||||||
@ -165,8 +196,12 @@ const handlePointerMove = (e: PointerEvent) => {
|
|||||||
const newX = e.clientX - parentRect.left - offsetX.value
|
const newX = e.clientX - parentRect.left - offsetX.value
|
||||||
const newY = e.clientY - parentRect.top - offsetY.value
|
const newY = e.clientY - parentRect.top - offsetY.value
|
||||||
|
|
||||||
|
// Clamp y position to minimum 0 (parent is already below header)
|
||||||
x.value = newX
|
x.value = newX
|
||||||
y.value = newY
|
y.value = Math.max(0, newY)
|
||||||
|
|
||||||
|
// Emit current position during drag
|
||||||
|
emit('dragging', props.id, x.value, y.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerUp = (e: PointerEvent) => {
|
const handlePointerUp = (e: PointerEvent) => {
|
||||||
@ -177,10 +212,15 @@ const handlePointerUp = (e: PointerEvent) => {
|
|||||||
draggableEl.value.releasePointerCapture(e.pointerId)
|
draggableEl.value.releasePointerCapture(e.pointerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snap to grid with icon dimensions
|
||||||
|
const snapped = desktopStore.snapToGrid(x.value, y.value, iconWidth.value, iconHeight.value)
|
||||||
|
x.value = snapped.x
|
||||||
|
y.value = snapped.y
|
||||||
|
|
||||||
// Snap icon to viewport bounds if outside
|
// Snap icon to viewport bounds if outside
|
||||||
if (viewportSize) {
|
if (viewportSize) {
|
||||||
const maxX = Math.max(0, viewportSize.width.value - iconWidth)
|
const maxX = Math.max(0, viewportSize.width.value - iconWidth.value)
|
||||||
const maxY = Math.max(0, viewportSize.height.value - iconHeight)
|
const maxY = Math.max(0, viewportSize.height.value - iconHeight.value)
|
||||||
x.value = Math.max(0, Math.min(maxX, x.value))
|
x.value = Math.max(0, Math.min(maxX, x.value))
|
||||||
y.value = Math.max(0, Math.min(maxY, y.value))
|
y.value = Math.max(0, Math.min(maxY, y.value))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,15 @@
|
|||||||
@dragover.prevent="handleDragOver"
|
@dragover.prevent="handleDragOver"
|
||||||
@drop.prevent="handleDrop($event, workspace.id)"
|
@drop.prevent="handleDrop($event, workspace.id)"
|
||||||
>
|
>
|
||||||
<!-- Grid Pattern Background -->
|
<!-- Drop Target Zone (visible during drag) -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 pointer-events-none opacity-30"
|
v-if="dropTargetZone"
|
||||||
|
class="absolute border-2 border-blue-500 bg-blue-500/10 rounded-lg pointer-events-none z-10 transition-all duration-75"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundImage:
|
left: `${dropTargetZone.x}px`,
|
||||||
'linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)',
|
top: `${dropTargetZone.y}px`,
|
||||||
backgroundSize: '32px 32px',
|
width: `${dropTargetZone.width}px`,
|
||||||
|
height: `${dropTargetZone.height}px`,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -79,6 +81,7 @@
|
|||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@position-changed="handlePositionChanged"
|
@position-changed="handlePositionChanged"
|
||||||
@drag-start="handleDragStart"
|
@drag-start="handleDragStart"
|
||||||
|
@dragging="handleDragging"
|
||||||
@drag-end="handleDragEnd"
|
@drag-end="handleDragEnd"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -249,8 +252,6 @@ const {
|
|||||||
const { getWorkspaceBackgroundStyle, getWorkspaceContextMenuItems } =
|
const { getWorkspaceBackgroundStyle, getWorkspaceContextMenuItems } =
|
||||||
workspaceStore
|
workspaceStore
|
||||||
|
|
||||||
const { x: mouseX } = useMouse()
|
|
||||||
|
|
||||||
const desktopEl = useTemplateRef('desktopEl')
|
const desktopEl = useTemplateRef('desktopEl')
|
||||||
|
|
||||||
// Track desktop viewport size reactively
|
// Track desktop viewport size reactively
|
||||||
@ -284,9 +285,41 @@ const selectionBoxStyle = computed(() => {
|
|||||||
|
|
||||||
// Drag state for desktop icons
|
// Drag state for desktop icons
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const currentDraggedItemId = ref<string>()
|
const currentDraggedItem = reactive({
|
||||||
const currentDraggedItemType = ref<string>()
|
id: '',
|
||||||
const currentDraggedReferenceId = ref<string>()
|
itemType: '',
|
||||||
|
referenceId: '',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Track mouse position for showing drop target
|
||||||
|
const { x: mouseX, y: mouseY } = useMouse()
|
||||||
|
|
||||||
|
const dropTargetZone = computed(() => {
|
||||||
|
if (!isDragging.value) return null
|
||||||
|
|
||||||
|
// Use the actual icon position during drag, not the mouse position
|
||||||
|
const iconX = currentDraggedItem.x
|
||||||
|
const iconY = currentDraggedItem.y
|
||||||
|
|
||||||
|
// Use snapToGrid to get the exact position where the icon will land
|
||||||
|
const snapped = desktopStore.snapToGrid(
|
||||||
|
iconX,
|
||||||
|
iconY,
|
||||||
|
currentDraggedItem.width || undefined,
|
||||||
|
currentDraggedItem.height || undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: snapped.x,
|
||||||
|
y: snapped.y,
|
||||||
|
width: currentDraggedItem.width || desktopStore.gridCellSize,
|
||||||
|
height: currentDraggedItem.height || desktopStore.gridCellSize,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Window drag state for snap zones
|
// Window drag state for snap zones
|
||||||
const isWindowDragging = ref(false)
|
const isWindowDragging = ref(false)
|
||||||
@ -378,20 +411,43 @@ const handlePositionChanged = async (id: string, x: number, y: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDragStart = (id: string, itemType: string, referenceId: string) => {
|
const handleDragStart = (
|
||||||
|
id: string,
|
||||||
|
itemType: string,
|
||||||
|
referenceId: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
) => {
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
currentDraggedItemId.value = id
|
currentDraggedItem.id = id
|
||||||
currentDraggedItemType.value = itemType
|
currentDraggedItem.itemType = itemType
|
||||||
currentDraggedReferenceId.value = referenceId
|
currentDraggedItem.referenceId = referenceId
|
||||||
|
currentDraggedItem.width = width
|
||||||
|
currentDraggedItem.height = height
|
||||||
|
currentDraggedItem.x = x
|
||||||
|
currentDraggedItem.y = y
|
||||||
allowSwipe.value = false // Disable Swiper during icon drag
|
allowSwipe.value = false // Disable Swiper during icon drag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragging = (id: string, x: number, y: number) => {
|
||||||
|
if (currentDraggedItem.id === id) {
|
||||||
|
currentDraggedItem.x = x
|
||||||
|
currentDraggedItem.y = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDragEnd = async () => {
|
const handleDragEnd = async () => {
|
||||||
// Cleanup drag state
|
// Cleanup drag state
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
currentDraggedItemId.value = undefined
|
currentDraggedItem.id = ''
|
||||||
currentDraggedItemType.value = undefined
|
currentDraggedItem.itemType = ''
|
||||||
currentDraggedReferenceId.value = undefined
|
currentDraggedItem.referenceId = ''
|
||||||
|
currentDraggedItem.width = 0
|
||||||
|
currentDraggedItem.height = 0
|
||||||
|
currentDraggedItem.x = 0
|
||||||
|
currentDraggedItem.y = 0
|
||||||
allowSwipe.value = true // Re-enable Swiper after drag
|
allowSwipe.value = true // Re-enable Swiper after drag
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,15 +482,18 @@ const handleDrop = async (event: DragEvent, workspaceId: string) => {
|
|||||||
const desktopRect = (
|
const desktopRect = (
|
||||||
event.currentTarget as HTMLElement
|
event.currentTarget as HTMLElement
|
||||||
).getBoundingClientRect()
|
).getBoundingClientRect()
|
||||||
const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
const rawX = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
||||||
const y = Math.max(0, event.clientY - desktopRect.top - 32)
|
const rawY = Math.max(0, event.clientY - desktopRect.top - 32)
|
||||||
|
|
||||||
|
// Snap to grid
|
||||||
|
const snapped = desktopStore.snapToGrid(rawX, rawY)
|
||||||
|
|
||||||
// Create desktop icon on the specific workspace
|
// Create desktop icon on the specific workspace
|
||||||
await desktopStore.addDesktopItemAsync(
|
await desktopStore.addDesktopItemAsync(
|
||||||
item.type as DesktopItemType,
|
item.type as DesktopItemType,
|
||||||
item.id,
|
item.id,
|
||||||
x,
|
snapped.x,
|
||||||
y,
|
snapped.y,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
65
src/components/haex/icon.vue
Normal file
65
src/components/haex/icon.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="inline-flex">
|
||||||
|
<UTooltip :text="tooltip">
|
||||||
|
<!-- Bundled Icon (iconify) -->
|
||||||
|
<UIcon
|
||||||
|
v-if="isBundledIcon"
|
||||||
|
:name="name"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- External Image (Extension icon) -->
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="imageUrl"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string
|
||||||
|
tooltip?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Check if it's a bundled icon (no file extension)
|
||||||
|
const isBundledIcon = computed(() => {
|
||||||
|
return !props.name.match(/\.(png|jpg|jpeg|svg|gif|webp|ico)$/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert file path to Tauri URL for images
|
||||||
|
const imageUrl = ref('')
|
||||||
|
const showFallback = ref(false)
|
||||||
|
|
||||||
|
// Default fallback icon
|
||||||
|
const FALLBACK_ICON = 'i-heroicons-puzzle-piece-solid'
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!isBundledIcon.value && !showFallback.value) {
|
||||||
|
// Convert local file path to Tauri asset URL
|
||||||
|
imageUrl.value = convertFileSrc(props.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
console.warn(`Failed to load icon: ${props.name}`)
|
||||||
|
showFallback.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fallback icon if image failed to load
|
||||||
|
const name = computed(() => {
|
||||||
|
if (showFallback.value) {
|
||||||
|
return FALLBACK_ICON
|
||||||
|
}
|
||||||
|
return props.name
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -47,6 +47,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Grid Settings -->
|
||||||
|
<div
|
||||||
|
class="col-span-2 mt-4 border-t border-gray-200 dark:border-gray-700 pt-4"
|
||||||
|
>
|
||||||
|
<h3 class="text-lg font-semibold mb-4">{{ t('desktopGrid.title') }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2">{{ t('desktopGrid.iconSize.label') }}</div>
|
||||||
|
<div>
|
||||||
|
<USelect
|
||||||
|
v-model="iconSizePreset"
|
||||||
|
:items="iconSizePresetOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="h-full" />
|
<div class="h-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,6 +78,7 @@ import {
|
|||||||
remove,
|
remove,
|
||||||
} from '@tauri-apps/plugin-fs'
|
} from '@tauri-apps/plugin-fs'
|
||||||
import { appLocalDataDir } from '@tauri-apps/api/path'
|
import { appLocalDataDir } from '@tauri-apps/api/path'
|
||||||
|
import { DesktopIconSizePreset } from '~/stores/vault/settings'
|
||||||
|
|
||||||
const { t, setLocale } = useI18n()
|
const { t, setLocale } = useI18n()
|
||||||
|
|
||||||
@ -104,8 +120,40 @@ const workspaceStore = useWorkspaceStore()
|
|||||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||||
const { updateWorkspaceBackgroundAsync } = workspaceStore
|
const { updateWorkspaceBackgroundAsync } = workspaceStore
|
||||||
|
|
||||||
|
const desktopStore = useDesktopStore()
|
||||||
|
const { iconSizePreset } = storeToRefs(desktopStore)
|
||||||
|
const { syncDesktopIconSizeAsync, updateDesktopIconSizeAsync } = desktopStore
|
||||||
|
|
||||||
|
// Icon size preset options
|
||||||
|
const iconSizePresetOptions = [
|
||||||
|
{
|
||||||
|
label: t('desktopGrid.iconSize.presets.small'),
|
||||||
|
value: DesktopIconSizePreset.small,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('desktopGrid.iconSize.presets.medium'),
|
||||||
|
value: DesktopIconSizePreset.medium,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('desktopGrid.iconSize.presets.large'),
|
||||||
|
value: DesktopIconSizePreset.large,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('desktopGrid.iconSize.presets.extraLarge'),
|
||||||
|
value: DesktopIconSizePreset.extraLarge,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Watch for icon size preset changes and update DB
|
||||||
|
watch(iconSizePreset, async (newPreset) => {
|
||||||
|
if (newPreset) {
|
||||||
|
await updateDesktopIconSizeAsync(newPreset)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await readDeviceNameAsync()
|
await readDeviceNameAsync()
|
||||||
|
await syncDesktopIconSizeAsync()
|
||||||
})
|
})
|
||||||
|
|
||||||
const onUpdateDeviceNameAsync = async () => {
|
const onUpdateDeviceNameAsync = async () => {
|
||||||
@ -295,6 +343,22 @@ de:
|
|||||||
label: Hintergrund entfernen
|
label: Hintergrund entfernen
|
||||||
success: Hintergrund erfolgreich entfernt
|
success: Hintergrund erfolgreich entfernt
|
||||||
error: Fehler beim Entfernen des Hintergrunds
|
error: Fehler beim Entfernen des Hintergrunds
|
||||||
|
desktopGrid:
|
||||||
|
title: Desktop-Raster
|
||||||
|
columns:
|
||||||
|
label: Spalten
|
||||||
|
unit: Spalten
|
||||||
|
rows:
|
||||||
|
label: Zeilen
|
||||||
|
unit: Zeilen
|
||||||
|
iconSize:
|
||||||
|
label: Icon-Größe
|
||||||
|
presets:
|
||||||
|
small: Klein
|
||||||
|
medium: Mittel
|
||||||
|
large: Groß
|
||||||
|
extraLarge: Sehr groß
|
||||||
|
unit: px
|
||||||
en:
|
en:
|
||||||
language: Language
|
language: Language
|
||||||
design: Design
|
design: Design
|
||||||
@ -322,4 +386,20 @@ en:
|
|||||||
label: Remove Background
|
label: Remove Background
|
||||||
success: Background successfully removed
|
success: Background successfully removed
|
||||||
error: Error removing background
|
error: Error removing background
|
||||||
|
desktopGrid:
|
||||||
|
title: Desktop Grid
|
||||||
|
columns:
|
||||||
|
label: Columns
|
||||||
|
unit: columns
|
||||||
|
rows:
|
||||||
|
label: Rows
|
||||||
|
unit: rows
|
||||||
|
iconSize:
|
||||||
|
label: Icon Size
|
||||||
|
presets:
|
||||||
|
small: Small
|
||||||
|
medium: Medium
|
||||||
|
large: Large
|
||||||
|
extraLarge: Extra Large
|
||||||
|
unit: px
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -26,10 +26,10 @@
|
|||||||
>
|
>
|
||||||
<!-- Left: Icon -->
|
<!-- Left: Icon -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<img
|
<HaexIcon
|
||||||
v-if="icon"
|
v-if="icon"
|
||||||
:src="icon"
|
:name="icon"
|
||||||
:alt="title"
|
:tooltip="title"
|
||||||
class="w-5 h-5 object-contain shrink-0"
|
class="w-5 h-5 object-contain shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,17 +25,70 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Window Icons Preview -->
|
||||||
|
<div
|
||||||
|
v-if="workspaceWindows.length > 0"
|
||||||
|
class="flex flex-wrap gap-2 items-center"
|
||||||
|
>
|
||||||
|
<!-- Show first 8 window icons -->
|
||||||
|
<HaexIcon
|
||||||
|
v-for="window in visibleWindows"
|
||||||
|
:key="window.id"
|
||||||
|
:name="window.icon || 'i-heroicons-window'"
|
||||||
|
:tooltip="window.title"
|
||||||
|
class="size-6 opacity-70"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Show remaining count badge if more than 8 windows -->
|
||||||
|
<UBadge
|
||||||
|
v-if="remainingCount > 0"
|
||||||
|
color="neutral"
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
+{{ remainingCount }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state when no windows -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-sm text-gray-400 dark:text-gray-600 italic"
|
||||||
|
>
|
||||||
|
{{ t('noWindows') }}
|
||||||
|
</div>
|
||||||
</UCard>
|
</UCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{ workspace: IWorkspace }>()
|
const props = defineProps<{ workspace: IWorkspace }>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const windowManager = useWindowManagerStore()
|
const windowManager = useWindowManagerStore()
|
||||||
|
|
||||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||||
|
|
||||||
|
// Get all windows for this workspace
|
||||||
|
const workspaceWindows = computed(() => {
|
||||||
|
return windowManager.windows.filter(
|
||||||
|
(window) => window.workspaceId === props.workspace.id,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Limit to 8 visible icons
|
||||||
|
const MAX_VISIBLE_ICONS = 8
|
||||||
|
const visibleWindows = computed(() => {
|
||||||
|
return workspaceWindows.value.slice(0, MAX_VISIBLE_ICONS)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Count remaining windows
|
||||||
|
const remainingCount = computed(() => {
|
||||||
|
const remaining = workspaceWindows.value.length - MAX_VISIBLE_ICONS
|
||||||
|
return remaining > 0 ? remaining : 0
|
||||||
|
})
|
||||||
|
|
||||||
const cardEl = useTemplateRef('cardEl')
|
const cardEl = useTemplateRef('cardEl')
|
||||||
const isDragOver = ref(false)
|
const isDragOver = ref(false)
|
||||||
|
|
||||||
@ -96,3 +149,10 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
noWindows: Keine Fenster geöffnet
|
||||||
|
en:
|
||||||
|
noWindows: No windows open
|
||||||
|
</i18n>
|
||||||
|
|||||||
54
src/components/haex/workspace/drawer.vue
Normal file
54
src/components/haex/workspace/drawer.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<UDrawer
|
||||||
|
v-model:open="isOverviewMode"
|
||||||
|
direction="left"
|
||||||
|
:overlay="false"
|
||||||
|
:modal="false"
|
||||||
|
title="Workspaces"
|
||||||
|
description="Workspaces"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="py-8 pl-8 pr-4 h-full overflow-y-auto">
|
||||||
|
<!-- Workspace Cards -->
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<HaexWorkspaceCard
|
||||||
|
v-for="workspace in workspaces"
|
||||||
|
:key="workspace.id"
|
||||||
|
:workspace
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add New Workspace Button -->
|
||||||
|
<UButton
|
||||||
|
block
|
||||||
|
variant="outline"
|
||||||
|
class="mt-6"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
:label="t('add')"
|
||||||
|
@click="handleAddWorkspaceAsync"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const { workspaces, isOverviewMode } = storeToRefs(workspaceStore)
|
||||||
|
|
||||||
|
const handleAddWorkspaceAsync = async () => {
|
||||||
|
const workspace = await workspaceStore.addWorkspaceAsync()
|
||||||
|
nextTick(() => {
|
||||||
|
workspaceStore.slideToWorkspace(workspace?.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
add: Workspace hinzufügen
|
||||||
|
en:
|
||||||
|
add: Add Workspace
|
||||||
|
</i18n>
|
||||||
@ -24,17 +24,42 @@ export const withCrdtColumns = <
|
|||||||
haexTimestamp: text(crdtColumnNames.haexTimestamp),
|
haexTimestamp: text(crdtColumnNames.haexTimestamp),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const haexDevices = sqliteTable(
|
||||||
|
tableNames.haex.devices.name,
|
||||||
|
withCrdtColumns({
|
||||||
|
id: text(tableNames.haex.devices.columns.id)
|
||||||
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
|
.primaryKey(),
|
||||||
|
deviceId: text(tableNames.haex.devices.columns.deviceId)
|
||||||
|
.notNull()
|
||||||
|
.unique(),
|
||||||
|
name: text(tableNames.haex.devices.columns.name).notNull(),
|
||||||
|
createdAt: text(tableNames.haex.devices.columns.createdAt).default(
|
||||||
|
sql`(CURRENT_TIMESTAMP)`,
|
||||||
|
),
|
||||||
|
updatedAt: integer(tableNames.haex.devices.columns.updatedAt, {
|
||||||
|
mode: 'timestamp',
|
||||||
|
}).$onUpdate(() => new Date()),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
export type InsertHaexDevices = typeof haexDevices.$inferInsert
|
||||||
|
export type SelectHaexDevices = typeof haexDevices.$inferSelect
|
||||||
|
|
||||||
export const haexSettings = sqliteTable(
|
export const haexSettings = sqliteTable(
|
||||||
tableNames.haex.settings.name,
|
tableNames.haex.settings.name,
|
||||||
withCrdtColumns({
|
withCrdtColumns({
|
||||||
id: text()
|
id: text(tableNames.haex.settings.columns.id)
|
||||||
.$defaultFn(() => crypto.randomUUID())
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.primaryKey(),
|
.primaryKey(),
|
||||||
key: text(),
|
deviceId: text(tableNames.haex.settings.columns.deviceId).references(
|
||||||
type: text(),
|
(): AnySQLiteColumn => haexDevices.id,
|
||||||
value: text(),
|
{ onDelete: 'cascade' },
|
||||||
|
),
|
||||||
|
key: text(tableNames.haex.settings.columns.key),
|
||||||
|
type: text(tableNames.haex.settings.columns.type),
|
||||||
|
value: text(tableNames.haex.settings.columns.value),
|
||||||
}),
|
}),
|
||||||
(table) => [unique().on(table.key, table.type, table.value)],
|
(table) => [unique().on(table.deviceId, table.key, table.type)],
|
||||||
)
|
)
|
||||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||||
export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
"name": "haex_settings",
|
"name": "haex_settings",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": "id",
|
"id": "id",
|
||||||
|
"deviceId": "device_id",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"type": "type",
|
"type": "type",
|
||||||
"value": "value",
|
"value": "value",
|
||||||
@ -89,6 +90,18 @@
|
|||||||
"haexTimestamp": "haex_timestamp"
|
"haexTimestamp": "haex_timestamp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"devices": {
|
||||||
|
"name": "haex_devices",
|
||||||
|
"columns": {
|
||||||
|
"id": "id",
|
||||||
|
"deviceId": "device_id",
|
||||||
|
"name": "name",
|
||||||
|
"createdAt": "created_at",
|
||||||
|
"updatedAt": "updated_at",
|
||||||
|
|
||||||
|
"haexTimestamp": "haex_timestamp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"crdt": {
|
"crdt": {
|
||||||
"logs": {
|
"logs": {
|
||||||
|
|||||||
@ -59,47 +59,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Workspace Drawer -->
|
<!-- Workspace Drawer -->
|
||||||
<UDrawer
|
<HaexWorkspaceDrawer />
|
||||||
v-model:open="isOverviewMode"
|
|
||||||
direction="left"
|
|
||||||
:dismissible="false"
|
|
||||||
:overlay="false"
|
|
||||||
:modal="false"
|
|
||||||
title="Workspaces"
|
|
||||||
description="Workspaces"
|
|
||||||
>
|
|
||||||
<template #content>
|
|
||||||
<div class="p-6 h-full overflow-y-auto">
|
|
||||||
<UButton
|
|
||||||
block
|
|
||||||
trailing-icon="mdi-close"
|
|
||||||
class="text-2xl font-bold ext-gray-900 dark:text-white mb-4"
|
|
||||||
@click="isOverviewMode = false"
|
|
||||||
>
|
|
||||||
Workspaces
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<!-- Workspace Cards -->
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<HaexWorkspaceCard
|
|
||||||
v-for="workspace in workspaces"
|
|
||||||
:key="workspace.id"
|
|
||||||
:workspace
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add New Workspace Button -->
|
|
||||||
<UButton
|
|
||||||
block
|
|
||||||
variant="outline"
|
|
||||||
class="mt-6"
|
|
||||||
@click="handleAddWorkspace"
|
|
||||||
icon="i-heroicons-plus"
|
|
||||||
:label="t('workspaces.add')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UDrawer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -116,15 +76,7 @@ const { showWindowOverview, openWindowsCount } = storeToRefs(
|
|||||||
useWindowManagerStore(),
|
useWindowManagerStore(),
|
||||||
)
|
)
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
const { isOverviewMode } = storeToRefs(useWorkspaceStore())
|
||||||
const { workspaces, isOverviewMode } = storeToRefs(workspaceStore)
|
|
||||||
|
|
||||||
const handleAddWorkspace = async () => {
|
|
||||||
const workspace = await workspaceStore.addWorkspaceAsync()
|
|
||||||
nextTick(() => {
|
|
||||||
workspaceStore.slideToWorkspace(workspace?.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measure header height and store it in UI store
|
// Measure header height and store it in UI store
|
||||||
const headerEl = useTemplateRef('headerEl')
|
const headerEl = useTemplateRef('headerEl')
|
||||||
@ -140,15 +92,11 @@ watch(height, (newHeight) => {
|
|||||||
de:
|
de:
|
||||||
search:
|
search:
|
||||||
label: Suche
|
label: Suche
|
||||||
|
|
||||||
workspaces:
|
workspaces:
|
||||||
label: Workspaces
|
label: Workspaces
|
||||||
add: Workspace hinzufügen
|
|
||||||
en:
|
en:
|
||||||
search:
|
search:
|
||||||
label: Search
|
label: Search
|
||||||
|
|
||||||
workspaces:
|
workspaces:
|
||||||
label: Workspaces
|
label: Workspaces
|
||||||
add: Add Workspace
|
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -53,6 +53,7 @@ const { addDeviceNameAsync } = useDeviceStore()
|
|||||||
const { deviceId } = storeToRefs(useDeviceStore())
|
const { deviceId } = storeToRefs(useDeviceStore())
|
||||||
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
||||||
useVaultSettingsStore()
|
useVaultSettingsStore()
|
||||||
|
const { syncDesktopIconSizeAsync } = useDesktopStore()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@ -62,6 +63,7 @@ onMounted(async () => {
|
|||||||
syncLocaleAsync(),
|
syncLocaleAsync(),
|
||||||
syncThemeAsync(),
|
syncThemeAsync(),
|
||||||
syncVaultNameAsync(),
|
syncVaultNameAsync(),
|
||||||
|
syncDesktopIconSizeAsync(),
|
||||||
loadExtensionsAsync(),
|
loadExtensionsAsync(),
|
||||||
readNotificationsAsync(),
|
readNotificationsAsync(),
|
||||||
])
|
])
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { haexDesktopItems } from '~/database/schemas'
|
import { haexDesktopItems, haexDevices } from '~/database/schemas'
|
||||||
import type {
|
import type {
|
||||||
InsertHaexDesktopItems,
|
InsertHaexDesktopItems,
|
||||||
SelectHaexDesktopItems,
|
SelectHaexDesktopItems,
|
||||||
} from '~/database/schemas'
|
} from '~/database/schemas'
|
||||||
|
import {
|
||||||
|
DesktopIconSizePreset,
|
||||||
|
iconSizePresetValues,
|
||||||
|
} from '~/stores/vault/settings'
|
||||||
import de from './de.json'
|
import de from './de.json'
|
||||||
import en from './en.json'
|
import en from './en.json'
|
||||||
|
|
||||||
@ -20,6 +24,10 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
|||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||||
const { $i18n } = useNuxtApp()
|
const { $i18n } = useNuxtApp()
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
const { isSmallScreen } = storeToRefs(uiStore)
|
||||||
|
const deviceStore = useDeviceStore()
|
||||||
|
const settingsStore = useVaultSettingsStore()
|
||||||
|
|
||||||
$i18n.setLocaleMessage('de', {
|
$i18n.setLocaleMessage('de', {
|
||||||
desktop: de,
|
desktop: de,
|
||||||
@ -29,6 +37,86 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
|||||||
const desktopItems = ref<IDesktopItem[]>([])
|
const desktopItems = ref<IDesktopItem[]>([])
|
||||||
const selectedItemIds = ref<Set<string>>(new Set())
|
const selectedItemIds = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// Desktop Grid Settings (stored in DB per device)
|
||||||
|
const iconSizePreset = ref<DesktopIconSizePreset>(DesktopIconSizePreset.medium)
|
||||||
|
|
||||||
|
// Get device internal ID from DB
|
||||||
|
const getDeviceInternalIdAsync = async () => {
|
||||||
|
if (!deviceStore.deviceId || !currentVault.value?.drizzle) return undefined
|
||||||
|
|
||||||
|
const device = await currentVault.value.drizzle.query.haexDevices.findFirst({
|
||||||
|
where: eq(haexDevices.deviceId, deviceStore.deviceId),
|
||||||
|
})
|
||||||
|
|
||||||
|
return device?.id ? device.id : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync icon size from DB
|
||||||
|
const syncDesktopIconSizeAsync = async () => {
|
||||||
|
const deviceInternalId = await getDeviceInternalIdAsync()
|
||||||
|
if (!deviceInternalId) return
|
||||||
|
|
||||||
|
const preset = await settingsStore.syncDesktopIconSizeAsync(deviceInternalId)
|
||||||
|
iconSizePreset.value = preset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update icon size in DB
|
||||||
|
const updateDesktopIconSizeAsync = async (preset: DesktopIconSizePreset) => {
|
||||||
|
const deviceInternalId = await getDeviceInternalIdAsync()
|
||||||
|
if (!deviceInternalId) return
|
||||||
|
|
||||||
|
await settingsStore.updateDesktopIconSizeAsync(deviceInternalId, preset)
|
||||||
|
iconSizePreset.value = preset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactive grid settings based on screen size
|
||||||
|
const effectiveGridColumns = computed(() => {
|
||||||
|
return isSmallScreen.value ? 4 : 8
|
||||||
|
})
|
||||||
|
|
||||||
|
const effectiveGridRows = computed(() => {
|
||||||
|
return isSmallScreen.value ? 5 : 6
|
||||||
|
})
|
||||||
|
|
||||||
|
const effectiveIconSize = computed(() => {
|
||||||
|
return iconSizePresetValues[iconSizePreset.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate grid cell size based on icon size
|
||||||
|
const gridCellSize = computed(() => {
|
||||||
|
// Add padding around icon (20px extra for spacing)
|
||||||
|
return effectiveIconSize.value + 20
|
||||||
|
})
|
||||||
|
|
||||||
|
// Snap position to grid (centers icon in cell)
|
||||||
|
// iconWidth and iconHeight are optional - if provided, they're used for centering
|
||||||
|
const snapToGrid = (x: number, y: number, iconWidth?: number, iconHeight?: number) => {
|
||||||
|
const cellSize = gridCellSize.value
|
||||||
|
|
||||||
|
// Calculate which grid cell the position falls into
|
||||||
|
const col = Math.floor(x / cellSize)
|
||||||
|
const row = Math.floor(y / cellSize)
|
||||||
|
|
||||||
|
// Use provided dimensions or fall back to cell size
|
||||||
|
const actualIconWidth = iconWidth || cellSize
|
||||||
|
const actualIconHeight = iconHeight || cellSize
|
||||||
|
|
||||||
|
// Center the icon in the cell(s) it occupies
|
||||||
|
const cellsWide = Math.max(1, Math.ceil(actualIconWidth / cellSize))
|
||||||
|
const cellsHigh = Math.max(1, Math.ceil(actualIconHeight / cellSize))
|
||||||
|
|
||||||
|
const totalWidth = cellsWide * cellSize
|
||||||
|
const totalHeight = cellsHigh * cellSize
|
||||||
|
|
||||||
|
const paddingX = (totalWidth - actualIconWidth) / 2
|
||||||
|
const paddingY = (totalHeight - actualIconHeight) / 2
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: col * cellSize + paddingX,
|
||||||
|
y: row * cellSize + paddingY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadDesktopItemsAsync = async () => {
|
const loadDesktopItemsAsync = async () => {
|
||||||
if (!currentVault.value?.drizzle) {
|
if (!currentVault.value?.drizzle) {
|
||||||
console.error('Kein Vault geöffnet')
|
console.error('Kein Vault geöffnet')
|
||||||
@ -347,5 +435,14 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
|||||||
toggleSelection,
|
toggleSelection,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
isItemSelected,
|
isItemSelected,
|
||||||
|
// Grid settings
|
||||||
|
iconSizePreset,
|
||||||
|
syncDesktopIconSizeAsync,
|
||||||
|
updateDesktopIconSizeAsync,
|
||||||
|
effectiveGridColumns,
|
||||||
|
effectiveGridRows,
|
||||||
|
effectiveIconSize,
|
||||||
|
gridCellSize,
|
||||||
|
snapToGrid,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -45,7 +45,9 @@ export const useDeviceStore = defineStore('vaultDeviceStore', () => {
|
|||||||
|
|
||||||
const isKnownDeviceAsync = async () => {
|
const isKnownDeviceAsync = async () => {
|
||||||
const { readDeviceNameAsync } = useVaultSettingsStore()
|
const { readDeviceNameAsync } = useVaultSettingsStore()
|
||||||
return !!(await readDeviceNameAsync(deviceId.value))
|
const device = await readDeviceNameAsync(deviceId.value)
|
||||||
|
console.log('device', device)
|
||||||
|
return !!device
|
||||||
}
|
}
|
||||||
|
|
||||||
const readDeviceNameAsync = async (id?: string) => {
|
const readDeviceNameAsync = async (id?: string) => {
|
||||||
@ -54,7 +56,8 @@ export const useDeviceStore = defineStore('vaultDeviceStore', () => {
|
|||||||
|
|
||||||
if (!_id) return
|
if (!_id) return
|
||||||
|
|
||||||
deviceName.value = (await readDeviceNameAsync(_id))?.value ?? ''
|
const device = await readDeviceNameAsync(_id)
|
||||||
|
deviceName.value = device?.name ?? ''
|
||||||
|
|
||||||
return deviceName.value
|
return deviceName.value
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,14 +4,29 @@ import * as schema from '~/database/schemas/haex'
|
|||||||
import type { Locale } from 'vue-i18n'
|
import type { Locale } from 'vue-i18n'
|
||||||
|
|
||||||
export enum VaultSettingsTypeEnum {
|
export enum VaultSettingsTypeEnum {
|
||||||
deviceName = 'deviceName',
|
|
||||||
settings = 'settings',
|
settings = 'settings',
|
||||||
|
system = 'system',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VaultSettingsKeyEnum {
|
export enum VaultSettingsKeyEnum {
|
||||||
locale = 'locale',
|
locale = 'locale',
|
||||||
theme = 'theme',
|
theme = 'theme',
|
||||||
vaultName = 'vaultName',
|
vaultName = 'vaultName',
|
||||||
|
desktopIconSize = 'desktopIconSize',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DesktopIconSizePreset {
|
||||||
|
small = 'small',
|
||||||
|
medium = 'medium',
|
||||||
|
large = 'large',
|
||||||
|
extraLarge = 'extra-large',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const iconSizePresetValues: Record<DesktopIconSizePreset, number> = {
|
||||||
|
[DesktopIconSizePreset.small]: 60,
|
||||||
|
[DesktopIconSizePreset.medium]: 80,
|
||||||
|
[DesktopIconSizePreset.large]: 120,
|
||||||
|
[DesktopIconSizePreset.extraLarge]: 160,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vaultDeviceNameSchema = z.string().min(3).max(255)
|
export const vaultDeviceNameSchema = z.string().min(3).max(255)
|
||||||
@ -118,20 +133,22 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
|||||||
.where(eq(schema.haexSettings.key, 'vaultName'))
|
.where(eq(schema.haexSettings.key, 'vaultName'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const readDeviceNameAsync = async (id?: string) => {
|
const readDeviceNameAsync = async (deviceId?: string) => {
|
||||||
const { currentVault } = useVaultStore()
|
const { currentVault } = useVaultStore()
|
||||||
|
|
||||||
if (!id) return undefined
|
if (!deviceId) return undefined
|
||||||
|
|
||||||
const deviceName =
|
const device =
|
||||||
await currentVault?.drizzle?.query.haexSettings.findFirst({
|
await currentVault?.drizzle?.query.haexDevices.findFirst({
|
||||||
where: and(
|
where: eq(schema.haexDevices.deviceId, deviceId),
|
||||||
eq(schema.haexSettings.type, VaultSettingsTypeEnum.deviceName),
|
|
||||||
eq(schema.haexSettings.key, id),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return deviceName?.id ? deviceName : undefined
|
// Workaround für Drizzle Bug: findFirst gibt manchmal Objekt mit undefined Werten zurück
|
||||||
|
// https://github.com/drizzle-team/drizzle-orm/issues/3872
|
||||||
|
// Prüfe ob das Device wirklich existiert (id muss gesetzt sein, da NOT NULL)
|
||||||
|
if (!device?.id) return undefined
|
||||||
|
|
||||||
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
const addDeviceNameAsync = async ({
|
const addDeviceNameAsync = async ({
|
||||||
@ -149,10 +166,9 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentVault?.drizzle?.insert(schema.haexSettings).values({
|
return currentVault?.drizzle?.insert(schema.haexDevices).values({
|
||||||
type: VaultSettingsTypeEnum.deviceName,
|
deviceId,
|
||||||
key: deviceId,
|
name: deviceName,
|
||||||
value: deviceName,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,14 +185,49 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
|||||||
if (!isNameOk.success) return
|
if (!isNameOk.success) return
|
||||||
|
|
||||||
return currentVault?.drizzle
|
return currentVault?.drizzle
|
||||||
?.update(schema.haexSettings)
|
?.update(schema.haexDevices)
|
||||||
.set({
|
.set({
|
||||||
value: deviceName,
|
name: deviceName,
|
||||||
})
|
})
|
||||||
|
.where(eq(schema.haexDevices.deviceId, deviceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncDesktopIconSizeAsync = async (deviceInternalId: string) => {
|
||||||
|
const iconSizeRow =
|
||||||
|
await currentVault.value?.drizzle.query.haexSettings.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(schema.haexSettings.deviceId, deviceInternalId),
|
||||||
|
eq(schema.haexSettings.key, VaultSettingsKeyEnum.desktopIconSize),
|
||||||
|
eq(schema.haexSettings.type, VaultSettingsTypeEnum.system),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!iconSizeRow?.id) {
|
||||||
|
// Kein Eintrag vorhanden, erstelle einen mit Default (medium)
|
||||||
|
await currentVault.value?.drizzle.insert(schema.haexSettings).values({
|
||||||
|
deviceId: deviceInternalId,
|
||||||
|
key: VaultSettingsKeyEnum.desktopIconSize,
|
||||||
|
type: VaultSettingsTypeEnum.system,
|
||||||
|
value: DesktopIconSizePreset.medium,
|
||||||
|
})
|
||||||
|
return DesktopIconSizePreset.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconSizeRow.value as DesktopIconSizePreset
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDesktopIconSizeAsync = async (
|
||||||
|
deviceInternalId: string,
|
||||||
|
preset: DesktopIconSizePreset,
|
||||||
|
) => {
|
||||||
|
return await currentVault.value?.drizzle
|
||||||
|
.update(schema.haexSettings)
|
||||||
|
.set({ value: preset })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(schema.haexSettings.key, deviceId),
|
eq(schema.haexSettings.deviceId, deviceInternalId),
|
||||||
eq(schema.haexSettings.type, VaultSettingsTypeEnum.deviceName),
|
eq(schema.haexSettings.key, VaultSettingsKeyEnum.desktopIconSize),
|
||||||
|
eq(schema.haexSettings.type, VaultSettingsTypeEnum.system),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -191,5 +242,7 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
|||||||
updateLocaleAsync,
|
updateLocaleAsync,
|
||||||
updateThemeAsync,
|
updateThemeAsync,
|
||||||
updateVaultNameAsync,
|
updateVaultNameAsync,
|
||||||
|
syncDesktopIconSizeAsync,
|
||||||
|
updateDesktopIconSizeAsync,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user