mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 13:35:23 +02:00
feat: Migrate to Nuxt 3 framework (#5184)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
parent
89ab7fac25
commit
c24d532608
403 changed files with 23959 additions and 19557 deletions
|
@ -1,19 +1,17 @@
|
|||
<template>
|
||||
<div scoped-slot></div>
|
||||
<div scoped-slot />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
/**
|
||||
* Renderless component that only renders if the user is logged in.
|
||||
* and has advanced options toggled.
|
||||
*/
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
setup(_, ctx) {
|
||||
const { $auth } = useContext();
|
||||
const $auth = useMealieAuth();
|
||||
|
||||
const r = $auth?.user?.advanced || false;
|
||||
const r = $auth.user.value?.advanced || false;
|
||||
|
||||
return () => {
|
||||
return r ? ctx.slots.default?.() : null;
|
||||
|
|
|
@ -2,32 +2,33 @@
|
|||
<v-tooltip
|
||||
ref="copyToolTip"
|
||||
v-model="show"
|
||||
:color="copied? 'success lighten-1' : 'red lighten-1'"
|
||||
:color="copied? 'success-lighten-1' : 'red-lighten-1'"
|
||||
top
|
||||
:open-on-hover="false"
|
||||
:open-on-click="true"
|
||||
close-delay="500"
|
||||
transition="slide-y-transition"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
variant="flat"
|
||||
:icon="icon"
|
||||
:color="color"
|
||||
retain-focus-on-click
|
||||
:class="btnClass"
|
||||
:disabled="copyText !== '' ? false : true"
|
||||
@click="
|
||||
on.click;
|
||||
textToClipboard();
|
||||
"
|
||||
@blur="on.blur"
|
||||
v-bind="props"
|
||||
@click="textToClipboard()"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.contentCopy }}</v-icon>
|
||||
{{ icon ? "" : $t("general.copy") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>
|
||||
<v-icon left dark>
|
||||
<v-icon
|
||||
start
|
||||
dark
|
||||
>
|
||||
{{ $globals.icons.clipboardCheck }}
|
||||
</v-icon>
|
||||
<slot v-if="!isSupported"> {{ $t("general.your-browser-does-not-support-clipboard") }} </slot>
|
||||
|
@ -37,11 +38,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useClipboard } from "@vueuse/core"
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { VTooltip } from "~/types/vuetify";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
copyText: {
|
||||
type: String,
|
||||
|
@ -61,7 +60,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { copy, copied, isSupported } = useClipboard()
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
const show = ref(false);
|
||||
const copyToolTip = ref<VTooltip | null>(null);
|
||||
|
||||
|
@ -73,7 +72,7 @@ export default defineComponent({
|
|||
if (isSupported.value) {
|
||||
await copy(props.copyText);
|
||||
if (copied.value) {
|
||||
console.log(`Copied\n${props.copyText}`)
|
||||
console.log(`Copied\n${props.copyText}`);
|
||||
}
|
||||
else {
|
||||
console.warn("Copy failed: ", copied.value);
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
<template>
|
||||
<v-form ref="file">
|
||||
<input ref="uploader" class="d-none" type="file" :accept="accept" @change="onFileChanged" />
|
||||
<input
|
||||
ref="uploader"
|
||||
class="d-none"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
@change="onFileChanged"
|
||||
>
|
||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||
<v-btn :loading="isSelecting" :small="small" :color="color" :text="textBtn" :disabled="disabled" @click="onButtonClick">
|
||||
<v-icon left> {{ effIcon }}</v-icon>
|
||||
<v-btn
|
||||
:loading="isSelecting"
|
||||
:small="small"
|
||||
:color="color"
|
||||
:variant="textBtn ? 'text' : 'elevated'"
|
||||
:disabled="disabled"
|
||||
@click="onButtonClick"
|
||||
>
|
||||
<v-icon start>
|
||||
{{ effIcon }}
|
||||
</v-icon>
|
||||
{{ text ? text : defaultText }}
|
||||
</v-btn>
|
||||
</slot>
|
||||
|
@ -11,12 +26,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const UPLOAD_EVENT = "uploaded";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
|
@ -57,14 +71,15 @@ export default defineComponent({
|
|||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const file = ref<File | null>(null);
|
||||
const uploader = ref<HTMLInputElement | null>(null);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
const { i18n, $globals } = useContext();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const effIcon = props.icon ? props.icon : $globals.icons.upload;
|
||||
|
||||
const defaultText = i18n.t("general.upload");
|
||||
|
@ -82,11 +97,15 @@ export default defineComponent({
|
|||
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file.value);
|
||||
|
||||
const response = await api.upload.file(props.url, formData);
|
||||
|
||||
if (response) {
|
||||
context.emit(UPLOAD_EVENT, response);
|
||||
try {
|
||||
const response = await api.upload.file(props.url, formData);
|
||||
if (response) {
|
||||
context.emit(UPLOAD_EVENT, response);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
context.emit(UPLOAD_EVENT, null);
|
||||
}
|
||||
isSelecting.value = false;
|
||||
}
|
||||
|
@ -107,7 +126,7 @@ export default defineComponent({
|
|||
() => {
|
||||
isSelecting.value = false;
|
||||
},
|
||||
{ once: true }
|
||||
{ once: true },
|
||||
);
|
||||
uploader.value?.click();
|
||||
}
|
||||
|
|
|
@ -1,19 +1,36 @@
|
|||
<template>
|
||||
<div class="mx-auto my-3 justify-center" style="display: flex;">
|
||||
<div
|
||||
class="mx-auto my-3 justify-center"
|
||||
style="display: flex;"
|
||||
>
|
||||
<div style="display: inline;">
|
||||
<v-progress-circular :width="size.width" :size="size.size" color="primary lighten-2" indeterminate>
|
||||
<v-progress-circular
|
||||
:width="size.width"
|
||||
:size="size.size"
|
||||
color="primary-lighten-2"
|
||||
indeterminate
|
||||
>
|
||||
<div class="text-center">
|
||||
<v-icon :size="size.icon" color="primary lighten-2">
|
||||
<v-icon
|
||||
:size="size.icon"
|
||||
color="primary-lighten-2"
|
||||
>
|
||||
{{ $globals.icons.primary }}
|
||||
</v-icon>
|
||||
<div v-if="large" class="text-small">
|
||||
<div
|
||||
v-if="large"
|
||||
class="text-small"
|
||||
>
|
||||
<slot>
|
||||
{{ (small || tiny) ? "" : waitingText }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</v-progress-circular>
|
||||
<div v-if="!large" class="text-small">
|
||||
<div
|
||||
v-if="!large"
|
||||
class="text-small"
|
||||
>
|
||||
<slot>
|
||||
{{ (small || tiny) ? "" : waitingTextCalculated }}
|
||||
</slot>
|
||||
|
@ -23,9 +40,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
|
@ -50,7 +65,7 @@ export default defineComponent({
|
|||
waitingText: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const size = computed(() => {
|
||||
|
@ -67,7 +82,8 @@ export default defineComponent({
|
|||
icon: 30,
|
||||
size: 50,
|
||||
};
|
||||
} else if (props.large) {
|
||||
}
|
||||
else if (props.large) {
|
||||
return {
|
||||
width: 4,
|
||||
icon: 120,
|
||||
|
@ -81,7 +97,7 @@ export default defineComponent({
|
|||
};
|
||||
});
|
||||
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const waitingTextCalculated = props.waitingText == null ? i18n.t("general.loading-recipes") : props.waitingText;
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
<template>
|
||||
<v-toolbar color="transparent" flat>
|
||||
<BaseButton color="null" rounded secondary @click="$router.go(-1)">
|
||||
<template #icon> {{ $globals.icons.arrowLeftBold }}</template>
|
||||
<v-toolbar
|
||||
color="transparent"
|
||||
flat
|
||||
>
|
||||
<BaseButton
|
||||
color="null"
|
||||
rounded
|
||||
secondary
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.arrowLeftBold }}
|
||||
</template>
|
||||
{{ $t('general.back') }}
|
||||
</BaseButton>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
back: {
|
||||
type: Boolean,
|
||||
|
|
|
@ -1,49 +1,64 @@
|
|||
<template>
|
||||
<v-card :color="color" :dark="dark" flat :width="width" class="my-2">
|
||||
<v-card
|
||||
:color="color"
|
||||
:dark="dark"
|
||||
flat
|
||||
:width="width"
|
||||
class="my-2"
|
||||
>
|
||||
<v-row>
|
||||
<v-col v-for="(inputField, index) in items" :key="index" class="py-0" cols="12" sm="12">
|
||||
<v-divider v-if="inputField.section" class="my-2" />
|
||||
<v-card-title v-if="inputField.section" class="pl-0">
|
||||
<v-col
|
||||
v-for="(inputField, index) in items"
|
||||
:key="index"
|
||||
class="py-0"
|
||||
cols="12"
|
||||
sm="12"
|
||||
>
|
||||
<v-divider
|
||||
v-if="inputField.section"
|
||||
class="my-2"
|
||||
/>
|
||||
<v-card-title
|
||||
v-if="inputField.section"
|
||||
class="pl-0"
|
||||
>
|
||||
{{ inputField.section }}
|
||||
</v-card-title>
|
||||
<v-card-text v-if="inputField.sectionDetails" class="pl-0 mt-0 pt-0">
|
||||
<v-card-text
|
||||
v-if="inputField.sectionDetails"
|
||||
class="pl-0 mt-0 pt-0"
|
||||
>
|
||||
{{ inputField.sectionDetails }}
|
||||
</v-card-text>
|
||||
|
||||
<!-- Check Box -->
|
||||
<v-checkbox
|
||||
v-if="inputField.type === fieldTypes.BOOLEAN"
|
||||
v-model="value[inputField.varName]"
|
||||
class="my-0 py-0"
|
||||
v-model="modelValue[inputField.varName]"
|
||||
:name="inputField.varName"
|
||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||
@change="emitBlur"
|
||||
>
|
||||
:hint="inputField.hint"
|
||||
hide-details="auto"
|
||||
density="comfortable"
|
||||
@change="emitBlur">
|
||||
<template #label>
|
||||
<div>
|
||||
<v-card-text class="text-body-1 my-0 py-0">
|
||||
{{ inputField.label }}
|
||||
</v-card-text>
|
||||
<v-card-text v-if="inputField.hint" class="text-caption my-0 py-0">
|
||||
{{ inputField.hint }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<span class="ml-4">
|
||||
{{ inputField.label }}
|
||||
</span>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
|
||||
<!-- Text Field -->
|
||||
<v-text-field
|
||||
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
|
||||
v-model="value[inputField.varName]"
|
||||
v-model="modelValue[inputField.varName]"
|
||||
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
|
||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||
filled
|
||||
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
:autofocus="index === 0"
|
||||
dense
|
||||
density="comfortable"
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:hint="inputField.hint || ''"
|
||||
|
@ -55,15 +70,15 @@
|
|||
<!-- Text Area -->
|
||||
<v-textarea
|
||||
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
||||
v-model="value[inputField.varName]"
|
||||
v-model="modelValue[inputField.varName]"
|
||||
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
|
||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||
filled
|
||||
rounded
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="rounded-lg"
|
||||
rows="3"
|
||||
auto-grow
|
||||
dense
|
||||
density="comfortable"
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:hint="inputField.hint || ''"
|
||||
|
@ -75,42 +90,53 @@
|
|||
<!-- Option Select -->
|
||||
<v-select
|
||||
v-else-if="inputField.type === fieldTypes.SELECT"
|
||||
v-model="value[inputField.varName]"
|
||||
v-model="modelValue[inputField.varName]"
|
||||
:readonly="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (readonlyFields && readonlyFields.includes(inputField.varName))"
|
||||
:disabled="(inputField.disableUpdate && updateMode) || (!updateMode && inputField.disableCreate) || (disabledFields && disabledFields.includes(inputField.varName))"
|
||||
filled
|
||||
rounded
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="rounded-lg"
|
||||
:prepend-icon="inputField.icons ? value[inputField.varName] : null"
|
||||
:prepend-icon="inputField.icons ? modelValue[inputField.varName] : null"
|
||||
:label="inputField.label"
|
||||
:name="inputField.varName"
|
||||
:items="inputField.options"
|
||||
:item-text="inputField.itemText"
|
||||
:item-title="inputField.itemText"
|
||||
:item-value="inputField.itemValue"
|
||||
:return-object="false"
|
||||
:hint="inputField.hint"
|
||||
density="comfortable"
|
||||
persistent-hint
|
||||
lazy-validation
|
||||
@blur="emitBlur"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<div>
|
||||
<v-list-item-title>{{ item.raw.text }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.raw.description }}</v-list-item-subtitle>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- Color Picker -->
|
||||
<div v-else-if="inputField.type === fieldTypes.COLOR" class="d-flex" style="width: 100%">
|
||||
<div
|
||||
v-else-if="inputField.type === fieldTypes.COLOR"
|
||||
class="d-flex"
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ on }">
|
||||
<v-btn class="my-2 ml-auto" style="min-width: 200px" :color="value[inputField.varName]" dark v-on="on">
|
||||
<template #activator="{ props: templateProps }">
|
||||
<v-btn
|
||||
class="my-2 ml-auto"
|
||||
style="min-width: 200px"
|
||||
:color="modelValue[inputField.varName]"
|
||||
dark
|
||||
v-bind="templateProps"
|
||||
>
|
||||
{{ inputField.label }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-color-picker
|
||||
v-model="value[inputField.varName]"
|
||||
v-model="modelValue[inputField.varName]"
|
||||
value="#7417BE"
|
||||
hide-canvas
|
||||
hide-inputs
|
||||
|
@ -122,21 +148,34 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="inputField.type === fieldTypes.OBJECT">
|
||||
<auto-form v-model="value[inputField.varName]" :color="color" :items="inputField.items" @blur="emitBlur" />
|
||||
<auto-form
|
||||
v-model="modelValue[inputField.varName]"
|
||||
:color="color"
|
||||
:items="inputField.items"
|
||||
@blur="emitBlur"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- List Type -->
|
||||
<div v-else-if="inputField.type === fieldTypes.LIST">
|
||||
<div v-for="(item, idx) in value[inputField.varName]" :key="idx">
|
||||
<div
|
||||
v-for="(item, idx) in modelValue[inputField.varName]"
|
||||
:key="idx"
|
||||
>
|
||||
<p>
|
||||
{{ inputField.label }} {{ idx + 1 }}
|
||||
<span>
|
||||
<BaseButton class="ml-5" x-small delete @click="removeByIndex(value[inputField.varName], idx)" />
|
||||
<BaseButton
|
||||
class="ml-5"
|
||||
x-small
|
||||
delete
|
||||
@click="removeByIndex(modelValue[inputField.varName], idx)"
|
||||
/>
|
||||
</span>
|
||||
</p>
|
||||
<v-divider class="mb-5 mx-2" />
|
||||
<auto-form
|
||||
v-model="value[inputField.varName][idx]"
|
||||
v-model="modelValue[inputField.varName][idx]"
|
||||
:color="color"
|
||||
:items="inputField.items"
|
||||
@blur="emitBlur"
|
||||
|
@ -144,7 +183,10 @@
|
|||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<BaseButton small @click="value[inputField.varName].push(getTemplate(inputField.items))">
|
||||
<BaseButton
|
||||
small
|
||||
@click="modelValue[inputField.varName].push(getTemplate(inputField.items))"
|
||||
>
|
||||
{{ $t("general.new") }}
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
|
@ -154,111 +196,96 @@
|
|||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
<script lang="ts" setup>
|
||||
import { validators } from "@/composables/use-validators";
|
||||
import { fieldTypes } from "@/composables/forms";
|
||||
import { AutoFormItems } from "~/types/auto-forms";
|
||||
import type { AutoFormItems } from "~/types/auto-forms";
|
||||
|
||||
const BLUR_EVENT = "blur";
|
||||
|
||||
type ValidatorKey = keyof typeof validators;
|
||||
|
||||
export default defineComponent({
|
||||
name: "AutoForm",
|
||||
props: {
|
||||
value: {
|
||||
default: null,
|
||||
type: [Object, Array],
|
||||
},
|
||||
updateMode: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
items: {
|
||||
default: null,
|
||||
type: Array as () => AutoFormItems,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: "max",
|
||||
},
|
||||
globalRules: {
|
||||
default: null,
|
||||
type: Array as () => string[],
|
||||
},
|
||||
color: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
dark: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
disabledFields: {
|
||||
default: null,
|
||||
type: Array as () => string[],
|
||||
},
|
||||
readonlyFields: {
|
||||
default: null,
|
||||
type: Array as () => string[],
|
||||
},
|
||||
// Use defineModel for v-model
|
||||
const modelValue = defineModel<[object, Array<any>]>();
|
||||
|
||||
const props = defineProps({
|
||||
updateMode: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
setup(props, context) {
|
||||
function rulesByKey(keys?: ValidatorKey[] | null) {
|
||||
if (keys === undefined || keys === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list = [] as ((v: string) => boolean | string)[];
|
||||
keys.forEach((key) => {
|
||||
const split = key.split(":");
|
||||
const validatorKey = split[0] as ValidatorKey;
|
||||
if (validatorKey in validators) {
|
||||
if (split.length === 1) {
|
||||
// @ts-ignore- validators[validatorKey] is a function
|
||||
list.push(validators[validatorKey]);
|
||||
} else {
|
||||
// @ts-ignore - validators[validatorKey] is a function
|
||||
list.push(validators[validatorKey](split[1]));
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
|
||||
|
||||
function removeByIndex(list: never[], index: number) {
|
||||
// Removes the item at the index
|
||||
list.splice(index, 1);
|
||||
}
|
||||
|
||||
function getTemplate(item: AutoFormItems) {
|
||||
const obj = {} as { [key: string]: string };
|
||||
|
||||
item.forEach((field) => {
|
||||
obj[field.varName] = "";
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function emitBlur() {
|
||||
context.emit(BLUR_EVENT, props.value);
|
||||
}
|
||||
|
||||
return {
|
||||
rulesByKey,
|
||||
defaultRules,
|
||||
removeByIndex,
|
||||
getTemplate,
|
||||
emitBlur,
|
||||
fieldTypes,
|
||||
validators,
|
||||
};
|
||||
items: {
|
||||
default: null,
|
||||
type: Array as () => AutoFormItems,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: "max",
|
||||
},
|
||||
globalRules: {
|
||||
default: null,
|
||||
type: Array as () => string[],
|
||||
},
|
||||
color: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
dark: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
disabledFields: {
|
||||
default: null,
|
||||
type: Array as () => string[],
|
||||
},
|
||||
readonlyFields: {
|
||||
default: null,
|
||||
type: Array as () => string[],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["blur", "update:modelValue"]);
|
||||
|
||||
function rulesByKey(keys?: ValidatorKey[] | null) {
|
||||
if (keys === undefined || keys === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list = [] as ((v: string) => boolean | string)[];
|
||||
keys.forEach((key) => {
|
||||
const split = key.split(":");
|
||||
const validatorKey = split[0] as ValidatorKey;
|
||||
if (validatorKey in validators) {
|
||||
if (split.length === 1) {
|
||||
list.push(validators[validatorKey]);
|
||||
}
|
||||
else {
|
||||
list.push(validators[validatorKey](split[1]));
|
||||
}
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
const defaultRules = computed(() => rulesByKey(props.globalRules as ValidatorKey[]));
|
||||
|
||||
function removeByIndex(list: never[], index: number) {
|
||||
// Removes the item at the index
|
||||
list.splice(index, 1);
|
||||
}
|
||||
|
||||
function getTemplate(item: AutoFormItems) {
|
||||
const obj = {} as { [key: string]: string };
|
||||
|
||||
item.forEach((field) => {
|
||||
obj[field.varName] = "";
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function emitBlur() {
|
||||
emit(BLUR_EVENT, modelValue.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
<template>
|
||||
<BannerWarning
|
||||
:title="$tc('banner-experimental.title')"
|
||||
:description="$tc('banner-experimental.description')"
|
||||
:title="$t('banner-experimental.title')"
|
||||
:description="$t('banner-experimental.description')"
|
||||
>
|
||||
<template v-if="issue" #default>
|
||||
<a :href="issue" target="_blank">{{ $t("banner-experimental.issue-link-text") }}</a>
|
||||
<template
|
||||
v-if="issue"
|
||||
#default
|
||||
>
|
||||
<a
|
||||
:href="issue"
|
||||
target="_blank"
|
||||
>{{ $t("banner-experimental.issue-link-text") }}</a>
|
||||
</template>
|
||||
</BannerWarning>
|
||||
</template>
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
<template>
|
||||
<v-alert border="left" colored-border type="warning" elevation="2" :icon="$globals.icons.alert">
|
||||
<v-alert
|
||||
border="start"
|
||||
border-color
|
||||
variant="tonal"
|
||||
type="warning"
|
||||
elevation="2"
|
||||
:icon="$globals.icons.alert"
|
||||
>
|
||||
<b v-if="title">{{ title }}</b>
|
||||
<div v-if="description">{{ description }}</div>
|
||||
<div v-if="$slots.default" class="py-2">
|
||||
<slot></slot>
|
||||
<div v-if="description">
|
||||
{{ description }}
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.default"
|
||||
class="py-2"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<template>
|
||||
<v-btn
|
||||
:color="color || btnAttrs.color"
|
||||
:small="small"
|
||||
:size="small ? 'small' : 'default'"
|
||||
:x-small="xSmall"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:outlined="btnStyle.outlined"
|
||||
:text="btnStyle.text"
|
||||
:variant="disabled ? 'tonal' : btnStyle.outlined ? 'outlined' : btnStyle.text ? 'text' : 'elevated'"
|
||||
:to="to"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@click="download ? downloadFile() : undefined"
|
||||
>
|
||||
<v-icon v-if="!iconRight" left>
|
||||
<v-icon
|
||||
v-if="!iconRight"
|
||||
start
|
||||
>
|
||||
<slot name="icon">
|
||||
{{ icon || btnAttrs.icon }}
|
||||
</slot>
|
||||
|
@ -20,7 +21,10 @@
|
|||
<slot name="default">
|
||||
{{ text || btnAttrs.text }}
|
||||
</slot>
|
||||
<v-icon v-if="iconRight" right>
|
||||
<v-icon
|
||||
v-if="iconRight"
|
||||
end
|
||||
>
|
||||
<slot name="icon">
|
||||
{{ icon || btnAttrs.icon }}
|
||||
</slot>
|
||||
|
@ -29,10 +33,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
name: "BaseButton",
|
||||
props: {
|
||||
// Types
|
||||
|
@ -117,7 +120,8 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $globals, i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const buttonOptions = {
|
||||
create: {
|
||||
text: i18n.t("general.create"),
|
||||
|
@ -159,15 +163,20 @@ export default defineComponent({
|
|||
const btnAttrs = computed(() => {
|
||||
if (props.delete) {
|
||||
return buttonOptions.delete;
|
||||
} else if (props.update) {
|
||||
}
|
||||
else if (props.update) {
|
||||
return buttonOptions.update;
|
||||
} else if (props.edit) {
|
||||
}
|
||||
else if (props.edit) {
|
||||
return buttonOptions.edit;
|
||||
} else if (props.cancel) {
|
||||
}
|
||||
else if (props.cancel) {
|
||||
return buttonOptions.cancel;
|
||||
} else if (props.save) {
|
||||
}
|
||||
else if (props.save) {
|
||||
return buttonOptions.save;
|
||||
} else if (props.download) {
|
||||
}
|
||||
else if (props.download) {
|
||||
return buttonOptions.download;
|
||||
}
|
||||
return buttonOptions.create;
|
||||
|
@ -191,7 +200,8 @@ export default defineComponent({
|
|||
const btnStyle = computed(() => {
|
||||
if (props.secondary) {
|
||||
return buttonStyles.secondary;
|
||||
} else if (props.minor || props.cancel) {
|
||||
}
|
||||
else if (props.minor || props.cancel) {
|
||||
return buttonStyles.minor;
|
||||
}
|
||||
return buttonStyles.defaults;
|
||||
|
|
|
@ -1,20 +1,44 @@
|
|||
<template>
|
||||
<v-item-group>
|
||||
<template v-for="btn in buttons">
|
||||
<v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-y top left :style="stretch ? 'width: 100%;' : ''">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn tile :large="large" icon v-bind="attrs" v-on="on">
|
||||
<v-menu
|
||||
v-if="btn.children"
|
||||
:key="'menu-' + btn.event"
|
||||
active-class="pa-0"
|
||||
offset-y
|
||||
top
|
||||
start
|
||||
:style="stretch ? 'width: 100%;' : ''"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
tile
|
||||
:large="large"
|
||||
icon
|
||||
variant="plain"
|
||||
v-bind="props"
|
||||
>
|
||||
<v-icon>
|
||||
{{ btn.icon }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<template v-for="(child, idx) in btn.children">
|
||||
<v-list-item :key="idx" dense @click="$emit(child.event)">
|
||||
<v-list density="compact">
|
||||
<template
|
||||
v-for="(child, idx) in btn.children"
|
||||
:key="idx"
|
||||
>
|
||||
<v-list-item
|
||||
density="compact"
|
||||
@click="$emit(child.event)"
|
||||
>
|
||||
<v-list-item-title>{{ child.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider v-if="child.divider" :key="`divider-${idx}`" class="my-1"></v-divider>
|
||||
<v-divider
|
||||
v-if="child.divider"
|
||||
:key="`divider-${idx}`"
|
||||
class="my-1"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
@ -23,11 +47,11 @@
|
|||
:key="'btn-' + btn.event"
|
||||
open-delay="200"
|
||||
transition="slide-y-reverse-transition"
|
||||
dense
|
||||
density="compact"
|
||||
bottom
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
tile
|
||||
icon
|
||||
|
@ -35,8 +59,8 @@
|
|||
:large="large"
|
||||
:disabled="btn.disabled"
|
||||
:style="stretch ? `width: ${maxButtonWidth};` : ''"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
variant="plain"
|
||||
v-bind="props"
|
||||
@click="$emit(btn.event)"
|
||||
>
|
||||
<v-icon> {{ btn.icon }} </v-icon>
|
||||
|
@ -49,8 +73,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export interface ButtonOption {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
|
@ -61,7 +83,7 @@ export interface ButtonOption {
|
|||
divider?: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
buttons: {
|
||||
type: Array as () => ButtonOption[],
|
||||
|
@ -74,13 +96,13 @@ export default defineComponent({
|
|||
stretch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const maxButtonWidth = computed(() => `${100 / props.buttons.length}%`);
|
||||
return {
|
||||
maxButtonWidth,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -7,25 +7,29 @@
|
|||
'mt-8': section,
|
||||
}"
|
||||
>
|
||||
<v-card-title class="headline pl-0 py-0">
|
||||
<v-icon v-if="icon !== ''" left>
|
||||
<v-card-title class="text-h5 pl-0 py-0" style="font-weight: normal;">
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
start
|
||||
>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
<v-card-text v-if="$slots.default" class="pt-2 pl-0">
|
||||
<v-card-text
|
||||
v-if="$slots.default"
|
||||
class="pt-2 pl-0"
|
||||
>
|
||||
<p class="pb-0 mb-0">
|
||||
<slot />
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
<v-divider class="mb-3" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
|
|
|
@ -1,59 +1,77 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot name="activator" v-bind="{ open }" />
|
||||
<slot
|
||||
name="activator"
|
||||
v-bind="{ open }"
|
||||
/>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
absolute
|
||||
:width="width"
|
||||
:max-width="maxWidth"
|
||||
:max-width="maxWidth ?? undefined"
|
||||
:content-class="top ? 'top-dialog' : undefined"
|
||||
:fullscreen="$vuetify.breakpoint.xsOnly"
|
||||
@keydown.enter="
|
||||
$emit('submit');
|
||||
dialog = false;
|
||||
"
|
||||
@click:outside="$emit('cancel')"
|
||||
@keydown.esc="$emit('cancel')"
|
||||
:fullscreen="$vuetify.display.xs"
|
||||
@keydown.enter="() => {
|
||||
emit('submit'); dialog = false;
|
||||
}"
|
||||
@click:outside="emit('cancel')"
|
||||
@keydown.esc="emit('cancel')"
|
||||
>
|
||||
<v-card height="100%">
|
||||
<v-app-bar dark dense :color="color" class="">
|
||||
<v-icon large left>
|
||||
<v-toolbar
|
||||
dark
|
||||
density="comfortable"
|
||||
:color="color"
|
||||
class="px-3 position-relative top-0 left-0 w-100"
|
||||
>
|
||||
<v-icon size="large">
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-progress-linear v-if="loading" class="mt-1" indeterminate color="primary"></v-progress-linear>
|
||||
<v-toolbar-title class="headline">
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
class="mt-1"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<slot v-bind="{ submitEvent }" />
|
||||
</div>
|
||||
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-divider class="mx-2" />
|
||||
|
||||
<v-card-actions>
|
||||
<slot name="card-actions">
|
||||
<v-btn
|
||||
text
|
||||
variant="text"
|
||||
color="grey"
|
||||
@click="
|
||||
dialog = false;
|
||||
$emit('cancel');
|
||||
emit('cancel');
|
||||
"
|
||||
>
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
|
||||
<slot name="custom-card-action"></slot>
|
||||
<BaseButton v-if="$listeners.delete" delete secondary @click="deleteEvent" />
|
||||
<slot name="custom-card-action" />
|
||||
<BaseButton
|
||||
v-if="$listeners.confirm"
|
||||
v-if="canDelete"
|
||||
delete
|
||||
secondary
|
||||
@click="deleteEvent"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="canConfirm"
|
||||
:color="color"
|
||||
type="submit"
|
||||
:disabled="submitDisabled"
|
||||
@click="
|
||||
$emit('confirm');
|
||||
emit('confirm');
|
||||
dialog = false;
|
||||
"
|
||||
>
|
||||
|
@ -63,141 +81,127 @@
|
|||
{{ $t("general.confirm") }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="$listeners.submit"
|
||||
v-if="canSubmit"
|
||||
type="submit"
|
||||
:disabled="submitDisabled"
|
||||
@click="submitEvent"
|
||||
>
|
||||
{{ submitText }}
|
||||
<template v-if="submitIcon" #icon>
|
||||
<template
|
||||
v-if="submitIcon"
|
||||
#icon
|
||||
>
|
||||
{{ submitIcon }}
|
||||
</template>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</v-card-actions>
|
||||
|
||||
<div v-if="$slots['below-actions']" class="pb-4">
|
||||
<slot name="below-actions"> </slot>
|
||||
<div
|
||||
v-if="$slots['below-actions']"
|
||||
class="pb-4"
|
||||
>
|
||||
<slot name="below-actions" />
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||
export default defineComponent({
|
||||
name: "BaseDialog",
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "Modal Title",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: "500",
|
||||
},
|
||||
maxWidth: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
top: {
|
||||
default: null,
|
||||
type: Boolean,
|
||||
},
|
||||
submitIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
submitText: {
|
||||
type: String,
|
||||
default: function () {
|
||||
return this.$t("general.create");
|
||||
},
|
||||
},
|
||||
submitDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
keepOpen: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const dialog = computed<boolean>({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("input", val);
|
||||
},
|
||||
});
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from "#app";
|
||||
|
||||
return {
|
||||
dialog,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
submitted: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
determineClose(): boolean {
|
||||
return this.submitted && !this.loading && !this.keepOpen;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
determineClose() {
|
||||
this.submitted = false;
|
||||
this.dialog = false;
|
||||
},
|
||||
dialog(val) {
|
||||
if (val) this.submitted = false;
|
||||
if (!val) this.$emit("close");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submitEvent() {
|
||||
this.$emit("submit");
|
||||
this.submitted = true;
|
||||
},
|
||||
deleteEvent() {
|
||||
this.$emit("delete");
|
||||
this.submitted = true;
|
||||
},
|
||||
open() {
|
||||
this.dialog = true;
|
||||
this.logDeprecatedProp("open");
|
||||
},
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.logDeprecatedProp("close");
|
||||
},
|
||||
logDeprecatedProp(val: string) {
|
||||
console.warn(
|
||||
`[BaseDialog] The method '${val}' is deprecated. Please use v-model="value" to manage state instead.`
|
||||
);
|
||||
},
|
||||
},
|
||||
interface DialogProps {
|
||||
modelValue: boolean;
|
||||
color?: string;
|
||||
title?: string;
|
||||
icon?: string | null;
|
||||
width?: number | string;
|
||||
maxWidth?: number | string | null;
|
||||
loading?: boolean;
|
||||
top?: boolean | null;
|
||||
submitIcon?: string | null;
|
||||
submitText?: string;
|
||||
submitDisabled?: boolean;
|
||||
keepOpen?: boolean;
|
||||
// actions
|
||||
canDelete?: boolean;
|
||||
canConfirm?: boolean;
|
||||
canSubmit?: boolean;
|
||||
}
|
||||
|
||||
interface DialogEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "submit" | "cancel" | "confirm" | "delete" | "close"): void;
|
||||
}
|
||||
|
||||
// Using TypeScript interface with withDefaults for props
|
||||
const props = withDefaults(defineProps<DialogProps>(), {
|
||||
color: "primary",
|
||||
title: "Modal Title",
|
||||
icon: null,
|
||||
width: "500",
|
||||
maxWidth: null,
|
||||
loading: false,
|
||||
top: null,
|
||||
submitIcon: null,
|
||||
submitText: () => useNuxtApp().$i18n.t("general.create"),
|
||||
submitDisabled: false,
|
||||
keepOpen: false,
|
||||
canDelete: false,
|
||||
canConfirm: false,
|
||||
canSubmit: false,
|
||||
});
|
||||
const emit = defineEmits<DialogEmits>();
|
||||
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: val => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
const submitted = ref(false);
|
||||
|
||||
const determineClose = computed(() => {
|
||||
return submitted.value && !props.loading && !props.keepOpen;
|
||||
});
|
||||
|
||||
watch(determineClose, (shouldClose) => {
|
||||
if (shouldClose) {
|
||||
submitted.value = false;
|
||||
dialog.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
watch(dialog, (val) => {
|
||||
if (val) submitted.value = false;
|
||||
if (!val) emit("close");
|
||||
});
|
||||
|
||||
function submitEvent() {
|
||||
emit("submit");
|
||||
submitted.value = true;
|
||||
}
|
||||
|
||||
function deleteEvent() {
|
||||
emit("delete");
|
||||
submitted.value = true;
|
||||
}
|
||||
|
||||
function open() {
|
||||
dialog.value = true;
|
||||
logDeprecatedProp("open");
|
||||
}
|
||||
|
||||
/* function close() {
|
||||
dialog.value = false;
|
||||
logDeprecatedProp("close");
|
||||
} */
|
||||
|
||||
function logDeprecatedProp(val: string) {
|
||||
console.warn(
|
||||
`[BaseDialog] The method '${val}' is deprecated. Please use v-model="value" to manage state instead.`,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<v-divider :width="width" :class="color" :style="`border-width: ${thickness} !important`" />
|
||||
<v-divider
|
||||
:width="width"
|
||||
:class="color"
|
||||
:style="`border-width: ${thickness} !important`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
width: {
|
||||
type: String,
|
||||
|
|
|
@ -1,59 +1,104 @@
|
|||
<template>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="primary" v-bind="{ ...attrs, ...$attrs }" :class="btnClass" :disabled="disabled" v-on="on">
|
||||
<v-icon v-if="activeObj.icon" left>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
v-bind="{ ...props, ...$attrs }"
|
||||
:class="btnClass"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<v-icon
|
||||
v-if="activeObj.icon"
|
||||
start
|
||||
>
|
||||
{{ activeObj.icon }}
|
||||
</v-icon>
|
||||
{{ mode === MODES.model ? activeObj.text : btnText }}
|
||||
<v-icon right>
|
||||
<v-icon end>
|
||||
{{ $globals.icons.chevronDown }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<!-- Model -->
|
||||
<v-list v-if="mode === MODES.model" dense>
|
||||
<v-list-item-group v-model="itemGroup">
|
||||
<template v-for="(item, index) in items">
|
||||
<div v-if="!item.hide" :key="index">
|
||||
<v-list-item @click="setValue(item)">
|
||||
<v-list-item-icon v-if="item.icon">
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider v-if="item.divider" :key="`divider-${index}`" class="my-1" ></v-divider>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<!-- Links -->
|
||||
<v-list v-else-if="mode === MODES.link" dense>
|
||||
<v-list-item-group v-model="itemGroup">
|
||||
<template v-for="(item, index) in items">
|
||||
<div v-if="!item.hide" :key="index">
|
||||
<v-list-item :to="item.to">
|
||||
<v-list-item-icon v-if="item.icon">
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider v-if="item.divider" :key="`divider-${index}`" class="my-1" ></v-divider>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<!-- Event -->
|
||||
<v-list v-else-if="mode === MODES.event" dense>
|
||||
<v-list
|
||||
v-if="mode === MODES.model"
|
||||
v-model:selected="itemGroup"
|
||||
density="compact"
|
||||
>
|
||||
<template v-for="(item, index) in items">
|
||||
<div v-if="!item.hide" :key="index">
|
||||
<v-list-item @click="$emit(item.event)">
|
||||
<v-list-item-icon v-if="item.icon">
|
||||
<div
|
||||
v-if="!item.hide"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item @click="setValue(item)">
|
||||
<template
|
||||
v-if="item.icon"
|
||||
#prepend
|
||||
>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider v-if="item.divider" :key="`divider-${index}`" class="my-1" ></v-divider>
|
||||
<v-divider
|
||||
v-if="item.divider"
|
||||
:key="`divider-${index}`"
|
||||
class="my-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list>
|
||||
<!-- Links -->
|
||||
<v-list
|
||||
v-else-if="mode === MODES.link"
|
||||
v-model:selected="itemGroup"
|
||||
density="compact"
|
||||
>
|
||||
<template v-for="(item, index) in items">
|
||||
<div
|
||||
v-if="!item.hide"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item :to="item.to">
|
||||
<template
|
||||
v-if="item.icon"
|
||||
#prepend
|
||||
>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider
|
||||
v-if="item.divider"
|
||||
:key="`divider-${index}`"
|
||||
class="my-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list>
|
||||
<!-- Event -->
|
||||
<v-list
|
||||
v-else-if="mode === MODES.event"
|
||||
density="compact"
|
||||
>
|
||||
<template v-for="(item, index) in items">
|
||||
<div
|
||||
v-if="!item.hide"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item @click="$emit(item.event)">
|
||||
<template
|
||||
v-if="item.icon"
|
||||
#prepend
|
||||
>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider
|
||||
v-if="item.divider"
|
||||
:key="`divider-${index}`"
|
||||
class="my-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list>
|
||||
|
@ -61,18 +106,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
|
||||
const INPUT_EVENT = "input";
|
||||
|
||||
type modes = "model" | "link" | "event";
|
||||
|
||||
const MODES = {
|
||||
model: "model",
|
||||
link: "link",
|
||||
event: "event",
|
||||
};
|
||||
|
||||
type modes = "model" | "link" | "event";
|
||||
|
||||
export interface MenuItem {
|
||||
text: string;
|
||||
icon?: string;
|
||||
|
@ -83,7 +124,7 @@ export interface MenuItem {
|
|||
hide?: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
mode: {
|
||||
type: String as () => modes,
|
||||
|
@ -98,7 +139,7 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
|
@ -112,10 +153,11 @@ export default defineComponent({
|
|||
type: String,
|
||||
required: false,
|
||||
default: function () {
|
||||
return this.$t("general.actions");
|
||||
}
|
||||
return useI18n().t("general.actions");
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const activeObj = ref<MenuItem>({
|
||||
text: "DEFAULT",
|
||||
|
@ -124,7 +166,7 @@ export default defineComponent({
|
|||
|
||||
let startIndex = 0;
|
||||
props.items.forEach((item, index) => {
|
||||
if (item.value === props.value) {
|
||||
if (item.value === props.modelValue) {
|
||||
startIndex = index;
|
||||
|
||||
activeObj.value = item;
|
||||
|
@ -133,7 +175,7 @@ export default defineComponent({
|
|||
const itemGroup = ref(startIndex);
|
||||
|
||||
function setValue(v: MenuItem) {
|
||||
context.emit(INPUT_EVENT, v.value);
|
||||
context.emit("update:modelValue", v.value);
|
||||
activeObj.value = v;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
<template>
|
||||
<div class="mt-4">
|
||||
<section class="d-flex flex-column align-center">
|
||||
<slot name="header"></slot>
|
||||
<h2 class="headline">
|
||||
<slot name="title"> 👋 Here's a Title </slot>
|
||||
<slot name="header" />
|
||||
<h2 class="text-h5">
|
||||
<slot name="title">
|
||||
👋 Here's a Title
|
||||
</slot>
|
||||
</h2>
|
||||
|
||||
<h3 class="subtitle-1">
|
||||
<slot> </slot>
|
||||
<slot />
|
||||
</h3>
|
||||
</section>
|
||||
<section class="d-flex">
|
||||
<slot name="content"></slot>
|
||||
<slot name="content" />
|
||||
</section>
|
||||
<v-divider v-if="divider" class="my-4"></v-divider>
|
||||
<v-divider
|
||||
v-if="divider"
|
||||
class="my-4"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
divider: {
|
||||
type: Boolean,
|
||||
|
@ -29,3 +32,11 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.subtitle-1 {
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
color: var(--v-text-caption);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
||||
<v-card
|
||||
v-bind="$attrs"
|
||||
:class="classes"
|
||||
class="v-card--material pa-3"
|
||||
>
|
||||
<div class="d-flex grow flex-wrap">
|
||||
<slot name="avatar">
|
||||
<v-sheet
|
||||
|
@ -10,12 +14,24 @@
|
|||
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
||||
dark
|
||||
>
|
||||
<v-icon v-if="icon" size="40"> {{ icon }} </v-icon>
|
||||
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
size="40"
|
||||
>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<div
|
||||
v-if="text"
|
||||
class="headline font-weight-thin"
|
||||
v-text="text"
|
||||
/>
|
||||
</v-sheet>
|
||||
</slot>
|
||||
|
||||
<div v-if="$slots['after-heading']" class="ml-auto">
|
||||
<div
|
||||
v-if="$slots['after-heading']"
|
||||
class="ml-auto"
|
||||
>
|
||||
<slot name="after-heading" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,7 +47,10 @@
|
|||
</template>
|
||||
|
||||
<template v-if="$slots.bottom">
|
||||
<v-divider v-if="!$slots.actions" class="mt-2" />
|
||||
<v-divider
|
||||
v-if="!$slots.actions"
|
||||
class="mt-2"
|
||||
/>
|
||||
|
||||
<div class="pb-0">
|
||||
<slot name="bottom" />
|
||||
|
@ -41,9 +60,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
name: "MaterialCard",
|
||||
|
||||
props: {
|
||||
|
@ -73,14 +90,13 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup() {
|
||||
const { $vuetify } = useContext();
|
||||
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const hasHeading = computed(() => false);
|
||||
const hasAltHeading = computed(() => false);
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
"v-card--material--has-heading": hasHeading,
|
||||
"mt-3": $vuetify.breakpoint.name === "xs" || $vuetify.breakpoint.name === "sm",
|
||||
"mt-3": $vuetify.display.name.value === "xs" || $vuetify.display.name.value === "sm",
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -3,14 +3,30 @@
|
|||
<LanguageDialog v-model="langDialog" />
|
||||
<v-card>
|
||||
<div>
|
||||
<v-toolbar width="100%" color="primary" class="d-flex justify-center" style="margin-bottom: 4rem" dark>
|
||||
<v-toolbar-title class="headline text-h4"> Mealie </v-toolbar-title>
|
||||
<v-toolbar
|
||||
width="100%"
|
||||
color="primary"
|
||||
class="d-flex justify-center"
|
||||
style="margin-bottom: 4rem"
|
||||
dark
|
||||
>
|
||||
<v-toolbar-title class="headline text-h4">
|
||||
Mealie
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<div class="icon-container">
|
||||
<v-divider class="icon-divider"></v-divider>
|
||||
<v-avatar class="pa-2 icon-avatar" color="primary" size="75">
|
||||
<svg class="icon-white" style="width: 75" viewBox="0 0 24 24">
|
||||
<v-divider class="icon-divider" />
|
||||
<v-avatar
|
||||
class="pa-2 icon-avatar"
|
||||
color="primary"
|
||||
size="75"
|
||||
>
|
||||
<svg
|
||||
class="icon-white"
|
||||
style="width: 75"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||
/>
|
||||
|
@ -19,12 +35,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-center grow items-center my-4">
|
||||
<slot :width="pageWidth"></slot>
|
||||
<slot :width="pageWidth" />
|
||||
</div>
|
||||
<div class="mx-2 my-4">
|
||||
<v-progress-linear
|
||||
v-if="value > 0"
|
||||
:value="Math.ceil((value/maxPageNumber)*100)"
|
||||
v-if="wizardPage > 0"
|
||||
:value="Math.ceil((wizardPage / maxPageNumber) * 100)"
|
||||
striped
|
||||
height="10"
|
||||
/>
|
||||
|
@ -45,12 +61,17 @@
|
|||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="nextButtonShow"
|
||||
variant="elevated"
|
||||
:disabled="!nextButtonEnable"
|
||||
:color="nextButtonColorRef"
|
||||
@click="incrementPage"
|
||||
>
|
||||
<div v-if="isSubmitting">
|
||||
<v-progress-circular indeterminate color="white" size="24" />
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="white"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-icon v-if="nextButtonIconRef && !nextButtonIconAfter">
|
||||
|
@ -64,8 +85,14 @@
|
|||
</v-btn>
|
||||
</v-card-actions>
|
||||
<v-card-actions class="justify-center flex-column py-8">
|
||||
<BaseButton large color="primary" @click="langDialog = true">
|
||||
<template #icon> {{ $globals.icons.translate }}</template>
|
||||
<BaseButton
|
||||
large
|
||||
color="primary"
|
||||
@click="langDialog = true"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.translate }}
|
||||
</template>
|
||||
{{ $t("language-dialog.choose-language") }}
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
|
@ -74,11 +101,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
|
@ -157,56 +182,65 @@ export default defineComponent({
|
|||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "submit"],
|
||||
setup(props, context) {
|
||||
const { $globals, i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const ready = ref(false);
|
||||
const langDialog = ref(false);
|
||||
|
||||
const prevButtonTextRef = computed(() => props.prevButtonText || i18n.tc("general.back"));
|
||||
const wizardPage = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => context.emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const prevButtonTextRef = computed(() => props.prevButtonText || i18n.t("general.back"));
|
||||
const prevButtonIconRef = computed(() => props.prevButtonIcon || $globals.icons.back);
|
||||
const nextButtonTextRef = computed(
|
||||
() => props.nextButtonText || (
|
||||
props.nextButtonIsSubmit ? i18n.tc("general.submit") : i18n.tc("general.next")
|
||||
)
|
||||
);
|
||||
props.nextButtonIsSubmit ? i18n.t("general.submit") : i18n.t("general.next")
|
||||
),
|
||||
);
|
||||
const nextButtonIconRef = computed(
|
||||
() => props.nextButtonIcon || (
|
||||
props.nextButtonIsSubmit ? $globals.icons.createAlt : $globals.icons.forward
|
||||
)
|
||||
props.nextButtonIsSubmit ? $globals.icons.createAlt : $globals.icons.forward
|
||||
),
|
||||
);
|
||||
const nextButtonColorRef = computed(
|
||||
() => props.nextButtonColor || (props.nextButtonIsSubmit ? "success" : "info")
|
||||
() => props.nextButtonColor || (props.nextButtonIsSubmit ? "success" : "info"),
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page < props.minPageNumber) {
|
||||
goToPage(props.minPageNumber);
|
||||
return;
|
||||
} else if (page > props.maxPageNumber) {
|
||||
}
|
||||
else if (page > props.maxPageNumber) {
|
||||
goToPage(props.maxPageNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
context.emit("input", page);
|
||||
wizardPage.value = page;
|
||||
}
|
||||
|
||||
function decrementPage() {
|
||||
goToPage(props.value - 1);
|
||||
goToPage(wizardPage.value - 1);
|
||||
}
|
||||
|
||||
function incrementPage() {
|
||||
if (props.nextButtonIsSubmit) {
|
||||
context.emit("submit", props.value);
|
||||
} else {
|
||||
goToPage(props.value + 1);
|
||||
context.emit("submit", wizardPage.value);
|
||||
}
|
||||
else {
|
||||
goToPage(wizardPage.value + 1);
|
||||
}
|
||||
}
|
||||
|
||||
ready.value = true;
|
||||
|
||||
return {
|
||||
wizardPage,
|
||||
ready,
|
||||
langDialog,
|
||||
prevButtonTextRef,
|
||||
|
@ -217,7 +251,7 @@ export default defineComponent({
|
|||
decrementPage,
|
||||
incrementPage,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-btn outlined class="rounded-xl my-1 mx-1" :to="to">
|
||||
<v-icon v-if="icon != ''" left>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
class="rounded-xl my-1 mx-1"
|
||||
:to="to"
|
||||
>
|
||||
<v-icon
|
||||
v-if="icon != ''"
|
||||
start
|
||||
>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
{{ text }}
|
||||
|
@ -10,9 +17,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-menu
|
||||
offset-y
|
||||
left
|
||||
start
|
||||
:bottom="!menuTop"
|
||||
:nudge-bottom="!menuTop ? '5' : '0'"
|
||||
:top="menuTop"
|
||||
|
@ -11,18 +11,30 @@
|
|||
open-on-hover
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
:class="{ 'rounded-circle': fab }"
|
||||
:small="fab"
|
||||
:color="color"
|
||||
:icon="!fab"
|
||||
dark
|
||||
v-bind="props"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon>{{ $globals.icons.dotsVertical }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(item, index) in items" :key="index" @click="$emit(item.event)">
|
||||
<v-list-item-icon>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
@click="$emit(item.event)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="item.color ? item.color : undefined">
|
||||
{{ item.icon }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
@ -30,10 +42,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { ContextMenuItem } from "~/composables/use-context-presents";
|
||||
import type { ContextMenuItem } from "~/composables/use-context-presents";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
|
@ -49,7 +60,7 @@ export default defineComponent({
|
|||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "grey darken-2",
|
||||
default: "grey-darken-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card-actions>
|
||||
<v-menu v-if="tableConfig.hideColumns" offset-y bottom nudge-bottom="6" :close-on-content-click="false">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="accent" class="mr-2" dark v-bind="attrs" v-on="on">
|
||||
<v-menu
|
||||
v-if="tableConfig.hideColumns"
|
||||
offset-y
|
||||
bottom
|
||||
nudge-bottom="6"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
color="accent"
|
||||
variant="elevated"
|
||||
v-bind="props"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.cog }}
|
||||
</v-icon>
|
||||
|
@ -16,12 +26,12 @@
|
|||
:key="itemValue.text + itemValue.show"
|
||||
v-model="filteredHeaders"
|
||||
:value="itemValue.value"
|
||||
dense
|
||||
density="compact"
|
||||
flat
|
||||
inset
|
||||
:label="itemValue.text"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
@ -30,41 +40,52 @@
|
|||
:disabled="selected.length < 1"
|
||||
mode="event"
|
||||
color="info"
|
||||
variant="elevated"
|
||||
:items="bulkActions"
|
||||
v-on="bulkActionListener"
|
||||
>
|
||||
</BaseOverflowButton>
|
||||
<slot name="button-row"> </slot>
|
||||
v-bind="bulkActionListener"
|
||||
/>
|
||||
<slot name="button-row" />
|
||||
</v-card-actions>
|
||||
<div class="mx-2 clip-width">
|
||||
<v-text-field v-model="search" :label="$t('search.search')"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
variant="underlined"
|
||||
:label="$t('search.search')"
|
||||
/>
|
||||
</div>
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
item-key="id"
|
||||
:show-select="bulkActions.length > 0"
|
||||
:headers="activeHeaders"
|
||||
:sort-by="initialSort"
|
||||
:sort-desc="initialSortDesc"
|
||||
:show-select="bulkActions.length > 0"
|
||||
:sort-by="sortBy"
|
||||
:items="data || []"
|
||||
:items-per-page="15"
|
||||
:search="search"
|
||||
class="elevation-2"
|
||||
>
|
||||
<template v-for="header in activeHeaders" #[`item.${header.value}`]="{ item }">
|
||||
<slot :name="'item.' + header.value" v-bind="{ item }"> {{ item[header.value] }}</slot>
|
||||
<template
|
||||
v-for="header in headersWithoutActions"
|
||||
#[`item.${header.value}`]="{ item }"
|
||||
>
|
||||
<slot
|
||||
:name="'item.' + header.value"
|
||||
v-bind="{ item }"
|
||||
>
|
||||
{{ item[header.value] }}
|
||||
</slot>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.edit,
|
||||
text: $tc('general.edit'),
|
||||
text: $t('general.edit'),
|
||||
event: 'edit',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $tc('general.delete'),
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
]"
|
||||
|
@ -74,8 +95,11 @@
|
|||
</template>
|
||||
</v-data-table>
|
||||
<v-card-actions class="justify-end">
|
||||
<slot name="button-bottom"> </slot>
|
||||
<BaseButton color="info" @click="downloadAsJson(data, 'export.json')">
|
||||
<slot name="button-bottom" />
|
||||
<BaseButton
|
||||
color="info"
|
||||
@click="downloadAsJson(data, 'export.json')"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.download }}
|
||||
</template>
|
||||
|
@ -86,7 +110,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { downloadAsJson } from "~/composables/use-utils";
|
||||
|
||||
export interface TableConfig {
|
||||
|
@ -109,7 +132,7 @@ export interface BulkAction {
|
|||
event: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
tableConfig: {
|
||||
type: Object as () => TableConfig,
|
||||
|
@ -139,28 +162,34 @@ export default defineComponent({
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["delete-one", "edit-one"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const sortBy = computed(() => [{
|
||||
key: props.initialSort,
|
||||
order: props.initialSortDesc ? "desc" : "asc",
|
||||
}]);
|
||||
|
||||
// ===========================================================
|
||||
// Reactive Headers
|
||||
const filteredHeaders = ref<string[]>([]);
|
||||
|
||||
// Set default filtered
|
||||
filteredHeaders.value = (() => {
|
||||
const filtered: string[] = [];
|
||||
props.headers.forEach((element) => {
|
||||
if (element.show) {
|
||||
filtered.push(element.value);
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
})();
|
||||
|
||||
const activeHeaders = computed(() => {
|
||||
const filtered = props.headers.filter((header) => filteredHeaders.value.includes(header.value));
|
||||
filtered.push({ text: "", value: "actions", show: true, align: "right" });
|
||||
return filtered;
|
||||
const filteredHeaders = computed<string[]>(() => {
|
||||
return props.headers.filter(header => header.show).map(header => header.value);
|
||||
});
|
||||
|
||||
const headersWithoutActions = computed(() =>
|
||||
props.headers
|
||||
.filter(header => filteredHeaders.value.includes(header.value))
|
||||
.map(header => ({
|
||||
...header,
|
||||
title: i18n.t(header.text),
|
||||
})),
|
||||
);
|
||||
|
||||
const activeHeaders = computed(() => [
|
||||
...headersWithoutActions.value,
|
||||
{ title: "", value: "actions", show: true, align: "end" },
|
||||
]);
|
||||
|
||||
const selected = ref<any[]>([]);
|
||||
|
||||
// ===========================================================
|
||||
|
@ -183,8 +212,10 @@ export default defineComponent({
|
|||
const search = ref("");
|
||||
|
||||
return {
|
||||
sortBy,
|
||||
selected,
|
||||
filteredHeaders,
|
||||
headersWithoutActions,
|
||||
activeHeaders,
|
||||
bulkActionListener,
|
||||
search,
|
||||
|
@ -198,4 +229,7 @@ export default defineComponent({
|
|||
.clip-width {
|
||||
max-width: 400px;
|
||||
}
|
||||
.v-btn--disabled {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<template>
|
||||
<v-btn x-small :href="href" color="primary" target="_blank">
|
||||
<v-icon left small>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
:href="href"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
{{ $globals.icons.folderOutline }}
|
||||
</v-icon>
|
||||
{{ $tc("about.docs") }}
|
||||
{{ $t("about.docs") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
link: {
|
||||
type: String,
|
||||
|
|
|
@ -1,27 +1,39 @@
|
|||
<template>
|
||||
<div ref="el" :class="isOverDropZone ? 'over' : ''">
|
||||
<div v-if="isOverDropZone" class="overlay"></div>
|
||||
<div v-if="isOverDropZone" class="absolute text-container">
|
||||
<p class="text-center drop-text"> {{ $t("recipe.drop-image") }} </p>
|
||||
<div
|
||||
ref="el"
|
||||
:class="isOverDropZone ? 'over' : ''"
|
||||
>
|
||||
<div
|
||||
v-if="isOverDropZone"
|
||||
class="overlay"
|
||||
/>
|
||||
<div
|
||||
v-if="isOverDropZone"
|
||||
class="absolute text-container"
|
||||
>
|
||||
<p class="text-center drop-text">
|
||||
{{ $t("recipe.drop-image") }}
|
||||
</p>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { useDropZone } from "@vueuse/core";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
emits: ["drop"],
|
||||
setup(_, context) {
|
||||
const el = ref<HTMLDivElement>();
|
||||
|
||||
function onDrop(files: File[]) {
|
||||
context.emit("drop", files);
|
||||
function onDrop(files: File[] | null) {
|
||||
if (files) {
|
||||
context.emit("drop", files);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore - This should work?
|
||||
const { isOverDropZone } = useDropZone(el, onDrop);
|
||||
const { isOverDropZone } = useDropZone(el, files => onDrop(files));
|
||||
|
||||
return { el, isOverDropZone };
|
||||
},
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-menu top offset-y :right="right" :left="!right" open-on-hover>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn :small="small" icon v-bind="attrs" v-on="on" @click.stop>
|
||||
<v-icon :small="small"> {{ $globals.icons.help }} </v-icon>
|
||||
<v-menu
|
||||
top
|
||||
offset-y
|
||||
:right="right"
|
||||
:left="!right"
|
||||
open-on-hover
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
:size="small ? 'small' : undefined"
|
||||
icon
|
||||
v-bind="props"
|
||||
variant="flat"
|
||||
@click.stop
|
||||
>
|
||||
<v-icon :small="small">
|
||||
{{ $globals.icons.help }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card max-width="300px">
|
||||
<v-card-text>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
@ -16,9 +30,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
|
|
|
@ -1,152 +1,165 @@
|
|||
<template>
|
||||
<v-container class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="8" align-self="center">
|
||||
<Cropper
|
||||
ref="cropper"
|
||||
class="cropper"
|
||||
:src="img"
|
||||
:default-size="defaultSize"
|
||||
:style="`height: ${cropperHeight}; width: ${cropperWidth};`"
|
||||
/>
|
||||
<v-container class="pa-0">
|
||||
<v-row no-gutters>
|
||||
<v-col
|
||||
cols="8"
|
||||
align-self="center"
|
||||
>
|
||||
<Cropper
|
||||
ref="cropper"
|
||||
class="cropper"
|
||||
:src="img"
|
||||
:default-size="defaultSize"
|
||||
:style="`height: ${cropperHeight}; width: ${cropperWidth};`"
|
||||
/>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col
|
||||
cols="2"
|
||||
align-self="center"
|
||||
>
|
||||
<v-container class="pa-0 mx-0">
|
||||
<v-row
|
||||
v-for="(row, keyRow) in controls"
|
||||
:key="keyRow"
|
||||
>
|
||||
<v-col
|
||||
v-for="(control, keyControl) in row"
|
||||
:key="keyControl"
|
||||
:cols="12 / row.length"
|
||||
class="py-2 mx-0"
|
||||
style="display: flex; align-items: center; justify-content: center;"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
:color="control.color"
|
||||
@click="control.callback()"
|
||||
>
|
||||
<v-icon> {{ control.icon }} </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
<v-col cols="2" align-self="center">
|
||||
<v-container class="pa-0 mx-0">
|
||||
<v-row v-for="(row, keyRow) in controls" :key="keyRow">
|
||||
<v-col
|
||||
v-for="(control, keyControl) in row" :key="keyControl"
|
||||
:cols="12 / row.length"
|
||||
class="py-2 mx-0"
|
||||
style="display: flex; align-items: center; justify-content: center;"
|
||||
>
|
||||
<v-btn icon :color="control.color" @click="control.callback()">
|
||||
<v-icon> {{ control.icon }} </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
import { Cropper } from "vue-advanced-cropper";
|
||||
import "vue-advanced-cropper/dist/style.css";
|
||||
|
||||
export default defineComponent({
|
||||
components: { Cropper },
|
||||
props: {
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cropperHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
cropperWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
export default defineNuxtComponent({
|
||||
components: { Cropper },
|
||||
props: {
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup(_, context) {
|
||||
const cropper = ref<Cropper>();
|
||||
const { $globals } = useContext();
|
||||
|
||||
interface Control {
|
||||
color: string;
|
||||
icon: string;
|
||||
callback: CallableFunction;
|
||||
}
|
||||
|
||||
const controls = ref<Control[][]>([
|
||||
[
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.flipHorizontal,
|
||||
callback: () => flip(true, false),
|
||||
},
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.flipVertical,
|
||||
callback: () => flip(false, true),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.rotateLeft,
|
||||
callback: () => rotate(-90),
|
||||
},
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.rotateRight,
|
||||
callback: () => rotate(90),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
color: "success",
|
||||
icon: $globals.icons.save,
|
||||
callback: () => save(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropper.value.rotate(angle);
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { canvas } = cropper.value.getResult();
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
context.emit("save", blob);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
cropper,
|
||||
controls,
|
||||
flip,
|
||||
rotate,
|
||||
save,
|
||||
};
|
||||
cropperHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
cropperWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ["save"],
|
||||
setup(_, context) {
|
||||
const cropper = ref<Cropper>();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
methods: {
|
||||
// @ts-expect-error https://advanced-cropper.github.io/vue-advanced-cropper/guides/advanced-recipes.html
|
||||
defaultSize({ imageSize, visibleArea }) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
height: (visibleArea || imageSize).height,
|
||||
};
|
||||
interface Control {
|
||||
color: string;
|
||||
icon: string;
|
||||
callback: CallableFunction;
|
||||
}
|
||||
|
||||
const controls = ref<Control[][]>([
|
||||
[
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.flipHorizontal,
|
||||
callback: () => flip(true, false),
|
||||
},
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.flipVertical,
|
||||
callback: () => flip(false, true),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.rotateLeft,
|
||||
callback: () => rotate(-90),
|
||||
},
|
||||
{
|
||||
color: "info",
|
||||
icon: $globals.icons.rotateRight,
|
||||
callback: () => rotate(90),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
color: "success",
|
||||
icon: $globals.icons.save,
|
||||
callback: () => save(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
cropper.value.rotate(angle);
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { canvas } = cropper.value.getResult();
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
context.emit("save", blob);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cropper,
|
||||
controls,
|
||||
flip,
|
||||
rotate,
|
||||
save,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
// @ts-expect-error https://advanced-cropper.github.io/vue-advanced-cropper/guides/advanced-recipes.html
|
||||
defaultSize({ imageSize, visibleArea }) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
height: (visibleArea || imageSize).height,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,22 +1,44 @@
|
|||
<template>
|
||||
<v-text-field v-model="inputVal" :label="$t('general.color')">
|
||||
<v-text-field
|
||||
v-model="inputVal"
|
||||
:label="$t('general.color')"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-btn class="elevation-0" small height="30px" width="30px" :color="inputVal || 'grey'" @click="setRandomHex">
|
||||
<v-btn
|
||||
class="elevation-0"
|
||||
size="small"
|
||||
height="30px"
|
||||
width="30px"
|
||||
:color="inputVal || 'grey'"
|
||||
@click="setRandomHex"
|
||||
>
|
||||
<v-icon color="white">
|
||||
{{ $globals.icons.refreshCircle }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-menu v-model="menu" left nudge-left="30" nudge-top="20" :close-on-content-click="false">
|
||||
<template #activator="{ on }">
|
||||
<v-icon v-on="on">
|
||||
<v-menu
|
||||
v-model="menu"
|
||||
start
|
||||
nudge-left="30"
|
||||
nudge-top="20"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-icon v-bind="props">
|
||||
{{ $globals.icons.formatColorFill }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text class="pa-0">
|
||||
<v-color-picker v-model="inputVal" flat hide-inputs show-swatches swatches-max-height="200" />
|
||||
<v-color-picker
|
||||
v-model="inputVal"
|
||||
flat
|
||||
hide-inputs
|
||||
show-swatches
|
||||
swatches-max-height="200"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
@ -25,24 +47,23 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const menu = ref(false);
|
||||
|
||||
const inputVal = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,22 +3,27 @@
|
|||
ref="autocompleteRef"
|
||||
v-model="itemVal"
|
||||
v-bind="$attrs"
|
||||
:search-input.sync="searchInput"
|
||||
item-text="name"
|
||||
v-model:search="searchInput"
|
||||
item-title="name"
|
||||
return-object
|
||||
:items="items"
|
||||
:prepend-icon="icon || $globals.icons.tags"
|
||||
auto-select-first
|
||||
clearable
|
||||
color="primary"
|
||||
hide-details
|
||||
@keyup.enter="emitCreate"
|
||||
>
|
||||
<template v-if="$listeners.create" #no-data>
|
||||
<div class="caption text-center pb-2">{{ $t("recipe.press-enter-to-create") }}</div>
|
||||
</template>
|
||||
<template v-if="$listeners.create" #append-item>
|
||||
<template
|
||||
v-if="create"
|
||||
#append-item
|
||||
>
|
||||
<div class="px-2">
|
||||
<BaseButton block small @click="emitCreate"></BaseButton>
|
||||
<BaseButton
|
||||
block
|
||||
size="small"
|
||||
@click="emitCreate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
@ -44,13 +49,13 @@
|
|||
* Both the ID and Item can be synced. The item can be synced using the v-model syntax and the itemId can be synced
|
||||
* using the .sync syntax `item-id.sync="item.labelId"`
|
||||
*/
|
||||
import { computed, defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineComponent({
|
||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object as () => MultiPurposeLabelSummary | IngredientFood | IngredientUnit,
|
||||
required: false,
|
||||
default: () => {
|
||||
|
@ -70,7 +75,12 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "update:item-id", "create"],
|
||||
setup(props, context) {
|
||||
const autocompleteRef = ref<HTMLInputElement>();
|
||||
const searchInput = ref("");
|
||||
|
@ -85,11 +95,16 @@ export default defineComponent({
|
|||
|
||||
const itemVal = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
try {
|
||||
return Object.keys(props.modelValue).length !== 0 ? props.modelValue : null;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: (val) => {
|
||||
itemIdVal.value = val?.id || undefined;
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div class="d-flex align-center" style="max-width: 60px">
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="max-width: 60px"
|
||||
>
|
||||
<v-text-field
|
||||
v-model.number="quantity"
|
||||
hide-details
|
||||
|
@ -8,17 +11,14 @@
|
|||
:max="max"
|
||||
type="number"
|
||||
class="rounded-xl"
|
||||
small
|
||||
text
|
||||
>
|
||||
</v-text-field>
|
||||
size="small"
|
||||
variant="plain"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
name: "VInputNumber",
|
||||
props: {
|
||||
min: {
|
||||
|
@ -37,18 +37,19 @@ export default defineComponent({
|
|||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const quantity = computed({
|
||||
get: () => {
|
||||
return Number(props.value);
|
||||
return Number(props.modelValue);
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,52 +1,70 @@
|
|||
<template>
|
||||
<BaseDialog v-model="dialog" :icon="$globals.icons.translate" :title="$tc('language-dialog.choose-language')">
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
:icon="$globals.icons.translate"
|
||||
:title="$t('language-dialog.choose-language')"
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("language-dialog.select-description") }}
|
||||
<v-autocomplete v-model="locale" :items="locales" item-text="name" class="my-3" hide-details outlined offset>
|
||||
<template #item="{ item }">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ item.name }} </v-list-item-title>
|
||||
<v-list-item-subtitle> {{ item.progress }}% {{ $tc("language-dialog.translated") }} </v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-autocomplete
|
||||
v-model="locale"
|
||||
:items="locales"
|
||||
item-title="name"
|
||||
class="my-3"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
offset
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<div
|
||||
v-bind="props"
|
||||
class="px-2 py-2"
|
||||
>
|
||||
<v-list-item-title> {{ item.raw.name }} </v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.raw.progress }}% {{ $t("language-dialog.translated") }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
<i18n path="language-dialog.how-to-contribute-description">
|
||||
<i18n-t keypath="language-dialog.how-to-contribute-description">
|
||||
<template #read-the-docs-link>
|
||||
<a href="https://docs.mealie.io/contributors/translating/" target="_blank">{{
|
||||
$t("language-dialog.read-the-docs")
|
||||
}}</a>
|
||||
<a
|
||||
href="https://docs.mealie.io/contributors/translating/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("language-dialog.read-the-docs") }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n>
|
||||
</i18n-t>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import type { LocaleObject } from "@nuxtjs/i18n";
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const dialog = computed<boolean>({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("input", val);
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const { locales: LOCALES, locale, i18n } = useLocales();
|
||||
watch(locale, () => {
|
||||
dialog.value = false; // Close dialog when locale changes
|
||||
});
|
||||
|
||||
const locales = LOCALES.filter((locale) =>
|
||||
(i18n.locales as LocaleObject[]).map((i18nLocale) => i18nLocale.code).includes(locale.value)
|
||||
const locales = LOCALES.filter(lc =>
|
||||
i18n.locales.value.map(i18nLocale => i18nLocale.code).includes(lc.value as any),
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -58,5 +76,3 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="displayPreview" class="d-flex justify-end">
|
||||
<div
|
||||
v-if="displayPreview"
|
||||
class="d-flex justify-end"
|
||||
>
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: previewState ? $globals.icons.edit : $globals.icons.eye,
|
||||
text: previewState ? $tc('general.edit') : $tc('markdown-editor.preview-markdown-button-label'),
|
||||
text: previewState ? $t('general.edit') : $t('markdown-editor.preview-markdown-button-label'),
|
||||
event: 'toggle',
|
||||
},
|
||||
]"
|
||||
|
@ -19,20 +22,22 @@
|
|||
:class="label == '' ? '' : 'mt-5'"
|
||||
:label="label"
|
||||
auto-grow
|
||||
dense
|
||||
density="compact"
|
||||
rows="4"
|
||||
variant="underlined"
|
||||
/>
|
||||
<SafeMarkdown
|
||||
v-else
|
||||
:source="modelValue"
|
||||
/>
|
||||
<SafeMarkdown v-else :source="value" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
name: "MarkdownEditor",
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
@ -53,6 +58,7 @@ export default defineComponent({
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "input:preview"],
|
||||
setup(props, context) {
|
||||
const fallbackPreview = ref(false);
|
||||
const previewState = computed({
|
||||
|
@ -62,7 +68,8 @@ export default defineComponent({
|
|||
set: (val) => {
|
||||
if (props.preview) {
|
||||
context.emit("input:preview", val);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
fallbackPreview.value = val;
|
||||
}
|
||||
},
|
||||
|
@ -70,10 +77,10 @@ export default defineComponent({
|
|||
|
||||
const inputVal = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("input", val);
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
return {
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
<template>
|
||||
<VJsoneditor
|
||||
:value="value"
|
||||
:height="height"
|
||||
:options="options"
|
||||
:attrs="$attrs"
|
||||
@input="$emit('input', $event)"
|
||||
></VJsoneditor>
|
||||
<JsonEditorVue
|
||||
:model-value="modelValue"
|
||||
v-bind="$attrs"
|
||||
:style="{ height }"
|
||||
:stringified="false"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-ignore v-jsoneditor has no types
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { defineComponent } from "vue";
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { VJsoneditor },
|
||||
name: "RecipeJsonEditor",
|
||||
components: { JsonEditorVue },
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
@ -24,10 +24,34 @@ export default defineComponent({
|
|||
type: String,
|
||||
default: "1500px",
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(_, { emit }) {
|
||||
function parseEvent(event: any): object {
|
||||
if (!event) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
if (event.json) {
|
||||
return event.json;
|
||||
}
|
||||
else if (event.text) {
|
||||
return JSON.parse(event.text);
|
||||
}
|
||||
else {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
function onChange(event: any) {
|
||||
emit("update:modelValue", parseEvent(event));
|
||||
}
|
||||
return {
|
||||
onChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,19 +5,22 @@
|
|||
item-key="id"
|
||||
class="elevation-0"
|
||||
:items-per-page="50"
|
||||
@click:row="handleRowClick"
|
||||
@click:row="($event, { item }) => handleRowClick(item)"
|
||||
>
|
||||
<template #item.category="{ item }">
|
||||
<template #[`item.category`]="{ item }">
|
||||
{{ capitalize(item.category) }}
|
||||
</template>
|
||||
<template #item.timestamp="{ item }">
|
||||
{{ $d(Date.parse(item.timestamp), "long") }}
|
||||
<template #[`item.timestamp`]="{ item }">
|
||||
{{ $d(Date.parse(item.timestamp!), "long") }}
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
{{ capitalize(item.status) }}
|
||||
<template #[`item.status`]="{ item }">
|
||||
{{ capitalize(item.status!) }}
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn icon @click.stop="deleteReport(item.id)">
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="deleteReport(item.id)"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
@ -25,27 +28,27 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||
import { ReportSummary } from "~/lib/api/types/reports";
|
||||
import type { ReportSummary } from "~/lib/api/types/reports";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
required: true,
|
||||
type: Array as () => Array<ReportSummary>,
|
||||
},
|
||||
},
|
||||
emits: ["delete"],
|
||||
|
||||
setup(_, context) {
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const headers = [
|
||||
{ text: i18n.t("category.category"), value: "category" },
|
||||
{ text: i18n.t("general.name"), value: "name" },
|
||||
{ text: i18n.t("general.timestamp"), value: "timestamp" },
|
||||
{ text: i18n.t("general.status"), value: "status" },
|
||||
{ text: i18n.t("general.delete"), value: "actions" },
|
||||
{ title: i18n.t("category.category"), value: "category", key: "category" },
|
||||
{ title: i18n.t("general.name"), value: "name", key: "name" },
|
||||
{ title: i18n.t("general.timestamp"), value: "timestamp", key: "timestamp" },
|
||||
{ title: i18n.t("general.status"), value: "status", key: "status" },
|
||||
{ title: i18n.t("general.delete"), value: "actions", key: "actions" },
|
||||
];
|
||||
|
||||
function handleRowClick(item: ReportSummary) {
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
<template>
|
||||
<VueMarkdown :source="sanitizeMarkdown(source)"></VueMarkdown>
|
||||
<MDC
|
||||
:value="value"
|
||||
tag="article"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-ignore vue-markdown has no types
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VueMarkdown,
|
||||
},
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
source: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props) {
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
|
@ -28,40 +25,44 @@ export default defineComponent({
|
|||
// List based on
|
||||
// https://support.zendesk.com/hc/en-us/articles/4408824584602-Allowing-unsafe-HTML-in-help-center-articles
|
||||
ALLOWED_TAGS: [
|
||||
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
||||
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
|
||||
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
|
||||
],
|
||||
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
||||
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
|
||||
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
|
||||
],
|
||||
ADD_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border",
|
||||
],
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border",
|
||||
],
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
const value = computed(() => {
|
||||
return sanitizeMarkdown(props.source) || "";
|
||||
});
|
||||
|
||||
return {
|
||||
sanitizeMarkdown,
|
||||
value,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep table {
|
||||
:deep(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep th, ::v-deep td {
|
||||
:deep(th, td) {
|
||||
border: 1px solid;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
::v-deep th {
|
||||
:deep(th) {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
<template>
|
||||
<v-card :min-width="minWidth" :to="to" :hover="to ? true : false">
|
||||
<v-card
|
||||
:min-width="minWidth"
|
||||
:to="to"
|
||||
:hover="to ? true : false"
|
||||
>
|
||||
<div class="d-flex flex-no-wrap">
|
||||
<v-avatar class="ml-3 mr-0 mt-3" color="primary" size="36">
|
||||
<v-icon color="white" class="pa-1">
|
||||
<v-avatar
|
||||
class="ml-3 mr-0 mt-3"
|
||||
color="primary"
|
||||
size="36"
|
||||
>
|
||||
<v-icon
|
||||
color="white"
|
||||
class="pa-1"
|
||||
size="x-large"
|
||||
>
|
||||
{{ activeIcon }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<v-card-title class="text-subtitle-1 pt-2 pb-2">
|
||||
<slot name="title"></slot>
|
||||
<slot name="title" />
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="pb-2">
|
||||
<slot name="value"></slot>
|
||||
<slot name="value" />
|
||||
</v-card-subtitle>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,9 +31,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
|
@ -37,7 +47,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $globals } = useContext();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const activeIcon = computed(() => {
|
||||
return props.icon ?? $globals.icons.primary;
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<component :is="tag">
|
||||
<slot name="activator" v-bind="{ toggle, state }"> </slot>
|
||||
<slot v-bind="{ state, toggle }"></slot>
|
||||
<slot
|
||||
name="activator"
|
||||
v-bind="{ toggle, state }"
|
||||
/>
|
||||
<slot v-bind="{ state, toggle }" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -19,7 +20,8 @@ export default defineComponent({
|
|||
default: "div",
|
||||
},
|
||||
},
|
||||
setup(_, context) {
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const state = ref(false);
|
||||
|
||||
const toggle = () => {
|
||||
|
@ -27,7 +29,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
watch(state, () => {
|
||||
context.emit("input", state);
|
||||
context.emit("update:modelValue", state.value);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,49 +1,53 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="wakeIsSupported"
|
||||
class="d-print-none d-flex px-2"
|
||||
:class="$vuetify.breakpoint.smAndDown ? 'justify-center' : 'justify-end'"
|
||||
>
|
||||
<v-switch v-model="wakeLock" small :label="$t('recipe.screen-awake')" />
|
||||
</div>
|
||||
<div
|
||||
v-if="wakeIsSupported"
|
||||
class="d-print-none d-flex px-2"
|
||||
:class="$vuetify.display.smAndDown ? 'justify-center' : 'justify-end'"
|
||||
>
|
||||
<v-switch
|
||||
v-model="wakeLock"
|
||||
color="primary"
|
||||
:label="$t('recipe.screen-awake')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, onMounted, onUnmounted } from "@nuxtjs/composition-api";
|
||||
import { useWakeLock } from "@vueuse/core";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { isSupported: wakeIsSupported, isActive, request, release } = useWakeLock();
|
||||
const wakeLock = computed({
|
||||
get: () => isActive.value,
|
||||
set: () => {
|
||||
if (isActive.value) {
|
||||
unlockScreen();
|
||||
} else {
|
||||
lockScreen();
|
||||
}
|
||||
},
|
||||
});
|
||||
async function lockScreen() {
|
||||
if (wakeIsSupported) {
|
||||
console.debug("Wake Lock Requested");
|
||||
await request("screen");
|
||||
}
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const { isSupported: wakeIsSupported, isActive, request, release } = useWakeLock();
|
||||
const wakeLock = computed({
|
||||
get: () => isActive.value,
|
||||
set: () => {
|
||||
if (isActive.value) {
|
||||
unlockScreen();
|
||||
}
|
||||
async function unlockScreen() {
|
||||
if (wakeIsSupported || isActive) {
|
||||
console.debug("Wake Lock Released");
|
||||
await release();
|
||||
}
|
||||
else {
|
||||
lockScreen();
|
||||
}
|
||||
onMounted(() => lockScreen());
|
||||
onUnmounted(() => unlockScreen());
|
||||
},
|
||||
});
|
||||
async function lockScreen() {
|
||||
if (wakeIsSupported) {
|
||||
console.debug("Wake Lock Requested");
|
||||
await request("screen");
|
||||
}
|
||||
}
|
||||
async function unlockScreen() {
|
||||
if (wakeIsSupported || isActive) {
|
||||
console.debug("Wake Lock Released");
|
||||
await release();
|
||||
}
|
||||
}
|
||||
onMounted(() => lockScreen());
|
||||
onUnmounted(() => unlockScreen());
|
||||
|
||||
return {
|
||||
wakeLock,
|
||||
wakeIsSupported,
|
||||
};
|
||||
},
|
||||
return {
|
||||
wakeLock,
|
||||
wakeIsSupported,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue