mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-08 22:15:34 -04:00
chore: script setup components (#7299)
This commit is contained in:
@@ -1,21 +1,13 @@
|
||||
<template>
|
||||
<div scoped-slot />
|
||||
<slot v-if="advanced" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Renderless component that only renders if the user is logged in.
|
||||
* and has advanced options toggled.
|
||||
*/
|
||||
export default defineNuxtComponent({
|
||||
setup(_, ctx) {
|
||||
const auth = useMealieAuth();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
const r = auth.user.value?.advanced || false;
|
||||
|
||||
return () => {
|
||||
return r ? ctx.slots.default?.() : null;
|
||||
};
|
||||
},
|
||||
});
|
||||
const advanced = auth.user.value?.advanced || false;
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
close-delay="500"
|
||||
transition="slide-y-transition"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
variant="flat"
|
||||
:icon="icon"
|
||||
@@ -16,7 +16,7 @@
|
||||
retain-focus-on-click
|
||||
:class="btnClass"
|
||||
:disabled="copyText !== '' ? false : true"
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
@click="textToClipboard()"
|
||||
>
|
||||
<v-icon>{{ $globals.icons.contentCopy }}</v-icon>
|
||||
@@ -33,66 +33,53 @@
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
copyText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
const props = defineProps({
|
||||
copyText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
setup(props) {
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
const show = ref(false);
|
||||
const copyToolTip = ref<VTooltip | null>(null);
|
||||
const copiedSuccess = ref<boolean | null>(null);
|
||||
|
||||
async function textToClipboard() {
|
||||
if (isSupported.value) {
|
||||
await copy(props.copyText);
|
||||
if (copied.value) {
|
||||
copiedSuccess.value = true;
|
||||
console.info(`Copied\n${props.copyText}`);
|
||||
}
|
||||
else {
|
||||
copiedSuccess.value = false;
|
||||
console.error("Copy failed: ", copied.value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn("Clipboard is currently not supported by your browser. Ensure you're on a secure (https) site.");
|
||||
}
|
||||
|
||||
show.value = true;
|
||||
setTimeout(() => {
|
||||
show.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
copyToolTip,
|
||||
textToClipboard,
|
||||
copied,
|
||||
isSupported,
|
||||
copiedSuccess,
|
||||
};
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
const show = ref(false);
|
||||
const copiedSuccess = ref<boolean | null>(null);
|
||||
|
||||
async function textToClipboard() {
|
||||
if (isSupported.value) {
|
||||
await copy(props.copyText);
|
||||
if (copied.value) {
|
||||
copiedSuccess.value = true;
|
||||
console.info(`Copied\n${props.copyText}`);
|
||||
}
|
||||
else {
|
||||
copiedSuccess.value = false;
|
||||
console.error("Copy failed: ", copied.value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn("Clipboard is currently not supported by your browser. Ensure you're on a secure (https) site.");
|
||||
}
|
||||
|
||||
show.value = true;
|
||||
setTimeout(() => {
|
||||
show.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-form ref="files">
|
||||
<v-form ref="form">
|
||||
<input
|
||||
ref="uploader"
|
||||
class="d-none"
|
||||
@@ -26,144 +26,129 @@
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const UPLOAD_EVENT = "uploaded";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
post: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: "archive",
|
||||
},
|
||||
textBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "info",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
setup(props, context) {
|
||||
const files = ref<File[]>([]);
|
||||
const uploader = ref<HTMLInputElement | null>(null);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const effIcon = props.icon ? props.icon : $globals.icons.upload;
|
||||
|
||||
const defaultText = i18n.t("general.upload");
|
||||
|
||||
const api = useUserApi();
|
||||
async function upload() {
|
||||
if (files.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSelecting.value = true;
|
||||
|
||||
if (!props.post) {
|
||||
// NOTE: To preserve behaviour for other parents of this component,
|
||||
// we emit a single File if !props.multiple.
|
||||
context.emit(UPLOAD_EVENT, props.multiple ? files.value : files.value[0]);
|
||||
isSelecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// WARN: My change is only for !props.post.
|
||||
// I have not added support for multiple files in the API.
|
||||
// Existing call-sites never passed the `multiple` prop,
|
||||
// so this case will only be hit if the prop is set to true.
|
||||
if (props.multiple && files.value.length > 1) {
|
||||
console.warn("Multiple file uploads are not supported by the API.");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files.value[0];
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function onFileChanged(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (target.files !== null && target.files.length > 0) {
|
||||
files.value = Array.from(target.files);
|
||||
upload();
|
||||
}
|
||||
}
|
||||
|
||||
function onButtonClick() {
|
||||
isSelecting.value = true;
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
isSelecting.value = false;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
uploader.value?.click();
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
uploader,
|
||||
isSelecting,
|
||||
effIcon,
|
||||
defaultText,
|
||||
onFileChanged,
|
||||
onButtonClick,
|
||||
};
|
||||
post: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: "archive",
|
||||
},
|
||||
textBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "info",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "uploaded", payload: File | File[] | unknown | null): void;
|
||||
}>();
|
||||
|
||||
const selectedFiles = ref<File[]>([]);
|
||||
const uploader = ref<HTMLInputElement | null>(null);
|
||||
const isSelecting = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const effIcon = props.icon ? props.icon : $globals.icons.upload;
|
||||
|
||||
const defaultText = i18n.t("general.upload");
|
||||
|
||||
const api = useUserApi();
|
||||
async function upload() {
|
||||
if (selectedFiles.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSelecting.value = true;
|
||||
|
||||
if (!props.post) {
|
||||
emit(UPLOAD_EVENT, props.multiple ? selectedFiles.value : selectedFiles.value[0]);
|
||||
isSelecting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.multiple && selectedFiles.value.length > 1) {
|
||||
console.warn("Multiple file uploads are not supported by the API.");
|
||||
return;
|
||||
}
|
||||
|
||||
const file = selectedFiles.value[0];
|
||||
const formData = new FormData();
|
||||
formData.append(props.fileName, file);
|
||||
|
||||
try {
|
||||
const response = await api.upload.file(props.url, formData);
|
||||
if (response) {
|
||||
emit(UPLOAD_EVENT, response);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
emit(UPLOAD_EVENT, null);
|
||||
}
|
||||
|
||||
isSelecting.value = false;
|
||||
}
|
||||
|
||||
function onFileChanged(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (target.files !== null && target.files.length > 0) {
|
||||
selectedFiles.value = Array.from(target.files);
|
||||
upload();
|
||||
}
|
||||
}
|
||||
|
||||
function onButtonClick() {
|
||||
isSelecting.value = true;
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
isSelecting.value = false;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
uploader.value?.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -39,71 +39,63 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
medium: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
waitingText: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
setup(props) {
|
||||
const size = computed(() => {
|
||||
if (props.tiny) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 0,
|
||||
size: 25,
|
||||
};
|
||||
}
|
||||
if (props.small) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 30,
|
||||
size: 50,
|
||||
};
|
||||
}
|
||||
else if (props.large) {
|
||||
return {
|
||||
width: 4,
|
||||
icon: 120,
|
||||
size: 200,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 3,
|
||||
icon: 75,
|
||||
size: 125,
|
||||
};
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const waitingTextCalculated = props.waitingText == null ? i18n.t("general.loading-recipes") : props.waitingText;
|
||||
|
||||
return {
|
||||
size,
|
||||
waitingTextCalculated,
|
||||
};
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
medium: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
waitingText: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const size = computed(() => {
|
||||
if (props.tiny) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 0,
|
||||
size: 25,
|
||||
};
|
||||
}
|
||||
if (props.small) {
|
||||
return {
|
||||
width: 2,
|
||||
icon: 30,
|
||||
size: 50,
|
||||
};
|
||||
}
|
||||
else if (props.large) {
|
||||
return {
|
||||
width: 4,
|
||||
icon: 120,
|
||||
size: 200,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 3,
|
||||
icon: 75,
|
||||
size: 125,
|
||||
};
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const waitingTextCalculated = props.waitingText == null ? i18n.t("general.loading-recipes") : props.waitingText;
|
||||
</script>
|
||||
|
||||
@@ -18,13 +18,11 @@
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
</BannerWarning>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
issue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
issue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<v-alert
|
||||
border="start"
|
||||
border-color
|
||||
variant="tonal"
|
||||
type="warning"
|
||||
elevation="2"
|
||||
@@ -20,19 +19,17 @@
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
};
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -32,191 +32,168 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
name: "BaseButton",
|
||||
props: {
|
||||
// Types
|
||||
cancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
update: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
save: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Download
|
||||
download: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
downloadUrl: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
// Property
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Styles
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
xSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
minor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconRight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
cancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
setup(props) {
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const buttonOptions = {
|
||||
create: {
|
||||
text: i18n.t("general.create"),
|
||||
icon: $globals.icons.createAlt,
|
||||
color: "success",
|
||||
},
|
||||
update: {
|
||||
text: i18n.t("general.update"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "success",
|
||||
},
|
||||
save: {
|
||||
text: i18n.t("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
color: "success",
|
||||
},
|
||||
edit: {
|
||||
text: i18n.t("general.edit"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "info",
|
||||
},
|
||||
delete: {
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
color: "error",
|
||||
},
|
||||
cancel: {
|
||||
text: i18n.t("general.cancel"),
|
||||
icon: $globals.icons.close,
|
||||
color: "grey",
|
||||
},
|
||||
download: {
|
||||
text: i18n.t("general.download"),
|
||||
icon: $globals.icons.download,
|
||||
color: "info",
|
||||
},
|
||||
};
|
||||
|
||||
const btnAttrs = computed(() => {
|
||||
if (props.delete) {
|
||||
return buttonOptions.delete;
|
||||
}
|
||||
else if (props.update) {
|
||||
return buttonOptions.update;
|
||||
}
|
||||
else if (props.edit) {
|
||||
return buttonOptions.edit;
|
||||
}
|
||||
else if (props.cancel) {
|
||||
return buttonOptions.cancel;
|
||||
}
|
||||
else if (props.save) {
|
||||
return buttonOptions.save;
|
||||
}
|
||||
else if (props.download) {
|
||||
return buttonOptions.download;
|
||||
}
|
||||
return buttonOptions.create;
|
||||
});
|
||||
|
||||
const buttonStyles = {
|
||||
defaults: {
|
||||
text: false,
|
||||
outlined: false,
|
||||
},
|
||||
secondary: {
|
||||
text: false,
|
||||
outlined: true,
|
||||
},
|
||||
minor: {
|
||||
text: true,
|
||||
outlined: false,
|
||||
},
|
||||
};
|
||||
|
||||
const btnStyle = computed(() => {
|
||||
if (props.secondary) {
|
||||
return buttonStyles.secondary;
|
||||
}
|
||||
else if (props.minor || props.cancel) {
|
||||
return buttonStyles.minor;
|
||||
}
|
||||
return buttonStyles.defaults;
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
function downloadFile() {
|
||||
api.utils.download(props.downloadUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
btnAttrs,
|
||||
btnStyle,
|
||||
downloadFile,
|
||||
};
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
update: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
save: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delete: {
|
||||
type: Boolean,
|
||||
default: false },
|
||||
download: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
downloadUrl: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
xSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
minor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconRight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const buttonOptions = {
|
||||
create: {
|
||||
text: i18n.t("general.create"),
|
||||
icon: $globals.icons.createAlt,
|
||||
color: "success",
|
||||
},
|
||||
update: {
|
||||
text: i18n.t("general.update"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "success",
|
||||
},
|
||||
save: {
|
||||
text: i18n.t("general.save"),
|
||||
icon: $globals.icons.save,
|
||||
color: "success",
|
||||
},
|
||||
edit: {
|
||||
text: i18n.t("general.edit"),
|
||||
icon: $globals.icons.edit,
|
||||
color: "info",
|
||||
},
|
||||
delete: {
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
color: "error",
|
||||
},
|
||||
cancel: {
|
||||
text: i18n.t("general.cancel"),
|
||||
icon: $globals.icons.close,
|
||||
color: "grey",
|
||||
},
|
||||
download: {
|
||||
text: i18n.t("general.download"),
|
||||
icon: $globals.icons.download,
|
||||
color: "info",
|
||||
},
|
||||
};
|
||||
|
||||
const btnAttrs = computed(() => {
|
||||
if (props.delete) {
|
||||
return buttonOptions.delete;
|
||||
}
|
||||
if (props.update) {
|
||||
return buttonOptions.update;
|
||||
}
|
||||
if (props.edit) {
|
||||
return buttonOptions.edit;
|
||||
}
|
||||
if (props.cancel) {
|
||||
return buttonOptions.cancel;
|
||||
}
|
||||
if (props.save) {
|
||||
return buttonOptions.save;
|
||||
}
|
||||
if (props.download) {
|
||||
return buttonOptions.download;
|
||||
}
|
||||
return buttonOptions.create;
|
||||
});
|
||||
|
||||
const buttonStyles = {
|
||||
defaults: { text: false, outlined: false },
|
||||
secondary: { text: false, outlined: true },
|
||||
minor: { text: true, outlined: false },
|
||||
};
|
||||
|
||||
const btnStyle = computed(() => {
|
||||
if (props.secondary) {
|
||||
return buttonStyles.secondary;
|
||||
}
|
||||
if (props.minor || props.cancel) {
|
||||
return buttonStyles.minor;
|
||||
}
|
||||
return buttonStyles.defaults;
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
function downloadFile() {
|
||||
api.utils.download(props.downloadUrl);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
start
|
||||
:style="stretch ? 'width: 100%;' : ''"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
tile
|
||||
:large="large"
|
||||
icon
|
||||
variant="plain"
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ btn.icon }}
|
||||
@@ -51,7 +51,7 @@
|
||||
location="bottom"
|
||||
content-class="text-caption"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
tile
|
||||
icon
|
||||
@@ -60,7 +60,7 @@
|
||||
:disabled="btn.disabled"
|
||||
:style="stretch ? `width: ${maxButtonWidth};` : ''"
|
||||
variant="plain"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
@click="$emit(btn.event)"
|
||||
>
|
||||
<v-icon> {{ btn.icon }} </v-icon>
|
||||
@@ -72,7 +72,7 @@
|
||||
</v-item-group>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
export interface ButtonOption {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
@@ -83,26 +83,20 @@ export interface ButtonOption {
|
||||
divider?: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
buttons: {
|
||||
type: Array as () => ButtonOption[],
|
||||
required: true,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
stretch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
buttons: {
|
||||
type: Array as () => ButtonOption[],
|
||||
required: true,
|
||||
},
|
||||
setup(props) {
|
||||
const maxButtonWidth = computed(() => `${100 / props.buttons.length}%`);
|
||||
return {
|
||||
maxButtonWidth,
|
||||
};
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
stretch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const maxButtonWidth = computed(() => `${100 / props.buttons.length}%`);
|
||||
</script>
|
||||
|
||||
@@ -29,21 +29,19 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
section: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
section: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,21 +6,19 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
width: {
|
||||
type: String,
|
||||
default: "100px",
|
||||
},
|
||||
thickness: {
|
||||
type: String,
|
||||
default: "2px",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "accent",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: "100px",
|
||||
},
|
||||
thickness: {
|
||||
type: String,
|
||||
default: "2px",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "accent",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: hoverProps }">
|
||||
<v-btn
|
||||
color="primary"
|
||||
v-bind="{ ...props, ...$attrs }"
|
||||
v-bind="{ ...hoverProps, ...$attrs }"
|
||||
:class="btnClass"
|
||||
:disabled="disabled"
|
||||
>
|
||||
@@ -105,7 +105,7 @@
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
const MODES = {
|
||||
model: "model",
|
||||
link: "link",
|
||||
@@ -124,67 +124,57 @@ export interface MenuItem {
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
mode: {
|
||||
type: String as () => modes,
|
||||
default: "model",
|
||||
},
|
||||
items: {
|
||||
type: Array as () => MenuItem[],
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: function () {
|
||||
return useI18n().t("general.actions");
|
||||
},
|
||||
},
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String as () => modes,
|
||||
default: "model",
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const activeObj = ref<MenuItem>({
|
||||
text: "DEFAULT",
|
||||
value: "",
|
||||
});
|
||||
|
||||
let startIndex = 0;
|
||||
props.items.forEach((item, index) => {
|
||||
if (item.value === props.modelValue) {
|
||||
startIndex = index;
|
||||
|
||||
activeObj.value = item;
|
||||
}
|
||||
});
|
||||
const itemGroup = ref(startIndex);
|
||||
|
||||
function setValue(v: MenuItem) {
|
||||
context.emit("update:modelValue", v.value);
|
||||
activeObj.value = v;
|
||||
}
|
||||
|
||||
return {
|
||||
MODES,
|
||||
activeObj,
|
||||
itemGroup,
|
||||
setValue,
|
||||
};
|
||||
items: {
|
||||
type: Array as () => MenuItem[],
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
btnClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: function () {
|
||||
return useI18n().t("general.actions");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
});
|
||||
|
||||
const activeObj = ref<MenuItem>({
|
||||
text: "DEFAULT",
|
||||
value: "",
|
||||
});
|
||||
|
||||
let startIndex = 0;
|
||||
props.items.forEach((item, index) => {
|
||||
if (item.value === modelValue.value) {
|
||||
startIndex = index;
|
||||
|
||||
activeObj.value = item;
|
||||
}
|
||||
});
|
||||
const itemGroup = ref(startIndex);
|
||||
|
||||
function setValue(v: MenuItem) {
|
||||
modelValue.value = v.value || "";
|
||||
activeObj.value = v;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,13 +22,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<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
|
||||
:color="color"
|
||||
:max-height="icon ? 90 : undefined"
|
||||
:width="icon ? 'auto' : '100%'"
|
||||
elevation="6"
|
||||
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-sheet>
|
||||
</slot>
|
||||
|
||||
<div
|
||||
v-if="$slots['after-heading']"
|
||||
class="ml-auto"
|
||||
>
|
||||
<slot name="after-heading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
|
||||
<template v-if="$slots.actions">
|
||||
<v-divider class="mt-2" />
|
||||
|
||||
<v-card-actions class="pb-0">
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.bottom">
|
||||
<v-divider
|
||||
v-if="!$slots.actions"
|
||||
class="mt-2"
|
||||
/>
|
||||
|
||||
<div class="pb-0">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
name: "MaterialCard",
|
||||
|
||||
props: {
|
||||
avatar: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
image: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const display = useDisplay();
|
||||
const hasHeading = computed(() => false);
|
||||
const hasAltHeading = computed(() => false);
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
"v-card--material--has-heading": hasHeading,
|
||||
"mt-3": display.name.value === "xs" || display.name.value === "sm",
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
hasHeading,
|
||||
hasAltHeading,
|
||||
classes,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.v-card--material
|
||||
&__avatar
|
||||
position: relative
|
||||
top: -64px
|
||||
margin-bottom: -32px
|
||||
|
||||
&__heading
|
||||
position: relative
|
||||
top: -40px
|
||||
transition: .3s ease
|
||||
z-index: 1
|
||||
</style>
|
||||
@@ -16,21 +16,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -40,27 +40,25 @@
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuItem } from "~/composables/use-context-presents";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
required: true,
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "grey-darken-2",
|
||||
},
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
required: true,
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "grey-darken-2",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,20 +4,13 @@
|
||||
</pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const prettyJson = JSON.stringify(props.data, null, 2);
|
||||
|
||||
return {
|
||||
prettyJson,
|
||||
};
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const prettyJson = JSON.stringify(props.data, null, 2);
|
||||
</script>
|
||||
|
||||
@@ -15,21 +15,16 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const href = computed(() => {
|
||||
// TODO: dynamically set docs link based off env
|
||||
return `https://nightly.mealie.io${props.link}`;
|
||||
});
|
||||
|
||||
return { href };
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const href = computed(() => {
|
||||
// TODO: dynamically set docs link based off env
|
||||
return `https://docs.mealie.io${props.link}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,25 +19,27 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useDropZone } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
emits: ["drop"],
|
||||
setup(_, context) {
|
||||
const el = ref<HTMLDivElement>();
|
||||
|
||||
function onDrop(files: File[] | null) {
|
||||
if (files) {
|
||||
context.emit("drop", files);
|
||||
}
|
||||
}
|
||||
|
||||
const { isOverDropZone } = useDropZone(el, files => onDrop(files));
|
||||
|
||||
return { el, isOverDropZone };
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["drop"]);
|
||||
|
||||
const el = ref<HTMLDivElement>();
|
||||
|
||||
function onDrop(files: File[] | null) {
|
||||
if (files) {
|
||||
emit("drop", files);
|
||||
}
|
||||
}
|
||||
|
||||
const { isOverDropZone } = useDropZone(el, files => onDrop(files));
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
@@ -29,17 +29,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
right: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
right: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
color="success"
|
||||
:icon="$globals.icons.save"
|
||||
:disabled="submitted"
|
||||
@click="() => save()"
|
||||
@click="save"
|
||||
/>
|
||||
<v-menu offset-y :close-on-content-click="false" location="bottom center">
|
||||
<template #activator="{ props }">
|
||||
<v-btn color="info" v-bind="props" :icon="$globals.icons.edit" :disabled="submitted" />
|
||||
<template #activator="{ props: slotProps }">
|
||||
<v-btn color="info" v-bind="slotProps" :icon="$globals.icons.edit" :disabled="submitted" />
|
||||
</template>
|
||||
<v-list class="mt-1">
|
||||
<template v-for="(row, keyRow) in controls" :key="keyRow">
|
||||
@@ -55,117 +55,98 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { Cropper } from "vue-advanced-cropper";
|
||||
import "vue-advanced-cropper/dist/style.css";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { Cropper },
|
||||
props: {
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cropperHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
cropperWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
defineProps({
|
||||
img: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emits: ["save", "delete"],
|
||||
setup(_, context) {
|
||||
const cropper = ref<any>();
|
||||
const changed = ref(0);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
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),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) {
|
||||
return;
|
||||
}
|
||||
cropper.value.rotate(angle);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
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,
|
||||
changed,
|
||||
};
|
||||
cropperHeight: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
methods: {
|
||||
defaultSize({ imageSize, visibleArea }) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
height: (visibleArea || imageSize).height,
|
||||
};
|
||||
},
|
||||
cropperWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
submitted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save", item: Blob): void;
|
||||
(e: "delete"): void;
|
||||
}>();
|
||||
|
||||
const cropper = ref<any>(null);
|
||||
const changed = ref(0);
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
type Control = {
|
||||
color: string;
|
||||
icon: string;
|
||||
callback: CallableFunction;
|
||||
};
|
||||
|
||||
function flip(hortizontal: boolean, vertical?: boolean) {
|
||||
if (!cropper.value) return;
|
||||
cropper.value.flip(hortizontal, vertical);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
function rotate(angle: number) {
|
||||
if (!cropper.value) return;
|
||||
cropper.value.rotate(angle);
|
||||
changed.value = changed.value + 1;
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function save() {
|
||||
if (!cropper.value) return;
|
||||
const { canvas } = cropper.value.getResult();
|
||||
if (!canvas) return;
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
emit("save", blob);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function defaultSize({ imageSize, visibleArea }: any) {
|
||||
return {
|
||||
width: (visibleArea || imageSize).width,
|
||||
height: (visibleArea || imageSize).height,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
v-model="inputVal"
|
||||
v-model="modelValue"
|
||||
:label="$t('general.color')"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -9,7 +9,7 @@
|
||||
size="small"
|
||||
height="30px"
|
||||
width="30px"
|
||||
:color="inputVal || 'grey'"
|
||||
:color="modelValue || 'grey'"
|
||||
@click="setRandomHex"
|
||||
>
|
||||
<v-icon color="white">
|
||||
@@ -33,7 +33,7 @@
|
||||
<v-card>
|
||||
<v-card-text class="pa-0">
|
||||
<v-color-picker
|
||||
v-model="inputVal"
|
||||
v-model="modelValue"
|
||||
flat
|
||||
hide-inputs
|
||||
show-swatches
|
||||
@@ -46,42 +46,21 @@
|
||||
</v-text-field>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const menu = ref(false);
|
||||
|
||||
const inputVal = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
function getRandomHex() {
|
||||
return "#000000".replace(/0/g, function () {
|
||||
return (~~(Math.random() * 16)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function setRandomHex() {
|
||||
inputVal.value = getRandomHex();
|
||||
}
|
||||
|
||||
return {
|
||||
menu,
|
||||
setRandomHex,
|
||||
inputVal,
|
||||
};
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const menu = ref(false);
|
||||
|
||||
function getRandomHex() {
|
||||
return "#000000".replace(/0/g, function () {
|
||||
return (~~(Math.random() * 16)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function setRandomHex() {
|
||||
modelValue.value = getRandomHex();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,105 +30,60 @@
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* The InputLabelType component is a wrapper for v-autocomplete. It is used to abstract the selection functionality
|
||||
* of some common types within Mealie. This can mostly be used with any type of object provided it has a name and id
|
||||
* property. The name property is used to display the name of the object in the autocomplete dropdown. The id property
|
||||
* is used to store the id of the object in the itemId property.
|
||||
*
|
||||
* Supported Types
|
||||
* - MultiPurposeLabel
|
||||
* - RecipeIngredientFood
|
||||
* - RecipeIngredientUnit
|
||||
*
|
||||
* TODO: Add RecipeTag / Category to this selector
|
||||
* Future Supported Types
|
||||
* - RecipeTags
|
||||
* - RecipeCategories
|
||||
*
|
||||
* 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"`
|
||||
*/
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
import { useSearch } from "~/composables/use-search";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => MultiPurposeLabelSummary | IngredientFood | IngredientUnit,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: Array as () => Array<MultiPurposeLabelSummary | IngredientFood | IngredientUnit>,
|
||||
required: true,
|
||||
},
|
||||
itemId: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// v-model for the selected item
|
||||
const modelValue = defineModel<MultiPurposeLabelSummary | IngredientFood | IngredientUnit | null>({ default: () => null });
|
||||
|
||||
// support v-model:item-id binding
|
||||
const itemId = defineModel<string | undefined>("item-id", { default: undefined });
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as () => Array<MultiPurposeLabelSummary | IngredientFood | IngredientUnit>,
|
||||
required: true,
|
||||
},
|
||||
emits: ["update:modelValue", "update:item-id", "create"],
|
||||
setup(props, context) {
|
||||
const autocompleteRef = ref<HTMLInputElement>();
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered: filteredItems } = useSearch(computed(() => props.items));
|
||||
|
||||
const itemIdVal = computed({
|
||||
get: () => {
|
||||
return props.itemId || undefined;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:item-id", val);
|
||||
},
|
||||
});
|
||||
|
||||
const itemVal = computed({
|
||||
get: () => {
|
||||
try {
|
||||
return Object.keys(props.modelValue).length !== 0 ? props.modelValue : null;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: (val) => {
|
||||
itemIdVal.value = val?.id || undefined;
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
function emitCreate() {
|
||||
if (props.items.some(item => item.name === searchInput.value)) {
|
||||
return;
|
||||
}
|
||||
context.emit("create", searchInput.value);
|
||||
autocompleteRef.value?.blur();
|
||||
}
|
||||
|
||||
return {
|
||||
autocompleteRef,
|
||||
itemVal,
|
||||
itemIdVal,
|
||||
searchInput,
|
||||
filteredItems,
|
||||
emitCreate,
|
||||
};
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "create", val: string): void;
|
||||
}>();
|
||||
|
||||
const autocompleteRef = ref<HTMLInputElement>();
|
||||
|
||||
// Use the search composable
|
||||
const { search: searchInput, filtered: filteredItems } = useSearch(computed(() => props.items));
|
||||
|
||||
const itemVal = computed({
|
||||
get: () => {
|
||||
if (!modelValue.value || Object.keys(modelValue.value).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return modelValue.value;
|
||||
},
|
||||
set: (val) => {
|
||||
itemId.value = val?.id || "";
|
||||
modelValue.value = val;
|
||||
},
|
||||
});
|
||||
|
||||
function emitCreate() {
|
||||
if (props.items.some(item => item.name === searchInput.value)) {
|
||||
return;
|
||||
}
|
||||
emit("create", searchInput.value);
|
||||
autocompleteRef.value?.blur();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
v-model="dialog"
|
||||
v-model="modelValue"
|
||||
:icon="$globals.icons.translate"
|
||||
:title="$t('language-dialog.choose-language')"
|
||||
>
|
||||
@@ -43,50 +43,26 @@
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { normalizeFilter } from "~/composables/use-utils";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value),
|
||||
});
|
||||
const modelValue = defineModel<boolean>({ default: () => false });
|
||||
|
||||
const { locales: LOCALES, locale, i18n } = useLocales();
|
||||
const { locales: LOCALES, locale, i18n } = useLocales();
|
||||
|
||||
const selectedLocale = ref(locale.value);
|
||||
const onLocaleSelect = (value: string) => {
|
||||
if (value && locales.some(l => l.value === value)) {
|
||||
locale.value = value as any;
|
||||
}
|
||||
};
|
||||
const selectedLocale = ref(locale.value);
|
||||
const onLocaleSelect = (value: string) => {
|
||||
if (value && locales.some(l => l.value === value)) {
|
||||
locale.value = value as any;
|
||||
}
|
||||
};
|
||||
|
||||
watch(locale, () => {
|
||||
dialog.value = false; // Close dialog when locale changes
|
||||
});
|
||||
|
||||
const locales = LOCALES.filter(lc =>
|
||||
i18n.locales.value.map(i18nLocale => i18nLocale.code).includes(lc.value as any),
|
||||
);
|
||||
|
||||
return {
|
||||
dialog,
|
||||
i18n,
|
||||
locales,
|
||||
locale,
|
||||
selectedLocale,
|
||||
onLocaleSelect,
|
||||
normalizeFilter,
|
||||
};
|
||||
},
|
||||
watch(locale, () => {
|
||||
modelValue.value = false; // Close dialog when locale changes
|
||||
});
|
||||
|
||||
const locales = LOCALES.filter(lc =>
|
||||
i18n.locales.value.map(i18nLocale => i18nLocale.code).includes(lc.value as any),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<v-textarea
|
||||
v-if="!previewState"
|
||||
v-bind="textarea"
|
||||
v-model="inputVal"
|
||||
v-model="modelValue"
|
||||
:class="label == '' ? '' : 'mt-5'"
|
||||
:label="label"
|
||||
auto-grow
|
||||
@@ -33,60 +33,42 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
name: "MarkdownEditor",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
displayPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Object as () => unknown,
|
||||
default: () => ({}),
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
emits: ["update:modelValue", "input:preview"],
|
||||
setup(props, context) {
|
||||
const fallbackPreview = ref(false);
|
||||
const previewState = computed({
|
||||
get: () => {
|
||||
return props.preview ?? fallbackPreview.value;
|
||||
},
|
||||
set: (val) => {
|
||||
if (props.preview) {
|
||||
context.emit("input:preview", val);
|
||||
}
|
||||
else {
|
||||
fallbackPreview.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
displayPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
textarea: {
|
||||
type: Object as () => unknown,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const inputVal = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
return {
|
||||
previewState,
|
||||
inputVal,
|
||||
};
|
||||
const emit = defineEmits<{
|
||||
(e: "input:preview", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<string>("modelValue");
|
||||
|
||||
const fallbackPreview = ref(false);
|
||||
const previewState = computed({
|
||||
get: () => props.preview ?? fallbackPreview.value,
|
||||
set: (val: boolean) => {
|
||||
if (props.preview) {
|
||||
emit("input:preview", val);
|
||||
}
|
||||
else {
|
||||
fallbackPreview.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,53 +8,40 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "RecipeJsonEditor",
|
||||
components: { JsonEditorVue },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "1500px",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
function parseEvent(event: any): object {
|
||||
if (!event) {
|
||||
return props.modelValue || {};
|
||||
}
|
||||
try {
|
||||
if (event.json) {
|
||||
return event.json;
|
||||
}
|
||||
else if (event.text) {
|
||||
return JSON.parse(event.text);
|
||||
}
|
||||
else {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return props.modelValue || {};
|
||||
}
|
||||
}
|
||||
function onChange(event: any) {
|
||||
const parsed = parseEvent(event);
|
||||
if (parsed !== props.modelValue) {
|
||||
emit("update:modelValue", parsed);
|
||||
}
|
||||
}
|
||||
return {
|
||||
onChange,
|
||||
};
|
||||
const modelValue = defineModel<object>("modelValue", { default: () => ({}) });
|
||||
defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: "1500px",
|
||||
},
|
||||
});
|
||||
|
||||
function parseEvent(event: any): object {
|
||||
if (!event) {
|
||||
return modelValue.value || {};
|
||||
}
|
||||
try {
|
||||
if (event.json) {
|
||||
return event.json;
|
||||
}
|
||||
else if (event.text) {
|
||||
return JSON.parse(event.text);
|
||||
}
|
||||
else {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return modelValue.value || {};
|
||||
}
|
||||
}
|
||||
function onChange(event: any) {
|
||||
const parsed = parseEvent(event);
|
||||
if (parsed !== modelValue.value) {
|
||||
modelValue.value = parsed;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,54 +27,46 @@
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ReportSummary } from "~/lib/api/types/reports";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
items: {
|
||||
required: true,
|
||||
type: Array as () => Array<ReportSummary>,
|
||||
},
|
||||
},
|
||||
emits: ["delete"],
|
||||
|
||||
setup(_, context) {
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const headers = [
|
||||
{ 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) {
|
||||
if (item.status === "in-progress") {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/group/reports/${item.id}`);
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function deleteReport(id: string) {
|
||||
context.emit("delete", id);
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
handleRowClick,
|
||||
capitalize,
|
||||
deleteReport,
|
||||
};
|
||||
defineProps({
|
||||
items: {
|
||||
type: Array as () => Array<ReportSummary>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", id: string): void;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const headers = [
|
||||
{ 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) {
|
||||
if (item.status === "in-progress") {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/group/reports/${item.id}`);
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function deleteReport(id: string) {
|
||||
emit("delete", id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div v-html="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
@@ -11,62 +11,55 @@ enum DOMPurifyHook {
|
||||
UponSanitizeAttribute = "uponSanitizeAttribute",
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
source: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
const props = defineProps({
|
||||
source: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
setup(props) {
|
||||
const ALLOWED_STYLE_TAGS = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
});
|
||||
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
const ALLOWED_STYLE_TAGS = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||
});
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||
});
|
||||
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
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",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
],
|
||||
});
|
||||
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
});
|
||||
|
||||
const value = computed(() => {
|
||||
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
});
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
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",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
};
|
||||
},
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
const value = computed(() => {
|
||||
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,33 +30,26 @@
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
minWidth: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
setup(props) {
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const activeIcon = computed(() => {
|
||||
return props.icon ?? $globals.icons.primary;
|
||||
});
|
||||
|
||||
return {
|
||||
activeIcon,
|
||||
};
|
||||
minWidth: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const activeIcon = computed(() => {
|
||||
return props.icon ?? $globals.icons.primary;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,40 +2,26 @@
|
||||
<component :is="tag">
|
||||
<slot
|
||||
name="activator"
|
||||
v-bind="{ toggle, state }"
|
||||
v-bind="{ toggle, modelValue }"
|
||||
/>
|
||||
<slot v-bind="{ state, toggle }" />
|
||||
<slot v-bind="{ modelValue, toggle }" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: "div",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const state = ref(false);
|
||||
<script setup lang="ts">
|
||||
const modelValue = defineModel({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
state.value = !state.value;
|
||||
};
|
||||
|
||||
watch(state, () => {
|
||||
context.emit("update:modelValue", state.value);
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
toggle,
|
||||
};
|
||||
defineProps({
|
||||
tag: {
|
||||
type: String,
|
||||
default: "div",
|
||||
},
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
modelValue.value = !modelValue.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -12,42 +12,33 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useWakeLock } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
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");
|
||||
}
|
||||
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());
|
||||
|
||||
return {
|
||||
wakeLock,
|
||||
wakeIsSupported,
|
||||
};
|
||||
},
|
||||
});
|
||||
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());
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user