commit 2c5ec6b2819d467e9a231ae65ebd31fc0867a385 Author: Martin Drechsel Date: Wed Apr 2 18:54:55 2025 +0200 init commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..5047afc --- /dev/null +++ b/README.md @@ -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 ` + + diff --git a/src/components/haex/browser/index.vue b/src/components/haex/browser/index.vue new file mode 100644 index 0000000..e88478e --- /dev/null +++ b/src/components/haex/browser/index.vue @@ -0,0 +1,155 @@ + + + diff --git a/src/components/haex/browser/tabBar.vue b/src/components/haex/browser/tabBar.vue new file mode 100644 index 0000000..bcd2b88 --- /dev/null +++ b/src/components/haex/browser/tabBar.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/haex/browser/urlBar.vue b/src/components/haex/browser/urlBar.vue new file mode 100644 index 0000000..17f93d9 --- /dev/null +++ b/src/components/haex/browser/urlBar.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/ui/button/action.vue b/src/components/ui/button/action.vue new file mode 100644 index 0000000..b414566 --- /dev/null +++ b/src/components/ui/button/action.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/components/ui/button/index.vue b/src/components/ui/button/index.vue new file mode 100644 index 0000000..b26029f --- /dev/null +++ b/src/components/ui/button/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/ui/button/types.d.ts b/src/components/ui/button/types.d.ts new file mode 100644 index 0000000..4a9fc13 --- /dev/null +++ b/src/components/ui/button/types.d.ts @@ -0,0 +1,8 @@ +import type { RouteLocationRaw } from 'vue-router'; + +export interface IActionMenuItem { + label: string; + icon?: string; + action?: () => Promise; + to?: RouteLocationRaw; +} diff --git a/src/components/ui/dialog/index.vue b/src/components/ui/dialog/index.vue new file mode 100644 index 0000000..1d24dfb --- /dev/null +++ b/src/components/ui/dialog/index.vue @@ -0,0 +1,121 @@ + + + + + +{ + "de": { + "close": "Schließen" + }, + "en": { + "close": "Close" + } +} + diff --git a/src/components/ui/dialog/test.vue b/src/components/ui/dialog/test.vue new file mode 100644 index 0000000..b37bd4b --- /dev/null +++ b/src/components/ui/dialog/test.vue @@ -0,0 +1,58 @@ + diff --git a/src/components/ui/dialog/title.vue b/src/components/ui/dialog/title.vue new file mode 100644 index 0000000..d31fa39 --- /dev/null +++ b/src/components/ui/dialog/title.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/ui/input/index.vue b/src/components/ui/input/index.vue new file mode 100644 index 0000000..4991ad5 --- /dev/null +++ b/src/components/ui/input/index.vue @@ -0,0 +1,199 @@ + + + diff --git a/src/components/ui/input/password.vue b/src/components/ui/input/password.vue new file mode 100644 index 0000000..d644880 --- /dev/null +++ b/src/components/ui/input/password.vue @@ -0,0 +1,54 @@ + + + + + +{ + "de": { + "password": "Passwort" + }, + "en": { + "password": "Password" + } +} + diff --git a/src/components/ui/input/url.vue b/src/components/ui/input/url.vue new file mode 100644 index 0000000..2e6cc95 --- /dev/null +++ b/src/components/ui/input/url.vue @@ -0,0 +1,56 @@ + + + + + +{ + "de": { + "url": "Url" + }, + "en": { + "url": "Url" + } +} + diff --git a/src/components/ui/logo/itemis.vue b/src/components/ui/logo/itemis.vue new file mode 100644 index 0000000..199194a --- /dev/null +++ b/src/components/ui/logo/itemis.vue @@ -0,0 +1,91 @@ + diff --git a/src/components/ui/sidebar/button.vue b/src/components/ui/sidebar/button.vue new file mode 100644 index 0000000..96799c5 --- /dev/null +++ b/src/components/ui/sidebar/button.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/ui/sidebar/index.vue b/src/components/ui/sidebar/index.vue new file mode 100644 index 0000000..b809892 --- /dev/null +++ b/src/components/ui/sidebar/index.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/components/ui/sidebar/link.vue b/src/components/ui/sidebar/link.vue new file mode 100644 index 0000000..74d6fb1 --- /dev/null +++ b/src/components/ui/sidebar/link.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/components/ui/sidebar/test.vue b/src/components/ui/sidebar/test.vue new file mode 100644 index 0000000..02b5deb --- /dev/null +++ b/src/components/ui/sidebar/test.vue @@ -0,0 +1,268 @@ + + + + + +de: + notifications: + label: Benachrichtigungen + view_all: Alle ansehen +en: + notifications: + label: Notifications + view_all: View all + diff --git a/src/components/ui/text/gradient.vue b/src/components/ui/text/gradient.vue new file mode 100644 index 0000000..7ea83ac --- /dev/null +++ b/src/components/ui/text/gradient.vue @@ -0,0 +1,7 @@ + diff --git a/src/components/ui/tooltip/index.vue b/src/components/ui/tooltip/index.vue new file mode 100644 index 0000000..28a173b --- /dev/null +++ b/src/components/ui/tooltip/index.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/components/vault/button/create.vue b/src/components/vault/button/create.vue new file mode 100644 index 0000000..cea0a65 --- /dev/null +++ b/src/components/vault/button/create.vue @@ -0,0 +1,165 @@ + + + + + +{ + "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" + } +} + diff --git a/src/components/vault/button/open.vue b/src/components/vault/button/open.vue new file mode 100644 index 0000000..8695722 --- /dev/null +++ b/src/components/vault/button/open.vue @@ -0,0 +1,179 @@ + + + + + +{ + "de": { + "open": "Öffnen", + "abort": "Abbrechen", + "database": { + "open": "Vault öffnen" + } + }, + + "en": { + "open": "Open", + "abort": "Abort", + "database": { + "open": "Open Vault" + } + } +} + diff --git a/src/components/vault/button/schema.ts b/src/components/vault/button/schema.ts new file mode 100644 index 0000000..03a0b52 --- /dev/null +++ b/src/components/vault/button/schema.ts @@ -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'), +}; diff --git a/src/components/vault/card/edit.vue b/src/components/vault/card/edit.vue new file mode 100644 index 0000000..89e7b81 --- /dev/null +++ b/src/components/vault/card/edit.vue @@ -0,0 +1,250 @@ + + + + + +{ + "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" + } + } +} + diff --git a/src/components/vault/card/index.vue b/src/components/vault/card/index.vue new file mode 100644 index 0000000..4dc09ba --- /dev/null +++ b/src/components/vault/card/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/vault/dialog/item/create.vue b/src/components/vault/dialog/item/create.vue new file mode 100644 index 0000000..0d4d0ba --- /dev/null +++ b/src/components/vault/dialog/item/create.vue @@ -0,0 +1,32 @@ + + + + + +{ + "de": { + "title": "Eintrag erstellen" + }, + "en": { + "title": "Create Entry" + } +} + diff --git a/src/components/vault/group/index.vue b/src/components/vault/group/index.vue new file mode 100644 index 0000000..1fe4cc6 --- /dev/null +++ b/src/components/vault/group/index.vue @@ -0,0 +1,117 @@ + + + + + +{ + "de": { + "vaultGroup": { + "name": "Name", + "description": "Beschreibung", + "icon": "Icon", + "color": "Farbe" + } + }, + "en": { + "vaultGroup": { + "name": "Name", + "description": "Description", + "icon": "Icon", + "color": "Color" + } + } +} + diff --git a/src/components/vault/list/entry.vue b/src/components/vault/list/entry.vue new file mode 100644 index 0000000..e0438ec --- /dev/null +++ b/src/components/vault/list/entry.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/vault/list/index.vue b/src/components/vault/list/index.vue new file mode 100644 index 0000000..1b036b7 --- /dev/null +++ b/src/components/vault/list/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/src/components/vault/modal/saveChanges.vue b/src/components/vault/modal/saveChanges.vue new file mode 100644 index 0000000..a97f7ed --- /dev/null +++ b/src/components/vault/modal/saveChanges.vue @@ -0,0 +1,78 @@ + + + + + +{ + "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" + } + } +} + diff --git a/src/composables/helper.ts b/src/composables/helper.ts new file mode 100644 index 0000000..2d1cfb2 --- /dev/null +++ b/src/composables/helper.ts @@ -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 => { + 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 = >(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 = (x: T, k: PropertyKey): k is keyof T => { + return k in x; +}; diff --git a/src/i18n/i18n.config.ts b/src/i18n/i18n.config.ts new file mode 100644 index 0000000..00f3f67 --- /dev/null +++ b/src/i18n/i18n.config.ts @@ -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, + }, + }, + }; +}); diff --git a/src/layouts/app.vue b/src/layouts/app.vue new file mode 100644 index 0000000..e095e6a --- /dev/null +++ b/src/layouts/app.vue @@ -0,0 +1,271 @@ + + + + + +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 + diff --git a/src/layouts/default.vue b/src/layouts/default.vue new file mode 100644 index 0000000..516fc20 --- /dev/null +++ b/src/layouts/default.vue @@ -0,0 +1,5 @@ + diff --git a/src/pages/index.vue b/src/pages/index.vue new file mode 100644 index 0000000..5ed60a1 --- /dev/null +++ b/src/pages/index.vue @@ -0,0 +1,155 @@ + + + + + +{ + "de": { + "welcome": "Viel Spass mit", + "lastUsed": "Zuletzt verwendete Vaults", + "sponsors": "Powered by" + }, + "en": { + "welcome": "Have fun with", + "lastUsed": "Last used Vaults", + "sponsors": "Powered by" + } +} + diff --git a/src/pages/test.vue b/src/pages/test.vue new file mode 100644 index 0000000..9ba44da --- /dev/null +++ b/src/pages/test.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/pages/vault.vue b/src/pages/vault.vue new file mode 100644 index 0000000..30bd29c --- /dev/null +++ b/src/pages/vault.vue @@ -0,0 +1,7 @@ + diff --git a/src/pages/vault/[vaultId]/browser.vue b/src/pages/vault/[vaultId]/browser.vue new file mode 100644 index 0000000..31b763d --- /dev/null +++ b/src/pages/vault/[vaultId]/browser.vue @@ -0,0 +1,127 @@ + + + diff --git a/src/pages/vault/[vaultId]/extensions/[extensionId].vue b/src/pages/vault/[vaultId]/extensions/[extensionId].vue new file mode 100644 index 0000000..f5a5984 --- /dev/null +++ b/src/pages/vault/[vaultId]/extensions/[extensionId].vue @@ -0,0 +1,13 @@ + + + diff --git a/src/pages/vault/[vaultId]/extensions/index.vue b/src/pages/vault/[vaultId]/extensions/index.vue new file mode 100644 index 0000000..e505b2f --- /dev/null +++ b/src/pages/vault/[vaultId]/extensions/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/pages/vault/[vaultId]/index.vue b/src/pages/vault/[vaultId]/index.vue new file mode 100644 index 0000000..d9b4880 --- /dev/null +++ b/src/pages/vault/[vaultId]/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/plugins/flyonui.client.ts b/src/plugins/flyonui.client.ts new file mode 100644 index 0000000..2d2dd3e --- /dev/null +++ b/src/plugins/flyonui.client.ts @@ -0,0 +1,13 @@ +import 'flyonui/flyonui'; +import { type IStaticMethods } from 'flyonui/flyonui'; +declare global { + interface Window { + HSStaticMethods: IStaticMethods; + } +} + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('page:finish', () => { + window.HSStaticMethods.autoInit(); + }); +}); diff --git a/src/public/favicon.ico b/src/public/favicon.ico new file mode 100644 index 0000000..18993ad Binary files /dev/null and b/src/public/favicon.ico differ diff --git a/src/public/itemis.svg b/src/public/itemis.svg new file mode 100644 index 0000000..e1eb942 --- /dev/null +++ b/src/public/itemis.svg @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/src/public/logo.svg b/src/public/logo.svg new file mode 100644 index 0000000..4048008 --- /dev/null +++ b/src/public/logo.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/public/logo_square.svg b/src/public/logo_square.svg new file mode 100644 index 0000000..1bf1568 --- /dev/null +++ b/src/public/logo_square.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/public/robots.txt b/src/public/robots.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/public/robots.txt @@ -0,0 +1 @@ + diff --git a/src/stores/browser/extensions.ts b/src/stores/browser/extensions.ts new file mode 100644 index 0000000..000a9d7 --- /dev/null +++ b/src/stores/browser/extensions.ts @@ -0,0 +1,90 @@ +export interface ResourceRequestDetails { + url: string; + resourceType: string; + tabId?: string; + frameId?: number; +} + +export interface ResourceRequestResult { + cancel: boolean; + redirectUrl?: string; +} + +export interface ContentScript { + code: string; + matches?: string[]; + runAt?: 'document_start' | 'document_end' | 'document_idle'; +} + +export interface Extension { + id: string; + name: string; + version: string; + description?: string; + processNavigation?: (url: string) => boolean; + processResourceRequest?: ( + details: ResourceRequestDetails + ) => ResourceRequestResult; + contentScripts?: ContentScript[]; +} + +export const useBrowserExtensionStore = defineStore( + 'useBrowserExtensionStore', + () => { + const extensions = ref([]); + const isInitialized = ref(false); + + return { + extensions, + isInitialized, + initializeAsync, + processNavigation, + injectContentScripts, + }; + } +); + +const initializeAsync = async () => { + const { isInitialized } = storeToRefs(useBrowserExtensionStore()); + if (isInitialized.value) return; + + // Lade Erweiterungen aus dem Erweiterungsverzeichnis + try { + const extensions = await loadExtensionsAsync(); + for (const extension of extensions) { + registerExtension(extension); + } + + isInitialized.value = true; + console.log(`${extensions.length} Erweiterungen geladen`); + } catch (error) { + console.error('Fehler beim Laden der Erweiterungen:', error); + } +}; + +const loadExtensionsAsync = async (): Promise => { + // In einer realen Implementierung würden Sie hier Erweiterungen aus einem Verzeichnis laden + // Für dieses Beispiel verwenden wir hartcodierte Erweiterungen + /* const adBlocker = (await import('./ad-blocker')).default; + const trackerBlocker = (await import('./tracker-blocker')).default; */ + + return []; +}; + +const registerExtension = (extension: Extension): boolean => { + const { extensions } = storeToRefs(useBrowserExtensionStore()); + if (!extension.id || !extension.name) { + console.error('Ungültige Erweiterung:', extension); + return false; + } + + console.log(`Erweiterung registriert: ${extension.name}`); + extensions.value.push(extension); + return true; +}; + +const processNavigation = (url: string) => { + return true; +}; + +const injectContentScripts = (t: string) => {}; diff --git a/src/stores/browser/index.ts b/src/stores/browser/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/stores/extensions/index.ts b/src/stores/extensions/index.ts new file mode 100644 index 0000000..d6107c6 --- /dev/null +++ b/src/stores/extensions/index.ts @@ -0,0 +1,57 @@ +import { getSingleRouteParam } from '~/composables/helper'; +import type { RouteLocationRaw } from 'vue-router'; + +export interface IExtensionLink { + name: string; + icon: string; + tooltip?: string; + id: string; +} + +export const useExtensionsStore = defineStore('extensionsStore', () => { + const extensions = ref([ + { + id: 'haex-browser', + name: 'Haex Browser', + icon: 'solar:global-outline', + }, + + { + id: 'extensions', + name: 'sidebar.extensions', + icon: 'gg:extension', + }, + + { + id: 'settings', + name: 'sidebar.settings', + icon: 'ph:gear-six', + }, + ]); + + const currentRoute = useRouter().currentRoute.value; + + const isActive = (id: string) => + computed( + () => + currentRoute.name === 'extension' && + currentRoute.params.extensionId === id + ); + + const loadAsync = async (id: string) => { + extensions.value.some(async (extension) => { + if (extension.id === id) { + await navigateTo( + useLocalePath()({ name: 'extension', params: { extensionId: id } }) + ); + } else { + } + }); + }; + + return { + extensions, + loadAsync, + isActive, + }; +}); diff --git a/src/stores/ui/de.json b/src/stores/ui/de.json new file mode 100644 index 0000000..20fdf3e --- /dev/null +++ b/src/stores/ui/de.json @@ -0,0 +1,5 @@ +{ + "light": "Hell", + "dark": "Dunkel", + "soft": "Soft" +} diff --git a/src/stores/ui/en.json b/src/stores/ui/en.json new file mode 100644 index 0000000..d1adc1c --- /dev/null +++ b/src/stores/ui/en.json @@ -0,0 +1,5 @@ +{ + "light": "Light", + "dark": "Dark", + "soft": "Soft" +} diff --git a/src/stores/ui/index.ts b/src/stores/ui/index.ts new file mode 100644 index 0000000..0bc5480 --- /dev/null +++ b/src/stores/ui/index.ts @@ -0,0 +1,41 @@ +import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'; +import de from './de.json'; +import en from './en.json'; + +export const useUiStore = defineStore('uiStore', () => { + const breakpoints = useBreakpoints(breakpointsTailwind); + + const currentScreenSize = computed(() => + breakpoints.active().value.length > 0 ? breakpoints.active().value : 'xs' + ); + + const { t } = useI18n({ + messages: { + de: { ui: de }, + en: { ui: en }, + }, + }); + + const availableThemes = ref([ + { + value: 'dark', + name: t('ui.dark'), + icon: 'line-md:moon-rising-alt-loop', + }, + { + value: 'light', + name: t('ui.light'), + icon: 'line-md:moon-to-sunny-outline-loop-transition', + }, + { value: 'soft', name: t('ui.soft'), icon: 'line-md:paint-drop' }, + ]); + + const currentTheme = ref(availableThemes.value[0].value); + + return { + breakpoints, + currentScreenSize, + currentTheme, + availableThemes, + }; +}); diff --git a/src/stores/ui/notifications.ts b/src/stores/ui/notifications.ts new file mode 100644 index 0000000..219a190 --- /dev/null +++ b/src/stores/ui/notifications.ts @@ -0,0 +1,22 @@ +export interface IHaexNotication { + title: string; + description?: string; + icon?: string; + image?: string; + alt?: string; +} + +export const useNotificationStore = defineStore('notificationStore', () => { + const notifications = ref([ + { + title: 'huhu', + alt: 'test', + description: 'Ganz was tolles', + image: 'https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png', + }, + ]); + + return { + notifications, + }; +}); diff --git a/src/stores/ui/sidebar.ts b/src/stores/ui/sidebar.ts new file mode 100644 index 0000000..be74a95 --- /dev/null +++ b/src/stores/ui/sidebar.ts @@ -0,0 +1,44 @@ +import { getSingleRouteParam } from '~/composables/helper'; +import type { RouteLocationRaw } from 'vue-router'; + +export interface ISidebarItem { + name: string; + icon: string; + tooltip?: string; + id: string; + type: 'browser' | 'extension'; +} + +export const useSidebarStore = defineStore('sidebarStore', () => { + const menu = ref([ + { + id: 'haex-browser', + name: 'Haex Browser', + icon: 'solar:global-outline', + type: 'browser', + }, + + { + id: 'haex-vault', + name: 'Haex Vault', + icon: 'gg:extension', + type: 'extension', + }, + ]); + + /* const loadAsync = async (id: string) => { + extensions.value.some(async (extension) => { + if (extension.id === id) { + await navigateTo( + useLocalePath()({ name: 'extension', params: { extensionId: id } }) + ); + } else { + } + }); + }; */ + + return { + menu, + //loadAsync, + }; +}); diff --git a/src/stores/vault/index.ts b/src/stores/vault/index.ts new file mode 100644 index 0000000..dda1da9 --- /dev/null +++ b/src/stores/vault/index.ts @@ -0,0 +1,231 @@ +//import Database from '@tauri-apps/plugin-sql'; +import { drizzle, SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; +//import Database from "tauri-plugin-sql-api"; +import * as schema from '@/../src-tauri/database/schemas/vault'; + +import { invoke } from '@tauri-apps/api/core'; +import { count } from 'drizzle-orm'; +import { platform } from '@tauri-apps/plugin-os'; + +interface IVault { + //database: Database; + path: string; + password: string; + name: string; + drizzle: SqliteRemoteDatabase; +} +interface IOpenVaults { + [vaultPath: string]: IVault; +} + +export const useVaultStore = defineStore('vaultStore', () => { + const currentVaultId = computed({ + get: () => + getSingleRouteParam(useRouter().currentRoute.value.params.vaultId), + set: (newVaultId) => { + useRouter().currentRoute.value.params.vaultId = newVaultId ?? ''; + }, + }); + + const read_only = computed({ + get: () => { + console.log( + 'query showSidebar', + useRouter().currentRoute.value.query.readonly + ); + return JSON.parse( + getSingleRouteParam(useRouter().currentRoute.value.query.readonly) || + 'false' + ); + }, + set: (readonly) => { + const router = useRouter(); + router.replace({ + query: { + ...router.currentRoute.value.query, + readonly: JSON.stringify(readonly ? true : false), + }, + }); + }, + }); + + const openVaults = ref(); + + const currentVault = ref(); + + /* computed(() => { + console.log('compute currentVault', currentVaultId.value, openVaults.value); + return openVaults.value?.[currentVaultId.value ?? '']; + }); */ + + watch( + currentVaultId, + async () => { + /* if (!openVaults.value?.[currentVaultId.value ?? '']) { + console.log( + 'no vaultId', + currentVault.value, + openVaults.value?.[currentVaultId.value ?? ''] + ); + return await navigateTo(useLocalePath()({ name: 'vaultOpen' })); + } else */ + currentVault.value = openVaults.value?.[currentVaultId.value ?? '']; + }, + { immediate: true } + ); + + const openAsync = async ({ + path = '', + password, + }: { + path: string; + password: string; + }) => { + //const sqlitePath = path?.startsWith('sqlite:') ? path : `sqlite:${path}`; + + console.log('try to open db', path, password); + + const result = await invoke('open_encrypted_database', { + path, + key: password, + }); + + console.log('open vault from store', result); + if (!(await testDatabaseReadAsync())) throw new Error('Passwort falsch'); + //const db = await Database.load(sqlitePath); + + const vaultId = await getVaultIdAsync(path); + const seperator = platform() === 'windows' ? '\\' : '/'; + const fileName = path.split(seperator).pop(); + console.log('opened db fileName', fileName, vaultId); + + openVaults.value = { + ...openVaults.value, + [vaultId]: { + //database: db, + path, + password, + name: fileName ?? path, + drizzle: drizzle( + async (sql, params, method) => { + let rows: any = []; + let results = []; + + // If the query is a SELECT, use the select method + if (isSelectQuery(sql)) { + rows = await invoke('db_select', { sql, params }).catch((e) => { + console.error('SQL Error:', e); + return []; + }); + } else { + // Otherwise, use the execute method + rows = await invoke('db_execute', { sql, params }).catch((e) => { + console.error('SQL Error:', e); + return []; + }); + return { rows: [] }; + } + + rows = rows.map((row: any) => { + return Object.values(row); + }); + + // If the method is "all", return all rows + results = method === 'all' ? rows : rows[0]; + + return { rows: results }; + }, + // Pass the schema to the drizzle instance + { schema: schema, logger: true } + ), + }, + }; + + const { addVaultAsync } = useLastVaultStore(); + await addVaultAsync({ path }); + + return vaultId; + }; + + const testDatabaseReadAsync = async () => { + try { + currentVault.value?.drizzle + .select({ count: count() }) + .from(schema.haexExtensions); + return true; + } catch (error) { + return false; + } + }; + + const refreshDatabaseAsync = async () => { + console.log('refreshDatabaseAsync'); + /* if (!currentVault.value?.database.close) { + return navigateTo(useLocaleRoute()({ name: 'vaultOpen' })); + } */ + }; + + const createAsync = async ({ + path, + password, + }: { + path: string; + password: string; + }) => { + /* const existDb = await exists('default.db', { + baseDir: BaseDirectory.Resource, + }); */ + + /* const existDb = await resolveResource('resources/default.db'); + if (!existDb) throw new Error('Keine Datenbank da'); + await copyFile(existDb, path); */ + const result = await invoke('create_encrypted_database', { + path, + key: password, + }); + console.log('create_encrypted_database', result); + return 'aaaaa'; //await openAsync({ path, password }); + }; + + const closeAsync = async () => { + if (!currentVaultId.value) return; + + /* if ( + typeof openVaults.value?.[currentVaultId.value]?.database?.close === + 'function' + ) { + console.log('close db', openVaults.value?.[currentVaultId.value]); + return openVaults.value?.[currentVaultId.value]?.database?.close(); + } */ + delete openVaults.value?.[currentVaultId.value]; + }; + + return { + closeAsync, + createAsync, + currentVault, + currentVaultId, + openAsync, + openVaults, + refreshDatabaseAsync, + read_only, + }; +}); + +const getVaultIdAsync = async (path: string) => { + const encoder = new TextEncoder(); + const data = encoder.encode(path); + + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); // convert bytes to hex string + console.log('vaultId', hashHex); + return hashHex; +}; + +function isSelectQuery(sql: string): boolean { + const selectRegex = /^\s*SELECT\b/i; + return selectRegex.test(sql); +} diff --git a/src/stores/vault/lastVaults.ts b/src/stores/vault/lastVaults.ts new file mode 100644 index 0000000..c55f413 --- /dev/null +++ b/src/stores/vault/lastVaults.ts @@ -0,0 +1,80 @@ +import { load, Store } from '@tauri-apps/plugin-store'; + +interface ILastVault { + lastUsed: Date; + name: string; + path: string; +} + +export const useLastVaultStore = defineStore('lastVaultStore', () => { + const { + public: { haexVault }, + } = useRuntimeConfig(); + + const lastVaults = ref([]); + + const keyName = 'lastVaults'; + + const getStoreAsync = async () => { + return await load(haexVault.lastVaultFileName); + }; + + const syncLastVaultsAsync = async () => { + const store = await getStoreAsync(); + lastVaults.value = + (await store.get(keyName))?.sort( + (a, b) => +new Date(b.lastUsed) - +new Date(a.lastUsed) + ) ?? []; + + return lastVaults.value; + }; + + const addVaultAsync = async ({ + name, + path, + }: { + name?: string; + path: string; + }) => { + if (!lastVaults.value) await syncLastVaultsAsync(); + + const saveName = name || getFileNameFromPath(path); + lastVaults.value = lastVaults.value.filter((vault) => vault.path !== path); + lastVaults.value.push({ lastUsed: new Date(), name: saveName, path }); + await saveLastVaultsAsync(); + }; + + const removeVaultAsync = async (vaultPath: string) => { + console.log('remove', vaultPath, lastVaults.value); + lastVaults.value = lastVaults.value.filter( + (vault) => vault.path !== vaultPath + ); + await saveLastVaultsAsync(); + }; + + const saveLastVaultsAsync = async () => { + const store = await getStoreAsync(); + console.log('save lastVaults', keyName, lastVaults.value); + await store.set(keyName, lastVaults.value); + await syncLastVaultsAsync(); + }; + + return { + addVaultAsync, + syncLastVaultsAsync, + lastVaults, + removeVaultAsync, + saveLastVaultsAsync, + }; +}); + +const getFileNameFromPath = (path: string) => { + const lastBackslashIndex = path.lastIndexOf('\\'); + const lastSlashIndex = path.lastIndexOf('/'); + + const lastIndex = Math.max(lastBackslashIndex, lastSlashIndex); + + const fileName = path.substring(lastIndex + 1); + + return fileName; +}; diff --git a/src/utils/webview-bridge.js b/src/utils/webview-bridge.js new file mode 100644 index 0000000..d1c4b22 --- /dev/null +++ b/src/utils/webview-bridge.js @@ -0,0 +1,60 @@ +(function () { + // Überwache das Erstellen von Elementen, die Ressourcen laden + const originalCreateElement = document.createElement; + document.createElement = function (tagName) { + const element = originalCreateElement.call(document, tagName); + + if ( + tagName.toLowerCase() === 'img' || + tagName.toLowerCase() === 'script' || + tagName.toLowerCase() === 'iframe' + ) { + // Überwache das Setzen des src-Attributs + const originalSetAttribute = element.setAttribute; + element.setAttribute = function (name, value) { + if (name === 'src') { + // Prüfe, ob die Ressource blockiert werden soll + window.__TAURI__ + .invoke('block_resource_request', { + url: value, + resourceType: tagName.toLowerCase(), + }) + .then((shouldBlock) => { + if (shouldBlock) { + console.log(`Ressourcenanfrage blockiert: ${value}`); + return; + } + originalSetAttribute.call(element, name, value); + }); + } else { + originalSetAttribute.call(element, name, value); + } + }; + } + + return element; + }; + + // Wenn die Tauri HTTP API verwendet wird, können wir sie hier überwachen + if (window.__TAURI__ && window.__TAURI__.http) { + const originalFetch = window.__TAURI__.http.fetch; + window.__TAURI__.http.fetch = async function (options) { + // Prüfe, ob die Ressource blockiert werden soll + const shouldBlock = await window.__TAURI__.invoke( + 'block_resource_request', + { + url: options.url, + resourceType: 'tauri-fetch', + } + ); + + if (shouldBlock) { + throw new Error(`Ressourcenanfrage blockiert: ${options.url}`); + } + + return originalFetch.call(this, options); + }; + } + + console.log('Webview-Bridge geladen'); +})(); diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..4d981c7 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,148 @@ +import { defineConfig } from '@nuxtjs/tailwindcss/config'; +//import { iconsPlugin, dynamicIconsPlugin } from '@egoist/tailwindcss-icons'; +//import colors from 'tailwindcss/colors'; +import themes from 'flyonui/src/theming/themes'; +//import * as tailwindMotion from 'tailwindcss-motion'; +import { addDynamicIconSelectors } from '@iconify/tailwind'; + +export default defineConfig({ + content: ['./src/**/*.{vue,ts,svg}', './node_modules/flyonui/dist/js/*.js'], + darkMode: 'selector', + plugins: [ + /* iconsPlugin(), + dynamicIconsPlugin(), */ + addDynamicIconSelectors(), + require('flyonui'), + require('flyonui/plugin'), + //tailwindMotion, + ], + + flyonui: { + themes: [ + { + light: { + ...themes.light, + /* primary: colors.teal[500], + secondary: colors.purple[500], */ + }, + soft: { + ...themes.soft, + /* primary: colors.teal[500], + secondary: colors.purple[500], */ + }, + dark: { + ...themes.dark, + /* primary: colors.cyan[700], //colors.teal[600], + secondary: colors.purple[500], */ + /* 'primary-content': '#000516', + 'secondary': '#008f9c', + 'secondary-content': '#000709', + 'accent': '#007f7a', + 'accent-content': '#d3e5e3', + 'neutral': '#321a15', + 'neutral-content': '#d3ccca', + 'base-100': '#002732', + 'base-200': '#00202a', + 'base-300': '#001a22', + 'base-content': '#c8cfd2', + 'info': '#0086b2', + 'info-content': '#00060c', + 'success': '#a5da00', + 'success-content': '#0a1100', + 'warning': '#ff8d00', + 'warning-content': '#160700', + 'error': '#c83849', + 'error-content': '#f9d9d9', */ + }, + }, + ], + /* themes: [ + { + dark: { + 'primary': colors.teal[500], + 'primary-content': '#010811', + 'secondary': colors.purple[500], + 'secondary-content': '#130201', + 'accent': '#9b59b6', + 'accent-content': '#ebddf1', + 'neutral': '#95a5a6', + 'neutral-content': '#080a0a', + 'base-100': colors.slate[100], + 'base-200': colors.slate[400], + 'base-300': colors.slate[900], + 'base-content': colors.slate[800], + 'info': '#1abc9c', + 'info-content': '#000d09', + 'success': '#2ecc71', + 'success-content': '#010f04', + 'warning': '#f1c40f', + 'warning-content': '#140e00', + 'error': '#e74c3c', + 'error-content': '#130201', + }, + + light: { + 'primary': colors.teal[500], + 'primary-content': '#010811', + 'secondary': colors.purple[500], + 'secondary-content': '#130201', + 'accent': '#9b59b6', + 'accent-content': '#ebddf1', + 'neutral': '#95a5a6', + 'neutral-content': '#080a0a', + 'base-100': '#ecf0f1', + 'base-200': '#cdd1d2', + 'base-300': '#afb2b3', + 'base-content': '#131414', + 'info': '#1abc9c', + 'info-content': '#000d09', + 'success': '#2ecc71', + 'success-content': '#010f04', + 'warning': '#f1c40f', + 'warning-content': '#140e00', + 'error': '#e74c3c', + 'error-content': '#130201', + }, + }, + ], */ + }, + + /* theme: { + extend: { + colors: { + 'primary-active': colors.teal[600], + 'primary-focus': colors.teal[400], + 'primary-hover': colors.teal[400], + 'primary': colors.teal[500], + + 'dark': { + 'primary-active': colors.teal[700], + 'primary-focus': colors.teal[600], + 'primary-hover': colors.teal[600], + 'primary': colors.teal[500], + }, + 'secondary': colors.sky[500], + }, + + fontFamily: { + sans: [ + 'Adelle', + 'Roboto Slab', + 'DejaVu Serif', + 'Georgia', + 'Graphik', + 'sans-serif', + ], + serif: ['Merriweather', 'serif'], + }, + + screens: { + xs: '360px', + }, + + transitionProperty: { + height: 'height', + }, + }, + }, */ +}); // satisfies Config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a746f2a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +}