27 Commits

Author SHA1 Message Date
4fa3515e32 Bump version to 0.1.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 20:21:37 +01:00
c5c30fd4c4 Fix Vite 7.x TDZ error in __vite__mapDeps with post-build script
- Add post-build script to fix Temporal Dead Zone error in generated code
- Remove debug logging from stores and composables
- Simplify init-logger plugin to essential error handling
- Fix circular store dependency in useUiStore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 20:21:12 +01:00
8c7a02a019 Sync version numbers across all package files
Update Cargo.toml and tauri.conf.json to version 0.1.2 to match package.json
2025-11-01 19:33:42 +01:00
465fe19542 Clean up unused code and dependencies
- Remove commented-out code in Rust and TypeScript files
- Remove unused npm dependencies (@tauri-apps/plugin-http, @tauri-apps/plugin-sql, fuse.js)
- Remove commented imports in nuxt.config.ts
- Remove commented dependencies in Cargo.toml
2025-11-01 19:32:34 +01:00
d2d0f8996b Fix runtime CSP error by allowing inline scripts
Added 'unsafe-inline' to script-src CSP directive to fix JavaScript
initialization errors in production builds. Nuxt's generated modules
require inline script execution.

- Fixes: "Cannot access uninitialized variable" error
- Fixes: CSP script execution blocking
- Version bump to 0.1.2
2025-11-01 19:00:36 +01:00
f727d00639 Bump version to 0.1.1 2025-11-01 17:21:10 +01:00
a946b14f69 Fix Android assets upload to correct release
Use gh CLI to upload Android APK and AAB to the tagged release.
2025-11-01 17:20:13 +01:00
471baec284 Simplify Android build: use default command for APK and AAB
tauri android build creates both APK and AAB by default.
2025-11-01 16:44:32 +01:00
8298d807f3 Fix Android build commands: use --apk and --aab flags
Changed from incorrect --bundle aab to correct --aab flag.
2025-11-01 16:34:15 +01:00
42e6459fbf Prevent duplicate builds on tag pushes
Build workflow now ignores all tags to avoid running alongside release workflow.
2025-11-01 16:06:35 +01:00
6ae87fc694 Fix Android OpenSSL build by adding NDK toolchain to PATH
Set proper CC, AR, and RANLIB environment variables for all Android targets
to enable OpenSSL cross-compilation with SQLCipher encryption.
2025-11-01 16:03:46 +01:00
f7867a5bde Restore SQLCipher encryption for Android and fix CI build
- Re-enable bundled-sqlcipher-vendored-openssl for Android
- Add NDK environment variables for OpenSSL compilation
- Install perl and make for OpenSSL build in CI
- Ensures encryption works on all platforms including Android
2025-11-01 15:39:44 +01:00
d82599f588 Fix Android build by using platform-specific rusqlite features
- Use bundled-sqlcipher-vendored-openssl for non-Android platforms
- Use bundled (standard SQLite) for Android to avoid OpenSSL compilation issues
- Resolves OpenSSL build errors on Android targets
2025-11-01 15:36:20 +01:00
72bb211a76 Fix secrets access in workflow conditional
- Move secrets to env block instead of if condition
- Use bash conditional to check if keystore is available
- Provide clear logging for signed vs unsigned builds
2025-11-01 15:28:06 +01:00
f14ce0d6ad Add Android signing configuration to Gradle
- Configure signingConfigs to read from environment variables
- Apply signing to release builds when keystore is available
- Support both signed and unsigned builds
2025-11-01 15:26:21 +01:00
af09f4524d Remove iOS builds from CI/CD workflows 2025-11-01 15:21:58 +01:00
102832675d Fix Android build commands syntax
- Change from --apk to default build (produces APK)
- Change from --aab to --bundle aab for AAB generation
2025-11-01 15:20:49 +01:00
3490de2f51 Configure Android signing and disable iOS builds
- Add optional Android signing for build workflow (unsigned for testing)
- Require Android signing for release workflow
- Disable iOS builds (commented out) until Apple Developer Account is available
2025-11-01 15:06:56 +01:00
7c3af10938 Add Android and iOS builds to CI/CD pipelines 2025-11-01 15:00:33 +01:00
5c5d0785b9 Fix pnpm version conflict in CI workflows 2025-11-01 14:48:58 +01:00
121dd9dd00 Add GitHub Actions CI/CD pipelines
- Add build pipeline for Windows, macOS, and Linux
- Add release pipeline for automated releases
- Remove CLAUDE.md from git tracking
2025-11-01 14:46:01 +01:00
4ff6aee4d8 Fix Vue i18n warnings and component root node issues
- Set useScope: 'global' in UI store to prevent i18n scope conflicts
- Add wrapper div to vault page to ensure single root node for transitions
- Fixes 'Duplicate useI18n calling by local scope' warning
- Fixes 'Component inside <Transition> renders non-element root node' warning
2025-10-31 23:24:20 +01:00
dceb49ae90 Add context menu for vault actions and trash functionality
- Add UiButtonContext component for context menu support on buttons
- Implement vault trash functionality using trash crate
- Move vaults to system trash on desktop (with fallback to permanent delete on mobile)
- Add context menu to vault list items for better mobile UX
- Keep hover delete button for desktop users
2025-10-31 22:57:56 +01:00
5ea04a80e0 Fix Android safe-area handling and window maximization
- Fix extension signature verification on Android by canonicalizing paths (symlink compatibility)
- Implement proper safe-area-inset handling for mobile devices
- Add reactive header height measurement to UI store
- Fix maximized window positioning to respect safe-areas and header
- Create reusable HaexDebugOverlay component for mobile debugging
- Fix Swiper navigation by using absolute positioning instead of flex-1
- Remove debug logging after Android compatibility confirmed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 02:18:59 +01:00
65cf2e2c3c adjust gitignore 2025-10-30 22:01:31 +01:00
68d542b4d7 Update extension system and database migrations
Changes:
- Added CLAUDE.md with project instructions
- Updated extension manifest bindings (TypeScript)
- Regenerated database migrations (consolidated into single migration)
- Updated haex schema with table name handling
- Enhanced extension manager and manifest handling in Rust
- Updated extension store in frontend
- Updated vault.db

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 21:59:13 +01:00
f97cd4ad97 adjust drizzle backend.
return array of arrays
handle table names with quotes
2025-10-30 04:57:01 +01:00
55 changed files with 2538 additions and 2788 deletions

228
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,228 @@
name: Build
on:
push:
branches:
- main
- develop
tags-ignore:
- '**'
pull_request:
branches:
- main
- develop
workflow_dispatch:
jobs:
build-desktop:
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest'
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: ${{ matrix.args }}
- name: Upload artifacts (macOS)
if: matrix.platform == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: macos-${{ contains(matrix.args, 'aarch64') && 'aarch64' || 'x86_64' }}
path: |
src-tauri/target/*/release/bundle/dmg/*.dmg
src-tauri/target/*/release/bundle/macos/*.app
- name: Upload artifacts (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
uses: actions/upload-artifact@v4
with:
name: linux
path: |
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/appimage/*.AppImage
src-tauri/target/release/bundle/rpm/*.rpm
- name: Upload artifacts (Windows)
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows
path: |
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
build-android:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Rust Android targets
run: |
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
- name: Setup NDK
uses: nttld/setup-ndk@v1
with:
ndk-version: r26d
id: setup-ndk
- name: Setup Android NDK environment for OpenSSL
run: |
echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
echo "NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
# Add all Android toolchains to PATH for OpenSSL cross-compilation
echo "${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
# Set CC, AR, RANLIB for each target
echo "CC_aarch64_linux_android=aarch64-linux-android24-clang" >> $GITHUB_ENV
echo "AR_aarch64_linux_android=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_aarch64_linux_android=llvm-ranlib" >> $GITHUB_ENV
echo "CC_armv7_linux_androideabi=armv7a-linux-androideabi24-clang" >> $GITHUB_ENV
echo "AR_armv7_linux_androideabi=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_armv7_linux_androideabi=llvm-ranlib" >> $GITHUB_ENV
echo "CC_i686_linux_android=i686-linux-android24-clang" >> $GITHUB_ENV
echo "AR_i686_linux_android=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_i686_linux_android=llvm-ranlib" >> $GITHUB_ENV
echo "CC_x86_64_linux_android=x86_64-linux-android24-clang" >> $GITHUB_ENV
echo "AR_x86_64_linux_android=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_x86_64_linux_android=llvm-ranlib" >> $GITHUB_ENV
- name: Install build dependencies for OpenSSL
run: |
sudo apt-get update
sudo apt-get install -y perl make
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Setup Keystore (if secrets available)
env:
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
if [ -n "$ANDROID_KEYSTORE" ]; then
echo "$ANDROID_KEYSTORE" | base64 -d > $HOME/keystore.jks
echo "ANDROID_KEYSTORE_PATH=$HOME/keystore.jks" >> $GITHUB_ENV
echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> $GITHUB_ENV
echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> $GITHUB_ENV
echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> $GITHUB_ENV
echo "Keystore configured for signing"
else
echo "No keystore configured, building unsigned APK"
fi
- name: Build Android APK and AAB (unsigned if no keystore)
run: pnpm tauri android build
- name: Upload Android artifacts
uses: actions/upload-artifact@v4
with:
name: android
path: |
src-tauri/gen/android/app/build/outputs/apk/**/*.apk
src-tauri/gen/android/app/build/outputs/bundle/**/*.aab

251
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,251 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-22.04
outputs:
release_id: ${{ steps.create-release.outputs.release_id }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Get version
run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
- name: Create release
id: create-release
uses: actions/github-script@v7
with:
script: |
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${process.env.PACKAGE_VERSION}`,
name: `haex-hub v${process.env.PACKAGE_VERSION}`,
body: 'Take a look at the assets to download and install this app.',
draft: true,
prerelease: false
})
core.setOutput('release_id', data.id)
core.setOutput('upload_url', data.upload_url)
return data.id
build-desktop:
needs: create-release
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest'
args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Build and release Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}
build-android:
needs: create-release
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Rust Android targets
run: |
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
- name: Setup NDK
uses: nttld/setup-ndk@v1
with:
ndk-version: r26d
id: setup-ndk
- name: Setup Android NDK environment for OpenSSL
run: |
echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
echo "NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
# Add all Android toolchains to PATH for OpenSSL cross-compilation
echo "${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
# Set CC, AR, RANLIB for each target
echo "CC_aarch64_linux_android=aarch64-linux-android24-clang" >> $GITHUB_ENV
echo "AR_aarch64_linux_android=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_aarch64_linux_android=llvm-ranlib" >> $GITHUB_ENV
echo "CC_armv7_linux_androideabi=armv7a-linux-androideabi24-clang" >> $GITHUB_ENV
echo "AR_armv7_linux_androideabi=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_armv7_linux_androideabi=llvm-ranlib" >> $GITHUB_ENV
echo "CC_i686_linux_android=i686-linux-android24-clang" >> $GITHUB_ENV
echo "AR_i686_linux_android=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_i686_linux_android=llvm-ranlib" >> $GITHUB_ENV
echo "CC_x86_64_linux_android=x86_64-linux-android24-clang" >> $GITHUB_ENV
echo "AR_x86_64_linux_android=llvm-ar" >> $GITHUB_ENV
echo "RANLIB_x86_64_linux_android=llvm-ranlib" >> $GITHUB_ENV
- name: Install build dependencies for OpenSSL
run: |
sudo apt-get update
sudo apt-get install -y perl make
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Setup Keystore (required for release)
run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > $HOME/keystore.jks
echo "ANDROID_KEYSTORE_PATH=$HOME/keystore.jks" >> $GITHUB_ENV
echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> $GITHUB_ENV
echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> $GITHUB_ENV
- name: Build Android APK and AAB (signed)
run: pnpm tauri android build
- name: Upload Android artifacts to Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ github.ref_name }} \
src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk \
src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab \
--clobber
publish-release:
permissions:
contents: write
runs-on: ubuntu-22.04
needs: [create-release, build-desktop, build-android]
steps:
- name: Publish release
id: publish-release
uses: actions/github-script@v7
env:
release_id: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.release_id,
draft: false,
prerelease: false
})

2
.gitignore vendored
View File

@ -27,3 +27,5 @@ src-tauri/target
nogit* nogit*
.claude .claude
.output .output
target
CLAUDE.md

View File

@ -1,5 +1,3 @@
//import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
@ -31,7 +29,6 @@ export default defineNuxtConfig({
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxt/icon', '@nuxt/icon',
'@nuxt/eslint', '@nuxt/eslint',
//"@nuxt/image",
'@nuxt/fonts', '@nuxt/fonts',
'@nuxt/ui', '@nuxt/ui',
], ],
@ -125,7 +122,6 @@ export default defineNuxtConfig({
}, },
vite: { vite: {
//plugins: [tailwindcss()],
// Better support for Tauri CLI output // Better support for Tauri CLI output
clearScreen: false, clearScreen: false,
// Enable environment variables // Enable environment variables

View File

@ -1,10 +1,10 @@
{ {
"name": "haex-hub", "name": "haex-hub",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build && node scripts/fix-vite-mapdeps.js",
"dev": "nuxt dev", "dev": "nuxt dev",
"drizzle:generate": "drizzle-kit generate", "drizzle:generate": "drizzle-kit generate",
"drizzle:migrate": "drizzle-kit migrate", "drizzle:migrate": "drizzle-kit migrate",
@ -28,11 +28,9 @@
"@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",
"@tauri-apps/plugin-http": "2.5.2",
"@tauri-apps/plugin-notification": "2.3.1", "@tauri-apps/plugin-notification": "2.3.1",
"@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-opener": "^2.5.2",
"@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-sql": "2.3.0",
"@tauri-apps/plugin-store": "^2.4.1", "@tauri-apps/plugin-store": "^2.4.1",
"@vueuse/components": "^13.9.0", "@vueuse/components": "^13.9.0",
"@vueuse/core": "^13.9.0", "@vueuse/core": "^13.9.0",
@ -40,7 +38,6 @@
"@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.38.0",
"fuse.js": "^7.1.0",
"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.16",

688
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* Post-build script to fix the Vite 7.x TDZ error in __vite__mapDeps
* This script patches the generated JavaScript files after the build
*/
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
const NUXT_DIR = join(process.cwd(), '.output/public/_nuxt')
async function fixFile(filePath) {
const content = await readFile(filePath, 'utf-8')
const fixedContent = content.replace(
/const __vite__mapDeps=\(i,m=__vite__mapDeps,/g,
'let __vite__mapDeps;__vite__mapDeps=(i,m=__vite__mapDeps,'
)
if (fixedContent !== content) {
await writeFile(filePath, fixedContent, 'utf-8')
console.log(`✓ Fixed TDZ error in ${filePath.split('/').pop()}`)
return true
}
return false
}
async function main() {
try {
const files = await readdir(NUXT_DIR)
const jsFiles = files.filter((f) => f.endsWith('.js'))
let fixedCount = 0
for (const file of jsFiles) {
const filePath = join(NUXT_DIR, file)
const fixed = await fixFile(filePath)
if (fixed) fixedCount++
}
if (fixedCount > 0) {
console.log(`\n✓ Fixed __vite__mapDeps TDZ error in ${fixedCount} file(s)`)
} else {
console.log('\n✓ No __vite__mapDeps TDZ errors found')
}
} catch (error) {
console.error('Error fixing __vite__mapDeps:', error)
process.exit(1)
}
}
main()

1236
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "haex-hub" name = "haex-hub"
version = "0.1.0" version = "0.1.3"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
@ -20,14 +20,7 @@ tauri-build = { version = "2.2", features = [] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
[dependencies] [dependencies]
rusqlite = { version = "0.37.0", features = [ tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
"load_extension",
"bundled-sqlcipher-vendored-openssl",
"functions",
] }
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
#sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
base64 = "0.22" base64 = "0.22"
ed25519-dalek = "2.1" ed25519-dalek = "2.1"
fs_extra = "1.3.0" fs_extra = "1.3.0"
@ -54,3 +47,10 @@ uhlc = "0.8.2"
url = "2.5.7" url = "2.5.7"
uuid = { version = "1.18.1", features = ["v4"] } uuid = { version = "1.18.1", features = ["v4"] }
zip = "6.0.0" zip = "6.0.0"
[target.'cfg(not(target_os = "android"))'.dependencies]
trash = "5.2.0"
rusqlite = { version = "0.37.0", features = ["load_extension", "bundled-sqlcipher-vendored-openssl", "functions"] }
[target.'cfg(target_os = "android")'.dependencies]
rusqlite = { version = "0.37.0", features = ["load_extension", "bundled-sqlcipher-vendored-openssl", "functions"] }

View File

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, devServerUrl: string | null, }; export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, entry: string | null, singleInstance: boolean | null, devServerUrl: string | null, };

View File

@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExtensionPermissions } from "./ExtensionPermissions"; import type { ExtensionPermissions } from "./ExtensionPermissions";
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, }; export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, };

View File

@ -60,11 +60,12 @@ CREATE TABLE `haex_extensions` (
`version` text NOT NULL, `version` text NOT NULL,
`author` text, `author` text,
`description` text, `description` text,
`entry` text DEFAULT 'index.html' NOT NULL, `entry` text DEFAULT 'index.html',
`homepage` text, `homepage` text,
`enabled` integer DEFAULT true, `enabled` integer DEFAULT true,
`icon` text, `icon` text,
`signature` text NOT NULL, `signature` text NOT NULL,
`single_instance` integer DEFAULT false,
`haex_timestamp` text `haex_timestamp` text
); );
--> statement-breakpoint --> statement-breakpoint
@ -94,6 +95,7 @@ CREATE TABLE `haex_settings` (
CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint
CREATE TABLE `haex_workspaces` ( CREATE TABLE `haex_workspaces` (
`id` text PRIMARY KEY NOT NULL, `id` text PRIMARY KEY NOT NULL,
`device_id` text NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`position` integer DEFAULT 0 NOT NULL, `position` integer DEFAULT 0 NOT NULL,
`haex_timestamp` text `haex_timestamp` text

View File

@ -1 +0,0 @@
ALTER TABLE `haex_workspaces` ADD `device_id` text NOT NULL;

View File

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "bcdd9ad3-a87a-4a43-9eba-673f94b10287", "id": "8dc25226-70f9-4d2e-89d4-f3a6b2bdf58d",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"haex_crdt_configs": { "haex_crdt_configs": {
@ -411,7 +411,7 @@
"name": "entry", "name": "entry",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": "'index.html'" "default": "'index.html'"
}, },
@ -444,6 +444,14 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"single_instance": {
"name": "single_instance",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"haex_timestamp": { "haex_timestamp": {
"name": "haex_timestamp", "name": "haex_timestamp",
"type": "text", "type": "text",
@ -619,6 +627,13 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"device_id": {
"name": "device_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": { "name": {
"name": "name", "name": "name",
"type": "text", "type": "text",

View File

@ -1,677 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "27735348-b9c5-4bc6-9cc5-dd707ad689b9",
"prevId": "bcdd9ad3-a87a-4a43-9eba-673f94b10287",
"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_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": true,
"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
},
"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
},
"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_key_type_value_unique": {
"name": "haex_settings_key_type_value_unique",
"columns": [
"key",
"type",
"value"
],
"isUnique": true
}
},
"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
},
"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

@ -5,15 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1761430560028, "when": 1761821821609,
"tag": "0000_secret_ender_wiggin", "tag": "0000_dashing_night_nurse",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1761581351395,
"tag": "0001_late_the_renegades",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@ -47,11 +47,12 @@ export const haexExtensions = sqliteTable(
version: text().notNull(), version: text().notNull(),
author: text(), author: text(),
description: text(), description: text(),
entry: text().notNull().default('index.html'), entry: text().default('index.html'),
homepage: text(), homepage: text(),
enabled: integer({ mode: 'boolean' }).default(true), enabled: integer({ mode: 'boolean' }).default(true),
icon: text(), icon: text(),
signature: text().notNull(), signature: text().notNull(),
single_instance: integer({ mode: 'boolean' }).default(false),
}), }),
(table) => [ (table) => [
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren // UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren

Binary file not shown.

View File

@ -24,6 +24,23 @@ android {
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
} }
signingConfigs {
create("release") {
val keystorePath = System.getenv("ANDROID_KEYSTORE_PATH")
val keystorePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD")
val keyAlias = System.getenv("ANDROID_KEY_ALIAS")
val keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
if (keystorePath != null && keystorePassword != null && keyAlias != null && keyPassword != null) {
storeFile = file(keystorePath)
storePassword = keystorePassword
this.keyAlias = keyAlias
this.keyPassword = keyPassword
}
}
}
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true" manifestPlaceholders["usesCleartextTraffic"] = "true"
@ -43,6 +60,12 @@ android {
.plus(getDefaultProguardFile("proguard-android-optimize.txt")) .plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray() .toList().toTypedArray()
) )
// Sign with release config if available
val releaseSigningConfig = signingConfigs.getByName("release")
if (releaseSigningConfig.storeFile != null) {
signingConfig = releaseSigningConfig
}
} }
} }
kotlinOptions { kotlinOptions {

View File

@ -11,8 +11,6 @@ const INSERT_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_insert";
const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update"; const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
const DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete"; const DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete";
//const SYNC_ACTIVE_KEY: &str = "sync_active";
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp"; pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
/// Name der custom UUID-Generierungs-Funktion (registriert in database::core::open_and_init_db) /// Name der custom UUID-Generierungs-Funktion (registriert in database::core::open_and_init_db)
@ -85,7 +83,8 @@ impl ColumnInfo {
} }
fn is_safe_identifier(name: &str) -> bool { fn is_safe_identifier(name: &str) -> bool {
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') // Allow alphanumeric characters, underscores, and hyphens (for extension names like "nuxt-app")
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
} }
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein. /// Richtet CRDT-Trigger für eine einzelne Tabelle ein.

View File

@ -89,8 +89,15 @@ pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
/// Utility für SQL-Parsing - parst mehrere SQL-Statements /// Utility für SQL-Parsing - parst mehrere SQL-Statements
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> { pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
let dialect = SQLiteDialect {}; let dialect = SQLiteDialect {};
Parser::parse_sql(&dialect, sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(), // Normalize whitespace: replace multiple whitespaces (including newlines, tabs) with single space
let normalized_sql = sql
.split_whitespace()
.collect::<Vec<&str>>()
.join(" ");
Parser::parse_sql(&dialect, &normalized_sql).map_err(|e| DatabaseError::ParseError {
reason: format!("Failed to parse SQL: {}", e),
sql: sql.to_string(), sql: sql.to_string(),
}) })
} }

View File

@ -20,6 +20,8 @@ use std::time::UNIX_EPOCH;
use std::{fs, sync::Arc}; use std::{fs, sync::Arc};
use tauri::{path::BaseDirectory, AppHandle, Manager, State}; use tauri::{path::BaseDirectory, AppHandle, Manager, State};
use tauri_plugin_fs::FsExt; use tauri_plugin_fs::FsExt;
#[cfg(not(target_os = "android"))]
use trash;
use ts_rs::TS; use ts_rs::TS;
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>); pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
@ -133,7 +135,6 @@ pub fn get_vaults_directory(app_handle: &AppHandle) -> Result<String, DatabaseEr
Ok(vaults_dir.to_string_lossy().to_string()) Ok(vaults_dir.to_string_lossy().to_string())
} }
//#[serde(tag = "type", content = "details")]
#[derive(Debug, Serialize, Deserialize, TS)] #[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -212,7 +213,60 @@ pub fn vault_exists(app_handle: AppHandle, vault_name: String) -> Result<bool, D
Ok(Path::new(&vault_path).exists()) Ok(Path::new(&vault_path).exists())
} }
/// Deletes a vault database file /// Moves a vault database file to trash (or deletes permanently if trash is unavailable)
#[tauri::command]
pub fn move_vault_to_trash(
app_handle: AppHandle,
vault_name: String,
) -> Result<String, DatabaseError> {
// On Android, trash is not available, so delete permanently
#[cfg(target_os = "android")]
{
println!(
"Android platform detected, permanently deleting vault '{}'",
vault_name
);
return delete_vault(app_handle, vault_name);
}
// On non-Android platforms, try to use trash
#[cfg(not(target_os = "android"))]
{
let vault_path = get_vault_path(&app_handle, &vault_name)?;
let vault_shm_path = format!("{}-shm", vault_path);
let vault_wal_path = format!("{}-wal", vault_path);
if !Path::new(&vault_path).exists() {
return Err(DatabaseError::IoError {
path: vault_path,
reason: "Vault does not exist".to_string(),
});
}
// Try to move to trash first (works on desktop systems)
let moved_to_trash = trash::delete(&vault_path).is_ok();
if moved_to_trash {
// Also try to move auxiliary files to trash (ignore errors as they might not exist)
let _ = trash::delete(&vault_shm_path);
let _ = trash::delete(&vault_wal_path);
Ok(format!(
"Vault '{}' successfully moved to trash",
vault_name
))
} else {
// Fallback: Permanent deletion if trash fails
println!(
"Trash not available, falling back to permanent deletion for vault '{}'",
vault_name
);
delete_vault(app_handle, vault_name)
}
}
}
/// Deletes a vault database file permanently (bypasses trash)
#[tauri::command] #[tauri::command]
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> { pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
let vault_path = get_vault_path(&app_handle, &vault_name)?; let vault_path = get_vault_path(&app_handle, &vault_name)?;
@ -395,9 +449,6 @@ pub fn open_encrypted_database(
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<String, DatabaseError> { ) -> Result<String, DatabaseError> {
println!("Opening encrypted database vault_path: {}", vault_path); println!("Opening encrypted database vault_path: {}", vault_path);
// Vault-Pfad aus dem Namen ableiten
//let vault_path = get_vault_path(&app_handle, &vault_name)?;
println!("Resolved vault path: {}", vault_path); println!("Resolved vault path: {}", vault_path);
if !Path::new(&vault_path).exists() { if !Path::new(&vault_path).exists() {

View File

@ -66,17 +66,124 @@ impl ExtensionManager {
Self::default() Self::default()
} }
/// Helper function to validate path and check for path traversal
/// Returns the cleaned path if valid, or None if invalid/not found
/// If require_exists is true, returns None if path doesn't exist
pub fn validate_path_in_directory(
base_dir: &PathBuf,
relative_path: &str,
require_exists: bool,
) -> Result<Option<PathBuf>, ExtensionError> {
// Check for path traversal patterns
if relative_path.contains("..") {
return Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal attempt: {}", relative_path),
});
}
// Clean the path (same logic as in protocol.rs)
let clean_path = relative_path
.replace('\\', "/")
.trim_start_matches('/')
.split('/')
.filter(|&part| !part.is_empty() && part != "." && part != "..")
.collect::<PathBuf>();
let full_path = base_dir.join(&clean_path);
// Check if file/directory exists (if required)
if require_exists && !full_path.exists() {
return Ok(None);
}
// Verify path is within base directory
let canonical_base = base_dir
.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
if let Ok(canonical_path) = full_path.canonicalize() {
if !canonical_path.starts_with(&canonical_base) {
return Err(ExtensionError::SecurityViolation {
reason: format!("Path outside base directory: {}", relative_path),
});
}
Ok(Some(canonical_path))
} else {
// Path doesn't exist yet - still validate it would be within base
if full_path.starts_with(&canonical_base) {
Ok(Some(full_path))
} else {
Err(ExtensionError::SecurityViolation {
reason: format!("Path outside base directory: {}", relative_path),
})
}
}
}
/// Validates icon path and falls back to favicon.ico if not specified
fn validate_and_resolve_icon_path(
extension_dir: &PathBuf,
haextension_dir: &str,
icon_path: Option<&str>,
) -> Result<Option<String>, ExtensionError> {
// If icon is specified in manifest, validate it
if let Some(icon) = icon_path {
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, icon, true)? {
return Ok(Some(clean_path.to_string_lossy().to_string()));
} else {
eprintln!("WARNING: Icon path specified in manifest not found: {}", icon);
// Continue to fallback logic
}
}
// Fallback 1: Check haextension/favicon.ico
let haextension_favicon = format!("{}/favicon.ico", haextension_dir);
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)? {
return Ok(Some(clean_path.to_string_lossy().to_string()));
}
// Fallback 2: Check public/favicon.ico
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)? {
return Ok(Some(clean_path.to_string_lossy().to_string()));
}
// No icon found
Ok(None)
}
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest /// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
fn extract_and_validate_extension( fn extract_and_validate_extension(
bytes: Vec<u8>, bytes: Vec<u8>,
temp_prefix: &str, temp_prefix: &str,
app_handle: &AppHandle,
) -> Result<ExtractedExtension, ExtensionError> { ) -> Result<ExtractedExtension, ExtensionError> {
let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4())); // Use app_cache_dir for better Android compatibility
let cache_dir = app_handle
.path()
.app_cache_dir()
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot get app cache dir: {}", e),
})?;
let temp_id = uuid::Uuid::new_v4();
let temp = cache_dir.join(format!("{}_{}", temp_prefix, temp_id));
let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp"));
// Write bytes to a temporary ZIP file first (important for Android file system)
fs::write(&zip_file_path, &bytes).map_err(|e| {
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
})?;
// Create extraction directory
fs::create_dir_all(&temp) fs::create_dir_all(&temp)
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?; .map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| { // Open ZIP file from disk (more reliable on Android than from memory)
let zip_file = fs::File::open(&zip_file_path).map_err(|e| {
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
})?;
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
ExtensionError::InstallationFailed { ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e), reason: format!("Invalid ZIP: {}", e),
} }
@ -88,6 +195,9 @@ impl ExtensionManager {
reason: format!("Cannot extract ZIP: {}", e), reason: format!("Cannot extract ZIP: {}", e),
})?; })?;
// Clean up temporary ZIP file
let _ = fs::remove_file(&zip_file_path);
// Read haextension_dir from config if it exists, otherwise use default // Read haextension_dir from config if it exists, otherwise use default
let config_path = temp.join("haextension.config.json"); let config_path = temp.join("haextension.config.json");
let haextension_dir = if config_path.exists() { let haextension_dir = if config_path.exists() {
@ -108,42 +218,17 @@ impl ExtensionManager {
.unwrap_or("haextension") .unwrap_or("haextension")
.to_string(); .to_string();
// Security: Validate that haextension_dir doesn't contain ".." for path traversal
if dir.contains("..") {
return Err(ExtensionError::ManifestError {
reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(),
});
}
dir dir
} else { } else {
"haextension".to_string() "haextension".to_string()
}; };
// Build the manifest path // Validate manifest path using helper function
let manifest_path = temp.join(&haextension_dir).join("manifest.json"); let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)?
// Ensure the resolved path is still within temp directory (safety check against path traversal) .ok_or_else(|| ExtensionError::ManifestError {
let canonical_temp = temp.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// Only check if manifest_path parent exists to avoid errors
if let Some(parent) = manifest_path.parent() {
if let Ok(canonical_manifest_dir) = parent.canonicalize() {
if !canonical_manifest_dir.starts_with(&canonical_temp) {
return Err(ExtensionError::ManifestError {
reason: "Security violation: manifest path outside extension directory".to_string(),
});
}
}
}
// Check if manifest exists
if !manifest_path.exists() {
return Err(ExtensionError::ManifestError {
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir), reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
}); })?;
}
let actual_dir = temp.clone(); let actual_dir = temp.clone();
let manifest_content = let manifest_content =
@ -151,7 +236,11 @@ impl ExtensionManager {
reason: format!("Cannot read manifest: {}", e), reason: format!("Cannot read manifest: {}", e),
})?; })?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// Validate and resolve icon path with fallback logic
let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?;
manifest.icon = validated_icon;
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| { let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
ExtensionError::SignatureVerificationFailed { ExtensionError::SignatureVerificationFailed {
@ -427,9 +516,10 @@ impl ExtensionManager {
pub async fn preview_extension_internal( pub async fn preview_extension_internal(
&self, &self,
app_handle: &AppHandle,
file_bytes: Vec<u8>, file_bytes: Vec<u8>,
) -> Result<ExtensionPreview, ExtensionError> { ) -> Result<ExtensionPreview, ExtensionError> {
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?; let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
let is_valid_signature = ExtensionCrypto::verify_signature( let is_valid_signature = ExtensionCrypto::verify_signature(
&extracted.manifest.public_key, &extracted.manifest.public_key,
@ -454,7 +544,7 @@ impl ExtensionManager {
custom_permissions: EditablePermissions, custom_permissions: EditablePermissions,
state: &State<'_, AppState>, state: &State<'_, AppState>,
) -> Result<String, ExtensionError> { ) -> Result<String, ExtensionError> {
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?; let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft) // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
ExtensionCrypto::verify_signature( ExtensionCrypto::verify_signature(
@ -525,7 +615,7 @@ impl ExtensionManager {
// 1. Extension-Eintrag erstellen mit generierter UUID // 1. Extension-Eintrag erstellen mit generierter UUID
let insert_ext_sql = format!( let insert_ext_sql = format!(
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
TABLE_EXTENSIONS TABLE_EXTENSIONS
); );
@ -545,6 +635,7 @@ impl ExtensionManager {
extracted.manifest.homepage, extracted.manifest.homepage,
extracted.manifest.description, extracted.manifest.description,
true, // enabled true, // enabled
extracted.manifest.single_instance.unwrap_or(false),
], ],
)?; )?;
@ -623,7 +714,7 @@ impl ExtensionManager {
// Lade alle Daten aus der Datenbank // Lade alle Daten aus der Datenbank
let extensions = with_connection(&state.db, |conn| { let extensions = with_connection(&state.db, |conn| {
let sql = format!( let sql = format!(
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}", "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}",
TABLE_EXTENSIONS TABLE_EXTENSIONS
); );
eprintln!("DEBUG: SQL Query before transformation: {}", sql); eprintln!("DEBUG: SQL Query before transformation: {}", sql);
@ -655,13 +746,16 @@ impl ExtensionManager {
})? })?
.to_string(), .to_string(),
author: row[3].as_str().map(String::from), author: row[3].as_str().map(String::from),
entry: row[4].as_str().unwrap_or("index.html").to_string(), entry: row[4].as_str().map(String::from),
icon: row[5].as_str().map(String::from), icon: row[5].as_str().map(String::from),
public_key: row[6].as_str().unwrap_or("").to_string(), public_key: row[6].as_str().unwrap_or("").to_string(),
signature: row[7].as_str().unwrap_or("").to_string(), signature: row[7].as_str().unwrap_or("").to_string(),
permissions: ExtensionPermissions::default(), permissions: ExtensionPermissions::default(),
homepage: row[8].as_str().map(String::from), homepage: row[8].as_str().map(String::from),
description: row[9].as_str().map(String::from), description: row[9].as_str().map(String::from),
single_instance: row[11]
.as_bool()
.or_else(|| row[11].as_i64().map(|v| v != 0)),
}; };
let enabled = row[10] let enabled = row[10]
@ -722,33 +816,12 @@ impl ExtensionManager {
Ok(config_content) => { Ok(config_content) => {
match serde_json::from_str::<serde_json::Value>(&config_content) { match serde_json::from_str::<serde_json::Value>(&config_content) {
Ok(config) => { Ok(config) => {
let dir = config config
.get("dev") .get("dev")
.and_then(|dev| dev.get("haextension_dir")) .and_then(|dev| dev.get("haextension_dir"))
.and_then(|dir| dir.as_str()) .and_then(|dir| dir.as_str())
.unwrap_or("haextension") .unwrap_or("haextension")
.to_string(); .to_string()
// Security: Validate that haextension_dir doesn't contain ".."
if dir.contains("..") {
eprintln!(
"DEBUG: Invalid haextension_dir for: {}, contains '..'",
extension_id
);
self.missing_extensions
.lock()
.map_err(|e| ExtensionError::MutexPoisoned {
reason: e.to_string(),
})?
.push(MissingExtension {
id: extension_id.clone(),
public_key: extension_data.manifest.public_key.clone(),
name: extension_data.manifest.name.clone(),
version: extension_data.manifest.version.clone(),
});
continue;
}
dir
} }
Err(_) => "haextension".to_string(), Err(_) => "haextension".to_string(),
} }
@ -759,12 +832,14 @@ impl ExtensionManager {
"haextension".to_string() "haextension".to_string()
}; };
// Check if manifest.json exists in the haextension_dir // Validate manifest.json path using helper function
let manifest_path = extension_path.join(&haextension_dir).join("manifest.json"); let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
if !manifest_path.exists() { if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)?
.is_none()
{
eprintln!( eprintln!(
"DEBUG: manifest.json missing for: {} at {:?}", "DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json",
extension_id, manifest_path extension_id, haextension_dir
); );
self.missing_extensions self.missing_extensions
.lock() .lock()

View File

@ -57,13 +57,20 @@ pub struct ExtensionManifest {
pub name: String, pub name: String,
pub version: String, pub version: String,
pub author: Option<String>, pub author: Option<String>,
pub entry: String, #[serde(default = "default_entry_value")]
pub entry: Option<String>,
pub icon: Option<String>, pub icon: Option<String>,
pub public_key: String, pub public_key: String,
pub signature: String, pub signature: String,
pub permissions: ExtensionPermissions, pub permissions: ExtensionPermissions,
pub homepage: Option<String>, pub homepage: Option<String>,
pub description: Option<String>, pub description: Option<String>,
#[serde(default)]
pub single_instance: Option<bool>,
}
fn default_entry_value() -> Option<String> {
Some("index.html".to_string())
} }
impl ExtensionManifest { impl ExtensionManifest {
@ -172,6 +179,8 @@ pub struct ExtensionInfoResponse {
pub description: Option<String>, pub description: Option<String>,
pub homepage: Option<String>, pub homepage: Option<String>,
pub icon: Option<String>, pub icon: Option<String>,
pub entry: Option<String>,
pub single_instance: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub dev_server_url: Option<String>, pub dev_server_url: Option<String>,
} }
@ -197,6 +206,8 @@ impl ExtensionInfoResponse {
description: extension.manifest.description.clone(), description: extension.manifest.description.clone(),
homepage: extension.manifest.homepage.clone(), homepage: extension.manifest.homepage.clone(),
icon: extension.manifest.icon.clone(), icon: extension.manifest.icon.clone(),
entry: extension.manifest.entry.clone(),
single_instance: extension.manifest.single_instance,
dev_server_url, dev_server_url,
}) })
} }

View File

@ -48,7 +48,9 @@ impl ExtensionCrypto {
let relative = path.strip_prefix(dir) let relative = path.strip_prefix(dir)
.unwrap_or(&path) .unwrap_or(&path)
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string()
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
.replace('\\', "/");
(relative, path) (relative, path)
}) })
.collect(); .collect();
@ -56,16 +58,30 @@ impl ExtensionCrypto {
// 3. Sortiere nach relativen Pfaden // 3. Sortiere nach relativen Pfaden
relative_files.sort_by(|a, b| a.0.cmp(&b.0)); relative_files.sort_by(|a, b| a.0.cmp(&b.0));
println!("=== Files to hash ({}): ===", relative_files.len());
for (rel, _) in &relative_files {
println!(" - {}", rel);
}
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
// Canonicalize manifest path for comparison (important on Android where symlinks may differ)
// Also ensure the canonical path is still within the allowed directory (security check)
let canonical_manifest_path = manifest_path.canonicalize()
.unwrap_or_else(|_| manifest_path.to_path_buf());
// Security: Verify canonical manifest path is still within dir
let canonical_dir = dir.canonicalize()
.unwrap_or_else(|_| dir.to_path_buf());
if !canonical_manifest_path.starts_with(&canonical_dir) {
return Err(ExtensionError::ManifestError {
reason: format!("Manifest path resolves outside of extension directory (potential path traversal)"),
});
}
// 4. Inhalte der sortierten Dateien hashen // 4. Inhalte der sortierten Dateien hashen
for (_relative, file_path) in relative_files { for (_relative, file_path) in relative_files {
if file_path == manifest_path { // Canonicalize file_path for comparison
let canonical_file_path = file_path.canonicalize()
.unwrap_or_else(|_| file_path.clone());
if canonical_file_path == canonical_manifest_path {
// FÜR DIE MANIFEST.JSON: // FÜR DIE MANIFEST.JSON:
let content_str = fs::read_to_string(&file_path) let content_str = fs::read_to_string(&file_path)
.map_err(|e| ExtensionError::Filesystem { source: e })?; .map_err(|e| ExtensionError::Filesystem { source: e })?;
@ -94,8 +110,12 @@ impl ExtensionCrypto {
reason: format!("Failed to serialize manifest: {}", e), reason: format!("Failed to serialize manifest: {}", e),
} }
})?; })?;
println!("canonical_manifest_content: {}", canonical_manifest_content);
hasher.update(canonical_manifest_content.as_bytes()); // Normalisiere Zeilenenden zu Unix-Style (\n), wie Node.js JSON.stringify es macht
// Dies ist wichtig für plattformübergreifende Konsistenz (Desktop vs Android)
let normalized_content = canonical_manifest_content.replace("\r\n", "\n");
hasher.update(normalized_content.as_bytes());
} else { } else {
// FÜR ALLE ANDEREN DATEIEN: // FÜR ALLE ANDEREN DATEIEN:
let content = let content =

View File

@ -64,7 +64,13 @@ impl SqlExecutor {
// Trigger-Logik für CREATE TABLE // Trigger-Logik für CREATE TABLE
if let Statement::CreateTable(create_table_details) = statement { if let Statement::CreateTable(create_table_details) = statement {
let table_name_str = create_table_details.name.to_string(); let raw_name = create_table_details.name.to_string();
// Remove quotes from table name
let table_name_str = raw_name
.trim_matches('"')
.trim_matches('`')
.to_string();
eprintln!("DEBUG: Setting up triggers for table: {}", table_name_str);
trigger::setup_triggers_for_table(tx, &table_name_str, false)?; trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
} }
@ -158,7 +164,13 @@ impl SqlExecutor {
// Trigger-Logik für CREATE TABLE // Trigger-Logik für CREATE TABLE
if let Statement::CreateTable(create_table_details) = statement { if let Statement::CreateTable(create_table_details) = statement {
let table_name_str = create_table_details.name.to_string(); let raw_name = create_table_details.name.to_string();
// Remove quotes from table name
let table_name_str = raw_name
.trim_matches('"')
.trim_matches('`')
.to_string();
eprintln!("DEBUG: Setting up triggers for table (RETURNING): {}", table_name_str);
trigger::setup_triggers_for_table(tx, &table_name_str, false)?; trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
} }

View File

@ -5,6 +5,7 @@ use crate::crdt::transformer::CrdtTransformer;
use crate::crdt::trigger; use crate::crdt::trigger;
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter}; use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
use crate::database::error::DatabaseError; use crate::database::error::DatabaseError;
use crate::extension::database::executor::SqlExecutor;
use crate::extension::error::ExtensionError; use crate::extension::error::ExtensionError;
use crate::extension::permissions::validator::SqlPermissionValidator; use crate::extension::permissions::validator::SqlPermissionValidator;
use crate::AppState; use crate::AppState;
@ -110,7 +111,7 @@ pub async fn extension_sql_execute(
public_key: String, public_key: String,
name: String, name: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<String>, ExtensionError> { ) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
// Get extension to retrieve its ID // Get extension to retrieve its ID
let extension = state let extension = state
.extension_manager .extension_manager
@ -129,58 +130,87 @@ pub async fn extension_sql_execute(
// SQL parsing // SQL parsing
let mut ast_vec = parse_sql_statements(sql)?; let mut ast_vec = parse_sql_statements(sql)?;
if ast_vec.len() != 1 {
return Err(ExtensionError::Database {
source: DatabaseError::ExecutionError {
sql: sql.to_string(),
reason: "extension_sql_execute should only receive a single SQL statement"
.to_string(),
table: None,
},
});
}
let mut statement = ast_vec.pop().unwrap();
// Check if statement has RETURNING clause
let has_returning = crate::database::core::statement_has_returning(&statement);
// Database operation // Database operation
with_connection(&state.db, |conn| { with_connection(&state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?; let tx = conn.transaction().map_err(DatabaseError::from)?;
let transformer = CrdtTransformer::new(); let transformer = CrdtTransformer::new();
let executor = StatementExecutor::new(&tx);
// Get HLC service reference
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(),
})?;
// Generate HLC timestamp // Generate HLC timestamp
let hlc_timestamp = state let hlc_timestamp = hlc_service
.hlc
.lock()
.unwrap()
.new_timestamp_and_persist(&tx) .new_timestamp_and_persist(&tx)
.map_err(|e| DatabaseError::HlcError { .map_err(|e| DatabaseError::HlcError {
reason: e.to_string(), reason: e.to_string(),
})?; })?;
// Transform statements // Transform statement
let mut modified_schema_tables = HashSet::new(); transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
for statement in &mut ast_vec {
if let Some(table_name) =
transformer.transform_execute_statement(statement, &hlc_timestamp)?
{
modified_schema_tables.insert(table_name);
}
}
// Convert parameters // Convert parameters to references
let sql_values = ValueConverter::convert_params(&params)?; let sql_values = ValueConverter::convert_params(&params)?;
let param_refs: Vec<&dyn rusqlite::ToSql> = sql_values.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
// Execute statements let result = if has_returning {
for statement in ast_vec { // Use query_internal for statements with RETURNING
executor.execute_statement_with_params(&statement, &sql_values)?; let (_, rows) = SqlExecutor::query_internal_typed(&tx, &hlc_service, &statement.to_string(), &param_refs)?;
rows
} else {
// Use execute_internal for statements without RETURNING
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &statement.to_string(), &param_refs)?;
vec![]
};
if let Statement::CreateTable(create_table_details) = statement { // Handle CREATE TABLE trigger setup
let table_name_str = create_table_details.name.to_string(); if let Statement::CreateTable(ref create_table_details) = statement {
println!( // Extract table name and remove quotes (both " and `)
"Table '{}' created by extension, setting up CRDT triggers...", let raw_name = create_table_details.name.to_string();
table_name_str println!("DEBUG: Raw table name from AST: {:?}", raw_name);
); println!("DEBUG: Raw table name chars: {:?}", raw_name.chars().collect::<Vec<_>>());
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
println!( let table_name_str = raw_name
"Triggers for table '{}' successfully created.", .trim_matches('"')
table_name_str .trim_matches('`')
); .to_string();
}
println!("DEBUG: Cleaned table name: {:?}", table_name_str);
println!("DEBUG: Cleaned table name chars: {:?}", table_name_str.chars().collect::<Vec<_>>());
println!(
"Table '{}' created by extension, setting up CRDT triggers...",
table_name_str
);
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
println!(
"Triggers for table '{}' successfully created.",
table_name_str
);
} }
// Commit transaction // Commit transaction
tx.commit().map_err(DatabaseError::from)?; tx.commit().map_err(DatabaseError::from)?;
Ok(modified_schema_tables.into_iter().collect()) Ok(result)
}) })
.map_err(ExtensionError::from) .map_err(ExtensionError::from)
} }
@ -192,7 +222,7 @@ pub async fn extension_sql_select(
public_key: String, public_key: String,
name: String, name: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<JsonValue>, ExtensionError> { ) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
// Get extension to retrieve its ID // Get extension to retrieve its ID
let extension = state let extension = state
.extension_manager .extension_manager
@ -229,10 +259,9 @@ pub async fn extension_sql_select(
} }
} }
// Database operation // Database operation - return Vec<Vec<JsonValue>> like sql_select_with_crdt
with_connection(&state.db, |conn| { with_connection(&state.db, |conn| {
let sql_params = ValueConverter::convert_params(&params)?; let sql_params = ValueConverter::convert_params(&params)?;
// Hard Delete: Keine SELECT-Transformation mehr nötig
let stmt_to_execute = ast_vec.pop().unwrap(); let stmt_to_execute = ast_vec.pop().unwrap();
let transformed_sql = stmt_to_execute.to_string(); let transformed_sql = stmt_to_execute.to_string();
@ -245,51 +274,34 @@ pub async fn extension_sql_select(
table: None, table: None,
})?; })?;
let column_names: Vec<String> = prepared_stmt let num_columns = prepared_stmt.column_count();
.column_names() let mut rows = prepared_stmt
.into_iter() .query(params_from_iter(sql_params.iter()))
.map(|s| s.to_string())
.collect();
let rows = prepared_stmt
.query_map(params_from_iter(sql_params.iter()), |row| {
row_to_json_value(row, &column_names)
})
.map_err(|e| DatabaseError::QueryError { .map_err(|e| DatabaseError::QueryError {
reason: e.to_string(), reason: e.to_string(),
})?; })?;
let mut results = Vec::new(); let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
for row_result in rows {
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError { while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
reason: e.to_string(), reason: e.to_string(),
})?); })? {
let mut row_values: Vec<JsonValue> = Vec::new();
for i in 0..num_columns {
let value_ref = row.get_ref(i).map_err(|e| DatabaseError::QueryError {
reason: e.to_string(),
})?;
let json_value = crate::database::core::convert_value_ref_to_json(value_ref)?;
row_values.push(json_value);
}
result_vec.push(row_values);
} }
Ok(results) Ok(result_vec)
}) })
.map_err(ExtensionError::from) .map_err(ExtensionError::from)
} }
/// Konvertiert eine SQLite-Zeile zu JSON
fn row_to_json_value(
row: &rusqlite::Row,
columns: &[String],
) -> Result<JsonValue, rusqlite::Error> {
let mut map = serde_json::Map::new();
for (i, col_name) in columns.iter().enumerate() {
let value = row.get::<usize, rusqlite::types::Value>(i)?;
let json_value = match value {
rusqlite::types::Value::Null => JsonValue::Null,
rusqlite::types::Value::Integer(i) => json!(i),
rusqlite::types::Value::Real(f) => json!(f),
rusqlite::types::Value::Text(s) => json!(s),
rusqlite::types::Value::Blob(blob) => json!(blob.to_vec()),
};
map.insert(col_name.clone(), json_value);
}
Ok(JsonValue::Object(map))
}
/// Validiert Parameter gegen SQL-Platzhalter /// Validiert Parameter gegen SQL-Platzhalter
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> { fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {

View File

@ -1,7 +1,7 @@
/// src-tauri/src/extension/mod.rs /// src-tauri/src/extension/mod.rs
use crate::{ use crate::{
extension::{ extension::{
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview}, core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
error::ExtensionError, error::ExtensionError,
}, },
AppState, AppState,
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
#[tauri::command] #[tauri::command]
pub async fn preview_extension( pub async fn preview_extension(
app_handle: AppHandle,
state: State<'_, AppState>, state: State<'_, AppState>,
file_bytes: Vec<u8>, file_bytes: Vec<u8>,
) -> Result<ExtensionPreview, ExtensionError> { ) -> Result<ExtensionPreview, ExtensionError> {
state state
.extension_manager .extension_manager
.preview_extension_internal(file_bytes) .preview_extension_internal(&app_handle, file_bytes)
.await .await
} }
@ -320,20 +321,19 @@ pub async fn load_dev_extension(
} }
eprintln!("✅ Dev server is reachable"); eprintln!("✅ Dev server is reachable");
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json // 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
let manifest_path = extension_path_buf let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
.join(&haextension_dir) let manifest_path = ExtensionManager::validate_path_in_directory(
.join("manifest.json"); &extension_path_buf,
&manifest_relative_path,
// Check if manifest exists true,
if !manifest_path.exists() { )?
return Err(ExtensionError::ManifestError { .ok_or_else(|| ExtensionError::ManifestError {
reason: format!( reason: format!(
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.", "Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
manifest_path.display() haextension_dir
), ),
}); })?;
}
// 3. Read and parse manifest // 3. Read and parse manifest
let manifest_content = let manifest_content =
@ -343,9 +343,8 @@ pub async fn load_dev_extension(
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name> // 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
let key_prefix = manifest.public_key.chars().take(8).collect::<String>(); let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
// 5. Check if dev extension already exists (allow reload) // 5. Check if dev extension already exists (allow reload)
if let Some(existing) = state if let Some(existing) = state

View File

@ -197,6 +197,30 @@ impl PermissionManager {
action: Action, action: Action,
table_name: &str, table_name: &str,
) -> Result<(), ExtensionError> { ) -> Result<(), ExtensionError> {
// Remove quotes from table name if present (from SDK's getTableName())
let clean_table_name = table_name.trim_matches('"');
// Auto-allow: Extensions have full access to their own tables
// Table format: {publicKey}__{extensionName}__{tableName}
// Extension ID format: dev_{publicKey}_{extensionName} or {publicKey}_{extensionName}
// Get the extension to check if this is its own table
let extension = app_state
.extension_manager
.get_extension(extension_id)
.ok_or_else(|| ExtensionError::ValidationError {
reason: format!("Extension with ID {} not found", extension_id),
})?;
// Build expected table prefix: {publicKey}__{extensionName}__
let expected_prefix = format!("{}__{}__", extension.manifest.public_key, extension.manifest.name);
if clean_table_name.starts_with(&expected_prefix) {
// This is the extension's own table - auto-allow
return Ok(());
}
// Not own table - check explicit permissions
let permissions = Self::get_permissions(app_state, extension_id).await?; let permissions = Self::get_permissions(app_state, extension_id).await?;
let has_permission = permissions let has_permission = permissions
@ -205,7 +229,7 @@ impl PermissionManager {
.filter(|perm| perm.resource_type == ResourceType::Db) .filter(|perm| perm.resource_type == ResourceType::Db)
.filter(|perm| perm.action == action) // action ist nicht mehr Option .filter(|perm| perm.action == action) // action ist nicht mehr Option
.any(|perm| { .any(|perm| {
if perm.target != "*" && perm.target != table_name { if perm.target != "*" && perm.target != clean_table_name {
return false; return false;
} }
true true

View File

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

View File

@ -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.0", "version": "0.1.3",
"identifier": "space.haex.hub", "identifier": "space.haex.hub",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
@ -25,7 +25,8 @@
"'self'", "'self'",
"http://tauri.localhost", "http://tauri.localhost",
"haex-extension:", "haex-extension:",
"'wasm-unsafe-eval'" "'wasm-unsafe-eval'",
"'unsafe-inline'"
], ],
"style-src": [ "style-src": [
"'self'", "'self'",

View File

@ -3,6 +3,8 @@ export default defineAppConfig({
colors: { colors: {
primary: 'sky', primary: 'sky',
secondary: 'fuchsia', secondary: 'fuchsia',
warning: 'yellow',
danger: 'red',
}, },
}, },
}) })

View File

@ -14,18 +14,44 @@
@apply cursor-not-allowed; @apply cursor-not-allowed;
} }
/* Define safe-area-insets as CSS custom properties for JavaScript access */
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-inset-left: env(safe-area-inset-left, 0px);
--safe-area-inset-right: env(safe-area-inset-right, 0px);
}
/* Verhindere Scrolling auf html und body */
html {
overflow: hidden;
margin: 0;
padding: 0;
height: 100dvh;
height: 100vh; /* Fallback */
width: 100%;
}
body {
overflow: hidden;
margin: 0;
height: 100%;
width: 100%;
padding: 0;
}
#__nuxt { #__nuxt {
/* Stellt sicher, dass die App immer die volle Höhe hat */ /* Volle Höhe des body */
min-height: 100vh; height: 100%;
width: 100%;
/* Sorgt dafür, dass Padding die Höhe nicht sprengt */ /* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
@apply box-border; padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Die Safe-Area Paddings */ box-sizing: border-box;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
} }
} }

View File

@ -0,0 +1,61 @@
<template>
<div
v-if="data"
class="fixed top-2 right-2 bg-black/90 text-white text-xs p-3 rounded-lg shadow-2xl max-w-sm z-[9999] backdrop-blur-sm"
>
<div class="flex justify-between items-start gap-3 mb-2">
<span class="font-bold text-sm">{{ title }}</span>
<div class="flex gap-1">
<button
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
@click="copyToClipboardAsync"
>
Copy
</button>
<button
v-if="dismissible"
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
@click="handleDismiss"
>
</button>
</div>
</div>
<pre class="text-xs whitespace-pre-wrap font-mono overflow-auto max-h-96">{{ formattedData }}</pre>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
data: Record<string, any> | null
title?: string
dismissible?: boolean
}>(),
{
title: 'Debug Info',
dismissible: false,
},
)
const emit = defineEmits<{
dismiss: []
}>()
const formattedData = computed(() => {
if (!props.data) return ''
return JSON.stringify(props.data, null, 2)
})
const copyToClipboardAsync = async () => {
try {
await navigator.clipboard.writeText(formattedData.value)
} catch (err) {
console.error('Failed to copy debug info:', err)
}
}
const handleDismiss = () => {
emit('dismiss')
}
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
ref="desktopEl" ref="desktopEl"
class="w-full h-full relative overflow-hidden" class="absolute inset-0 overflow-hidden"
> >
<Swiper <Swiper
:modules="[SwiperNavigation]" :modules="[SwiperNavigation]"
@ -13,7 +13,7 @@
:no-swiping="true" :no-swiping="true"
no-swiping-class="no-swipe" no-swiping-class="no-swipe"
:allow-touch-move="allowSwipe" :allow-touch-move="allowSwipe"
class="w-full h-full" class="h-full w-full"
direction="vertical" direction="vertical"
@swiper="onSwiperInit" @swiper="onSwiperInit"
@slide-change="onSlideChange" @slide-change="onSlideChange"
@ -115,6 +115,14 @@
:source-height="window.sourceHeight" :source-height="window.sourceHeight"
:is-opening="window.isOpening" :is-opening="window.isOpening"
:is-closing="window.isClosing" :is-closing="window.isClosing"
:warning-level="
window.type === 'extension' &&
availableExtensions.find(
(ext) => ext.id === window.sourceId,
)?.devServerUrl
? 'warning'
: undefined
"
class="no-swipe" class="no-swipe"
@close="windowManager.closeWindow(window.id)" @close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)" @minimize="windowManager.minimizeWindow(window.id)"
@ -164,6 +172,13 @@
:source-height="window.sourceHeight" :source-height="window.sourceHeight"
:is-opening="window.isOpening" :is-opening="window.isOpening"
:is-closing="window.isClosing" :is-closing="window.isClosing"
:warning-level="
window.type === 'extension' &&
availableExtensions.find((ext) => ext.id === window.sourceId)
?.devServerUrl
? 'warning'
: undefined
"
class="no-swipe" class="no-swipe"
@close="windowManager.closeWindow(window.id)" @close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)" @minimize="windowManager.minimizeWindow(window.id)"

