mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
Compare commits
11 Commits
v0.1.12
...
554cb7762d
| Author | SHA1 | Date | |
|---|---|---|---|
| 554cb7762d | |||
| 5856a73e5b | |||
| 38cc6f36d4 | |||
| 0d4059e518 | |||
| c551641737 | |||
| 75093485bd | |||
| e1be08cb76 | |||
| 7d1f346c4b | |||
| af61972342 | |||
| 6187e32f89 | |||
| 43ba246174 |
26
README.md
26
README.md
@ -168,6 +168,32 @@ pnpm install
|
|||||||
pnpm tauri dev
|
pnpm tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 📦 Release Process
|
||||||
|
|
||||||
|
Create a new release using the automated scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Patch release (0.1.13 → 0.1.14)
|
||||||
|
pnpm release:patch
|
||||||
|
|
||||||
|
# Minor release (0.1.13 → 0.2.0)
|
||||||
|
pnpm release:minor
|
||||||
|
|
||||||
|
# Major release (0.1.13 → 1.0.0)
|
||||||
|
pnpm release:major
|
||||||
|
```
|
||||||
|
|
||||||
|
The script automatically:
|
||||||
|
1. Updates version in `package.json`
|
||||||
|
2. Creates a git commit
|
||||||
|
3. Creates a git tag
|
||||||
|
4. Pushes to remote
|
||||||
|
|
||||||
|
GitHub Actions will then automatically:
|
||||||
|
- Build desktop apps (macOS, Linux, Windows)
|
||||||
|
- Build Android apps (APK and AAB)
|
||||||
|
- Create and publish a GitHub release
|
||||||
|
|
||||||
#### 🧭 Summary
|
#### 🧭 Summary
|
||||||
|
|
||||||
HaexHub aims to:
|
HaexHub aims to:
|
||||||
|
|||||||
37
package.json
37
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "haex-hub",
|
"name": "haex-hub",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@ -14,6 +14,9 @@
|
|||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
|
"release:patch": "node scripts/release.js patch",
|
||||||
|
"release:minor": "node scripts/release.js minor",
|
||||||
|
"release:major": "node scripts/release.js major",
|
||||||
"tauri:build:debug": "tauri build --debug",
|
"tauri:build:debug": "tauri build --debug",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
@ -23,9 +26,9 @@
|
|||||||
"@nuxt/icon": "2.0.0",
|
"@nuxt/icon": "2.0.0",
|
||||||
"@nuxt/ui": "4.1.0",
|
"@nuxt/ui": "4.1.0",
|
||||||
"@nuxtjs/i18n": "10.0.6",
|
"@nuxtjs/i18n": "10.0.6",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"@supabase/supabase-js": "^2.79.0",
|
"@supabase/supabase-js": "^2.80.0",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tauri-apps/api": "^2.9.0",
|
"@tauri-apps/api": "^2.9.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
@ -38,32 +41,32 @@
|
|||||||
"@vueuse/gesture": "^2.0.0",
|
"@vueuse/gesture": "^2.0.0",
|
||||||
"@vueuse/nuxt": "^13.9.0",
|
"@vueuse/nuxt": "^13.9.0",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.39.1",
|
||||||
"nuxt-zod-i18n": "^1.12.1",
|
"nuxt-zod-i18n": "^1.12.1",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.17",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/hugeicons": "^1.2.17",
|
"@iconify-json/hugeicons": "^1.2.17",
|
||||||
"@iconify-json/lucide": "^1.2.71",
|
"@iconify-json/lucide": "^1.2.72",
|
||||||
"@iconify/json": "^2.2.401",
|
"@iconify/json": "^2.2.404",
|
||||||
"@iconify/tailwind4": "^1.0.6",
|
"@iconify/tailwind4": "^1.1.0",
|
||||||
"@libsql/client": "^0.15.15",
|
"@libsql/client": "^0.15.15",
|
||||||
"@tauri-apps/cli": "^2.9.1",
|
"@tauri-apps/cli": "^2.9.3",
|
||||||
"@types/node": "^24.9.1",
|
"@types/node": "^24.10.0",
|
||||||
"@vitejs/plugin-vue": "6.0.1",
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
"@vue/compiler-sfc": "^3.5.22",
|
"@vue/compiler-sfc": "^3.5.24",
|
||||||
"drizzle-kit": "^0.31.5",
|
"drizzle-kit": "^0.31.6",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.5.0",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.3",
|
"vite": "^7.2.2",
|
||||||
"vue-tsc": "3.0.6"
|
"vue-tsc": "3.0.6"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
|||||||
3220
pnpm-lock.yaml
generated
3220
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
91
scripts/release.js
Executable file
91
scripts/release.js
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
|
||||||
|
const versionType = process.argv[2];
|
||||||
|
|
||||||
|
if (!['patch', 'minor', 'major'].includes(versionType)) {
|
||||||
|
console.error('Usage: pnpm release <patch|minor|major>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current package.json
|
||||||
|
const packageJsonPath = join(rootDir, 'package.json');
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
if (!currentVersion) {
|
||||||
|
console.error('No version found in package.json');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse version
|
||||||
|
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||||
|
|
||||||
|
// Calculate new version
|
||||||
|
let newVersion;
|
||||||
|
switch (versionType) {
|
||||||
|
case 'major':
|
||||||
|
newVersion = `${major + 1}.0.0`;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
newVersion = `${major}.${minor + 1}.0`;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📦 Bumping version from ${currentVersion} to ${newVersion}`);
|
||||||
|
|
||||||
|
// Update package.json
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log('✅ Updated package.json');
|
||||||
|
|
||||||
|
// Git operations
|
||||||
|
try {
|
||||||
|
// Check if there are uncommitted changes
|
||||||
|
const status = execSync('git status --porcelain', { encoding: 'utf8' });
|
||||||
|
const hasOtherChanges = status
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line && !line.includes('package.json'))
|
||||||
|
.length > 0;
|
||||||
|
|
||||||
|
if (hasOtherChanges) {
|
||||||
|
console.error('❌ There are uncommitted changes besides package.json. Please commit or stash them first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add and commit package.json
|
||||||
|
execSync('git add package.json', { stdio: 'inherit' });
|
||||||
|
execSync(`git commit -m "Bump version to ${newVersion}"`, { stdio: 'inherit' });
|
||||||
|
console.log('✅ Committed version bump');
|
||||||
|
|
||||||
|
// Create tag
|
||||||
|
execSync(`git tag v${newVersion}`, { stdio: 'inherit' });
|
||||||
|
console.log(`✅ Created tag v${newVersion}`);
|
||||||
|
|
||||||
|
// Push changes and tag
|
||||||
|
console.log('📤 Pushing to remote...');
|
||||||
|
execSync('git push', { stdio: 'inherit' });
|
||||||
|
execSync(`git push origin v${newVersion}`, { stdio: 'inherit' });
|
||||||
|
console.log('✅ Pushed changes and tag');
|
||||||
|
|
||||||
|
console.log('\n🎉 Release v' + newVersion + ' created successfully!');
|
||||||
|
console.log('📋 GitHub Actions will now build and publish the release.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Git operation failed:', error.message);
|
||||||
|
// Rollback package.json changes
|
||||||
|
packageJson.version = currentVersion;
|
||||||
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log('↩️ Rolled back package.json changes');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
6
src-tauri/bindings/ExtensionErrorCode.ts
Normal file
6
src-tauri/bindings/ExtensionErrorCode.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error codes for frontend handling
|
||||||
|
*/
|
||||||
|
export type ExtensionErrorCode = "SecurityViolation" | "NotFound" | "PermissionDenied" | "MutexPoisoned" | "Database" | "Filesystem" | "FilesystemWithPath" | "Http" | "Shell" | "Manifest" | "Validation" | "InvalidPublicKey" | "InvalidSignature" | "InvalidActionString" | "SignatureVerificationFailed" | "CalculateHash" | "Installation";
|
||||||
6
src-tauri/bindings/SerializedExtensionError.ts
Normal file
6
src-tauri/bindings/SerializedExtensionError.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized representation of ExtensionError for TypeScript
|
||||||
|
*/
|
||||||
|
export type SerializedExtensionError = { code: number, type: string, message: string, extension_id: string | null, };
|
||||||
@ -30,10 +30,15 @@
|
|||||||
"fs:allow-resource-write-recursive",
|
"fs:allow-resource-write-recursive",
|
||||||
"fs:allow-download-read-recursive",
|
"fs:allow-download-read-recursive",
|
||||||
"fs:allow-download-write-recursive",
|
"fs:allow-download-write-recursive",
|
||||||
|
"fs:allow-temp-read-recursive",
|
||||||
|
"fs:allow-temp-write-recursive",
|
||||||
"fs:default",
|
"fs:default",
|
||||||
{
|
{
|
||||||
"identifier": "fs:scope",
|
"identifier": "fs:scope",
|
||||||
"allow": [{ "path": "**" }]
|
"allow": [
|
||||||
|
{ "path": "**" },
|
||||||
|
{ "path": "$TEMP/**" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"http:allow-fetch-send",
|
"http:allow-fetch-send",
|
||||||
"http:allow-fetch",
|
"http:allow-fetch",
|
||||||
@ -44,6 +49,12 @@
|
|||||||
"notification:allow-is-permission-granted",
|
"notification:allow-is-permission-granted",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"opener:allow-open-url",
|
"opener:allow-open-url",
|
||||||
|
{
|
||||||
|
"identifier": "opener:allow-open-path",
|
||||||
|
"allow": [
|
||||||
|
{ "path": "$TEMP/**" }
|
||||||
|
]
|
||||||
|
},
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"os:allow-hostname",
|
"os:allow-hostname",
|
||||||
"os:default",
|
"os:default",
|
||||||
|
|||||||
Binary file not shown.
@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-applocaldata-read-recursive","fs:allow-applocaldata-write-recursive","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-mkdir","fs:allow-exists","fs:allow-remove","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-applocaldata-read-recursive","fs:allow-applocaldata-write-recursive","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-mkdir","fs:allow-exists","fs:allow-remove","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"},{"path":"$TEMP/**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url",{"identifier":"opener:allow-open-path","allow":[{"path":"$TEMP/**"}]},"opener:default","os:allow-hostname","os:default","store:default"]}}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
// src-tauri/src/extension/error.rs
|
// src-tauri/src/extension/error.rs
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
|
|
||||||
/// Error codes for frontend handling
|
/// Error codes for frontend handling
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub enum ExtensionErrorCode {
|
pub enum ExtensionErrorCode {
|
||||||
SecurityViolation = 1000,
|
SecurityViolation = 1000,
|
||||||
NotFound = 1001,
|
NotFound = 1001,
|
||||||
@ -25,6 +27,17 @@ pub enum ExtensionErrorCode {
|
|||||||
Installation = 5000,
|
Installation = 5000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialized representation of ExtensionError for TypeScript
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct SerializedExtensionError {
|
||||||
|
pub code: u16,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub error_type: String,
|
||||||
|
pub message: String,
|
||||||
|
pub extension_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl serde::Serialize for ExtensionErrorCode {
|
impl serde::Serialize for ExtensionErrorCode {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "haex-hub",
|
"productName": "haex-hub",
|
||||||
"version": "0.1.4",
|
"version": "0.1.13",
|
||||||
"identifier": "space.haex.hub",
|
"identifier": "space.haex.hub",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<UDrawer
|
<UiDrawer
|
||||||
v-model:open="open"
|
v-model:open="open"
|
||||||
direction="right"
|
direction="right"
|
||||||
:title="t('launcher.title')"
|
:title="t('launcher.title')"
|
||||||
@ -7,9 +7,6 @@
|
|||||||
:overlay="false"
|
:overlay="false"
|
||||||
:modal="false"
|
:modal="false"
|
||||||
:handle-only="true"
|
:handle-only="true"
|
||||||
:ui="{
|
|
||||||
content: 'w-dvw max-w-md sm:max-w-fit',
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<UButton
|
<UButton
|
||||||
icon="material-symbols:apps"
|
icon="material-symbols:apps"
|
||||||
@ -66,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDrawer>
|
</UiDrawer>
|
||||||
|
|
||||||
<!-- Uninstall Confirmation Dialog -->
|
<!-- Uninstall Confirmation Dialog -->
|
||||||
<UiDialogConfirm
|
<UiDialogConfirm
|
||||||
|
|||||||
@ -163,8 +163,9 @@ const loadDevExtensionAsync = async () => {
|
|||||||
extensionPath.value = ''
|
extensionPath.value = ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load dev extension:', error)
|
console.error('Failed to load dev extension:', error)
|
||||||
|
const { getErrorMessage } = useExtensionError()
|
||||||
add({
|
add({
|
||||||
description: t('add.errors.loadFailed') + error,
|
description: `${t('add.errors.loadFailed')}: ${getErrorMessage(error)}`,
|
||||||
color: 'error',
|
color: 'error',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@ -196,8 +197,9 @@ const reloadDevExtensionAsync = async (extension: ExtensionInfoResponse) => {
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to reload dev extension:', error)
|
console.error('Failed to reload dev extension:', error)
|
||||||
|
const { getErrorMessage } = useExtensionError()
|
||||||
add({
|
add({
|
||||||
description: t('list.errors.reloadFailed') + error,
|
description: `${t('list.errors.reloadFailed')}: ${getErrorMessage(error)}`,
|
||||||
color: 'error',
|
color: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -223,8 +225,9 @@ const removeDevExtensionAsync = async (extension: ExtensionInfoResponse) => {
|
|||||||
await loadExtensionsAsync()
|
await loadExtensionsAsync()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to remove dev extension:', error)
|
console.error('Failed to remove dev extension:', error)
|
||||||
|
const { getErrorMessage } = useExtensionError()
|
||||||
add({
|
add({
|
||||||
description: t('list.errors.removeFailed') + error,
|
description: `${t('list.errors.removeFailed')}: ${getErrorMessage(error)}`,
|
||||||
color: 'error',
|
color: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getAvailableContentHeight } from '~/utils/viewport'
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@ -329,31 +330,11 @@ const handleMaximize = () => {
|
|||||||
const bounds = getViewportBounds()
|
const bounds = getViewportBounds()
|
||||||
|
|
||||||
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
||||||
// Get safe-area-insets from CSS variables for debug
|
|
||||||
const safeAreaTop = parseFloat(
|
|
||||||
getComputedStyle(document.documentElement).getPropertyValue(
|
|
||||||
'--safe-area-inset-top',
|
|
||||||
) || '0',
|
|
||||||
)
|
|
||||||
const safeAreaBottom = parseFloat(
|
|
||||||
getComputedStyle(document.documentElement).getPropertyValue(
|
|
||||||
'--safe-area-inset-bottom',
|
|
||||||
) || '0',
|
|
||||||
)
|
|
||||||
|
|
||||||
// Desktop container uses 'absolute inset-0' which stretches over full viewport
|
|
||||||
// bounds.height = full viewport height (includes header area + safe-areas)
|
|
||||||
// We need to calculate available space properly
|
|
||||||
|
|
||||||
// Get header height from UI store (measured reactively in layout)
|
|
||||||
const uiStore = useUiStore()
|
|
||||||
const headerHeight = uiStore.headerHeight
|
|
||||||
|
|
||||||
x.value = 0
|
x.value = 0
|
||||||
y.value = 0 // Start below header and status bar
|
y.value = 0
|
||||||
width.value = bounds.width
|
width.value = bounds.width
|
||||||
// Height: viewport - header - both safe-areas
|
// Use helper function to calculate correct height with safe areas
|
||||||
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
height.value = getAvailableContentHeight()
|
||||||
isMaximized.value = true
|
isMaximized.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<UDrawer
|
<UiDrawer
|
||||||
v-model:open="localShowWindowOverview"
|
v-model:open="localShowWindowOverview"
|
||||||
direction="bottom"
|
direction="bottom"
|
||||||
:title="t('modal.title')"
|
:title="t('modal.title')"
|
||||||
@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDrawer>
|
</UiDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<UDrawer
|
<UiDrawer
|
||||||
v-model:open="isOverviewMode"
|
v-model:open="isOverviewMode"
|
||||||
direction="left"
|
direction="left"
|
||||||
:overlay="false"
|
:overlay="false"
|
||||||
@ -8,7 +8,7 @@
|
|||||||
description="Workspaces"
|
description="Workspaces"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="py-8 pl-8 pr-4 h-full overflow-y-auto">
|
<div class="pl-8 pr-4 overflow-y-auto py-8">
|
||||||
<!-- Workspace Cards -->
|
<!-- Workspace Cards -->
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<HaexWorkspaceCard
|
<HaexWorkspaceCard
|
||||||
@ -29,7 +29,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UDrawer>
|
</UiDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
32
src/components/ui/Drawer.vue
Normal file
32
src/components/ui/Drawer.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<UDrawer
|
||||||
|
v-bind="$attrs"
|
||||||
|
:ui="{
|
||||||
|
content:
|
||||||
|
'pb-[env(safe-area-inset-bottom)] pt-[env(safe-area-inset-top)] ',
|
||||||
|
...(ui || {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(_, name) in $slots"
|
||||||
|
#[name]="slotData"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
:name="name"
|
||||||
|
v-bind="slotData"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DrawerProps } from '@nuxt/ui'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around UDrawer that automatically applies safe area insets for mobile devices.
|
||||||
|
* Passes through all props and slots to UDrawer.
|
||||||
|
*/
|
||||||
|
const props = defineProps</* @vue-ignore */ DrawerProps>()
|
||||||
|
|
||||||
|
const { ui } = toRefs(props)
|
||||||
|
</script>
|
||||||
@ -83,8 +83,6 @@ const filteredSlots = computed(() => {
|
|||||||
Object.entries(useSlots()).filter(([name]) => name !== 'trailing'),
|
Object.entries(useSlots()).filter(([name]) => name !== 'trailing'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isSmallScreen } = storeToRefs(useUiStore())
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
|
|||||||
@ -1,38 +1,29 @@
|
|||||||
// composables/extensionMessageHandler.ts
|
// composables/extensionMessageHandler.ts
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
import {
|
import {
|
||||||
EXTENSION_PROTOCOL_NAME,
|
EXTENSION_PROTOCOL_NAME,
|
||||||
EXTENSION_PROTOCOL_PREFIX,
|
EXTENSION_PROTOCOL_PREFIX,
|
||||||
} from '~/config/constants'
|
} from '~/config/constants'
|
||||||
import type { Platform } from '@tauri-apps/plugin-os'
|
import {
|
||||||
|
handleDatabaseMethodAsync,
|
||||||
interface ExtensionRequest {
|
handleFilesystemMethodAsync,
|
||||||
id: string
|
handleHttpMethodAsync,
|
||||||
method: string
|
handlePermissionsMethodAsync,
|
||||||
params: Record<string, unknown>
|
handleContextMethodAsync,
|
||||||
timestamp: number
|
handleStorageMethodAsync,
|
||||||
}
|
setContextGetters,
|
||||||
|
type ExtensionRequest,
|
||||||
|
type ExtensionInstance,
|
||||||
|
} from './handlers'
|
||||||
|
|
||||||
// Globaler Handler - nur einmal registriert
|
// Globaler Handler - nur einmal registriert
|
||||||
let globalHandlerRegistered = false
|
let globalHandlerRegistered = false
|
||||||
interface ExtensionInstance {
|
|
||||||
extension: IHaexHubExtension
|
|
||||||
windowId: string
|
|
||||||
}
|
|
||||||
const iframeRegistry = new Map<HTMLIFrameElement, ExtensionInstance>()
|
const iframeRegistry = new Map<HTMLIFrameElement, ExtensionInstance>()
|
||||||
// Map event.source (WindowProxy) to extension instance for sandbox-compatible matching
|
// Map event.source (WindowProxy) to extension instance for sandbox-compatible matching
|
||||||
const sourceRegistry = new Map<Window, ExtensionInstance>()
|
const sourceRegistry = new Map<Window, ExtensionInstance>()
|
||||||
// Reverse map: window ID to Window for broadcasting (supports multiple windows per extension)
|
// Reverse map: window ID to Window for broadcasting (supports multiple windows per extension)
|
||||||
const windowIdToWindowMap = new Map<string, Window>()
|
const windowIdToWindowMap = new Map<string, Window>()
|
||||||
|
|
||||||
// Store context values that need to be accessed outside setup
|
|
||||||
let contextGetters: {
|
|
||||||
getTheme: () => string
|
|
||||||
getLocale: () => string
|
|
||||||
getPlatform: () => Platform | undefined
|
|
||||||
} | null = null
|
|
||||||
|
|
||||||
const registerGlobalMessageHandler = () => {
|
const registerGlobalMessageHandler = () => {
|
||||||
if (globalHandlerRegistered) return
|
if (globalHandlerRegistered) return
|
||||||
|
|
||||||
@ -227,13 +218,11 @@ export const useExtensionMessageHandler = (
|
|||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const { platform } = useDeviceStore()
|
const { platform } = useDeviceStore()
|
||||||
// Store getters for use outside setup context
|
// Store getters for use outside setup context
|
||||||
if (!contextGetters) {
|
setContextGetters({
|
||||||
contextGetters = {
|
|
||||||
getTheme: () => currentTheme.value?.value || 'system',
|
getTheme: () => currentTheme.value?.value || 'system',
|
||||||
getLocale: () => locale.value,
|
getLocale: () => locale.value,
|
||||||
getPlatform: () => platform,
|
getPlatform: () => platform,
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Registriere globalen Handler beim ersten Aufruf
|
// Registriere globalen Handler beim ersten Aufruf
|
||||||
registerGlobalMessageHandler()
|
registerGlobalMessageHandler()
|
||||||
@ -275,12 +264,7 @@ export const registerExtensionIFrame = (
|
|||||||
// Stelle sicher, dass der globale Handler registriert ist
|
// Stelle sicher, dass der globale Handler registriert ist
|
||||||
registerGlobalMessageHandler()
|
registerGlobalMessageHandler()
|
||||||
|
|
||||||
// Warnung wenn Context Getters nicht initialisiert wurden
|
// Note: Context getters should be initialized via useExtensionMessageHandler first
|
||||||
if (!contextGetters) {
|
|
||||||
console.warn(
|
|
||||||
'Context getters not initialized. Make sure useExtensionMessageHandler was called in setup context first.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
iframeRegistry.set(iframe, { extension, windowId })
|
iframeRegistry.set(iframe, { extension, windowId })
|
||||||
}
|
}
|
||||||
@ -338,221 +322,21 @@ export const broadcastContextToAllExtensions = (context: {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ExtensionHandler] Broadcasting context to all extensions:', context)
|
console.log(
|
||||||
|
'[ExtensionHandler] Broadcasting context to all extensions:',
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
// Send to all registered extension windows
|
// Send to all registered extension windows
|
||||||
for (const [_, instance] of iframeRegistry.entries()) {
|
for (const [_, instance] of iframeRegistry.entries()) {
|
||||||
const win = windowIdToWindowMap.get(instance.windowId)
|
const win = windowIdToWindowMap.get(instance.windowId)
|
||||||
if (win) {
|
if (win) {
|
||||||
console.log('[ExtensionHandler] Sending context to:', instance.extension.name, instance.windowId)
|
console.log(
|
||||||
|
'[ExtensionHandler] Sending context to:',
|
||||||
|
instance.extension.name,
|
||||||
|
instance.windowId,
|
||||||
|
)
|
||||||
win.postMessage(message, '*')
|
win.postMessage(message, '*')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Database Methods
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function handleDatabaseMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension, // Direkter Typ
|
|
||||||
) {
|
|
||||||
const params = request.params as {
|
|
||||||
query?: string
|
|
||||||
params?: unknown[]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (request.method) {
|
|
||||||
case 'haextension.db.query': {
|
|
||||||
try {
|
|
||||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
|
||||||
sql: params.query || '',
|
|
||||||
params: params.params || [],
|
|
||||||
publicKey: extension.publicKey,
|
|
||||||
name: extension.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows,
|
|
||||||
rowsAffected: 0,
|
|
||||||
lastInsertId: undefined,
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// If error is about non-SELECT statements (INSERT/UPDATE/DELETE with RETURNING),
|
|
||||||
// automatically retry with execute
|
|
||||||
if (error?.message?.includes('Only SELECT statements are allowed')) {
|
|
||||||
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
|
||||||
sql: params.query || '',
|
|
||||||
params: params.params || [],
|
|
||||||
publicKey: extension.publicKey,
|
|
||||||
name: extension.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows,
|
|
||||||
rowsAffected: rows.length,
|
|
||||||
lastInsertId: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'haextension.db.execute': {
|
|
||||||
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
|
||||||
sql: params.query || '',
|
|
||||||
params: params.params || [],
|
|
||||||
publicKey: extension.publicKey,
|
|
||||||
name: extension.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows,
|
|
||||||
rowsAffected: 1,
|
|
||||||
lastInsertId: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'haextension.db.transaction': {
|
|
||||||
const statements =
|
|
||||||
(request.params as { statements?: string[] }).statements || []
|
|
||||||
|
|
||||||
for (const stmt of statements) {
|
|
||||||
await invoke('extension_sql_execute', {
|
|
||||||
sql: stmt,
|
|
||||||
params: [],
|
|
||||||
publicKey: extension.publicKey,
|
|
||||||
name: extension.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown database method: ${request.method}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ==========================================
|
|
||||||
// Filesystem Methods (TODO)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function handleFilesystemMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension,
|
|
||||||
) {
|
|
||||||
if (!request || !extension) return
|
|
||||||
// TODO: Implementiere Filesystem Commands im Backend
|
|
||||||
throw new Error('Filesystem methods not yet implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// HTTP Methods (TODO)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function handleHttpMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension,
|
|
||||||
) {
|
|
||||||
if (!extension || !request) {
|
|
||||||
throw new Error('Extension not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implementiere HTTP Commands im Backend
|
|
||||||
throw new Error('HTTP methods not yet implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Permission Methods (TODO)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function handlePermissionsMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension,
|
|
||||||
) {
|
|
||||||
if (!extension || !request) {
|
|
||||||
throw new Error('Extension not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implementiere Permission Request UI
|
|
||||||
throw new Error('Permission methods not yet implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Context Methods
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function handleContextMethodAsync(request: ExtensionRequest) {
|
|
||||||
switch (request.method) {
|
|
||||||
case 'haextension.context.get':
|
|
||||||
if (!contextGetters) {
|
|
||||||
throw new Error(
|
|
||||||
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
theme: contextGetters.getTheme(),
|
|
||||||
locale: contextGetters.getLocale(),
|
|
||||||
platform: contextGetters.getPlatform(),
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown context method: ${request.method}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Storage Methods
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function handleStorageMethodAsync(
|
|
||||||
request: ExtensionRequest,
|
|
||||||
instance: ExtensionInstance,
|
|
||||||
) {
|
|
||||||
// Storage is now per-window, not per-extension
|
|
||||||
const storageKey = `ext_${instance.extension.id}_${instance.windowId}_`
|
|
||||||
console.log(
|
|
||||||
`[HaexHub Storage] ${request.method} for window ${instance.windowId}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch (request.method) {
|
|
||||||
case 'haextension.storage.getItem': {
|
|
||||||
const key = request.params.key as string
|
|
||||||
return localStorage.getItem(storageKey + key)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'haextension.storage.setItem': {
|
|
||||||
const key = request.params.key as string
|
|
||||||
const value = request.params.value as string
|
|
||||||
localStorage.setItem(storageKey + key, value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'haextension.storage.removeItem': {
|
|
||||||
const key = request.params.key as string
|
|
||||||
localStorage.removeItem(storageKey + key)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'haextension.storage.clear': {
|
|
||||||
// Remove only instance-specific keys
|
|
||||||
const keys = Object.keys(localStorage).filter((k) =>
|
|
||||||
k.startsWith(storageKey),
|
|
||||||
)
|
|
||||||
keys.forEach((k) => localStorage.removeItem(k))
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'haextension.storage.keys': {
|
|
||||||
// Return only instance-specific keys (without prefix)
|
|
||||||
const keys = Object.keys(localStorage)
|
|
||||||
.filter((k) => k.startsWith(storageKey))
|
|
||||||
.map((k) => k.substring(storageKey.length))
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown storage method: ${request.method}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
36
src/composables/handlers/context.ts
Normal file
36
src/composables/handlers/context.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { Platform } from '@tauri-apps/plugin-os'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
|
// Context getters are set from the main handler during initialization
|
||||||
|
let contextGetters: {
|
||||||
|
getTheme: () => string
|
||||||
|
getLocale: () => string
|
||||||
|
getPlatform: () => Platform | undefined
|
||||||
|
} | null = null
|
||||||
|
|
||||||
|
export function setContextGetters(getters: {
|
||||||
|
getTheme: () => string
|
||||||
|
getLocale: () => string
|
||||||
|
getPlatform: () => Platform | undefined
|
||||||
|
}) {
|
||||||
|
contextGetters = getters
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||||
|
switch (request.method) {
|
||||||
|
case 'haextension.context.get':
|
||||||
|
if (!contextGetters) {
|
||||||
|
throw new Error(
|
||||||
|
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
theme: contextGetters.getTheme(),
|
||||||
|
locale: contextGetters.getLocale(),
|
||||||
|
platform: contextGetters.getPlatform(),
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown context method: ${request.method}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/composables/handlers/database.ts
Normal file
84
src/composables/handlers/database.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
|
export async function handleDatabaseMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
const params = request.params as {
|
||||||
|
query?: string
|
||||||
|
params?: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request.method) {
|
||||||
|
case 'haextension.db.query': {
|
||||||
|
try {
|
||||||
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||||
|
sql: params.query || '',
|
||||||
|
params: params.params || [],
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
rowsAffected: 0,
|
||||||
|
lastInsertId: undefined,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If error is about non-SELECT statements (INSERT/UPDATE/DELETE with RETURNING),
|
||||||
|
// automatically retry with execute
|
||||||
|
if (error?.message?.includes('Only SELECT statements are allowed')) {
|
||||||
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||||
|
sql: params.query || '',
|
||||||
|
params: params.params || [],
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
rowsAffected: rows.length,
|
||||||
|
lastInsertId: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.db.execute': {
|
||||||
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||||
|
sql: params.query || '',
|
||||||
|
params: params.params || [],
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
rowsAffected: 1,
|
||||||
|
lastInsertId: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.db.transaction': {
|
||||||
|
const statements =
|
||||||
|
(request.params as { statements?: string[] }).statements || []
|
||||||
|
|
||||||
|
for (const stmt of statements) {
|
||||||
|
await invoke('extension_sql_execute', {
|
||||||
|
sql: stmt,
|
||||||
|
params: [],
|
||||||
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown database method: ${request.method}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/composables/handlers/filesystem.ts
Normal file
92
src/composables/handlers/filesystem.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { save } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { writeFile } from '@tauri-apps/plugin-fs'
|
||||||
|
import { openPath } from '@tauri-apps/plugin-opener'
|
||||||
|
import { tempDir, join } from '@tauri-apps/api/path'
|
||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
|
export async function handleFilesystemMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
if (!request || !extension) return
|
||||||
|
|
||||||
|
switch (request.method) {
|
||||||
|
case 'haextension.fs.saveFile': {
|
||||||
|
const params = request.params as {
|
||||||
|
data: number[]
|
||||||
|
defaultPath?: string
|
||||||
|
title?: string
|
||||||
|
filters?: Array<{ name: string; extensions: string[] }>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert number array back to Uint8Array
|
||||||
|
const data = new Uint8Array(params.data)
|
||||||
|
|
||||||
|
// Open save dialog
|
||||||
|
const filePath = await save({
|
||||||
|
defaultPath: params.defaultPath,
|
||||||
|
title: params.title || 'Save File',
|
||||||
|
filters: params.filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
// User cancelled
|
||||||
|
if (!filePath) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file
|
||||||
|
await writeFile(filePath, data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.fs.showImage': {
|
||||||
|
// This method is now handled by the frontend using PhotoSwipe
|
||||||
|
// We keep it for backwards compatibility but it's a no-op
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
useFrontend: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.fs.openFile': {
|
||||||
|
const params = request.params as {
|
||||||
|
data: number[]
|
||||||
|
fileName: string
|
||||||
|
mimeType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert number array back to Uint8Array
|
||||||
|
const data = new Uint8Array(params.data)
|
||||||
|
|
||||||
|
// Get temp directory and create file path
|
||||||
|
const tempDirPath = await tempDir()
|
||||||
|
const tempFilePath = await join(tempDirPath, params.fileName)
|
||||||
|
|
||||||
|
// Write file to temp directory
|
||||||
|
await writeFile(tempFilePath, data)
|
||||||
|
|
||||||
|
// Open file with system's default viewer
|
||||||
|
await openPath(tempFilePath)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('[Filesystem] Error opening file:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown filesystem method: ${request.method}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/composables/handlers/http.ts
Normal file
14
src/composables/handlers/http.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
|
export async function handleHttpMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
if (!extension || !request) {
|
||||||
|
throw new Error('Extension not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implementiere HTTP Commands im Backend
|
||||||
|
throw new Error('HTTP methods not yet implemented')
|
||||||
|
}
|
||||||
10
src/composables/handlers/index.ts
Normal file
10
src/composables/handlers/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Export all handler functions
|
||||||
|
export { handleDatabaseMethodAsync } from './database'
|
||||||
|
export { handleFilesystemMethodAsync } from './filesystem'
|
||||||
|
export { handleHttpMethodAsync } from './http'
|
||||||
|
export { handlePermissionsMethodAsync } from './permissions'
|
||||||
|
export { handleContextMethodAsync, setContextGetters } from './context'
|
||||||
|
export { handleStorageMethodAsync } from './storage'
|
||||||
|
|
||||||
|
// Export shared types
|
||||||
|
export type { ExtensionRequest, ExtensionInstance } from './types'
|
||||||
14
src/composables/handlers/permissions.ts
Normal file
14
src/composables/handlers/permissions.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import type { ExtensionRequest } from './types'
|
||||||
|
|
||||||
|
export async function handlePermissionsMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
extension: IHaexHubExtension,
|
||||||
|
) {
|
||||||
|
if (!extension || !request) {
|
||||||
|
throw new Error('Extension not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implementiere Permission Request UI
|
||||||
|
throw new Error('Permission methods not yet implemented')
|
||||||
|
}
|
||||||
52
src/composables/handlers/storage.ts
Normal file
52
src/composables/handlers/storage.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { ExtensionRequest, ExtensionInstance } from './types'
|
||||||
|
|
||||||
|
export async function handleStorageMethodAsync(
|
||||||
|
request: ExtensionRequest,
|
||||||
|
instance: ExtensionInstance,
|
||||||
|
) {
|
||||||
|
// Storage is now per-window, not per-extension
|
||||||
|
const storageKey = `ext_${instance.extension.id}_${instance.windowId}_`
|
||||||
|
console.log(
|
||||||
|
`[HaexHub Storage] ${request.method} for window ${instance.windowId}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (request.method) {
|
||||||
|
case 'haextension.storage.getItem': {
|
||||||
|
const key = request.params.key as string
|
||||||
|
return localStorage.getItem(storageKey + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.storage.setItem': {
|
||||||
|
const key = request.params.key as string
|
||||||
|
const value = request.params.value as string
|
||||||
|
localStorage.setItem(storageKey + key, value)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.storage.removeItem': {
|
||||||
|
const key = request.params.key as string
|
||||||
|
localStorage.removeItem(storageKey + key)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.storage.clear': {
|
||||||
|
// Remove only instance-specific keys
|
||||||
|
const keys = Object.keys(localStorage).filter((k) =>
|
||||||
|
k.startsWith(storageKey),
|
||||||
|
)
|
||||||
|
keys.forEach((k) => localStorage.removeItem(k))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'haextension.storage.keys': {
|
||||||
|
// Return only instance-specific keys (without prefix)
|
||||||
|
const keys = Object.keys(localStorage)
|
||||||
|
.filter((k) => k.startsWith(storageKey))
|
||||||
|
.map((k) => k.substring(storageKey.length))
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown storage method: ${request.method}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/composables/handlers/types.ts
Normal file
14
src/composables/handlers/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Shared types for extension message handlers
|
||||||
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
|
||||||
|
export interface ExtensionRequest {
|
||||||
|
id: string
|
||||||
|
method: string
|
||||||
|
params: Record<string, unknown>
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionInstance {
|
||||||
|
extension: IHaexHubExtension
|
||||||
|
windowId: string
|
||||||
|
}
|
||||||
43
src/composables/useExtensionError.ts
Normal file
43
src/composables/useExtensionError.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import type { SerializedExtensionError } from '~~/src-tauri/bindings/SerializedExtensionError'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if error is a SerializedExtensionError
|
||||||
|
*/
|
||||||
|
export function isSerializedExtensionError(error: unknown): error is SerializedExtensionError {
|
||||||
|
return (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'code' in error &&
|
||||||
|
'message' in error &&
|
||||||
|
'type' in error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract error message from unknown error type
|
||||||
|
*/
|
||||||
|
export function getErrorMessage(error: unknown): string {
|
||||||
|
if (isSerializedExtensionError(error)) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for handling extension errors
|
||||||
|
*/
|
||||||
|
export function useExtensionError() {
|
||||||
|
return {
|
||||||
|
isSerializedExtensionError,
|
||||||
|
getErrorMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { defineAsyncComponent, type Component } from 'vue'
|
import { defineAsyncComponent, type Component } from 'vue'
|
||||||
|
import { getFullscreenDimensions } from '~/utils/viewport'
|
||||||
|
|
||||||
export interface IWindow {
|
export interface IWindow {
|
||||||
id: string
|
id: string
|
||||||
@ -191,11 +192,30 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
const viewportHeight = window.innerHeight - 60
|
const viewportHeight = window.innerHeight - 60
|
||||||
|
|
||||||
console.log('viewportHeight', window.innerHeight, viewportHeight)
|
console.log('viewportHeight', window.innerHeight, viewportHeight)
|
||||||
const windowHeight = Math.min(height, viewportHeight)
|
|
||||||
|
// Check if we're on a small screen
|
||||||
|
const { isSmallScreen } = useUiStore()
|
||||||
|
|
||||||
|
let windowWidth: number
|
||||||
|
let windowHeight: number
|
||||||
|
let x: number
|
||||||
|
let y: number
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
// On small screens, make window fullscreen starting at 0,0
|
||||||
|
// Use helper function to calculate correct dimensions with safe areas
|
||||||
|
const fullscreen = getFullscreenDimensions()
|
||||||
|
x = fullscreen.x
|
||||||
|
y = fullscreen.y
|
||||||
|
windowWidth = fullscreen.width
|
||||||
|
windowHeight = fullscreen.height
|
||||||
|
} else {
|
||||||
|
// On larger screens, use normal sizing and positioning
|
||||||
|
windowHeight = Math.min(height, viewportHeight)
|
||||||
|
|
||||||
// Adjust width proportionally if needed (optional)
|
// Adjust width proportionally if needed (optional)
|
||||||
const aspectRatio = width / height
|
const aspectRatio = width / height
|
||||||
const windowWidth = Math.min(
|
windowWidth = Math.min(
|
||||||
width,
|
width,
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
windowHeight * aspectRatio,
|
windowHeight * aspectRatio,
|
||||||
@ -205,8 +225,9 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
const offset = currentWorkspaceWindows.value.length * 30
|
const offset = currentWorkspaceWindows.value.length * 30
|
||||||
const centerX = Math.max(0, (viewportWidth - windowWidth) / 1 / 3)
|
const centerX = Math.max(0, (viewportWidth - windowWidth) / 1 / 3)
|
||||||
const centerY = Math.max(0, (viewportHeight - windowHeight) / 1 / 3)
|
const centerY = Math.max(0, (viewportHeight - windowHeight) / 1 / 3)
|
||||||
const x = Math.min(centerX + offset, viewportWidth - windowWidth)
|
x = Math.min(centerX + offset, viewportWidth - windowWidth)
|
||||||
const y = Math.min(centerY + offset, viewportHeight - windowHeight)
|
y = Math.min(centerY + offset, viewportHeight - windowHeight)
|
||||||
|
}
|
||||||
|
|
||||||
const newWindow: IWindow = {
|
const newWindow: IWindow = {
|
||||||
id: windowId,
|
id: windowId,
|
||||||
|
|||||||
63
src/utils/viewport.ts
Normal file
63
src/utils/viewport.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Viewport and safe area utilities
|
||||||
|
|
||||||
|
export interface ViewportDimensions {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
safeAreaTop: number
|
||||||
|
safeAreaBottom: number
|
||||||
|
headerHeight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get viewport dimensions with safe areas and header height
|
||||||
|
*/
|
||||||
|
export function getViewportDimensions(): ViewportDimensions {
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
const viewportHeight = window.innerHeight - 40 // Subtract header height
|
||||||
|
|
||||||
|
// Get safe-area-insets from CSS variables
|
||||||
|
const safeAreaTop = parseFloat(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
'--safe-area-inset-top',
|
||||||
|
) || '0',
|
||||||
|
)
|
||||||
|
const safeAreaBottom = parseFloat(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
'--safe-area-inset-bottom',
|
||||||
|
) || '0',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get header height from UI store
|
||||||
|
const { headerHeight } = useUiStore()
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: viewportWidth,
|
||||||
|
height: viewportHeight,
|
||||||
|
safeAreaTop,
|
||||||
|
safeAreaBottom,
|
||||||
|
headerHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate available content height (viewport height minus safe areas)
|
||||||
|
* Note: viewport height already excludes header, so we only subtract safe areas
|
||||||
|
*/
|
||||||
|
export function getAvailableContentHeight(): number {
|
||||||
|
const dimensions = getViewportDimensions()
|
||||||
|
return dimensions.height - dimensions.safeAreaTop - dimensions.safeAreaBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate fullscreen window dimensions (for small screens)
|
||||||
|
*/
|
||||||
|
export function getFullscreenDimensions() {
|
||||||
|
const dimensions = getViewportDimensions()
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: dimensions.width,
|
||||||
|
height: getAvailableContentHeight(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user