mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
removed haex-pass components
This commit is contained in:
@ -62,11 +62,12 @@
|
||||
class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
v-if="isInstalled"
|
||||
v-if="extension.isInstalled"
|
||||
class="flex items-center gap-1 text-success font-medium"
|
||||
>
|
||||
<UIcon name="i-heroicons-check-circle-solid" />
|
||||
<span>{{ t('installed') }}</span>
|
||||
<span v-if="!extension.installedVersion">{{ t('installed') }}</span>
|
||||
<span v-else>{{ t('installedVersion', { version: extension.installedVersion }) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="extension.downloads"
|
||||
@ -112,11 +113,11 @@
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<UButton
|
||||
:label="isInstalled ? t('installed') : t('install')"
|
||||
:color="isInstalled ? 'neutral' : 'primary'"
|
||||
:disabled="isInstalled"
|
||||
:label="getInstallButtonLabel()"
|
||||
:color="extension.isInstalled && !extension.installedVersion ? 'neutral' : 'primary'"
|
||||
:disabled="extension.isInstalled && !extension.installedVersion"
|
||||
:icon="
|
||||
isInstalled ? 'i-heroicons-check' : 'i-heroicons-arrow-down-tray'
|
||||
extension.isInstalled && !extension.installedVersion ? 'i-heroicons-check' : 'i-heroicons-arrow-down-tray'
|
||||
"
|
||||
size="sm"
|
||||
@click.stop="$emit('install')"
|
||||
@ -134,23 +135,10 @@
|
||||
</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
|
||||
}
|
||||
import type { IMarketplaceExtension } from '~/types/haexhub'
|
||||
|
||||
defineProps<{
|
||||
extension: MarketplaceExtension
|
||||
isInstalled?: boolean
|
||||
const props = defineProps<{
|
||||
extension: IMarketplaceExtension
|
||||
}>()
|
||||
|
||||
defineEmits(['click', 'install', 'details'])
|
||||
@ -162,6 +150,16 @@ const formatNumber = (num: number) => {
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const getInstallButtonLabel = () => {
|
||||
if (!props.extension.isInstalled) {
|
||||
return t('install')
|
||||
}
|
||||
if (props.extension.installedVersion) {
|
||||
return t('update')
|
||||
}
|
||||
return t('installed')
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
@ -169,12 +167,16 @@ de:
|
||||
by: von
|
||||
install: Installieren
|
||||
installed: Installiert
|
||||
installedVersion: 'Installiert (v{version})'
|
||||
update: Aktualisieren
|
||||
details: Details
|
||||
verified: Verifiziert
|
||||
en:
|
||||
by: by
|
||||
install: Install
|
||||
installed: Installed
|
||||
installedVersion: 'Installed (v{version})'
|
||||
update: Update
|
||||
details: Details
|
||||
verified: Verified
|
||||
</i18n>
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<HaexPassCard
|
||||
:title
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="flex flex-col gap-4 w-full p-4">
|
||||
<slot />
|
||||
|
||||
<UiInput
|
||||
v-show="!read_only"
|
||||
v-model.trim="passwordGroup.name"
|
||||
:label="t('group.name')"
|
||||
:placeholder="t('group.name')"
|
||||
:with-copy-button="read_only"
|
||||
:read_only
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
v-show="!read_only || passwordGroup.description?.length"
|
||||
v-model.trim="passwordGroup.description"
|
||||
:read_only
|
||||
:label="t('group.description')"
|
||||
:placeholder="t('group.description')"
|
||||
:with-copy-button="read_only"
|
||||
/>
|
||||
|
||||
<UiSelectColor
|
||||
v-model="passwordGroup.color"
|
||||
:read_only
|
||||
:label="t('group.color')"
|
||||
:placeholder="t('group.color')"
|
||||
/>
|
||||
|
||||
<UiSelectIcon
|
||||
v-model="passwordGroup.icon"
|
||||
:read_only
|
||||
:label="t('group.icon')"
|
||||
:placeholder="t('group.icon')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot name="footer" />
|
||||
</HaexPassCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
|
||||
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const { t } = useI18n()
|
||||
const showConfirmation = ref(false)
|
||||
const passwordGroup = defineModel<SelectHaexPasswordsGroups>({ required: true })
|
||||
const read_only = defineModel<boolean>('read_only')
|
||||
const props = defineProps<{
|
||||
originally: SelectHaexPasswordsGroups
|
||||
title: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
submit: [to?: RouteLocationNormalizedLoadedGeneric]
|
||||
close: [void]
|
||||
back: [void]
|
||||
reject: [to?: RouteLocationNormalizedLoadedGeneric]
|
||||
}>()
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
console.log('group has changes', props.originally, passwordGroup.value)
|
||||
if (!props.originally) {
|
||||
if (
|
||||
passwordGroup.value.color?.length ||
|
||||
passwordGroup.value.description?.length ||
|
||||
passwordGroup.value.icon?.length ||
|
||||
passwordGroup.value.name?.length
|
||||
) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (
|
||||
JSON.stringify(props.originally) !== JSON.stringify(passwordGroup.value)
|
||||
)
|
||||
})
|
||||
|
||||
const onClose = () => {
|
||||
/* if (props.originally) passwordGroup.value = { ...props.originally };
|
||||
emit('close'); */
|
||||
console.log('close group card')
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
group:
|
||||
name: Name
|
||||
description: Beschreibung
|
||||
icon: Icon
|
||||
color: Farbe
|
||||
|
||||
en:
|
||||
group:
|
||||
name: Name
|
||||
description: Description
|
||||
icon: Icon
|
||||
color: Color
|
||||
</i18n>
|
||||
@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<UiCard
|
||||
:title
|
||||
:icon
|
||||
>
|
||||
<slot />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ title: string; icon?: string }>()
|
||||
</script>
|
||||
@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<UiDialogConfirm
|
||||
v-model:open="showConfirmDeleteDialog"
|
||||
:confirm-label="final ? t('final.label') : t('label')"
|
||||
:title="final ? t('final.title') : t('title', { itemName })"
|
||||
@abort="$emit('abort')"
|
||||
@confirm="$emit('confirm')"
|
||||
>
|
||||
<template #body>
|
||||
{{
|
||||
final ? t('final.question', { itemName }) : t('question', { itemName })
|
||||
}}
|
||||
</template>
|
||||
</UiDialogConfirm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const showConfirmDeleteDialog = defineModel<boolean>('open')
|
||||
defineProps<{ final?: boolean; itemName?: string | null }>()
|
||||
|
||||
defineEmits(['confirm', 'abort'])
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
title: Eintrag löschen
|
||||
question: Soll der Eintrag "{itemName}" in den Papierkorb verschoben werden?
|
||||
label: Verschieben
|
||||
|
||||
final:
|
||||
title: Eintrag endgültig löschen
|
||||
question: Soll der Eintrag "{itemName}" endgültig gelöscht werden?
|
||||
label: Löschen
|
||||
|
||||
en:
|
||||
title: Delete Entry
|
||||
question: Should the “{itemName}” entry be moved to the recycle bin?
|
||||
label: Move
|
||||
|
||||
final:
|
||||
title: Delete entry permanently
|
||||
question: Should the entry “{itemName}” be permanently deleted?
|
||||
label: Delete
|
||||
</i18n>
|
||||
@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<UiDialogConfirm
|
||||
v-model:open="showUnsavedChangesDialog"
|
||||
:confirm-label="t('label')"
|
||||
:title="t('title')"
|
||||
@abort="$emit('abort')"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex items-center h-full">
|
||||
{{ t('question') }}
|
||||
</div>
|
||||
</template>
|
||||
</UiDialogConfirm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const showUnsavedChangesDialog = defineModel<boolean>('open')
|
||||
const ignoreChanges = defineModel<boolean>('ignoreChanges')
|
||||
const { hasChanges } = defineProps<{ hasChanges: boolean }>()
|
||||
|
||||
const emit = defineEmits(['confirm', 'abort'])
|
||||
|
||||
const onConfirm = () => {
|
||||
ignoreChanges.value = true
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
if (hasChanges && !ignoreChanges.value) {
|
||||
showUnsavedChangesDialog.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
title: Nicht gespeicherte Änderungen
|
||||
question: Sollen die Änderungen verworfen werden?
|
||||
label: Verwerfen
|
||||
|
||||
en:
|
||||
title: Unsaved changes
|
||||
question: Should the changes be discarded?
|
||||
label: Discard
|
||||
</i18n>
|
||||
@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<ul class="flex items-center gap-2 p-2">
|
||||
<li>
|
||||
<NuxtLinkLocale :to="{ name: 'passwordGroupItems' }">
|
||||
<Icon
|
||||
name="mdi:safe"
|
||||
size="24"
|
||||
/>
|
||||
</NuxtLinkLocale>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Icon
|
||||
name="tabler:chevron-right"
|
||||
class="rtl:rotate-180"
|
||||
/>
|
||||
<NuxtLinkLocale
|
||||
:to="{ name: 'passwordGroupItems', params: { groupId: item.id } }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</NuxtLinkLocale>
|
||||
</li>
|
||||
|
||||
<li class="ml-2">
|
||||
<UTooltip :text="t('edit')">
|
||||
<NuxtLinkLocale
|
||||
:to="{
|
||||
name: 'passwordGroupEdit',
|
||||
params: { groupId: lastGroup?.id },
|
||||
}"
|
||||
>
|
||||
<Icon name="mdi:pencil" />
|
||||
</NuxtLinkLocale>
|
||||
</UTooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const groups = defineProps<{ items: SelectHaexPasswordsGroups[] }>()
|
||||
|
||||
const lastGroup = computed(() => groups.items.at(-1))
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
edit: Bearbeiten
|
||||
|
||||
en:
|
||||
edit: Edit
|
||||
</i18n>
|
||||
@ -1,71 +0,0 @@
|
||||
export const usePasswordGroup = () => {
|
||||
const areItemsEqual = (
|
||||
groupA: unknown | unknown[] | null,
|
||||
groupB: unknown | unknown[] | null,
|
||||
) => {
|
||||
console.log('compare values', groupA, groupB)
|
||||
if (groupA === groupB) return true
|
||||
|
||||
if (Array.isArray(groupA) && Array.isArray(groupB)) {
|
||||
console.log('compare object arrays', groupA, groupB)
|
||||
if (groupA.length === groupB.length) return true
|
||||
|
||||
return groupA.some((group, index) => {
|
||||
return areObjectsEqual(group, groupA[index])
|
||||
})
|
||||
}
|
||||
return areObjectsEqual(groupA, groupB)
|
||||
}
|
||||
|
||||
const deepEqual = (obj1: unknown, obj2: unknown) => {
|
||||
console.log('compare values', obj1, obj2)
|
||||
if (obj1 === obj2) return true
|
||||
|
||||
// Null/undefined Check
|
||||
if (obj1 == null || obj2 == null) return obj1 === obj2
|
||||
|
||||
// Typ-Check
|
||||
if (typeof obj1 !== typeof obj2) return false
|
||||
|
||||
// Primitive Typen
|
||||
if (typeof obj1 !== 'object') return obj1 === obj2
|
||||
|
||||
// Arrays
|
||||
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
|
||||
|
||||
if (Array.isArray(obj1)) {
|
||||
if (obj1.length !== obj2.length) return false
|
||||
for (let i = 0; i < obj1.length; i++) {
|
||||
if (!deepEqual(obj1[i], obj2[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Date Objekte
|
||||
if (obj1 instanceof Date && obj2 instanceof Date) {
|
||||
return obj1.getTime() === obj2.getTime()
|
||||
}
|
||||
|
||||
// RegExp Objekte
|
||||
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
|
||||
return obj1.toString() === obj2.toString()
|
||||
}
|
||||
|
||||
// Objekte
|
||||
const keys1 = Object.keys(obj1)
|
||||
const keys2 = Object.keys(obj2)
|
||||
|
||||
if (keys1.length !== keys2.length) return false
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!keys2.includes(key)) return false
|
||||
if (!deepEqual(obj1[key], obj2[key])) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return {
|
||||
areItemsEqual,
|
||||
deepEqual,
|
||||
}
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<UCard
|
||||
v-if="group"
|
||||
:ui="{ root: [''] }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon
|
||||
:name="
|
||||
mode === 'edit'
|
||||
? 'mdi:folder-edit-outline'
|
||||
: 'mdi:folder-plus-outline'
|
||||
"
|
||||
size="24"
|
||||
/>
|
||||
<span>{{ mode === 'edit' ? t('title.edit') : t('title.create') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form class="flex flex-col gap-4 w-full p-4">
|
||||
<UiInput
|
||||
ref="nameRef"
|
||||
v-model="group.name"
|
||||
:label="t('name')"
|
||||
:placeholder="t('name')"
|
||||
:read-only
|
||||
autofocus
|
||||
@keyup.enter="$emit('submit')"
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
v-model="group.description"
|
||||
:label="t('description')"
|
||||
:placeholder="t('description')"
|
||||
:read-only
|
||||
@keyup.enter="$emit('submit')"
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<!-- <UiSelectIcon
|
||||
v-model="group.icon"
|
||||
default-icon="mdi:folder-outline"
|
||||
:readOnly
|
||||
/>
|
||||
|
||||
<UiSelectColor
|
||||
v-model="group.color"
|
||||
:readOnly
|
||||
/> -->
|
||||
</div>
|
||||
</form>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const group = defineModel<SelectHaexPasswordsGroups | null>()
|
||||
const { readOnly = false } = defineProps<{
|
||||
readOnly?: boolean
|
||||
mode: 'create' | 'edit'
|
||||
}>()
|
||||
const emit = defineEmits(['close', 'submit'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const nameRef = useTemplateRef('nameRef')
|
||||
onStartTyping(() => {
|
||||
nameRef.value?.$el.focus()
|
||||
})
|
||||
|
||||
const { escape } = useMagicKeys()
|
||||
|
||||
watchEffect(async () => {
|
||||
if (escape?.value) {
|
||||
await nextTick()
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
name: Name
|
||||
description: Beschreibung
|
||||
icon: Icon
|
||||
color: Farbe
|
||||
create: Erstellen
|
||||
save: Speichern
|
||||
abort: Abbrechen
|
||||
title:
|
||||
create: Gruppe erstellen
|
||||
edit: Gruppe ändern
|
||||
|
||||
en:
|
||||
name: Name
|
||||
description: Description
|
||||
icon: Icon
|
||||
color: Color
|
||||
create: Create
|
||||
save: Save
|
||||
abort: Abort
|
||||
title:
|
||||
create: Create group
|
||||
edit: Edit group
|
||||
</i18n>
|
||||
@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-scroll">
|
||||
<form
|
||||
class="flex flex-col gap-4 w-full p-4"
|
||||
@submit.prevent="$emit('submit')"
|
||||
>
|
||||
<UiInput
|
||||
v-show="!readOnly || itemDetails.title"
|
||||
ref="titleRef"
|
||||
v-model.trim="itemDetails.title"
|
||||
:check-input="check"
|
||||
:label="t('item.title')"
|
||||
:placeholder="t('item.title')"
|
||||
:read-only
|
||||
:with-copy-button
|
||||
autofocus
|
||||
@keyup.enter="$emit('submit')"
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
v-show="!readOnly || itemDetails.username"
|
||||
v-model.trim="itemDetails.username"
|
||||
:check-input="check"
|
||||
:label="t('item.username')"
|
||||
:placeholder="t('item.username')"
|
||||
:with-copy-button
|
||||
:read-only
|
||||
@keyup.enter="$emit('submit')"
|
||||
/>
|
||||
|
||||
<UiInputPassword
|
||||
v-show="!readOnly || itemDetails.password"
|
||||
v-model.trim="itemDetails.password"
|
||||
:check-input="check"
|
||||
:read-only
|
||||
:with-copy-button
|
||||
@keyup.enter="$emit('submit')"
|
||||
>
|
||||
<template #append>
|
||||
<!-- <UiDialogPasswordGenerator
|
||||
v-if="!readOnly"
|
||||
class="join-item"
|
||||
:password="itemDetails.password"
|
||||
v-model="preventClose"
|
||||
/> -->
|
||||
</template>
|
||||
</UiInputPassword>
|
||||
|
||||
<UiInputUrl
|
||||
v-show="!readOnly || itemDetails.url"
|
||||
v-model="itemDetails.url"
|
||||
:label="t('item.url')"
|
||||
:placeholder="t('item.url')"
|
||||
:read-only
|
||||
:with-copy-button
|
||||
@keyup.enter="$emit('submit')"
|
||||
/>
|
||||
|
||||
<!-- <UiSelectIcon
|
||||
v-show="!readOnly"
|
||||
:default-icon="defaultIcon || 'mdi:key-outline'"
|
||||
:readOnly
|
||||
v-model="itemDetails.icon"
|
||||
/> -->
|
||||
|
||||
<UiTextarea
|
||||
v-show="!readOnly || itemDetails.note"
|
||||
v-model="itemDetails.note"
|
||||
:label="t('item.note')"
|
||||
:placeholder="t('item.note')"
|
||||
:readOnly
|
||||
:with-copy-button
|
||||
@keyup.enter.stop
|
||||
color="error"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectHaexPasswordsItemDetails } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
defineProps<{
|
||||
defaultIcon?: string | null
|
||||
readOnly?: boolean
|
||||
withCopyButton?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits(['submit'])
|
||||
const { t } = useI18n()
|
||||
|
||||
const itemDetails = defineModel<SelectHaexPasswordsItemDetails>({
|
||||
required: true,
|
||||
})
|
||||
|
||||
//const preventClose = defineModel<boolean>('preventClose')
|
||||
|
||||
const check = defineModel<boolean>('check-input', { default: false })
|
||||
|
||||
/* onKeyStroke('escape', (e) => {
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
}) */
|
||||
|
||||
const titleRef = useTemplateRef('titleRef')
|
||||
onStartTyping(() => {
|
||||
titleRef.value?.$el?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
item:
|
||||
title: Titel
|
||||
username: Nutzername
|
||||
password: Passwort
|
||||
url: Url
|
||||
note: Notiz
|
||||
|
||||
en:
|
||||
item:
|
||||
title: Title
|
||||
username: Username
|
||||
password: Password
|
||||
url: Url
|
||||
note: Note
|
||||
</i18n>
|
||||
@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-scroll flex">
|
||||
{{ _history }}
|
||||
<UiList v-show="_history.length">
|
||||
<!-- <UiListButton v-for="item in _history">
|
||||
<div
|
||||
class="flex items-start bg-slate-100 gap-x-2 w-full h-20 overflow-clip"
|
||||
>
|
||||
<div class="flex flex-col justify-between h-full py-2">
|
||||
<h6 class="text-sm whitespace-nowrap bg-orange-200">
|
||||
vorheriger {{ item.changedProperty }}
|
||||
</h6>
|
||||
<UiInput
|
||||
:model-value="item.oldValue"
|
||||
with-copy-button
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sm:flex flex-col justify-between h-full py-2 hidden">
|
||||
<h6 class="text-sm">neuer Wert</h6>
|
||||
<UiInput
|
||||
:model-value="item.newValue"
|
||||
with-copy-button
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between h-full py-2">
|
||||
<h6 class="text-sm md:text-base bg-orange-200">geändert_am</h6>
|
||||
<span class="bg-red-100 py-1 md:py-2">
|
||||
{{ item.createdAt }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiListButton>
|
||||
-->
|
||||
</UiList>
|
||||
|
||||
<div
|
||||
v-show="!_history.length"
|
||||
class="content-center w-full text-center"
|
||||
>
|
||||
{{ t('noHistory') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <UiTable
|
||||
v-if="history?.length"
|
||||
:headers
|
||||
:items="_history"
|
||||
autofocus
|
||||
>
|
||||
<template #column-oldValue="{ item }: { item: string }">
|
||||
<UiInput
|
||||
:model-value="item"
|
||||
with-copy-button
|
||||
class="min-w-24"
|
||||
/>
|
||||
</template>
|
||||
</UiTable> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
SelectHaexPasswordsGroupItems,
|
||||
SelectHaexPasswordsItemDetails,
|
||||
SelectHaexPasswordsItemHistory,
|
||||
} from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const history = defineModel<SelectHaexPasswordsItemHistory[]>()
|
||||
|
||||
const _history = computed(
|
||||
() =>
|
||||
history.value?.map((change) => ({
|
||||
changedProperty: t(change.changedProperty!),
|
||||
createdAt: new Date(change.createdAt!).toLocaleDateString(),
|
||||
newValue: change.newValue,
|
||||
oldValue: change.oldValue,
|
||||
})) ?? [],
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
interface ITableHeader {
|
||||
label?: string
|
||||
'item-value': string
|
||||
}
|
||||
const headers: ITableHeader[] = [
|
||||
{ 'item-value': 'changedProperty', label: t('changedProperty') },
|
||||
{ 'item-value': 'oldValue', label: t('oldValue') },
|
||||
{ 'item-value': 'newValue', label: t('newValue') },
|
||||
{ 'item-value': 'createdAt', label: t('createdAt') },
|
||||
]
|
||||
</script>
|
||||
|
||||
<i18n lang="json">
|
||||
{
|
||||
"de": {
|
||||
"noHistory": "Eintrag wurde bisher nicht geändert",
|
||||
"changedProperty": "Änderung",
|
||||
"createdAt": "geändert am",
|
||||
"newValue": "neuer Wert",
|
||||
"oldValue": "alter Wert",
|
||||
"password": "Passwort",
|
||||
"title": "Titel",
|
||||
"url": "Url",
|
||||
"username": "Nutzername"
|
||||
},
|
||||
"en": {
|
||||
"noHistory": "No changes so far",
|
||||
"changedProperty": "Changes",
|
||||
"createdAt": "changed at",
|
||||
"newValue": "new Value",
|
||||
"oldValue": "old Value",
|
||||
"password": "Password",
|
||||
"title": "Title",
|
||||
"url": "Url",
|
||||
"username": "Username"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@ -1,182 +0,0 @@
|
||||
<template>
|
||||
<div class="p-1">
|
||||
<UCard
|
||||
class="rounded overflow-auto p-0 h-full"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="">
|
||||
<UTabs
|
||||
:items="tabs"
|
||||
variant="link"
|
||||
:ui="{ trigger: 'grow' }"
|
||||
class="gap-4 w-full"
|
||||
>
|
||||
<template #details>
|
||||
<HaexPassItemDetails
|
||||
v-if="details"
|
||||
v-model="details"
|
||||
with-copy-button
|
||||
:read-only
|
||||
:defaultIcon
|
||||
v-model:prevent-close="preventClose"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #keyValue>
|
||||
<HaexPassItemKeyValue
|
||||
v-if="keyValues"
|
||||
v-model="keyValues"
|
||||
v-model:items-to-add="keyValuesAdd"
|
||||
v-model:items-to-delete="keyValuesDelete"
|
||||
:read-only
|
||||
:item-id="details!.id"
|
||||
/>
|
||||
</template>
|
||||
</UTabs>
|
||||
|
||||
<!-- <div class="h-full pb-8">
|
||||
<div
|
||||
id="vaultDetailsId"
|
||||
role="tabpanel"
|
||||
class="h-full"
|
||||
:aria-labelledby="id.details"
|
||||
>
|
||||
<HaexPassItemDetails
|
||||
v-if="details"
|
||||
v-model="details"
|
||||
with-copy-button
|
||||
:read_only
|
||||
:defaultIcon
|
||||
v-model:prevent-close="preventClose"
|
||||
@submit="$emit('submit')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="tabs-basic-2"
|
||||
class="hidden"
|
||||
role="tabpanel"
|
||||
:aria-labelledby="id.keyValue"
|
||||
>
|
||||
<HaexPassItemKeyValue
|
||||
v-if="keyValues"
|
||||
v-model="keyValues"
|
||||
v-model:items-to-add="keyValuesAdd"
|
||||
v-model:items-to-delete="keyValuesDelete"
|
||||
:read_only
|
||||
:item-id="details!.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="tabs-basic-3"
|
||||
class="hidden h-full"
|
||||
role="tabpanel"
|
||||
:aria-labelledby="id.history"
|
||||
>
|
||||
<HaexPassItemHistory />
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TabsItem } from '@nuxt/ui'
|
||||
import type {
|
||||
SelectHaexPasswordsItemDetails,
|
||||
SelectHaexPasswordsItemHistory,
|
||||
SelectHaexPasswordsItemKeyValues,
|
||||
} from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
defineProps<{
|
||||
defaultIcon?: string | null
|
||||
history: SelectHaexPasswordsItemHistory[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
addKeyValue: []
|
||||
removeKeyValue: [string]
|
||||
submit: []
|
||||
}>()
|
||||
|
||||
const readOnly = defineModel<boolean>('readOnly', { default: false })
|
||||
|
||||
const details = defineModel<SelectHaexPasswordsItemDetails | null>('details', {
|
||||
required: true,
|
||||
})
|
||||
|
||||
const keyValues = defineModel<SelectHaexPasswordsItemKeyValues[]>('keyValues', {
|
||||
default: [],
|
||||
})
|
||||
|
||||
const keyValuesAdd = defineModel<SelectHaexPasswordsItemKeyValues[]>(
|
||||
'keyValuesAdd',
|
||||
{ default: [] },
|
||||
)
|
||||
const keyValuesDelete = defineModel<SelectHaexPasswordsItemKeyValues[]>(
|
||||
'keyValuesDelete',
|
||||
{ default: [] },
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
/* const id = reactive({
|
||||
details: useId(),
|
||||
keyValue: useId(),
|
||||
history: useId(),
|
||||
content: {},
|
||||
}) */
|
||||
|
||||
const preventClose = ref(false)
|
||||
|
||||
const onClose = () => {
|
||||
if (preventClose.value) return
|
||||
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const tabs = ref<TabsItem[]>([
|
||||
{
|
||||
label: t('tab.details'),
|
||||
icon: 'material-symbols:key-outline',
|
||||
slot: 'details' as const,
|
||||
},
|
||||
{
|
||||
label: t('tab.keyValue'),
|
||||
icon: 'fluent:group-list-20-filled',
|
||||
slot: 'keyValue' as const,
|
||||
},
|
||||
{
|
||||
label: t('tab.history'),
|
||||
icon: 'material-symbols:history',
|
||||
slot: 'history' as const,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<i18n lang="json">
|
||||
{
|
||||
"de": {
|
||||
"create": "Anlegen",
|
||||
"abort": "Abbrechen",
|
||||
"tab": {
|
||||
"details": "Details",
|
||||
"keyValue": "Extra",
|
||||
"history": "Verlauf"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"create": "Create",
|
||||
"abort": "Abort",
|
||||
"tab": {
|
||||
"details": "Details",
|
||||
"keyValue": "Extra",
|
||||
"history": "History"
|
||||
}
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UiList
|
||||
v-if="items.length || itemsToAdd.length"
|
||||
class="flex-1"
|
||||
>
|
||||
<li
|
||||
v-for="item in [...items, ...itemsToAdd]"
|
||||
:key="item.id"
|
||||
:class="{ 'bg-primary/20': currentSelected === item }"
|
||||
class="flex gap-2 hover:bg-primary/20 px-4 items-center"
|
||||
@click="currentSelected = item"
|
||||
>
|
||||
<button class="flex items-center no-underline w-full py-2">
|
||||
<input
|
||||
v-model="item.key"
|
||||
:readonly="currentSelected !== item || readOnly"
|
||||
class="flex-1 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<UiButton
|
||||
v-if="!readOnly"
|
||||
:class="[currentSelected === item ? 'visible' : 'invisible']"
|
||||
variant="outline"
|
||||
color="error"
|
||||
icon="mdi:trash-outline"
|
||||
@click="deleteItem(item.id)"
|
||||
/>
|
||||
</li>
|
||||
</UiList>
|
||||
|
||||
<UTextarea
|
||||
v-if="items.length || itemsToAdd.length"
|
||||
:readOnly="readOnly || !currentSelected"
|
||||
class="flex-1 min-w-52 border-base-content/25"
|
||||
v-model="currentValue"
|
||||
with-copy-button
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="!readOnly"
|
||||
class="flex py-4 gap-2 justify-center items-end flex-wrap"
|
||||
>
|
||||
<UiButton
|
||||
@click="addItem"
|
||||
class="btn-primary btn-outline flex-1-1 min-w-40"
|
||||
icon="mdi:plus"
|
||||
>
|
||||
<!-- <Icon name="mdi:plus" />
|
||||
<p class="hidden sm:inline-block">{{ t('add') }}</p> -->
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectHaexPasswordsItemKeyValues } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const { itemId } = defineProps<{ readOnly?: boolean; itemId: string }>()
|
||||
|
||||
const items = defineModel<SelectHaexPasswordsItemKeyValues[]>({ default: [] })
|
||||
|
||||
const itemsToDelete = defineModel<SelectHaexPasswordsItemKeyValues[]>(
|
||||
'itemsToDelete',
|
||||
{ default: [] },
|
||||
)
|
||||
const itemsToAdd = defineModel<SelectHaexPasswordsItemKeyValues[]>(
|
||||
'itemsToAdd',
|
||||
{ default: [] },
|
||||
)
|
||||
|
||||
defineEmits<{ add: []; remove: [string] }>()
|
||||
|
||||
//const { t } = useI18n()
|
||||
|
||||
const currentSelected = ref<SelectHaexPasswordsItemKeyValues | undefined>(
|
||||
items.value?.at(0),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => itemId,
|
||||
() => (currentSelected.value = items.value?.at(0)),
|
||||
)
|
||||
//const currentValue = computed(() => currentSelected.value?.value || '')
|
||||
const currentValue = computed({
|
||||
get: () => currentSelected.value?.value || '',
|
||||
set(newValue: string) {
|
||||
if (currentSelected.value) currentSelected.value.value = newValue
|
||||
},
|
||||
})
|
||||
|
||||
const addItem = () => {
|
||||
itemsToAdd.value?.push({
|
||||
id: crypto.randomUUID(),
|
||||
itemId,
|
||||
key: '',
|
||||
value: '',
|
||||
updateAt: null,
|
||||
haex_tombstone: null,
|
||||
})
|
||||
}
|
||||
|
||||
const deleteItem = (id: string) => {
|
||||
const item = items.value.find((item) => item.id === id)
|
||||
if (item) {
|
||||
itemsToDelete.value?.push(item)
|
||||
items.value = items.value.filter((item) => item.id !== id)
|
||||
}
|
||||
|
||||
itemsToAdd.value = itemsToAdd.value?.filter((item) => item.id !== id) ?? []
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
add: Hinzufügen
|
||||
key: Schlüssel
|
||||
value: Wert
|
||||
|
||||
en:
|
||||
add: Add
|
||||
key: Key
|
||||
value: Value
|
||||
</i18n>
|
||||
@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end h-12"
|
||||
:class="[isVisible ? 'left-16' : 'left-0']"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<UiButton
|
||||
v-show="showCloseButton"
|
||||
:tooltip="t('abort')"
|
||||
icon="mdi:close"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
class="pointer-events-auto"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UiButton
|
||||
v-show="showEditButton"
|
||||
icon="mdi:pencil-outline"
|
||||
class="pointer-events-auto"
|
||||
size="xl"
|
||||
:tooltip="t('edit')"
|
||||
@click="$emit('edit')"
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
v-show="showReadonlyButton"
|
||||
icon="mdi:pencil-off-outline"
|
||||
class="pointer-events-auto"
|
||||
size="xl"
|
||||
:tooltip="t('readonly')"
|
||||
@click="$emit('readonly')"
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
v-show="showSaveButton"
|
||||
icon="mdi:content-save-outline"
|
||||
size="xl"
|
||||
class="pointer-events-auto"
|
||||
:class="{ 'animate-pulse': hasChanges }"
|
||||
:tooltip="t('save')"
|
||||
@click="$emit('save')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<UiButton
|
||||
v-show="showDeleteButton"
|
||||
color="error"
|
||||
icon="mdi:trash-outline"
|
||||
class="pointer-events-auto"
|
||||
variant="ghost"
|
||||
:tooltip="t('delete')"
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { isVisible } = storeToRefs(useSidebarStore())
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
hasChanges?: boolean
|
||||
showCloseButton?: boolean
|
||||
showDeleteButton?: boolean
|
||||
showEditButton?: boolean
|
||||
showReadonlyButton?: boolean
|
||||
showSaveButton?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits(['close', 'edit', 'readonly', 'save', 'delete'])
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
save: Speichern
|
||||
abort: Abbrechen
|
||||
edit: Bearbeiten
|
||||
readonly: Lesemodus
|
||||
delete: Löschen
|
||||
|
||||
en:
|
||||
save: Save
|
||||
abort: Abort
|
||||
edit: Edit
|
||||
readonly: Read Mode
|
||||
delete: Delete
|
||||
</i18n>
|
||||
@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="menuItems?.length"
|
||||
class="flex-1 h-full"
|
||||
>
|
||||
<ul
|
||||
ref="listRef"
|
||||
class="flex flex-col w-full h-full gap-y-2 first:rounded-t-md last:rounded-b-md p-1"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="item.id"
|
||||
v-on-long-press="[
|
||||
onLongPressCallbackHook,
|
||||
{
|
||||
delay: 1000,
|
||||
},
|
||||
]"
|
||||
class="bg-accented rounded-lg hover:bg-base-content/20 origin-to intersect:motion-preset-slide-down intersect:motion-ease-spring-bouncier intersect:motion-delay ease-in-out shadow"
|
||||
:class="{
|
||||
'bg-elevated/30 outline outline-accent hover:bg-base-content/20':
|
||||
selectedItems.has(item) ||
|
||||
(currentSelectedItem?.id === item.id &&
|
||||
longPressedHook &&
|
||||
!selectedItems.has(item)),
|
||||
'opacity-60 shadow-accent': selectedGroupItems?.some(
|
||||
(_item) => _item.id === item.id,
|
||||
),
|
||||
}"
|
||||
:style="{ '--motion-delay': `${50 * index}ms` }"
|
||||
@mousedown="
|
||||
longPressedHook
|
||||
? (currentSelectedItem = null)
|
||||
: (currentSelectedItem = item)
|
||||
"
|
||||
>
|
||||
<HaexPassMobileMenuItem
|
||||
v-bind="item"
|
||||
@click="onClickItemAsync(item)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex justify-center items-center flex-1"
|
||||
>
|
||||
<UiIconNoData class="text-primary size-24 shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vOnLongPress } from '@vueuse/components'
|
||||
import type { IPasswordMenuItem } from './types'
|
||||
|
||||
defineProps<{
|
||||
menuItems: IPasswordMenuItem[]
|
||||
}>()
|
||||
|
||||
defineEmits(['add'])
|
||||
const selectedItems = defineModel<Set<IPasswordMenuItem>>('selectedItems', {
|
||||
default: new Set(),
|
||||
})
|
||||
|
||||
const currentSelectedItem = ref<IPasswordMenuItem | null>()
|
||||
|
||||
const longPressedHook = ref(false)
|
||||
|
||||
const onLongPressCallbackHook = (_: PointerEvent) => {
|
||||
longPressedHook.value = true
|
||||
}
|
||||
|
||||
watch(longPressedHook, () => {
|
||||
if (!longPressedHook.value) selectedItems.value.clear()
|
||||
})
|
||||
|
||||
watch(selectedItems, () => {
|
||||
if (!selectedItems.value.size) longPressedHook.value = false
|
||||
})
|
||||
|
||||
const localePath = useLocalePath()
|
||||
const { ctrl } = useMagicKeys()
|
||||
const { search } = storeToRefs(useSearchStore())
|
||||
|
||||
const onClickItemAsync = async (item: IPasswordMenuItem) => {
|
||||
currentSelectedItem.value = null
|
||||
|
||||
if (longPressedHook.value || selectedItems.value.size || ctrl?.value) {
|
||||
if (selectedItems.value?.has(item)) {
|
||||
selectedItems.value.delete(item)
|
||||
} else {
|
||||
selectedItems.value?.add(item)
|
||||
}
|
||||
|
||||
if (!selectedItems.value.size) longPressedHook.value = false
|
||||
} else {
|
||||
if (item.type === 'group')
|
||||
await navigateTo(
|
||||
localePath({
|
||||
name: 'passwordGroupItems',
|
||||
params: {
|
||||
...useRouter().currentRoute.value.params,
|
||||
groupId: item.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
else {
|
||||
await navigateTo(
|
||||
localePath({
|
||||
name: 'passwordItemEdit',
|
||||
params: { ...useRouter().currentRoute.value.params, itemId: item.id },
|
||||
}),
|
||||
)
|
||||
}
|
||||
search.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const listRef = useTemplateRef('listRef')
|
||||
onClickOutside(listRef, async () => {
|
||||
// needed cause otherwise the unselect is to fast for other processing like "edit selected group"
|
||||
setTimeout(() => {
|
||||
longPressedHook.value = false
|
||||
}, 50)
|
||||
})
|
||||
|
||||
const { selectedGroupItems } = storeToRefs(usePasswordGroupStore())
|
||||
</script>
|
||||
@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
class="flex gap-4 w-full px-4 py-2"
|
||||
:style="{ color: menuItem.color ?? '' }"
|
||||
@click="$emit('click', menuItem)"
|
||||
>
|
||||
<Icon
|
||||
:name="menuIcon"
|
||||
size="24"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<p class="w-full flex-1 text-start truncate font-bold">
|
||||
{{ menuItem?.name }}
|
||||
</p>
|
||||
|
||||
<Icon
|
||||
v-if="menuItem.type === 'group'"
|
||||
name="mdi:chevron-right"
|
||||
size="24"
|
||||
class="text-base-content"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IPasswordMenuItem } from './types'
|
||||
|
||||
defineEmits<{ click: [group?: IPasswordMenuItem] }>()
|
||||
|
||||
const menuItem = defineProps<IPasswordMenuItem>()
|
||||
|
||||
const menuIcon = computed(() =>
|
||||
menuItem?.icon
|
||||
? menuItem.icon
|
||||
: menuItem.type === 'group'
|
||||
? 'mdi:folder-outline'
|
||||
: 'mdi:key-outline',
|
||||
)
|
||||
</script>
|
||||
@ -1,7 +0,0 @@
|
||||
export interface IPasswordMenuItem {
|
||||
color?: string | null
|
||||
icon: string | null
|
||||
id: string
|
||||
name: string | null
|
||||
type: 'group' | 'item'
|
||||
}
|
||||
@ -27,6 +27,11 @@ const items: DropdownMenuItem[] = [
|
||||
label: t('settings'),
|
||||
to: useLocalePath()({ name: 'settings' }),
|
||||
},
|
||||
{
|
||||
icon: 'mdi:code-braces',
|
||||
label: t('developer'),
|
||||
to: useLocalePath()({ name: 'settings-developer' }),
|
||||
},
|
||||
{
|
||||
icon: 'tabler:logout',
|
||||
label: t('close'),
|
||||
@ -39,9 +44,11 @@ const items: DropdownMenuItem[] = [
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
settings: 'Einstellungen'
|
||||
developer: 'Entwickler'
|
||||
close: 'Vault schließen'
|
||||
|
||||
en:
|
||||
settings: 'Settings'
|
||||
developer: 'Developer'
|
||||
close: 'Close Vault'
|
||||
</i18n>
|
||||
|
||||
Reference in New Issue
Block a user