View File

@ -15,7 +15,7 @@
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div <div
v-if="preview?.manifest.icon" v-if="preview?.manifest.icon"
class="w-16 h-16 flex-shrink-0" class="w-16 h-16 shrink-0"
> >
<UIcon <UIcon
:name="preview.manifest.icon" :name="preview.manifest.icon"
@ -184,7 +184,6 @@ const shellPermissions = computed({
}, },
}) })
const permissionAccordionItems = computed(() => { const permissionAccordionItems = computed(() => {
const items = [] const items = []

View File

@ -1,5 +1,13 @@
<template> <template>
<UPopover v-model:open="open"> <UDrawer
v-model:open="open"
direction="right"
:title="t('launcher.title')"
:description="t('launcher.description')"
:ui="{
content: 'w-dvw max-w-md sm:max-w-fit',
}"
>
<UButton <UButton
icon="material-symbols:apps" icon="material-symbols:apps"
color="neutral" color="neutral"
@ -9,58 +17,64 @@
/> />
<template #content> <template #content>
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll"> <div class="p-4 h-full overflow-y-auto">
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) --> <div class="flex flex-wrap">
<UContextMenu <!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
v-for="item in launcherItems" <UContextMenu
:key="item.id" v-for="item in launcherItems"
:items="getContextMenuItems(item)" :key="item.id"
> :items="getContextMenuItems(item)"
>
<UiButton
square
size="lg"
variant="ghost"
:ui="{
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
leadingIcon: 'size-10',
label: 'w-full',
}"
:icon="item.icon"
:label="item.name"
:tooltip="item.name"
draggable="true"
@click="openItem(item)"
@dragstart="handleDragStart($event, item)"
@dragend="handleDragEnd"
/>
</UContextMenu>
<!-- Disabled Extensions (grayed out) -->
<UiButton <UiButton
v-for="extension in disabledExtensions"
:key="extension.id"
square square
size="lg" size="xl"
variant="ghost" variant="ghost"
:disabled="true"
:ui="{ :ui="{
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing', base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
leadingIcon: 'size-10', leadingIcon: 'size-10',
label: 'w-full', label: 'w-full',
}" }"
:icon="item.icon" :icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
:label="item.name" :label="extension.name"
:tooltip="item.name" :tooltip="`${extension.name} (${t('disabled')})`"
draggable="true"
@click="openItem(item)"
@dragstart="handleDragStart($event, item)"
@dragend="handleDragEnd"
/> />
</UContextMenu> </div>
</div>
<!-- Disabled Extensions (grayed out) -->
<UiButton
v-for="extension in disabledExtensions"
:key="extension.id"
square
size="xl"
variant="ghost"
:disabled="true"
:ui="{
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
leadingIcon: 'size-10',
label: 'w-full',
}"
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
:label="extension.name"
:tooltip="`${extension.name} (${t('disabled')})`"
/>
</ul>
</template> </template>
</UPopover> </UDrawer>
<!-- Uninstall Confirmation Dialog --> <!-- Uninstall Confirmation Dialog -->
<UiDialogConfirm <UiDialogConfirm
v-model:open="showUninstallDialog" v-model:open="showUninstallDialog"
:title="t('uninstall.confirm.title')" :title="t('uninstall.confirm.title')"
:description="t('uninstall.confirm.description', { name: extensionToUninstall?.name || '' })" :description="
t('uninstall.confirm.description', {
name: extensionToUninstall?.name || '',
})
"
:confirm-label="t('uninstall.confirm.button')" :confirm-label="t('uninstall.confirm.button')"
confirm-icon="i-heroicons-trash" confirm-icon="i-heroicons-trash"
@confirm="confirmUninstall" @confirm="confirmUninstall"
@ -237,6 +251,9 @@ const handleDragEnd = () => {
de: de:
disabled: Deaktiviert disabled: Deaktiviert
marketplace: Marketplace marketplace: Marketplace
launcher:
title: App Launcher
description: Wähle eine App zum Öffnen
contextMenu: contextMenu:
open: Öffnen open: Öffnen
uninstall: Deinstallieren uninstall: Deinstallieren
@ -249,6 +266,9 @@ de:
en: en:
disabled: Disabled disabled: Disabled
marketplace: Marketplace marketplace: Marketplace
launcher:
title: App Launcher
description: Select an app to open
contextMenu: contextMenu:
open: Open open: Open
uninstall: Uninstall uninstall: Uninstall

View File

@ -2,6 +2,7 @@
<UiDialogConfirm <UiDialogConfirm
:confirm-label="t('create')" :confirm-label="t('create')"
@confirm="onCreateAsync" @confirm="onCreateAsync"
:description="t('description')"
> >
<UiButton <UiButton
:label="t('vault.create')" :label="t('vault.create')"
@ -55,7 +56,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { vaultSchema } from './schema' import { vaultSchema } from './schema'
const { t } = useI18n() const { t } = useI18n({
useScope: 'local',
})
const vault = reactive<{ const vault = reactive<{
name: string name: string
@ -118,6 +121,7 @@ de:
name: HaexVault name: HaexVault
title: Neue {haexvault} erstellen title: Neue {haexvault} erstellen
create: Erstellen create: Erstellen
description: Erstelle eine neue Vault für deine Daten
en: en:
vault: vault:
@ -127,4 +131,5 @@ en:
name: HaexVault name: HaexVault
title: Create new {haexvault} title: Create new {haexvault}
create: Create create: Create
description: Create a new vault for your data
</i18n> </i18n>

View File

@ -58,7 +58,9 @@ const props = defineProps<{
path?: string path?: string
}>() }>()
const { t } = useI18n() const { t } = useI18n({
useScope: 'local',
})
const vault = reactive<{ const vault = reactive<{
name: string name: string

View File

@ -3,11 +3,17 @@
ref="windowEl" ref="windowEl"
:style="windowStyle" :style="windowStyle"
:class="[ :class="[
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden isolate', 'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ', 'transition-all ease-out duration-600',
'flex flex-col @container', 'flex flex-col @container',
{ 'select-none': isResizingOrDragging }, { 'select-none': isResizingOrDragging },
isActive ? 'z-20' : 'z-10', isActive ? 'z-20' : 'z-10',
// Border colors based on warning level
warningLevel === 'warning'
? 'border-2 border-warning-500'
: warningLevel === 'danger'
? 'border-2 border-danger-500'
: 'border border-gray-200 dark:border-gray-700',
]" ]"
@mousedown="handleActivate" @mousedown="handleActivate"
> >
@ -86,6 +92,7 @@ const props = defineProps<{
sourceHeight?: number sourceHeight?: number
isOpening?: boolean isOpening?: boolean
isClosing?: boolean isClosing?: boolean
warningLevel?: 'warning' | 'danger' // Warning indicator (e.g., dev extension, dangerous permissions)
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -315,13 +322,33 @@ 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 y.value = 0 // Start below header and status bar
width.value = bounds.width width.value = bounds.width
height.value = bounds.height // Height: viewport - header - both safe-areas
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
isMaximized.value = true isMaximized.value = true
} }
console.log('handleMaximize', preMaximizeState, bounds)
} }
} }

