init commit

This commit is contained in:
Martin Drechsel
2025-04-02 18:54:55 +02:00
commit 2c5ec6b281
126 changed files with 21323 additions and 0 deletions

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# Tauri + Vue + TypeScript
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

10
drizzle.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src-tauri/database/schemas/**.ts',
out: './src-tauri/database/migrations',
dialect: 'sqlite',
dbCredentials: {
url: './src-tauri/database/vault.db',
},
});

80
nuxt.config.ts Normal file
View File

@ -0,0 +1,80 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
modules: [
'nuxt-zod-i18n',
'@nuxtjs/i18n',
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@vueuse/nuxt',
'@nuxt/icon',
'nuxt-snackbar',
],
imports: {
dirs: ['composables/**', 'stores/**', 'components/**', 'pages/**'],
},
i18n: {
strategy: 'prefix_and_default',
defaultLocale: 'de',
vueI18n: '../src/i18n/i18n.config.ts',
locales: [
{ code: 'de', language: 'de-DE', isCatchallLocale: true },
{ code: 'en', language: 'en-EN' },
],
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root', // recommended
},
types: 'composition',
bundle: {
optimizeTranslationDirective: false,
},
},
/* zodI18n: {
localeCodesMapping: {
'en-GB': 'en',
'de-DE': 'de',
},
}, */
runtimeConfig: {
public: {
haexVault: {
lastVaultFileName: 'lastVaults.json',
//defaultDatabase: 'src/database/default.db',
},
},
},
devtools: { enabled: true },
srcDir: './src',
// Enable SSG
ssr: false,
// Enables the development server to be discoverable by other devices when running on iOS physical devices
devServer: { host: process.env.TAURI_DEV_HOST || 'localhost' },
vite: {
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables
// Additional environment variables can be found at
// https://v2.tauri.app/reference/environment-variables/
envPrefix: ['VITE_', 'TAURI_'],
server: {
// Tauri requires a consistent port
strictPort: true,
},
/* plugins: [wasm(), topLevelAwait()],
worker: {
format: 'es',
plugins: () => [wasm(), topLevelAwait()],
}, */
},
});

59
package.json Normal file
View File

@ -0,0 +1,59 @@
{
"name": "haex-hub",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"tauri": "tauri",
"drizzle:generate": "drizzle-kit generate",
"drizzle:migrate": "drizzle-kit migrate"
},
"dependencies": {
"@libsql/client": "^0.15.1",
"@nuxt/icon": "1.11.0",
"@nuxtjs/i18n": "^9.4.0",
"@pinia/nuxt": "^0.10.1",
"@tauri-apps/api": "^2.4.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-http": "~2.4.2",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-sql": "~2.2.0",
"@tauri-apps/plugin-store": "^2.2.0",
"@vueuse/core": "^13.0.0",
"@vueuse/nuxt": "^13.0.0",
"drizzle-orm": "^0.41.0",
"nuxt": "^3.16.1",
"nuxt-snackbar": "1.3.0",
"nuxt-zod-i18n": "^1.11.5",
"vue": "^3.5.13",
"zod": "^3.24.2"
},
"devDependencies": {
"@egoist/tailwindcss-icons": "^1.9.0",
"@iconify/json": "^2.2.321",
"@iconify/tailwind": "^1.2.0",
"@nuxtjs/tailwindcss": "^6.13.2",
"@tauri-apps/cli": "^2.4.0",
"@vitejs/plugin-vue": "^5.2.3",
"drizzle-kit": "^0.30.6",
"flyonui": "^1.3.1",
"typescript": "~5.6.3",
"vite": "^6.2.3",
"vue-tsc": "^2.2.8"
},
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
"pnpm": {
"ignoredBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"vue-demi"
]
}
}

9228
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
public/tauri.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

2
src-tauri/.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL=sqlite:database/vault.db
SQLX_OFFLINE=true

7
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View File

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, extension_id, resource, operation, path \n FROM haex_extensions_permissions \n WHERE extension_id = ? AND resource = ? AND operation = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "extension_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "resource",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "operation",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "path",
"ordinal": 4,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
true,
true,
true,
true
]
},
"hash": "a73e92ff12dca9b046a6440b9a68b002662b594f7f569ee71de11e00c23ca625"
}

5862
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

37
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "haex-hub"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "haex_hub_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
rusqlite = { version = "0.34.0", features = [
"load_extension",
"bundled-sqlcipher",
] }
#libsqlite3-sys = { version = "0.32", features = ["bundled-sqlcipher"] }
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
#libsqlite3-sys = { version = "0.28.0", features = ["bundled-sqlcipher"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlparser = { version = "0.55.0", features = [] }
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-opener = "2"
tauri-plugin-os = "2"
tauri-plugin-store = "2"
tauri-plugin-http = "2"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,28 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"fs:allow-read-file",
"fs:allow-resource-read-recursive",
"fs:default",
"http:allow-fetch-send",
"http:allow-fetch",
"http:default",
"opener:allow-open-url",
"opener:default",
"os:default",
"store:default",
"core:window:allow-create",
"core:window:default",
"core:window:allow-get-all-windows",
"core:window:allow-show",
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window",
"core:webview:default",
"core:webview:allow-webview-show"
]
}

View File

@ -0,0 +1,26 @@
CREATE TABLE `haex_extensions` (
`id` text PRIMARY KEY NOT NULL,
`author` text,
`enabled` integer,
`name` text,
`url` text,
`version` text
);
--> statement-breakpoint
CREATE TABLE `haex_extensions_permissions` (
`id` text PRIMARY KEY NOT NULL,
`extension_id` text,
`resource` text,
`operation` text,
`path` text,
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `haex_extensions_permissions_extension_id_resource_operation_path_unique` ON `haex_extensions_permissions` (`extension_id`,`resource`,`operation`,`path`);--> statement-breakpoint
CREATE TABLE `haex_settings` (
`id` text PRIMARY KEY NOT NULL,
`key` text,
`value_text` text,
`value_json` text,
`value_number` numeric
);

View File

@ -0,0 +1,185 @@
{
"version": "6",
"dialect": "sqlite",
"id": "fc5a7c9d-4846-4120-a762-cc2ea00504b9",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"haex_extensions": {
"name": "haex_extensions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"version": {
"name": "version",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"haex_extensions_permissions": {
"name": "haex_extensions_permissions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"extension_id": {
"name": "extension_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"resource": {
"name": "resource",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"operation": {
"name": "operation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
"columns": [
"extension_id",
"resource",
"operation",
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
"tableFrom": "haex_extensions_permissions",
"tableTo": "haex_extensions",
"columnsFrom": [
"extension_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"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
},
"value_text": {
"name": "value_text",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"value_json": {
"name": "value_json",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"value_number": {
"name": "value_number",
"type": "numeric",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1742903332283,
"tag": "0000_zippy_scourge",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,52 @@
import {
integer,
sqliteTable,
text,
type AnySQLiteColumn,
unique,
numeric,
} from 'drizzle-orm/sqlite-core';
export const haexSettings = sqliteTable('haex_settings', {
id: text().primaryKey(),
key: text(),
value_text: text(),
value_json: text({ mode: 'json' }),
value_number: numeric(),
});
export const haexExtensions = sqliteTable('haex_extensions', {
id: text().primaryKey(),
author: text(),
enabled: integer(),
name: text(),
url: text(),
version: text(),
});
export const haexExtensionsPermissions = sqliteTable(
'haex_extensions_permissions',
{
id: text().primaryKey(),
extensionId: text('extension_id').references(
(): AnySQLiteColumn => haexExtensions.id
),
resource: text({ enum: ['fs', 'http', 'database'] }),
operation: text({ enum: ['read', 'write', 'create'] }),
path: text(),
},
(table) => [
unique().on(table.extensionId, table.resource, table.operation, table.path),
]
);
export type InsertHaexSettings = typeof haexSettings.$inferInsert;
export type SelectHaexSettings = typeof haexSettings.$inferSelect;
export type InsertHaexExtensions = typeof haexExtensions.$inferInsert;
export type SelectHaexExtensions = typeof haexExtensions.$inferSelect;
export type InsertHaexExtensionsPermissions =
typeof haexExtensionsPermissions.$inferInsert;
export type SelectHaexExtensionsPermissions =
typeof haexExtensionsPermissions.$inferSelect;

BIN
src-tauri/database/vault.db Normal file

Binary file not shown.

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,285 @@
//mod middleware;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tauri::{webview, AppHandle, LogicalPosition, LogicalSize, Manager, WebviewUrl, Window};
//use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct Tab {
pub id: String,
pub webview_label: String,
pub title: String,
pub url: String,
pub is_loading: bool,
pub is_visible: bool,
}
pub struct BrowserManager {
tabs: Arc<Mutex<HashMap<String, Tab>>>,
active_tab_id: Arc<Mutex<Option<String>>>,
//middleware: Arc<RoutingMiddleware>,
}
impl BrowserManager {
pub fn new() -> Self {
Self {
tabs: Arc::new(Mutex::new(HashMap::new())),
active_tab_id: Arc::new(Mutex::new(None)),
//middleware: Arc::new(RoutingMiddleware::new()),
}
}
/* pub async fn create_window(app: tauri::AppHandle) -> Result<tauri::WebviewWindow, _> {
let webview_window = tauri::WebviewWindowBuilder::new(
&app,
"label",
tauri::WebviewUrl::App("index.html".into()),
)
.build()
.unwrap();
Ok(webview_window);
} */
pub fn create_tab(&self, app: AppHandle, url: &str) {
// Generiere eine eindeutige ID für den Tab
/* let tab_id = Uuid::new_v4().to_string();
let webview_label = format!("webview-{}", tab_id); */
// Überprüfe URL mit Middleware
//let processed_url = self.middleware.process_url(url);
// Hole das Hauptfenster
let main_window = app.get_webview_window("main").unwrap();
// Berechne die Position und Größe für den Webview
// Hier nehmen wir an, dass wir einen Header-Bereich von 100 Pixeln haben
/* let window_size = main_window.inner_size()?;
let header_height = 100.0;
let webview_position = LogicalPosition::new(0.0, header_height);
let webview_size = LogicalSize::new(window_size.width, window_size.height - header_height);
*/
/* let webview = tauri::WebviewWindowBuilder::new(
&app,
"label",
//WebviewUrl::External(processed_url.parse().unwrap()),
WebviewUrl::External(url),
)
.build()
.unwrap() */
/* .on_navigation(move |url| {
// Middleware für Navigation anwenden
self.middleware.process_navigation(url.as_str())
})
.on_web_resource_request(move |request, response| {
// Middleware für HTTP-Anfragen anwenden
self.middleware.process_request(request, response)
}); */
// Erstelle Tab-Objekt
/* let tab = Tab {
id: tab_id.clone(),
webview_label: webview_label.clone(),
title: "Neuer Tab".to_string(),
url: processed_url.to_string(),
is_loading: true,
is_visible: false,
}; */
// Speichere Tab
/* {
let mut tabs = self.tabs.lock().unwrap();
tabs.insert(tab_id.clone(), tab.clone());
} */
// Setze als aktiven Tab
//self.activate_tab(app, &tab_id)?;
// Injiziere die Webview-Bridge
/* let script = include_str!("../assets/webview-bridge.js");
webview.evaluate_script(script)?; */
// Registriere Event-Handler für Titeländerungen
let tab_manager = self.clone();
//let tab_id_clone = tab_id.clone();
/* webview.listen("tauri://title-changed", move |event| {
if let Some(title) = event.payload().and_then(|p| p.as_str()) {
tab_manager.update_tab_title(&tab_id_clone, title);
}
}); */
// Registriere Event-Handler für Ladestatus
let tab_manager = self.clone();
//let tab_id_clone = tab_id.clone();
/* webview.listen("tauri://load-changed", move |event| {
if let Some(status) = event.payload().and_then(|p| p.as_str()) {
let is_loading = status == "loading";
tab_manager.update_tab_loading_status(&tab_id_clone, is_loading);
}
}); */
//Ok()
}
pub fn close_tab(&self, app: &AppHandle, tab_id: &str) -> Result<(), tauri::Error> {
// Hole das Hauptfenster
let main_window = app.get_webview_window("main").unwrap();
// Entferne Tab aus der Verwaltung
let webview_label = {
let mut tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.remove(tab_id) {
tab.webview_label
} else {
return Ok(());
}
};
// Entferne den Webview
//main_window.remove_child(&webview_label)?;
// Aktualisiere aktiven Tab, falls nötig
{
let mut active_tab_id = self.active_tab_id.lock().unwrap();
if active_tab_id.as_ref().map_or(false, |id| id == tab_id) {
// Wähle einen anderen Tab als aktiv
let tabs = self.tabs.lock().unwrap();
*active_tab_id = tabs.keys().next().cloned();
// Aktiviere den neuen Tab, falls vorhanden
if let Some(new_active_id) = active_tab_id.clone() {
drop(active_tab_id); // Mutex freigeben vor dem rekursiven Aufruf
self.activate_tab(app, &new_active_id)?;
}
}
}
Ok(())
}
pub fn activate_tab(&self, app: &AppHandle, tab_id: &str) -> Result<(), tauri::Error> {
// Hole das Hauptfenster
let main_window = app.get_webview_window("main").unwrap();
// Setze Tab als aktiv
{
let mut active_tab_id = self.active_tab_id.lock().unwrap();
*active_tab_id = Some(tab_id.to_string());
}
// Verstecke alle anderen Tabs und zeige den aktiven
let mut tabs = self.tabs.lock().unwrap();
for (id, tab) in tabs.iter_mut() {
if id == tab_id {
// Zeige den aktiven Tab
/* main_window
.get_webview_window(&tab.webview_label)?
.set_visible(true)?; */
tab.is_visible = true;
} else {
// Verstecke alle anderen Tabs
/* main_window
.get_webview_window(&tab.webview_label)?
.set_visible(false)?; */
tab.is_visible = false;
}
}
Ok(())
}
pub fn navigate_to_url(
&self,
app: &AppHandle,
tab_id: &str,
url: &str,
) -> Result<(), tauri::Error> {
// Überprüfe URL mit Middleware
//let processed_url = self.middleware.process_url(url);
// Aktualisiere URL im Tab
{
let mut tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get_mut(tab_id) {
tab.url = url.to_string() //processed_url.to_string();
}
}
// Navigiere zum URL im Webview
let tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get(tab_id) {
let main_window = app.get_webview_window("main").unwrap();
/* let webview = main_window.get_webview_window(&tab.webview_label)?;
webview.navigate(&processed_url)?; */
}
Ok(())
}
pub fn get_all_tabs(&self) -> Vec<Tab> {
let tabs = self.tabs.lock().unwrap();
tabs.values().cloned().collect()
}
pub fn get_active_tab_id(&self) -> Option<String> {
let active_tab_id = self.active_tab_id.lock().unwrap();
active_tab_id.clone()
}
pub fn update_tab_title(&self, tab_id: &str, title: &str) {
let mut tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get_mut(tab_id) {
tab.title = title.to_string();
}
}
pub fn update_tab_loading_status(&self, tab_id: &str, is_loading: bool) {
let mut tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get_mut(tab_id) {
tab.is_loading = is_loading;
}
}
// Weitere Methoden für Browser-Navigation
pub fn go_back(&self, app: &AppHandle, tab_id: &str) -> Result<(), tauri::Error> {
let tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get(tab_id) {
let main_window = app.get_webview_window("main").unwrap();
/* let webview = main_window.get_webview(&tab.webview_label)?;
webview.evaluate_script("window.history.back()")?; */
}
Ok(())
}
pub fn go_forward(&self, app: &AppHandle, tab_id: &str) -> Result<(), tauri::Error> {
let tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get(tab_id) {
let main_window = app.get_webview_window("main").unwrap();
/* let webview = main_window.get_webview(&tab.webview_label)?;
webview.evaluate_script("window.history.forward()")?; */
}
Ok(())
}
pub fn inject_content_script(
&self,
app: &AppHandle,
tab_id: &str,
script: &str,
) -> Result<(), tauri::Error> {
let tabs = self.tabs.lock().unwrap();
if let Some(tab) = tabs.get(tab_id) {
let main_window = app.get_webview_window("main").unwrap();
/* let webview = main_window.get_webview(&tab.webview_label)?;
webview.evaluate_script(script)?; */
}
Ok(())
}
pub fn clone(&self) -> Self {
Self {
tabs: Arc::clone(&self.tabs),
active_tab_id: Arc::clone(&self.active_tab_id),
//middleware: Arc::clone(&self.middleware),
}
}
}

View File

@ -0,0 +1,125 @@
use std::sync::{Arc, Mutex};
use tauri::http::{Request, Response, ResponseBuilder};
pub struct RoutingMiddleware {
extensions: Arc<Mutex<Vec<Box<dyn MiddlewareExtension + Send + Sync>>>>,
}
pub trait MiddlewareExtension: Send + Sync {
fn name(&self) -> &str;
fn process_url(&self, url: &str) -> String;
fn process_navigation(&self, url: &str) -> bool;
fn process_request(&self, request: &Request, response: &mut Response) -> bool;
}
impl RoutingMiddleware {
pub fn new() -> Self {
let mut middleware = Self {
extensions: Arc::new(Mutex::new(Vec::new())),
};
// Registriere Standard-Erweiterungen
//middleware.register_extension(Box::new(AdBlockerExtension::new()));
middleware
}
pub fn register_extension(&mut self, extension: Box<dyn MiddlewareExtension + Send + Sync>) {
let mut extensions = self.extensions.lock().unwrap();
extensions.push(extension);
}
pub fn process_url(&self, url: &str) -> String {
let extensions = self.extensions.lock().unwrap();
let mut processed_url = url.to_string();
for extension in extensions.iter() {
processed_url = extension.process_url(&processed_url);
}
processed_url
}
pub fn process_navigation(&self, url: &str) -> bool {
let extensions = self.extensions.lock().unwrap();
for extension in extensions.iter() {
if !extension.process_navigation(url) {
return false;
}
}
true
}
pub fn process_request(&self, request: &Request, response: &mut Response) -> bool {
let extensions = self.extensions.lock().unwrap();
for extension in extensions.iter() {
if extension.process_request(request, response) {
return true;
}
}
false
}
}
// Beispiel für eine Ad-Blocker-Erweiterung
struct AdBlockerExtension {
block_patterns: Vec<String>,
}
impl AdBlockerExtension {
fn new() -> Self {
Self {
block_patterns: vec![
"ads".to_string(),
"analytics".to_string(),
"tracker".to_string(),
"banner".to_string(),
],
}
}
fn is_blocked_url(&self, url: &str) -> bool {
for pattern in &self.block_patterns {
if url.contains(pattern) {
return true;
}
}
false
}
}
impl MiddlewareExtension for AdBlockerExtension {
fn name(&self) -> &str {
"AdBlocker"
}
fn process_url(&self, url: &str) -> String {
// Für vollständige Navigationen blockieren wir normalerweise nicht die ganze Seite
url.to_string()
}
fn process_navigation(&self, url: &str) -> bool {
// Blockiere nur vollständige Navigationen zu Werbeseiten
let is_ad_site = url.contains("doubleclick.net")
|| url.contains("googleadservices.com")
|| url.contains("ads.example.com");
!is_ad_site
}
fn process_request(&self, request: &Request, response: &mut Response) -> bool {
let url = request.uri().to_string();
if self.is_blocked_url(&url) {
println!("AdBlocker: Blockiere Anfrage: {}", url);
*response = ResponseBuilder::new()
.status(403)
.body("Zugriff verweigert durch AdBlocker".as_bytes().to_vec())
.unwrap();
return true;
}
false
}
}

View File

@ -0,0 +1,188 @@
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, State};
mod manager;
#[derive(Serialize, Deserialize)]
pub struct TabInfo {
id: String,
title: String,
url: String,
is_loading: bool,
is_active: bool,
}
// Einfache Kommandos für die Tab-Verwaltung
#[tauri::command]
pub fn create_tab(app_handle: tauri::AppHandle, tab_id: String, url: String) -> Result<(), String> {
let main_window = app_handle
.get_webview_window("main")
.ok_or("Hauptfenster nicht gefunden")?;
let window_size = main_window.inner_size().map_err(|e| e.to_string())?;
// Erstelle eine neue Webview als eigenständiges Fenster
let webview = tauri::WebviewWindowBuilder::new(
&app_handle,
tab_id.clone(),
tauri::WebviewUrl::External(url.parse::<tauri::Url>().map_err(|e| e.to_string())?),
//tauri::WebviewUrl::External("http://google.de"),
)
.title(format!("Tab: {}", tab_id))
.inner_size(window_size.width as f64, window_size.height as f64 - 50.0)
.position(0.0, 50.0)
.build()
.map_err(|e| e.to_string())?;
// Sende die Tab-ID zurück an das Hauptfenster
/* main_window
.emit("tab-created", tab_id)
.map_err(|e| e.to_string())?; */
Ok(())
}
#[tauri::command]
pub fn show_tab(app_handle: tauri::AppHandle, tab_id: String) -> Result<(), String> {
// Hole alle Webview-Fenster
let windows = app_handle.webview_windows();
// Zeige das ausgewählte Tab und verstecke die anderen
for (id, window) in windows {
if id != "main" {
// Hauptfenster nicht verstecken
if id == tab_id {
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
} else {
window.hide().map_err(|e| e.to_string())?;
}
}
}
Ok(())
}
#[tauri::command]
pub fn close_tab(app_handle: tauri::AppHandle, tab_id: String) -> Result<(), String> {
if let Some(window) = app_handle.get_webview_window(&tab_id) {
window.close().map_err(|e| e.to_string())?;
}
Ok(())
}
/* #[tauri::command]
pub fn create_tab(app: AppHandle, url: String) -> Result<TabInfo, String> {
let browser_manager = app.state::<manager::BrowserManager>();
match browser_manager.create_tab(&app, &url) {
Ok(tab) => {
let active_tab_id = browser_manager.get_active_tab_id();
let is_active = active_tab_id.as_ref().map_or(false, |id| id == &tab.id);
let main = app.get_webview_window("main");
//main.unwrap().
// Sende Event an Frontend
/* app.emit_all(
"tab-created",
TabInfo {
id: tab.id.clone(),
title: tab.title.clone(),
url: tab.url.clone(),
is_loading: tab.is_loading,
is_active,
},
)
.unwrap(); */
Ok(TabInfo {
id: tab.id,
title: tab.title,
url: tab.url,
is_loading: tab.is_loading,
is_active: true,
})
}
Err(e) => Err(format!("Fehler beim Erstellen des Tabs: {}", e)),
}
} */
/* #[tauri::command]
pub fn close_tab(app: AppHandle, tab_id: String) -> Result<(), String> {
let browser_manager = app.state::<manager::BrowserManager>();
match browser_manager.close_tab(&app, &tab_id) {
Ok(_) => {
// Sende Event an Frontend
//app.emit_all("tab-closed", tab_id).unwrap();
Ok(())
}
Err(e) => Err(format!("Fehler beim Schließen des Tabs: {}", e)),
}
} */
#[tauri::command]
pub fn navigate_to_url(app: AppHandle, tab_id: String, url: String) -> Result<(), String> {
let browser_manager = app.state::<manager::BrowserManager>();
match browser_manager.navigate_to_url(&app, &tab_id, &url) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Fehler bei der Navigation: {}", e)),
}
}
#[tauri::command]
pub fn get_current_url(app: AppHandle, tab_id: String) -> Result<String, String> {
let browser_manager = app.state::<manager::BrowserManager>();
let tabs = browser_manager.get_all_tabs();
for tab in tabs {
if tab.id == tab_id {
return Ok(tab.url);
}
}
Err("Tab nicht gefunden".to_string())
}
#[tauri::command]
pub fn go_back(app: AppHandle, tab_id: String) -> Result<(), String> {
let browser_manager = app.state::<manager::BrowserManager>();
match browser_manager.go_back(&app, &tab_id) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Fehler beim Zurückgehen: {}", e)),
}
}
#[tauri::command]
pub fn go_forward(app: AppHandle, tab_id: String) -> Result<(), String> {
let browser_manager = app.state::<manager::BrowserManager>();
match browser_manager.go_forward(&app, &tab_id) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Fehler beim Vorwärtsgehen: {}", e)),
}
}
#[tauri::command]
pub fn block_resource_request(url: String, resource_type: String) -> bool {
// Diese Funktion wird vom Frontend aufgerufen, um zu prüfen, ob eine Ressource blockiert werden soll
// Die eigentliche Logik wird im JavaScript-Erweiterungssystem implementiert
// Hier könnten Sie zusätzliche Rust-seitige Prüfungen durchführen
println!("Prüfe Ressourcenanfrage: {} (Typ: {})", url, resource_type);
// Einfache Prüfung für Beispielzwecke
url.contains("ads") || url.contains("analytics") || url.contains("tracker")
}
#[tauri::command]
pub fn inject_content_script(app: AppHandle, tab_id: String, script: String) -> Result<(), String> {
let browser_manager = app.state::<manager::BrowserManager>();
match browser_manager.inject_content_script(&app, &tab_id, &script) {
Ok(_) => Ok(()),
Err(e) => Err(format!("Fehler beim Injizieren des Scripts: {}", e)),
}
}

View File

@ -0,0 +1,160 @@
// database/core.rs
use crate::database::DbConnection;
use rusqlite::{Connection, OpenFlags};
use serde_json::json;
use std::fs;
use std::path::Path;
use tauri::State;
/// Führt SQL-Schreiboperationen (INSERT, UPDATE, DELETE, CREATE) ohne Berechtigungsprüfung aus
pub async fn execute(
sql: &str,
params: &[String],
state: &State<'_, DbConnection>,
) -> Result<String, String> {
let db = state.0.lock().map_err(|e| format!("Mutex-Fehler: {}", e))?;
let conn = db.as_ref().ok_or("Keine Datenbankverbindung vorhanden")?;
let rows_affected = conn
.execute(sql, rusqlite::params_from_iter(params.iter()))
.map_err(|e| format!("SQL-Ausführungsfehler: {}", e))?;
let last_id = conn.last_insert_rowid();
Ok(serde_json::to_string(&json!({
"rows_affected": rows_affected,
"last_insert_id": last_id
}))
.map_err(|e| format!("JSON-Serialisierungsfehler: {}", e))?)
}
/// Führt SQL-Leseoperationen (SELECT) ohne Berechtigungsprüfung aus
pub async fn select(
sql: &str,
params: &[String],
state: &State<'_, DbConnection>,
) -> Result<Vec<Vec<String>>, String> {
let db = state.0.lock().map_err(|e| format!("Mutex-Fehler: {}", e))?;
let conn = db.as_ref().ok_or("Keine Datenbankverbindung vorhanden")?;
let mut stmt = conn
.prepare(sql)
.map_err(|e| format!("SQL-Vorbereitungsfehler: {}", e))?;
let columns = stmt.column_count();
let mut rows = stmt
.query(rusqlite::params_from_iter(params.iter()))
.map_err(|e| format!("SQL-Abfragefehler: {}", e))?;
let mut result = Vec::new();
while let Some(row) = rows
.next()
.map_err(|e| format!("Zeilenabruffehler: {}", e))?
{
let mut row_data = Vec::new();
for i in 0..columns {
let value: String = row
.get(i)
.map_err(|e| format!("Datentypfehler in Spalte {}: {}", i, e))?;
row_data.push(value);
}
result.push(row_data);
}
Ok(result)
}
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, String> {
let flags = if create {
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
} else {
OpenFlags::SQLITE_OPEN_READ_WRITE
};
let conn = Connection::open_with_flags(path, flags).map_err(|e| e.to_string())?;
conn.pragma_update(None, "key", key)
.map_err(|e| e.to_string())?;
Ok(conn)
}
/// Kopiert eine Datei von einem Pfad zu einem anderen
pub fn copy_file<S: AsRef<Path>, T: AsRef<Path>>(
source_path: S,
target_path: T,
) -> Result<(), String> {
let source = source_path.as_ref();
let target = target_path.as_ref();
// Check if source file exists
if !source.exists() {
return Err(format!("Source file '{}' does not exist", source.display()));
}
// Check if source is a file (not a directory)
if !source.is_file() {
return Err(format!("Source '{}' is not a file", source.display()));
}
// Copy the file and preserve metadata (permissions, timestamps)
fs::copy(source, target)
.map(|_| ())
.map_err(|e| format!("Failed to copy file: {}", e))?;
Ok(())
}
// Hilfsfunktionen für SQL-Parsing
pub fn extract_tables_from_query(query: &sqlparser::ast::Query) -> Vec<String> {
let mut tables = Vec::new();
extract_tables_from_set_expr(&query.body, &mut tables);
tables
}
fn extract_tables_from_set_expr(set_expr: &sqlparser::ast::SetExpr, tables: &mut Vec<String>) {
match set_expr {
sqlparser::ast::SetExpr::Select(select) => {
for from in &select.from {
extract_tables_from_table_with_joins(from, tables);
}
}
sqlparser::ast::SetExpr::Query(query) => {
extract_tables_from_set_expr(&query.body, tables);
}
sqlparser::ast::SetExpr::SetOperation { left, right, .. } => {
extract_tables_from_set_expr(left, tables);
extract_tables_from_set_expr(right, tables);
}
_ => (), // Andere Fälle wie Values oder Insert ignorieren
}
}
fn extract_tables_from_table_with_joins(
table_with_joins: &sqlparser::ast::TableWithJoins,
tables: &mut Vec<String>,
) {
extract_tables_from_table_factor(&table_with_joins.relation, tables);
for join in &table_with_joins.joins {
extract_tables_from_table_factor(&join.relation, tables);
}
}
fn extract_tables_from_table_factor(
table_factor: &sqlparser::ast::TableFactor,
tables: &mut Vec<String>,
) {
match table_factor {
sqlparser::ast::TableFactor::Table { name, .. } => {
tables.push(name.to_string());
}
sqlparser::ast::TableFactor::Derived { subquery, .. } => {
extract_tables_from_set_expr(&subquery.body, tables);
}
sqlparser::ast::TableFactor::NestedJoin {
table_with_joins, ..
} => {
extract_tables_from_table_with_joins(table_with_joins, tables);
}
_ => (), // Andere Fälle wie TableFunction ignorieren
}
}

View File

@ -0,0 +1,122 @@
// database/mod.rs
pub mod core;
use rusqlite::Connection;
use std::path::Path;
use std::sync::Mutex;
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
pub struct DbConnection(pub Mutex<Option<Connection>>);
// Öffentliche Funktionen für direkten Datenbankzugriff
#[tauri::command]
pub async fn sql_select(
sql: String,
params: Vec<String>,
state: State<'_, DbConnection>,
) -> Result<Vec<Vec<String>>, String> {
core::select(&sql, &params, &state).await
}
#[tauri::command]
pub async fn sql_execute(
sql: String,
params: Vec<String>,
state: State<'_, DbConnection>,
) -> Result<String, String> {
core::execute(&sql, &params, &state).await
}
/// Erstellt eine verschlüsselte Datenbank
#[tauri::command]
pub fn create_encrypted_database(
app_handle: AppHandle,
path: String,
key: String,
state: State<'_, DbConnection>,
) -> Result<String, String> {
// Ressourcenpfad zur eingebundenen Datenbank auflösen
let resource_path = app_handle
.path()
.resolve("resources/vault.db", BaseDirectory::Resource)
.map_err(|e| format!("Fehler beim Auflösen des Ressourcenpfads: {}", e))?;
// Prüfen, ob die Ressourcendatei existiert
if !resource_path.exists() {
return Err(format!(
"Ressourcendatenbank wurde nicht gefunden: {}",
resource_path.display()
));
}
// Sicherstellen, dass das Zielverzeichnis existiert
if let Some(parent) = Path::new(&path).parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Fehler beim Erstellen des Zielverzeichnisses: {}", e))?;
}
}
// Kopieren der Ressourcen-Datenbank zum Zielpfad
core::copy_file(&resource_path, &path)?;
// Öffnen der kopierten Datenbank ohne Verschlüsselung
let conn = Connection::open(&path).map_err(|e| {
format!(
"Fehler beim Öffnen der kopierten Datenbank: {}",
e.to_string()
)
})?;
// Verschlüsseln der Datenbank mit dem angegebenen Schlüssel
conn.pragma_update(None, "key", &key)
.map_err(|e| format!("Fehler beim Verschlüsseln der Datenbank: {}", e.to_string()))?;
// Schließen der Verbindung, um sicherzustellen, dass Änderungen gespeichert werden
drop(conn);
// Öffnen der verschlüsselten Datenbank mit dem Schlüssel
let encrypted_conn = core::open_and_init_db(&path, &key, false)
.map_err(|e| format!("Fehler beim Öffnen der verschlüsselten Datenbank: {}", e))?;
// Überprüfen, ob die Datenbank korrekt verschlüsselt wurde, indem wir eine einfache Abfrage ausführen
let validation_result: Result<i32, _> =
encrypted_conn.query_row("SELECT 1", [], |row| row.get(0));
if let Err(e) = validation_result {
return Err(format!(
"Fehler beim Testen der verschlüsselten Datenbank: {}",
e.to_string()
));
}
// Aktualisieren der Datenbankverbindung im State
let mut db = state
.0
.lock()
.map_err(|e| format!("Mutex-Fehler: {}", e.to_string()))?;
*db = Some(encrypted_conn);
Ok(format!(
"Verschlüsselte CRDT-Datenbank erstellt unter: {} (kopiert aus Ressource)",
path
))
}
/// Öffnet eine verschlüsselte Datenbank
#[tauri::command]
pub fn open_encrypted_database(
path: String,
key: String,
state: State<'_, DbConnection>,
) -> Result<String, String> {
if !std::path::Path::new(&path).exists() {
return Err("Datenbankdatei nicht gefunden".into());
}
let conn = core::open_and_init_db(&path, &key, false)?;
let mut db = state.0.lock().map_err(|e| e.to_string())?;
*db = Some(conn);
Ok(format!("Verschlüsselte CRDT-Datenbank geöffnet: {}", path))
}

