polyfill for spa added. works now on android

This commit is contained in:
2025-10-09 11:16:25 +02:00
parent c8c3a5c73f
commit fa3348a5ad
35 changed files with 2566 additions and 373 deletions

View File

@ -137,33 +137,46 @@ import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
const { t } = useI18n()
const open = defineModel<boolean>('open', { default: false })
const props = defineProps<{
preview?: ExtensionPreview | null
}>()
const preview = defineModel<ExtensionPreview | null>('preview', {
default: null,
})
const databasePermissions = ref(
props.preview?.editable_permissions?.database || [],
)
const filesystemPermissions = ref(
props.preview?.editable_permissions?.filesystem || [],
)
const httpPermissions = ref(props.preview?.editable_permissions?.http || [])
const shellPermissions = ref(props.preview?.editable_permissions?.shell || [])
// Watch for preview changes
watch(
() => props.preview,
(newPreview) => {
if (newPreview?.editable_permissions) {
databasePermissions.value = newPreview.editable_permissions.database || []
filesystemPermissions.value =
newPreview.editable_permissions.filesystem || []
httpPermissions.value = newPreview.editable_permissions.http || []
shellPermissions.value = newPreview.editable_permissions.shell || []
const databasePermissions = computed({
get: () => preview.value?.editable_permissions?.database || [],
set: (value) => {
if (preview.value?.editable_permissions) {
preview.value.editable_permissions.database = value
}
},
{ immediate: true },
)
})
const filesystemPermissions = computed({
get: () => preview.value?.editable_permissions?.filesystem || [],
set: (value) => {
if (preview.value?.editable_permissions) {
preview.value.editable_permissions.filesystem = value
}
},
})
const httpPermissions = computed({
get: () => preview.value?.editable_permissions?.http || [],
set: (value) => {
if (preview.value?.editable_permissions) {
preview.value.editable_permissions.http = value
}
},
})
const shellPermissions = computed({
get: () => preview.value?.editable_permissions?.shell || [],
set: (value) => {
if (preview.value?.editable_permissions) {
preview.value.editable_permissions.shell = value
}
},
})
const permissionAccordionItems = computed(() => {
const items = []
@ -213,12 +226,7 @@ const onDeny = () => {
const onConfirm = () => {
open.value = false
emit('confirm', {
database: databasePermissions.value,
filesystem: filesystemPermissions.value,
http: httpPermissions.value,
shell: shellPermissions.value,
})
emit('confirm')
}
</script>

View File

@ -1,33 +1,87 @@
<template>
<UiDialogConfirm v-model:open="open">
<UiDialogConfirm
v-model:open="open"
@abort="onDeny"
@confirm="onConfirm"
>
<template #title>
<i18n-t keypath="title" tag="p">
<template #extensionName>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
</i18n-t>
{{ t('title', { extensionName: preview?.manifest.name }) }}
</template>
<p>{{ t("question", { extensionName: manifest?.name }) }}</p>
<template #body>
<div class="flex flex-col gap-4">
<p>{{ t('question', { extensionName: preview?.manifest.name }) }}</p>
<UAlert
color="warning"
variant="soft"
:title="t('warning.title')"
:description="t('warning.description')"
icon="i-heroicons-exclamation-triangle"
/>
<div
v-if="preview"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4"
>
<div class="flex items-center gap-3">
<UIcon
v-if="preview.manifest.icon"
:name="preview.manifest.icon"
class="w-12 h-12"
/>
<div class="flex-1">
<h4 class="font-semibold">
{{ preview.manifest.name }}
</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('version') }}: {{ preview.manifest.version }}
</p>
</div>
</div>
</div>
</div>
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import type { IHaexHubExtensionManifest } from "~/types/haexhub";
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
const { t } = useI18n();
const { t } = useI18n()
const open = defineModel<boolean>("open", { default: false });
const { manifest } = defineProps<{ manifest?: IHaexHubExtensionManifest | null }>();
const open = defineModel<boolean>('open', { default: false })
const preview = defineModel<ExtensionPreview | null>('preview', {
default: null,
})
const emit = defineEmits(['deny', 'confirm'])
const onDeny = () => {
open.value = false
emit('deny')
}
const onConfirm = () => {
open.value = false
emit('confirm')
}
</script>
<i18n lang="yaml">
de:
title: "{extensionName} bereits installiert"
question: Soll die Erweiterung {extensionName} erneut installiert werden?
de:
title: '{extensionName} bereits installiert'
question: Soll die Erweiterung {extensionName} erneut installiert werden?
warning:
title: Achtung
description: Die vorhandene Version wird vollständig entfernt und durch die neue Version ersetzt. Dieser Vorgang kann nicht rückgängig gemacht werden.
version: Version
en:
title: "{extensionName} is already installed"
question: Do you want to reinstall {extensionName}?
en:
title: '{extensionName} is already installed'
question: Do you want to reinstall {extensionName}?
warning:
title: Warning
description: The existing version will be completely removed and replaced with the new version. This action cannot be undone.
version: Version
</i18n>

View File

@ -1,43 +1,106 @@
<template>
<UiDialogConfirm v-model:open="open" :title="t('title')" @confirm="onConfirm">
<div>
<i18n-t keypath="question" tag="p">
<template #name>
<span class="font-bold text-primary">{{ extension?.name }}</span>
</template>
</i18n-t>
</div>
<UiDialogConfirm
v-model:open="open"
@abort="onAbort"
@confirm="onConfirm"
>
<template #title>
{{ t('title') }}
</template>
<template #body>
<div class="flex flex-col gap-4">
<i18n-t
keypath="question"
tag="p"
>
<template #name>
<span class="font-bold text-primary">{{ extension?.name }}</span>
</template>
</i18n-t>
<UAlert
color="error"
variant="soft"
:title="t('warning.title')"
:description="t('warning.description')"
icon="i-heroicons-exclamation-triangle"
/>
<div
v-if="extension"
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4"
>
<div class="flex items-center gap-3">
<UIcon
v-if="extension.icon"
:name="extension.icon"
class="w-12 h-12"
/>
<UIcon
v-else
name="i-heroicons-puzzle-piece"
class="w-12 h-12 text-gray-400"
/>
<div class="flex-1">
<h4 class="font-semibold">
{{ extension.name }}
</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('version') }}: {{ extension.version }}
</p>
<p
v-if="extension.author"
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ t('author') }}: {{ extension.author }}
</p>
</div>
</div>
</div>
</div>
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import type { IHaexHubExtension } from "~/types/haexhub";
import type { IHaexHubExtension } from '~/types/haexhub'
const emit = defineEmits(["confirm"]);
const emit = defineEmits(['confirm', 'abort'])
const { t } = useI18n();
const { t } = useI18n()
defineProps<{ extension?: IHaexHubExtension }>();
defineProps<{ extension?: IHaexHubExtension }>()
const open = defineModel<boolean>("open");
const open = defineModel<boolean>('open')
const onAbort = () => {
open.value = false
emit('abort')
}
const onConfirm = () => {
open.value = false;
emit("confirm");
};
open.value = false
emit('confirm')
}
</script>
<i18n lang="json">{
"de": {
"title": "Erweiterung löschen",
"question": "Soll {name} wirklich gelöscht werden?",
"abort": "Abbrechen",
"remove": "Löschen"
},
"en": {
"title": "Remove Extension",
"question": "Should {name} really be deleted?",
"abort": "Abort",
"remove": "Remove"
}
}</i18n>
<i18n lang="yaml">
de:
title: Erweiterung entfernen
question: Möchtest du {name} wirklich entfernen?
warning:
title: Achtung
description: Diese Aktion kann nicht rückgängig gemacht werden. Alle Daten der Erweiterung werden dauerhaft gelöscht.
version: Version
author: Autor
en:
title: Remove Extension
question: Do you really want to remove {name}?
warning:
title: Warning
description: This action cannot be undone. All extension data will be permanently deleted.
version: Version
author: Author
</i18n>