View File

@ -1,90 +1,76 @@
<template> <template>
<UModal <UDrawer
v-model:open="localShowWindowOverview" v-model:open="localShowWindowOverview"
direction="bottom"
:title="t('modal.title')" :title="t('modal.title')"
:description="t('modal.description')" :description="t('modal.description')"
fullscreen
> >
<template #content> <template #content>
<div class="flex flex-col h-full"> <div class="h-full overflow-y-auto p-6 justify-center flex">
<!-- Header --> <!-- Window Thumbnails Flex Layout -->
<div <div
class="flex items-center justify-end border-b p-2 border-gray-200 dark:border-gray-700" v-if="windows.length > 0"
class="flex flex-wrap gap-6 justify-center-safe items-start"
> >
<UButton
icon="i-heroicons-x-mark"
color="error"
variant="soft"
@click="localShowWindowOverview = false"
/>
</div>
<!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto p-6 justify-center flex">
<!-- Window Thumbnails Flex Layout -->
<div <div
v-if="windows.length > 0" v-for="window in windows"
class="flex flex-wrap gap-6 justify-center-safe items-start" :key="window.id"
class="relative group cursor-pointer"
> >
<div <!-- Window Title Bar -->
v-for="window in windows" <div class="flex items-center gap-3 mb-2 px-2">
:key="window.id" <UIcon
class="relative group cursor-pointer" v-if="window.icon"
> :name="window.icon"
<!-- Window Title Bar --> class="size-5 shrink-0"
<div class="flex items-center gap-3 mb-2 px-2"> />
<UIcon <div class="flex-1 min-w-0">
v-if="window.icon" <p class="font-semibold text-sm truncate">
:name="window.icon" {{ window.title }}
class="size-5 shrink-0" </p>
/>
<div class="flex-1 min-w-0">
<p class="font-semibold text-sm truncate">
{{ window.title }}
</p>
</div>
<!-- Minimized Badge -->
<UBadge
v-if="window.isMinimized"
color="info"
size="xs"
:title="t('minimized')"
/>
</div> </div>
<!-- Minimized Badge -->
<UBadge
v-if="window.isMinimized"
color="info"
size="xs"
:title="t('minimized')"
/>
</div>
<!-- Scaled Window Preview Container / Teleport Target --> <!-- Scaled Window Preview Container / Teleport Target -->
<div
:id="`window-preview-${window.id}`"
class="relative bg-gray-100 dark:bg-gray-900 rounded-xl overflow-hidden border-2 border-gray-200 dark:border-gray-700 group-hover:border-primary-500 transition-all shadow-lg"
:style="getCardStyle(window)"
@click="handleRestoreAndActivateWindow(window.id)"
>
<!-- Hover Overlay -->
<div <div
:id="`window-preview-${window.id}`" class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
class="relative bg-gray-100 dark:bg-gray-900 rounded-xl overflow-hidden border-2 border-gray-200 dark:border-gray-700 group-hover:border-primary-500 transition-all shadow-lg" />
:style="getCardStyle(window)"
@click="handleRestoreAndActivateWindow(window.id)"
>
<!-- Hover Overlay -->
<div
class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
/>
</div>
</div> </div>
</div> </div>
</div>
<!-- Empty State --> <!-- Empty State -->
<div <div
v-else v-else
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400" class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
> >
<UIcon <UIcon
name="i-heroicons-window" name="i-heroicons-window"
class="size-16 mb-4 shrink-0" class="size-16 mb-4 shrink-0"
/> />
<p class="text-lg font-medium">No windows open</p> <p class="text-lg font-medium">No windows open</p>
<p class="text-sm"> <p class="text-sm">
Open an extension or system window to see it here Open an extension or system window to see it here
</p> </p>
</div>
</div> </div>
</div> </div>
</template> </template>
</UModal> </UDrawer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

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