View File

@ -0,0 +1,46 @@
mod permissions;
use crate::database;
use crate::database::DbConnection;
//use crate::models::ExtensionState;
use tauri::{AppHandle, State};
// Extension-bezogene Funktionen mit extension_-Präfix
/// Lädt eine Extension aus einer Manifest-Datei
/* #[tauri::command]
pub fn extension_load(
manifest_path: String,
app: AppHandle,
) -> Result<crate::models::ExtensionManifest, String> {
let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?;
let manifest: crate::models::ExtensionManifest =
serde_json::from_str(&manifest_content).map_err(|e| e.to_string())?;
app.state::<ExtensionState>()
.add_extension(manifest_path.clone(), manifest.clone());
Ok(manifest)
} */
/// Führt SQL-Leseoperationen mit Berechtigungsprüfung aus
#[tauri::command]
pub async fn extension_sql_select(
app: AppHandle,
extension_id: String,
sql: String,
params: Vec<String>,
state: State<'_, DbConnection>,
) -> Result<Vec<Vec<String>>, String> {
permissions::check_read_permission(&app, &extension_id, &sql).await?;
database::core::select(&sql, &params, &state).await
}
/// Führt SQL-Schreiboperationen mit Berechtigungsprüfung aus
#[tauri::command]
pub async fn extension_sql_execute(
app: AppHandle,
extension_id: String,
sql: String,
params: Vec<String>,
state: State<'_, DbConnection>,
) -> Result<String, String> {
permissions::check_write_permission(&app, &extension_id, &sql).await?;
database::core::execute(&sql, &params, &state).await
}

View File

@ -0,0 +1,203 @@
// database/permissions.rs
use crate::database::core::extract_tables_from_query;
use crate::database::DbConnection;
use crate::models::DbExtensionPermission;
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
use tauri::{AppHandle, Manager};
/// Prüft Leseberechtigungen für eine Extension basierend auf Datenbankeinträgen
pub async fn check_read_permission(
app: &AppHandle,
extension_id: &str,
sql: &str,
) -> Result<(), String> {
// SQL-Statement parsen
let dialect = SQLiteDialect {};
let statements = Parser::parse_sql(&dialect, sql).map_err(|e| e.to_string())?;
let statement = statements
.into_iter()
.next()
.ok_or("Keine SQL-Anweisung gefunden")?;
// Berechtigungsprüfung für SELECT-Statements
if let sqlparser::ast::Statement::Query(query) = statement {
let tables = extract_tables_from_query(&query);
// Berechtigungen aus der Datenbank abrufen
let db_state = app.state::<DbConnection>();
let permissions =
get_extension_permissions(db_state, extension_id, "database", "read").await?;
// Prüfen, ob alle benötigten Tabellen in den Berechtigungen enthalten sind
for table in tables {
let has_permission = permissions.iter().any(|perm| perm.path.contains(&table));
if !has_permission {
return Err(format!("Keine Leseberechtigung für Tabelle {}", table));
}
}
Ok(())
} else {
Err("Nur SELECT-Anweisungen erlaubt".into())
}
}
/// Prüft Schreibberechtigungen für eine Extension basierend auf Datenbankeinträgen
pub async fn check_write_permission(
app: &AppHandle,
extension_id: &str,
sql: &str,
) -> Result<(), String> {
// SQL-Statement parsen
let dialect = SQLiteDialect {};
let statements = Parser::parse_sql(&dialect, sql).map_err(|e| e.to_string())?;
let statement = statements
.into_iter()
.next()
.ok_or("Keine SQL-Anweisung gefunden")?;
// Berechtigungsprüfung basierend auf Statement-Typ
match statement {
sqlparser::ast::Statement::Insert(insert) => {
let table_name = match insert.table {
sqlparser::ast::TableObject::TableName(name) => name.to_string(),
_ => return Err("Ungültige Tabellenangabe in INSERT".into()),
};
// Berechtigungen aus der Datenbank abrufen
let db_state = app.state::<DbConnection>();
let permissions =
get_extension_permissions(db_state, extension_id, "database", "write").await?;
// Prüfen, ob die Tabelle in den Berechtigungen enthalten ist
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(&table_name));
if !has_permission {
return Err(format!(
"Keine Schreibberechtigung für Tabelle {}",
table_name
));
}
}
sqlparser::ast::Statement::Update { table, .. } => {
let table_name = table.relation.to_string();
// Berechtigungen aus der Datenbank abrufen
let db_state = app.state::<DbConnection>();
let permissions =
get_extension_permissions(db_state, extension_id, "database", "write").await?;
// Prüfen, ob die Tabelle in den Berechtigungen enthalten ist
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(&table_name));
if !has_permission {
return Err(format!(
"Keine Schreibberechtigung für Tabelle {}",
table_name
));
}
}
sqlparser::ast::Statement::Delete(delete) => {
let from_tables = match delete.from {
sqlparser::ast::FromTable::WithFromKeyword(tables) => tables,
sqlparser::ast::FromTable::WithoutKeyword(tables) => tables,
};
if from_tables.is_empty() && delete.tables.is_empty() {
return Err("Keine Tabelle in DELETE angegeben".into());
}
let table_name = if !from_tables.is_empty() {
from_tables[0].relation.to_string()
} else {
delete.tables[0].to_string()
};
// Berechtigungen aus der Datenbank abrufen
let db_state = app.state::<DbConnection>();
let permissions =
get_extension_permissions(db_state, extension_id, "database", "write").await?;
// Prüfen, ob die Tabelle in den Berechtigungen enthalten ist
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(&table_name));
if !has_permission {
return Err(format!(
"Keine Schreibberechtigung für Tabelle {}",
table_name
));
}
}
sqlparser::ast::Statement::CreateTable(create_table) => {
let table_name = create_table.name.to_string();
// Berechtigungen aus der Datenbank abrufen
let db_state = app.state::<DbConnection>();
let permissions =
get_extension_permissions(db_state, extension_id, "database", "create").await?;
// Prüfen, ob die Tabelle in den Berechtigungen enthalten ist
let has_permission = permissions
.iter()
.any(|perm| perm.path.contains(&table_name));
if !has_permission {
return Err(format!(
"Keine Erstellungsberechtigung für Tabelle {}",
table_name
));
}
}
_ => return Err("Nur Schreiboperationen erlaubt (nutze 'select' für Abfragen)".into()),
}
Ok(())
}
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
async fn get_extension_permissions(
db_state: tauri::State<'_, DbConnection>,
extension_id: &str,
resource: &str,
operation: &str,
) -> Result<Vec<DbExtensionPermission>, String> {
let db = db_state
.0
.lock()
.map_err(|e| format!("Mutex-Fehler: {}", e))?;
let conn = db.as_ref().ok_or("Keine Datenbankverbindung vorhanden")?;
let mut stmt = conn
.prepare(
"SELECT id, extension_id, resource, operation, path
FROM haex_vault_extension_permissions
WHERE extension_id = ? AND resource = ? AND operation = ?",
)
.map_err(|e| format!("SQL-Vorbereitungsfehler: {}", e))?;
let rows = stmt
.query_map(&[extension_id, resource, operation], |row| {
Ok(DbExtensionPermission {
id: row.get(0)?,
extension_id: row.get(1)?,
resource: row.get(2)?,
operation: row.get(3)?,
path: row.get(4)?,
})
})
.map_err(|e| format!("SQL-Abfragefehler: {}", e))?;
let mut permissions = Vec::new();
for row in rows {
permissions.push(row.map_err(|e| format!("Fehler beim Lesen der Berechtigungen: {}", e))?);
}
Ok(permissions)
}

View File

@ -0,0 +1 @@
pub mod database;

32
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,32 @@
mod browser;
mod database;
mod extension;
mod models;
use database::DbConnection;
//use models::ExtensionState;
use std::sync::Mutex;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.manage(DbConnection(Mutex::new(None)))
//.manage(ExtensionState::default())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_store::Builder::new().build())
.invoke_handler(tauri::generate_handler![
database::create_encrypted_database,
database::open_encrypted_database,
database::sql_execute,
database::sql_select,
extension::database::extension_sql_execute,
extension::database::extension_sql_select,
browser::create_tab
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
haex_hub_lib::run()
}

50
src-tauri/src/models.rs Normal file
View File

@ -0,0 +1,50 @@
// models.rs
use serde::{Deserialize, Serialize};
//use std::sync::Mutex;
#[derive(Serialize, Deserialize, Clone)]
pub struct ExtensionManifest {
pub name: String,
pub entry: String,
pub permissions: ExtensionPermissions,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ExtensionPermissions {
pub database: Option<DatabasePermissions>,
pub http: Option<Vec<String>>,
pub filesystem: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct DatabasePermissions {
pub read: Option<Vec<String>>,
pub write: Option<Vec<String>>,
pub create: Option<Vec<String>>,
}
/* #[derive(Default)]
pub struct ExtensionState {
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
}
impl ExtensionState {
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
let mut extensions = self.extensions.lock().unwrap();
extensions.insert(path, manifest);
}
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
let extensions = self.extensions.lock().unwrap();
extensions.values().find(|p| p.name == addon_id).cloned()
}
} */
#[derive(Debug, Serialize, Deserialize)]
pub struct DbExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource: String,
pub operation: String,
pub path: String,
}

38
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "haex-hub",
"version": "0.1.0",
"identifier": "space.haex.hub",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:3001",
"beforeBuildCommand": "pnpm generate",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "haex-hub",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": {
"database/vault.db": "resources/vault.db"
}
}
}

27
src/app.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<div>
<NuxtLayout :data-theme="currentTheme">
<NuxtPage />
<NuxtSnackbar />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
const { currentTheme } = storeToRefs(useUiStore());
</script>
<style>
.fade-enter-active {
transition: all 1s ease-out;
}
.fade-leave-active {
transition: all 1s ease-out reverse;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<div class="browser">
<div class="browser-controls">
<button
@click="$emit('goBack', activeTabId)"
:disabled="!activeTabId"
>
</button>
<button
@click="$emit('goForward', activeTabId)"
:disabled="!activeTabId"
>
</button>
<button @click="$emit('createTab')">+</button>
<HaexBrowserUrlBar
:url="activeTab?.url || ''"
:isLoading="activeTab?.isLoading || false"
@submit="handleUrlSubmit"
/>
</div>
<HaexBrowserTabBar
:tabs="tabs"
:activeTabId="activeTabId"
@closeTab="$emit('closeTab', $event)"
@activateTab="$emit('activateTab', $event)"
/>
<div
class="browser-content"
ref="contentRef"
>
<!-- Die eigentlichen Webview-Inhalte werden von Tauri verwaltet -->
<div
v-if="!activeTabId"
class="empty-state"
>
<p>
Kein Tab geöffnet. Erstellen Sie einen neuen Tab mit dem + Button.
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { Window } from '@tauri-apps/api/window';
import { getCurrentWebview, Webview } from '@tauri-apps/api/webview';
/* const appWindow = new Window('uniqueLabel');
const webview = new Webview(appWindow, 'theUniqueLabel', {
url: 'https://www.google.de',
x: 0,
y: 0,
height: 1000,
width: 1000,
});
webview.once('tauri://created', function () {
console.log('create new webview');
}); */
interface Tab {
id: string;
title: string;
url: string;
isLoading: boolean;
isActive: boolean;
window_label: string;
}
interface Props {
tabs: Tab[];
activeTabId: string | null;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'createTab'): void;
(e: 'closeTab', tabId: string): void;
(e: 'navigate', tabId: string, url: string): void;
(e: 'goBack', tabId: string | null): void;
(e: 'goForward', tabId: string | null): void;
(e: 'activateTab', tabId: string | null): void;
}>();
const { initializeAsync, processNavigation, injectContentScripts } =
useBrowserExtensionStore();
const contentRef = ref<HTMLDivElement | null>(null);
//const extensionManager = ref<ExtensionManager>(new ExtensionManager());
const activeTab = computed(() =>
props.tabs?.find((tab) => tab.id === props.activeTabId)
);
onMounted(async () => {
// Initialisiere das Erweiterungssystem
await initializeAsync();
// Aktualisiere die Webview-Größe
await updateWebviewBoundsAsync();
//window.addEventListener('resize', updateWebviewBounds);
});
// Wenn ein neuer Tab aktiviert wird, injiziere Content-Scripts
/* watch(
() => props.activeTabId,
async (newTabId) => {
if (newTabId && props.tabs.length > 0) {
const activeTab = props.tabs.find((tab) => tab.id === newTabId);
if (activeTab) {
// Warte kurz, bis die Seite geladen ist
setTimeout(() => {
injectContentScripts(activeTab.window_label);
}, 500);
// Aktualisiere die Webview-Größe
updateWebviewBounds();
}
}
}
); */
const handleUrlSubmit = (url: string) => {
if (props.activeTabId) {
// Prüfe URL mit Erweiterungen vor der Navigation
if (processNavigation(url)) {
emit('navigate', props.activeTabId, url);
} else {
console.log('Navigation blockiert durch Erweiterung');
// Hier könnten Sie eine Benachrichtigung anzeigen
}
}
};
const updateWebviewBoundsAsync = async () => {
if (!contentRef.value) return;
const rect = contentRef.value.getBoundingClientRect();
const bounds = {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
};
/* await invoke('update_window_bounds', {
contentBounds: { x: bounds.x, y: bounds.y },
contentSize: { width: bounds.width, height: bounds.height },
}); */
};
</script>

View File

@ -0,0 +1,43 @@
<template>
<div class="tab-bar">
<div
v-for="tab in tabs"
:key="tab.id"
class="tab"
:class="{ active: tab.id === activeTabId }"
@click="$emit('activateTab', tab.id)"
>
<span class="tab-title">
{{ tab.title || 'Neuer Tab' }}
</span>
<button
class="tab-close"
@click.stop="$emit('closeTab', tab.id)"
>
×
</button>
</div>
</div>
</template>
<script setup lang="ts">
interface Tab {
id: string;
title: string;
url: string;
isLoading: boolean;
isActive: boolean;
}
interface Props {
tabs: Tab[];
activeTabId: string | null;
}
defineProps<Props>();
defineEmits<{
(e: 'closeTab', tabId: string): void;
(e: 'activateTab', tabId: string): void;
}>();
</script>

View File

