Add sync backend infrastructure and improve grid snapping

- Add haexSyncBackends table with CRDT support for multi-backend sync
- Implement useSyncBackendsStore for managing sync server configurations
- Fix desktop icon grid snapping for all icon sizes (small to extra-large)
- Add Supabase client dependency for future sync implementation
- Generate database migration for sync_backends table
This commit is contained in:
2025-11-05 01:08:09 +01:00
parent b465c117b0
commit be7dff72dd
9 changed files with 1121 additions and 17 deletions

View File

@ -24,6 +24,7 @@
"@nuxt/ui": "4.1.0",
"@nuxtjs/i18n": "10.0.6",
"@pinia/nuxt": "^0.11.2",
"@supabase/supabase-js": "^2.79.0",
"@tailwindcss/vite": "^4.1.16",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-dialog": "^2.4.2",

69
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
'@pinia/nuxt':
specifier: ^0.11.2
version: 0.11.2(magicast@0.5.1)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))
'@supabase/supabase-js':
specifier: ^2.79.0
version: 2.79.0
'@tailwindcss/vite':
specifier: ^4.1.16
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
@ -1894,6 +1897,30 @@ packages:
peerDependencies:
eslint: '>=9.0.0'
'@supabase/auth-js@2.79.0':
resolution: {integrity: sha512-p2GKvdbF9d/6C+dtS6iNcSicPr6eUfkvovD60HWlWsD+oOjC483DzFWrzGjNpBwnswhfMRP8Qn3rYA0VWaOfjw==}
engines: {node: '>=20.0.0'}
'@supabase/functions-js@2.79.0':
resolution: {integrity: sha512-WaiU6b+Z+ZfJOjFhpMKdajt42weiFUrA6TVW5oGd6WfPGajFiKZJJIAvuK0g7KDKaYowtQrOo5+Ais+PcuZ1qA==}
engines: {node: '>=20.0.0'}
'@supabase/postgrest-js@2.79.0':
resolution: {integrity: sha512-2i8EFm3/49ecjt6dk/TGVROBbtOmhryiC4NL3u0FBIrm2hqj+FvbELv1jjM6r+a6abnh+uzIV/bFsWHAa/k3/w==}
engines: {node: '>=20.0.0'}
'@supabase/realtime-js@2.79.0':
resolution: {integrity: sha512-foaZujNBycAqLizUcuLyyFyDitfPnEMVO4CiKXNwaMCDVMoVX4QR6n4gpJLUC5BGzc20Mte6vSJLbk4MN90Prw==}
engines: {node: '>=20.0.0'}
'@supabase/storage-js@2.79.0':
resolution: {integrity: sha512-PLSeKX1/BZhGWCT972w4TvVOCcw/xh4TsowtUBiZvPx4OdHT7dB1q0DXKwVUfKbWk5UUC+6XAq4ZU/ZCtdgn6w==}
engines: {node: '>=20.0.0'}
'@supabase/supabase-js@2.79.0':
resolution: {integrity: sha512-x9ndEaBSwoRnFOOZGhh2CeV69Uz4B/EOSGCbKysDhTiYakiCAdDXaNuLPluviKU/Aot+F7BglXZDZ0YJ3GpGrw==}
engines: {node: '>=20.0.0'}
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
@ -2116,6 +2143,9 @@ packages:
resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==}
deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed.
'@types/phoenix@1.6.6':
resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@ -7556,6 +7586,43 @@ snapshots:
estraverse: 5.3.0
picomatch: 4.0.3
'@supabase/auth-js@2.79.0':
dependencies:
tslib: 2.8.1
'@supabase/functions-js@2.79.0':
dependencies:
tslib: 2.8.1
'@supabase/postgrest-js@2.79.0':
dependencies:
tslib: 2.8.1
'@supabase/realtime-js@2.79.0':
dependencies:
'@types/phoenix': 1.6.6
'@types/ws': 8.18.1
tslib: 2.8.1
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@supabase/storage-js@2.79.0':
dependencies:
tslib: 2.8.1
'@supabase/supabase-js@2.79.0':
dependencies:
'@supabase/auth-js': 2.79.0
'@supabase/functions-js': 2.79.0
'@supabase/postgrest-js': 2.79.0
'@supabase/realtime-js': 2.79.0
'@supabase/storage-js': 2.79.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
@ -7740,6 +7807,8 @@ snapshots:
dependencies:
parse-path: 7.1.0
'@types/phoenix@1.6.6': {}
'@types/resolve@1.20.2': {}
'@types/web-bluetooth@0.0.20': {}