View File

@ -4,11 +4,10 @@
<UButton <UButton
class="pointer-events-auto" class="pointer-events-auto"
v-bind="{ v-bind="{
...{ size: isSmallScreen ? 'lg' : 'md' },
...buttonProps, ...buttonProps,
...$attrs, ...$attrs,
}" }"
@click="(e) => $emit('click', e)" @click="$emit('click', $event)"
> >
<template <template
v-for="(_, slotName) in $slots" v-for="(_, slotName) in $slots"

View File

@ -368,7 +368,8 @@ async function handleDatabaseMethodAsync(
const rows = await invoke<unknown[]>('extension_sql_select', { const rows = await invoke<unknown[]>('extension_sql_select', {
sql: params.query || '', sql: params.query || '',
params: params.params || [], params: params.params || [],
extensionId: extension.id, publicKey: extension.publicKey,
name: extension.name,
}) })
return { return {
@ -379,14 +380,15 @@ async function handleDatabaseMethodAsync(
} }
case 'haextension.db.execute': { case 'haextension.db.execute': {
await invoke<string[]>('extension_sql_execute', { const rows = await invoke<unknown[]>('extension_sql_execute', {
sql: params.query || '', sql: params.query || '',
params: params.params || [], params: params.params || [],
extensionId: extension.id, publicKey: extension.publicKey,
name: extension.name,
}) })
return { return {
rows: [], rows,
rowsAffected: 1, rowsAffected: 1,
lastInsertId: undefined, lastInsertId: undefined,
} }
@ -400,7 +402,8 @@ async function handleDatabaseMethodAsync(
await invoke('extension_sql_execute', { await invoke('extension_sql_execute', {
sql: stmt, sql: stmt,
params: [], params: [],
extensionId: extension.id, publicKey: extension.publicKey,
name: extension.name,
}) })
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="w-dvw h-dvh flex flex-col"> <div class="w-full h-dvh flex flex-col">
<UPageHeader <UPageHeader
ref="headerEl"
as="header" as="header"
:ui="{ :ui="{
root: ['px-8 py-0'], root: ['px-8 py-0'],
@ -24,7 +25,7 @@
variant="outline" variant="outline"
icon="i-bi-person-workspace" icon="i-bi-person-workspace"
size="lg" size="lg"
:tooltip="t('header.workspaces')" :tooltip="t('workspaces.label')"
@click="isOverviewMode = !isOverviewMode" @click="isOverviewMode = !isOverviewMode"
/> />
</div> </div>
@ -53,7 +54,7 @@
</template> </template>
</UPageHeader> </UPageHeader>
<main class="flex-1 overflow-hidden bg-elevated flex flex-col"> <main class="overflow-hidden relative bg-elevated h-full">
<slot /> <slot />
</main> </main>
@ -93,11 +94,9 @@
variant="outline" variant="outline"
class="mt-6" class="mt-6"
@click="handleAddWorkspace" @click="handleAddWorkspace"
icon="i-heroicons-plus"
:label="t('workspaces.add')"
> >
<template #leading>
<UIcon name="i-heroicons-plus" />
</template>
New Workspace
</UButton> </UButton>
</div> </div>
</template> </template>
@ -127,6 +126,15 @@ const handleAddWorkspace = async () => {
workspaceStore.slideToWorkspace(workspace?.id) workspaceStore.slideToWorkspace(workspace?.id)
}) })
} }
// Measure header height and store it in UI store
const headerEl = useTemplateRef('headerEl')
const { height } = useElementSize(headerEl)
const uiStore = useUiStore()
watch(height, (newHeight) => {
uiStore.headerHeight = newHeight
})
</script> </script>
<i18n lang="yaml"> <i18n lang="yaml">
@ -134,12 +142,14 @@ de:
search: search:
label: Suche label: Suche
header: workspaces:
workspaces: Workspaces label: Workspaces
add: Workspace hinzufügen
en: en:
search: search:
label: Search label: Search
header: workspaces:
workspaces: Workspaces label: Workspaces
add: Add Workspace
</i18n> </i18n>

View File

@ -1,112 +1,101 @@
<template> <template>
<div> <div class="h-full">
<NuxtLayout> <NuxtLayout>
<UDashboardPanel <div
id="inbox-1" class="flex flex-col justify-center items-center gap-5 mx-auto h-full overflow-scroll"
resizable
class=""
> >
<template #body> <UiLogoHaexhub class="bg-primary p-3 size-16 rounded-full shrink-0" />
<div class="items-center justify-center flex relative flex-1"> <span
<!-- <div class="absolute top-0 right-0"> class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center"
<UiDropdownLocale @select="onSelectLocale" /> >
</div> --> <p class="whitespace-nowrap">
{{ t('welcome') }}
</p>
<UiTextGradient>Haex Hub</UiTextGradient>
</span>
<div <div class="flex flex-col gap-4 h-24 items-stretch justify-center">
class="flex flex-col justify-center items-center gap-5 max-w-3xl" <HaexVaultCreate />
>
<UiLogoHaexhub
class="bg-primary p-3 size-16 rounded-full shrink-0"
/>
<span
class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center"
>
<p class="whitespace-nowrap">
{{ t('welcome') }}
</p>
<UiTextGradient>Haex Hub</UiTextGradient>
</span>
<div <HaexVaultOpen
class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto" v-model:open="passwordPromptOpen"
> :path="selectedVault?.path"
<HaexVaultCreate /> />
</div>
<HaexVaultOpen <div
v-model:open="passwordPromptOpen" v-show="lastVaults.length"
:path="selectedVault?.path" class="max-w-md w-full sm:px-5"
/> >
</div> <div class="font-thin text-sm pb-1 w-full">
{{ t('lastUsed') }}
<div
v-show="lastVaults.length"
class="w-full"
>
<div class="font-thin text-sm justify-start px-2 pb-1">
{{ t('lastUsed') }}
</div>
<div
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-scroll"
>
<div
v-for="vault in lastVaults"
:key="vault.name"
class="flex items-center justify-between group overflow-x-scroll"
>
<UButton
variant="ghost"
color="neutral"
class="flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full px-3"
@click="
() => {
passwordPromptOpen = true
selectedVault = vault
}
"
>
<span class="block">
{{ vault.name }}
</span>
</UButton>
<UButton
color="error"
square
class="absolute right-2 hidden group-hover:flex min-w-6"
>
<Icon
name="mdi:trash-can-outline"
@click="prepareRemoveVault(vault.name)"
/>
</UButton>
</div>
</div>
</div>
<div class="flex flex-col items-center gap-2">
<h4>{{ t('sponsors') }}</h4>
<div>
<UButton
variant="link"
@click="openUrl('https://itemis.com')"
>
<UiLogoItemis class="text-[#00457C]" />
</UButton>
</div>
</div>
</div>
<UiDialogConfirm
v-model:open="showRemoveDialog"
:title="t('remove.title')"
:description="
t('remove.description', { vaultName: vaultToBeRemoved })
"
@confirm="onConfirmRemoveAsync"
/>
</div> </div>
</template>
</UDashboardPanel> <div
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-scroll"
>
<div
v-for="vault in lastVaults"
:key="vault.name"
class="flex items-center justify-between group overflow-x-scroll"
>
<UiButtonContext
variant="ghost"
color="neutral"
size="xl"
class="flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full hover:bg-default"
:context-menu-items="[
{
icon: 'mdi:trash-can-outline',
label: t('remove.button'),
onSelect: () => prepareRemoveVault(vault.name),
color: 'error',
},
]"
@click="
() => {
passwordPromptOpen = true
selectedVault = vault
}
"
>
<span class="block">
{{ vault.name }}
</span>
</UiButtonContext>
<UButton
color="error"
square
class="absolute right-2 hidden group-hover:flex min-w-6"
>
<Icon
name="mdi:trash-can-outline"
@click="prepareRemoveVault(vault.name)"
/>
</UButton>
</div>
</div>
</div>
<div class="flex flex-col items-center gap-2">
<h4>{{ t('sponsors') }}</h4>
<div>
<UButton
variant="link"
@click="openUrl('https://itemis.com')"
>
<UiLogoItemis class="text-[#00457C]" />
</UButton>
</div>
</div>
</div>
<UiDialogConfirm
v-model:open="showRemoveDialog"
:title="t('remove.title')"
:description="t('remove.description', { vaultName: vaultToBeRemoved })"
@confirm="onConfirmRemoveAsync"
/>
</NuxtLayout> </NuxtLayout>
</div> </div>
</template> </template>
@ -129,6 +118,9 @@ const showRemoveDialog = ref(false)
const { lastVaults } = storeToRefs(useLastVaultStore()) const { lastVaults } = storeToRefs(useLastVaultStore())
const { syncLastVaultsAsync, moveVaultToTrashAsync } = useLastVaultStore()
const { syncDeviceIdAsync } = useDeviceStore()
const vaultToBeRemoved = ref('') const vaultToBeRemoved = ref('')
const prepareRemoveVault = (vaultName: string) => { const prepareRemoveVault = (vaultName: string) => {
vaultToBeRemoved.value = vaultName vaultToBeRemoved.value = vaultName
@ -138,7 +130,7 @@ const prepareRemoveVault = (vaultName: string) => {
const toast = useToast() const toast = useToast()
const onConfirmRemoveAsync = async () => { const onConfirmRemoveAsync = async () => {
try { try {
await removeVaultAsync(vaultToBeRemoved.value) await moveVaultToTrashAsync(vaultToBeRemoved.value)
showRemoveDialog.value = false showRemoveDialog.value = false
await syncLastVaultsAsync() await syncLastVaultsAsync()
} catch (error) { } catch (error) {
@ -149,9 +141,6 @@ const onConfirmRemoveAsync = async () => {
} }
} }
const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
const { syncDeviceIdAsync } = useDeviceStore()
onMounted(async () => { onMounted(async () => {
try { try {
await syncLastVaultsAsync() await syncLastVaultsAsync()
@ -168,6 +157,7 @@ de:
lastUsed: 'Zuletzt verwendete Vaults' lastUsed: 'Zuletzt verwendete Vaults'
sponsors: Supported by sponsors: Supported by
remove: remove:
button: Löschen
title: Vault löschen title: Vault löschen
description: Möchtest du die Vault {vaultName} wirklich löschen? description: Möchtest du die Vault {vaultName} wirklich löschen?
@ -176,6 +166,7 @@ en:
lastUsed: 'Last used Vaults' lastUsed: 'Last used Vaults'
sponsors: 'Supported by' sponsors: 'Supported by'
remove: remove:
button: Delete
title: Delete Vault title: Delete Vault
description: Are you sure you really want to delete {vaultName}? description: Are you sure you really want to delete {vaultName}?
</i18n> </i18n>

View File

@ -9,6 +9,7 @@
v-model:open="showNewDeviceDialog" v-model:open="showNewDeviceDialog"
:confirm-label="t('newDevice.save')" :confirm-label="t('newDevice.save')"
:title="t('newDevice.title')" :title="t('newDevice.title')"
:description="t('newDevice.setName')"
confirm-icon="mdi:content-save-outline" confirm-icon="mdi:content-save-outline"
@abort="showNewDeviceDialog = false" @abort="showNewDeviceDialog = false"
@confirm="onSetDeviceNameAsync" @confirm="onSetDeviceNameAsync"

View File

@ -1,6 +1,8 @@
<template> <template>
<div class="w-full h-full flex items-center justify-center"> <div>
<HaexDesktop /> <UDashboardPanel resizable>
<HaexDesktop />
</UDashboardPanel>
</div> </div>
</template> </template>

View File

@ -0,0 +1,25 @@
export default defineNuxtPlugin({
name: 'init-logger',
enforce: 'pre',
parallel: false,
setup() {
// Add global error handler for better debugging
window.addEventListener('error', (event) => {
console.error('[HaexHub] Global error caught:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
stack: event.error?.stack,
})
})
window.addEventListener('unhandledrejection', (event) => {
console.error('[HaexHub] Unhandled rejection:', {
reason: event.reason,
promise: event.promise,
})
})
},
})

View File

@ -50,7 +50,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
currentExtension.value.publicKey, currentExtension.value.publicKey,
currentExtension.value.name, currentExtension.value.name,
currentExtension.value.version, currentExtension.value.version,
'index.html', currentExtension.value.entry ?? 'index.html',
currentExtension.value.devServerUrl ?? undefined, currentExtension.value.devServerUrl ?? undefined,
) )
}) })

View File

@ -1,5 +1,6 @@
import { breakpointsTailwind } from '@vueuse/core' import { breakpointsTailwind } from '@vueuse/core'
import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler' import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler'
import de from './de.json' import de from './de.json'
import en from './en.json' import en from './en.json'
@ -10,8 +11,9 @@ export const useUiStore = defineStore('uiStore', () => {
const isSmallScreen = breakpoints.smaller('sm') const isSmallScreen = breakpoints.smaller('sm')
const { $i18n } = useNuxtApp() const { $i18n } = useNuxtApp()
const { locale } = useI18n() const { locale } = useI18n({
const { platform } = useDeviceStore() useScope: 'global',
})
$i18n.setLocaleMessage('de', { $i18n.setLocaleMessage('de', {
ui: de, ui: de,
@ -60,19 +62,23 @@ export const useUiStore = defineStore('uiStore', () => {
}) })
// Broadcast theme and locale changes to extensions // Broadcast theme and locale changes to extensions
watch([currentThemeName, locale], () => { watch([currentThemeName, locale], async () => {
const deviceStore = useDeviceStore()
const platformValue = await deviceStore.platform
broadcastContextToAllExtensions({ broadcastContextToAllExtensions({
theme: currentThemeName.value, theme: currentThemeName.value,
locale: locale.value, locale: locale.value,
platform, platform: platformValue,
}) })
}) })
const viewportHeightWithoutHeader = ref(0) const viewportHeightWithoutHeader = ref(0)
const headerHeight = ref(0)
return { return {
availableThemes, availableThemes,
viewportHeightWithoutHeader, viewportHeightWithoutHeader,
headerHeight,
currentTheme, currentTheme,
currentThemeName, currentThemeName,
defaultTheme, defaultTheme,

View File

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

View File

@ -1,14 +0,0 @@
Blocking waiting for file lock on build directory
23.313630402s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: stale: missing "/home/haex/Projekte/haex-hub/src-tauri/src/database/schemas/crdt.ts"
23.319711685s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: false }/TargetInner { ..: lib_target("haex_hub_lib", ["staticlib", "cdylib", "rlib"], "/home/haex/Projekte/haex-hub/src-tauri/src/lib.rs", Edition2021) }
23.319734303s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "build_script_build" })
23.322781234s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="build-script-build"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/RunCustomBuild/TargetInner { ..: custom_build_target("build-script-build", "/home/haex/Projekte/haex-hub/src-tauri/build.rs", Edition2021) }
23.322837026s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="build-script-build"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleItem(MissingFile("/home/haex/Projekte/haex-hub/src-tauri/src/database/schemas/crdt.ts")))
23.335082427s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: true }/TargetInner { ..: lib_target("haex_hub_lib", ["staticlib", "cdylib", "rlib"], "/home/haex/Projekte/haex-hub/src-tauri/src/lib.rs", Edition2021) }
23.335103454s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex_hub_lib"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "build_script_build" })
23.336844253s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: false }/TargetInner { name: "haex-hub", doc: true, ..: with_path("/home/haex/Projekte/haex-hub/src-tauri/src/main.rs", Edition2021) }
23.336854407s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "haex_hub_lib" })
23.338162602s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: fingerprint dirty for haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)/Check { test: true }/TargetInner { name: "haex-hub", doc: true, ..: with_path("/home/haex/Projekte/haex-hub/src-tauri/src/main.rs", Edition2021) }
23.338170106s INFO prepare_target{force=false package_id=haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri) target="haex-hub"}: cargo::core::compiler::fingerprint: dirty: FsStatusOutdated(StaleDepFingerprint { name: "haex_hub_lib" })
Compiling haex-hub v0.1.0 (/home/haex/Projekte/haex-hub/src-tauri)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 25.44s

File diff suppressed because one or more lines are too long