@ -0,0 +1,57 @@
<template>
<form
class="url-bar"
@submit.prevent="handleSubmit"
>
<input
type="text"
v-model="inputValue"
placeholder="URL eingeben"
/>
<span
v-if="isLoading"
class="loading-indicator"
>Laden...</span
>
<button
v-else
type="submit"
>
Go
</button>
</form>
</template>
<script setup lang="ts">
const props = defineProps({
url: {
type: String,
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['submit']);
const inputValue = ref(props.url);
watch(
() => props.url,
(newUrl) => {
inputValue.value = newUrl;
}
);
const handleSubmit = () => {
// URL validieren und ggf. Protokoll hinzufügen
let processedUrl = inputValue.value.trim();
if (processedUrl && !processedUrl.match(/^[a-zA-Z]+:\/\//)) {
processedUrl = 'https://' + processedUrl;
}
emit('submit', processedUrl);
};
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="fixed z-10">
<div
class="dropdown relative inline-flex [--placement:top] [--strategy:absolute]"
>
<button
:id
class="dropdown-toggle btn btn-primary btn-lg btn-square dropdown-open:rotate-45"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Menu"
>
<Icon
:name="icon"
size="46"
/>
</button>
<div
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60 rtl:left-0 bg-transparent"
role="menu"
aria-orientation="vertical"
:aria-labelledby="id"
>
<ul
class="dropdown-open:ease-in dropdown-open:translate-x-0 -translate-x-1 rtl:translate-x-1 transition duration-300 ease-out"
data-dropdown-transition
>
<li
v-for="link in menu"
class="dropdown-item hover:bg-transparent"
>
<NuxtLinkLocale
v-if="link.to"
:to="link.to"
class="btn btn-primary flex items-center no-underline rounded-lg flex-nowrap"
>
<Icon
v-if="link.icon"
:name="link.icon"
class="me-3"
/>
{{ link.label }}
</NuxtLinkLocale>
<button
v-else
@click="link.action"
class="link hover:link-primary flex items-center no-underline w-full"
>
<Icon
v-if="link.icon"
:name="link.icon"
class="me-3"
/>
{{ link.label }}
</button>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { IActionMenuItem } from './types';
defineProps({
menu: {
type: Array as PropType<IActionMenuItem[]>,
},
icon: {
type: String,
default: 'mdi:plus',
},
});
const id = useId();
</script>

View File

@ -0,0 +1,25 @@
<template>
<button
class="btn join-item"
:class="{
'btn-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
:type
>
<slot />
</button>
</template>
<script setup lang="ts">
const { currentScreenSize } = storeToRefs(useUiStore());
defineProps({
type: {
type: String as PropType<'reset' | 'submit' | 'button'>,
default: 'button',
},
});
</script>

8
src/components/ui/button/types.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import type { RouteLocationRaw } from 'vue-router';
export interface IActionMenuItem {
label: string;
icon?: string;
action?: () => Promise<unknown>;
to?: RouteLocationRaw;
}

View File

@ -0,0 +1,121 @@
<template>
<slot
name="trigger"
:id
>
</slot>
<div
:id
class="overlay modal overlay-open:opacity-100 hidden modal-middle [--tab-accessibility-limited:false] overflow-scroll p-0 sm:p-4"
role="dialog"
ref="modalRef"
>
<div
class="overlay-animation-target overlay-open:mt-4 overlay-open:duration-500 mt-12 transition-all ease-out modal-dialog overlay-open:opacity-100"
>
<div class="modal-content">
<div class="modal-header">
<slot name="title">
<h3
v-if="title"
class="modal-title text-base sm:text-lg"
>
{{ title }}
</h3>
</slot>
<button
type="button"
class="btn btn-text btn-circle btn-sm absolute end-3 top-3"
:aria-label="t('close')"
@click="open = false"
tabindex="1"
>
<Icon
name="mdi:close"
size="18"
/>
</button>
</div>
<div class="modal-body text-sm sm:text-base py-1">
<slot />
</div>
<div class="modal-footer flex-wrap">
<slot name="buttons" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { HSOverlay } from 'flyonui/flyonui';
export interface IDom {
class?: String;
text: String;
}
const id = useId();
defineProps({
trigger: {
type: Object as PropType<IDom>,
default: () => ({
class: '',
text: '',
}),
},
title: {
type: String,
default: '',
},
description: {
type: Object as PropType<IDom>,
default: () => ({
class: '',
text: '',
}),
required: false,
},
});
const open = defineModel<boolean>('open', { default: false });
const { t } = useI18n();
const modalRef = useTemplateRef('modalRef');
const modal = ref<HSOverlay>();
watch(open, async () => {
if (open.value) {
//console.log('open modal', modal.value?.open);
await modal.value?.open();
} else {
await modal.value?.close(true);
}
});
onMounted(() => {
if (!modalRef.value) return;
modal.value = new HSOverlay(modalRef.value, { isClosePrev: true });
modal.value.on('close', () => {
console.log('close it from event', open.value);
open.value = false;
});
});
</script>
<i18n lang="json">
{
"de": {
"close": "Schließen"
},
"en": {
"close": "Close"
}
}
</i18n>

View File

@ -0,0 +1,58 @@
<template>
<button
type="button"
class="btn btn-primary"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="basic-modal"
data-overlay="#basic-modal"
>
Open modal
</button>
<div
id="basic-modal"
class="overlay modal overlay-open:opacity-100 hidden"
role="dialog"
tabindex="-1"
>
<div class="modal-dialog overlay-open:opacity-100">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Dialog Title</h3>
<button
type="button"
class="btn btn-text btn-circle btn-sm absolute end-3 top-3"
aria-label="Close"
data-overlay="#basic-modal"
>
<span class="icon-[tabler--x] size-4"></span>
</button>
</div>
<div class="modal-body">
This is some placeholder content to show the scrolling behavior for
modals. Instead of repeating the text in the modal, we use an inline
style to set a minimum height, thereby extending the length of the
overall modal and demonstrating the overflow scrolling. When content
becomes longer than the height of the viewport, scrolling will move
the modal as needed.
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-soft btn-secondary"
data-overlay="#basic-modal"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
>
Save changes
</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<h3 class="modal-title">
<slot />
</h3>
</template>

View File

@ -0,0 +1,199 @@
<template>
<span>
<fieldset class="join w-full">
<slot name="prepend" />
<span class="input-group join-item">
<span
v-if="prependIcon || prependLabel"
class="input-group-text"
>
<label v-if="prependLabel">
{{ prependLabel }}
</label>
<Icon :name="prependIcon" />
</span>
<div class="relative w-full">
<input
:id
:name="name ?? id"
:placeholder="placeholder || label"
:type
:autofocus
class="input input-floating peer join-item"
:class="{
'input-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
v-bind="$attrs"
v-model="input"
ref="inputRef"
:readonly="read_only"
/>
<label
v-if="label"
:for="id"
class="input-floating-label"
>
{{ label }}
</label>
</div>
<span
v-if="appendIcon || appendLabel"
class="input-group-text"
>
<label
v-if="appendLabel"
class=""
>
{{ appendLabel }}
</label>
<Icon :name="appendIcon" />
</span>
</span>
<slot name="append" />
<UiButton
v-if="withCopyButton"
class="btn-outline btn-accent h-auto"
@click="copy(`${input}`)"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
<!-- <button
v-if="withCopyButton"
class="btn btn-outline btn-accent join-item h-auto"
:class="{
'btn-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
@click="copy(`${input}`)"
type="button"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</button> -->
</fieldset>
<span
class="flex flex-col px-2 pt-0.5"
v-show="errors"
>
<span
v-for="error in errors"
class="label-text-alt text-error"
>
{{ error }}
</span>
</span>
</span>
</template>
<script setup lang="ts">
import { type ZodSchema } from 'zod';
const inputRef = useTemplateRef('inputRef');
defineExpose({ inputRef });
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
placeholder: {
type: String,
default: '',
},
type: {
type: String as PropType<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
>,
default: 'text',
},
label: String,
name: String,
prependIcon: {
type: String,
default: '',
},
prependLabel: String,
appendIcon: {
type: String,
default: '',
},
appendLabel: String,
rules: Object as PropType<ZodSchema>,
checkInput: Boolean,
withCopyButton: Boolean,
autofocus: Boolean,
read_only: Boolean,
});
const input = defineModel<string | number | undefined | null>({
default: '',
required: true,
});
const { currentScreenSize } = storeToRefs(useUiStore());
onMounted(() => {
if (props.autofocus && inputRef.value) inputRef.value.focus();
});
const errors = defineModel<string[] | undefined>('errors');
const id = useId();
watch(input, () => checkInput());
watch(
() => props.checkInput,
() => {
checkInput();
}
);
const emit = defineEmits(['error']);
const checkInput = () => {
if (props.rules) {
const result = props.rules.safeParse(input.value);
//console.log('check result', result.error, props.rules);
if (!result.success) {
errors.value = result.error.errors.map((error) => error.message);
emit('error', errors.value);
} else {
errors.value = [];
}
}
};
const { copy, copied } = useClipboard();
</script>

View File

@ -0,0 +1,54 @@
<template>
<UiInput
:check-input
:label="label || t('password')"
:placeholder="placeholder || t('password')"
:rules
:type="type"
:autofocus
v-model="value"
>
<template #append>
<UiButton
class="btn-outline btn-accent h-auto"
@click="tooglePasswordType"
>
<Icon :name="type === 'password' ? 'mdi:eye' : 'mdi:eye-off'" />
</UiButton>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod';
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
const value = defineModel<string | number | null | undefined>();
defineProps({
label: String,
placeholder: String,
checkInput: Boolean,
rules: Object as PropType<ZodSchema>,
autofocus: Boolean,
});
const type = ref<'password' | 'text'>('password');
const tooglePasswordType = () => {
type.value = type.value === 'password' ? 'text' : 'password';
};
</script>
<i18n lang="json">
{
"de": {
"password": "Passwort"
},
"en": {
"password": "Password"
}
}
</i18n>

View File

@ -0,0 +1,56 @@
<template>
<UiInput
:autofocus
:check-input="checkInput"
:label="label || t('url')"
:placeholder="placeholder || t('url')"
:read_only
:rules
:with-copy-button
v-model.trim="value"
>
<template #append>
<UiButton
v-if="read_only"
@click="openUrl(`${value}`)"
class="btn-outline btn-accent h-auto"
:class="{
disabled: !value?.length,
}"
>
<Icon name="streamline:web" />
</UiButton>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod';
import { openUrl } from '@tauri-apps/plugin-opener';
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
const value = defineModel<string | null | undefined>();
defineProps({
label: String,
placeholder: String,
checkInput: Boolean,
rules: Object as PropType<ZodSchema>,
autofocus: Boolean,
withCopyButton: Boolean,
read_only: Boolean,
});
</script>
<i18n lang="json">
{
"de": {
"url": "Url"
},
"en": {
"url": "Url"
}
}
</i18n>

View File

@ -0,0 +1,91 @@
<template>
<svg
id="logo"
class="fill-current stroke-current w-[160px]"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 286.3 85"
xml:space="preserve"
>
<switch>
<g>
<g class="logo-imagesss">
<circle
fill="white"
cx="42.5"
cy="42.5"
r="40"
></circle>
<path
d="M42.3,83.4c-22.6,0-40.9-18.4-40.9-40.9c0-22.6,18.4-40.9,40.9-40.9c22.6,0,40.9,18.4,40.9,40.9
C83.3,65.1,64.9,83.4,42.3,83.4z M42.3,5.8C22.1,5.8,5.7,22.3,5.7,42.5s16.5,36.7,36.7,36.7S79,62.7,79,42.5S62.6,5.8,42.3,5.8z
"
></path>
<g>
<g>
<polygon
points="38.8,69.8 38.8,31.7 22.3,31.7 22.3,38.5 29.8,38.5 29.8,69.8 "
></polygon>
<path
d="M34.1,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6
C39.9,15.9,37.3,13.2,34.1,13.2z"
></path>
</g>
<g>
<polygon
points="45.9,69.8 45.9,31.7 62.4,31.7 62.4,38.5 54.9,38.5 54.9,69.8 "
></polygon>
<path
d="M50.6,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C44.8,15.9,47.4,13.2,50.6,13.2z"
></path>
</g>
</g>
</g>
<g class="logo-textsss">
<path
d="M136.1,63.6c-4,0-5.3-2.6-5.3-6V38.5h10.6v-6.7h-10.6v-6.7h-9c0,7,0,29.1,0,32.7
c0,4.2,1.6,7.5,3.8,9.7c2.3,2.2,5.6,3.3,9.8,3.3c5.1,0,8.4-1.8,10.6-4.2l-4.7-6C140.2,62.1,138.5,63.6,136.1,63.6z"
></path>
<path
d="M217.7,30.7c-4.9,0-8.2,1.6-10.4,3.8c-2.2-2.2-5.5-3.8-10.4-3.8c-15,0-14.9,12.1-14.9,15
s0,24.1,0,24.1h9V45.7c0-8.5,4.9-8.3,5.9-8.3c1,0,5.9-0.3,5.9,8.3v24.1h0h9h0V45.7c0-8.5,4.9-8.3,5.9-8.3c1,0,5.9-0.3,5.9,8.3
v24.1h9c0,0,0-21.2,0-24.1C232.6,42.8,232.7,30.7,217.7,30.7z"
></path>
<path
d="M273.2,46.4c-4.3-1.4-6-2.5-6-5.2c0-2,1.1-3.8,4.3-3.8c3.2,0,4.5,3.3,5.1,4.8
c2.7-1.5,5.3-2.9,6.6-3.6c-2.5-6-6.3-7.9-12-7.9c-8,0-11.7,5.5-11.7,10.6c0,6.5,2.9,9.8,11.2,12.2c6,1.8,6.5,4.7,6.2,6.2
c-0.3,1.7-1.6,3.6-5.3,3.6c-3.6,0-5.8-3.8-6.8-5.4c-1.8,1.1-3.4,2.1-6.4,3.8c2.1,5,6.8,9.1,13.5,9.1c7.9,0,12.9-5.1,12.9-12.1
C284.9,51,279.6,48.5,273.2,46.4z"
></path>
<g>
<polygon
points="239.7,69.8 239.7,31.7 256.2,31.7 256.2,38.5 248.7,38.5 248.7,69.8 "
></polygon>
<path
d="M244.4,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C238.6,15.9,241.2,13.2,244.4,13.2z"
></path>
</g>
<g>
<polygon
points="114.7,69.8 114.7,31.7 98.1,31.7 98.1,38.5 105.7,38.5 105.7,69.8 "
></polygon>
<path
d="M110,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6C115.8,15.9,113.2,13.2,110,13.2
z"
></path>
</g>
<path
d="M176.4,52.4v-3.7c0-12.3-4.7-18-14.8-18c-9.3,0-14.7,6.6-14.7,18v4c0,11.5,5.8,18.2,15.8,18.2
c6.6,0,10.8-3.7,12.7-7.9c-2.2-1.4-4.6-2.8-6.1-3.8c-1,1.7-2.9,4.4-6.7,4.4c-5.8,0-7-5.9-7-10.9v-0.2H176.4z M155.7,45.7
c0.2-7.1,3.3-8.2,6-8.2c2.6,0,5.9,1,6,8.2H155.7z"
></path>
</g>
</g>
</switch>
</svg>
</template>