View File

@ -0,0 +1,157 @@
<template>
<UCard
:ui="{
root: 'hover:shadow-lg transition-shadow duration-200 cursor-pointer',
body: 'flex flex-col gap-3',
}"
@click="$emit('open')"
>
<div class="flex items-start gap-4">
<!-- Icon -->
<div class="flex-shrink-0">
<div
v-if="extension.icon"
class="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center"
>
<UIcon
:name="extension.icon"
class="w-10 h-10 text-primary"
/>
</div>
<div
v-else
class="w-16 h-16 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center"
>
<UIcon
name="i-heroicons-puzzle-piece"
class="w-10 h-10 text-gray-400"
/>
</div>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold truncate">
{{ extension.name }}
</h3>
<p
v-if="extension.author"
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ t('by') }} {{ extension.author }}
</p>
</div>
<UBadge
:label="extension.version"
color="neutral"
variant="subtle"
/>
</div>
<p
v-if="extension.description"
class="text-sm text-gray-600 dark:text-gray-300 mt-2 line-clamp-2"
>
{{ extension.description }}
</p>
<!-- Installed Badge -->
<div class="flex items-center gap-2 mt-3">
<UBadge
:label="t('installed')"
color="success"
variant="subtle"
>
<template #leading>
<UIcon name="i-heroicons-check-circle" />
</template>
</UBadge>
<UBadge
v-if="extension.enabled"
:label="t('enabled')"
color="primary"
variant="soft"
/>
<UBadge
v-else
:label="t('disabled')"
color="neutral"
variant="soft"
/>
</div>
</div>
</div>
<!-- Actions -->
<template #footer>
<div class="flex items-center justify-between gap-2">
<UButton
:label="t('open')"
color="primary"
icon="i-heroicons-arrow-right"
size="sm"
@click.stop="$emit('open')"
/>
<div class="flex gap-2">
<UButton
:label="t('settings')"
color="neutral"
variant="ghost"
icon="i-heroicons-cog-6-tooth"
size="sm"
@click.stop="$emit('settings')"
/>
<UButton
:label="t('remove')"
color="error"
variant="ghost"
icon="i-heroicons-trash"
size="sm"
@click.stop="$emit('remove')"
/>
</div>
</div>
</template>
</UCard>
</template>
<script setup lang="ts">
interface InstalledExtension {
id: string
name: string
version: string
author?: string
description?: string
icon?: string
enabled?: boolean
}
defineProps<{
extension: InstalledExtension
}>()
defineEmits(['open', 'settings', 'remove'])
const { t } = useI18n()
</script>
<i18n lang="yaml">
de:
by: von
installed: Installiert
enabled: Aktiviert
disabled: Deaktiviert
open: Öffnen
settings: Einstellungen
remove: Entfernen
en:
by: by
installed: Installed
enabled: Enabled
disabled: Disabled
open: Open
settings: Settings
remove: Remove
</i18n>

