mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-19 07:20:50 +01:00
Compare commits
34 Commits
16b71d9ea8
...
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 |
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
|
||||||
|
})
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,3 +27,5 @@ src-tauri/target
|
|||||||
nogit*
|
nogit*
|
||||||
.claude
|
.claude
|
||||||
.output
|
.output
|
||||||
|
target
|
||||||
|
CLAUDE.md
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'drizzle-kit'
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: './src-tauri/database/schemas/**.ts',
|
schema: './src/database/schemas/**.ts',
|
||||||
out: './src-tauri/database/migrations',
|
out: './src-tauri/database/migrations',
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
//import tailwindcss from '@tailwindcss/vite'
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
@ -16,6 +14,9 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
|
head: {
|
||||||
|
viewport: 'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
||||||
|
},
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
name: 'fade',
|
name: 'fade',
|
||||||
},
|
},
|
||||||
@ -28,7 +29,6 @@ export default defineNuxtConfig({
|
|||||||
'@vueuse/nuxt',
|
'@vueuse/nuxt',
|
||||||
'@nuxt/icon',
|
'@nuxt/icon',
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
//"@nuxt/image",
|
|
||||||
'@nuxt/fonts',
|
'@nuxt/fonts',
|
||||||
'@nuxt/ui',
|
'@nuxt/ui',
|
||||||
],
|
],
|
||||||
@ -108,8 +108,7 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
haexVault: {
|
haexVault: {
|
||||||
lastVaultFileName: 'lastVaults.json',
|
deviceFileName: 'device.json',
|
||||||
instanceFileName: 'instance.json',
|
|
||||||
defaultVaultName: 'HaexHub',
|
defaultVaultName: 'HaexHub',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -123,7 +122,6 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
//plugins: [tailwindcss()],
|
|
||||||
// Better support for Tauri CLI output
|
// Better support for Tauri CLI output
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
// Enable environment variables
|
// Enable environment variables
|
||||||
|
|||||||
25
package.json
25
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "haex-hub",
|
"name": "haex-hub",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@ -21,27 +21,23 @@
|
|||||||
"@nuxt/eslint": "1.9.0",
|
"@nuxt/eslint": "1.9.0",
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "2.0.0",
|
"@nuxt/icon": "2.0.0",
|
||||||
"@nuxt/ui": "4.0.0",
|
"@nuxt/ui": "4.1.0",
|
||||||
"@nuxtjs/i18n": "10.0.6",
|
"@nuxtjs/i18n": "10.0.6",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tauri-apps/api": "^2.9.0",
|
"@tauri-apps/api": "^2.9.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-http": "2.5.2",
|
|
||||||
"@tauri-apps/plugin-notification": "2.3.1",
|
"@tauri-apps/plugin-notification": "2.3.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||||
"@tauri-apps/plugin-os": "^2.3.1",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-sql": "2.3.0",
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
"@tauri-apps/plugin-store": "^2.4.0",
|
|
||||||
"@vueuse/components": "^13.9.0",
|
"@vueuse/components": "^13.9.0",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"@vueuse/gesture": "^2.0.0",
|
"@vueuse/gesture": "^2.0.0",
|
||||||
"@vueuse/nuxt": "^13.9.0",
|
"@vueuse/nuxt": "^13.9.0",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"fuse.js": "^7.1.0",
|
|
||||||
"nuxt": "^4.1.3",
|
|
||||||
"nuxt-zod-i18n": "^1.12.1",
|
"nuxt-zod-i18n": "^1.12.1",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
@ -51,8 +47,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/hugeicons": "^1.2.17",
|
"@iconify-json/hugeicons": "^1.2.17",
|
||||||
"@iconify-json/lucide": "^1.2.70",
|
"@iconify-json/lucide": "^1.2.71",
|
||||||
"@iconify/json": "^2.2.399",
|
"@iconify/json": "^2.2.401",
|
||||||
"@iconify/tailwind4": "^1.0.6",
|
"@iconify/tailwind4": "^1.0.6",
|
||||||
"@libsql/client": "^0.15.15",
|
"@libsql/client": "^0.15.15",
|
||||||
"@tauri-apps/cli": "^2.9.1",
|
"@tauri-apps/cli": "^2.9.1",
|
||||||
@ -61,11 +57,12 @@
|
|||||||
"@vue/compiler-sfc": "^3.5.22",
|
"@vue/compiler-sfc": "^3.5.22",
|
||||||
"drizzle-kit": "^0.31.5",
|
"drizzle-kit": "^0.31.5",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
|
"nuxt": "^4.2.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "7.1.3",
|
"vite": "^7.1.3",
|
||||||
"vue-tsc": "3.0.6"
|
"vue-tsc": "3.0.6"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"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]
|
[package]
|
||||||
name = "haex-hub"
|
name = "haex-hub"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -20,14 +20,7 @@ tauri-build = { version = "2.2", features = [] }
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusqlite = { version = "0.37.0", features = [
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
"load_extension",
|
|
||||||
"bundled-sqlcipher-vendored-openssl",
|
|
||||||
"functions",
|
|
||||||
] }
|
|
||||||
|
|
||||||
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
|
|
||||||
#sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
ed25519-dalek = "2.1"
|
ed25519-dalek = "2.1"
|
||||||
fs_extra = "1.3.0"
|
fs_extra = "1.3.0"
|
||||||
@ -39,18 +32,26 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1.0.143"
|
serde_json = "1.0.143"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
||||||
tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
|
tauri = { version = "2.9.1", features = ["protocol-asset", "devtools"] }
|
||||||
tauri-plugin-dialog = "2.4.0"
|
tauri-plugin-dialog = "2.4.2"
|
||||||
tauri-plugin-fs = "2.4.0"
|
tauri-plugin-fs = "2.4.0"
|
||||||
tauri-plugin-http = "2.5.2"
|
tauri-plugin-http = "2.5.4"
|
||||||
tauri-plugin-notification = "2.3.1"
|
tauri-plugin-notification = "2.3.3"
|
||||||
tauri-plugin-opener = "2.5.0"
|
tauri-plugin-opener = "2.5.2"
|
||||||
tauri-plugin-os = "2.3"
|
tauri-plugin-os = "2.3.2"
|
||||||
tauri-plugin-persisted-scope = "2.3.2"
|
tauri-plugin-persisted-scope = "2.3.4"
|
||||||
tauri-plugin-store = "2.4.0"
|
tauri-plugin-store = "2.4.1"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
|
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
|
||||||
uhlc = "0.8.2"
|
uhlc = "0.8.2"
|
||||||
|
url = "2.5.7"
|
||||||
uuid = { version = "1.18.1", features = ["v4"] }
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||||||
zip = "6.0.0"
|
zip = "6.0.0"
|
||||||
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.
|
// 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.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
||||||
|
|
||||||
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, };
|
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, };
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
"notification:allow-create-channel",
|
"notification:allow-create-channel",
|
||||||
"notification:allow-list-channels",
|
"notification:allow-list-channels",
|
||||||
"notification:allow-notify",
|
"notification:allow-notify",
|
||||||
|
"notification:allow-is-permission-granted",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"opener:allow-open-url",
|
"opener:allow-open-url",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { writeFileSync, mkdirSync } from 'node:fs'
|
import { writeFileSync, mkdirSync } from 'node:fs'
|
||||||
import { join, dirname } from 'node:path'
|
import { join, dirname } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import tablesNames from './tableNames.json'
|
import tablesNames from '../../src/database/tableNames.json'
|
||||||
import { schema } from './index'
|
import { schema } from '../../src/database/index'
|
||||||
import { getTableColumns } from 'drizzle-orm'
|
import { getTableColumns } from 'drizzle-orm'
|
||||||
import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'
|
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,102 +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,
|
|
||||||
`extension_id` text,
|
|
||||||
`system_window_id` text,
|
|
||||||
`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,
|
|
||||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
||||||
CONSTRAINT "item_reference" CHECK(("haex_desktop_items"."item_type" = 'extension' AND "haex_desktop_items"."extension_id" IS NOT NULL AND "haex_desktop_items"."system_window_id" IS NULL) OR ("haex_desktop_items"."item_type" = 'system' AND "haex_desktop_items"."system_window_id" IS NOT NULL AND "haex_desktop_items"."extension_id" IS NULL) OR ("haex_desktop_items"."item_type" = 'file' AND "haex_desktop_items"."system_window_id" IS NOT NULL AND "haex_desktop_items"."extension_id" IS NULL) OR ("haex_desktop_items"."item_type" = 'folder' AND "haex_desktop_items"."system_window_id" IS NOT NULL AND "haex_desktop_items"."extension_id" IS NULL))
|
|
||||||
);
|
|
||||||
--> 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,670 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "bcdd9ad3-a87a-4a43-9eba-673f94b10287",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"extension_id": {
|
|
||||||
"name": "extension_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"system_window_id": {
|
|
||||||
"name": "system_window_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"position_x": {
|
|
||||||
"name": "position_x",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"position_y": {
|
|
||||||
"name": "position_y",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_desktop_items_workspace_id_haex_workspaces_id_fk": {
|
|
||||||
"name": "haex_desktop_items_workspace_id_haex_workspaces_id_fk",
|
|
||||||
"tableFrom": "haex_desktop_items",
|
|
||||||
"tableTo": "haex_workspaces",
|
|
||||||
"columnsFrom": [
|
|
||||||
"workspace_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"haex_desktop_items_extension_id_haex_extensions_id_fk": {
|
|
||||||
"name": "haex_desktop_items_extension_id_haex_extensions_id_fk",
|
|
||||||
"tableFrom": "haex_desktop_items",
|
|
||||||
"tableTo": "haex_extensions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"extension_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {
|
|
||||||
"item_reference": {
|
|
||||||
"name": "item_reference",
|
|
||||||
"value": "(\"haex_desktop_items\".\"item_type\" = 'extension' AND \"haex_desktop_items\".\"extension_id\" IS NOT NULL AND \"haex_desktop_items\".\"system_window_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'system' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'file' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'folder' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"haex_extension_permissions": {
|
|
||||||
"name": "haex_extension_permissions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"extension_id": {
|
|
||||||
"name": "extension_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"resource_type": {
|
|
||||||
"name": "resource_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"name": "action",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"name": "target",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"constraints": {
|
|
||||||
"name": "constraints",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "status",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'denied'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
|
||||||
"columns": [
|
|
||||||
"extension_id",
|
|
||||||
"resource_type",
|
|
||||||
"action",
|
|
||||||
"target"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
|
||||||
"tableFrom": "haex_extension_permissions",
|
|
||||||
"tableTo": "haex_extensions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"extension_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extensions": {
|
|
||||||
"name": "haex_extensions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"name": "public_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"name": "version",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "author",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"entry": {
|
|
||||||
"name": "entry",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'index.html'"
|
|
||||||
},
|
|
||||||
"homepage": {
|
|
||||||
"name": "homepage",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"name": "signature",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extensions_public_key_name_unique": {
|
|
||||||
"name": "haex_extensions_public_key_name_unique",
|
|
||||||
"columns": [
|
|
||||||
"public_key",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_notifications": {
|
|
||||||
"name": "haex_notifications",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"alt": {
|
|
||||||
"name": "alt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"name": "date",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"read": {
|
|
||||||
"name": "read",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"name": "source",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"name": "text",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_settings": {
|
|
||||||
"name": "haex_settings",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_settings_key_type_value_unique": {
|
|
||||||
"name": "haex_settings_key_type_value_unique",
|
|
||||||
"columns": [
|
|
||||||
"key",
|
|
||||||
"type",
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_workspaces": {
|
|
||||||
"name": "haex_workspaces",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"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": 1761430560028,
|
|
||||||
"tag": "0000_secret_ender_wiggin",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -24,6 +24,23 @@ android {
|
|||||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
val keystorePath = System.getenv("ANDROID_KEYSTORE_PATH")
|
||||||
|
val keystorePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD")
|
||||||
|
val keyAlias = System.getenv("ANDROID_KEY_ALIAS")
|
||||||
|
val keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
|
||||||
|
|
||||||
|
if (keystorePath != null && keystorePassword != null && keyAlias != null && keyPassword != null) {
|
||||||
|
storeFile = file(keystorePath)
|
||||||
|
storePassword = keystorePassword
|
||||||
|
this.keyAlias = keyAlias
|
||||||
|
this.keyPassword = keyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("debug") {
|
getByName("debug") {
|
||||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
@ -43,6 +60,12 @@ android {
|
|||||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
.toList().toTypedArray()
|
.toList().toTypedArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sign with release config if available
|
||||||
|
val releaseSigningConfig = signingConfigs.getByName("release")
|
||||||
|
if (releaseSigningConfig.storeFile != null) {
|
||||||
|
signingConfig = releaseSigningConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
|||||||
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."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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`"
|
"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",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"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.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"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.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"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.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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`"
|
"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",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"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.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"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.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"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.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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`"
|
"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",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"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.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"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.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"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.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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`"
|
"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",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"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.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"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.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"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.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"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.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"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",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"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.",
|
"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() {
|
pub fn generate_table_names() {
|
||||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
||||||
println!("Generiere Tabellennamen nach {}", out_dir);
|
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 dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||||
|
|
||||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
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())
|
f.write_all(code.as_bytes())
|
||||||
.expect("Konnte nicht in Zieldatei schreiben");
|
.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
|
/// 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 UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
||||||
const DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete";
|
const DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete";
|
||||||
|
|
||||||
//const SYNC_ACTIVE_KEY: &str = "sync_active";
|
|
||||||
|
|
||||||
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
|
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
|
||||||
|
|
||||||
/// Name der custom UUID-Generierungs-Funktion (registriert in database::core::open_and_init_db)
|
/// Name der custom UUID-Generierungs-Funktion (registriert in database::core::open_and_init_db)
|
||||||
@ -85,7 +83,8 @@ impl ColumnInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_safe_identifier(name: &str) -> bool {
|
fn is_safe_identifier(name: &str) -> bool {
|
||||||
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
// Allow alphanumeric characters, underscores, and hyphens (for extension names like "nuxt-app")
|
||||||
|
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein.
|
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein.
|
||||||
|
|||||||
@ -89,8 +89,15 @@ pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
|
|||||||
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
||||||
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
||||||
let dialect = SQLiteDialect {};
|
let dialect = SQLiteDialect {};
|
||||||
Parser::parse_sql(&dialect, sql).map_err(|e| DatabaseError::ParseError {
|
|
||||||
reason: e.to_string(),
|
// Normalize whitespace: replace multiple whitespaces (including newlines, tabs) with single space
|
||||||
|
let normalized_sql = sql
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
Parser::parse_sql(&dialect, &normalized_sql).map_err(|e| DatabaseError::ParseError {
|
||||||
|
reason: format!("Failed to parse SQL: {}", e),
|
||||||
sql: sql.to_string(),
|
sql: sql.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ use std::time::UNIX_EPOCH;
|
|||||||
use std::{fs, sync::Arc};
|
use std::{fs, sync::Arc};
|
||||||
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
||||||
use tauri_plugin_fs::FsExt;
|
use tauri_plugin_fs::FsExt;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use trash;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
||||||
@ -133,7 +135,6 @@ pub fn get_vaults_directory(app_handle: &AppHandle) -> Result<String, DatabaseEr
|
|||||||
Ok(vaults_dir.to_string_lossy().to_string())
|
Ok(vaults_dir.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
//#[serde(tag = "type", content = "details")]
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -212,7 +213,60 @@ pub fn vault_exists(app_handle: AppHandle, vault_name: String) -> Result<bool, D
|
|||||||
Ok(Path::new(&vault_path).exists())
|
Ok(Path::new(&vault_path).exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a vault database file
|
/// Moves a vault database file to trash (or deletes permanently if trash is unavailable)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn move_vault_to_trash(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
vault_name: String,
|
||||||
|
) -> Result<String, DatabaseError> {
|
||||||
|
// On Android, trash is not available, so delete permanently
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"Android platform detected, permanently deleting vault '{}'",
|
||||||
|
vault_name
|
||||||
|
);
|
||||||
|
return delete_vault(app_handle, vault_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On non-Android platforms, try to use trash
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
||||||
|
let vault_shm_path = format!("{}-shm", vault_path);
|
||||||
|
let vault_wal_path = format!("{}-wal", vault_path);
|
||||||
|
|
||||||
|
if !Path::new(&vault_path).exists() {
|
||||||
|
return Err(DatabaseError::IoError {
|
||||||
|
path: vault_path,
|
||||||
|
reason: "Vault does not exist".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to move to trash first (works on desktop systems)
|
||||||
|
let moved_to_trash = trash::delete(&vault_path).is_ok();
|
||||||
|
|
||||||
|
if moved_to_trash {
|
||||||
|
// Also try to move auxiliary files to trash (ignore errors as they might not exist)
|
||||||
|
let _ = trash::delete(&vault_shm_path);
|
||||||
|
let _ = trash::delete(&vault_wal_path);
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"Vault '{}' successfully moved to trash",
|
||||||
|
vault_name
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// Fallback: Permanent deletion if trash fails
|
||||||
|
println!(
|
||||||
|
"Trash not available, falling back to permanent deletion for vault '{}'",
|
||||||
|
vault_name
|
||||||
|
);
|
||||||
|
delete_vault(app_handle, vault_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a vault database file permanently (bypasses trash)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
|
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
|
||||||
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
||||||
@ -395,9 +449,6 @@ pub fn open_encrypted_database(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<String, DatabaseError> {
|
) -> Result<String, DatabaseError> {
|
||||||
println!("Opening encrypted database vault_path: {}", vault_path);
|
println!("Opening encrypted database vault_path: {}", vault_path);
|
||||||
|
|
||||||
// Vault-Pfad aus dem Namen ableiten
|
|
||||||
//let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
|
||||||
println!("Resolved vault path: {}", vault_path);
|
println!("Resolved vault path: {}", vault_path);
|
||||||
|
|
||||||
if !Path::new(&vault_path).exists() {
|
if !Path::new(&vault_path).exists() {
|
||||||
|
|||||||
@ -66,17 +66,124 @@ impl ExtensionManager {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to validate path and check for path traversal
|
||||||
|
/// Returns the cleaned path if valid, or None if invalid/not found
|
||||||
|
/// If require_exists is true, returns None if path doesn't exist
|
||||||
|
pub fn validate_path_in_directory(
|
||||||
|
base_dir: &PathBuf,
|
||||||
|
relative_path: &str,
|
||||||
|
require_exists: bool,
|
||||||
|
) -> Result<Option<PathBuf>, ExtensionError> {
|
||||||
|
// Check for path traversal patterns
|
||||||
|
if relative_path.contains("..") {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path traversal attempt: {}", relative_path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the path (same logic as in protocol.rs)
|
||||||
|
let clean_path = relative_path
|
||||||
|
.replace('\\', "/")
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.split('/')
|
||||||
|
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||||
|
.collect::<PathBuf>();
|
||||||
|
|
||||||
|
let full_path = base_dir.join(&clean_path);
|
||||||
|
|
||||||
|
// Check if file/directory exists (if required)
|
||||||
|
if require_exists && !full_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify path is within base directory
|
||||||
|
let canonical_base = base_dir
|
||||||
|
.canonicalize()
|
||||||
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
|
|
||||||
|
if let Ok(canonical_path) = full_path.canonicalize() {
|
||||||
|
if !canonical_path.starts_with(&canonical_base) {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path outside base directory: {}", relative_path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Some(canonical_path))
|
||||||
|
} else {
|
||||||
|
// Path doesn't exist yet - still validate it would be within base
|
||||||
|
if full_path.starts_with(&canonical_base) {
|
||||||
|
Ok(Some(full_path))
|
||||||
|
} else {
|
||||||
|
Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path outside base directory: {}", relative_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates icon path and falls back to favicon.ico if not specified
|
||||||
|
fn validate_and_resolve_icon_path(
|
||||||
|
extension_dir: &PathBuf,
|
||||||
|
haextension_dir: &str,
|
||||||
|
icon_path: Option<&str>,
|
||||||
|
) -> Result<Option<String>, ExtensionError> {
|
||||||
|
// If icon is specified in manifest, validate it
|
||||||
|
if let Some(icon) = icon_path {
|
||||||
|
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, icon, true)? {
|
||||||
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
|
} else {
|
||||||
|
eprintln!("WARNING: Icon path specified in manifest not found: {}", icon);
|
||||||
|
// Continue to fallback logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 1: Check haextension/favicon.ico
|
||||||
|
let haextension_favicon = format!("{}/favicon.ico", haextension_dir);
|
||||||
|
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)? {
|
||||||
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 2: Check public/favicon.ico
|
||||||
|
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)? {
|
||||||
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No icon found
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
|
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
|
||||||
fn extract_and_validate_extension(
|
fn extract_and_validate_extension(
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
temp_prefix: &str,
|
temp_prefix: &str,
|
||||||
|
app_handle: &AppHandle,
|
||||||
) -> Result<ExtractedExtension, ExtensionError> {
|
) -> Result<ExtractedExtension, ExtensionError> {
|
||||||
let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4()));
|
// Use app_cache_dir for better Android compatibility
|
||||||
|
let cache_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_cache_dir()
|
||||||
|
.map_err(|e| ExtensionError::InstallationFailed {
|
||||||
|
reason: format!("Cannot get app cache dir: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let temp_id = uuid::Uuid::new_v4();
|
||||||
|
let temp = cache_dir.join(format!("{}_{}", temp_prefix, temp_id));
|
||||||
|
let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp"));
|
||||||
|
|
||||||
|
// Write bytes to a temporary ZIP file first (important for Android file system)
|
||||||
|
fs::write(&zip_file_path, &bytes).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Create extraction directory
|
||||||
fs::create_dir_all(&temp)
|
fs::create_dir_all(&temp)
|
||||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| {
|
// Open ZIP file from disk (more reliable on Android than from memory)
|
||||||
|
let zip_file = fs::File::open(&zip_file_path).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
|
||||||
ExtensionError::InstallationFailed {
|
ExtensionError::InstallationFailed {
|
||||||
reason: format!("Invalid ZIP: {}", e),
|
reason: format!("Invalid ZIP: {}", e),
|
||||||
}
|
}
|
||||||
@ -88,6 +195,9 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot extract ZIP: {}", e),
|
reason: format!("Cannot extract ZIP: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Clean up temporary ZIP file
|
||||||
|
let _ = fs::remove_file(&zip_file_path);
|
||||||
|
|
||||||
// Read haextension_dir from config if it exists, otherwise use default
|
// Read haextension_dir from config if it exists, otherwise use default
|
||||||
let config_path = temp.join("haextension.config.json");
|
let config_path = temp.join("haextension.config.json");
|
||||||
let haextension_dir = if config_path.exists() {
|
let haextension_dir = if config_path.exists() {
|
||||||
@ -108,42 +218,17 @@ impl ExtensionManager {
|
|||||||
.unwrap_or("haextension")
|
.unwrap_or("haextension")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
// Security: Validate that haextension_dir doesn't contain ".." for path traversal
|
|
||||||
if dir.contains("..") {
|
|
||||||
return Err(ExtensionError::ManifestError {
|
|
||||||
reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dir
|
dir
|
||||||
} else {
|
} else {
|
||||||
"haextension".to_string()
|
"haextension".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the manifest path
|
// Validate manifest path using helper function
|
||||||
let manifest_path = temp.join(&haextension_dir).join("manifest.json");
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
|
let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)?
|
||||||
// Ensure the resolved path is still within temp directory (safety check against path traversal)
|
.ok_or_else(|| ExtensionError::ManifestError {
|
||||||
let canonical_temp = temp.canonicalize()
|
|
||||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
|
||||||
|
|
||||||
// Only check if manifest_path parent exists to avoid errors
|
|
||||||
if let Some(parent) = manifest_path.parent() {
|
|
||||||
if let Ok(canonical_manifest_dir) = parent.canonicalize() {
|
|
||||||
if !canonical_manifest_dir.starts_with(&canonical_temp) {
|
|
||||||
return Err(ExtensionError::ManifestError {
|
|
||||||
reason: "Security violation: manifest path outside extension directory".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if manifest exists
|
|
||||||
if !manifest_path.exists() {
|
|
||||||
return Err(ExtensionError::ManifestError {
|
|
||||||
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
||||||
});
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
let actual_dir = temp.clone();
|
let actual_dir = temp.clone();
|
||||||
let manifest_content =
|
let manifest_content =
|
||||||
@ -151,7 +236,11 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot read manifest: {}", e),
|
reason: format!("Cannot read manifest: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
|
// Validate and resolve icon path with fallback logic
|
||||||
|
let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?;
|
||||||
|
manifest.icon = validated_icon;
|
||||||
|
|
||||||
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
||||||
ExtensionError::SignatureVerificationFailed {
|
ExtensionError::SignatureVerificationFailed {
|
||||||
@ -427,9 +516,10 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
pub async fn preview_extension_internal(
|
pub async fn preview_extension_internal(
|
||||||
&self,
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?;
|
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
||||||
|
|
||||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||||
&extracted.manifest.public_key,
|
&extracted.manifest.public_key,
|
||||||
@ -454,7 +544,7 @@ impl ExtensionManager {
|
|||||||
custom_permissions: EditablePermissions,
|
custom_permissions: EditablePermissions,
|
||||||
state: &State<'_, AppState>,
|
state: &State<'_, AppState>,
|
||||||
) -> Result<String, ExtensionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?;
|
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
|
||||||
|
|
||||||
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
||||||
ExtensionCrypto::verify_signature(
|
ExtensionCrypto::verify_signature(
|
||||||
@ -525,7 +615,7 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
// 1. Extension-Eintrag erstellen mit generierter UUID
|
// 1. Extension-Eintrag erstellen mit generierter UUID
|
||||||
let insert_ext_sql = format!(
|
let insert_ext_sql = format!(
|
||||||
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
TABLE_EXTENSIONS
|
TABLE_EXTENSIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -545,6 +635,7 @@ impl ExtensionManager {
|
|||||||
extracted.manifest.homepage,
|
extracted.manifest.homepage,
|
||||||
extracted.manifest.description,
|
extracted.manifest.description,
|
||||||
true, // enabled
|
true, // enabled
|
||||||
|
extracted.manifest.single_instance.unwrap_or(false),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -623,7 +714,7 @@ impl ExtensionManager {
|
|||||||
// Lade alle Daten aus der Datenbank
|
// Lade alle Daten aus der Datenbank
|
||||||
let extensions = with_connection(&state.db, |conn| {
|
let extensions = with_connection(&state.db, |conn| {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}",
|
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}",
|
||||||
TABLE_EXTENSIONS
|
TABLE_EXTENSIONS
|
||||||
);
|
);
|
||||||
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
||||||
@ -655,13 +746,16 @@ impl ExtensionManager {
|
|||||||
})?
|
})?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
author: row[3].as_str().map(String::from),
|
author: row[3].as_str().map(String::from),
|
||||||
entry: row[4].as_str().unwrap_or("index.html").to_string(),
|
entry: row[4].as_str().map(String::from),
|
||||||
icon: row[5].as_str().map(String::from),
|
icon: row[5].as_str().map(String::from),
|
||||||
public_key: row[6].as_str().unwrap_or("").to_string(),
|
public_key: row[6].as_str().unwrap_or("").to_string(),
|
||||||
signature: row[7].as_str().unwrap_or("").to_string(),
|
signature: row[7].as_str().unwrap_or("").to_string(),
|
||||||
permissions: ExtensionPermissions::default(),
|
permissions: ExtensionPermissions::default(),
|
||||||
homepage: row[8].as_str().map(String::from),
|
homepage: row[8].as_str().map(String::from),
|
||||||
description: row[9].as_str().map(String::from),
|
description: row[9].as_str().map(String::from),
|
||||||
|
single_instance: row[11]
|
||||||
|
.as_bool()
|
||||||
|
.or_else(|| row[11].as_i64().map(|v| v != 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let enabled = row[10]
|
let enabled = row[10]
|
||||||
@ -722,33 +816,12 @@ impl ExtensionManager {
|
|||||||
Ok(config_content) => {
|
Ok(config_content) => {
|
||||||
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
let dir = config
|
config
|
||||||
.get("dev")
|
.get("dev")
|
||||||
.and_then(|dev| dev.get("haextension_dir"))
|
.and_then(|dev| dev.get("haextension_dir"))
|
||||||
.and_then(|dir| dir.as_str())
|
.and_then(|dir| dir.as_str())
|
||||||
.unwrap_or("haextension")
|
.unwrap_or("haextension")
|
||||||
.to_string();
|
.to_string()
|
||||||
|
|
||||||
// Security: Validate that haextension_dir doesn't contain ".."
|
|
||||||
if dir.contains("..") {
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG: Invalid haextension_dir for: {}, contains '..'",
|
|
||||||
extension_id
|
|
||||||
);
|
|
||||||
self.missing_extensions
|
|
||||||
.lock()
|
|
||||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
|
||||||
reason: e.to_string(),
|
|
||||||
})?
|
|
||||||
.push(MissingExtension {
|
|
||||||
id: extension_id.clone(),
|
|
||||||
public_key: extension_data.manifest.public_key.clone(),
|
|
||||||
name: extension_data.manifest.name.clone(),
|
|
||||||
version: extension_data.manifest.version.clone(),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
dir
|
|
||||||
}
|
}
|
||||||
Err(_) => "haextension".to_string(),
|
Err(_) => "haextension".to_string(),
|
||||||
}
|
}
|
||||||
@ -759,12 +832,14 @@ impl ExtensionManager {
|
|||||||
"haextension".to_string()
|
"haextension".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if manifest.json exists in the haextension_dir
|
// Validate manifest.json path using helper function
|
||||||
let manifest_path = extension_path.join(&haextension_dir).join("manifest.json");
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
if !manifest_path.exists() {
|
if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"DEBUG: manifest.json missing for: {} at {:?}",
|
"DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json",
|
||||||
extension_id, manifest_path
|
extension_id, haextension_dir
|
||||||
);
|
);
|
||||||
self.missing_extensions
|
self.missing_extensions
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@ -57,13 +57,20 @@ pub struct ExtensionManifest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub entry: String,
|
#[serde(default = "default_entry_value")]
|
||||||
|
pub entry: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
pub permissions: ExtensionPermissions,
|
pub permissions: ExtensionPermissions,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub single_instance: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_entry_value() -> Option<String> {
|
||||||
|
Some("index.html".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionManifest {
|
impl ExtensionManifest {
|
||||||
@ -172,6 +179,8 @@ pub struct ExtensionInfoResponse {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
|
pub entry: Option<String>,
|
||||||
|
pub single_instance: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub dev_server_url: Option<String>,
|
pub dev_server_url: Option<String>,
|
||||||
}
|
}
|
||||||
@ -197,6 +206,8 @@ impl ExtensionInfoResponse {
|
|||||||
description: extension.manifest.description.clone(),
|
description: extension.manifest.description.clone(),
|
||||||
homepage: extension.manifest.homepage.clone(),
|
homepage: extension.manifest.homepage.clone(),
|
||||||
icon: extension.manifest.icon.clone(),
|
icon: extension.manifest.icon.clone(),
|
||||||
|
entry: extension.manifest.entry.clone(),
|
||||||
|
single_instance: extension.manifest.single_instance,
|
||||||
dev_server_url,
|
dev_server_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,9 @@ impl ExtensionCrypto {
|
|||||||
let relative = path.strip_prefix(dir)
|
let relative = path.strip_prefix(dir)
|
||||||
.unwrap_or(&path)
|
.unwrap_or(&path)
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string()
|
||||||
|
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
|
||||||
|
.replace('\\', "/");
|
||||||
(relative, path)
|
(relative, path)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -56,16 +58,30 @@ impl ExtensionCrypto {
|
|||||||
// 3. Sortiere nach relativen Pfaden
|
// 3. Sortiere nach relativen Pfaden
|
||||||
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
println!("=== Files to hash ({}): ===", relative_files.len());
|
|
||||||
for (rel, _) in &relative_files {
|
|
||||||
println!(" - {}", rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
|
|
||||||
|
// Canonicalize manifest path for comparison (important on Android where symlinks may differ)
|
||||||
|
// Also ensure the canonical path is still within the allowed directory (security check)
|
||||||
|
let canonical_manifest_path = manifest_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| manifest_path.to_path_buf());
|
||||||
|
|
||||||
|
// Security: Verify canonical manifest path is still within dir
|
||||||
|
let canonical_dir = dir.canonicalize()
|
||||||
|
.unwrap_or_else(|_| dir.to_path_buf());
|
||||||
|
|
||||||
|
if !canonical_manifest_path.starts_with(&canonical_dir) {
|
||||||
|
return Err(ExtensionError::ManifestError {
|
||||||
|
reason: format!("Manifest path resolves outside of extension directory (potential path traversal)"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Inhalte der sortierten Dateien hashen
|
// 4. Inhalte der sortierten Dateien hashen
|
||||||
for (_relative, file_path) in relative_files {
|
for (_relative, file_path) in relative_files {
|
||||||
if file_path == manifest_path {
|
// Canonicalize file_path for comparison
|
||||||
|
let canonical_file_path = file_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| file_path.clone());
|
||||||
|
|
||||||
|
if canonical_file_path == canonical_manifest_path {
|
||||||
// FÜR DIE MANIFEST.JSON:
|
// FÜR DIE MANIFEST.JSON:
|
||||||
let content_str = fs::read_to_string(&file_path)
|
let content_str = fs::read_to_string(&file_path)
|
||||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
@ -94,8 +110,12 @@ impl ExtensionCrypto {
|
|||||||
reason: format!("Failed to serialize manifest: {}", e),
|
reason: format!("Failed to serialize manifest: {}", e),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
|
||||||
hasher.update(canonical_manifest_content.as_bytes());
|
// Normalisiere Zeilenenden zu Unix-Style (\n), wie Node.js JSON.stringify es macht
|
||||||
|
// Dies ist wichtig für plattformübergreifende Konsistenz (Desktop vs Android)
|
||||||
|
let normalized_content = canonical_manifest_content.replace("\r\n", "\n");
|
||||||
|
|
||||||
|
hasher.update(normalized_content.as_bytes());
|
||||||
} else {
|
} else {
|
||||||
// FÜR ALLE ANDEREN DATEIEN:
|
// FÜR ALLE ANDEREN DATEIEN:
|
||||||
let content =
|
let content =
|
||||||
|
|||||||
@ -64,7 +64,13 @@ impl SqlExecutor {
|
|||||||
|
|
||||||
// Trigger-Logik für CREATE TABLE
|
// Trigger-Logik für CREATE TABLE
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
if let Statement::CreateTable(create_table_details) = statement {
|
||||||
let table_name_str = create_table_details.name.to_string();
|
let raw_name = create_table_details.name.to_string();
|
||||||
|
// Remove quotes from table name
|
||||||
|
let table_name_str = raw_name
|
||||||
|
.trim_matches('"')
|
||||||
|
.trim_matches('`')
|
||||||
|
.to_string();
|
||||||
|
eprintln!("DEBUG: Setting up triggers for table: {}", table_name_str);
|
||||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +164,13 @@ impl SqlExecutor {
|
|||||||
|
|
||||||
// Trigger-Logik für CREATE TABLE
|
// Trigger-Logik für CREATE TABLE
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
if let Statement::CreateTable(create_table_details) = statement {
|
||||||
let table_name_str = create_table_details.name.to_string();
|
let raw_name = create_table_details.name.to_string();
|
||||||
|
// Remove quotes from table name
|
||||||
|
let table_name_str = raw_name
|
||||||
|
.trim_matches('"')
|
||||||
|
.trim_matches('`')
|
||||||
|
.to_string();
|
||||||
|
eprintln!("DEBUG: Setting up triggers for table (RETURNING): {}", table_name_str);
|
||||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use crate::crdt::transformer::CrdtTransformer;
|
|||||||
use crate::crdt::trigger;
|
use crate::crdt::trigger;
|
||||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
@ -110,7 +111,7 @@ pub async fn extension_sql_execute(
|
|||||||
public_key: String,
|
public_key: String,
|
||||||
name: String,
|
name: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<String>, ExtensionError> {
|
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||||
// Get extension to retrieve its ID
|
// Get extension to retrieve its ID
|
||||||
let extension = state
|
let extension = state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
@ -129,58 +130,87 @@ pub async fn extension_sql_execute(
|
|||||||
// SQL parsing
|
// SQL parsing
|
||||||
let mut ast_vec = parse_sql_statements(sql)?;
|
let mut ast_vec = parse_sql_statements(sql)?;
|
||||||
|
|
||||||
|
if ast_vec.len() != 1 {
|
||||||
|
return Err(ExtensionError::Database {
|
||||||
|
source: DatabaseError::ExecutionError {
|
||||||
|
sql: sql.to_string(),
|
||||||
|
reason: "extension_sql_execute should only receive a single SQL statement"
|
||||||
|
.to_string(),
|
||||||
|
table: None,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut statement = ast_vec.pop().unwrap();
|
||||||
|
|
||||||
|
// Check if statement has RETURNING clause
|
||||||
|
let has_returning = crate::database::core::statement_has_returning(&statement);
|
||||||
|
|
||||||
// Database operation
|
// Database operation
|
||||||
with_connection(&state.db, |conn| {
|
with_connection(&state.db, |conn| {
|
||||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let transformer = CrdtTransformer::new();
|
let transformer = CrdtTransformer::new();
|
||||||
let executor = StatementExecutor::new(&tx);
|
|
||||||
|
// Get HLC service reference
|
||||||
|
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||||
|
reason: "Failed to lock HLC service".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Generate HLC timestamp
|
// Generate HLC timestamp
|
||||||
let hlc_timestamp = state
|
let hlc_timestamp = hlc_service
|
||||||
.hlc
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.new_timestamp_and_persist(&tx)
|
.new_timestamp_and_persist(&tx)
|
||||||
.map_err(|e| DatabaseError::HlcError {
|
.map_err(|e| DatabaseError::HlcError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Transform statements
|
// Transform statement
|
||||||
let mut modified_schema_tables = HashSet::new();
|
transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
|
||||||
for statement in &mut ast_vec {
|
|
||||||
if let Some(table_name) =
|
|
||||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
|
||||||
{
|
|
||||||
modified_schema_tables.insert(table_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert parameters
|
// Convert parameters to references
|
||||||
let sql_values = ValueConverter::convert_params(¶ms)?;
|
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
|
let result = if has_returning {
|
||||||
for statement in ast_vec {
|
// Use query_internal for statements with RETURNING
|
||||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
let (_, rows) = SqlExecutor::query_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶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 {
|
// Handle CREATE TABLE trigger setup
|
||||||
let table_name_str = create_table_details.name.to_string();
|
if let Statement::CreateTable(ref create_table_details) = statement {
|
||||||
println!(
|
// Extract table name and remove quotes (both " and `)
|
||||||
"Table '{}' created by extension, setting up CRDT triggers...",
|
let raw_name = create_table_details.name.to_string();
|
||||||
table_name_str
|
println!("DEBUG: Raw table name from AST: {:?}", raw_name);
|
||||||
);
|
println!("DEBUG: Raw table name chars: {:?}", raw_name.chars().collect::<Vec<_>>());
|
||||||
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
|
||||||
println!(
|
let table_name_str = raw_name
|
||||||
"Triggers for table '{}' successfully created.",
|
.trim_matches('"')
|
||||||
table_name_str
|
.trim_matches('`')
|
||||||
);
|
.to_string();
|
||||||
}
|
|
||||||
|
println!("DEBUG: Cleaned table name: {:?}", table_name_str);
|
||||||
|
println!("DEBUG: Cleaned table name chars: {:?}", table_name_str.chars().collect::<Vec<_>>());
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Table '{}' created by extension, setting up CRDT triggers...",
|
||||||
|
table_name_str
|
||||||
|
);
|
||||||
|
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
||||||
|
println!(
|
||||||
|
"Triggers for table '{}' successfully created.",
|
||||||
|
table_name_str
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit transaction
|
// Commit transaction
|
||||||
tx.commit().map_err(DatabaseError::from)?;
|
tx.commit().map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(modified_schema_tables.into_iter().collect())
|
Ok(result)
|
||||||
})
|
})
|
||||||
.map_err(ExtensionError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
@ -192,7 +222,7 @@ pub async fn extension_sql_select(
|
|||||||
public_key: String,
|
public_key: String,
|
||||||
name: String,
|
name: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||||
// Get extension to retrieve its ID
|
// Get extension to retrieve its ID
|
||||||
let extension = state
|
let extension = state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
@ -229,10 +259,9 @@ pub async fn extension_sql_select(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database operation
|
// Database operation - return Vec<Vec<JsonValue>> like sql_select_with_crdt
|
||||||
with_connection(&state.db, |conn| {
|
with_connection(&state.db, |conn| {
|
||||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
|
||||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||||
let transformed_sql = stmt_to_execute.to_string();
|
let transformed_sql = stmt_to_execute.to_string();
|
||||||
|
|
||||||
@ -245,51 +274,34 @@ pub async fn extension_sql_select(
|
|||||||
table: None,
|
table: None,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let column_names: Vec<String> = prepared_stmt
|
let num_columns = prepared_stmt.column_count();
|
||||||
.column_names()
|
let mut rows = prepared_stmt
|
||||||
.into_iter()
|
.query(params_from_iter(sql_params.iter()))
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rows = prepared_stmt
|
|
||||||
.query_map(params_from_iter(sql_params.iter()), |row| {
|
|
||||||
row_to_json_value(row, &column_names)
|
|
||||||
})
|
|
||||||
.map_err(|e| DatabaseError::QueryError {
|
.map_err(|e| DatabaseError::QueryError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||||
for row_result in rows {
|
|
||||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?);
|
})? {
|
||||||
|
let mut row_values: Vec<JsonValue> = Vec::new();
|
||||||
|
for i in 0..num_columns {
|
||||||
|
let value_ref = row.get_ref(i).map_err(|e| DatabaseError::QueryError {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
let json_value = crate::database::core::convert_value_ref_to_json(value_ref)?;
|
||||||
|
row_values.push(json_value);
|
||||||
|
}
|
||||||
|
result_vec.push(row_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(results)
|
Ok(result_vec)
|
||||||
})
|
})
|
||||||
.map_err(ExtensionError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
|
||||||
fn row_to_json_value(
|
|
||||||
row: &rusqlite::Row,
|
|
||||||
columns: &[String],
|
|
||||||
) -> Result<JsonValue, rusqlite::Error> {
|
|
||||||
let mut map = serde_json::Map::new();
|
|
||||||
for (i, col_name) in columns.iter().enumerate() {
|
|
||||||
let value = row.get::<usize, rusqlite::types::Value>(i)?;
|
|
||||||
let json_value = match value {
|
|
||||||
rusqlite::types::Value::Null => JsonValue::Null,
|
|
||||||
rusqlite::types::Value::Integer(i) => json!(i),
|
|
||||||
rusqlite::types::Value::Real(f) => json!(f),
|
|
||||||
rusqlite::types::Value::Text(s) => json!(s),
|
|
||||||
rusqlite::types::Value::Blob(blob) => json!(blob.to_vec()),
|
|
||||||
};
|
|
||||||
map.insert(col_name.clone(), json_value);
|
|
||||||
}
|
|
||||||
Ok(JsonValue::Object(map))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validiert Parameter gegen SQL-Platzhalter
|
/// Validiert Parameter gegen SQL-Platzhalter
|
||||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/// src-tauri/src/extension/mod.rs
|
/// src-tauri/src/extension/mod.rs
|
||||||
use crate::{
|
use crate::{
|
||||||
extension::{
|
extension::{
|
||||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||||
error::ExtensionError,
|
error::ExtensionError,
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn preview_extension(
|
pub async fn preview_extension(
|
||||||
|
app_handle: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
state
|
state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.preview_extension_internal(file_bytes)
|
.preview_extension_internal(&app_handle, file_bytes)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,20 +321,19 @@ pub async fn load_dev_extension(
|
|||||||
}
|
}
|
||||||
eprintln!("✅ Dev server is reachable");
|
eprintln!("✅ Dev server is reachable");
|
||||||
|
|
||||||
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
// 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||||
let manifest_path = extension_path_buf
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
.join(&haextension_dir)
|
let manifest_path = ExtensionManager::validate_path_in_directory(
|
||||||
.join("manifest.json");
|
&extension_path_buf,
|
||||||
|
&manifest_relative_path,
|
||||||
// Check if manifest exists
|
true,
|
||||||
if !manifest_path.exists() {
|
)?
|
||||||
return Err(ExtensionError::ManifestError {
|
.ok_or_else(|| ExtensionError::ManifestError {
|
||||||
reason: format!(
|
reason: format!(
|
||||||
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.",
|
"Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||||
manifest_path.display()
|
haextension_dir
|
||||||
),
|
),
|
||||||
});
|
})?;
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Read and parse manifest
|
// 3. Read and parse manifest
|
||||||
let manifest_content =
|
let manifest_content =
|
||||||
@ -343,9 +343,8 @@ pub async fn load_dev_extension(
|
|||||||
|
|
||||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name>
|
// 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
|
||||||
let key_prefix = manifest.public_key.chars().take(8).collect::<String>();
|
let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
|
||||||
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
|
|
||||||
|
|
||||||
// 5. Check if dev extension already exists (allow reload)
|
// 5. Check if dev extension already exists (allow reload)
|
||||||
if let Some(existing) = state
|
if let Some(existing) = state
|
||||||
|
|||||||
@ -197,6 +197,30 @@ impl PermissionManager {
|
|||||||
action: Action,
|
action: Action,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
) -> Result<(), ExtensionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
|
// Remove quotes from table name if present (from SDK's getTableName())
|
||||||
|
let clean_table_name = table_name.trim_matches('"');
|
||||||
|
|
||||||
|
// Auto-allow: Extensions have full access to their own tables
|
||||||
|
// Table format: {publicKey}__{extensionName}__{tableName}
|
||||||
|
// Extension ID format: dev_{publicKey}_{extensionName} or {publicKey}_{extensionName}
|
||||||
|
|
||||||
|
// Get the extension to check if this is its own table
|
||||||
|
let extension = app_state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension(extension_id)
|
||||||
|
.ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Extension with ID {} not found", extension_id),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Build expected table prefix: {publicKey}__{extensionName}__
|
||||||
|
let expected_prefix = format!("{}__{}__", extension.manifest.public_key, extension.manifest.name);
|
||||||
|
|
||||||
|
if clean_table_name.starts_with(&expected_prefix) {
|
||||||
|
// This is the extension's own table - auto-allow
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not own table - check explicit permissions
|
||||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||||
|
|
||||||
let has_permission = permissions
|
let has_permission = permissions
|
||||||
@ -205,7 +229,7 @@ impl PermissionManager {
|
|||||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||||
.any(|perm| {
|
.any(|perm| {
|
||||||
if perm.target != "*" && perm.target != table_name {
|
if perm.target != "*" && perm.target != clean_table_name {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|||||||
@ -68,6 +68,7 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
database::create_encrypted_database,
|
database::create_encrypted_database,
|
||||||
database::delete_vault,
|
database::delete_vault,
|
||||||
|
database::move_vault_to_trash,
|
||||||
database::list_vaults,
|
database::list_vaults,
|
||||||
database::open_encrypted_database,
|
database::open_encrypted_database,
|
||||||
database::sql_execute_with_crdt,
|
database::sql_execute_with_crdt,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "haex-hub",
|
"productName": "haex-hub",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"identifier": "space.haex.hub",
|
"identifier": "space.haex.hub",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"devUrl": "http://localhost:3003",
|
"devUrl": "http://localhost:3003",
|
||||||
"beforeBuildCommand": "pnpm generate",
|
"beforeBuildCommand": "pnpm generate",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../.output/public"
|
||||||
},
|
},
|
||||||
|
|
||||||
"app": {
|
"app": {
|
||||||
@ -25,7 +25,8 @@
|
|||||||
"'self'",
|
"'self'",
|
||||||
"http://tauri.localhost",
|
"http://tauri.localhost",
|
||||||
"haex-extension:",
|
"haex-extension:",
|
||||||
"'wasm-unsafe-eval'"
|
"'wasm-unsafe-eval'",
|
||||||
|
"'unsafe-inline'"
|
||||||
],
|
],
|
||||||
"style-src": [
|
"style-src": [
|
||||||
"'self'",
|
"'self'",
|
||||||
|
|||||||
@ -3,6 +3,8 @@ export default defineAppConfig({
|
|||||||
colors: {
|
colors: {
|
||||||
primary: 'sky',
|
primary: 'sky',
|
||||||
secondary: 'fuchsia',
|
secondary: 'fuchsia',
|
||||||
|
warning: 'yellow',
|
||||||
|
danger: 'red',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<UApp :locale="locales[locale]">
|
<UApp :locale="locales[locale]">
|
||||||
<div data-vaul-drawer-wrapper>
|
<div data-vaul-drawer-wrapper>
|
||||||
<NuxtLayout>
|
<NuxtPage />
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
</div>
|
||||||
</UApp>
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -13,6 +13,46 @@
|
|||||||
[disabled] {
|
[disabled] {
|
||||||
@apply cursor-not-allowed;
|
@apply cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Define safe-area-insets as CSS custom properties for JavaScript access */
|
||||||
|
:root {
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verhindere Scrolling auf html und body */
|
||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
height: 100vh; /* Fallback */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__nuxt {
|
||||||
|
/* 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 {
|
@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>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="desktopEl"
|
ref="desktopEl"
|
||||||
class="w-full h-full relative overflow-hidden isolate"
|
class="absolute inset-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<Swiper
|
<Swiper
|
||||||
:modules="[SwiperNavigation]"
|
:modules="[SwiperNavigation]"
|
||||||
@ -13,7 +13,7 @@
|
|||||||
:no-swiping="true"
|
:no-swiping="true"
|
||||||
no-swiping-class="no-swipe"
|
no-swiping-class="no-swipe"
|
||||||
:allow-touch-move="allowSwipe"
|
:allow-touch-move="allowSwipe"
|
||||||
class="w-full h-full"
|
class="h-full w-full"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
@swiper="onSwiperInit"
|
@swiper="onSwiperInit"
|
||||||
@slide-change="onSlideChange"
|
@slide-change="onSlideChange"
|
||||||
@ -24,7 +24,7 @@
|
|||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full h-full relative isolate"
|
class="w-full h-full relative"
|
||||||
@click.self.stop="handleDesktopClick"
|
@click.self.stop="handleDesktopClick"
|
||||||
@mousedown.left.self="handleAreaSelectStart"
|
@mousedown.left.self="handleAreaSelectStart"
|
||||||
@dragover.prevent="handleDragOver"
|
@dragover.prevent="handleDragOver"
|
||||||
@ -51,7 +51,6 @@
|
|||||||
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="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'"
|
:class="showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
||||||
/>
|
/>
|
||||||
<!-- </Transition> -->
|
|
||||||
|
|
||||||
<!-- Area Selection Box -->
|
<!-- Area Selection Box -->
|
||||||
<div
|
<div
|
||||||
@ -116,6 +115,14 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find(
|
||||||
|
(ext) => ext.id === window.sourceId,
|
||||||
|
)?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
@ -165,6 +172,13 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||||
|
?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
@ -199,52 +213,6 @@
|
|||||||
|
|
||||||
<!-- Window Overview Modal -->
|
<!-- Window Overview Modal -->
|
||||||
<HaexWindowOverview />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -600,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 () => {
|
/* const handleRemoveWorkspace = async () => {
|
||||||
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
v-if="preview?.manifest.icon"
|
v-if="preview?.manifest.icon"
|
||||||
class="w-16 h-16 flex-shrink-0"
|
class="w-16 h-16 shrink-0"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
:name="preview.manifest.icon"
|
:name="preview.manifest.icon"
|
||||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const permissionAccordionItems = computed(() => {
|
const permissionAccordionItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<UPopover v-model:open="open">
|
<UDrawer
|
||||||
|
v-model:open="open"
|
||||||
|
direction="right"
|
||||||
|
:title="t('launcher.title')"
|
||||||
|
:description="t('launcher.description')"
|
||||||
|
:ui="{
|
||||||
|
content: 'w-dvw max-w-md sm:max-w-fit',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
icon="material-symbols:apps"
|
icon="material-symbols:apps"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
@ -9,58 +17,64 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
<div class="p-4 h-full overflow-y-auto">
|
||||||
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
<div class="flex flex-wrap">
|
||||||
<UContextMenu
|
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
||||||
v-for="item in launcherItems"
|
<UContextMenu
|
||||||
:key="item.id"
|
v-for="item in launcherItems"
|
||||||
:items="getContextMenuItems(item)"
|
:key="item.id"
|
||||||
>
|
:items="getContextMenuItems(item)"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
square
|
||||||
|
size="lg"
|
||||||
|
variant="ghost"
|
||||||
|
:ui="{
|
||||||
|
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
|
||||||
|
leadingIcon: 'size-10',
|
||||||
|
label: 'w-full',
|
||||||
|
}"
|
||||||
|
:icon="item.icon"
|
||||||
|
:label="item.name"
|
||||||
|
:tooltip="item.name"
|
||||||
|
draggable="true"
|
||||||
|
@click="openItem(item)"
|
||||||
|
@dragstart="handleDragStart($event, item)"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
/>
|
||||||
|
</UContextMenu>
|
||||||
|
|
||||||
|
<!-- Disabled Extensions (grayed out) -->
|
||||||
<UiButton
|
<UiButton
|
||||||
|
v-for="extension in disabledExtensions"
|
||||||
|
:key="extension.id"
|
||||||
square
|
square
|
||||||
size="lg"
|
size="xl"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
:disabled="true"
|
||||||
:ui="{
|
:ui="{
|
||||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
|
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
|
||||||
leadingIcon: 'size-10',
|
leadingIcon: 'size-10',
|
||||||
label: 'w-full',
|
label: 'w-full',
|
||||||
}"
|
}"
|
||||||
:icon="item.icon"
|
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
|
||||||
:label="item.name"
|
:label="extension.name"
|
||||||
:tooltip="item.name"
|
:tooltip="`${extension.name} (${t('disabled')})`"
|
||||||
draggable="true"
|
|
||||||
@click="openItem(item)"
|
|
||||||
@dragstart="handleDragStart($event, item)"
|
|
||||||
@dragend="handleDragEnd"
|
|
||||||
/>
|
/>
|
||||||
</UContextMenu>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Disabled Extensions (grayed out) -->
|
|
||||||
<UiButton
|
|
||||||
v-for="extension in disabledExtensions"
|
|
||||||
:key="extension.id"
|
|
||||||
square
|
|
||||||
size="xl"
|
|
||||||
variant="ghost"
|
|
||||||
:disabled="true"
|
|
||||||
:ui="{
|
|
||||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
|
|
||||||
leadingIcon: 'size-10',
|
|
||||||
label: 'w-full',
|
|
||||||
}"
|
|
||||||
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
|
|
||||||
:label="extension.name"
|
|
||||||
:tooltip="`${extension.name} (${t('disabled')})`"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UDrawer>
|
||||||
|
|
||||||
<!-- Uninstall Confirmation Dialog -->
|
<!-- Uninstall Confirmation Dialog -->
|
||||||
<UiDialogConfirm
|
<UiDialogConfirm
|
||||||
v-model:open="showUninstallDialog"
|
v-model:open="showUninstallDialog"
|
||||||
:title="t('uninstall.confirm.title')"
|
:title="t('uninstall.confirm.title')"
|
||||||
:description="t('uninstall.confirm.description', { name: extensionToUninstall?.name || '' })"
|
:description="
|
||||||
|
t('uninstall.confirm.description', {
|
||||||
|
name: extensionToUninstall?.name || '',
|
||||||
|
})
|
||||||
|
"
|
||||||
:confirm-label="t('uninstall.confirm.button')"
|
:confirm-label="t('uninstall.confirm.button')"
|
||||||
confirm-icon="i-heroicons-trash"
|
confirm-icon="i-heroicons-trash"
|
||||||
@confirm="confirmUninstall"
|
@confirm="confirmUninstall"
|
||||||
@ -237,6 +251,9 @@ const handleDragEnd = () => {
|
|||||||
de:
|
de:
|
||||||
disabled: Deaktiviert
|
disabled: Deaktiviert
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
|
launcher:
|
||||||
|
title: App Launcher
|
||||||
|
description: Wähle eine App zum Öffnen
|
||||||
contextMenu:
|
contextMenu:
|
||||||
open: Öffnen
|
open: Öffnen
|
||||||
uninstall: Deinstallieren
|
uninstall: Deinstallieren
|
||||||
@ -249,6 +266,9 @@ de:
|
|||||||
en:
|
en:
|
||||||
disabled: Disabled
|
disabled: Disabled
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
|
launcher:
|
||||||
|
title: App Launcher
|
||||||
|
description: Select an app to open
|
||||||
contextMenu:
|
contextMenu:
|
||||||
open: Open
|
open: Open
|
||||||
uninstall: Uninstall
|
uninstall: Uninstall
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<UiDialogConfirm
|
<UiDialogConfirm
|
||||||
:confirm-label="t('create')"
|
:confirm-label="t('create')"
|
||||||
@confirm="onCreateAsync"
|
@confirm="onCreateAsync"
|
||||||
|
:description="t('description')"
|
||||||
>
|
>
|
||||||
<UiButton
|
<UiButton
|
||||||
:label="t('vault.create')"
|
:label="t('vault.create')"
|
||||||
@ -55,7 +56,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vaultSchema } from './schema'
|
import { vaultSchema } from './schema'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n({
|
||||||
|
useScope: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
const vault = reactive<{
|
const vault = reactive<{
|
||||||
name: string
|
name: string
|
||||||
@ -118,6 +121,7 @@ de:
|
|||||||
name: HaexVault
|
name: HaexVault
|
||||||
title: Neue {haexvault} erstellen
|
title: Neue {haexvault} erstellen
|
||||||
create: Erstellen
|
create: Erstellen
|
||||||
|
description: Erstelle eine neue Vault für deine Daten
|
||||||
|
|
||||||
en:
|
en:
|
||||||
vault:
|
vault:
|
||||||
@ -127,4 +131,5 @@ en:
|
|||||||
name: HaexVault
|
name: HaexVault
|
||||||
title: Create new {haexvault}
|
title: Create new {haexvault}
|
||||||
create: Create
|
create: Create
|
||||||
|
description: Create a new vault for your data
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
:description="vault.path || path"
|
:description="vault.path || path"
|
||||||
@confirm="onOpenDatabase"
|
@confirm="onOpenDatabase"
|
||||||
>
|
>
|
||||||
<!-- <UiButton
|
<UiButton
|
||||||
:label="t('vault.open')"
|
:label="t('vault.open')"
|
||||||
:ui="{
|
:ui="{
|
||||||
base: 'px-3 py-2',
|
base: 'px-3 py-2',
|
||||||
@ -14,8 +14,7 @@
|
|||||||
size="xl"
|
size="xl"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
block
|
block
|
||||||
@click.stop="onLoadDatabase"
|
/>
|
||||||
/> -->
|
|
||||||
|
|
||||||
<template #title>
|
<template #title>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
@ -59,7 +58,9 @@ const props = defineProps<{
|
|||||||
path?: string
|
path?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n({
|
||||||
|
useScope: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
const vault = reactive<{
|
const vault = reactive<{
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
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"
|
ref="windowEl"
|
||||||
:style="windowStyle"
|
:style="windowStyle"
|
||||||
:class="[
|
:class="[
|
||||||
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden isolate',
|
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
|
||||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
'transition-all ease-out duration-600',
|
||||||
'flex flex-col @container',
|
'flex flex-col @container',
|
||||||
{ 'select-none': isResizingOrDragging },
|
{ 'select-none': isResizingOrDragging },
|
||||||
isActive ? 'z-20' : 'z-10',
|
isActive ? 'z-20' : 'z-10',
|
||||||
|
// Border colors based on warning level
|
||||||
|
warningLevel === 'warning'
|
||||||
|
? 'border-2 border-warning-500'
|
||||||
|
: warningLevel === 'danger'
|
||||||
|
? 'border-2 border-danger-500'
|
||||||
|
: 'border border-gray-200 dark:border-gray-700',
|
||||||
]"
|
]"
|
||||||
@mousedown="handleActivate"
|
@mousedown="handleActivate"
|
||||||
>
|
>
|
||||||
@ -38,37 +44,21 @@
|
|||||||
|
|
||||||
<!-- Right: Window Controls -->
|
<!-- Right: Window Controls -->
|
||||||
<div class="flex items-center gap-1 justify-end">
|
<div class="flex items-center gap-1 justify-end">
|
||||||
<button
|
<HaexWindowButton
|
||||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
variant="minimize"
|
||||||
@click.stop="handleMinimize"
|
@click.stop="handleMinimize"
|
||||||
>
|
/>
|
||||||
<UIcon
|
|
||||||
name="i-heroicons-minus"
|
<HaexWindowButton
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
:is-maximized
|
||||||
/>
|
variant="maximize"
|
||||||
</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"
|
|
||||||
@click.stop="handleMaximize"
|
@click.stop="handleMaximize"
|
||||||
>
|
/>
|
||||||
<UIcon
|
|
||||||
:name="
|
<HaexWindowButton
|
||||||
isMaximized
|
variant="close"
|
||||||
? '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"
|
|
||||||
@click.stop="handleClose"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -102,6 +92,7 @@ const props = defineProps<{
|
|||||||
sourceHeight?: number
|
sourceHeight?: number
|
||||||
isOpening?: boolean
|
isOpening?: boolean
|
||||||
isClosing?: boolean
|
isClosing?: boolean
|
||||||
|
warningLevel?: 'warning' | 'danger' // Warning indicator (e.g., dev extension, dangerous permissions)
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -331,13 +322,33 @@ const handleMaximize = () => {
|
|||||||
const bounds = getViewportBounds()
|
const bounds = getViewportBounds()
|
||||||
|
|
||||||
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
||||||
|
// Get safe-area-insets from CSS variables for debug
|
||||||
|
const safeAreaTop = parseFloat(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
'--safe-area-inset-top',
|
||||||
|
) || '0',
|
||||||
|
)
|
||||||
|
const safeAreaBottom = parseFloat(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
'--safe-area-inset-bottom',
|
||||||
|
) || '0',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Desktop container uses 'absolute inset-0' which stretches over full viewport
|
||||||
|
// bounds.height = full viewport height (includes header area + safe-areas)
|
||||||
|
// We need to calculate available space properly
|
||||||
|
|
||||||
|
// Get header height from UI store (measured reactively in layout)
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
const headerHeight = uiStore.headerHeight
|
||||||
|
|
||||||
x.value = 0
|
x.value = 0
|
||||||
y.value = 0
|
y.value = 0 // Start below header and status bar
|
||||||
width.value = bounds.width
|
width.value = bounds.width
|
||||||
height.value = bounds.height
|
// Height: viewport - header - both safe-areas
|
||||||
|
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||||
isMaximized.value = true
|
isMaximized.value = true
|
||||||
}
|
}
|
||||||
console.log('handleMaximize', preMaximizeState, bounds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,90 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<UModal
|
<UDrawer
|
||||||
v-model:open="localShowWindowOverview"
|
v-model:open="localShowWindowOverview"
|
||||||
|
direction="bottom"
|
||||||
:title="t('modal.title')"
|
:title="t('modal.title')"
|
||||||
:description="t('modal.description')"
|
:description="t('modal.description')"
|
||||||
fullscreen
|
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-col h-full">
|
<div class="h-full overflow-y-auto p-6 justify-center flex">
|
||||||
<!-- Header -->
|
<!-- Window Thumbnails Flex Layout -->
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-end border-b p-2 border-gray-200 dark:border-gray-700"
|
v-if="windows.length > 0"
|
||||||
|
class="flex flex-wrap gap-6 justify-center-safe items-start"
|
||||||
>
|
>
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-x-mark"
|
|
||||||
color="error"
|
|
||||||
variant="soft"
|
|
||||||
@click="localShowWindowOverview = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scrollable Content -->
|
|
||||||
<div class="flex-1 overflow-y-auto p-6 justify-center flex">
|
|
||||||
<!-- Window Thumbnails Flex Layout -->
|
|
||||||
<div
|
<div
|
||||||
v-if="windows.length > 0"
|
v-for="window in windows"
|
||||||
class="flex flex-wrap gap-6 justify-center-safe items-start"
|
:key="window.id"
|
||||||
|
class="relative group cursor-pointer"
|
||||||
>
|
>
|
||||||
<div
|
<!-- Window Title Bar -->
|
||||||
v-for="window in windows"
|
<div class="flex items-center gap-3 mb-2 px-2">
|
||||||
:key="window.id"
|
<UIcon
|
||||||
class="relative group cursor-pointer"
|
v-if="window.icon"
|
||||||
>
|
:name="window.icon"
|
||||||
<!-- Window Title Bar -->
|
class="size-5 shrink-0"
|
||||||
<div class="flex items-center gap-3 mb-2 px-2">
|
/>
|
||||||
<UIcon
|
<div class="flex-1 min-w-0">
|
||||||
v-if="window.icon"
|
<p class="font-semibold text-sm truncate">
|
||||||
:name="window.icon"
|
{{ window.title }}
|
||||||
class="size-5 shrink-0"
|
</p>
|
||||||
/>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="font-semibold text-sm truncate">
|
|
||||||
{{ window.title }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- Minimized Badge -->
|
|
||||||
<UBadge
|
|
||||||
v-if="window.isMinimized"
|
|
||||||
color="info"
|
|
||||||
size="xs"
|
|
||||||
:title="t('minimized')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Minimized Badge -->
|
||||||
|
<UBadge
|
||||||
|
v-if="window.isMinimized"
|
||||||
|
color="info"
|
||||||
|
size="xs"
|
||||||
|
:title="t('minimized')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Scaled Window Preview Container / Teleport Target -->
|
<!-- Scaled Window Preview Container / Teleport Target -->
|
||||||
|
<div
|
||||||
|
:id="`window-preview-${window.id}`"
|
||||||
|
class="relative bg-gray-100 dark:bg-gray-900 rounded-xl overflow-hidden border-2 border-gray-200 dark:border-gray-700 group-hover:border-primary-500 transition-all shadow-lg"
|
||||||
|
:style="getCardStyle(window)"
|
||||||
|
@click="handleRestoreAndActivateWindow(window.id)"
|
||||||
|
>
|
||||||
|
<!-- Hover Overlay -->
|
||||||
<div
|
<div
|
||||||
:id="`window-preview-${window.id}`"
|
class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
|
||||||
class="relative bg-gray-100 dark:bg-gray-900 rounded-xl overflow-hidden border-2 border-gray-200 dark:border-gray-700 group-hover:border-primary-500 transition-all shadow-lg"
|
/>
|
||||||
:style="getCardStyle(window)"
|
|
||||||
@click="handleRestoreAndActivateWindow(window.id)"
|
|
||||||
>
|
|
||||||
<!-- Hover Overlay -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
|
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
name="i-heroicons-window"
|
name="i-heroicons-window"
|
||||||
class="size-16 mb-4 shrink-0"
|
class="size-16 mb-4 shrink-0"
|
||||||
/>
|
/>
|
||||||
<p class="text-lg font-medium">No windows open</p>
|
<p class="text-lg font-medium">No windows open</p>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
Open an extension or system window to see it here
|
Open an extension or system window to see it here
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
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
|
<UButton
|
||||||
class="pointer-events-auto"
|
class="pointer-events-auto"
|
||||||
v-bind="{
|
v-bind="{
|
||||||
...{ size: isSmallScreen ? 'lg' : 'md' },
|
|
||||||
...buttonProps,
|
...buttonProps,
|
||||||
...$attrs,
|
...$attrs,
|
||||||
}"
|
}"
|
||||||
@click="(e) => $emit('click', e)"
|
@click="$emit('click', $event)"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(_, slotName) in $slots"
|
v-for="(_, slotName) in $slots"
|
||||||
|
|||||||
@ -368,7 +368,8 @@ async function handleDatabaseMethodAsync(
|
|||||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -379,14 +380,15 @@ async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'haextension.db.execute': {
|
case 'haextension.db.execute': {
|
||||||
await invoke<string[]>('extension_sql_execute', {
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows,
|
||||||
rowsAffected: 1,
|
rowsAffected: 1,
|
||||||
lastInsertId: undefined,
|
lastInsertId: undefined,
|
||||||
}
|
}
|
||||||
@ -400,7 +402,8 @@ async function handleDatabaseMethodAsync(
|
|||||||
await invoke('extension_sql_execute', {
|
await invoke('extension_sql_execute', {
|
||||||
sql: stmt,
|
sql: stmt,
|
||||||
params: [],
|
params: [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||||
import tableNames from '../tableNames.json'
|
import tableNames from '~/database/tableNames.json'
|
||||||
|
|
||||||
export const haexCrdtLogs = sqliteTable(
|
export const haexCrdtLogs = sqliteTable(
|
||||||
tableNames.haex.crdt.logs.name,
|
tableNames.haex.crdt.logs.name,
|
||||||
{
|
{
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
haexTimestamp: text(tableNames.haex.crdt.logs.columns.haexTimestamp),
|
haexTimestamp: text(tableNames.haex.crdt.logs.columns.haexTimestamp),
|
||||||
tableName: text(tableNames.haex.crdt.logs.columns.tableName),
|
tableName: text(tableNames.haex.crdt.logs.columns.tableName),
|
||||||
rowPks: text(tableNames.haex.crdt.logs.columns.rowPks, { mode: 'json' }),
|
rowPks: text(tableNames.haex.crdt.logs.columns.rowPks, { mode: 'json' }),
|
||||||
@ -33,8 +33,8 @@ export const haexCrdtSnapshots = sqliteTable(
|
|||||||
tableNames.haex.crdt.snapshots.name,
|
tableNames.haex.crdt.snapshots.name,
|
||||||
{
|
{
|
||||||
snapshotId: text(tableNames.haex.crdt.snapshots.columns.snapshotId)
|
snapshotId: text(tableNames.haex.crdt.snapshots.columns.snapshotId)
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
created: text(),
|
created: text(),
|
||||||
epochHlc: text(tableNames.haex.crdt.snapshots.columns.epochHlc),
|
epochHlc: text(tableNames.haex.crdt.snapshots.columns.epochHlc),
|
||||||
locationUrl: text(tableNames.haex.crdt.snapshots.columns.locationUrl),
|
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, {
|
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs.name, {
|
||||||
key: text()
|
key: text().primaryKey(),
|
||||||
.primaryKey()
|
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
|
||||||
value: text(),
|
value: text(),
|
||||||
})
|
})
|
||||||
@ -8,31 +8,32 @@ import {
|
|||||||
type AnySQLiteColumn,
|
type AnySQLiteColumn,
|
||||||
type SQLiteColumnBuilderBase,
|
type SQLiteColumnBuilderBase,
|
||||||
} from 'drizzle-orm/sqlite-core'
|
} 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)
|
// Helper function to add common CRDT columns ( haexTimestamp)
|
||||||
export const withCrdtColumns = <
|
export const withCrdtColumns = <
|
||||||
T extends Record<string, SQLiteColumnBuilderBase>,
|
T extends Record<string, SQLiteColumnBuilderBase>,
|
||||||
>(
|
>(
|
||||||
columns: T,
|
columns: T,
|
||||||
columnNames: { haexTimestamp: string },
|
|
||||||
) => ({
|
) => ({
|
||||||
...columns,
|
...columns,
|
||||||
haexTimestamp: text(columnNames.haexTimestamp),
|
haexTimestamp: text(crdtColumnNames.haexTimestamp),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const haexSettings = sqliteTable(
|
export const haexSettings = sqliteTable(
|
||||||
tableNames.haex.settings.name,
|
tableNames.haex.settings.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
key: text(),
|
key: text(),
|
||||||
type: text(),
|
type: text(),
|
||||||
value: text(),
|
value: text(),
|
||||||
|
}),
|
||||||
haexTimestamp: text(tableNames.haex.settings.columns.haexTimestamp),
|
|
||||||
},
|
|
||||||
(table) => [unique().on(table.key, table.type, table.value)],
|
(table) => [unique().on(table.key, table.type, table.value)],
|
||||||
)
|
)
|
||||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||||
@ -40,22 +41,22 @@ export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
|||||||
|
|
||||||
export const haexExtensions = sqliteTable(
|
export const haexExtensions = sqliteTable(
|
||||||
tableNames.haex.extensions.name,
|
tableNames.haex.extensions.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
public_key: text().notNull(),
|
public_key: text().notNull(),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
version: text().notNull(),
|
version: text().notNull(),
|
||||||
author: text(),
|
author: text(),
|
||||||
description: text(),
|
description: text(),
|
||||||
entry: text().notNull().default('index.html'),
|
entry: text().default('index.html'),
|
||||||
homepage: text(),
|
homepage: text(),
|
||||||
enabled: integer({ mode: 'boolean' }).default(true),
|
enabled: integer({ mode: 'boolean' }).default(true),
|
||||||
icon: text(),
|
icon: text(),
|
||||||
signature: text().notNull(),
|
signature: text().notNull(),
|
||||||
haexTimestamp: text(tableNames.haex.extensions.columns.haexTimestamp),
|
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||||
},
|
}),
|
||||||
(table) => [
|
(table) => [
|
||||||
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
||||||
unique().on(table.public_key, table.name),
|
unique().on(table.public_key, table.name),
|
||||||
@ -66,10 +67,10 @@ export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
|||||||
|
|
||||||
export const haexExtensionPermissions = sqliteTable(
|
export const haexExtensionPermissions = sqliteTable(
|
||||||
tableNames.haex.extension_permissions.name,
|
tableNames.haex.extension_permissions.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
extensionId: text(tableNames.haex.extension_permissions.columns.extensionId)
|
extensionId: text(tableNames.haex.extension_permissions.columns.extensionId)
|
||||||
.notNull()
|
.notNull()
|
||||||
.references((): AnySQLiteColumn => haexExtensions.id, {
|
.references((): AnySQLiteColumn => haexExtensions.id, {
|
||||||
@ -88,10 +89,7 @@ export const haexExtensionPermissions = sqliteTable(
|
|||||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||||
() => new Date(),
|
() => new Date(),
|
||||||
),
|
),
|
||||||
haexTimestamp: text(
|
}),
|
||||||
tableNames.haex.extension_permissions.columns.haexTimestamp,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
(table) => [
|
(table) => [
|
||||||
unique().on(
|
unique().on(
|
||||||
table.extensionId,
|
table.extensionId,
|
||||||
@ -108,8 +106,10 @@ export type SelecthaexExtensionPermissions =
|
|||||||
|
|
||||||
export const haexNotifications = sqliteTable(
|
export const haexNotifications = sqliteTable(
|
||||||
tableNames.haex.notifications.name,
|
tableNames.haex.notifications.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text().primaryKey(),
|
id: text()
|
||||||
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
|
.primaryKey(),
|
||||||
alt: text(),
|
alt: text(),
|
||||||
date: text(),
|
date: text(),
|
||||||
icon: text(),
|
icon: text(),
|
||||||
@ -121,26 +121,23 @@ export const haexNotifications = sqliteTable(
|
|||||||
type: text({
|
type: text({
|
||||||
enum: ['error', 'success', 'warning', 'info', 'log'],
|
enum: ['error', 'success', 'warning', 'info', 'log'],
|
||||||
}).notNull(),
|
}).notNull(),
|
||||||
haexTimestamp: text(tableNames.haex.notifications.columns.haexTimestamp),
|
}),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
||||||
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
||||||
|
|
||||||
export const haexWorkspaces = sqliteTable(
|
export const haexWorkspaces = sqliteTable(
|
||||||
tableNames.haex.workspaces.name,
|
tableNames.haex.workspaces.name,
|
||||||
withCrdtColumns(
|
withCrdtColumns({
|
||||||
{
|
id: text(tableNames.haex.workspaces.columns.id)
|
||||||
id: text(tableNames.haex.workspaces.columns.id)
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.primaryKey()
|
.primaryKey(),
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
deviceId: text(tableNames.haex.workspaces.columns.deviceId).notNull(),
|
||||||
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
||||||
position: integer(tableNames.haex.workspaces.columns.position)
|
position: integer(tableNames.haex.workspaces.columns.position)
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
},
|
}),
|
||||||
tableNames.haex.workspaces.columns,
|
|
||||||
),
|
|
||||||
(table) => [unique().on(table.position)],
|
(table) => [unique().on(table.position)],
|
||||||
)
|
)
|
||||||
export type InsertHaexWorkspaces = typeof haexWorkspaces.$inferInsert
|
export type InsertHaexWorkspaces = typeof haexWorkspaces.$inferInsert
|
||||||
@ -148,33 +145,31 @@ export type SelectHaexWorkspaces = typeof haexWorkspaces.$inferSelect
|
|||||||
|
|
||||||
export const haexDesktopItems = sqliteTable(
|
export const haexDesktopItems = sqliteTable(
|
||||||
tableNames.haex.desktop_items.name,
|
tableNames.haex.desktop_items.name,
|
||||||
withCrdtColumns(
|
withCrdtColumns({
|
||||||
{
|
id: text(tableNames.haex.desktop_items.columns.id)
|
||||||
id: text(tableNames.haex.desktop_items.columns.id)
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.primaryKey()
|
.primaryKey(),
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
||||||
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
.notNull()
|
||||||
.notNull()
|
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
enum: ['system', 'extension', 'file', 'folder'],
|
||||||
enum: ['system', 'extension', 'file', 'folder'],
|
}).notNull(),
|
||||||
}).notNull(),
|
// Für Extensions (wenn itemType = 'extension')
|
||||||
// Für Extensions (wenn itemType = 'extension')
|
extensionId: text(
|
||||||
extensionId: text(tableNames.haex.desktop_items.columns.extensionId)
|
tableNames.haex.desktop_items.columns.extensionId,
|
||||||
.references((): AnySQLiteColumn => haexExtensions.id, {
|
).references((): AnySQLiteColumn => haexExtensions.id, {
|
||||||
onDelete: 'cascade',
|
onDelete: 'cascade',
|
||||||
}),
|
}),
|
||||||
// Für System Windows (wenn itemType = 'system')
|
// Für System Windows (wenn itemType = 'system')
|
||||||
systemWindowId: text(tableNames.haex.desktop_items.columns.systemWindowId),
|
systemWindowId: text(tableNames.haex.desktop_items.columns.systemWindowId),
|
||||||
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
positionY: integer(tableNames.haex.desktop_items.columns.positionY)
|
positionY: integer(tableNames.haex.desktop_items.columns.positionY)
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
},
|
}),
|
||||||
tableNames.haex.desktop_items.columns,
|
|
||||||
),
|
|
||||||
(table) => [
|
(table) => [
|
||||||
check(
|
check(
|
||||||
'item_reference',
|
'item_reference',
|
||||||
@ -67,6 +67,7 @@
|
|||||||
"name": "haex_workspaces",
|
"name": "haex_workspaces",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": "id",
|
"id": "id",
|
||||||
|
"deviceId": "device_id",
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"position": "position",
|
"position": "position",
|
||||||
"createdAt": "created_at",
|
"createdAt": "created_at",
|
||||||
@ -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>
|
<template>
|
||||||
<div class="bg-default isolate w-dvw h-dvh flex flex-col">
|
<div class="w-full h-dvh flex flex-col">
|
||||||
<slot />
|
<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>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
|
|||||||
@ -1,113 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="items-center justify-center flex w-full h-full relative">
|
<div class="h-full">
|
||||||
<div class="absolute top-8 right-8 sm:top-4 sm:right-4">
|
<NuxtLayout>
|
||||||
<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
|
<div
|
||||||
v-show="lastVaults.length"
|
class="flex flex-col justify-center items-center gap-5 mx-auto h-full overflow-scroll"
|
||||||
class="w-full"
|
|
||||||
>
|
>
|
||||||
<div class="font-thin text-sm justify-start px-2 pb-1">
|
<UiLogoHaexhub class="bg-primary p-3 size-16 rounded-full shrink-0" />
|
||||||
{{ t('lastUsed') }}
|
<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>
|
||||||
|
|
||||||
<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
|
<div
|
||||||
v-for="vault in lastVaults"
|
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border overflow-scroll"
|
||||||
:key="vault.name"
|
|
||||||
class="flex items-center justify-between group overflow-x-scroll"
|
|
||||||
>
|
>
|
||||||
<UButton
|
<div
|
||||||
variant="ghost"
|
v-for="vault in lastVaults"
|
||||||
color="neutral"
|
:key="vault.name"
|
||||||
class="flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full px-3"
|
class="flex items-center justify-between group overflow-x-scroll"
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
passwordPromptOpen = true
|
|
||||||
selectedVault = vault
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<span class="block">
|
<UiButtonContext
|
||||||
{{ vault.name }}
|
variant="ghost"
|
||||||
</span>
|
color="neutral"
|
||||||
</UButton>
|
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
|
<UButton
|
||||||
color="error"
|
variant="link"
|
||||||
square
|
@click="openUrl('https://itemis.com')"
|
||||||
class="absolute right-2 hidden group-hover:flex min-w-6"
|
|
||||||
>
|
>
|
||||||
<Icon
|
<UiLogoItemis class="text-[#00457C]" />
|
||||||
name="mdi:trash-can-outline"
|
|
||||||
@click="prepareRemoveVault(vault.name)"
|
|
||||||
/>
|
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
<UiDialogConfirm
|
||||||
<h4>{{ t('sponsors') }}</h4>
|
v-model:open="showRemoveDialog"
|
||||||
<div>
|
:title="t('remove.title')"
|
||||||
<UButton
|
:description="t('remove.description', { vaultName: vaultToBeRemoved })"
|
||||||
variant="link"
|
@confirm="onConfirmRemoveAsync"
|
||||||
@click="openUrl('https://itemis.com')"
|
/>
|
||||||
>
|
</NuxtLayout>
|
||||||
<UiLogoItemis class="text-[#00457C]" />
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<UiDialogConfirm
|
|
||||||
v-model:open="showRemoveDialog"
|
|
||||||
:title="t('remove.title')"
|
|
||||||
:description="t('remove.description', { vaultName: vaultToBeRemoved })"
|
|
||||||
@confirm="onConfirmRemoveAsync"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import type { Locale } from 'vue-i18n'
|
|
||||||
import type { VaultInfo } from '@bindings/VaultInfo'
|
import type { VaultInfo } from '@bindings/VaultInfo'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
name: 'vaultOpen',
|
name: 'vaultOpen',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t, setLocale } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const passwordPromptOpen = ref(false)
|
const passwordPromptOpen = ref(false)
|
||||||
const selectedVault = ref<VaultInfo>()
|
const selectedVault = ref<VaultInfo>()
|
||||||
|
|
||||||
const showRemoveDialog = ref(false)
|
const showRemoveDialog = ref(false)
|
||||||
const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
|
|
||||||
const { lastVaults } = storeToRefs(useLastVaultStore())
|
const { lastVaults } = storeToRefs(useLastVaultStore())
|
||||||
|
|
||||||
|
const { syncLastVaultsAsync, moveVaultToTrashAsync } = useLastVaultStore()
|
||||||
|
const { syncDeviceIdAsync } = useDeviceStore()
|
||||||
|
|
||||||
const vaultToBeRemoved = ref('')
|
const vaultToBeRemoved = ref('')
|
||||||
const prepareRemoveVault = (vaultName: string) => {
|
const prepareRemoveVault = (vaultName: string) => {
|
||||||
vaultToBeRemoved.value = vaultName
|
vaultToBeRemoved.value = vaultName
|
||||||
@ -117,7 +130,7 @@ const prepareRemoveVault = (vaultName: string) => {
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const onConfirmRemoveAsync = async () => {
|
const onConfirmRemoveAsync = async () => {
|
||||||
try {
|
try {
|
||||||
await removeVaultAsync(vaultToBeRemoved.value)
|
await moveVaultToTrashAsync(vaultToBeRemoved.value)
|
||||||
showRemoveDialog.value = false
|
showRemoveDialog.value = false
|
||||||
await syncLastVaultsAsync()
|
await syncLastVaultsAsync()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -127,17 +140,15 @@ const onConfirmRemoveAsync = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await syncLastVaultsAsync()
|
await syncLastVaultsAsync()
|
||||||
|
await syncDeviceIdAsync()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('ERROR: ', error)
|
console.error('ERROR: ', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSelectLocale = async (locale: Locale) => {
|
|
||||||
await setLocale(locale)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
@ -146,6 +157,7 @@ de:
|
|||||||
lastUsed: 'Zuletzt verwendete Vaults'
|
lastUsed: 'Zuletzt verwendete Vaults'
|
||||||
sponsors: Supported by
|
sponsors: Supported by
|
||||||
remove:
|
remove:
|
||||||
|
button: Löschen
|
||||||
title: Vault löschen
|
title: Vault löschen
|
||||||
description: Möchtest du die Vault {vaultName} wirklich löschen?
|
description: Möchtest du die Vault {vaultName} wirklich löschen?
|
||||||
|
|
||||||
@ -154,6 +166,7 @@ en:
|
|||||||
lastUsed: 'Last used Vaults'
|
lastUsed: 'Last used Vaults'
|
||||||
sponsors: 'Supported by'
|
sponsors: 'Supported by'
|
||||||
remove:
|
remove:
|
||||||
|
button: Delete
|
||||||
title: Delete Vault
|
title: Delete Vault
|
||||||
description: Are you sure you really want to delete {vaultName}?
|
description: Are you sure you really want to delete {vaultName}?
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-auto">
|
<div>
|
||||||
<NuxtLayout name="app">
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
||||||
@ -9,6 +9,7 @@
|
|||||||
v-model:open="showNewDeviceDialog"
|
v-model:open="showNewDeviceDialog"
|
||||||
:confirm-label="t('newDevice.save')"
|
:confirm-label="t('newDevice.save')"
|
||||||
:title="t('newDevice.title')"
|
:title="t('newDevice.title')"
|
||||||
|
:description="t('newDevice.setName')"
|
||||||
confirm-icon="mdi:content-save-outline"
|
confirm-icon="mdi:content-save-outline"
|
||||||
@abort="showNewDeviceDialog = false"
|
@abort="showNewDeviceDialog = false"
|
||||||
@confirm="onSetDeviceNameAsync"
|
@confirm="onSetDeviceNameAsync"
|
||||||
@ -48,7 +49,7 @@ const newDeviceName = ref<string>('unknown')
|
|||||||
const { readNotificationsAsync } = useNotificationStore()
|
const { readNotificationsAsync } = useNotificationStore()
|
||||||
const { isKnownDeviceAsync } = useDeviceStore()
|
const { isKnownDeviceAsync } = useDeviceStore()
|
||||||
const { loadExtensionsAsync } = useExtensionsStore()
|
const { loadExtensionsAsync } = useExtensionsStore()
|
||||||
const { setDeviceIdIfNotExistsAsync, addDeviceNameAsync } = useDeviceStore()
|
const { addDeviceNameAsync } = useDeviceStore()
|
||||||
const { deviceId } = storeToRefs(useDeviceStore())
|
const { deviceId } = storeToRefs(useDeviceStore())
|
||||||
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
||||||
useVaultSettingsStore()
|
useVaultSettingsStore()
|
||||||
@ -56,11 +57,11 @@ const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// Sync settings first before other initialization
|
// Sync settings first before other initialization
|
||||||
|
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
syncLocaleAsync(),
|
syncLocaleAsync(),
|
||||||
syncThemeAsync(),
|
syncThemeAsync(),
|
||||||
syncVaultNameAsync(),
|
syncVaultNameAsync(),
|
||||||
setDeviceIdIfNotExistsAsync(),
|
|
||||||
loadExtensionsAsync(),
|
loadExtensionsAsync(),
|
||||||
readNotificationsAsync(),
|
readNotificationsAsync(),
|
||||||
])
|
])
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex items-center justify-center">
|
<div>
|
||||||
<HaexDesktop />
|
<UDashboardPanel resizable>
|
||||||
|
<HaexDesktop />
|
||||||
|
</UDashboardPanel>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
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,9 +1,9 @@
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
|
import { haexDesktopItems } from '~/database/schemas'
|
||||||
import type {
|
import type {
|
||||||
InsertHaexDesktopItems,
|
InsertHaexDesktopItems,
|
||||||
SelectHaexDesktopItems,
|
SelectHaexDesktopItems,
|
||||||
} from '~~/src-tauri/database/schemas'
|
} from '~/database/schemas'
|
||||||
import de from './de.json'
|
import de from './de.json'
|
||||||
import en from './en.json'
|
import en from './en.json'
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { asc, eq } from 'drizzle-orm'
|
|||||||
import {
|
import {
|
||||||
haexWorkspaces,
|
haexWorkspaces,
|
||||||
type SelectHaexWorkspaces,
|
type SelectHaexWorkspaces,
|
||||||
} from '~~/src-tauri/database/schemas'
|
} from '~/database/schemas'
|
||||||
import type { Swiper } from 'swiper/types'
|
import type { Swiper } from 'swiper/types'
|
||||||
|
|
||||||
export type IWorkspace = SelectHaexWorkspaces
|
export type IWorkspace = SelectHaexWorkspaces
|
||||||
@ -10,6 +10,7 @@ export type IWorkspace = SelectHaexWorkspaces
|
|||||||
export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||||
const vaultStore = useVaultStore()
|
const vaultStore = useVaultStore()
|
||||||
const windowStore = useWindowManagerStore()
|
const windowStore = useWindowManagerStore()
|
||||||
|
const { deviceId } = storeToRefs(useDeviceStore())
|
||||||
|
|
||||||
const { currentVault } = storeToRefs(vaultStore)
|
const { currentVault } = storeToRefs(vaultStore)
|
||||||
|
|
||||||
@ -31,10 +32,16 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!deviceId.value) {
|
||||||
|
console.error('Keine DeviceId vergeben')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const items = await currentVault.value.drizzle
|
const items = await currentVault.value.drizzle
|
||||||
.select()
|
.select()
|
||||||
.from(haexWorkspaces)
|
.from(haexWorkspaces)
|
||||||
|
.where(eq(haexWorkspaces.deviceId, deviceId.value))
|
||||||
.orderBy(asc(haexWorkspaces.position))
|
.orderBy(asc(haexWorkspaces.position))
|
||||||
|
|
||||||
workspaces.value = items
|
workspaces.value = items
|
||||||
@ -58,11 +65,16 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
throw new Error('Kein Vault geöffnet')
|
throw new Error('Kein Vault geöffnet')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!deviceId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newIndex = workspaces.value.length + 1
|
const newIndex = workspaces.value.length + 1
|
||||||
const newWorkspace = {
|
const newWorkspace = {
|
||||||
name: name || `Workspace ${newIndex}`,
|
name: name || `Workspace ${newIndex}`,
|
||||||
position: workspaces.value.length,
|
position: workspaces.value.length,
|
||||||
|
deviceId: deviceId.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await currentVault.value.drizzle
|
const result = await currentVault.value.drizzle
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
currentExtension.value.publicKey,
|
currentExtension.value.publicKey,
|
||||||
currentExtension.value.name,
|
currentExtension.value.name,
|
||||||
currentExtension.value.version,
|
currentExtension.value.version,
|
||||||
'index.html',
|
currentExtension.value.entry ?? 'index.html',
|
||||||
currentExtension.value.devServerUrl ?? undefined,
|
currentExtension.value.devServerUrl ?? undefined,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { breakpointsTailwind } from '@vueuse/core'
|
import { breakpointsTailwind } from '@vueuse/core'
|
||||||
import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler'
|
import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler'
|
||||||
|
|
||||||
import de from './de.json'
|
import de from './de.json'
|
||||||
import en from './en.json'
|
import en from './en.json'
|
||||||
|
|
||||||
@ -10,8 +11,9 @@ export const useUiStore = defineStore('uiStore', () => {
|
|||||||
const isSmallScreen = breakpoints.smaller('sm')
|
const isSmallScreen = breakpoints.smaller('sm')
|
||||||
|
|
||||||
const { $i18n } = useNuxtApp()
|
const { $i18n } = useNuxtApp()
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n({
|
||||||
const { platform } = useDeviceStore()
|
useScope: 'global',
|
||||||
|
})
|
||||||
|
|
||||||
$i18n.setLocaleMessage('de', {
|
$i18n.setLocaleMessage('de', {
|
||||||
ui: de,
|
ui: de,
|
||||||
@ -60,19 +62,23 @@ export const useUiStore = defineStore('uiStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Broadcast theme and locale changes to extensions
|
// Broadcast theme and locale changes to extensions
|
||||||
watch([currentThemeName, locale], () => {
|
watch([currentThemeName, locale], async () => {
|
||||||
|
const deviceStore = useDeviceStore()
|
||||||
|
const platformValue = await deviceStore.platform
|
||||||
broadcastContextToAllExtensions({
|
broadcastContextToAllExtensions({
|
||||||
theme: currentThemeName.value,
|
theme: currentThemeName.value,
|
||||||
locale: locale.value,
|
locale: locale.value,
|
||||||
platform,
|
platform: platformValue,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const viewportHeightWithoutHeader = ref(0)
|
const viewportHeightWithoutHeader = ref(0)
|
||||||
|
const headerHeight = ref(0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableThemes,
|
availableThemes,
|
||||||
viewportHeightWithoutHeader,
|
viewportHeightWithoutHeader,
|
||||||
|
headerHeight,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
currentThemeName,
|
currentThemeName,
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
|
|||||||
@ -4,8 +4,18 @@ import {
|
|||||||
platform as tauriPlatform,
|
platform as tauriPlatform,
|
||||||
} from '@tauri-apps/plugin-os'
|
} from '@tauri-apps/plugin-os'
|
||||||
|
|
||||||
export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
const deviceIdKey = 'deviceId'
|
||||||
const deviceId = ref<string>()
|
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())
|
const platform = computedAsync(() => tauriPlatform())
|
||||||
|
|
||||||
@ -15,7 +25,7 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
|||||||
|
|
||||||
const getDeviceIdAsync = async () => {
|
const getDeviceIdAsync = async () => {
|
||||||
const store = await getStoreAsync()
|
const store = await getStoreAsync()
|
||||||
return store.get<string>('id')
|
return await store.get<string>(deviceIdKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStoreAsync = async () => {
|
const getStoreAsync = async () => {
|
||||||
@ -23,30 +33,19 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
|||||||
public: { haexVault },
|
public: { haexVault },
|
||||||
} = useRuntimeConfig()
|
} = useRuntimeConfig()
|
||||||
|
|
||||||
return await load(haexVault.instanceFileName || 'instance.json')
|
return await load(haexVault.deviceFileName || defaultDeviceFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setDeviceIdAsync = async (id?: string) => {
|
const setDeviceIdAsync = async (id?: string) => {
|
||||||
const store = await getStoreAsync()
|
const store = await getStoreAsync()
|
||||||
const _id = id || crypto.randomUUID()
|
const _id = id || crypto.randomUUID()
|
||||||
await store.set('id', _id)
|
await store.set(deviceIdKey, _id)
|
||||||
deviceId.value = _id
|
|
||||||
return _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 isKnownDeviceAsync = async () => {
|
||||||
const { readDeviceNameAsync } = useVaultSettingsStore()
|
const { readDeviceNameAsync } = useVaultSettingsStore()
|
||||||
const deviceId = await getDeviceIdAsync()
|
return !!(await readDeviceNameAsync(deviceId.value))
|
||||||
return deviceId ? (await readDeviceNameAsync(deviceId)) || false : false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const readDeviceNameAsync = async (id?: string) => {
|
const readDeviceNameAsync = async (id?: string) => {
|
||||||
@ -99,12 +98,13 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
|
|||||||
addDeviceNameAsync,
|
addDeviceNameAsync,
|
||||||
deviceId,
|
deviceId,
|
||||||
deviceName,
|
deviceName,
|
||||||
|
getDeviceIdAsync,
|
||||||
hostname,
|
hostname,
|
||||||
isKnownDeviceAsync,
|
isKnownDeviceAsync,
|
||||||
platform,
|
platform,
|
||||||
readDeviceNameAsync,
|
readDeviceNameAsync,
|
||||||
setDeviceIdAsync,
|
setDeviceIdAsync,
|
||||||
setDeviceIdIfNotExistsAsync,
|
syncDeviceIdAsync,
|
||||||
updateDeviceNameAsync,
|
updateDeviceNameAsync,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { drizzle } from 'drizzle-orm/sqlite-proxy'
|
import { drizzle } from 'drizzle-orm/sqlite-proxy'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { schema } from '@/../src-tauri/database/index'
|
import { schema } from '~/database'
|
||||||
import type {
|
import type {
|
||||||
AsyncRemoteCallback,
|
AsyncRemoteCallback,
|
||||||
SqliteRemoteDatabase,
|
SqliteRemoteDatabase,
|
||||||
@ -21,11 +21,12 @@ export const useVaultStore = defineStore('vaultStore', () => {
|
|||||||
public: { haexVault },
|
public: { haexVault },
|
||||||
} = useRuntimeConfig()
|
} = useRuntimeConfig()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const currentVaultId = computed<string | undefined>({
|
const currentVaultId = computed<string | undefined>({
|
||||||
get: () =>
|
get: () =>
|
||||||
getSingleRouteParam(useRouter().currentRoute.value.params.vaultId),
|
getSingleRouteParam(router.currentRoute.value.params.vaultId),
|
||||||
set: (newVaultId) => {
|
set: (newVaultId) => {
|
||||||
useRouter().currentRoute.value.params.vaultId = newVaultId ?? ''
|
router.currentRoute.value.params.vaultId = newVaultId ?? ''
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -22,9 +22,14 @@ export const useLastVaultStore = defineStore('lastVaultStore', () => {
|
|||||||
return await invoke('delete_vault', { vaultName })
|
return await invoke('delete_vault', { vaultName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moveVaultToTrashAsync = async (vaultName: string) => {
|
||||||
|
return await invoke('move_vault_to_trash', { vaultName })
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
syncLastVaultsAsync,
|
syncLastVaultsAsync,
|
||||||
lastVaults,
|
lastVaults,
|
||||||
removeVaultAsync,
|
removeVaultAsync,
|
||||||
|
moveVaultToTrashAsync,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { and, eq, or, type SQLWrapper } from 'drizzle-orm'
|
|||||||
import {
|
import {
|
||||||
haexNotifications,
|
haexNotifications,
|
||||||
type InsertHaexNotifications,
|
type InsertHaexNotifications,
|
||||||
} from '~~/src-tauri/database/schemas/haex'
|
} from '~/database/schemas/haex'
|
||||||
import {
|
import {
|
||||||
isPermissionGranted,
|
isPermissionGranted,
|
||||||
requestPermission,
|
requestPermission,
|
||||||
@ -31,7 +31,12 @@ export const useNotificationStore = defineStore('notificationStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkNotificationAsync = async () => {
|
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
|
return isNotificationAllowed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { z } from 'zod'
|
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'
|
import type { Locale } from 'vue-i18n'
|
||||||
|
|
||||||
export enum VaultSettingsTypeEnum {
|
export enum VaultSettingsTypeEnum {
|
||||||
@ -118,9 +118,11 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
|
|||||||
.where(eq(schema.haexSettings.key, 'vaultName'))
|
.where(eq(schema.haexSettings.key, 'vaultName'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const readDeviceNameAsync = async (id: string) => {
|
const readDeviceNameAsync = async (id?: string) => {
|
||||||
const { currentVault } = useVaultStore()
|
const { currentVault } = useVaultStore()
|
||||||
|
|
||||||
|
if (!id) return undefined
|
||||||
|
|
||||||
const deviceName =
|
const deviceName =
|
||||||
await currentVault?.drizzle?.query.haexSettings.findFirst({
|
await currentVault?.drizzle?.query.haexSettings.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|||||||
Reference in New Issue
Block a user