mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-18 15:00:52 +01:00
Compare commits
39 Commits
9281a85deb
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 4833dee89a | |||
| a80c783576 | |||
| 4e1e4ae601 | |||
| 6a7f58a513 | |||
| 3ed8d6bc05 | |||
| 81a72da26c | |||
| 4fa3515e32 | |||
| c5c30fd4c4 | |||
| 8c7a02a019 | |||
| 465fe19542 | |||
| d2d0f8996b | |||
| f727d00639 | |||
| a946b14f69 | |||
| 471baec284 | |||
| 8298d807f3 | |||
| 42e6459fbf | |||
| 6ae87fc694 | |||
| f7867a5bde | |||
| d82599f588 | |||
| 72bb211a76 | |||
| f14ce0d6ad | |||
| af09f4524d | |||
| 102832675d | |||
| 3490de2f51 | |||
| 7c3af10938 | |||
| 5c5d0785b9 | |||
| 121dd9dd00 | |||
| 4ff6aee4d8 | |||
| dceb49ae90 | |||
| 5ea04a80e0 | |||
| 65cf2e2c3c | |||
| 68d542b4d7 | |||
| f97cd4ad97 | |||
| ef225b281f | |||
| 16b71d9ea8 | |||
| 5ee5ced8c0 | |||
| 86b65f117d | |||
| 5fdea155d1 | |||
| cb0c8d71f4 |
228
.github/workflows/build.yml
vendored
Normal file
228
.github/workflows/build.yml
vendored
Normal 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
251
.github/workflows/release.yml
vendored
Normal 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
|
||||
})
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -26,4 +26,6 @@ dist-ssr
|
||||
src-tauri/target
|
||||
nogit*
|
||||
.claude
|
||||
.output
|
||||
.output
|
||||
target
|
||||
CLAUDE.md
|
||||
@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src-tauri/database/schemas/**.ts',
|
||||
schema: './src/database/schemas/**.ts',
|
||||
out: './src-tauri/database/migrations',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
//import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
@ -16,6 +14,9 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
viewport: 'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
||||
},
|
||||
pageTransition: {
|
||||
name: 'fade',
|
||||
},
|
||||
@ -28,7 +29,6 @@ export default defineNuxtConfig({
|
||||
'@vueuse/nuxt',
|
||||
'@nuxt/icon',
|
||||
'@nuxt/eslint',
|
||||
//"@nuxt/image",
|
||||
'@nuxt/fonts',
|
||||
'@nuxt/ui',
|
||||
],
|
||||
@ -108,8 +108,7 @@ export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
haexVault: {
|
||||
lastVaultFileName: 'lastVaults.json',
|
||||
instanceFileName: 'instance.json',
|
||||
deviceFileName: 'device.json',
|
||||
defaultVaultName: 'HaexHub',
|
||||
},
|
||||
},
|
||||
@ -123,7 +122,6 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
vite: {
|
||||
//plugins: [tailwindcss()],
|
||||
// Better support for Tauri CLI output
|
||||
clearScreen: false,
|
||||
// Enable environment variables
|
||||
|
||||
25
package.json
25
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "haex-hub",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
@ -21,27 +21,23 @@
|
||||
"@nuxt/eslint": "1.9.0",
|
||||
"@nuxt/fonts": "0.11.4",
|
||||
"@nuxt/icon": "2.0.0",
|
||||
"@nuxt/ui": "4.0.0",
|
||||
"@nuxt/ui": "4.1.0",
|
||||
"@nuxtjs/i18n": "10.0.6",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-http": "2.5.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-notification": "2.3.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"@tauri-apps/plugin-sql": "2.3.0",
|
||||
"@tauri-apps/plugin-store": "^2.4.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@vueuse/components": "^13.9.0",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"@vueuse/nuxt": "^13.9.0",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"eslint": "^9.38.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"nuxt": "^4.1.3",
|
||||
"nuxt-zod-i18n": "^1.12.1",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwindcss": "^4.1.16",
|
||||
@ -51,8 +47,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/hugeicons": "^1.2.17",
|
||||
"@iconify-json/lucide": "^1.2.70",
|
||||
"@iconify/json": "^2.2.399",
|
||||
"@iconify-json/lucide": "^1.2.71",
|
||||
"@iconify/json": "^2.2.401",
|
||||
"@iconify/tailwind4": "^1.0.6",
|
||||
"@libsql/client": "^0.15.15",
|
||||
"@tauri-apps/cli": "^2.9.1",
|
||||
@ -61,11 +57,12 @@
|
||||
"@vue/compiler-sfc": "^3.5.22",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"globals": "^16.4.0",
|
||||
"nuxt": "^4.2.0",
|
||||
"prettier": "3.6.2",
|
||||
"tsx": "^4.20.6",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "7.1.3",
|
||||
"vite": "^7.1.3",
|
||||
"vue-tsc": "3.0.6"
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
1712
pnpm-lock.yaml
generated
1712
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1303
src-tauri/Cargo.lock
generated
1303
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "haex-hub"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@ -20,14 +20,7 @@ tauri-build = { version = "2.2", features = [] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.37.0", features = [
|
||||
"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"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
base64 = "0.22"
|
||||
ed25519-dalek = "2.1"
|
||||
fs_extra = "1.3.0"
|
||||
@ -39,18 +32,26 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
sha2 = "0.10.9"
|
||||
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
||||
tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
|
||||
tauri-plugin-dialog = "2.4.0"
|
||||
tauri = { version = "2.9.1", features = ["protocol-asset", "devtools"] }
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-http = "2.5.2"
|
||||
tauri-plugin-notification = "2.3.1"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3"
|
||||
tauri-plugin-persisted-scope = "2.3.2"
|
||||
tauri-plugin-store = "2.4.0"
|
||||
tauri-plugin-http = "2.5.4"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tauri-plugin-opener = "2.5.2"
|
||||
tauri-plugin-os = "2.3.2"
|
||||
tauri-plugin-persisted-scope = "2.3.4"
|
||||
tauri-plugin-store = "2.4.1"
|
||||
thiserror = "2.0.17"
|
||||
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
|
||||
uhlc = "0.8.2"
|
||||
url = "2.5.7"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
zip = "6.0.0"
|
||||
url = "2.5.7"
|
||||
rusqlite = { version = "0.37.0", features = [
|
||||
"load_extension",
|
||||
"bundled-sqlcipher-vendored-openssl",
|
||||
"functions",
|
||||
] }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
trash = "5.2.5"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
// 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, };
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// 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";
|
||||
|
||||
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, };
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
"notification:allow-create-channel",
|
||||
"notification:allow-list-channels",
|
||||
"notification:allow-notify",
|
||||
"notification:allow-is-permission-granted",
|
||||
"notification:default",
|
||||
"opener:allow-open-url",
|
||||
"opener:default",
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import tablesNames from './tableNames.json'
|
||||
import { schema } from './index'
|
||||
import tablesNames from '../../src/database/tableNames.json'
|
||||
import { schema } from '../../src/database/index'
|
||||
import { getTableColumns } from 'drizzle-orm'
|
||||
import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung
|
||||
import * as schema from './schemas' // Importiere alles aus deiner Schema-Datei
|
||||
export * as schema from './schemas'
|
||||
// sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument.
|
||||
// Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden.
|
||||
// Sie muss aber vorhanden sein, um drizzle() aufrufen zu können.
|
||||
const dummyExecutor = async (
|
||||
sql: string,
|
||||
params: unknown[],
|
||||
method: 'all' | 'run' | 'get' | 'values',
|
||||
) => {
|
||||
console.warn(
|
||||
`Frontend Drizzle Executor wurde aufgerufen (Methode: ${method}). Das sollte im Tauri-Invoke-Workflow nicht passieren!`,
|
||||
)
|
||||
// Wir geben leere Ergebnisse zurück, um die Typen zufriedenzustellen, falls es doch aufgerufen wird.
|
||||
return { rows: [] } // Für 'run' (z.B. bei INSERT/UPDATE)
|
||||
}
|
||||
|
||||
// Erstelle die Drizzle-Instanz für den SQLite-Dialekt
|
||||
// Übergib den dummyExecutor und das importierte Schema
|
||||
export const db = drizzle(dummyExecutor, { schema })
|
||||
|
||||
// Exportiere auch alle Schema-Definitionen weiter, damit man alles aus einer Datei importieren kann
|
||||
@ -1,99 +0,0 @@
|
||||
CREATE TABLE `haex_crdt_configs` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`value` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_crdt_logs` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`haex_timestamp` text,
|
||||
`table_name` text,
|
||||
`row_pks` text,
|
||||
`op_type` text,
|
||||
`column_name` text,
|
||||
`new_value` text,
|
||||
`old_value` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_haex_timestamp` ON `haex_crdt_logs` (`haex_timestamp`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_table_row` ON `haex_crdt_logs` (`table_name`,`row_pks`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_crdt_snapshots` (
|
||||
`snapshot_id` text PRIMARY KEY NOT NULL,
|
||||
`created` text,
|
||||
`epoch_hlc` text,
|
||||
`location_url` text,
|
||||
`file_size_bytes` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_desktop_items` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`workspace_id` text NOT NULL,
|
||||
`item_type` text NOT NULL,
|
||||
`reference_id` text NOT NULL,
|
||||
`position_x` integer DEFAULT 0 NOT NULL,
|
||||
`position_y` integer DEFAULT 0 NOT NULL,
|
||||
`haex_timestamp` text,
|
||||
FOREIGN KEY (`workspace_id`) REFERENCES `haex_workspaces`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_extension_permissions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`extension_id` text NOT NULL,
|
||||
`resource_type` text,
|
||||
`action` text,
|
||||
`target` text,
|
||||
`constraints` text,
|
||||
`status` text DEFAULT 'denied' NOT NULL,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_at` integer,
|
||||
`haex_timestamp` text,
|
||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_extension_permissions_extension_id_resource_type_action_target_unique` ON `haex_extension_permissions` (`extension_id`,`resource_type`,`action`,`target`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_extensions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`public_key` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`version` text NOT NULL,
|
||||
`author` text,
|
||||
`description` text,
|
||||
`entry` text DEFAULT 'index.html' NOT NULL,
|
||||
`homepage` text,
|
||||
`enabled` integer DEFAULT true,
|
||||
`icon` text,
|
||||
`signature` text NOT NULL,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_extensions_public_key_name_unique` ON `haex_extensions` (`public_key`,`name`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_notifications` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`alt` text,
|
||||
`date` text,
|
||||
`icon` text,
|
||||
`image` text,
|
||||
`read` integer,
|
||||
`source` text,
|
||||
`text` text,
|
||||
`title` text,
|
||||
`type` text NOT NULL,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_settings` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`key` text,
|
||||
`type` text,
|
||||
`value` text,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_workspaces` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`position` integer DEFAULT 0 NOT NULL,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_workspaces_position_unique` ON `haex_workspaces` (`position`);
|
||||
@ -1,645 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "21ca1268-1057-48c1-8647-29bd7cb67d49",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"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
|
||||
},
|
||||
"reference_id": {
|
||||
"name": "reference_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extension_permissions": {
|
||||
"name": "haex_extension_permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"extension_id": {
|
||||
"name": "extension_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"resource_type": {
|
||||
"name": "resource_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"target": {
|
||||
"name": "target",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"constraints": {
|
||||
"name": "constraints",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'denied'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource_type",
|
||||
"action",
|
||||
"target"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extension_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions": {
|
||||
"name": "haex_extensions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entry": {
|
||||
"name": "entry",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": 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
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1761216357702,
|
||||
"tag": "0000_bumpy_valkyrie",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@ -24,6 +24,23 @@ android {
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
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 {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
@ -43,6 +60,12 @@ android {
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
|
||||
// Sign with release config if available
|
||||
val releaseSigningConfig = signingConfigs.getByName("release")
|
||||
if (releaseSigningConfig.storeFile != null) {
|
||||
signingConfig = releaseSigningConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1400,10 +1400,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
@ -2277,10 +2277,10 @@
|
||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "core:app:default",
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||
@ -2324,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -2396,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -5541,10 +5565,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
|
||||
@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-read-file","fs:allow-read-dir","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-read-file","fs:allow-read-dir","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
||||
@ -1400,10 +1400,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
@ -2277,10 +2277,10 @@
|
||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "core:app:default",
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||
@ -2324,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -2396,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -5541,10 +5565,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
|
||||
@ -1400,10 +1400,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
@ -2277,10 +2277,10 @@
|
||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "core:app:default",
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||
@ -2324,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -2396,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -5541,10 +5565,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
|
||||
@ -1400,10 +1400,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
@ -2277,10 +2277,10 @@
|
||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "core:app:default",
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||
@ -2324,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -2396,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@ -5541,10 +5565,10 @@
|
||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||
},
|
||||
{
|
||||
"description": "An empty permission you can use to modify the global scope.",
|
||||
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||
"type": "string",
|
||||
"const": "fs:scope",
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
||||
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||
},
|
||||
{
|
||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||
|
||||
@ -21,7 +21,7 @@ struct TableDefinition {
|
||||
pub fn generate_table_names() {
|
||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
||||
println!("Generiere Tabellennamen nach {}", out_dir);
|
||||
let schema_path = Path::new("database/tableNames.json");
|
||||
let schema_path = Path::new("../src/database/tableNames.json");
|
||||
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||
|
||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
||||
@ -66,7 +66,7 @@ pub fn generate_table_names() {
|
||||
f.write_all(code.as_bytes())
|
||||
.expect("Konnte nicht in Zieldatei schreiben");
|
||||
|
||||
println!("cargo:rerun-if-changed=database/tableNames.json");
|
||||
println!("cargo:rerun-if-changed=../src/database/tableNames.json");
|
||||
}
|
||||
|
||||
/// Konvertiert einen String zu SCREAMING_SNAKE_CASE
|
||||
|
||||
@ -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 DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete";
|
||||
|
||||
//const SYNC_ACTIVE_KEY: &str = "sync_active";
|
||||
|
||||
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
|
||||
|
||||
/// 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 {
|
||||
!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.
|
||||
|
||||
@ -89,8 +89,15 @@ pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
|
||||
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
||||
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ use std::time::UNIX_EPOCH;
|
||||
use std::{fs, sync::Arc};
|
||||
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
||||
use tauri_plugin_fs::FsExt;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use trash;
|
||||
use ts_rs::TS;
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
//#[serde(tag = "type", content = "details")]
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[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())
|
||||
}
|
||||
|
||||
/// 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]
|
||||
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
|
||||
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
||||
@ -395,9 +449,6 @@ pub fn open_encrypted_database(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, DatabaseError> {
|
||||
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);
|
||||
|
||||
if !Path::new(&vault_path).exists() {
|
||||
|
||||
@ -66,17 +66,124 @@ impl ExtensionManager {
|
||||
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
|
||||
fn extract_and_validate_extension(
|
||||
bytes: Vec<u8>,
|
||||
temp_prefix: &str,
|
||||
app_handle: &AppHandle,
|
||||
) -> 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)
|
||||
.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 {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
}
|
||||
@ -88,38 +195,54 @@ impl ExtensionManager {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// Check if manifest.json is directly in temp or in a subdirectory
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let actual_dir = if manifest_path.exists() {
|
||||
temp.clone()
|
||||
} else {
|
||||
// manifest.json is in a subdirectory - find it
|
||||
let mut found_dir = None;
|
||||
for entry in fs::read_dir(&temp)
|
||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() && path.join("manifest.json").exists() {
|
||||
found_dir = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Clean up temporary ZIP file
|
||||
let _ = fs::remove_file(&zip_file_path);
|
||||
|
||||
found_dir.ok_or_else(|| ExtensionError::ManifestError {
|
||||
reason: "manifest.json not found in extension archive".to_string(),
|
||||
})?
|
||||
// Read haextension_dir from config if it exists, otherwise use default
|
||||
let config_path = temp.join("haextension.config.json");
|
||||
let haextension_dir = if config_path.exists() {
|
||||
let config_content = std::fs::read_to_string(&config_path)
|
||||
.map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
let config: serde_json::Value = serde_json::from_str(&config_content)
|
||||
.map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Invalid haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
let dir = config
|
||||
.get("dev")
|
||||
.and_then(|dev| dev.get("haextension_dir"))
|
||||
.and_then(|dir| dir.as_str())
|
||||
.unwrap_or("haextension")
|
||||
.to_string();
|
||||
|
||||
dir
|
||||
} else {
|
||||
"haextension".to_string()
|
||||
};
|
||||
|
||||
let manifest_path = actual_dir.join("manifest.json");
|
||||
// Validate manifest path using helper function
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)?
|
||||
.ok_or_else(|| ExtensionError::ManifestError {
|
||||
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
||||
})?;
|
||||
|
||||
let actual_dir = temp.clone();
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| {
|
||||
// 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| {
|
||||
ExtensionError::SignatureVerificationFailed {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
@ -393,9 +516,10 @@ impl ExtensionManager {
|
||||
|
||||
pub async fn preview_extension_internal(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
file_bytes: Vec<u8>,
|
||||
) -> 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(
|
||||
&extracted.manifest.public_key,
|
||||
@ -420,7 +544,7 @@ impl ExtensionManager {
|
||||
custom_permissions: EditablePermissions,
|
||||
state: &State<'_, AppState>,
|
||||
) -> 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)
|
||||
ExtensionCrypto::verify_signature(
|
||||
@ -491,7 +615,7 @@ impl ExtensionManager {
|
||||
|
||||
// 1. Extension-Eintrag erstellen mit generierter UUID
|
||||
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
|
||||
);
|
||||
|
||||
@ -511,6 +635,7 @@ impl ExtensionManager {
|
||||
extracted.manifest.homepage,
|
||||
extracted.manifest.description,
|
||||
true, // enabled
|
||||
extracted.manifest.single_instance.unwrap_or(false),
|
||||
],
|
||||
)?;
|
||||
|
||||
@ -589,7 +714,7 @@ impl ExtensionManager {
|
||||
// Lade alle Daten aus der Datenbank
|
||||
let extensions = with_connection(&state.db, |conn| {
|
||||
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
|
||||
);
|
||||
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
||||
@ -621,13 +746,16 @@ impl ExtensionManager {
|
||||
})?
|
||||
.to_string(),
|
||||
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),
|
||||
public_key: row[6].as_str().unwrap_or("").to_string(),
|
||||
signature: row[7].as_str().unwrap_or("").to_string(),
|
||||
permissions: ExtensionPermissions::default(),
|
||||
homepage: row[8].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]
|
||||
@ -661,9 +789,10 @@ impl ExtensionManager {
|
||||
&extension_data.manifest.version,
|
||||
)?;
|
||||
|
||||
if !extension_path.exists() || !extension_path.join("manifest.json").exists() {
|
||||
// Check if extension directory exists
|
||||
if !extension_path.exists() {
|
||||
eprintln!(
|
||||
"DEBUG: Extension files missing for: {} at {:?}",
|
||||
"DEBUG: Extension directory missing for: {} at {:?}",
|
||||
extension_id, extension_path
|
||||
);
|
||||
self.missing_extensions
|
||||
@ -680,6 +809,52 @@ impl ExtensionManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read haextension_dir from config if it exists, otherwise use default
|
||||
let config_path = extension_path.join("haextension.config.json");
|
||||
let haextension_dir = if config_path.exists() {
|
||||
match std::fs::read_to_string(&config_path) {
|
||||
Ok(config_content) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
||||
Ok(config) => {
|
||||
config
|
||||
.get("dev")
|
||||
.and_then(|dev| dev.get("haextension_dir"))
|
||||
.and_then(|dir| dir.as_str())
|
||||
.unwrap_or("haextension")
|
||||
.to_string()
|
||||
}
|
||||
Err(_) => "haextension".to_string(),
|
||||
}
|
||||
}
|
||||
Err(_) => "haextension".to_string(),
|
||||
}
|
||||
} else {
|
||||
"haextension".to_string()
|
||||
};
|
||||
|
||||
// Validate manifest.json path using helper function
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)?
|
||||
.is_none()
|
||||
{
|
||||
eprintln!(
|
||||
"DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json",
|
||||
extension_id, haextension_dir
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
eprintln!("DEBUG: Extension loaded successfully: {}", extension_id);
|
||||
|
||||
let extension = Extension {
|
||||
|
||||
@ -57,13 +57,20 @@ pub struct ExtensionManifest {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
#[serde(default = "default_entry_value")]
|
||||
pub entry: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: 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 {
|
||||
@ -172,6 +179,8 @@ pub struct ExtensionInfoResponse {
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub entry: Option<String>,
|
||||
pub single_instance: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dev_server_url: Option<String>,
|
||||
}
|
||||
@ -197,6 +206,8 @@ impl ExtensionInfoResponse {
|
||||
description: extension.manifest.description.clone(),
|
||||
homepage: extension.manifest.homepage.clone(),
|
||||
icon: extension.manifest.icon.clone(),
|
||||
entry: extension.manifest.entry.clone(),
|
||||
single_instance: extension.manifest.single_instance,
|
||||
dev_server_url,
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,28 +4,13 @@ use std::{
|
||||
};
|
||||
|
||||
// src-tauri/src/extension/crypto.rs
|
||||
use crate::extension::error::ExtensionError;
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub struct ExtensionCrypto;
|
||||
|
||||
impl ExtensionCrypto {
|
||||
/// Berechnet Hash vom Public Key (wie im SDK)
|
||||
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
|
||||
let public_key_bytes =
|
||||
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
|
||||
|
||||
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
|
||||
.map_err(|e| format!("Invalid public key: {}", e))?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(public_key.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
|
||||
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
|
||||
Ok(hex::encode(&result[..10]))
|
||||
}
|
||||
|
||||
/// Verifiziert Extension-Signatur
|
||||
pub fn verify_signature(
|
||||
public_key_hex: &str,
|
||||
@ -50,26 +35,64 @@ impl ExtensionCrypto {
|
||||
}
|
||||
|
||||
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
||||
pub fn hash_directory(dir: &Path) -> Result<String, String> {
|
||||
pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result<String, ExtensionError> {
|
||||
// 1. Alle Dateipfade rekursiv sammeln
|
||||
let mut all_files = Vec::new();
|
||||
Self::collect_files_recursively(dir, &mut all_files)
|
||||
.map_err(|e| format!("Failed to collect files: {}", e))?;
|
||||
all_files.sort();
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// 2. Konvertiere zu relativen Pfaden für konsistente Sortierung (wie im SDK)
|
||||
let mut relative_files: Vec<(String, PathBuf)> = all_files
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let relative = path.strip_prefix(dir)
|
||||
.unwrap_or(&path)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
|
||||
.replace('\\', "/");
|
||||
(relative, path)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 3. Sortiere nach relativen Pfaden
|
||||
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let manifest_path = dir.join("manifest.json");
|
||||
|
||||
// 2. Inhalte der sortierten Dateien hashen
|
||||
for file_path in all_files {
|
||||
if file_path == manifest_path {
|
||||
// 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
|
||||
for (_relative, file_path) in relative_files {
|
||||
// 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:
|
||||
let content_str = fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("Cannot read manifest file: {}", e))?;
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// Parse zu einem generischen JSON-Wert
|
||||
let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
|
||||
.map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
|
||||
let mut manifest: serde_json::Value =
|
||||
serde_json::from_str(&content_str).map_err(|e| {
|
||||
ExtensionError::ManifestError {
|
||||
reason: format!("Cannot parse manifest JSON: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
|
||||
if let Some(obj) = manifest.as_object_mut() {
|
||||
@ -80,13 +103,23 @@ impl ExtensionCrypto {
|
||||
}
|
||||
|
||||
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
|
||||
let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap();
|
||||
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
||||
hasher.update(canonical_manifest_content.as_bytes());
|
||||
// serde_json sortiert die Keys automatisch alphabetisch
|
||||
let canonical_manifest_content =
|
||||
serde_json::to_string_pretty(&manifest).map_err(|e| {
|
||||
ExtensionError::ManifestError {
|
||||
reason: format!("Failed to serialize manifest: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
// 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 {
|
||||
// FÜR ALLE ANDEREN DATEIEN:
|
||||
let content = fs::read(&file_path)
|
||||
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
|
||||
let content =
|
||||
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
hasher.update(&content);
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,13 @@ impl SqlExecutor {
|
||||
|
||||
// Trigger-Logik für CREATE TABLE
|
||||
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)?;
|
||||
}
|
||||
|
||||
@ -158,7 +164,13 @@ impl SqlExecutor {
|
||||
|
||||
// Trigger-Logik für CREATE TABLE
|
||||
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)?;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||
use crate::AppState;
|
||||
@ -110,7 +111,7 @@ pub async fn extension_sql_execute(
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||
// Get extension to retrieve its ID
|
||||
let extension = state
|
||||
.extension_manager
|
||||
@ -129,58 +130,87 @@ pub async fn extension_sql_execute(
|
||||
// SQL parsing
|
||||
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
|
||||
with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
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
|
||||
let hlc_timestamp = state
|
||||
.hlc
|
||||
.lock()
|
||||
.unwrap()
|
||||
let hlc_timestamp = hlc_service
|
||||
.new_timestamp_and_persist(&tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
// Transform statements
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
for statement in &mut ast_vec {
|
||||
if let Some(table_name) =
|
||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
||||
{
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
}
|
||||
// Transform statement
|
||||
transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
|
||||
|
||||
// Convert parameters
|
||||
// Convert parameters to references
|
||||
let sql_values = ValueConverter::convert_params(¶ms)?;
|
||||
let param_refs: Vec<&dyn rusqlite::ToSql> = sql_values.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
|
||||
|
||||
// Execute statements
|
||||
for statement in ast_vec {
|
||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
||||
let result = if has_returning {
|
||||
// Use query_internal for statements with RETURNING
|
||||
let (_, rows) = SqlExecutor::query_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶m_refs)?;
|
||||
rows
|
||||
} else {
|
||||
// Use execute_internal for statements without RETURNING
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶m_refs)?;
|
||||
vec![]
|
||||
};
|
||||
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
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
|
||||
);
|
||||
}
|
||||
// Handle CREATE TABLE trigger setup
|
||||
if let Statement::CreateTable(ref create_table_details) = statement {
|
||||
// Extract table name and remove quotes (both " and `)
|
||||
let raw_name = create_table_details.name.to_string();
|
||||
println!("DEBUG: Raw table name from AST: {:?}", raw_name);
|
||||
println!("DEBUG: Raw table name chars: {:?}", raw_name.chars().collect::<Vec<_>>());
|
||||
|
||||
let table_name_str = raw_name
|
||||
.trim_matches('"')
|
||||
.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
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
|
||||
Ok(modified_schema_tables.into_iter().collect())
|
||||
Ok(result)
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
@ -192,7 +222,7 @@ pub async fn extension_sql_select(
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||
// Get extension to retrieve its ID
|
||||
let extension = state
|
||||
.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| {
|
||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
@ -245,51 +274,34 @@ pub async fn extension_sql_select(
|
||||
table: None,
|
||||
})?;
|
||||
|
||||
let column_names: Vec<String> = prepared_stmt
|
||||
.column_names()
|
||||
.into_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)
|
||||
})
|
||||
let num_columns = prepared_stmt.column_count();
|
||||
let mut rows = prepared_stmt
|
||||
.query(params_from_iter(sql_params.iter()))
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row_result in rows {
|
||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: e.to_string(),
|
||||
})?);
|
||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/// src-tauri/src/extension/mod.rs
|
||||
use crate::{
|
||||
extension::{
|
||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
error::ExtensionError,
|
||||
},
|
||||
AppState,
|
||||
@ -37,7 +37,7 @@ pub async fn get_all_extensions(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<ExtensionInfoResponse>, String> {
|
||||
// Check if extensions are loaded, if not load them first
|
||||
let needs_loading = {
|
||||
/* let needs_loading = {
|
||||
let prod_exts = state
|
||||
.extension_manager
|
||||
.production_extensions
|
||||
@ -45,15 +45,15 @@ pub async fn get_all_extensions(
|
||||
.unwrap();
|
||||
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
|
||||
prod_exts.is_empty() && dev_exts.is_empty()
|
||||
};
|
||||
}; */
|
||||
|
||||
if needs_loading {
|
||||
state
|
||||
.extension_manager
|
||||
.load_installed_extensions(&app_handle, &state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to load extensions: {:?}", e))?;
|
||||
}
|
||||
/* if needs_loading { */
|
||||
state
|
||||
.extension_manager
|
||||
.load_installed_extensions(&app_handle, &state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to load extensions: {:?}", e))?;
|
||||
/* } */
|
||||
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn preview_extension(
|
||||
app_handle: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
file_bytes: Vec<u8>,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.preview_extension_internal(file_bytes)
|
||||
.preview_extension_internal(&app_handle, file_bytes)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -193,13 +194,7 @@ pub async fn remove_extension(
|
||||
) -> Result<(), ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.remove_extension_internal(
|
||||
&app_handle,
|
||||
&public_key,
|
||||
&name,
|
||||
&version,
|
||||
&state,
|
||||
)
|
||||
.remove_extension_internal(&app_handle, &public_key, &name, &version, &state)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -223,6 +218,16 @@ pub fn is_extension_installed(
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct HaextensionConfig {
|
||||
dev: DevConfig,
|
||||
#[serde(default)]
|
||||
keys: KeysConfig,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, Default)]
|
||||
struct KeysConfig {
|
||||
#[serde(default)]
|
||||
public_key_path: Option<String>,
|
||||
#[serde(default)]
|
||||
private_key_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
@ -231,6 +236,8 @@ struct DevConfig {
|
||||
port: u16,
|
||||
#[serde(default = "default_host")]
|
||||
host: String,
|
||||
#[serde(default = "default_haextension_dir")]
|
||||
haextension_dir: String,
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
@ -241,10 +248,14 @@ fn default_host() -> String {
|
||||
"localhost".to_string()
|
||||
}
|
||||
|
||||
fn default_haextension_dir() -> String {
|
||||
"haextension".to_string()
|
||||
}
|
||||
|
||||
/// Check if a dev server is reachable by making a simple HTTP request
|
||||
async fn check_dev_server_health(url: &str) -> bool {
|
||||
use tauri_plugin_http::reqwest;
|
||||
use std::time::Duration;
|
||||
use tauri_plugin_http::reqwest;
|
||||
|
||||
// Try to connect with a short timeout
|
||||
let client = reqwest::Client::builder()
|
||||
@ -276,29 +287,28 @@ pub async fn load_dev_extension(
|
||||
|
||||
let extension_path_buf = PathBuf::from(&extension_path);
|
||||
|
||||
// 1. Read haextension.json to get dev server config
|
||||
let config_path = extension_path_buf.join("haextension.json");
|
||||
let (host, port) = if config_path.exists() {
|
||||
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
|
||||
ExtensionError::ValidationError {
|
||||
reason: format!("Failed to read haextension.json: {}", e),
|
||||
}
|
||||
})?;
|
||||
// 1. Read haextension.config.json to get dev server config and haextension directory
|
||||
let config_path = extension_path_buf.join("haextension.config.json");
|
||||
let (host, port, haextension_dir) = if config_path.exists() {
|
||||
let config_content =
|
||||
std::fs::read_to_string(&config_path).map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Failed to read haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
|
||||
ExtensionError::ValidationError {
|
||||
reason: format!("Failed to parse haextension.json: {}", e),
|
||||
}
|
||||
})?;
|
||||
let config: HaextensionConfig =
|
||||
serde_json::from_str(&config_content).map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Failed to parse haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
(config.dev.host, config.dev.port)
|
||||
(config.dev.host, config.dev.port, config.dev.haextension_dir)
|
||||
} else {
|
||||
// Default values if config doesn't exist
|
||||
(default_host(), default_port())
|
||||
(default_host(), default_port(), default_haextension_dir())
|
||||
};
|
||||
|
||||
let dev_server_url = format!("http://{}:{}", host, port);
|
||||
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
||||
eprintln!("📁 Haextension directory: {}", haextension_dir);
|
||||
|
||||
// 1.5. Check if dev server is running
|
||||
if !check_dev_server_health(&dev_server_url).await {
|
||||
@ -311,35 +321,30 @@ pub async fn load_dev_extension(
|
||||
}
|
||||
eprintln!("✅ Dev server is reachable");
|
||||
|
||||
// 2. Build path to manifest: <extension_path>/haextension/manifest.json
|
||||
let manifest_path = extension_path_buf.join("haextension").join("manifest.json");
|
||||
|
||||
// Check if manifest exists
|
||||
if !manifest_path.exists() {
|
||||
return Err(ExtensionError::ManifestError {
|
||||
reason: format!(
|
||||
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||
manifest_path.display()
|
||||
),
|
||||
});
|
||||
}
|
||||
// 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
let manifest_path = ExtensionManager::validate_path_in_directory(
|
||||
&extension_path_buf,
|
||||
&manifest_relative_path,
|
||||
true,
|
||||
)?
|
||||
.ok_or_else(|| ExtensionError::ManifestError {
|
||||
reason: format!(
|
||||
"Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||
haextension_dir
|
||||
),
|
||||
})?;
|
||||
|
||||
// 3. Read and parse manifest
|
||||
let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| {
|
||||
ExtensionError::ManifestError {
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Failed to read manifest: {}", e),
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name>
|
||||
let key_prefix = manifest
|
||||
.public_key
|
||||
.chars()
|
||||
.take(8)
|
||||
.collect::<String>();
|
||||
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
|
||||
// 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
|
||||
let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
|
||||
|
||||
// 5. Check if dev extension already exists (allow reload)
|
||||
if let Some(existing) = state
|
||||
@ -387,13 +392,11 @@ pub fn remove_dev_extension(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
// Only remove from dev_extensions, not production_extensions
|
||||
let mut dev_exts = state
|
||||
.extension_manager
|
||||
.dev_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
let mut dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| {
|
||||
ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
|
||||
// Find and remove by public_key and name
|
||||
let to_remove = dev_exts
|
||||
@ -406,10 +409,7 @@ pub fn remove_dev_extension(
|
||||
eprintln!("✅ Dev extension removed: {}", name);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ExtensionError::NotFound {
|
||||
public_key,
|
||||
name,
|
||||
})
|
||||
Err(ExtensionError::NotFound { public_key, name })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -197,6 +197,30 @@ impl PermissionManager {
|
||||
action: Action,
|
||||
table_name: &str,
|
||||
) -> 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 has_permission = permissions
|
||||
@ -205,7 +229,7 @@ impl PermissionManager {
|
||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||
.any(|perm| {
|
||||
if perm.target != "*" && perm.target != table_name {
|
||||
if perm.target != "*" && perm.target != clean_table_name {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
|
||||
@ -68,6 +68,7 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
database::create_encrypted_database,
|
||||
database::delete_vault,
|
||||
database::move_vault_to_trash,
|
||||
database::list_vaults,
|
||||
database::open_encrypted_database,
|
||||
database::sql_execute_with_crdt,
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "haex-hub",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.4",
|
||||
"identifier": "space.haex.hub",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:3003",
|
||||
"beforeBuildCommand": "pnpm generate",
|
||||
"frontendDist": "../dist"
|
||||
"frontendDist": "../.output/public"
|
||||
},
|
||||
|
||||
"app": {
|
||||
@ -25,7 +25,8 @@
|
||||
"'self'",
|
||||
"http://tauri.localhost",
|
||||
"haex-extension:",
|
||||
"'wasm-unsafe-eval'"
|
||||
"'wasm-unsafe-eval'",
|
||||
"'unsafe-inline'"
|
||||
],
|
||||
"style-src": [
|
||||
"'self'",
|
||||
|
||||
@ -3,6 +3,8 @@ export default defineAppConfig({
|
||||
colors: {
|
||||
primary: 'sky',
|
||||
secondary: 'fuchsia',
|
||||
warning: 'yellow',
|
||||
danger: 'red',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<UApp :locale="locales[locale]">
|
||||
<div data-vaul-drawer-wrapper>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
@ -13,6 +13,46 @@
|
||||
[disabled] {
|
||||
@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 {
|
||||
/* Volle Höhe des body */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
/* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
|
||||
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);
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
|
||||
61
src/components/haex/debug/overlay.vue
Normal file
61
src/components/haex/debug/overlay.vue
Normal 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>
|
||||
@ -37,7 +37,7 @@
|
||||
:alt="label"
|
||||
class="w-14 h-14 object-contain transition-transform duration-200"
|
||||
:class="{ 'scale-110': isSelected }"
|
||||
>
|
||||
/>
|
||||
<UIcon
|
||||
v-else
|
||||
name="i-heroicons-puzzle-piece-solid"
|
||||
@ -69,7 +69,7 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
itemType: 'extension' | 'file' | 'folder'
|
||||
itemType: DesktopItemType
|
||||
referenceId: string
|
||||
initialX: number
|
||||
initialY: number
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="desktopEl"
|
||||
class="w-full h-full relative overflow-hidden isolate"
|
||||
class="absolute inset-0 overflow-hidden"
|
||||
>
|
||||
<Swiper
|
||||
:modules="[SwiperNavigation]"
|
||||
@ -13,7 +13,7 @@
|
||||
:no-swiping="true"
|
||||
no-swiping-class="no-swipe"
|
||||
:allow-touch-move="allowSwipe"
|
||||
class="w-full h-full"
|
||||
class="h-full w-full"
|
||||
direction="vertical"
|
||||
@swiper="onSwiperInit"
|
||||
@slide-change="onSlideChange"
|
||||
@ -24,9 +24,11 @@
|
||||
class="w-full h-full"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full relative isolate"
|
||||
class="w-full h-full relative"
|
||||
@click.self.stop="handleDesktopClick"
|
||||
@mousedown.left.self="handleAreaSelectStart"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@drop.prevent="handleDrop($event, workspace.id)"
|
||||
>
|
||||
<!-- Grid Pattern Background -->
|
||||
<div
|
||||
@ -39,18 +41,16 @@
|
||||
/>
|
||||
|
||||
<!-- Snap Dropzones (only visible when window drag near edge) -->
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showLeftSnapZone"
|
||||
class="absolute left-0 top-0 bottom-0 w-1/2 bg-blue-500/20 border-2 border-blue-500 pointer-events-none backdrop-blur-sm z-40"
|
||||
/>
|
||||
</Transition>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showRightSnapZone"
|
||||
class="absolute right-0 top-0 bottom-0 w-1/2 bg-blue-500/20 border-2 border-blue-500 pointer-events-none backdrop-blur-sm z-40"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div
|
||||
class="absolute left-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
||||
:class="showLeftSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute right-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
||||
:class="showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
||||
/>
|
||||
|
||||
<!-- Area Selection Box -->
|
||||
<div
|
||||
@ -81,128 +81,131 @@
|
||||
v-for="window in getWorkspaceWindows(workspace.id)"
|
||||
:key="window.id"
|
||||
>
|
||||
<!-- Desktop container for when overview is closed -->
|
||||
<div
|
||||
:id="`desktop-container-${window.id}`"
|
||||
class="absolute"
|
||||
/>
|
||||
|
||||
<!-- Window with dynamic teleport -->
|
||||
<!-- Overview Mode: Teleport to window preview -->
|
||||
<Teleport
|
||||
:to="
|
||||
v-if="
|
||||
windowManager.showWindowOverview &&
|
||||
overviewWindowState.has(window.id)
|
||||
? `#window-preview-${window.id}`
|
||||
: `#desktop-container-${window.id}`
|
||||
"
|
||||
:to="`#window-preview-${window.id}`"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
windowManager.showWindowOverview &&
|
||||
overviewWindowState.has(window.id)
|
||||
"
|
||||
<div
|
||||
class="absolute origin-top-left"
|
||||
:style="{
|
||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute origin-top-left"
|
||||
:style="{
|
||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||
}"
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
:id="window.id"
|
||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||
v-model:height="overviewWindowState.get(window.id)!.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find(
|
||||
(ext) => ext.id === window.sourceId,
|
||||
)?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) =>
|
||||
windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
:id="window.id"
|
||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||
v-model:height="overviewWindowState.get(window.id)!.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) =>
|
||||
windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</div>
|
||||
</template>
|
||||
<HaexWindow
|
||||
v-else
|
||||
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||
:id="window.id"
|
||||
v-model:x="window.x"
|
||||
v-model:y="window.y"
|
||||
v-model:width="window.width"
|
||||
v-model:height="window.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Desktop Mode: Render directly in workspace -->
|
||||
<HaexWindow
|
||||
v-else
|
||||
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||
:id="window.id"
|
||||
v-model:x="window.x"
|
||||
v-model:y="window.y"
|
||||
v-model:width="window.width"
|
||||
v-model:height="window.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||
?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</template>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
@ -210,52 +213,6 @@
|
||||
|
||||
<!-- Window Overview Modal -->
|
||||
<HaexWindowOverview />
|
||||
|
||||
<!-- Workspace Drawer -->
|
||||
<UDrawer
|
||||
v-model:open="isOverviewMode"
|
||||
direction="left"
|
||||
:dismissible="false"
|
||||
:overlay="false"
|
||||
:modal="false"
|
||||
title="Workspaces"
|
||||
description="Workspaces"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-6 h-full overflow-y-auto">
|
||||
<UButton
|
||||
block
|
||||
trailing-icon="mdi-close"
|
||||
class="text-2xl font-bold ext-gray-900 dark:text-white mb-4"
|
||||
@click="isOverviewMode = false"
|
||||
>
|
||||
Workspaces
|
||||
</UButton>
|
||||
|
||||
<!-- Workspace Cards -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<HaexWorkspaceCard
|
||||
v-for="workspace in workspaces"
|
||||
:key="workspace.id"
|
||||
:workspace
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Add New Workspace Button -->
|
||||
<UButton
|
||||
block
|
||||
variant="outline"
|
||||
class="mt-6"
|
||||
@click="handleAddWorkspace"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-heroicons-plus" />
|
||||
</template>
|
||||
New Workspace
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -342,11 +299,24 @@ const getWorkspaceIcons = (workspaceId: string) => {
|
||||
return desktopItems.value
|
||||
.filter((item) => item.workspaceId === workspaceId)
|
||||
.map((item) => {
|
||||
if (item.itemType === 'system') {
|
||||
const systemWindow = windowManager
|
||||
.getAllSystemWindows()
|
||||
.find((win) => win.id === item.referenceId)
|
||||
|
||||
return {
|
||||
...item,
|
||||
label: systemWindow?.name || 'Unknown',
|
||||
icon: systemWindow?.icon || '',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.itemType === 'extension') {
|
||||
const extension = availableExtensions.value.find(
|
||||
(ext) => ext.id === item.referenceId,
|
||||
)
|
||||
|
||||
console.log('found ext', extension)
|
||||
return {
|
||||
...item,
|
||||
label: extension?.name || 'Unknown',
|
||||
@ -416,6 +386,53 @@ const handleDragEnd = async () => {
|
||||
allowSwipe.value = true // Re-enable Swiper after drag
|
||||
}
|
||||
|
||||
// Handle drag over for launcher items
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
if (!event.dataTransfer) return
|
||||
|
||||
// Check if this is a launcher item
|
||||
if (event.dataTransfer.types.includes('application/haex-launcher-item')) {
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
// Handle drop for launcher items
|
||||
const handleDrop = async (event: DragEvent, workspaceId: string) => {
|
||||
if (!event.dataTransfer) return
|
||||
|
||||
const launcherItemData = event.dataTransfer.getData(
|
||||
'application/haex-launcher-item',
|
||||
)
|
||||
if (!launcherItemData) return
|
||||
|
||||
try {
|
||||
const item = JSON.parse(launcherItemData) as {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
type: 'system' | 'extension'
|
||||
}
|
||||
|
||||
// Get drop position relative to desktop
|
||||
const desktopRect = (
|
||||
event.currentTarget as HTMLElement
|
||||
).getBoundingClientRect()
|
||||
const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
||||
const y = Math.max(0, event.clientY - desktopRect.top - 32)
|
||||
|
||||
// Create desktop icon on the specific workspace
|
||||
await desktopStore.addDesktopItemAsync(
|
||||
item.type as DesktopItemType,
|
||||
item.id,
|
||||
x,
|
||||
y,
|
||||
workspaceId,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to create desktop icon:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDesktopClick = () => {
|
||||
// Only clear selection if it was a simple click, not an area selection
|
||||
// Check if we just finished an area selection (box size > threshold)
|
||||
@ -444,6 +461,34 @@ const handleWindowDragStart = (windowId: string) => {
|
||||
|
||||
const handleWindowDragEnd = async () => {
|
||||
console.log('[Desktop] handleWindowDragEnd')
|
||||
|
||||
// Check if window should snap to left or right
|
||||
const draggingWindowId = windowManager.draggingWindowId
|
||||
|
||||
if (draggingWindowId) {
|
||||
if (showLeftSnapZone.value) {
|
||||
// Snap to left half
|
||||
windowManager.updateWindowPosition(draggingWindowId, 0, 0)
|
||||
windowManager.updateWindowSize(
|
||||
draggingWindowId,
|
||||
viewportWidth.value / 2,
|
||||
viewportHeight.value,
|
||||
)
|
||||
} else if (showRightSnapZone.value) {
|
||||
// Snap to right half
|
||||
windowManager.updateWindowPosition(
|
||||
draggingWindowId,
|
||||
viewportWidth.value / 2,
|
||||
0,
|
||||
)
|
||||
windowManager.updateWindowSize(
|
||||
draggingWindowId,
|
||||
viewportWidth.value / 2,
|
||||
viewportHeight.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
isWindowDragging.value = false
|
||||
windowManager.draggingWindowId = null // Clear from store
|
||||
allowSwipe.value = true // Re-enable Swiper after drag
|
||||
@ -523,17 +568,6 @@ const onSlideChange = (swiper: SwiperType) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Workspace control handlers
|
||||
const handleAddWorkspace = async () => {
|
||||
await workspaceStore.addWorkspaceAsync()
|
||||
// Swiper will auto-slide to new workspace because we switch in addWorkspaceAsync
|
||||
nextTick(() => {
|
||||
if (swiperInstance.value) {
|
||||
swiperInstance.value.slideTo(workspaces.value.length - 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* const handleRemoveWorkspace = async () => {
|
||||
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
||||
|
||||
@ -569,7 +603,10 @@ const MAX_PREVIEW_HEIGHT = 450 // 50% increase from 300
|
||||
|
||||
// Store window state for overview (position only, size stays original)
|
||||
const overviewWindowState = ref(
|
||||
new Map<string, { x: number; y: number; width: number; height: number; scale: number }>(),
|
||||
new Map<
|
||||
string,
|
||||
{ x: number; y: number; width: number; height: number; scale: number }
|
||||
>(),
|
||||
)
|
||||
|
||||
// Calculate scale and card dimensions for each window
|
||||
@ -593,7 +630,10 @@ watch(
|
||||
finalScale = MIN_PREVIEW_WIDTH / window.width
|
||||
}
|
||||
if (scaledHeight < MIN_PREVIEW_HEIGHT) {
|
||||
finalScale = Math.max(finalScale, MIN_PREVIEW_HEIGHT / window.height)
|
||||
finalScale = Math.max(
|
||||
finalScale,
|
||||
MIN_PREVIEW_HEIGHT / window.height,
|
||||
)
|
||||
}
|
||||
|
||||
overviewWindowState.value.set(window.id, {
|
||||
|
||||
@ -89,7 +89,11 @@ const removeExtensionAsync = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await extensionStore.removeExtensionAsync(extension.id, extension.version)
|
||||
await extensionStore.removeExtensionAsync(
|
||||
extension.publicKey,
|
||||
extension.name,
|
||||
extension.version,
|
||||
)
|
||||
await extensionStore.loadExtensionsAsync()
|
||||
|
||||
add({
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
v-if="preview?.manifest.icon"
|
||||
class="w-16 h-16 flex-shrink-0"
|
||||
class="w-16 h-16 shrink-0"
|
||||
>
|
||||
<UIcon
|
||||
:name="preview.manifest.icon"
|
||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const permissionAccordionItems = computed(() => {
|
||||
const items = []
|
||||
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
<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
|
||||
icon="material-symbols:apps"
|
||||
color="neutral"
|
||||
@ -9,48 +17,75 @@
|
||||
/>
|
||||
|
||||
<template #content>
|
||||
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
||||
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
||||
<UiButton
|
||||
v-for="item in launcherItems"
|
||||
:key="item.id"
|
||||
square
|
||||
size="xl"
|
||||
variant="ghost"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
:icon="item.icon"
|
||||
:label="item.name"
|
||||
:tooltip="item.name"
|
||||
@click="openItem(item)"
|
||||
/>
|
||||
<div class="p-4 h-full overflow-y-auto">
|
||||
<div class="flex flex-wrap">
|
||||
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
||||
<UContextMenu
|
||||
v-for="item in launcherItems"
|
||||
: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
|
||||
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>
|
||||
<!-- 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')})`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</UDrawer>
|
||||
|
||||
<!-- Uninstall Confirmation Dialog -->
|
||||
<UiDialogConfirm
|
||||
v-model:open="showUninstallDialog"
|
||||
:title="t('uninstall.confirm.title')"
|
||||
:description="
|
||||
t('uninstall.confirm.description', {
|
||||
name: extensionToUninstall?.name || '',
|
||||
})
|
||||
"
|
||||
:confirm-label="t('uninstall.confirm.button')"
|
||||
confirm-icon="i-heroicons-trash"
|
||||
@confirm="confirmUninstall"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const extensionStore = useExtensionsStore()
|
||||
const windowManagerStore = useWindowManagerStore()
|
||||
|
||||
@ -58,6 +93,10 @@ const { t } = useI18n()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
// Uninstall dialog state
|
||||
const showUninstallDialog = ref(false)
|
||||
const extensionToUninstall = ref<LauncherItem | null>(null)
|
||||
|
||||
// Unified launcher item type
|
||||
interface LauncherItem {
|
||||
id: string
|
||||
@ -119,14 +158,123 @@ const openItem = async (item: LauncherItem) => {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall extension - shows confirmation dialog first
|
||||
const uninstallExtension = async (item: LauncherItem) => {
|
||||
extensionToUninstall.value = item
|
||||
showUninstallDialog.value = true
|
||||
}
|
||||
|
||||
// Confirm uninstall - actually removes the extension
|
||||
const confirmUninstall = async () => {
|
||||
if (!extensionToUninstall.value) return
|
||||
|
||||
try {
|
||||
const extension = extensionStore.availableExtensions.find(
|
||||
(ext) => ext.id === extensionToUninstall.value!.id,
|
||||
)
|
||||
if (!extension) return
|
||||
|
||||
// Close all windows of this extension first
|
||||
const extensionWindows = windowManagerStore.windows.filter(
|
||||
(win) => win.type === 'extension' && win.sourceId === extension.id,
|
||||
)
|
||||
|
||||
for (const win of extensionWindows) {
|
||||
windowManagerStore.closeWindow(win.id)
|
||||
}
|
||||
|
||||
// Uninstall the extension
|
||||
await extensionStore.removeExtensionAsync(
|
||||
extension.publicKey,
|
||||
extension.name,
|
||||
extension.version,
|
||||
)
|
||||
|
||||
// Refresh available extensions list
|
||||
await extensionStore.loadExtensionsAsync()
|
||||
|
||||
// Close dialog and reset state
|
||||
showUninstallDialog.value = false
|
||||
extensionToUninstall.value = null
|
||||
} catch (error) {
|
||||
console.error('Failed to uninstall extension:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get context menu items for launcher item
|
||||
const getContextMenuItems = (item: LauncherItem) => {
|
||||
const items = [
|
||||
{
|
||||
label: t('contextMenu.open'),
|
||||
icon: 'i-heroicons-arrow-top-right-on-square',
|
||||
onSelect: () => openItem(item),
|
||||
},
|
||||
]
|
||||
|
||||
// Add uninstall option for extensions
|
||||
if (item.type === 'extension') {
|
||||
items.push({
|
||||
label: t('contextMenu.uninstall'),
|
||||
icon: 'i-heroicons-trash',
|
||||
onSelect: () => uninstallExtension(item),
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// Drag & Drop handling
|
||||
const handleDragStart = (event: DragEvent, item: LauncherItem) => {
|
||||
if (!event.dataTransfer) return
|
||||
|
||||
// Store the launcher item data
|
||||
event.dataTransfer.effectAllowed = 'copy'
|
||||
event.dataTransfer.setData(
|
||||
'application/haex-launcher-item',
|
||||
JSON.stringify(item),
|
||||
)
|
||||
|
||||
// Set drag image (optional - uses default if not set)
|
||||
const dragImage = event.target as HTMLElement
|
||||
if (dragImage) {
|
||||
event.dataTransfer.setDragImage(dragImage, 20, 20)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
// Cleanup if needed
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
disabled: Deaktiviert
|
||||
marketplace: Marketplace
|
||||
launcher:
|
||||
title: App Launcher
|
||||
description: Wähle eine App zum Öffnen
|
||||
contextMenu:
|
||||
open: Öffnen
|
||||
uninstall: Deinstallieren
|
||||
uninstall:
|
||||
confirm:
|
||||
title: Erweiterung deinstallieren
|
||||
description: Möchtest du wirklich "{name}" deinstallieren? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
button: Deinstallieren
|
||||
|
||||
en:
|
||||
disabled: Disabled
|
||||
marketplace: Marketplace
|
||||
launcher:
|
||||
title: App Launcher
|
||||
description: Select an app to open
|
||||
contextMenu:
|
||||
open: Open
|
||||
uninstall: Uninstall
|
||||
uninstall:
|
||||
confirm:
|
||||
title: Uninstall Extension
|
||||
description: Do you really want to uninstall "{name}"? This action cannot be undone.
|
||||
button: Uninstall
|
||||
</i18n>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<!-- Icon -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
v-if="extension.icon"
|
||||
class="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center"
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<UiDialogConfirm
|
||||
:confirm-label="t('create')"
|
||||
@confirm="onCreateAsync"
|
||||
:description="t('description')"
|
||||
>
|
||||
<UiButton
|
||||
:label="t('vault.create')"
|
||||
@ -55,7 +56,9 @@
|
||||
<script setup lang="ts">
|
||||
import { vaultSchema } from './schema'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
})
|
||||
|
||||
const vault = reactive<{
|
||||
name: string
|
||||
@ -118,6 +121,7 @@ de:
|
||||
name: HaexVault
|
||||
title: Neue {haexvault} erstellen
|
||||
create: Erstellen
|
||||
description: Erstelle eine neue Vault für deine Daten
|
||||
|
||||
en:
|
||||
vault:
|
||||
@ -127,4 +131,5 @@ en:
|
||||
name: HaexVault
|
||||
title: Create new {haexvault}
|
||||
create: Create
|
||||
description: Create a new vault for your data
|
||||
</i18n>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
:description="vault.path || path"
|
||||
@confirm="onOpenDatabase"
|
||||
>
|
||||
<!-- <UiButton
|
||||
<UiButton
|
||||
:label="t('vault.open')"
|
||||
:ui="{
|
||||
base: 'px-3 py-2',
|
||||
@ -14,8 +14,7 @@
|
||||
size="xl"
|
||||
variant="outline"
|
||||
block
|
||||
@click.stop="onLoadDatabase"
|
||||
/> -->
|
||||
/>
|
||||
|
||||
<template #title>
|
||||
<i18n-t
|
||||
@ -59,7 +58,9 @@ const props = defineProps<{
|
||||
path?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
})
|
||||
|
||||
const vault = reactive<{
|
||||
name: string
|
||||
|
||||
83
src/components/haex/window/button.vue
Normal file
83
src/components/haex/window/button.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<UTooltip :text="tooltip">
|
||||
<button
|
||||
class="size-8 shrink-0 rounded-lg flex justify-center transition-colors group"
|
||||
:class="variantClasses.buttonClass"
|
||||
@click="(e) => $emit('click', e)"
|
||||
>
|
||||
<UIcon
|
||||
:name="icon"
|
||||
class="size-4 text-gray-600 dark:text-gray-400"
|
||||
:class="variantClasses.iconClass"
|
||||
/>
|
||||
</button>
|
||||
</UTooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
variant: 'close' | 'maximize' | 'minimize'
|
||||
isMaximized?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits(['click'])
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (props.variant) {
|
||||
case 'close':
|
||||
return 'i-heroicons-x-mark'
|
||||
case 'maximize':
|
||||
return props.isMaximized
|
||||
? 'i-heroicons-arrows-pointing-in'
|
||||
: 'i-heroicons-arrows-pointing-out'
|
||||
default:
|
||||
return 'i-heroicons-minus'
|
||||
}
|
||||
})
|
||||
|
||||
const variantClasses = computed(() => {
|
||||
if (props.variant === 'close') {
|
||||
return {
|
||||
iconClass: 'group-hover:text-error',
|
||||
buttonClass: 'hover:bg-error/30 items-center',
|
||||
}
|
||||
} else if (props.variant === 'maximize') {
|
||||
return {
|
||||
iconClass: 'group-hover:text-warning',
|
||||
buttonClass: 'hover:bg-warning/30 items-center',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
iconClass: 'group-hover:text-success',
|
||||
buttonClass: 'hover:bg-success/30 items-end pb-1',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const tooltip = computed(() => {
|
||||
switch (props.variant) {
|
||||
case 'close':
|
||||
return t('close')
|
||||
case 'maximize':
|
||||
return props.isMaximized ? t('shrink') : t('maximize')
|
||||
default:
|
||||
return t('minimize')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
close: Schließen
|
||||
maximize: Maximieren
|
||||
shrink: Verkleinern
|
||||
minimize: Minimieren
|
||||
|
||||
en:
|
||||
close: Close
|
||||
maximize: Maximize
|
||||
shrink: Shrink
|
||||
minimize: Minimize
|
||||
</i18n>
|
||||
@ -3,11 +3,17 @@
|
||||
ref="windowEl"
|
||||
:style="windowStyle"
|
||||
:class="[
|
||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden isolate',
|
||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
|
||||
'transition-all ease-out duration-600',
|
||||
'flex flex-col @container',
|
||||
{ 'select-none': isResizingOrDragging },
|
||||
isActive ? 'z-100' : 'z-50',
|
||||
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"
|
||||
>
|
||||
@ -38,37 +44,21 @@
|
||||
|
||||
<!-- Right: Window Controls -->
|
||||
<div class="flex items-center gap-1 justify-end">
|
||||
<button
|
||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
||||
<HaexWindowButton
|
||||
variant="minimize"
|
||||
@click.stop="handleMinimize"
|
||||
>
|
||||
<UIcon
|
||||
name="i-heroicons-minus"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
||||
/>
|
||||
|
||||
<HaexWindowButton
|
||||
:is-maximized
|
||||
variant="maximize"
|
||||
@click.stop="handleMaximize"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
isMaximized
|
||||
? 'i-heroicons-arrows-pointing-in'
|
||||
: 'i-heroicons-arrows-pointing-out'
|
||||
"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="w-8 h-8 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 flex items-center justify-center transition-colors group"
|
||||
/>
|
||||
|
||||
<HaexWindowButton
|
||||
variant="close"
|
||||
@click.stop="handleClose"
|
||||
>
|
||||
<UIcon
|
||||
name="i-heroicons-x-mark"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-red-600 dark:group-hover:text-red-400"
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,6 +92,7 @@ const props = defineProps<{
|
||||
sourceHeight?: number
|
||||
isOpening?: boolean
|
||||
isClosing?: boolean
|
||||
warningLevel?: 'warning' | 'danger' // Warning indicator (e.g., dev extension, dangerous permissions)
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -331,13 +322,33 @@ const handleMaximize = () => {
|
||||
const bounds = getViewportBounds()
|
||||
|
||||
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
|
||||
y.value = 0
|
||||
y.value = 0 // Start below header and status bar
|
||||
width.value = bounds.width
|
||||
height.value = bounds.height
|
||||
// Height: viewport - header - both safe-areas
|
||||
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||
isMaximized.value = true
|
||||
}
|
||||
console.log('handleMaximize', preMaximizeState, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,90 +1,76 @@
|
||||
<template>
|
||||
<UModal
|
||||
<UDrawer
|
||||
v-model:open="localShowWindowOverview"
|
||||
direction="bottom"
|
||||
:title="t('modal.title')"
|
||||
:description="t('modal.description')"
|
||||
fullscreen
|
||||
>
|
||||
<template #content>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<div class="h-full overflow-y-auto p-6 justify-center flex">
|
||||
<!-- Window Thumbnails Flex Layout -->
|
||||
|
||||
<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
|
||||
v-if="windows.length > 0"
|
||||
class="flex flex-wrap gap-6 justify-start items-start"
|
||||
v-for="window in windows"
|
||||
:key="window.id"
|
||||
class="relative group cursor-pointer"
|
||||
>
|
||||
<div
|
||||
v-for="window in windows"
|
||||
:key="window.id"
|
||||
class="relative group cursor-pointer"
|
||||
>
|
||||
<!-- Window Title Bar -->
|
||||
<div class="flex items-center gap-3 mb-2 px-2">
|
||||
<UIcon
|
||||
v-if="window.icon"
|
||||
:name="window.icon"
|
||||
class="size-5 shrink-0"
|
||||
/>
|
||||
<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')"
|
||||
/>
|
||||
<!-- Window Title Bar -->
|
||||
<div class="flex items-center gap-3 mb-2 px-2">
|
||||
<UIcon
|
||||
v-if="window.icon"
|
||||
:name="window.icon"
|
||||
class="size-5 shrink-0"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<!-- 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
|
||||
: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
|
||||
class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<UIcon
|
||||
name="i-heroicons-window"
|
||||
class="size-16 mb-4 shrink-0"
|
||||
/>
|
||||
<p class="text-lg font-medium">No windows open</p>
|
||||
<p class="text-sm">
|
||||
Open an extension or system window to see it here
|
||||
</p>
|
||||
</div>
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<UIcon
|
||||
name="i-heroicons-window"
|
||||
class="size-16 mb-4 shrink-0"
|
||||
/>
|
||||
<p class="text-lg font-medium">No windows open</p>
|
||||
<p class="text-sm">
|
||||
Open an extension or system window to see it here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</UDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
28
src/components/ui/button/context.vue
Normal file
28
src/components/ui/button/context.vue
Normal 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>
|
||||
@ -4,11 +4,10 @@
|
||||
<UButton
|
||||
class="pointer-events-auto"
|
||||
v-bind="{
|
||||
...{ size: isSmallScreen ? 'lg' : 'md' },
|
||||
...buttonProps,
|
||||
...$attrs,
|
||||
}"
|
||||
@click="(e) => $emit('click', e)"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<template
|
||||
v-for="(_, slotName) in $slots"
|
||||
|
||||
@ -11,10 +11,6 @@ const { availableThemes, currentTheme } = storeToRefs(useUiStore())
|
||||
|
||||
const emit = defineEmits<{ select: [string] }>()
|
||||
|
||||
watchImmediate(availableThemes, () =>
|
||||
console.log('availableThemes', availableThemes),
|
||||
)
|
||||
|
||||
const items = computed<DropdownMenuItem[]>(() =>
|
||||
availableThemes?.value.map((theme) => ({
|
||||
...theme,
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
// composables/extensionContextBroadcast.ts
|
||||
// NOTE: This composable is deprecated. Use tabsStore.broadcastToAllTabs() instead.
|
||||
// Keeping for backwards compatibility.
|
||||
|
||||
import { getExtensionWindow } from './extensionMessageHandler'
|
||||
|
||||
export const useExtensionContextBroadcast = () => {
|
||||
// Globaler State für Extension IDs statt IFrames
|
||||
const extensionIds = useState<Set<string>>(
|
||||
'extension-ids',
|
||||
() => new Set(),
|
||||
)
|
||||
|
||||
const registerExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
||||
extensionIds.value.add(extensionId)
|
||||
}
|
||||
|
||||
const unregisterExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
||||
extensionIds.value.delete(extensionId)
|
||||
}
|
||||
|
||||
const broadcastContextChange = (context: {
|
||||
theme: string
|
||||
locale: string
|
||||
platform: string
|
||||
}) => {
|
||||
const message = {
|
||||
type: 'context.changed',
|
||||
data: { context },
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
extensionIds.value.forEach((extensionId) => {
|
||||
const win = getExtensionWindow(extensionId)
|
||||
if (win) {
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const broadcastSearchRequest = (query: string, requestId: string) => {
|
||||
const message = {
|
||||
type: 'search.request',
|
||||
data: {
|
||||
query: { query, limit: 10 },
|
||||
requestId,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
extensionIds.value.forEach((extensionId) => {
|
||||
const win = getExtensionWindow(extensionId)
|
||||
if (win) {
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
registerExtensionIframe,
|
||||
unregisterExtensionIframe,
|
||||
broadcastContextChange,
|
||||
broadcastSearchRequest,
|
||||
}
|
||||
}
|
||||
@ -166,20 +166,18 @@ const registerGlobalMessageHandler = () => {
|
||||
try {
|
||||
let result: unknown
|
||||
|
||||
if (request.method.startsWith('extension.')) {
|
||||
result = await handleExtensionMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('db.')) {
|
||||
result = await handleDatabaseMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('fs.')) {
|
||||
result = await handleFilesystemMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('http.')) {
|
||||
result = await handleHttpMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('permissions.')) {
|
||||
result = await handlePermissionsMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('context.')) {
|
||||
if (request.method.startsWith('haextension.context.')) {
|
||||
result = await handleContextMethodAsync(request)
|
||||
} else if (request.method.startsWith('storage.')) {
|
||||
} else if (request.method.startsWith('haextension.storage.')) {
|
||||
result = await handleStorageMethodAsync(request, instance)
|
||||
} else if (request.method.startsWith('haextension.db.')) {
|
||||
result = await handleDatabaseMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('haextension.fs.')) {
|
||||
result = await handleFilesystemMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('haextension.http.')) {
|
||||
result = await handleHttpMethodAsync(request, instance.extension)
|
||||
} else if (request.method.startsWith('haextension.permissions.')) {
|
||||
result = await handlePermissionsMethodAsync(request, instance.extension)
|
||||
} else {
|
||||
throw new Error(`Unknown method: ${request.method}`)
|
||||
}
|
||||
@ -328,30 +326,27 @@ export const getExtensionWindow = (extensionId: string): Window | undefined => {
|
||||
return getAllInstanceWindows(extensionId)[0]
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Extension Methods
|
||||
// ==========================================
|
||||
// Broadcast context changes to all extension instances
|
||||
export const broadcastContextToAllExtensions = (context: {
|
||||
theme: string
|
||||
locale: string
|
||||
platform?: string
|
||||
}) => {
|
||||
const message = {
|
||||
type: 'haextension.context.changed',
|
||||
data: { context },
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
async function handleExtensionMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'extension.getInfo': {
|
||||
const info = (await invoke('get_extension_info', {
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})) as Record<string, unknown>
|
||||
// Override allowedOrigin with the actual window origin
|
||||
// This fixes the dev-mode issue where Rust returns "tauri://localhost"
|
||||
// but the actual origin is "http://localhost:3003"
|
||||
return {
|
||||
...info,
|
||||
allowedOrigin: window.location.origin,
|
||||
}
|
||||
console.log('[ExtensionHandler] Broadcasting context to all extensions:', context)
|
||||
|
||||
// Send to all registered extension windows
|
||||
for (const [_, instance] of iframeRegistry.entries()) {
|
||||
const win = windowIdToWindowMap.get(instance.windowId)
|
||||
if (win) {
|
||||
console.log('[ExtensionHandler] Sending context to:', instance.extension.name, instance.windowId)
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown extension method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -369,11 +364,12 @@ async function handleDatabaseMethodAsync(
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
case 'db.query': {
|
||||
case 'haextension.db.query': {
|
||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -383,21 +379,22 @@ async function handleDatabaseMethodAsync(
|
||||
}
|
||||
}
|
||||
|
||||
case 'db.execute': {
|
||||
await invoke<string[]>('extension_sql_execute', {
|
||||
case 'haextension.db.execute': {
|
||||
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
rows,
|
||||
rowsAffected: 1,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
case 'db.transaction': {
|
||||
case 'haextension.db.transaction': {
|
||||
const statements =
|
||||
(request.params as { statements?: string[] }).statements || []
|
||||
|
||||
@ -405,7 +402,8 @@ async function handleDatabaseMethodAsync(
|
||||
await invoke('extension_sql_execute', {
|
||||
sql: stmt,
|
||||
params: [],
|
||||
extensionId: extension.id,
|
||||
publicKey: extension.publicKey,
|
||||
name: extension.name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -467,7 +465,7 @@ async function handlePermissionsMethodAsync(
|
||||
|
||||
async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||
switch (request.method) {
|
||||
case 'context.get':
|
||||
case 'haextension.context.get':
|
||||
if (!contextGetters) {
|
||||
throw new Error(
|
||||
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
||||
@ -499,25 +497,25 @@ async function handleStorageMethodAsync(
|
||||
)
|
||||
|
||||
switch (request.method) {
|
||||
case 'storage.getItem': {
|
||||
case 'haextension.storage.getItem': {
|
||||
const key = request.params.key as string
|
||||
return localStorage.getItem(storageKey + key)
|
||||
}
|
||||
|
||||
case 'storage.setItem': {
|
||||
case 'haextension.storage.setItem': {
|
||||
const key = request.params.key as string
|
||||
const value = request.params.value as string
|
||||
localStorage.setItem(storageKey + key, value)
|
||||
return null
|
||||
}
|
||||
|
||||
case 'storage.removeItem': {
|
||||
case 'haextension.storage.removeItem': {
|
||||
const key = request.params.key as string
|
||||
localStorage.removeItem(storageKey + key)
|
||||
return null
|
||||
}
|
||||
|
||||
case 'storage.clear': {
|
||||
case 'haextension.storage.clear': {
|
||||
// Remove only instance-specific keys
|
||||
const keys = Object.keys(localStorage).filter((k) =>
|
||||
k.startsWith(storageKey),
|
||||
@ -526,7 +524,7 @@ async function handleStorageMethodAsync(
|
||||
return null
|
||||
}
|
||||
|
||||
case 'storage.keys': {
|
||||
case 'haextension.storage.keys': {
|
||||
// Return only instance-specific keys (without prefix)
|
||||
const keys = Object.keys(localStorage)
|
||||
.filter((k) => k.startsWith(storageKey))
|
||||
|
||||
@ -14,12 +14,20 @@ export function useAndroidBackButton() {
|
||||
|
||||
// Track navigation history manually
|
||||
router.afterEach((to, from) => {
|
||||
console.log('[AndroidBack] Navigation:', { to: to.path, from: from.path, stackSize: historyStack.value.length })
|
||||
console.log('[AndroidBack] Navigation:', {
|
||||
to: to.path,
|
||||
from: from.path,
|
||||
stackSize: historyStack.value.length,
|
||||
})
|
||||
|
||||
// If navigating forward (new page)
|
||||
if (from.path && to.path !== from.path && !historyStack.value.includes(to.path)) {
|
||||
if (
|
||||
from.path &&
|
||||
to.path !== from.path &&
|
||||
!historyStack.value.includes(to.path)
|
||||
) {
|
||||
historyStack.value.push(from.path)
|
||||
console.log('[AndroidBack] Added to stack:', from.path, 'Stack:', historyStack.value)
|
||||
//console.log('[AndroidBack] Added to stack:', from.path, 'Stack:', historyStack.value)
|
||||
}
|
||||
})
|
||||
|
||||
@ -31,7 +39,10 @@ export function useAndroidBackButton() {
|
||||
|
||||
// Listen to close requested event (triggered by Android back button)
|
||||
unlisten = await appWindow.onCloseRequested(async (event) => {
|
||||
console.log('[AndroidBack] Back button pressed, stack size:', historyStack.value.length)
|
||||
console.log(
|
||||
'[AndroidBack] Back button pressed, stack size:',
|
||||
historyStack.value.length,
|
||||
)
|
||||
|
||||
// Check if we have history
|
||||
if (historyStack.value.length > 0) {
|
||||
@ -40,7 +51,10 @@ export function useAndroidBackButton() {
|
||||
|
||||
// Remove current page from stack
|
||||
historyStack.value.pop()
|
||||
console.log('[AndroidBack] Going back, new stack size:', historyStack.value.length)
|
||||
console.log(
|
||||
'[AndroidBack] Going back, new stack size:',
|
||||
historyStack.value.length,
|
||||
)
|
||||
|
||||
// Navigate back in router
|
||||
router.back()
|
||||
|
||||
1
src/database/index.ts
Normal file
1
src/database/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as schema from './schemas'
|
||||
@ -1,12 +1,12 @@
|
||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
import tableNames from '~/database/tableNames.json'
|
||||
|
||||
export const haexCrdtLogs = sqliteTable(
|
||||
tableNames.haex.crdt.logs.name,
|
||||
{
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
haexTimestamp: text(tableNames.haex.crdt.logs.columns.haexTimestamp),
|
||||
tableName: text(tableNames.haex.crdt.logs.columns.tableName),
|
||||
rowPks: text(tableNames.haex.crdt.logs.columns.rowPks, { mode: 'json' }),
|
||||
@ -33,8 +33,8 @@ export const haexCrdtSnapshots = sqliteTable(
|
||||
tableNames.haex.crdt.snapshots.name,
|
||||
{
|
||||
snapshotId: text(tableNames.haex.crdt.snapshots.columns.snapshotId)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
created: text(),
|
||||
epochHlc: text(tableNames.haex.crdt.snapshots.columns.epochHlc),
|
||||
locationUrl: text(tableNames.haex.crdt.snapshots.columns.locationUrl),
|
||||
@ -45,8 +45,6 @@ export const haexCrdtSnapshots = sqliteTable(
|
||||
)
|
||||
|
||||
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs.name, {
|
||||
key: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
key: text().primaryKey(),
|
||||
value: text(),
|
||||
})
|
||||
@ -1,5 +1,6 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import {
|
||||
check,
|
||||
integer,
|
||||
sqliteTable,
|
||||
text,
|
||||
@ -7,31 +8,32 @@ import {
|
||||
type AnySQLiteColumn,
|
||||
type SQLiteColumnBuilderBase,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
import tableNames from '~/database/tableNames.json'
|
||||
|
||||
const crdtColumnNames = {
|
||||
haexTimestamp: 'haex_timestamp',
|
||||
}
|
||||
|
||||
// Helper function to add common CRDT columns ( haexTimestamp)
|
||||
export const withCrdtColumns = <
|
||||
T extends Record<string, SQLiteColumnBuilderBase>,
|
||||
>(
|
||||
columns: T,
|
||||
columnNames: { haexTimestamp: string },
|
||||
) => ({
|
||||
...columns,
|
||||
haexTimestamp: text(columnNames.haexTimestamp),
|
||||
haexTimestamp: text(crdtColumnNames.haexTimestamp),
|
||||
})
|
||||
|
||||
export const haexSettings = sqliteTable(
|
||||
tableNames.haex.settings.name,
|
||||
{
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
key: text(),
|
||||
type: text(),
|
||||
value: text(),
|
||||
|
||||
haexTimestamp: text(tableNames.haex.settings.columns.haexTimestamp),
|
||||
},
|
||||
}),
|
||||
(table) => [unique().on(table.key, table.type, table.value)],
|
||||
)
|
||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||
@ -39,22 +41,22 @@ export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
||||
|
||||
export const haexExtensions = sqliteTable(
|
||||
tableNames.haex.extensions.name,
|
||||
{
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
public_key: text().notNull(),
|
||||
name: text().notNull(),
|
||||
version: text().notNull(),
|
||||
author: text(),
|
||||
description: text(),
|
||||
entry: text().notNull().default('index.html'),
|
||||
entry: text().default('index.html'),
|
||||
homepage: text(),
|
||||
enabled: integer({ mode: 'boolean' }).default(true),
|
||||
icon: text(),
|
||||
signature: text().notNull(),
|
||||
haexTimestamp: text(tableNames.haex.extensions.columns.haexTimestamp),
|
||||
},
|
||||
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||
}),
|
||||
(table) => [
|
||||
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
||||
unique().on(table.public_key, table.name),
|
||||
@ -65,10 +67,10 @@ export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
||||
|
||||
export const haexExtensionPermissions = sqliteTable(
|
||||
tableNames.haex.extension_permissions.name,
|
||||
{
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
extensionId: text(tableNames.haex.extension_permissions.columns.extensionId)
|
||||
.notNull()
|
||||
.references((): AnySQLiteColumn => haexExtensions.id, {
|
||||
@ -87,10 +89,7 @@ export const haexExtensionPermissions = sqliteTable(
|
||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
haexTimestamp: text(
|
||||
tableNames.haex.extension_permissions.columns.haexTimestamp,
|
||||
),
|
||||
},
|
||||
}),
|
||||
(table) => [
|
||||
unique().on(
|
||||
table.extensionId,
|
||||
@ -107,8 +106,10 @@ export type SelecthaexExtensionPermissions =
|
||||
|
||||
export const haexNotifications = sqliteTable(
|
||||
tableNames.haex.notifications.name,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
alt: text(),
|
||||
date: text(),
|
||||
icon: text(),
|
||||
@ -120,26 +121,23 @@ export const haexNotifications = sqliteTable(
|
||||
type: text({
|
||||
enum: ['error', 'success', 'warning', 'info', 'log'],
|
||||
}).notNull(),
|
||||
haexTimestamp: text(tableNames.haex.notifications.columns.haexTimestamp),
|
||||
},
|
||||
}),
|
||||
)
|
||||
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
||||
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
||||
|
||||
export const haexWorkspaces = sqliteTable(
|
||||
tableNames.haex.workspaces.name,
|
||||
withCrdtColumns(
|
||||
{
|
||||
id: text(tableNames.haex.workspaces.columns.id)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
||||
position: integer(tableNames.haex.workspaces.columns.position)
|
||||
.notNull()
|
||||
.default(0),
|
||||
},
|
||||
tableNames.haex.workspaces.columns,
|
||||
),
|
||||
withCrdtColumns({
|
||||
id: text(tableNames.haex.workspaces.columns.id)
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
deviceId: text(tableNames.haex.workspaces.columns.deviceId).notNull(),
|
||||
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
||||
position: integer(tableNames.haex.workspaces.columns.position)
|
||||
.notNull()
|
||||
.default(0),
|
||||
}),
|
||||
(table) => [unique().on(table.position)],
|
||||
)
|
||||
export type InsertHaexWorkspaces = typeof haexWorkspaces.$inferInsert
|
||||
@ -147,29 +145,37 @@ export type SelectHaexWorkspaces = typeof haexWorkspaces.$inferSelect
|
||||
|
||||
export const haexDesktopItems = sqliteTable(
|
||||
tableNames.haex.desktop_items.name,
|
||||
withCrdtColumns(
|
||||
{
|
||||
id: text(tableNames.haex.desktop_items.columns.id)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
||||
.notNull()
|
||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||
enum: ['extension', 'file', 'folder'],
|
||||
}).notNull(),
|
||||
referenceId: text(
|
||||
tableNames.haex.desktop_items.columns.referenceId,
|
||||
).notNull(), // extensionId für extensions, filePath für files/folders
|
||||
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
||||
.notNull()
|
||||
.default(0),
|
||||
positionY: integer(tableNames.haex.desktop_items.columns.positionY)
|
||||
.notNull()
|
||||
.default(0),
|
||||
},
|
||||
tableNames.haex.desktop_items.columns,
|
||||
),
|
||||
withCrdtColumns({
|
||||
id: text(tableNames.haex.desktop_items.columns.id)
|
||||
.$defaultFn(() => crypto.randomUUID())
|
||||
.primaryKey(),
|
||||
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
||||
.notNull()
|
||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||
enum: ['system', 'extension', 'file', 'folder'],
|
||||
}).notNull(),
|
||||
// Für Extensions (wenn itemType = 'extension')
|
||||
extensionId: text(
|
||||
tableNames.haex.desktop_items.columns.extensionId,
|
||||
).references((): AnySQLiteColumn => haexExtensions.id, {
|
||||
onDelete: 'cascade',
|
||||
}),
|
||||
// Für System Windows (wenn itemType = 'system')
|
||||
systemWindowId: text(tableNames.haex.desktop_items.columns.systemWindowId),
|
||||
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
||||
.notNull()
|
||||
.default(0),
|
||||
positionY: integer(tableNames.haex.desktop_items.columns.positionY)
|
||||
.notNull()
|
||||
.default(0),
|
||||
}),
|
||||
(table) => [
|
||||
check(
|
||||
'item_reference',
|
||||
sql`(${table.itemType} = 'extension' AND ${table.extensionId} IS NOT NULL AND ${table.systemWindowId} IS NULL) OR (${table.itemType} = 'system' AND ${table.systemWindowId} IS NOT NULL AND ${table.extensionId} IS NULL) OR (${table.itemType} = 'file' AND ${table.systemWindowId} IS NOT NULL AND ${table.extensionId} IS NULL) OR (${table.itemType} = 'folder' AND ${table.systemWindowId} IS NOT NULL AND ${table.extensionId} IS NULL)`,
|
||||
),
|
||||
],
|
||||
)
|
||||
export type InsertHaexDesktopItems = typeof haexDesktopItems.$inferInsert
|
||||
export type SelectHaexDesktopItems = typeof haexDesktopItems.$inferSelect
|
||||
@ -67,6 +67,7 @@
|
||||
"name": "haex_workspaces",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"deviceId": "device_id",
|
||||
"name": "name",
|
||||
"position": "position",
|
||||
"createdAt": "created_at",
|
||||
@ -80,7 +81,8 @@
|
||||
"id": "id",
|
||||
"workspaceId": "workspace_id",
|
||||
"itemType": "item_type",
|
||||
"referenceId": "reference_id",
|
||||
"extensionId": "extension_id",
|
||||
"systemWindowId": "system_window_id",
|
||||
"positionX": "position_x",
|
||||
"positionY": "position_y",
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col w-full h-full overflow-hidden">
|
||||
<div ref="headerRef">
|
||||
<UPageHeader
|
||||
as="header"
|
||||
:ui="{
|
||||
root: [
|
||||
'bg-default border-b border-accented sticky top-0 z-50 pt-2 px-8 h-header',
|
||||
],
|
||||
wrapper: [
|
||||
'flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4',
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<UiLogoHaexhub class="size-12 shrink-0" />
|
||||
|
||||
<NuxtLinkLocale
|
||||
class="link text-base-content link-neutral text-xl font-semibold no-underline flex items-center"
|
||||
:to="{ name: 'desktop' }"
|
||||
>
|
||||
<UiTextGradient class="text-nowrap">
|
||||
{{ currentVaultName }}
|
||||
</UiTextGradient>
|
||||
</NuxtLinkLocale>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:block="isSmallScreen"
|
||||
icon="i-bi-person-workspace"
|
||||
size="lg"
|
||||
@click="isOverviewMode = !isOverviewMode"
|
||||
/>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:block="isSmallScreen"
|
||||
icon="i-heroicons-squares-2x2"
|
||||
size="lg"
|
||||
@click="showWindowOverview = !showWindowOverview"
|
||||
>
|
||||
<template v-if="openWindowsCount > 0" #trailing>
|
||||
<UBadge
|
||||
:label="openWindowsCount.toString()"
|
||||
color="primary"
|
||||
size="xs"
|
||||
/>
|
||||
</template>
|
||||
</UButton>
|
||||
<HaexExtensionLauncher :block="isSmallScreen" />
|
||||
</template>
|
||||
</UPageHeader>
|
||||
</div>
|
||||
|
||||
<main class="flex-1 overflow-hidden bg-elevated">
|
||||
<NuxtPage />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { currentVaultName } = storeToRefs(useVaultStore())
|
||||
|
||||
const { isSmallScreen } = storeToRefs(useUiStore())
|
||||
|
||||
const { isOverviewMode } = storeToRefs(useWorkspaceStore())
|
||||
|
||||
const { showWindowOverview, openWindowsCount } = storeToRefs(
|
||||
useWindowManagerStore(),
|
||||
)
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
vault:
|
||||
close: Vault schließen
|
||||
|
||||
sidebar:
|
||||
close: Sidebar ausblenden
|
||||
show: Sidebar anzeigen
|
||||
|
||||
search:
|
||||
label: Suche
|
||||
en:
|
||||
vault:
|
||||
close: Close vault
|
||||
sidebar:
|
||||
close: close sidebar
|
||||
show: show sidebar
|
||||
|
||||
search:
|
||||
label: Search
|
||||
</i18n>
|
||||
@ -1,5 +1,155 @@
|
||||
<template>
|
||||
<div class="bg-default isolate w-dvw h-dvh flex flex-col">
|
||||
<slot />
|
||||
<div class="w-full h-dvh flex flex-col">
|
||||
<UPageHeader
|
||||
ref="headerEl"
|
||||
as="header"
|
||||
:ui="{
|
||||
root: ['px-8 py-0'],
|
||||
wrapper: ['flex flex-row items-center justify-between gap-4'],
|
||||
}"
|
||||
>
|
||||
<template #default>
|
||||
<div class="flex justify-between items-center py-1">
|
||||
<div>
|
||||
<!-- <NuxtLinkLocale
|
||||
class="link text-base-content link-neutral text-xl font-semibold no-underline flex items-center"
|
||||
:to="{ name: 'desktop' }"
|
||||
>
|
||||
<UiTextGradient class="text-nowrap">
|
||||
{{ currentVaultName }}
|
||||
</UiTextGradient>
|
||||
</NuxtLinkLocale> -->
|
||||
<UiButton
|
||||
v-if="currentVaultId"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-bi-person-workspace"
|
||||
size="lg"
|
||||
:tooltip="t('workspaces.label')"
|
||||
@click="isOverviewMode = !isOverviewMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="!currentVaultId">
|
||||
<UiDropdownLocale @select="onSelectLocale" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-row gap-2"
|
||||
>
|
||||
<UButton
|
||||
v-if="openWindowsCount > 0"
|
||||
color="primary"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
@click="showWindowOverview = !showWindowOverview"
|
||||
>
|
||||
{{ openWindowsCount }}
|
||||
</UButton>
|
||||
<HaexExtensionLauncher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<main class="overflow-hidden relative bg-elevated h-full">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<!-- Workspace Drawer -->
|
||||
<UDrawer
|
||||
v-model:open="isOverviewMode"
|
||||
direction="left"
|
||||
:dismissible="false"
|
||||
:overlay="false"
|
||||
:modal="false"
|
||||
title="Workspaces"
|
||||
description="Workspaces"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-6 h-full overflow-y-auto">
|
||||
<UButton
|
||||
block
|
||||
trailing-icon="mdi-close"
|
||||
class="text-2xl font-bold ext-gray-900 dark:text-white mb-4"
|
||||
@click="isOverviewMode = false"
|
||||
>
|
||||
Workspaces
|
||||
</UButton>
|
||||
|
||||
<!-- Workspace Cards -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<HaexWorkspaceCard
|
||||
v-for="workspace in workspaces"
|
||||
:key="workspace.id"
|
||||
:workspace
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Add New Workspace Button -->
|
||||
<UButton
|
||||
block
|
||||
variant="outline"
|
||||
class="mt-6"
|
||||
@click="handleAddWorkspace"
|
||||
icon="i-heroicons-plus"
|
||||
:label="t('workspaces.add')"
|
||||
>
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
const { t, setLocale } = useI18n()
|
||||
const onSelectLocale = async (locale: Locale) => {
|
||||
await setLocale(locale)
|
||||
}
|
||||
|
||||
const { currentVaultId } = storeToRefs(useVaultStore())
|
||||
const { showWindowOverview, openWindowsCount } = storeToRefs(
|
||||
useWindowManagerStore(),
|
||||
)
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { workspaces, isOverviewMode } = storeToRefs(workspaceStore)
|
||||
|
||||
const handleAddWorkspace = async () => {
|
||||
const workspace = await workspaceStore.addWorkspaceAsync()
|
||||
nextTick(() => {
|
||||
workspaceStore.slideToWorkspace(workspace?.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Measure header height and store it in UI store
|
||||
const headerEl = useTemplateRef('headerEl')
|
||||
const { height } = useElementSize(headerEl)
|
||||
const uiStore = useUiStore()
|
||||
|
||||
watch(height, (newHeight) => {
|
||||
uiStore.headerHeight = newHeight
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
search:
|
||||
label: Suche
|
||||
|
||||
workspaces:
|
||||
label: Workspaces
|
||||
add: Workspace hinzufügen
|
||||
en:
|
||||
search:
|
||||
label: Search
|
||||
|
||||
workspaces:
|
||||
label: Workspaces
|
||||
add: Add Workspace
|
||||
</i18n>
|
||||
|
||||
@ -3,7 +3,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
|
||||
const toVaultId = getSingleRouteParam(to.params.vaultId)
|
||||
|
||||
console.log('middleware', openVaults.value?.[toVaultId])
|
||||
if (!openVaults.value?.[toVaultId]) {
|
||||
return await navigateTo(useLocalePath()({ name: 'vaultOpen' }))
|
||||
}
|
||||
|
||||
@ -1,113 +1,126 @@
|
||||
<template>
|
||||
<div class="items-center justify-center flex w-full h-full relative">
|
||||
<div class="absolute top-8 right-8 sm:top-4 sm:right-4">
|
||||
<UiDropdownLocale @select="onSelectLocale" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center items-center gap-5 max-w-3xl">
|
||||
<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 class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto">
|
||||
<HaexVaultCreate />
|
||||
|
||||
<HaexVaultOpen
|
||||
v-model:open="passwordPromptOpen"
|
||||
:path="selectedVault?.path"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-full">
|
||||
<NuxtLayout>
|
||||
<div
|
||||
v-show="lastVaults.length"
|
||||
class="w-full"
|
||||
class="flex flex-col justify-center items-center gap-5 mx-auto h-full overflow-scroll"
|
||||
>
|
||||
<div class="font-thin text-sm justify-start px-2 pb-1">
|
||||
{{ t('lastUsed') }}
|
||||
<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 class="flex flex-col gap-4 h-24 items-stretch justify-center">
|
||||
<HaexVaultCreate />
|
||||
|
||||
<HaexVaultOpen
|
||||
v-model:open="passwordPromptOpen"
|
||||
:path="selectedVault?.path"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-scroll"
|
||||
v-show="lastVaults.length"
|
||||
class="max-w-md w-full sm:px-5"
|
||||
>
|
||||
<div class="font-thin text-sm pb-1 w-full">
|
||||
{{ t('lastUsed') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="vault in lastVaults"
|
||||
:key="vault.name"
|
||||
class="flex items-center justify-between group overflow-x-scroll"
|
||||
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-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
|
||||
}
|
||||
"
|
||||
<div
|
||||
v-for="vault in lastVaults"
|
||||
:key="vault.name"
|
||||
class="flex items-center justify-between group overflow-x-scroll"
|
||||
>
|
||||
<span class="block">
|
||||
{{ vault.name }}
|
||||
</span>
|
||||
</UButton>
|
||||
<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
|
||||
color="error"
|
||||
square
|
||||
class="absolute right-2 hidden group-hover:flex min-w-6"
|
||||
variant="link"
|
||||
@click="openUrl('https://itemis.com')"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:trash-can-outline"
|
||||
@click="prepareRemoveVault(vault.name)"
|
||||
/>
|
||||
<UiLogoItemis class="text-[#00457C]" />
|
||||
</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"
|
||||
/>
|
||||
<UiDialogConfirm
|
||||
v-model:open="showRemoveDialog"
|
||||
:title="t('remove.title')"
|
||||
:description="t('remove.description', { vaultName: vaultToBeRemoved })"
|
||||
@confirm="onConfirmRemoveAsync"
|
||||
/>
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
import type { VaultInfo } from '@bindings/VaultInfo'
|
||||
|
||||
definePageMeta({
|
||||
name: 'vaultOpen',
|
||||
})
|
||||
|
||||
const { t, setLocale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
|
||||
const passwordPromptOpen = ref(false)
|
||||
const selectedVault = ref<VaultInfo>()
|
||||
|
||||
const showRemoveDialog = ref(false)
|
||||
const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
|
||||
|
||||
const { lastVaults } = storeToRefs(useLastVaultStore())
|
||||
|
||||
const { syncLastVaultsAsync, moveVaultToTrashAsync } = useLastVaultStore()
|
||||
const { syncDeviceIdAsync } = useDeviceStore()
|
||||
|
||||
const vaultToBeRemoved = ref('')
|
||||
const prepareRemoveVault = (vaultName: string) => {
|
||||
vaultToBeRemoved.value = vaultName
|
||||
@ -117,7 +130,7 @@ const prepareRemoveVault = (vaultName: string) => {
|
||||
const toast = useToast()
|
||||
const onConfirmRemoveAsync = async () => {
|
||||
try {
|
||||
await removeVaultAsync(vaultToBeRemoved.value)
|
||||
await moveVaultToTrashAsync(vaultToBeRemoved.value)
|
||||
showRemoveDialog.value = false
|
||||
await syncLastVaultsAsync()
|
||||
} catch (error) {
|
||||
@ -127,17 +140,15 @@ const onConfirmRemoveAsync = async () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await syncLastVaultsAsync()
|
||||
await syncDeviceIdAsync()
|
||||
} catch (error) {
|
||||
console.error('ERROR: ', error)
|
||||
}
|
||||
})
|
||||
|
||||
const onSelectLocale = async (locale: Locale) => {
|
||||
await setLocale(locale)
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
@ -146,6 +157,7 @@ de:
|
||||
lastUsed: 'Zuletzt verwendete Vaults'
|
||||
sponsors: Supported by
|
||||
remove:
|
||||
button: Löschen
|
||||
title: Vault löschen
|
||||
description: Möchtest du die Vault {vaultName} wirklich löschen?
|
||||
|
||||
@ -154,6 +166,7 @@ en:
|
||||
lastUsed: 'Last used Vaults'
|
||||
sponsors: 'Supported by'
|
||||
remove:
|
||||
button: Delete
|
||||
title: Delete Vault
|
||||
description: Are you sure you really want to delete {vaultName}?
|
||||
</i18n>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto">
|
||||
<NuxtLayout name="app">
|
||||
<div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
v-model:open="showNewDeviceDialog"
|
||||
:confirm-label="t('newDevice.save')"
|
||||
:title="t('newDevice.title')"
|
||||
:description="t('newDevice.setName')"
|
||||
confirm-icon="mdi:content-save-outline"
|
||||
@abort="showNewDeviceDialog = false"
|
||||
@confirm="onSetDeviceNameAsync"
|
||||
@ -48,7 +49,7 @@ const newDeviceName = ref<string>('unknown')
|
||||
const { readNotificationsAsync } = useNotificationStore()
|
||||
const { isKnownDeviceAsync } = useDeviceStore()
|
||||
const { loadExtensionsAsync } = useExtensionsStore()
|
||||
const { setDeviceIdIfNotExistsAsync, addDeviceNameAsync } = useDeviceStore()
|
||||
const { addDeviceNameAsync } = useDeviceStore()
|
||||
const { deviceId } = storeToRefs(useDeviceStore())
|
||||
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
||||
useVaultSettingsStore()
|
||||
@ -56,18 +57,17 @@ const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Sync settings first before other initialization
|
||||
|
||||
await Promise.allSettled([
|
||||
syncLocaleAsync(),
|
||||
syncThemeAsync(),
|
||||
syncVaultNameAsync(),
|
||||
setDeviceIdIfNotExistsAsync(),
|
||||
loadExtensionsAsync(),
|
||||
readNotificationsAsync(),
|
||||
])
|
||||
|
||||
const knownDevice = await isKnownDeviceAsync()
|
||||
|
||||
console.log('knownDevice', knownDevice)
|
||||
if (!knownDevice) {
|
||||
console.log('not known device')
|
||||
newDeviceName.value = hostname.value ?? 'unknown'
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<HaexDesktop />
|
||||
<div>
|
||||
<UDashboardPanel resizable>
|
||||
<HaexDesktop />
|
||||
</UDashboardPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,139 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="grid grid-rows-2 sm:grid-cols-2 sm:gap-2 p-2 max-w-2xl w-full h-fit"
|
||||
>
|
||||
<div class="p-2">{{ t('language') }}</div>
|
||||
<div><UiDropdownLocale @select="onSelectLocaleAsync" /></div>
|
||||
|
||||
<div class="p-2">{{ t('design') }}</div>
|
||||
<div><UiDropdownTheme @select="onSelectThemeAsync" /></div>
|
||||
|
||||
<div class="p-2">{{ t('vaultName.label') }}</div>
|
||||
<div>
|
||||
<UiInput
|
||||
v-model="currentVaultName"
|
||||
:placeholder="t('vaultName.label')"
|
||||
@change="onSetVaultNameAsync"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2">{{ t('notifications.label') }}</div>
|
||||
<div>
|
||||
<UiButton
|
||||
:label="t('notifications.requestPermission')"
|
||||
@click="requestNotificationPermissionAsync"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2">{{ t('deviceName.label') }}</div>
|
||||
<div>
|
||||
<UiInput
|
||||
v-model="deviceName"
|
||||
:placeholder="t('deviceName.label')"
|
||||
@change="onUpdateDeviceNameAsync"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Child routes (like developer.vue) will be rendered here -->
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
definePageMeta({
|
||||
name: 'settings',
|
||||
})
|
||||
|
||||
const { t, setLocale } = useI18n()
|
||||
|
||||
const { currentVaultName } = storeToRefs(useVaultStore())
|
||||
const { updateVaultNameAsync, updateLocaleAsync, updateThemeAsync } =
|
||||
useVaultSettingsStore()
|
||||
|
||||
const onSelectLocaleAsync = async (locale: Locale) => {
|
||||
await updateLocaleAsync(locale)
|
||||
await setLocale(locale)
|
||||
}
|
||||
|
||||
const { currentThemeName } = storeToRefs(useUiStore())
|
||||
|
||||
const onSelectThemeAsync = async (theme: string) => {
|
||||
currentThemeName.value = theme
|
||||
console.log('onSelectThemeAsync', currentThemeName.value)
|
||||
await updateThemeAsync(theme)
|
||||
}
|
||||
|
||||
const { add } = useToast()
|
||||
|
||||
const onSetVaultNameAsync = async () => {
|
||||
try {
|
||||
await updateVaultNameAsync(currentVaultName.value)
|
||||
add({ description: t('vaultName.update.success'), color: 'success' })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
add({ description: t('vaultName.update.error'), color: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const { requestNotificationPermissionAsync } = useNotificationStore()
|
||||
|
||||
const { deviceName } = storeToRefs(useDeviceStore())
|
||||
const { updateDeviceNameAsync, readDeviceNameAsync } = useDeviceStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await readDeviceNameAsync()
|
||||
})
|
||||
|
||||
const onUpdateDeviceNameAsync = async () => {
|
||||
const check = vaultDeviceNameSchema.safeParse(deviceName.value)
|
||||
if (!check.success) return
|
||||
try {
|
||||
await updateDeviceNameAsync({ name: deviceName.value })
|
||||
add({ description: t('deviceName.update.success'), color: 'success' })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
add({ description: t('deviceName.update.error'), color: 'error' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
language: Sprache
|
||||
design: Design
|
||||
save: Änderung speichern
|
||||
notifications:
|
||||
label: Benachrichtigungen
|
||||
requestPermission: Benachrichtigung erlauben
|
||||
vaultName:
|
||||
label: Vaultname
|
||||
update:
|
||||
success: Vaultname erfolgreich aktualisiert
|
||||
error: Vaultname konnte nicht aktualisiert werden
|
||||
deviceName:
|
||||
label: Gerätename
|
||||
update:
|
||||
success: Gerätename wurde erfolgreich aktualisiert
|
||||
error: Gerätename konnte nich aktualisiert werden
|
||||
en:
|
||||
language: Language
|
||||
design: Design
|
||||
save: save changes
|
||||
notifications:
|
||||
label: Notifications
|
||||
requestPermission: Grant Permission
|
||||
vaultName:
|
||||
label: Vault Name
|
||||
update:
|
||||
success: Vault Name successfully updated
|
||||
error: Vault name could not be updated
|
||||
deviceName:
|
||||
label: Device name
|
||||
update:
|
||||
success: Device name has been successfully updated
|
||||
error: Device name could not be updated
|
||||
</i18n>
|
||||
@ -1,279 +0,0 @@
|
||||
<template>
|
||||
<div class="p-4 max-w-4xl mx-auto space-y-6">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-2xl font-bold">{{ t('title') }}</h1>
|
||||
<p class="text-sm opacity-70">{{ t('description') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Add Dev Extension Form -->
|
||||
<UCard class="p-4 space-y-4">
|
||||
<h2 class="text-lg font-semibold">{{ t('add.title') }}</h2>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium">{{ t('add.extensionPath') }}</label>
|
||||
<div class="flex gap-2">
|
||||
<UiInput
|
||||
v-model="extensionPath"
|
||||
:placeholder="t('add.extensionPathPlaceholder')"
|
||||
class="flex-1"
|
||||
/>
|
||||
<UiButton
|
||||
:label="t('add.browse')"
|
||||
variant="outline"
|
||||
@click="browseExtensionPathAsync"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs opacity-60">{{ t('add.extensionPathHint') }}</p>
|
||||
</div>
|
||||
|
||||
<UiButton
|
||||
:label="t('add.loadExtension')"
|
||||
:loading="isLoading"
|
||||
:disabled="!extensionPath"
|
||||
@click="loadDevExtensionAsync"
|
||||
/>
|
||||
</UCard>
|
||||
|
||||
<!-- List of Dev Extensions -->
|
||||
<div
|
||||
v-if="devExtensions.length > 0"
|
||||
class="space-y-2"
|
||||
>
|
||||
<h2 class="text-lg font-semibold">{{ t('list.title') }}</h2>
|
||||
|
||||
<UCard
|
||||
v-for="ext in devExtensions"
|
||||
:key="ext.id"
|
||||
class="p-4 flex items-center justify-between"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-medium">{{ ext.name }}</h3>
|
||||
<UBadge color="info">DEV</UBadge>
|
||||
</div>
|
||||
<p class="text-sm opacity-70">v{{ ext.version }}</p>
|
||||
<p class="text-xs opacity-50">{{ ext.publicKey.slice(0, 16) }}...</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UiButton
|
||||
:label="t('list.reload')"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="reloadDevExtensionAsync(ext)"
|
||||
/>
|
||||
<UiButton
|
||||
:label="t('list.remove')"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="error"
|
||||
@click="removeDevExtensionAsync(ext)"
|
||||
/>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-8 opacity-50"
|
||||
>
|
||||
{{ t('list.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
|
||||
definePageMeta({
|
||||
name: 'settings-developer',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const { add } = useToast()
|
||||
const { loadExtensionsAsync } = useExtensionsStore()
|
||||
|
||||
|
||||
// State
|
||||
const extensionPath = ref('')
|
||||
const isLoading = ref(false)
|
||||
const devExtensions = ref<
|
||||
Array<{
|
||||
id: string
|
||||
publicKey: string
|
||||
name: string
|
||||
version: string
|
||||
enabled: boolean
|
||||
}>
|
||||
>([])
|
||||
|
||||
// Load dev extensions on mount
|
||||
onMounted(async () => {
|
||||
await loadDevExtensionListAsync()
|
||||
})
|
||||
|
||||
// Browse for extension directory
|
||||
const browseExtensionPathAsync = async () => {
|
||||
try {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: t('add.browseTitle'),
|
||||
})
|
||||
|
||||
if (selected && typeof selected === 'string') {
|
||||
extensionPath.value = selected
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to browse directory:', error)
|
||||
add({
|
||||
description: t('add.errors.browseFailed'),
|
||||
color: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Load a dev extension
|
||||
const loadDevExtensionAsync = async () => {
|
||||
if (!extensionPath.value) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const extensionId = await invoke<string>('load_dev_extension', {
|
||||
extensionPath: extensionPath.value,
|
||||
})
|
||||
|
||||
add({
|
||||
description: t('add.success'),
|
||||
color: 'success',
|
||||
})
|
||||
|
||||
// Reload list
|
||||
await loadDevExtensionListAsync()
|
||||
|
||||
// Reload all extensions in the main extension store so they appear in the launcher
|
||||
await loadExtensionsAsync()
|
||||
|
||||
// Clear input
|
||||
extensionPath.value = ''
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load dev extension:', error)
|
||||
add({
|
||||
description: error || t('add.errors.loadFailed'),
|
||||
color: 'error',
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Load all dev extensions (for the list on this page)
|
||||
const loadDevExtensionListAsync = async () => {
|
||||
try {
|
||||
const extensions = await invoke<Array<any>>('get_all_dev_extensions')
|
||||
devExtensions.value = extensions
|
||||
} catch (error) {
|
||||
console.error('Failed to load dev extensions:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload a dev extension (removes and re-adds)
|
||||
const reloadDevExtensionAsync = async (ext: any) => {
|
||||
try {
|
||||
// Get the extension path from somewhere (we need to store this)
|
||||
// For now, just show a message
|
||||
add({
|
||||
description: t('list.reloadInfo'),
|
||||
color: 'info',
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Failed to reload dev extension:', error)
|
||||
add({
|
||||
description: error || t('list.errors.reloadFailed'),
|
||||
color: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a dev extension
|
||||
const removeDevExtensionAsync = async (ext: any) => {
|
||||
try {
|
||||
await invoke('remove_dev_extension', {
|
||||
publicKey: ext.publicKey,
|
||||
name: ext.name,
|
||||
})
|
||||
|
||||
add({
|
||||
description: t('list.removeSuccess'),
|
||||
color: 'success',
|
||||
})
|
||||
|
||||
// Reload list
|
||||
await loadDevExtensionListAsync()
|
||||
|
||||
// Reload all extensions store
|
||||
await loadExtensionsAsync()
|
||||
} catch (error: any) {
|
||||
console.error('Failed to remove dev extension:', error)
|
||||
add({
|
||||
description: error || t('list.errors.removeFailed'),
|
||||
color: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
title: Entwicklereinstellungen
|
||||
description: Lade Extensions im Entwicklungsmodus für schnelleres Testen mit Hot-Reload.
|
||||
add:
|
||||
title: Dev-Extension hinzufügen
|
||||
extensionPath: Extension-Pfad
|
||||
extensionPathPlaceholder: /pfad/zu/deiner/extension
|
||||
extensionPathHint: Pfad zum Extension-Projekt (enthält haextension/ und haextension.json)
|
||||
browse: Durchsuchen
|
||||
browseTitle: Extension-Verzeichnis auswählen
|
||||
loadExtension: Extension laden
|
||||
success: Dev-Extension erfolgreich geladen
|
||||
errors:
|
||||
browseFailed: Verzeichnis konnte nicht ausgewählt werden
|
||||
loadFailed: Extension konnte nicht geladen werden
|
||||
list:
|
||||
title: Geladene Dev-Extensions
|
||||
empty: Keine Dev-Extensions geladen
|
||||
reload: Neu laden
|
||||
remove: Entfernen
|
||||
reloadInfo: Extension wird beim nächsten Laden automatisch aktualisiert
|
||||
removeSuccess: Dev-Extension erfolgreich entfernt
|
||||
errors:
|
||||
reloadFailed: Extension konnte nicht neu geladen werden
|
||||
removeFailed: Extension konnte nicht entfernt werden
|
||||
|
||||
en:
|
||||
title: Developer Settings
|
||||
description: Load extensions in development mode for faster testing with hot-reload.
|
||||
add:
|
||||
title: Add Dev Extension
|
||||
extensionPath: Extension Path
|
||||
extensionPathPlaceholder: /path/to/your/extension
|
||||
extensionPathHint: Path to your extension project (contains haextension/ and haextension.json)
|
||||
browse: Browse
|
||||
browseTitle: Select Extension Directory
|
||||
loadExtension: Load Extension
|
||||
success: Dev extension loaded successfully
|
||||
errors:
|
||||
browseFailed: Failed to select directory
|
||||
loadFailed: Failed to load extension
|
||||
list:
|
||||
title: Loaded Dev Extensions
|
||||
empty: No dev extensions loaded
|
||||
reload: Reload
|
||||
remove: Remove
|
||||
reloadInfo: Extension will be automatically updated on next load
|
||||
removeSuccess: Dev extension removed successfully
|
||||
errors:
|
||||
reloadFailed: Failed to reload extension
|
||||
removeFailed: Failed to remove extension
|
||||
</i18n>
|
||||
25
src/plugins/init-logger.ts
Normal file
25
src/plugins/init-logger.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@ -1,17 +1,18 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
|
||||
import { haexDesktopItems } from '~/database/schemas'
|
||||
import type {
|
||||
InsertHaexDesktopItems,
|
||||
SelectHaexDesktopItems,
|
||||
} from '~~/src-tauri/database/schemas'
|
||||
} from '~/database/schemas'
|
||||
import de from './de.json'
|
||||
import en from './en.json'
|
||||
|
||||
export type DesktopItemType = 'extension' | 'file' | 'folder'
|
||||
export type DesktopItemType = 'extension' | 'file' | 'folder' | 'system'
|
||||
|
||||
export interface IDesktopItem extends SelectHaexDesktopItems {
|
||||
label?: string
|
||||
icon?: string
|
||||
referenceId: string // Computed: extensionId or systemWindowId
|
||||
}
|
||||
|
||||
export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
@ -45,7 +46,10 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
.from(haexDesktopItems)
|
||||
.where(eq(haexDesktopItems.workspaceId, currentWorkspace.value.id))
|
||||
|
||||
desktopItems.value = items
|
||||
desktopItems.value = items.map(item => ({
|
||||
...item,
|
||||
referenceId: item.itemType === 'extension' ? item.extensionId! : item.systemWindowId!,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Desktop-Items:', error)
|
||||
throw error
|
||||
@ -57,20 +61,23 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
referenceId: string,
|
||||
positionX: number = 0,
|
||||
positionY: number = 0,
|
||||
workspaceId?: string,
|
||||
) => {
|
||||
if (!currentVault.value?.drizzle) {
|
||||
throw new Error('Kein Vault geöffnet')
|
||||
}
|
||||
|
||||
if (!currentWorkspace.value) {
|
||||
const targetWorkspaceId = workspaceId || currentWorkspace.value?.id
|
||||
if (!targetWorkspaceId) {
|
||||
throw new Error('Kein Workspace aktiv')
|
||||
}
|
||||
|
||||
try {
|
||||
const newItem: InsertHaexDesktopItems = {
|
||||
workspaceId: currentWorkspace.value.id,
|
||||
workspaceId: targetWorkspaceId,
|
||||
itemType: itemType,
|
||||
referenceId: referenceId,
|
||||
extensionId: itemType === 'extension' ? referenceId : null,
|
||||
systemWindowId: itemType === 'system' || itemType === 'file' || itemType === 'folder' ? referenceId : null,
|
||||
positionX: positionX,
|
||||
positionY: positionY,
|
||||
}
|
||||
@ -81,11 +88,27 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
.returning()
|
||||
|
||||
if (result.length > 0 && result[0]) {
|
||||
desktopItems.value.push(result[0])
|
||||
return result[0]
|
||||
const itemWithRef = {
|
||||
...result[0],
|
||||
referenceId: itemType === 'extension' ? result[0].extensionId! : result[0].systemWindowId!,
|
||||
}
|
||||
desktopItems.value.push(itemWithRef)
|
||||
return itemWithRef
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Desktop-Items:', error)
|
||||
console.error('Fehler beim Hinzufügen des Desktop-Items:', {
|
||||
error,
|
||||
itemType,
|
||||
referenceId,
|
||||
workspaceId: targetWorkspaceId,
|
||||
position: { x: positionX, y: positionY }
|
||||
})
|
||||
|
||||
// Log full error details
|
||||
if (error && typeof error === 'object') {
|
||||
console.error('Full error object:', JSON.stringify(error, null, 2))
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -112,7 +135,11 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
if (result.length > 0 && result[0]) {
|
||||
const index = desktopItems.value.findIndex((item) => item.id === id)
|
||||
if (index !== -1) {
|
||||
desktopItems.value[index] = result[0]
|
||||
const item = result[0]
|
||||
desktopItems.value[index] = {
|
||||
...item,
|
||||
referenceId: item.itemType === 'extension' ? item.extensionId! : item.systemWindowId!,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -145,7 +172,14 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
referenceId: string,
|
||||
) => {
|
||||
return desktopItems.value.find(
|
||||
(item) => item.itemType === itemType && item.referenceId === referenceId,
|
||||
(item) => {
|
||||
if (item.itemType !== itemType) return false
|
||||
if (itemType === 'extension') {
|
||||
return item.extensionId === referenceId
|
||||
} else {
|
||||
return item.systemWindowId === referenceId
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -154,8 +188,23 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
referenceId: string,
|
||||
sourcePosition?: { x: number; y: number; width: number; height: number },
|
||||
) => {
|
||||
if (itemType === 'extension') {
|
||||
const windowManager = useWindowManagerStore()
|
||||
const windowManager = useWindowManagerStore()
|
||||
|
||||
if (itemType === 'system') {
|
||||
const systemWindow = windowManager.getAllSystemWindows().find(
|
||||
(win) => win.id === referenceId,
|
||||
)
|
||||
|
||||
if (systemWindow) {
|
||||
windowManager.openWindowAsync({
|
||||
sourceId: systemWindow.id,
|
||||
type: 'system',
|
||||
icon: systemWindow.icon,
|
||||
title: systemWindow.name,
|
||||
sourcePosition,
|
||||
})
|
||||
}
|
||||
} else if (itemType === 'extension') {
|
||||
const extensionsStore = useExtensionsStore()
|
||||
|
||||
const extension = extensionsStore.availableExtensions.find(
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import {
|
||||
haexWorkspaces,
|
||||
type SelectHaexWorkspaces,
|
||||
} from '~~/src-tauri/database/schemas'
|
||||
} from '~/database/schemas'
|
||||
import type { Swiper } from 'swiper/types'
|
||||
|
||||
export type IWorkspace = SelectHaexWorkspaces
|
||||
@ -10,6 +10,7 @@ export type IWorkspace = SelectHaexWorkspaces
|
||||
export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
const vaultStore = useVaultStore()
|
||||
const windowStore = useWindowManagerStore()
|
||||
const { deviceId } = storeToRefs(useDeviceStore())
|
||||
|
||||
const { currentVault } = storeToRefs(vaultStore)
|
||||
|
||||
@ -31,19 +32,24 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!deviceId.value) {
|
||||
console.error('Keine DeviceId vergeben')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
/* const items = await currentVault.value.drizzle
|
||||
const items = await currentVault.value.drizzle
|
||||
.select()
|
||||
.from(haexWorkspaces)
|
||||
.where(eq(haexWorkspaces.deviceId, deviceId.value))
|
||||
.orderBy(asc(haexWorkspaces.position))
|
||||
|
||||
console.log('loadWorkspacesAsync', items)
|
||||
workspaces.value = items */
|
||||
workspaces.value = items
|
||||
|
||||
// Create default workspace if none exist
|
||||
/* if (items.length === 0) { */
|
||||
await addWorkspaceAsync('Workspace 1')
|
||||
/* } */
|
||||
if (items.length === 0) {
|
||||
await addWorkspaceAsync('Workspace 1')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Workspaces:', error)
|
||||
throw error
|
||||
@ -59,17 +65,19 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
throw new Error('Kein Vault geöffnet')
|
||||
}
|
||||
|
||||
if (!deviceId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const newIndex = workspaces.value.length + 1
|
||||
const newWorkspace: SelectHaexWorkspaces = {
|
||||
id: crypto.randomUUID(),
|
||||
const newWorkspace = {
|
||||
name: name || `Workspace ${newIndex}`,
|
||||
position: workspaces.value.length,
|
||||
haexTimestamp: '',
|
||||
deviceId: deviceId.value,
|
||||
}
|
||||
workspaces.value.push(newWorkspace)
|
||||
currentWorkspaceIndex.value = workspaces.value.length - 1
|
||||
/* const result = await currentVault.value.drizzle
|
||||
|
||||
const result = await currentVault.value.drizzle
|
||||
.insert(haexWorkspaces)
|
||||
.values(newWorkspace)
|
||||
.returning()
|
||||
@ -78,7 +86,7 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
workspaces.value.push(result[0])
|
||||
currentWorkspaceIndex.value = workspaces.value.length - 1
|
||||
return result[0]
|
||||
} */
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Workspace:', error)
|
||||
throw error
|
||||
@ -106,27 +114,27 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
|
||||
if (index === -1) return
|
||||
|
||||
workspaces.value.splice(index, 1)
|
||||
workspaces.value.forEach((workspace, index) => (workspace.position = index))
|
||||
|
||||
try {
|
||||
/* await currentVault.value.drizzle.transaction(async (tx) => {
|
||||
await currentVault.value.drizzle.transaction(async (tx) => {
|
||||
// Delete workspace
|
||||
await tx
|
||||
.delete(haexWorkspaces)
|
||||
.where(eq(haexWorkspaces.id, workspaceId))
|
||||
|
||||
// Update local state
|
||||
workspaces.value.splice(index, 1)
|
||||
workspaces.value.forEach(
|
||||
(workspace, index) => (workspace.position = index),
|
||||
)
|
||||
workspaces.value.forEach((workspace, idx) => {
|
||||
workspace.position = idx
|
||||
})
|
||||
|
||||
// Update positions in database
|
||||
for (const workspace of workspaces.value) {
|
||||
await tx
|
||||
.update(haexWorkspaces)
|
||||
.set({ position: index })
|
||||
.where(eq(haexWorkspaces.position, workspace.position))
|
||||
.set({ position: workspace.position })
|
||||
.where(eq(haexWorkspaces.id, workspace.id))
|
||||
}
|
||||
}) */
|
||||
})
|
||||
|
||||
// Adjust current index if needed
|
||||
if (currentWorkspaceIndex.value >= workspaces.value.length) {
|
||||
|
||||
@ -50,7 +50,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
currentExtension.value.publicKey,
|
||||
currentExtension.value.name,
|
||||
currentExtension.value.version,
|
||||
'index.html',
|
||||
currentExtension.value.entry ?? 'index.html',
|
||||
currentExtension.value.devServerUrl ?? undefined,
|
||||
)
|
||||
})
|
||||
@ -90,6 +90,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
const extensions =
|
||||
await invoke<ExtensionInfoResponse[]>('get_all_extensions')
|
||||
|
||||
console.log('get_all_extensions', extensions)
|
||||
// ExtensionInfoResponse is now directly compatible with IHaexHubExtension
|
||||
availableExtensions.value = extensions
|
||||
} catch (error) {
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
// stores/extensions/tabs.ts
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
import { getExtensionWindow } from '~/composables/extensionMessageHandler'
|
||||
|
||||
interface ExtensionTab {
|
||||
extension: IHaexHubExtension
|
||||
iframe: HTMLIFrameElement | null
|
||||
isVisible: boolean
|
||||
lastAccessed: number
|
||||
}
|
||||
|
||||
export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
||||
// State
|
||||
const openTabs = ref(new Map<string, ExtensionTab>())
|
||||
const activeTabId = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const activeTab = computed(() => {
|
||||
if (!activeTabId.value) return null
|
||||
return openTabs.value.get(activeTabId.value) || null
|
||||
})
|
||||
|
||||
const tabCount = computed(() => openTabs.value.size)
|
||||
|
||||
const sortedTabs = computed(() => {
|
||||
return Array.from(openTabs.value.values()).sort(
|
||||
(a, b) => b.lastAccessed - a.lastAccessed,
|
||||
)
|
||||
})
|
||||
|
||||
const extensionsStore = useExtensionsStore()
|
||||
|
||||
// Actions
|
||||
const openTab = (extensionId: string) => {
|
||||
const extension = extensionsStore.availableExtensions.find(
|
||||
(ext) => ext.id === extensionId,
|
||||
)
|
||||
|
||||
if (!extension) {
|
||||
console.error(`Extension ${extensionId} nicht gefunden`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if extension is enabled
|
||||
if (!extension.enabled) {
|
||||
console.warn(
|
||||
`Extension ${extensionId} ist deaktiviert und kann nicht geöffnet werden`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Bereits geöffnet? Nur aktivieren
|
||||
if (openTabs.value.has(extensionId)) {
|
||||
setActiveTab(extensionId)
|
||||
return
|
||||
}
|
||||
|
||||
// Limit: Max 10 Tabs
|
||||
if (openTabs.value.size >= 10) {
|
||||
const oldestInactive = sortedTabs.value
|
||||
.filter((tab) => tab.extension.id !== activeTabId.value)
|
||||
.pop()
|
||||
|
||||
if (oldestInactive) {
|
||||
closeTab(oldestInactive.extension.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Neuen Tab erstellen
|
||||
openTabs.value.set(extensionId, {
|
||||
extension,
|
||||
iframe: null,
|
||||
isVisible: false,
|
||||
lastAccessed: Date.now(),
|
||||
})
|
||||
|
||||
setActiveTab(extensionId)
|
||||
}
|
||||
|
||||
const setActiveTab = (extensionId: string) => {
|
||||
// Verstecke aktuellen Tab
|
||||
if (activeTabId.value && openTabs.value.has(activeTabId.value)) {
|
||||
const currentTab = openTabs.value.get(activeTabId.value)!
|
||||
currentTab.isVisible = false
|
||||
}
|
||||
|
||||
// Zeige neuen Tab
|
||||
const newTab = openTabs.value.get(extensionId)
|
||||
if (newTab) {
|
||||
const now = Date.now()
|
||||
const inactiveDuration = now - newTab.lastAccessed
|
||||
const TEN_MINUTES = 10 * 60 * 1000
|
||||
|
||||
// Reload iframe if inactive for more than 10 minutes
|
||||
if (inactiveDuration > TEN_MINUTES && newTab.iframe) {
|
||||
console.log(
|
||||
`[TabStore] Reloading extension ${extensionId} after ${Math.round(inactiveDuration / 1000)}s inactivity`,
|
||||
)
|
||||
const currentSrc = newTab.iframe.src
|
||||
newTab.iframe.src = 'about:blank'
|
||||
// Small delay to ensure reload
|
||||
setTimeout(() => {
|
||||
if (newTab.iframe) {
|
||||
newTab.iframe.src = currentSrc
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
newTab.isVisible = true
|
||||
newTab.lastAccessed = now
|
||||
activeTabId.value = extensionId
|
||||
}
|
||||
}
|
||||
|
||||
const closeTab = (extensionId: string) => {
|
||||
const tab = openTabs.value.get(extensionId)
|
||||
if (!tab) return
|
||||
|
||||
// IFrame entfernen
|
||||
tab.iframe?.remove()
|
||||
openTabs.value.delete(extensionId)
|
||||
|
||||
// Nächsten Tab aktivieren
|
||||
if (activeTabId.value === extensionId) {
|
||||
const remaining = sortedTabs.value
|
||||
const nextTab = remaining[0]
|
||||
|
||||
if (nextTab) {
|
||||
setActiveTab(nextTab.extension.id)
|
||||
} else {
|
||||
activeTabId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registerIFrame = (extensionId: string, iframe: HTMLIFrameElement) => {
|
||||
const tab = openTabs.value.get(extensionId)
|
||||
if (tab) {
|
||||
tab.iframe = iframe
|
||||
}
|
||||
}
|
||||
|
||||
const broadcastToAllTabs = (message: unknown) => {
|
||||
openTabs.value.forEach(({ extension }) => {
|
||||
// Use sandbox-compatible window reference
|
||||
const win = getExtensionWindow(extension.id)
|
||||
if (win) {
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeAllTabs = () => {
|
||||
openTabs.value.forEach((tab) => tab.iframe?.remove())
|
||||
openTabs.value.clear()
|
||||
activeTabId.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
openTabs,
|
||||
activeTabId,
|
||||
// Getters
|
||||
activeTab,
|
||||
tabCount,
|
||||
sortedTabs,
|
||||
// Actions
|
||||
openTab,
|
||||
setActiveTab,
|
||||
closeTab,
|
||||
registerIFrame,
|
||||
broadcastToAllTabs,
|
||||
closeAllTabs,
|
||||
}
|
||||
})
|
||||
@ -1,4 +1,6 @@
|
||||
import { breakpointsTailwind } from '@vueuse/core'
|
||||
import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler'
|
||||
|
||||
import de from './de.json'
|
||||
import en from './en.json'
|
||||
|
||||
@ -9,6 +11,9 @@ export const useUiStore = defineStore('uiStore', () => {
|
||||
const isSmallScreen = breakpoints.smaller('sm')
|
||||
|
||||
const { $i18n } = useNuxtApp()
|
||||
const { locale } = useI18n({
|
||||
useScope: 'global',
|
||||
})
|
||||
|
||||
$i18n.setLocaleMessage('de', {
|
||||
ui: de,
|
||||
@ -53,15 +58,27 @@ export const useUiStore = defineStore('uiStore', () => {
|
||||
const colorMode = useColorMode()
|
||||
|
||||
watchImmediate(currentThemeName, () => {
|
||||
console.log('set colorMode', currentThemeName.value)
|
||||
colorMode.preference = currentThemeName.value
|
||||
})
|
||||
|
||||
// Broadcast theme and locale changes to extensions
|
||||
watch([currentThemeName, locale], async () => {
|
||||
const deviceStore = useDeviceStore()
|
||||
const platformValue = await deviceStore.platform
|
||||
broadcastContextToAllExtensions({
|
||||
theme: currentThemeName.value,
|
||||
locale: locale.value,
|
||||
platform: platformValue,
|
||||
})
|
||||
})
|
||||
|
||||
const viewportHeightWithoutHeader = ref(0)
|
||||
const headerHeight = ref(0)
|
||||
|
||||
return {
|
||||
availableThemes,
|
||||
viewportHeightWithoutHeader,
|
||||
headerHeight,
|
||||
currentTheme,
|
||||
currentThemeName,
|
||||
defaultTheme,
|
||||
|
||||
@ -4,8 +4,18 @@ import {
|
||||
platform as tauriPlatform,
|
||||
} from '@tauri-apps/plugin-os'
|
||||
|
||||
export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
||||
const deviceId = ref<string>()
|
||||
const deviceIdKey = 'deviceId'
|
||||
const defaultDeviceFileName = 'device.json'
|
||||
|
||||
export const useDeviceStore = defineStore('vaultDeviceStore', () => {
|
||||
const deviceId = ref<string | undefined>('')
|
||||
|
||||
const syncDeviceIdAsync = async () => {
|
||||
deviceId.value = await getDeviceIdAsync()
|
||||
if (deviceId.value) return deviceId.value
|
||||
|
||||
deviceId.value = await setDeviceIdAsync()
|
||||
}
|
||||
|
||||
const platform = computedAsync(() => tauriPlatform())
|
||||
|
||||
@ -15,7 +25,7 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
||||
|
||||
const getDeviceIdAsync = async () => {
|
||||
const store = await getStoreAsync()
|
||||
return store.get<string>('id')
|
||||
return await store.get<string>(deviceIdKey)
|
||||
}
|
||||
|
||||
const getStoreAsync = async () => {
|
||||
@ -23,36 +33,25 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
||||
public: { haexVault },
|
||||
} = useRuntimeConfig()
|
||||
|
||||
return await load(haexVault.instanceFileName || 'instance.json')
|
||||
return await load(haexVault.deviceFileName || defaultDeviceFileName)
|
||||
}
|
||||
|
||||
const setDeviceIdAsync = async (id?: string) => {
|
||||
const store = await getStoreAsync()
|
||||
const _id = id || crypto.randomUUID()
|
||||
await store.set('id', _id)
|
||||
deviceId.value = _id
|
||||
await store.set(deviceIdKey, _id)
|
||||
return _id
|
||||
}
|
||||
|
||||
const setDeviceIdIfNotExistsAsync = async () => {
|
||||
const _deviceId = await getDeviceIdAsync()
|
||||
if (_deviceId) {
|
||||
deviceId.value = _deviceId
|
||||
return deviceId.value
|
||||
}
|
||||
return await setDeviceIdAsync()
|
||||
}
|
||||
|
||||
const isKnownDeviceAsync = async () => {
|
||||
const { readDeviceNameAsync } = useVaultSettingsStore()
|
||||
const deviceId = await getDeviceIdAsync()
|
||||
return deviceId ? (await readDeviceNameAsync(deviceId)) || false : false
|
||||
return !!(await readDeviceNameAsync(deviceId.value))
|
||||
}
|
||||
|
||||
const readDeviceNameAsync = async (id?: string) => {
|
||||
const { readDeviceNameAsync } = useVaultSettingsStore()
|
||||
const _id = id || deviceId.value
|
||||
console.log('readDeviceNameAsync id', _id)
|
||||
|
||||
if (!_id) return
|
||||
|
||||
deviceName.value = (await readDeviceNameAsync(_id))?.value ?? ''
|
||||
@ -99,12 +98,13 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
||||
addDeviceNameAsync,
|
||||
deviceId,
|
||||
deviceName,
|
||||
getDeviceIdAsync,
|
||||
hostname,
|
||||
isKnownDeviceAsync,
|
||||
platform,
|
||||
readDeviceNameAsync,
|
||||
setDeviceIdAsync,
|
||||
setDeviceIdIfNotExistsAsync,
|
||||
syncDeviceIdAsync,
|
||||
updateDeviceNameAsync,
|
||||
}
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { drizzle } from 'drizzle-orm/sqlite-proxy'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { schema } from '@/../src-tauri/database/index'
|
||||
import { schema } from '~/database'
|
||||
import type {
|
||||
AsyncRemoteCallback,
|
||||
SqliteRemoteDatabase,
|
||||
@ -21,11 +21,12 @@ export const useVaultStore = defineStore('vaultStore', () => {
|
||||
public: { haexVault },
|
||||
} = useRuntimeConfig()
|
||||
|
||||
const router = useRouter()
|
||||
const currentVaultId = computed<string | undefined>({
|
||||
get: () =>
|
||||
getSingleRouteParam(useRouter().currentRoute.value.params.vaultId),
|
||||
getSingleRouteParam(router.currentRoute.value.params.vaultId),
|
||||
set: (newVaultId) => {
|
||||
useRouter().currentRoute.value.params.vaultId = newVaultId ?? ''
|
||||
router.currentRoute.value.params.vaultId = newVaultId ?? ''
|
||||
},
|
||||
})
|
||||
|
||||
@ -177,8 +178,8 @@ const drizzleCallback = (async (
|
||||
})
|
||||
}
|
||||
|
||||
console.log('drizzleCallback', method, sql, params)
|
||||
console.log('drizzleCallback rows', rows, rows.slice(0, 1))
|
||||
/* console.log('drizzleCallback', method, sql, params)
|
||||
console.log('drizzleCallback rows', rows, rows.slice(0, 1)) */
|
||||
|
||||
if (method === 'get') {
|
||||
return rows.length > 0 ? { rows: rows.at(0) } : { rows }
|
||||
|
||||
@ -22,9 +22,14 @@ export const useLastVaultStore = defineStore('lastVaultStore', () => {
|
||||
return await invoke('delete_vault', { vaultName })
|
||||
}
|
||||
|
||||
const moveVaultToTrashAsync = async (vaultName: string) => {
|
||||
return await invoke('move_vault_to_trash', { vaultName })
|
||||
}
|
||||
|
||||
return {
|
||||
syncLastVaultsAsync,
|
||||
lastVaults,
|
||||
removeVaultAsync,
|
||||
moveVaultToTrashAsync,
|
||||
}
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import { and, eq, or, type SQLWrapper } from 'drizzle-orm'
|
||||
import {
|
||||
haexNotifications,
|
||||
type InsertHaexNotifications,
|
||||
} from '~~/src-tauri/database/schemas/haex'
|
||||
} from '~/database/schemas/haex'
|
||||
import {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
@ -31,7 +31,12 @@ export const useNotificationStore = defineStore('notificationStore', () => {
|
||||
}
|
||||
|
||||
const checkNotificationAsync = async () => {
|
||||
isNotificationAllowed.value = await isPermissionGranted()
|
||||
try {
|
||||
isNotificationAllowed.value = await isPermissionGranted()
|
||||
} catch (error) {
|
||||
console.warn('Notification permission check failed:', error)
|
||||
isNotificationAllowed.value = false
|
||||
}
|
||||
return isNotificationAllowed.value
|
||||
}
|
||||
|
||||
@ -40,7 +45,6 @@ export const useNotificationStore = defineStore('notificationStore', () => {
|
||||
const readNotificationsAsync = async (filter?: SQLWrapper[]) => {
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
|
||||
console.log('readNotificationsAsync', filter)
|
||||
if (filter) {
|
||||
return await currentVault.value?.drizzle
|
||||
.select()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import * as schema from '@/../src-tauri/database/schemas/haex'
|
||||
import * as schema from '~/database/schemas/haex'
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
export enum VaultSettingsTypeEnum {
|
||||
@ -32,7 +32,6 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
||||
where: eq(schema.haexSettings.key, VaultSettingsKeyEnum.locale),
|
||||
})
|
||||
|
||||
console.log('found currentLocaleRow', currentLocaleRow)
|
||||
if (currentLocaleRow?.value) {
|
||||
const currentLocale = app.$i18n.availableLocales.find(
|
||||
(locale) => locale === currentLocaleRow.value,
|
||||
@ -119,9 +118,11 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
||||
.where(eq(schema.haexSettings.key, 'vaultName'))
|
||||
}
|
||||
|
||||
const readDeviceNameAsync = async (id: string) => {
|
||||
const readDeviceNameAsync = async (id?: string) => {
|
||||
const { currentVault } = useVaultStore()
|
||||
|
||||
if (!id) return undefined
|
||||
|
||||
const deviceName =
|
||||
await currentVault?.drizzle?.query.haexSettings.findFirst({
|
||||
where: and(
|
||||
@ -129,7 +130,7 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
||||
eq(schema.haexSettings.key, id),
|
||||
),
|
||||
})
|
||||
console.log('store: readDeviceNameAsync', deviceName)
|
||||
|
||||
return deviceName?.id ? deviceName : undefined
|
||||
}
|
||||
|
||||
@ -149,7 +150,6 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
||||
}
|
||||
|
||||
return currentVault?.drizzle?.insert(schema.haexSettings).values({
|
||||
//id: crypto.randomUUID(),
|
||||
type: VaultSettingsTypeEnum.deviceName,
|
||||
key: deviceId,
|
||||
value: deviceName,
|
||||
|
||||
Reference in New Issue
Block a user