View File

@ -0,0 +1,173 @@
<template>
<UCard
:ui="{
root: 'hover:shadow-lg transition-shadow duration-200 cursor-pointer',
body: 'flex flex-col gap-3',
}"
@click="$emit('click')"
>
<div class="flex items-start gap-4">
<!-- Icon -->
<div class="flex-shrink-0">
<div
v-if="extension.icon"
class="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center"
>
<UIcon
:name="extension.icon"
class="w-10 h-10 text-primary"
/>
</div>
<div
v-else
class="w-16 h-16 rounded-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center"
>
<UIcon
name="i-heroicons-puzzle-piece"
class="w-10 h-10 text-gray-400"
/>
</div>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold truncate">
{{ extension.name }}
</h3>
<p
v-if="extension.author"
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ t('by') }} {{ extension.author }}
</p>
</div>
<UBadge
:label="extension.version"
color="neutral"
variant="subtle"
/>
</div>
<p
v-if="extension.description"
class="text-sm text-gray-600 dark:text-gray-300 mt-2 line-clamp-2"
>
{{ extension.description }}
</p>
<!-- Stats -->
<div
class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400"
>
<div
v-if="extension.downloads"
class="flex items-center gap-1"
>
<UIcon name="i-heroicons-arrow-down-tray" />
<span>{{ formatNumber(extension.downloads) }}</span>
</div>
<div
v-if="extension.rating"
class="flex items-center gap-1"
>
<UIcon name="i-heroicons-star-solid" />
<span>{{ extension.rating }}</span>
</div>
<div
v-if="extension.verified"
class="flex items-center gap-1 text-green-600 dark:text-green-400"
>
<UIcon name="i-heroicons-check-badge-solid" />
<span>{{ t('verified') }}</span>
</div>
</div>
<!-- Tags -->
<div
v-if="extension.tags?.length"
class="flex flex-wrap gap-1 mt-2"
>
<UBadge
v-for="tag in extension.tags.slice(0, 3)"
:key="tag"
:label="tag"
size="xs"
color="primary"
variant="soft"
/>
</div>
</div>
</div>
<!-- Actions -->
<template #footer>
<div class="flex items-center justify-between gap-2">
<UButton
:label="isInstalled ? t('installed') : t('install')"
:color="isInstalled ? 'neutral' : 'primary'"
:disabled="isInstalled"
:icon="
isInstalled ? 'i-heroicons-check' : 'i-heroicons-arrow-down-tray'
"
size="sm"
@click.stop="$emit('install')"
/>
<UButton
:label="t('details')"
color="neutral"
variant="ghost"
size="sm"
@click.stop="$emit('details')"
/>
</div>
</template>
</UCard>
</template>
<script setup lang="ts">
interface MarketplaceExtension {
id: string
name: string
version: string
author?: string
description?: string
icon?: string
downloads?: number
rating?: number
verified?: boolean
tags?: string[]
downloadUrl?: string
}
defineProps<{
extension: MarketplaceExtension
isInstalled?: boolean
}>()
defineEmits(['click', 'install', 'details'])
const { t } = useI18n()
const formatNumber = (num: number) => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
return num.toString()
}
</script>
<i18n lang="yaml">
de:
by: von
install: Installieren
installed: Installiert
details: Details
verified: Verifiziert
en:
by: by
install: Install
installed: Installed
details: Details
verified: Verified
</i18n>

View File

@ -20,8 +20,8 @@
<USelectMenu
v-model="menuEntry"
:items="statusOptions"
value-attribute="value"
class="w-44"
:search-input="false"
>
<template #leading>
<UIcon