View File

@ -0,0 +1,24 @@
<template>
<UiTooltip
:tooltip="tooltip ?? label"
direction="right-end"
>
<button
class="link flex items-center justify-center py-3 hover:text-primary tooltip-toogle bg w-full"
@click="$emit('click')"
>
<Icon
:name="icon"
class="size-8"
/>
</button>
</UiTooltip>
</template>
<script setup lang="ts">
defineProps<{
label: string;
tooltip?: string;
icon: string;
}>();
</script>

View File

@ -0,0 +1,109 @@
<template>
<aside
class="flex shrink-0 transition-[width] ease-in duration-300 z-30 h-full overflow-hidden fixed sm:relative left-0 shadow border-r border-base-300"
>
<div class="sm:flex flex-col w-14 bg-base-200 shrink-0 h-full hidden">
<img
src="/logo.svg"
class="bg-primary p-3 size-16"
/>
<div class="flex flex-col justify-between h-full overflow-y-scroll z-10">
<div class="flex flex-col space-y-2 text-base-content/90">
<template v-for="item in menu.top">
<UiSidebarLink
v-if="item.to"
:to="item.to ?? ''"
:icon="item.icon"
:label="$t(item.label)"
/>
<UiSidebarButton
v-else
:icon="item.icon"
:label="$t(item.label)"
@click="item.click"
/>
</template>
</div>
<div class="flex flex-col space-y-2 text-base-content/90">
<template v-for="item in menu.bottom">
<UiSidebarLink
v-if="item.to"
:to="item.to ?? ''"
:icon="item.icon"
:label="$t(item.label)"
/>
<UiSidebarButton
v-else
:icon="item.icon"
:label="$t(item.label)"
@click="item.click"
/>
</template>
<!-- <UiSidebarLink
v-for="item in menu.bottom"
:icon="item.icon"
:to="item.to ?? ''"
:label="item.label"
@click="item.click"
/> -->
</div>
</div>
</div>
<!-- <div class="bg-base-100 flex flex-col w-full overflow-clip">
<div
class="h-16 flex items-center sm:justify-center justify-end md:justify-start bg-base-300 shrink-0"
>
<button
class="top-3 left-2 absolute z-30 duration-1000 btn btn-square btn-primary transition-opacity btn-outline sm:hidden"
@click="show = !show"
>
<Icon
name="mdi:menu"
size="28"
/>
</button>
<span
class="px-4 font-semibold text-base-content shrink-0 sm:bg-transparent bg-primary h-full flex items-center rounded-l-lg"
>
<p>Haex Vault</p>
</span>
<img
src="/logo.svg"
class="bg-primary p-3 size-16 shrink-0 sm:hidden rounded-r-lg"
/>
<button
class="btn btn-square btn-primary btn-outline mr-2 ml-auto hidden sm:flex"
@click="show = false"
>
<Icon
name="mdi:close"
size="28"
/>
</button>
</div>
<div class="overflow-scroll flex pb-4 relative">
<slot />
</div>
</div> -->
</aside>
</template>
<script lang="ts" setup>
defineProps({
menu: {
type: Object as PropType<ISidebarMenu>,
default: () => {},
},
});
//const show = ref(true);
const { show } = storeToRefs(useSidebarStore());
</script>

View File

@ -0,0 +1,61 @@
<template>
<li
@click="triggerNavigate"
class="hover:text-primary"
>
<UiTooltip
:tooltip="tooltip ?? name"
direction="right-end"
>
<NuxtLinkLocale
:class="{ ['bg-base-300']: isActive }"
:to="{
name: type === 'browser' ? 'haexBrowser' : 'haexExtension',
params: type === 'browser' ? {} : { extensionId: id },
}"
class="flex items-center justify-center cursor-pointer tooltip-toogle"
ref="link"
>
<Icon
:name="icon"
class="shrink-0 size-6"
/>
</NuxtLinkLocale>
</UiTooltip>
</li>
</template>
<script setup lang="ts">
import { type ISidebarItem } from '#imports';
const props = defineProps<ISidebarItem>();
const router = useRouter();
const isActive = computed(() => {
if (props.type === 'browser') {
return router.currentRoute.value.name === 'haexBrowser';
} else if (props.type === 'extension') {
return (
router.currentRoute.value.name === 'haexExtension' &&
getSingleRouteParam(router.currentRoute.value.params.extensionId) ===
props.id
);
}
});
const link = useTemplateRef('link');
const triggerNavigate = () => link.value?.$el.click();
/* computed(() => {
const found = useRouter()
.getRoutes()
.find((route) => route.name === useLocaleRoute()(props.to)?.name);
console.log('found route', found, useRoute());
return (
found?.name === useRoute().name ||
found?.children.some((child) => child.name === useRoute().name)
);
}); */
</script>

View File

@ -0,0 +1,268 @@
<template>
<nav
class="navbar bg-base-100 max-sm:rounded-box max-sm:shadow sm:border-b border-base-content/25 sm:z-[1] relative"
>
<button
type="button"
class="btn btn-text max-sm:btn-square sm:hidden me-2"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
data-overlay="#sidebar"
>
<span class="icon-[tabler--menu-2] size-5"></span>
</button>
<div class="flex flex-1 items-center">
<a
class="link text-base-content link-neutral text-xl font-semibold no-underline"
href="#"
>
<UiTextGradient>Haex Hub</UiTextGradient>
</a>
</div>
<div class="navbar-end flex items-center gap-4">
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle btn btn-text btn-circle dropdown-open:bg-base-content/10 size-10"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="indicator">
<span
v-show="notifications.length"
class="indicator-item bg-error size-2 rounded-full text-sm"
></span>
<span
class="icon-[tabler--bell] text-base-content size-[1.375rem]"
></span>
</div>
</button>
<div
class="dropdown-menu dropdown-open:opacity-100 hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-scrollable"
>
<div class="dropdown-header justify-center">
<h6 class="text-base-content text-base">
{{ t('notifications.label') }}
</h6>
</div>
<div
class="vertical-scrollbar horizontal-scrollbar rounded-scrollbar text-base-content/80 max-h-56 overflow-auto max-md:max-w-60"
>
<div
class="dropdown-item"
v-for="notification in notifications"
>
<div class="avatar">
<div class="w-10 rounded-full">
<img
v-if="notification.image"
:src="notification.image"
:alt="notification.alt ?? 'notification avatar'"
/>
<Icon
v-else-if="notification.icon"
:name="notification.icon"
/>
</div>
</div>
<div class="w-60">
<h6 class="truncate text-base">
{{ notification.title }}
</h6>
<small class="text-base-content/50 truncate">
{{ notification.description }}
</small>
</div>
</div>
</div>
<a
href="#"
class="dropdown-footer justify-center gap-1"
>
<span class="icon-[tabler--eye] size-4"></span>
{{ t('notifications.view_all') }}
</a>
</div>
</div>
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle flex items-center"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="avatar">
<div class="size-9.5 rounded-full">
<img
src="https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png"
alt="avatar 1"
/>
</div>
</div>
</button>
<ul
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-avatar"
>
<li class="dropdown-header gap-2">
<div class="avatar">
<div class="w-10 rounded-full">
<img
src="https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png"
alt="avatar"
/>
</div>
</div>
<div>
<h6 class="text-base-content text-base font-semibold">
John Doe
</h6>
<small class="text-base-content/50">Admin</small>
</div>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--user]"></span>
My Profile
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--settings]"></span>
Settings
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--receipt-rupee]"></span>
Billing
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--help-triangle]"></span>
FAQs
</a>
</li>
<li class="dropdown-footer gap-2">
<a
class="btn btn-error btn-soft btn-block"
href="#"
>
<span class="icon-[tabler--logout]"></span>
Sign out
</a>
</li>
</ul>
</div>
</div>
</nav>
<aside
id="sidebar"
class="overlay sm:shadow-none overlay-open:translate-x-0 drawer drawer-start hidden max-w-64 sm:absolute sm:z-0 sm:flex sm:translate-x-0 pt-16"
role="dialog"
tabindex="-1"
>
<div class="drawer-body px-2 pt-4">
<ul class="menu p-0">
<li v-for="item in menu">
<a href="#">
<span class="icon-[tabler--home] size-5"></span>
Home
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--user] size-5"></span>
Account
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--message] size-5"></span>
Notifications
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--mail] size-5"></span>
Email
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--calendar] size-5"></span>
Calendar
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--shopping-bag] size-5"></span>
Product
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--login] size-5"></span>
Sign In
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--logout-2] size-5"></span>
Sign Out
</a>
</li>
</ul>
</div>
</aside>
</template>
<script setup lang="ts">
const { t } = useI18n();
const { notifications } = storeToRefs(useNotificationStore());
const { menu } = storeToRefs(useSidebarStore());
</script>
<i18n lang="yaml">
de:
notifications:
label: Benachrichtigungen
view_all: Alle ansehen
en:
notifications:
label: Notifications
view_all: View all
</i18n>

View File

@ -0,0 +1,7 @@
<template>
<p
class="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent font-black"
>
<slot />
</p>
</template>

View File

@ -0,0 +1,61 @@
<template>
<div class="tooltip [--prevent-popper:false]">
<div
class="tooltip-toggle"
aria-label="Tooltip"
>
<slot>
<button class="btn btn-square">
<Icon name="mdi:chevron-up-box-outline" />
</button>
</slot>
<span
class="tooltip-content tooltip-shown:opacity-100 tooltip-shown:visible z-40"
role="tooltip"
>
<span
class="tooltip-body"
v-bind="$attrs"
>
{{ tooltip }}
</span>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue';
const props = defineProps({
direction: {
type: String as PropType<
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end'
>,
default: 'top',
},
tooltip: String,
trigger: {
type: String as PropType<'focus' | 'hover' | 'click'>,
default: 'hover',
},
});
defineOptions({
inheritAttrs: false,
});
</script>

View File

