refactore manifest and permission

This commit is contained in:
2025-10-02 01:42:30 +02:00
parent 56e75977cd
commit fb577a8699
51 changed files with 5634 additions and 2086 deletions

View File

@ -1,22 +1,44 @@
import { invoke } from '@tauri-apps/api/core'
import { appDataDir, join } from '@tauri-apps/api/path'
import { exists, readDir, readTextFile, remove } from '@tauri-apps/plugin-fs'
import { and, eq } from 'drizzle-orm'
import type {
IHaexHubExtension,
IHaexHubExtensionLink,
IHaexHubExtensionManifest,
} from '~/types/haexhub'
import { haexExtensions } from '~~/src-tauri/database/schemas/vault'
const manifestFileName = 'manifest.json'
const logoFileName = 'icon.svg'
interface ExtensionInfoResponse {
key_hash: string
name: string
full_id: string
version: string
display_name: string | null
namespace: string | null
allowed_origin: string
}
/* const manifestFileName = 'manifest.json'
const logoFileName = 'icon.svg' */
export const useExtensionsStore = defineStore('extensionsStore', () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([])
const { addNotificationAsync } = useNotificationStore()
const availableExtensions = ref<IHaexHubExtension[]>([])
const currentRoute = useRouter().currentRoute
const extensionLinks = computed<ISidebarItem[]>(() =>
const currentExtensionId = computed(() =>
getSingleRouteParam(currentRoute.value.params.extensionId),
)
const currentExtension = computed(() => {
if (!currentExtensionId.value) return null
return (
availableExtensions.value.find(
(ext) => ext.id === currentExtensionId.value,
) ?? null
)
})
/* const { addNotificationAsync } = useNotificationStore() */
/* const extensionLinks = computed<ISidebarItem[]>(() =>
availableExtensions.value
.filter((extension) => extension.enabled && extension.installed)
.map((extension) => ({
@ -26,9 +48,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
tooltip: extension.name ?? '',
to: { name: 'haexExtension', params: { extensionId: extension.id } },
})),
)
const currentRoute = useRouter().currentRoute
) */
const isActive = (id: string) =>
computed(
@ -37,32 +57,26 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
currentRoute.value.params.extensionId === id,
)
const currentExtension = computed(() => {
console.log('computed currentExtension', currentRoute.value.params)
if (currentRoute.value.meta.name !== 'haexExtension') return
const extensionEntry = computed(() => {
if (!currentExtension.value?.version || !currentExtension.value?.id)
return null
const extensionId = getSingleRouteParam(
currentRoute.value.params.extensionId,
const encodedInfo = encodeExtensionInfo(
currentExtension.value.id,
currentExtension.value.version,
)
console.log('extensionId from param', extensionId)
if (!extensionId) return
const extension = availableExtensions.value.find(
(extension) => extension.id === extensionId,
)
console.log('currentExtension', extension)
return extension
return `extension://${encodedInfo}`
})
const getExtensionPathAsync = async (
/* const getExtensionPathAsync = async (
extensionId?: string,
version?: string,
) => {
if (!extensionId || !version) return ''
return await join(await appDataDir(), 'extensions', extensionId, version)
}
} */
const checkSourceExtensionDirectoryAsync = async (
/* const checkSourceExtensionDirectoryAsync = async (
extensionDirectory: string,
) => {
try {
@ -82,22 +96,154 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
//throw error //new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
}
} */
const loadExtensionsAsync = async () => {
try {
const extensions =
await invoke<ExtensionInfoResponse[]>('get_all_extensions')
availableExtensions.value = extensions.map((ext) => ({
id: ext.key_hash,
name: ext.display_name || ext.name,
version: ext.version,
author: ext.namespace,
icon: null,
enabled: true,
}))
} catch (error) {
console.error('Fehler beim Laden der Extensions:', error)
throw error
}
}
const isExtensionInstalledAsync = async (
extension: Partial<IHaexHubExtension>,
) => {
/* const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore())
const extensions =
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(
extensions,
isExtensionInstalledAsync,
)
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
availableExtensions.value =
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? '',
icon: extension.icon ?? '',
author: extension.author ?? '',
version: extension.version ?? '',
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? []
console.log('loadExtensionsAsync', availableExtensions.value)
return true
} */
const installAsync = async (sourcePath: string | null) => {
if (!sourcePath) throw new Error('Kein Pfad angegeben')
try {
const extensionPath = await getExtensionPathAsync(
extension.id,
`${extension.version}`,
)
console.log(
`extension ${extension.id} is installed ${await exists(extensionPath)}`,
)
return await exists(extensionPath)
const extensionId = await invoke<string>('install_extension', {
sourcePath,
})
return extensionId
} catch (error) {
console.error(error)
console.error('Fehler bei Extension-Installation:', error)
throw error
}
}
/* const installAsync = async (extensionDirectory: string | null) => {
try {
if (!extensionDirectory)
throw new Error('Kein Ordner für Erweiterung angegeben')
const manifestPath = await join(extensionDirectory, manifestFileName)
const manifest = (await JSON.parse(
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
const destination = await getExtensionPathAsync(
manifest.id,
manifest.version,
)
await checkSourceExtensionDirectoryAsync(extensionDirectory)
await invoke('copy_directory', {
source: extensionDirectory,
destination,
})
const logoFilePath = await join(destination, logoFileName)
const logo = await readTextFile(logoFilePath)
const { currentVault } = storeToRefs(useVaultStore())
const res = await currentVault.value?.drizzle
.insert(haexExtensions)
.values({
id: manifest.id,
name: manifest.name,
author: manifest.author,
enabled: true,
url: manifest.url,
version: manifest.version,
icon: logo,
})
console.log('insert extensions', res)
addNotificationAsync({
type: 'success',
text: `${manifest.name} wurde installiert`,
})
} catch (error) {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
throw error
}
} */
const removeExtensionAsync = async (extensionId: string, version: string) => {
try {
await invoke('remove_extension', {
extensionId,
extensionVersion: version,
})
} catch (error) {
console.error('Fehler beim Entfernen der Extension:', error)
throw error
}
}
/* const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log('remove extension', id, version)
await removeExtensionFromVaultAsync(id, version)
await removeExtensionFilesAsync(id, version)
} catch (error) {
throw new Error(JSON.stringify(error))
}
} */
const isExtensionInstalledAsync = async ({
id,
version,
}: {
id: string
version: string
}) => {
try {
return await invoke<boolean>('is_extension_installed', {
extensionId: id,
extensionVersion: version,
})
} catch (error) {
console.error('Fehler beim Prüfen der Extension:', error)
return false
}
}
@ -156,7 +302,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
return true
}
const readManifestFileAsync = async (
/* const readManifestFileAsync = async (
extensionId: string,
version: string,
) => {
@ -173,173 +319,17 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
/*
TODO implement check, that manifest has valid data
*/
return manifest
} catch (error) {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
console.error('ERROR readManifestFileAsync', error)
}
}
} */
const installAsync = async (extensionDirectory: string | null) => {
try {
if (!extensionDirectory)
throw new Error('Kein Ordner für Erweiterung angegeben')
const manifestPath = await join(extensionDirectory, manifestFileName)
const manifest = (await JSON.parse(
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
const destination = await getExtensionPathAsync(
manifest.id,
manifest.version,
)
await checkSourceExtensionDirectoryAsync(extensionDirectory)
await invoke('copy_directory', {
source: extensionDirectory,
destination,
})
const logoFilePath = await join(destination, logoFileName)
const logo = await readTextFile(logoFilePath)
const { currentVault } = storeToRefs(useVaultStore())
const res = await currentVault.value?.drizzle
.insert(haexExtensions)
.values({
id: manifest.id,
name: manifest.name,
author: manifest.author,
enabled: true,
url: manifest.url,
version: manifest.version,
icon: logo,
})
console.log('insert extensions', res)
addNotificationAsync({
type: 'success',
text: `${manifest.name} wurde installiert`,
})
} catch (error) {
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
throw error
/*
const resourcePath = await resourceDir();
//const manifestPath = await join(extensionDirectory, 'manifest.json');
const manifestPath = await join(
resourcePath,
'extension',
'demo-addon',
'manifest.json'
);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
let htmlContent = await readTextFile(
await join(resourcePath, 'extension', 'demo-addon', 'index.html')
);
const replacements = [];
let match;
while ((match = regex.exec(htmlContent)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith('http')) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const fileContent = await readTextFile(
await join(resourcePath, 'extension', 'demo-addon', resource)
);
const blob = new Blob([fileContent], { type: getMimeType(resource) });
const blobUrl = URL.createObjectURL(blob);
console.log('blob', resource, blobUrl);
htmlContent = htmlContent.replace(
match,
`${prefix}${blobUrl}${suffix}`
);
}
console.log('htmlContent', htmlContent);
const blob = new Blob([htmlContent], { type: 'text/html' });
const iframeSrc = URL.createObjectURL(blob);
const manifestContent = await readTextFile(manifestPath);
console.log('iframeSrc', iframeSrc);
const manifest: PluginManifest = JSON.parse(manifestContent);
//const entryPath = await join(extensionDirectory, manifest.entry);
const entryPath = await join(
resourcePath,
'extension',
'demo-addon',
manifest.entry
);
console.log('extensionDirectory', extensionDirectory, entryPath);
const path = convertFileSrc(extensionDirectory, manifest.entry);
console.log('final path', path);
manifest.entry = iframeSrc;
/* await join(
path, //`file:/${extensionDirectory}`,
manifest.entry
); */
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
manifestPath,
}); */
/* const iframe = document.createElement('iframe');
iframe.src = manifest.entry;
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none'; */
/* const addonApi = {
db_execute: async (sql: string, params: string[] = []) => {
return invoke('db_execute', {
addonId: manifest.name,
sql,
params,
});
},
db_select: async (sql: string, params: string[] = []) => {
return invoke('db_select', {
addonId: manifest.name,
sql,
params,
});
},
}; */
/* iframe.onload = () => {
iframe.contentWindow?.postMessage(
{ type: 'init', payload: addonApi },
'*'
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
}
}
const extensionEntry = computedAsync(
/* const extensionEntry = computedAsync(
async () => {
try {
/* console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g; */
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log('extension id or entry missing', currentExtension.value)
@ -375,60 +365,19 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
},
null,
{ lazy: true },
)
const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore())
const extensions =
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(
extensions,
isExtensionInstalledAsync,
)
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
availableExtensions.value =
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? '',
icon: extension.icon ?? '',
author: extension.author ?? '',
version: extension.version ?? '',
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? []
console.log('loadExtensionsAsync', availableExtensions.value)
return true
}
const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log('remove extension', id, version)
await removeExtensionFromVaultAsync(id, version)
await removeExtensionFilesAsync(id, version)
} catch (error) {
throw new Error(JSON.stringify(error))
}
}
) */
return {
availableExtensions,
checkManifest,
currentExtension,
currentExtensionId,
extensionEntry,
extensionLinks,
installAsync,
isActive,
isExtensionInstalledAsync,
loadExtensionsAsync,
readManifestFileAsync,
removeExtensionAsync,
getExtensionPathAsync,
}
})
@ -438,7 +387,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
return 'text/plain'
} */
const removeExtensionFromVaultAsync = async (
/* const removeExtensionFromVaultAsync = async (
id: string | null,
version: string | null,
) => {
@ -457,9 +406,9 @@ const removeExtensionFromVaultAsync = async (
.delete(haexExtensions)
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
return removedExtensions
}
} */
const removeExtensionFilesAsync = async (
/* const removeExtensionFilesAsync = async (
id: string | null,
version: string | null,
) => {
@ -483,4 +432,13 @@ const removeExtensionFilesAsync = async (
console.error('ERROR removeExtensionFilesAsync', error)
throw new Error(JSON.stringify(error))
}
} */
function encodeExtensionInfo(id: string, version: string): string {
const info = { id, version }
const jsonString = JSON.stringify(info)
const bytes = new TextEncoder().encode(jsonString)
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}

View File

@ -0,0 +1,143 @@
// stores/extensions/tabs.ts
import type { IHaexHubExtension } from '~/types/haexhub'
interface ExtensionTab {
extension: IHaexHubExtension
iframe: HTMLIFrameElement | null
isVisible: boolean
lastAccessed: number
}
export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
// State
const openTabs = ref(new Map<string, ExtensionTab>())
const activeTabId = ref<string | null>(null)
// Getters
const activeTab = computed(() => {
if (!activeTabId.value) return null
return openTabs.value.get(activeTabId.value) || null
})
const tabCount = computed(() => openTabs.value.size)
const sortedTabs = computed(() => {
return Array.from(openTabs.value.values()).sort(
(a, b) => b.lastAccessed - a.lastAccessed,
)
})
// Actions
const openTab = (extensionId: string) => {
// Hole Extension-Info aus dem anderen Store
const extensionsStore = useExtensionsStore()
const extension = extensionsStore.availableExtensions.find(
(ext) => ext.id === extensionId,
)
if (!extension) {
console.error(`Extension ${extensionId} nicht gefunden`)
return
}
// Bereits geöffnet? Nur aktivieren
if (openTabs.value.has(extensionId)) {
setActiveTab(extensionId)
return
}
// Limit: Max 10 Tabs
if (openTabs.value.size >= 10) {
const oldestInactive = sortedTabs.value
.filter((tab) => tab.extension.id !== activeTabId.value)
.pop()
if (oldestInactive) {
closeTab(oldestInactive.extension.id)
}
}
// Neuen Tab erstellen
openTabs.value.set(extensionId, {
extension,
iframe: null,
isVisible: false,
lastAccessed: Date.now(),
})
setActiveTab(extensionId)
}
const setActiveTab = (extensionId: string) => {
// Verstecke aktuellen Tab
if (activeTabId.value && openTabs.value.has(activeTabId.value)) {
const currentTab = openTabs.value.get(activeTabId.value)!
currentTab.isVisible = false
}
// Zeige neuen Tab
const newTab = openTabs.value.get(extensionId)
if (newTab) {
newTab.isVisible = true
newTab.lastAccessed = Date.now()
activeTabId.value = extensionId
}
}
const closeTab = (extensionId: string) => {
const tab = openTabs.value.get(extensionId)
if (!tab) return
// IFrame entfernen
tab.iframe?.remove()
openTabs.value.delete(extensionId)
// Nächsten Tab aktivieren
if (activeTabId.value === extensionId) {
const remaining = sortedTabs.value
const nextTab = remaining[0]
if (nextTab) {
setActiveTab(nextTab.extension.id)
} else {
activeTabId.value = null
}
}
}
const registerIFrame = (extensionId: string, iframe: HTMLIFrameElement) => {
const tab = openTabs.value.get(extensionId)
if (tab) {
tab.iframe = iframe
}
}
const broadcastToAllTabs = (message: unknown) => {
openTabs.value.forEach(({ iframe }) => {
iframe?.contentWindow?.postMessage(message, '*')
})
}
const closeAllTabs = () => {
openTabs.value.forEach((tab) => tab.iframe?.remove())
openTabs.value.clear()
activeTabId.value = null
}
return {
// State
openTabs,
activeTabId,
// Getters
activeTab,
tabCount,
sortedTabs,
// Actions
openTab,
setActiveTab,
closeTab,
registerIFrame,
broadcastToAllTabs,
closeAllTabs,
}
})