View File

@ -0,0 +1,10 @@
CREATE TABLE `haex_sync_backends` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`server_url` text NOT NULL,
`enabled` integer DEFAULT true NOT NULL,
`priority` integer DEFAULT 0 NOT NULL,
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
`updated_at` integer,
`haex_timestamp` text
);

View File

@ -0,0 +1,843 @@
{
"version": "6",
"dialect": "sqlite",
"id": "bf82259e-9264-44e7-a60f-8cc14a1f22e2",
"prevId": "3aedf10c-2266-40f4-8549-0ff8b0588853",
"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_sync_backends": {
"name": "haex_sync_backends",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"server_url": {
"name": "server_url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"priority": {
"name": "priority",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"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": {},
"foreignKeys": {},
"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": {}
}
}

View File

@ -22,6 +22,13 @@
"when": 1762263814375,
"tag": "0002_loose_quasimodo",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1762300795436,
"tag": "0003_luxuriant_deathstrike",
"breakpoints": true
}
]
}

View File

@ -205,3 +205,30 @@ export const haexDesktopItems = sqliteTable(
)
export type InsertHaexDesktopItems = typeof haexDesktopItems.$inferInsert
export type SelectHaexDesktopItems = typeof haexDesktopItems.$inferSelect
export const haexSyncBackends = sqliteTable(
tableNames.haex.sync_backends.name,
withCrdtColumns({
id: text(tableNames.haex.sync_backends.columns.id)
.$defaultFn(() => crypto.randomUUID())
.primaryKey(),
name: text(tableNames.haex.sync_backends.columns.name).notNull(),
serverUrl: text(tableNames.haex.sync_backends.columns.serverUrl).notNull(),
enabled: integer(tableNames.haex.sync_backends.columns.enabled, {
mode: 'boolean',
})
.default(true)
.notNull(),
priority: integer(tableNames.haex.sync_backends.columns.priority)
.default(0)
.notNull(),
createdAt: text(tableNames.haex.sync_backends.columns.createdAt).default(
sql`(CURRENT_TIMESTAMP)`,
),
updatedAt: integer(tableNames.haex.sync_backends.columns.updatedAt, {
mode: 'timestamp',
}).$onUpdate(() => new Date()),
}),
)
export type InsertHaexSyncBackends = typeof haexSyncBackends.$inferInsert
export type SelectHaexSyncBackends = typeof haexSyncBackends.$inferSelect

View File

@ -102,6 +102,20 @@
"haexTimestamp": "haex_timestamp"
}
},
"sync_backends": {
"name": "haex_sync_backends",
"columns": {
"id": "id",
"name": "name",
"serverUrl": "server_url",
"enabled": "enabled",
"priority": "priority",
"createdAt": "created_at",
"updatedAt": "updated_at",
"haexTimestamp": "haex_timestamp"
}
},
"crdt": {
"logs": {

View File

@ -91,31 +91,34 @@ export const useDesktopStore = defineStore('desktopStore', () => {
iconHeight?: number,
) => {
const cellSize = gridCellSize.value
const halfCell = cellSize / 2
// Adjust y for grid offset
const adjustedY = Math.max(0, y + iconPadding)
// Use provided dimensions or fall back to the effective icon size (not cell size!)
const actualIconWidth = iconWidth || effectiveIconSize.value
const actualIconHeight = iconHeight || effectiveIconSize.value
// Calculate which grid cell the position falls into
const col = Math.floor(x / cellSize)
const row = Math.floor(adjustedY / cellSize)
// Add half the icon size to x/y to get the center point for snapping
const centerX = x + actualIconWidth / 2
const centerY = y + actualIconHeight / 2
// Use provided dimensions or fall back to cell size
const actualIconWidth = iconWidth || cellSize
const actualIconHeight = iconHeight || cellSize
// Find nearest grid cell center
// Grid cells are centered at: halfCell, halfCell + cellSize, halfCell + 2*cellSize, ...
// Which is: halfCell + (n * cellSize) for n = 0, 1, 2, ...
const col = Math.round((centerX - halfCell) / cellSize)
const row = Math.round((centerY - halfCell) / 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))
// Calculate the center of the target grid cell
const gridCenterX = halfCell + col * cellSize
const gridCenterY = halfCell + row * cellSize
const totalWidth = cellsWide * cellSize
const totalHeight = cellsHigh * cellSize
const paddingX = (totalWidth - actualIconWidth) / 2
const paddingY = (totalHeight - actualIconHeight) / 2
// Calculate the top-left position that centers the icon in the cell
const snappedX = gridCenterX - actualIconWidth / 2
const snappedY = gridCenterY - actualIconHeight / 2
return {
x: col * cellSize + paddingX - iconPadding,
y: row * cellSize + paddingY - iconPadding,
x: snappedX,
y: snappedY,
}
}

130
src/stores/sync/backends.ts Normal file
View File

@ -0,0 +1,130 @@
import { eq } from 'drizzle-orm'
import {
haexSyncBackends,
type InsertHaexSyncBackends,
type SelectHaexSyncBackends,
} from '~/database/schemas'
export const useSyncBackendsStore = defineStore('syncBackendsStore', () => {
const { currentVault } = storeToRefs(useVaultStore())
const backends = ref<SelectHaexSyncBackends[]>([])
const enabledBackends = computed(() =>
backends.value.filter((b) => b.enabled),
)
const sortedBackends = computed(() =>
[...backends.value].sort((a, b) => (b.priority || 0) - (a.priority || 0)),
)
// Load all sync backends from database
const loadBackendsAsync = async () => {
if (!currentVault.value?.drizzle) {
console.error('No vault opened')
return
}
try {
const result = await currentVault.value.drizzle
.select()
.from(haexSyncBackends)
backends.value = result
} catch (error) {
console.error('Failed to load sync backends:', error)
throw error
}
}
// Add a new sync backend
const addBackendAsync = async (backend: InsertHaexSyncBackends) => {
if (!currentVault.value?.drizzle) {
throw new Error('No vault opened')
}
try {
const result = await currentVault.value.drizzle
.insert(haexSyncBackends)
.values(backend)
.returning()
if (result.length > 0 && result[0]) {
backends.value.push(result[0])
return result[0]
}
} catch (error) {
console.error('Failed to add sync backend:', error)
throw error
}
}
// Update an existing sync backend
const updateBackendAsync = async (
id: string,
updates: Partial<InsertHaexSyncBackends>,
) => {
if (!currentVault.value?.drizzle) {
throw new Error('No vault opened')
}
try {
const result = await currentVault.value.drizzle
.update(haexSyncBackends)
.set(updates)
.where(eq(haexSyncBackends.id, id))
.returning()
if (result.length > 0 && result[0]) {
const index = backends.value.findIndex((b) => b.id === id)
if (index !== -1) {
backends.value[index] = result[0]
}
return result[0]
}
} catch (error) {
console.error('Failed to update sync backend:', error)
throw error
}
}
// Delete a sync backend
const deleteBackendAsync = async (id: string) => {
if (!currentVault.value?.drizzle) {
throw new Error('No vault opened')
}
try {
await currentVault.value.drizzle
.delete(haexSyncBackends)
.where(eq(haexSyncBackends.id, id))
backends.value = backends.value.filter((b) => b.id !== id)
} catch (error) {
console.error('Failed to delete sync backend:', error)
throw error
}
}
// Enable/disable a backend
const toggleBackendAsync = async (id: string, enabled: boolean) => {
return updateBackendAsync(id, { enabled })
}
// Update backend priority (for sync order)
const updatePriorityAsync = async (id: string, priority: number) => {
return updateBackendAsync(id, { priority })
}
return {
backends,
enabledBackends,
sortedBackends,
loadBackendsAsync,
addBackendAsync,
updateBackendAsync,
deleteBackendAsync,
toggleBackendAsync,
updatePriorityAsync,
}
})