@ -0,0 +1,165 @@
<template>
<UiDialog
:title="t('title')"
v-model:open="open"
>
<template #trigger="{ id }">
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap"
@click="open = true"
>
<Icon name="mdi:plus" />
{{ t('database.create') }}
</button>
</template>
<form
class="flex flex-col gap-4"
@submit="onCreateAsync"
>
<!-- @keyup.enter="onCreateAsync" -->
<UiInput
:check-input="check"
:label="t('database.label')"
:placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name"
autofocus
prepend-icon="mdi:safe"
v-model="database.name"
/>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
</form>
<template #buttons>
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
class="btn-primary"
@click="onCreateAsync"
>
{{ t('create') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { save } from '@tauri-apps/plugin-dialog';
import { useVaultStore } from '~/stores/vault';
import { vaultDatabaseSchema } from './schema';
const check = ref(false);
const open = ref();
const { t } = useI18n();
const database = reactive<{
name: string;
password: string;
path: string | null;
type: 'password' | 'text';
}>({
name: '',
password: '',
path: '',
type: 'password',
});
const initDatabase = () => {
database.name = t('database.name');
database.password = '';
database.path = '';
database.type = 'password';
};
initDatabase();
const { add } = useSnackbar();
const { createAsync } = useVaultStore();
//const { show } = storeToRefs(useSidebarStore());
const onCreateAsync = async () => {
check.value = true;
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name);
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
);
console.log(
'checks',
database.name,
nameCheck,
database.password,
passwordCheck
);
if (!nameCheck.success || !passwordCheck.success) return;
open.value = false;
try {
database.path = await save({ defaultPath: `${database.name}.db` });
console.log('data', database);
if (database.path && database.password) {
const vaultId = await createAsync({
path: database.path,
password: database.password,
});
//show.value = true;
await navigateTo(
useLocaleRoute()({ name: 'vault', params: { vaultId } })
);
}
} catch (error) {
console.error(error);
add({ type: 'error', text: JSON.stringify(error) });
}
};
const onClose = () => {
open.value = false;
initDatabase();
};
</script>
<i18n lang="json">
{
"de": {
"database": {
"label": "Datenbankname",
"placeholder": "Passwörter",
"create": "Neue Vault anlegen",
"name": "Passwörter"
},
"title": "Neue Datenbank anlegen",
"create": "Erstellen",
"abort": "Abbrechen",
"description": "Haex Vault für deine geheimsten Geheimnisse"
},
"en": {
"database": {
"label": "Databasename",
"placeholder": "Databasename",
"create": "Create new Vault",
"name": "Passwords"
},
"title": "Create New Database",
"create": "Create",
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}
</i18n>

View File

@ -0,0 +1,179 @@
<template>
<UiDialog v-model:open="isOpen">
<!-- @close="initDatabase" -->
<template #trigger>
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1"
@click="onLoadDatabase"
>
<Icon name="mdi:folder-open-outline" />
{{ t('database.open') }}
</button>
</template>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
@keyup.enter="onOpenDatabase"
autofocus
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
<template #buttons>
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
type="submit"
class="btn-primary"
@click="onOpenDatabase"
>
{{ t('open') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { open } from '@tauri-apps/plugin-dialog';
import { vaultDatabaseSchema } from './schema';
const { t } = useI18n();
const isOpen = defineModel('isOpen', { type: Boolean });
const props = defineProps({
path: String,
});
const check = ref(false);
const database = reactive<{
name: string;
password: string;
path: string | null;
type: 'password' | 'text';
}>({
name: '',
password: '',
path: '',
type: 'password',
});
const initDatabase = () => {
database.name = '';
database.password = '';
database.path = '';
database.type = 'password';
};
initDatabase();
const { add } = useSnackbar();
const handleError = (error: unknown) => {
isOpen.value = false;
add({ type: 'error', text: JSON.stringify(error) });
//console.error(error);
};
const { openAsync } = useVaultStore();
//const { show } = storeToRefs(useSidebarStore());
const onLoadDatabase = async () => {
try {
database.path = await open({
multiple: false,
directory: false,
filters: [
{
name: 'HaexVault',
extensions: ['db'],
},
],
});
if (!database.path) return;
isOpen.value = true;
} catch (error) {
handleError(error);
}
};
const localePath = useLocalePath();
const onOpenDatabase = async () => {
try {
check.value = true;
const path = database.path || props.path;
const pathCheck = vaultDatabaseSchema.path.safeParse(path);
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
);
if (!pathCheck.success || !passwordCheck.success || !path) {
add({ type: 'error', text: 'params falsch' });
return;
}
//console.log('try to open', path);
const vaultId = await openAsync({
path,
password: database.password,
});
if (!vaultId) {
add({ type: 'error', text: 'Vault konnte nicht geöffnet werden' });
return;
}
onClose();
/* await navigateTo(
localePath({
name: 'vaultGroup',
params: {
vaultId,
},
query: {
showSidebar: 'true',
},
})
); */
} catch (error) {
console.log(error);
handleError(error);
}
};
const onClose = () => {
initDatabase();
isOpen.value = false;
};
</script>
<i18n lang="json">
{
"de": {
"open": "Öffnen",
"abort": "Abbrechen",
"database": {
"open": "Vault öffnen"
}
},
"en": {
"open": "Open",
"abort": "Abort",
"database": {
"open": "Open Vault"
}
}
}
</i18n>

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const vaultDatabaseSchema = {
password: z.string().min(6).max(255),
name: z.string().min(1).max(255),
path: z.string().min(4).endsWith('.db'),
};

View File

@ -0,0 +1,250 @@
<template>
<VaultCard
@close="onClose"
@submit="onSubmit"
>
<template #header>
<div class="flex flex-wrap items-center justify-between w-full px-2 py-3">
<div class="w-full flex gap-2 justify-between items-center">
<div class="flex items-center gap-2">
<button
class="btn btn-square btn-primary btn-outline"
@click="onBack"
>
<Icon
name="mdi:chevron-left"
size="32"
/>
</button>
<button
class="btn btn-square btn-error btn-outline"
@click="onBack"
>
<Icon
name="mdi:trash-can-outline"
size="28"
/>
</button>
</div>
<slot name="buttons">
<div
v-if="read_only"
class="h-full"
>
<button
class="btn btn-square btn-primary btn-outline"
@click="read_only = false"
>
<Icon
name="mdi:pencil-outline"
size="24"
/>
</button>
</div>
<div
v-else
class="gap-2 h-full hidden md:flex"
>
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
>
<Icon name="mdi:close" />
<span class="hidden"> {{ t('abort') }} </span>
</button>
<button
class="btn btn-square btn-success btn-outline"
@click="onSubmit"
>
<Icon name="mdi:check" />
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
</slot>
</div>
<div
class="flex flex-col items-center w-full min-h-14 gap-2 py-1"
:class="{ '-ml-6': !show }"
:style="{ color }"
>
<Icon
v-if="icon"
:name="icon"
size="28"
/>
<h5
v-show="read_only"
class="overflow-hidden whitespace-nowrap"
>
{{ title }}
</h5>
</div>
</div>
</template>
<div class="h-full">
<slot />
<div
v-show="!read_only"
class="fixed bottom-2 left-0 w-full flex items-center justify-between px-4 md:hidden"
>
<div
class="transition-all duration-500"
:class="{ 'pl-96': show }"
>
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
>
<Icon name="mdi:close" />
<span class="hidden"> {{ t('abort') }} </span>
</button>
</div>
<div>
<button
class="btn btn-square btn-success"
@click="onSubmit"
>
<Icon name="mdi:check" />
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
<div></div>
</div>
<!-- <UiButtonAction
class=""
icon="mdi:content-save-outline"
><Icon name="mdi:content-save-outline" />
</UiButtonAction> -->
</div>
</VaultCard>
<VaultModalSaveChanges
v-model="showConfirmation"
@reject="onReject"
@submit="onSubmit"
/>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
const { t } = useI18n();
const { show } = storeToRefs(useSidebarStore());
const read_only = defineModel<boolean>('read_only', { default: false });
const props = defineProps({
color: String,
hasChanges: Boolean,
icon: String,
title: String,
});
const emit = defineEmits<{
back: [void];
close: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
submit: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
const showConfirmation = ref(false);
const to = ref<RouteLocationNormalizedLoadedGeneric>();
const isApprovedForLeave = ref(false);
const wantToGoBack = ref(false);
const onSubmit = () => {
showConfirmation.value = false;
isApprovedForLeave.value = true;
if (wantToGoBack.value) {
wantToGoBack.value = false;
read_only.value = true;
emit('submit');
} else {
emit('submit', to.value);
}
};
const onReject = () => {
showConfirmation.value = false;
isApprovedForLeave.value = true;
read_only.value = true;
if (wantToGoBack.value) {
wantToGoBack.value = false;
emit('back');
} else emit('reject', to.value);
};
const onBack = () => {
if (props.hasChanges) {
wantToGoBack.value = true;
showConfirmation.value = true;
} else {
emit('back');
}
};
const onClose = () => {
if (props.hasChanges) {
showConfirmation.value = true;
} else {
emit('close'); //read_only.value = true;
}
};
const onDelete = () => {};
onBeforeRouteLeave((_to, _from, next) => {
//console.log('check before leave', _to, _from);
to.value = _to;
if (isApprovedForLeave.value) {
isApprovedForLeave.value = false;
next();
} else if (props.hasChanges) {
showConfirmation.value = true;
} else {
next();
}
});
</script>
<i18n lang="json">
{
"de": {
"create": "Anlegen",
"abort": "Abbrechen",
"entry": {
"title": "Titel",
"username": "Nutzername",
"password": "Passwort",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "Verlauf"
}
},
"en": {
"create": "Create",
"abort": "Abort",
"entry": {
"title": "Title",
"username": "Username",
"password": "Password",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "History"
}
}
}
</i18n>

View File

@ -0,0 +1,42 @@
<template>
<div
class="bg-base-100 w-full mx-auto shadow h-full overflow-hidden pt-[7.5rem]"
>
<div
class="fixed top-0 right-0 z-10 transition-all duration-700 w-full font-semibold text-lg h-[7.5rem]"
:class="{ 'pl-96': show }"
>
<div
class="justify-center items-center flex flex-wrap border-b rounded-b border-secondary h-full"
:class="{ 'pl-12': !show }"
>
<slot name="header" />
</div>
</div>
<div class="h-full overflow-scroll bg-base-200">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
const { show } = storeToRefs(useSidebarStore());
const emit = defineEmits(['close', 'submit']);
const { escape, enter } = useMagicKeys();
watchEffect(async () => {
if (escape.value) {
await nextTick();
emit('close');
}
});
watchEffect(async () => {
if (enter.value) {
await nextTick();
emit('submit');
}
});
</script>

View File

@ -0,0 +1,32 @@
<template>
<UiDialog :title="t('title')">
<form class="flex flex-col">
<UiInput v-model="vaultItem.title" />
<UiInput v-model="vaultItem.username" />
<UiInput v-model="vaultItem.password" />
<UiInput v-model="vaultItem.note" />
</form>
</UiDialog>
</template>
<script setup lang="ts">
const { t } = useI18n();
const vaultItem = reactive({
title: '',
username: '',
password: '',
note: '',
});
</script>
<i18n lang="json">
{
"de": {
"title": "Eintrag erstellen"
},
"en": {
"title": "Create Entry"
}
}
</i18n>

View File

@ -0,0 +1,117 @@
<template>
<VaultCardEdit
v-if="vaultGroup"
:color="vaultGroup.color ?? 'text-base-content'"
:has-changes="hasChanges"
:icon="vaultGroup.icon ?? 'mdi:folder-outline'"
:title="vaultGroup.name ?? ''"
@back="$emit('back')"
@close="$emit('close')"
@reject="(to) => $emit('reject', to)"
@submit="(to) => $emit('submit', to)"
v-model:read_only="read_only"
>
<div class="flex flex-col gap-4 w-full p-4">
<UiInput
v-show="!read_only"
:label="t('vaultGroup.name')"
:placeholder="t('vaultGroup.name')"
:rules="vaultGroupSchema.name"
:with-copy-button="read_only"
:read_only
autofocus
v-model.trim="vaultGroup.name"
/>
<UiInput
v-show="!read_only || vaultGroup.description?.length"
:read_only
:label="t('vaultGroup.description')"
:placeholder="t('vaultGroup.description')"
:rules="vaultGroupSchema.description"
:with-copy-button="read_only"
v-model.trim="vaultGroup.description"
/>
<UiColorPicker
:read_only
:label="t('vaultGroup.color')"
:placeholder="t('vaultGroup.color')"
v-model="vaultGroup.color"
/>
<UiIconPicker
:read_only
:label="t('vaultGroup.icon')"
:placeholder="t('vaultGroup.icon')"
v-model="vaultGroup.icon"
/>
</div>
</VaultCardEdit>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
import {
vaultGroupSchema,
type SelectVaultGroup,
} from '~/database/schemas/vault';
const { t } = useI18n();
const showConfirmation = ref(false);
const vaultGroup = defineModel<SelectVaultGroup>({ required: true });
const read_only = defineModel<boolean>('read_only');
const props = defineProps({
originally: Object as PropType<SelectVaultGroup>,
});
defineEmits<{
submit: [to?: RouteLocationNormalizedLoadedGeneric];
close: [void];
back: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
const hasChanges = computed(() => {
console.log('group has changes', props.originally, vaultGroup.value);
if (!props.originally) {
if (
vaultGroup.value.color?.length ||
vaultGroup.value.description?.length ||
vaultGroup.value.icon?.length ||
vaultGroup.value.name?.length
) {
return true;
} else {
return false;
}
}
return JSON.stringify(props.originally) !== JSON.stringify(vaultGroup.value);
});
/* const onClose = () => {
if (props.originally) vaultGroup.value = { ...props.originally };
emit('close');
}; */
</script>
<i18n lang="json">
{
"de": {
"vaultGroup": {
"name": "Name",
"description": "Beschreibung",
"icon": "Icon",
"color": "Farbe"
}
},
"en": {
"vaultGroup": {
"name": "Name",
"description": "Description",
"icon": "Icon",
"color": "Color"
}
}
}
</i18n>

View File

@ -0,0 +1,42 @@
<template>
<UiListButton
v-if="entry"
:key="entry.id"
@click="navigateToEntryAsync(entry.id)"
class="text-base-content"
>
<div class="flex items-center gap-3">
<div class="w-8">
<Icon
v-if="entry.icon || groupIcon"
:name="entry.icon || groupIcon!"
/>
</div>
<div class="flex flex-col items-start">
<div v-if="!entry.title && !entry.username && !entry.url">
{{ entry.id }}
</div>
<div class="font-semibold">
{{ entry.title }}
</div>
<span class="text-sm">
{{ entry.username }}
</span>
<span class="text-sm">
{{ entry.url }}
</span>
</div>
</div>
</UiListButton>
</template>
<script setup lang="ts">
import type { SelectVaultEntry } from '~/database/schemas/vault';
defineProps({
entry: Object as PropType<SelectVaultEntry>,
groupIcon: [String, null],
});
const { navigateToEntryAsync } = useVaultEntryStore();
</script>

View File

@ -0,0 +1,7 @@
<template>
<UiList>
<slot />
</UiList>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,78 @@
<template>
<UiDialog
v-model:open="showConfirmation"
:title="t('dialog.title')"
>
{{ t('dialog.question') }}
<template #buttons>
<UiButton
class="btn-outline btn-error focus:bg-primary"
tabindex="10"
@click="$emit('reject')"
>
<Icon name="mdi:cancel" />
<span class="hidden sm:block">
{{ t('dialog.reject') }}
</span>
</UiButton>
<UiButton
class="btn-outline focus:bg-primary"
tabindex="11"
ref="abortButtonRef"
@click="showConfirmation = false"
>
<Icon name="mdi:close" />
<span class="hidden sm:block">
{{ t('dialog.abort') }}
</span>
</UiButton>
<UiButton
class="btn-outline btn-success"
tabindex="12"
@click="$emit('submit')"
>
<Icon name="mdi:check" />
<span class="hidden sm:block">
{{ t('dialog.save') }}
</span>
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
const showConfirmation = defineModel<boolean>();
const abortButtonRef = useTemplateRef('abortButtonRef');
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
onUpdated(() => {
abortButtonRef.value?.$el.focus();
});
defineEmits(['submit', 'reject']);
</script>
<i18n lang="json">
{
"de": {
"dialog": {
"title": "Ungespeicherte Änderungen",
"question": "Möchten Sie die Änderungen speichern?",
"reject": "Verwerfen",
"abort": "Abbrechen",
"save": "Speichern"
}
},
"en": {
"dialog": {
"title": "Unsaved Changes",
"question": "Would you like to save the changes?",
"reject": "Reject",
"abort": "Abort",
"save": "Save"
}
}
}
</i18n>

96
src/composables/helper.ts Normal file
View File

@ -0,0 +1,96 @@
import { H3Error } from 'h3';
export const bytesToBase64DataUrlAsync = async (
bytes: Uint8Array,
type = 'application/octet-stream'
) => {
return await new Promise((resolve, reject) => {
const reader = Object.assign(new FileReader(), {
onload: () => resolve(reader.result),
onerror: () => reject(reader.error),
});
reader.readAsDataURL(new File([new Blob([bytes])], '', { type }));
});
};
export const blobToImageAsync = (blob: Blob): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
console.log('transform blob', blob);
const url = URL.createObjectURL(blob);
let img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
resolve(img);
};
img.src = url;
});
};
export const deepToRaw = <T extends Record<string, any>>(sourceObj: T): T => {
const objectIterator = (input: any): any => {
if (Array.isArray(input)) {
return input.map((item) => objectIterator(item));
}
if (isRef(input) || isReactive(input) || isProxy(input)) {
return objectIterator(toRaw(input));
}
if (input && typeof input === 'object') {
return Object.keys(input).reduce((acc, key) => {
acc[key as keyof typeof acc] = objectIterator(input[key]);
return acc;
}, {} as T);
}
return input;
};
return objectIterator(sourceObj);
};
export const readableFileSize = (sizeInByte: number | string = 0) => {
if (!sizeInByte) {
return '0 KB';
}
const size =
typeof sizeInByte === 'string' ? parseInt(sizeInByte) : sizeInByte;
const sizeInKb = size / 1024;
const sizeInMb = sizeInKb / 1024;
const sizeInGb = sizeInMb / 1024;
const sizeInTb = sizeInGb / 1024;
if (sizeInTb > 1) return `${sizeInTb.toFixed(2)} TB`;
if (sizeInGb > 1) return `${sizeInGb.toFixed(2)} GB`;
if (sizeInMb > 1) return `${sizeInMb.toFixed(2)} MB`;
return `${sizeInKb.toFixed(2)} KB`;
};
import type { LocationQueryValue, RouteLocationRawI18n } from 'vue-router';
export const getSingleRouteParam = (
param: string | string[] | LocationQueryValue | LocationQueryValue[]
): string => {
const _param = Array.isArray(param) ? param.at(0) ?? '' : param ?? '';
//console.log('found param', _param, param);
return _param;
};
export const isRouteActive = (
to: RouteLocationRawI18n,
exact: boolean = false
) =>
computed(() => {
const found = useRouter()
.getRoutes()
.find((route) => route.name === useLocaleRoute()(to)?.name);
//console.log('found route', found, useRouter().currentRoute.value, to);
return exact
? found?.name === useRouter().currentRoute.value.name
: found?.name === useRouter().currentRoute.value.name ||
found?.children.some(
(child) => child.name === useRouter().currentRoute.value.name
);
});
export const isKey = <T extends object>(x: T, k: PropertyKey): k is keyof T => {
return k in x;
};

