init commit
16
README.md
Normal 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
@ -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
@ -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
@ -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
6
public/tauri.svg
Normal 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
@ -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
@ -0,0 +1,2 @@
|
||||
DATABASE_URL=sqlite:database/vault.db
|
||||
SQLX_OFFLINE=true
|
||||
7
src-tauri/.gitignore
vendored
Normal 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
|
||||
44
src-tauri/.sqlx/query-a73e92ff12dca9b046a6440b9a68b002662b594f7f569ee71de11e00c23ca625.json
generated
Normal 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
37
src-tauri/Cargo.toml
Normal 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
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
28
src-tauri/capabilities/default.json
Normal 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"
|
||||
]
|
||||
}
|
||||
26
src-tauri/database/migrations/0000_zippy_scourge.sql
Normal 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
|
||||
);
|
||||
185
src-tauri/database/migrations/meta/0000_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
13
src-tauri/database/migrations/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1742903332283,
|
||||
"tag": "0000_zippy_scourge",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
52
src-tauri/database/schemas/vault.ts
Normal 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
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 834 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
285
src-tauri/src/browser/manager.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src-tauri/src/browser/middleware.rs
Normal 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
|
||||
}
|
||||
}
|
||||
188
src-tauri/src/browser/mod.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
160
src-tauri/src/database/core.rs
Normal 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
|
||||
}
|
||||
}
|
||||
122
src-tauri/src/database/mod.rs
Normal 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, ¶ms, &state).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sql_execute(
|
||||
sql: String,
|
||||
params: Vec<String>,
|
||||
state: State<'_, DbConnection>,
|
||||
) -> Result<String, String> {
|
||||
core::execute(&sql, ¶ms, &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))
|
||||
}
|
||||
46
src-tauri/src/extension/database/mod.rs
Normal 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, ¶ms, &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, ¶ms, &state).await
|
||||
}
|
||||
203
src-tauri/src/extension/database/permissions.rs
Normal 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)
|
||||
}
|
||||
1
src-tauri/src/extension/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod database;
|
||||
32
src-tauri/src/lib.rs
Normal 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
@ -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
@ -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
@ -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
@ -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>
|
||||
155
src/components/haex/browser/index.vue
Normal 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>
|
||||
43
src/components/haex/browser/tabBar.vue
Normal 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>
|
||||
57
src/components/haex/browser/urlBar.vue
Normal 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>
|
||||
79
src/components/ui/button/action.vue
Normal 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>
|
||||
25
src/components/ui/button/index.vue
Normal 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
@ -0,0 +1,8 @@
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
export interface IActionMenuItem {
|
||||
label: string;
|
||||
icon?: string;
|
||||
action?: () => Promise<unknown>;
|
||||
to?: RouteLocationRaw;
|
||||
}
|
||||
121
src/components/ui/dialog/index.vue
Normal 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>
|
||||
58
src/components/ui/dialog/test.vue
Normal 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>
|
||||
5
src/components/ui/dialog/title.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<h3 class="modal-title">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
199
src/components/ui/input/index.vue
Normal 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>
|
||||
54
src/components/ui/input/password.vue
Normal 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>
|
||||
56
src/components/ui/input/url.vue
Normal 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>
|
||||
91
src/components/ui/logo/itemis.vue
Normal 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>
|
||||
24
src/components/ui/sidebar/button.vue
Normal 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>
|
||||
109
src/components/ui/sidebar/index.vue
Normal 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>
|
||||
61
src/components/ui/sidebar/link.vue
Normal 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>
|
||||
268
src/components/ui/sidebar/test.vue
Normal 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>
|
||||
7
src/components/ui/text/gradient.vue
Normal 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>
|
||||
61
src/components/ui/tooltip/index.vue
Normal 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>
|
||||
165
src/components/vault/button/create.vue
Normal 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>
|
||||
179
src/components/vault/button/open.vue
Normal 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>
|
||||
7
src/components/vault/button/schema.ts
Normal 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'),
|
||||
};
|
||||
250
src/components/vault/card/edit.vue
Normal 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>
|
||||
42
src/components/vault/card/index.vue
Normal 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>
|
||||
32
src/components/vault/dialog/item/create.vue
Normal 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>
|
||||
117
src/components/vault/group/index.vue
Normal 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>
|
||||
42
src/components/vault/list/entry.vue
Normal 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>
|
||||
7
src/components/vault/list/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<UiList>
|
||||
<slot />
|
||||
</UiList>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
78
src/components/vault/modal/saveChanges.vue
Normal 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
@ -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
@ -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
@ -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
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="w-screen h-screen overflow-scroll bg-base-200">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||