16
src/i18n/i18n.config.ts Normal file
View File

@ -0,0 +1,16 @@
/* import de from '@/stores/sidebar/de.json';
import en from '@/stores/sidebar/en.json'; */
export default defineI18nConfig(() => {
return {
legacy: false,
messages: {
de: {
//sidebar: de,
},
en: {
//sidebar: en,
},
},
};
});

271
src/layouts/app.vue Normal file
View File

@ -0,0 +1,271 @@
<template>
<div class="w-full h-full flex flex-col">
<nav
class="navbar bg-base-100 max-sm:rounded-box max-sm:shadow sm:border-b border-base-content/25 sm:z-20 relative"
>
<button
type="button"
class="btn btn-text max-sm:btn-square sm:hidden me-2"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
data-overlay="#sidebar"
>
<span class="icon-[tabler--menu-2] size-5"></span>
</button>
<div class="flex flex-1 items-center">
<a
class="link text-base-content link-neutral text-xl font-semibold no-underline"
href="#"
>
<UiTextGradient>Haex Hub</UiTextGradient>
</a>
</div>
<div class="navbar-end flex items-center gap-4">
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle btn btn-text btn-circle dropdown-open:bg-base-content/10 size-10"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="indicator">
<span
v-show="notifications.length"
class="indicator-item bg-error size-2 rounded-full text-sm"
></span>
<span
class="icon-[tabler--bell] text-base-content size-[1.375rem]"
></span>
</div>
</button>
<div
class="dropdown-menu dropdown-open:opacity-100 hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-scrollable"
>
<div class="dropdown-header justify-center">
<h6 class="text-base-content text-base">
{{ t('notifications.label') }}
</h6>
</div>
<div
class="vertical-scrollbar horizontal-scrollbar rounded-scrollbar text-base-content/80 max-h-56 overflow-auto max-md:max-w-60"
>
<div
class="dropdown-item"
v-for="notification in notifications"
>
<div class="avatar">
<div class="w-10 rounded-full">
<img
v-if="notification.image"
:src="notification.image"
:alt="notification.alt ?? 'notification avatar'"
/>
<Icon
v-else-if="notification.icon"
:name="notification.icon"
/>
</div>
</div>
<div class="w-60">
<h6 class="truncate text-base">
{{ notification.title }}
</h6>
<small class="text-base-content/50 truncate">
{{ notification.description }}
</small>
</div>
</div>
</div>
<a
href="#"
class="dropdown-footer justify-center gap-1"
>
<span class="icon-[tabler--eye] size-4"></span>
{{ t('notifications.view_all') }}
</a>
</div>
</div>
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle flex items-center"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="avatar">
<div class="size-9.5 rounded-full">
<img
src="https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png"
alt="avatar 1"
/>
</div>
</div>
</button>
<ul
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-avatar"
>
<li class="dropdown-header gap-2">
<div class="avatar">
<div class="w-10 rounded-full">
<img
src="https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png"
alt="avatar"
/>
</div>
</div>
<div>
<h6 class="text-base-content text-base font-semibold">
John Doe
</h6>
<small class="text-base-content/50">Admin</small>
</div>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--user]"></span>
My Profile
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--settings]"></span>
Settings
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--receipt-rupee]"></span>
Billing
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--help-triangle]"></span>
FAQs
</a>
</li>
<li class="dropdown-footer gap-2">
<button
class="btn btn-error btn-soft btn-block"
@click="onVaultCloseAsync"
>
<span class="icon-[tabler--logout]"></span>
{{ t('vault.close') }}
</button>
</li>
</ul>
</div>
</div>
</nav>
<aside
id="sidebar"
class="overlay sm:shadow-none overlay-open:translate-x-0 drawer drawer-start hidden sm:absolute max-w-14 sm:flex sm:translate-x-0 sm:pt-16 z-10"
role="dialog"
tabindex="-1"
>
<div class="drawer-body px-0 pt-4">
<ul class="menu p-0">
<!-- <li
> -->
<UiSidebarLink
v-bind="item"
v-for="item in menu"
:key="item.id"
/>
<!-- <UiTooltip
:tooltip="item.tooltip || item.name"
direction="right-start"
>
<NuxtLinkLocale
:class="{ ['bg-base-300']: isActive(item.id).value }"
:to="{
name: 'extension',
params: { extensionId: item.id, vaultId: 'test' },
}"
class="flex items-center justify-start hover:text-primary cursor-pointer tooltip-toogle"
>
<Icon
:name="item.icon"
class="size-6"
/>
<i
:class="item.icon"
class="shrink-0 size-8"
/>
</NuxtLinkLocale>
</UiTooltip> -->
<!-- </li> -->
</ul>
</div>
</aside>
<div class="overflow-hidden transition-all relative w-full">
<div class="h-full overflow-scroll sm:pl-14">
<NuxtPage />
</div>
</div>
<!-- <main class="sm:pl-14">
<NuxtPage />
</main> -->
</div>
</template>
<script setup lang="ts">
const { t } = useI18n();
const { notifications } = storeToRefs(useNotificationStore());
const { menu } = storeToRefs(useSidebarStore());
const { isActive } = useExtensionsStore();
const onExtensionSelectAsync = async (id: string) => {};
const onVaultCloseAsync = async () => {};
</script>
<i18n lang="yaml">
de:
notifications:
label: Benachrichtigungen
view_all: Alle ansehen
vault:
close: Vault schließen
en:
notifications:
label: Notifications
view_all: View all
vault:
close: Close vault
</i18n>

5
src/layouts/default.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div class="w-screen h-screen overflow-scroll bg-base-200">
<slot />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More