mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-10 20:13:33 -04:00
Compare commits
1 Commits
mealie-nex
...
renovate/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0883e3ca13 |
@@ -258,10 +258,6 @@
|
|||||||
class="text-center"
|
class="text-center"
|
||||||
@update:model-value="setRightParenthesisValue(field, index, $event)"
|
@update:model-value="setRightParenthesisValue(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- field actions -->
|
|
||||||
<v-col
|
|
||||||
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
|
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
|
||||||
:cols="config.items.fieldActions.cols(index)"
|
:cols="config.items.fieldActions.cols(index)"
|
||||||
:sm="config.items.fieldActions.sm(index)"
|
:sm="config.items.fieldActions.sm(index)"
|
||||||
|
|||||||
@@ -11,9 +11,7 @@
|
|||||||
>
|
>
|
||||||
<div class="d-flex flex-column ga-3">
|
<div class="d-flex flex-column ga-3">
|
||||||
<v-card-actions class="pa-0">
|
<v-card-actions class="pa-0">
|
||||||
<div class="position-relative" style="flex: 1;">
|
|
||||||
<InputLabelType
|
<InputLabelType
|
||||||
ref="foodInputRef"
|
|
||||||
v-model="listItem.food"
|
v-model="listItem.food"
|
||||||
v-model:item-id="listItem.foodId!"
|
v-model:item-id="listItem.foodId!"
|
||||||
:items="foods"
|
:items="foods"
|
||||||
@@ -21,18 +19,10 @@
|
|||||||
:icon="$globals.icons.foods"
|
:icon="$globals.icons.foods"
|
||||||
:style="rail ? 'margin-inline: 3px;' : undefined"
|
:style="rail ? 'margin-inline: 3px;' : undefined"
|
||||||
:search="rail"
|
:search="rail"
|
||||||
:menu-props="{ location: menuDirection }"
|
|
||||||
create
|
create
|
||||||
@create="createAssignFood"
|
@create="createAssignFood"
|
||||||
|
@focus="rail = false"
|
||||||
/>
|
/>
|
||||||
<!-- Intercept clicks when collapsed so the drawer expands before the autocomplete opens -->
|
|
||||||
<div
|
|
||||||
v-if="rail"
|
|
||||||
class="position-absolute"
|
|
||||||
style="inset: 0; cursor: text;"
|
|
||||||
@click="expandAndFocus"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<BaseButtonGroup
|
<BaseButtonGroup
|
||||||
v-if="!rail"
|
v-if="!rail"
|
||||||
:buttons="[
|
:buttons="[
|
||||||
@@ -94,20 +84,6 @@ defineEmits<{
|
|||||||
|
|
||||||
const { createAssignFood } = useShoppingListItemEditor(listItem);
|
const { createAssignFood } = useShoppingListItemEditor(listItem);
|
||||||
|
|
||||||
const { smAndDown } = useDisplay();
|
|
||||||
const menuDirection = computed(() => smAndDown.value ? "top" : "bottom");
|
|
||||||
|
|
||||||
const foodInputRef = ref<{ focus: () => void } | null>(null);
|
|
||||||
const rail = ref(true);
|
|
||||||
|
|
||||||
async function expandAndFocus() {
|
|
||||||
rail.value = false;
|
|
||||||
await nextTick();
|
|
||||||
setTimeout(() => {
|
|
||||||
foodInputRef.value?.focus();
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => listItem.value.quantity,
|
() => listItem.value.quantity,
|
||||||
(newQty) => {
|
(newQty) => {
|
||||||
@@ -124,4 +100,6 @@ watch(
|
|||||||
listItem.value.labelId = listItem.value.label?.id || null;
|
listItem.value.labelId = listItem.value.label?.id || null;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rail = ref(true);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,13 +11,11 @@
|
|||||||
>
|
>
|
||||||
<v-row
|
<v-row
|
||||||
v-touch="{
|
v-touch="{
|
||||||
move: ({ originalEvent: { touches: [{ screenX, screenY }] } }) => {
|
move: ({ originalEvent: { touches: [{ screenX }] } }) => {
|
||||||
swipeInfo.touchendX = screenX;
|
swipeInfo.touchendX = screenX;
|
||||||
swipeInfo.touchendY = screenY;
|
|
||||||
},
|
},
|
||||||
start: ({ originalEvent: { touches: [{ screenX, screenY }] } }) => {
|
start: ({ originalEvent: { touches: [{ screenX }] } }) => {
|
||||||
swipeInfo.touchstartX = screenX;
|
swipeInfo.touchstartX = screenX;
|
||||||
swipeInfo.touchstartY = screenY;
|
|
||||||
},
|
},
|
||||||
end: () => {
|
end: () => {
|
||||||
if (swiping < SWIPE_THRESHOLD) {
|
if (swiping < SWIPE_THRESHOLD) {
|
||||||
@@ -214,7 +212,6 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const SWIPE_THRESHOLD = 50;
|
const SWIPE_THRESHOLD = 50;
|
||||||
const SCROLL_THRESHOLD = 50;
|
|
||||||
|
|
||||||
const { isRtl } = useRtl();
|
const { isRtl } = useRtl();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -267,22 +264,14 @@ function save() {
|
|||||||
edit.value = false;
|
edit.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const swipeInfo: Ref<{ touchstartX?: number; touchendX?: number; touchstartY?: number; touchendY?: number }> = ref({});
|
const swipeInfo: Ref<{ touchstartX?: number; touchendX?: number }> = ref({ touchstartX: undefined, touchendX: undefined });
|
||||||
const swiping = computed(() => {
|
const swiping = computed(() => {
|
||||||
const { touchstartX, touchendX, touchstartY, touchendY } = swipeInfo.value ?? {};
|
const { touchstartX, touchendX } = swipeInfo.value ?? {};
|
||||||
if (touchstartX === undefined || touchendX === undefined) {
|
if (touchstartX === undefined || touchendX === undefined) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const deltaX = isRtl.value ? touchstartX - touchendX : touchendX - touchstartX;
|
const delta = isRtl.value ? touchstartX - touchendX : touchendX - touchstartX;
|
||||||
|
return Math.min(Math.max(0, delta), 100);
|
||||||
// If there's significant vertical movement, treat as a scroll gesture and ignore
|
|
||||||
if (touchstartY !== undefined && touchendY !== undefined) {
|
|
||||||
const deltaY = Math.abs(touchendY - touchstartY);
|
|
||||||
if (deltaY > SCROLL_THRESHOLD) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Math.min(Math.max(0, deltaX), 100);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeList = computed<RecipeSummary[]>(() => {
|
const recipeList = computed<RecipeSummary[]>(() => {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
:items="units"
|
:items="units"
|
||||||
:label="$t('recipe.unit')"
|
:label="$t('recipe.unit')"
|
||||||
:icon="$globals.icons.units"
|
:icon="$globals.icons.units"
|
||||||
:menu-props="{ location: menuDirection }"
|
|
||||||
style="flex: 3"
|
style="flex: 3"
|
||||||
create
|
create
|
||||||
@create="createAssignUnit"
|
@create="createAssignUnit"
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
v-model:item-id="listItem.labelId!"
|
v-model:item-id="listItem.labelId!"
|
||||||
:items="labels"
|
:items="labels"
|
||||||
:label="$t('shopping-list.label')"
|
:label="$t('shopping-list.label')"
|
||||||
:menu-props="{ location: menuDirection }"
|
|
||||||
style="flex: 1 0 200px"
|
style="flex: 1 0 200px"
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
@@ -77,9 +75,6 @@ const emit = defineEmits<{ (e: "save"): void }>();
|
|||||||
|
|
||||||
const { assignLabelToFood, createAssignUnit } = useShoppingListItemEditor(listItem);
|
const { assignLabelToFood, createAssignUnit } = useShoppingListItemEditor(listItem);
|
||||||
|
|
||||||
const { smAndDown } = useDisplay();
|
|
||||||
const menuDirection = computed(() => smAndDown.value ? "top" : "bottom");
|
|
||||||
|
|
||||||
function handleNoteKeyPress(event: KeyboardEvent) {
|
function handleNoteKeyPress(event: KeyboardEvent) {
|
||||||
// Save on Enter
|
// Save on Enter
|
||||||
if (!event.shiftKey && event.key === "Enter") {
|
if (!event.shiftKey && event.key === "Enter") {
|
||||||
|
|||||||
@@ -93,8 +93,4 @@ function emitCreate() {
|
|||||||
emit("create", searchInput.value);
|
emit("create", searchInput.value);
|
||||||
autocompleteRef.value?.blur();
|
autocompleteRef.value?.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
focus: () => autocompleteRef.value?.focus(),
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "简体中文 (Chinese simplified)",
|
name: "简体中文 (Chinese simplified)",
|
||||||
value: "zh-CN",
|
value: "zh-CN",
|
||||||
progress: 54,
|
progress: 55,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
pluralFoodHandling: "never",
|
pluralFoodHandling: "never",
|
||||||
},
|
},
|
||||||
@@ -143,7 +143,7 @@ export const LOCALES = [
|
|||||||
{
|
{
|
||||||
name: "Italiano (Italian)",
|
name: "Italiano (Italian)",
|
||||||
value: "it-IT",
|
value: "it-IT",
|
||||||
progress: 73,
|
progress: 72,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
pluralFoodHandling: "always",
|
pluralFoodHandling: "always",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
"token": "Nøgle",
|
"token": "Nøgle",
|
||||||
"tuesday": "Tirsdag",
|
"tuesday": "Tirsdag",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"undo": "Fortryd",
|
"undo": "Undo",
|
||||||
"update": "Gem",
|
"update": "Gem",
|
||||||
"updated": "Ændret",
|
"updated": "Ændret",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
"are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på, at du vil fjerne markeringen af alle elementer?",
|
"are-you-sure-you-want-to-uncheck-all-items": "Er du sikker på, at du vil fjerne markeringen af alle elementer?",
|
||||||
"are-you-sure-you-want-to-delete-checked-items": "Er du sikker på, at du vil sletter de valgte elementer?",
|
"are-you-sure-you-want-to-delete-checked-items": "Er du sikker på, at du vil sletter de valgte elementer?",
|
||||||
"no-shopping-lists-found": "Ingen Indkøbslister fundet",
|
"no-shopping-lists-found": "Ingen Indkøbslister fundet",
|
||||||
"item-checked-off": "{item} blev krydset af"
|
"item-checked-off": "{item} was checked off"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"all-recipes": "Alle opskrifter",
|
"all-recipes": "Alle opskrifter",
|
||||||
|
|||||||
@@ -943,7 +943,7 @@
|
|||||||
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
"are-you-sure-you-want-to-uncheck-all-items": "Are you sure you want to uncheck all items?",
|
||||||
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?",
|
"are-you-sure-you-want-to-delete-checked-items": "Are you sure you want to delete all checked items?",
|
||||||
"no-shopping-lists-found": "No Shopping Lists Found",
|
"no-shopping-lists-found": "No Shopping Lists Found",
|
||||||
"item-checked-off": "Checked off {item}"
|
"item-checked-off": "{item} was checked off"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"all-recipes": "All Recipes",
|
"all-recipes": "All Recipes",
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
"token": "Token",
|
"token": "Token",
|
||||||
"tuesday": "Kedd",
|
"tuesday": "Kedd",
|
||||||
"type": "Típus",
|
"type": "Típus",
|
||||||
"undo": "Visszavonás",
|
"undo": "Undo",
|
||||||
"update": "Frissítés",
|
"update": "Frissítés",
|
||||||
"updated": "Frissítve",
|
"updated": "Frissítve",
|
||||||
"upload": "Feltöltés",
|
"upload": "Feltöltés",
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
"are-you-sure-you-want-to-uncheck-all-items": "Biztos, hogy minden elem kijelölését visszavonja?",
|
"are-you-sure-you-want-to-uncheck-all-items": "Biztos, hogy minden elem kijelölését visszavonja?",
|
||||||
"are-you-sure-you-want-to-delete-checked-items": "Biztosan törölni akarja az összes bejelölt elemet?",
|
"are-you-sure-you-want-to-delete-checked-items": "Biztosan törölni akarja az összes bejelölt elemet?",
|
||||||
"no-shopping-lists-found": "Nem találhatók bevásárlólisták",
|
"no-shopping-lists-found": "Nem találhatók bevásárlólisták",
|
||||||
"item-checked-off": "{item} lett bejelölve"
|
"item-checked-off": "{item} was checked off"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"all-recipes": "Minden recept",
|
"all-recipes": "Minden recept",
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
"token": "Žeton",
|
"token": "Žeton",
|
||||||
"tuesday": "Torek",
|
"tuesday": "Torek",
|
||||||
"type": "Tip",
|
"type": "Tip",
|
||||||
"undo": "Razveljavi",
|
"undo": "Undo",
|
||||||
"update": "Posodobi",
|
"update": "Posodobi",
|
||||||
"updated": "Posodobljen",
|
"updated": "Posodobljen",
|
||||||
"upload": "Naloži",
|
"upload": "Naloži",
|
||||||
@@ -917,7 +917,7 @@
|
|||||||
"quantity": "Količina: {0}",
|
"quantity": "Količina: {0}",
|
||||||
"shopping-list": "Nakupovalni seznam",
|
"shopping-list": "Nakupovalni seznam",
|
||||||
"shopping-lists": "Nakupovalni seznami",
|
"shopping-lists": "Nakupovalni seznami",
|
||||||
"add-item": "Dodaj element",
|
"add-item": "Add item",
|
||||||
"food": "Živilo",
|
"food": "Živilo",
|
||||||
"note": "Opomba",
|
"note": "Opomba",
|
||||||
"label": "Oznaka",
|
"label": "Oznaka",
|
||||||
@@ -943,7 +943,7 @@
|
|||||||
"are-you-sure-you-want-to-uncheck-all-items": "Ali res ne želite izbrati vseh elementov?",
|
"are-you-sure-you-want-to-uncheck-all-items": "Ali res ne želite izbrati vseh elementov?",
|
||||||
"are-you-sure-you-want-to-delete-checked-items": "Ali ste prepričani, da želite izbrisati vse izbrane elemente?",
|
"are-you-sure-you-want-to-delete-checked-items": "Ali ste prepričani, da želite izbrisati vse izbrane elemente?",
|
||||||
"no-shopping-lists-found": "Ni nakupovalnih seznamov",
|
"no-shopping-lists-found": "Ni nakupovalnih seznamov",
|
||||||
"item-checked-off": "{item} je bil odkljukan"
|
"item-checked-off": "{item} was checked off"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"all-recipes": "Vsi recepti",
|
"all-recipes": "Vsi recepti",
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ export default defineNuxtRouteMiddleware((to) => {
|
|||||||
const { user } = useMealieAuth();
|
const { user } = useMealieAuth();
|
||||||
const groupSlug = user.value?.groupSlug;
|
const groupSlug = user.value?.groupSlug;
|
||||||
if (!groupSlug) {
|
if (!groupSlug) {
|
||||||
// Preserve the full path (including recipe_import_url query param) so the
|
return navigateTo("/login", { redirectCode: 301 });
|
||||||
// login page can redirect back here after successful authentication.
|
|
||||||
const redirect = encodeURIComponent(to.fullPath);
|
|
||||||
return navigateTo(`/login?redirect=${redirect}`, { redirectCode: 302 });
|
|
||||||
}
|
}
|
||||||
return navigateTo(`/g/${groupSlug}${to.fullPath}`, { redirectCode: 302 });
|
return navigateTo(`/g/${groupSlug}${to.fullPath}`, { redirectCode: 301 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form ref="form" @submit.prevent="requestLink()">
|
<v-form @submit.prevent="requestLink()">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="state.email"
|
v-model="state.email"
|
||||||
:prepend-inner-icon="$globals.icons.email"
|
:prepend-inner-icon="$globals.icons.email"
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
name="login"
|
name="login"
|
||||||
:label="$t('user.email')"
|
:label="$t('user.email')"
|
||||||
type="text"
|
type="text"
|
||||||
:rules="[validators.email]"
|
|
||||||
/>
|
/>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
{{ $t('user.forgot-password-text') }}
|
{{ $t('user.forgot-password-text') }}
|
||||||
@@ -64,15 +63,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { validators } from "~/composables/use-validators";
|
|
||||||
import type { VForm } from "~/types/auto-forms";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "basic",
|
layout: "basic",
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
email: "",
|
email: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -89,27 +84,17 @@ useSeoMeta({
|
|||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
async function requestLink() {
|
async function requestLink() {
|
||||||
if (!form.value) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { valid } = await form.value.validate();
|
|
||||||
if (!valid) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
// TODO: Fix Response to send meaningful error
|
// TODO: Fix Response to send meaningful error
|
||||||
const { response } = await api.email.sendForgotPassword({ email: state.email });
|
const { response } = await api.email.sendForgotPassword({ email: state.email });
|
||||||
|
|
||||||
state.loading = false;
|
|
||||||
|
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
|
state.loading = false;
|
||||||
state.error = false;
|
state.error = false;
|
||||||
alert.success(i18n.t("profile.email-sent"));
|
alert.success(i18n.t("profile.email-sent"));
|
||||||
await navigateTo("/login");
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
state.loading = false;
|
||||||
state.error = true;
|
state.error = true;
|
||||||
alert.error(i18n.t("profile.error-sending-email"));
|
alert.error(i18n.t("profile.error-sending-email"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,32 +198,15 @@ const recipeUrl = computed({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
// Prefer the 'url' share field (recipe_import_url, populated by Chrome when
|
return route.query.recipe_import_url as string | null;
|
||||||
// sharing a page URL). Fall back to the 'text' share field (recipe_import_text)
|
|
||||||
// for apps that share URLs as plain text, but only when the text value is
|
|
||||||
// actually a valid http/https URL — shared text can be arbitrary.
|
|
||||||
const urlFromField = route.query.recipe_import_url as string | null;
|
|
||||||
if (urlFromField) {
|
|
||||||
return urlFromField;
|
|
||||||
}
|
|
||||||
const textFromField = route.query.recipe_import_text as string | null;
|
|
||||||
if (textFromField) {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(textFromField);
|
|
||||||
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
||||||
return textFromField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* not a URL, ignore */ }
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (recipeUrl.value) {
|
if (recipeUrl.value && recipeUrl.value.includes("https")) {
|
||||||
// Apply legacy query params for older automations such as the Bookmarklet.
|
// Check if we have a query params for using keywords as tags or staying in edit mode.
|
||||||
// These are no longer used by the app itself but are easy to keep supporting.
|
// We don't use these in the app anymore, but older automations such as Bookmarklet might still use them,
|
||||||
|
// and they're easy enough to support.
|
||||||
const importKeywordsAsTagsParam = route.query.use_keywords;
|
const importKeywordsAsTagsParam = route.query.use_keywords;
|
||||||
if (importKeywordsAsTagsParam === "1") {
|
if (importKeywordsAsTagsParam === "1") {
|
||||||
importKeywordsAsTags.value = true;
|
importKeywordsAsTags.value = true;
|
||||||
@@ -240,9 +223,8 @@ onMounted(() => {
|
|||||||
stayInEditMode.value = false;
|
stayInEditMode.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The URL is pre-filled via the recipeUrl computed property.
|
createByUrl(recipeUrl.value, importKeywordsAsTags.value, false);
|
||||||
// Do not auto-submit: the user should review the import options and
|
return;
|
||||||
// confirm by clicking the submit button.
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDark, useSessionStorage, whenever } from "@vueuse/core";
|
import { useDark, whenever } from "@vueuse/core";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { usePasswordField } from "~/composables/use-passwords";
|
import { usePasswordField } from "~/composables/use-passwords";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
@@ -225,7 +225,6 @@ definePageMeta({
|
|||||||
const isDark = useDark();
|
const isDark = useDark();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const auth = useMealieAuth();
|
const auth = useMealieAuth();
|
||||||
const { $appInfo, $axios } = useNuxtApp();
|
const { $appInfo, $axios } = useNuxtApp();
|
||||||
@@ -236,9 +235,6 @@ const isFirstLogin = ref(false);
|
|||||||
const activityPreferences = useUserActivityPreferences();
|
const activityPreferences = useUserActivityPreferences();
|
||||||
const { getDefaultActivityRoute } = useDefaultActivity();
|
const { getDefaultActivityRoute } = useDefaultActivity();
|
||||||
|
|
||||||
// Survives the page reload that happens during OIDC redirect
|
|
||||||
const pendingShareRedirect = useSessionStorage<string | null>("pwa_share_redirect", null);
|
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("user.login"),
|
title: i18n.t("user.login"),
|
||||||
});
|
});
|
||||||
@@ -263,29 +259,14 @@ useAsyncData(useAsyncKey(), async () => {
|
|||||||
whenever(
|
whenever(
|
||||||
() => loggedIn.value && groupSlug.value,
|
() => loggedIn.value && groupSlug.value,
|
||||||
() => {
|
() => {
|
||||||
// First-login setup always takes priority
|
|
||||||
if (!isDemo.value && isFirstLogin.value && auth.user.value?.admin) {
|
|
||||||
router.push("/admin/setup");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// After login, honour a pending PWA share redirect.
|
|
||||||
// The redirect param can arrive via the URL query string (password login)
|
|
||||||
// or via sessionStorage (OIDC login, where the OIDC provider reload clears
|
|
||||||
// the query string).
|
|
||||||
const redirectFromQuery = route.query.redirect as string | undefined;
|
|
||||||
const redirectTarget = redirectFromQuery ?? pendingShareRedirect.value;
|
|
||||||
if (redirectTarget && redirectTarget.startsWith("/")) {
|
|
||||||
pendingShareRedirect.value = null;
|
|
||||||
router.push(redirectTarget);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultActivityRoute = getDefaultActivityRoute(
|
const defaultActivityRoute = getDefaultActivityRoute(
|
||||||
activityPreferences.value.defaultActivity,
|
activityPreferences.value.defaultActivity,
|
||||||
groupSlug.value,
|
groupSlug.value,
|
||||||
);
|
);
|
||||||
if (defaultActivityRoute) {
|
if (!isDemo.value && isFirstLogin.value && auth.user.value?.admin) {
|
||||||
|
router.push("/admin/setup");
|
||||||
|
}
|
||||||
|
else if (defaultActivityRoute) {
|
||||||
router.push(defaultActivityRoute);
|
router.push(defaultActivityRoute);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -335,13 +316,6 @@ async function oidcAuthenticate(callback = false) {
|
|||||||
oidcLoggingIn.value = false;
|
oidcLoggingIn.value = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Save any pending PWA share redirect before leaving for the OIDC provider.
|
|
||||||
// The OIDC callback reloads the page, which clears the query string, so we
|
|
||||||
// persist the target in sessionStorage and restore it after login.
|
|
||||||
const redirectTarget = route.query.redirect as string | undefined;
|
|
||||||
if (redirectTarget && redirectTarget.startsWith("/")) {
|
|
||||||
pendingShareRedirect.value = redirectTarget;
|
|
||||||
}
|
|
||||||
navigateTo("/api/auth/oauth", { external: true }); // start the redirect process
|
navigateTo("/api/auth/oauth", { external: true }); // start the redirect process
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form ref="form" @submit.prevent="requestLink()">
|
<v-form @submit.prevent="requestLink()">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="state.email"
|
v-model="state.email"
|
||||||
:prepend-inner-icon="$globals.icons.email"
|
:prepend-inner-icon="$globals.icons.email"
|
||||||
@@ -86,7 +86,6 @@ import { useUserApi } from "~/composables/api";
|
|||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { validators } from "@/composables/use-validators";
|
import { validators } from "@/composables/use-validators";
|
||||||
import { useRouteQuery } from "~/composables/use-router";
|
import { useRouteQuery } from "~/composables/use-router";
|
||||||
import type { VForm } from "~/types/auto-forms";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "basic",
|
layout: "basic",
|
||||||
@@ -100,8 +99,6 @@ const state = reactive({
|
|||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const passwordMatch = () => state.password === state.passwordConfirm || i18n.t("user.password-must-match");
|
const passwordMatch = () => state.password === state.passwordConfirm || i18n.t("user.password-must-match");
|
||||||
|
|
||||||
@@ -118,15 +115,6 @@ const token = useRouteQuery("token", "");
|
|||||||
// API
|
// API
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
async function requestLink() {
|
async function requestLink() {
|
||||||
if (!form.value) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { valid } = await form.value.validate();
|
|
||||||
if (!valid) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
// TODO: Fix Response to send meaningful error
|
// TODO: Fix Response to send meaningful error
|
||||||
const { response } = await api.users.resetPassword({
|
const { response } = await api.users.resetPassword({
|
||||||
@@ -139,11 +127,12 @@ async function requestLink() {
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
|
state.loading = false;
|
||||||
state.error = false;
|
state.error = false;
|
||||||
alert.success(i18n.t("user.password-updated"));
|
alert.success(i18n.t("user.password-updated"));
|
||||||
await navigateTo("/login");
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
state.loading = false;
|
||||||
state.error = true;
|
state.error = true;
|
||||||
alert.error(i18n.t("events.something-went-wrong"));
|
alert.error(i18n.t("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,7 +377,6 @@ const { store: allUnits } = useUnitStore();
|
|||||||
const { store: allFoods } = useFoodStore();
|
const { store: allFoods } = useFoodStore();
|
||||||
|
|
||||||
function itemCheckedToast(item: ShoppingListItemOut) {
|
function itemCheckedToast(item: ShoppingListItemOut) {
|
||||||
setTimeout(() => {
|
|
||||||
alert.info(
|
alert.info(
|
||||||
i18n.t("shopping-list.item-checked-off", { item: item.food?.name || item.note || i18n.t("recipe.ingredient") }),
|
i18n.t("shopping-list.item-checked-off", { item: item.food?.name || item.note || i18n.t("recipe.ingredient") }),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -392,7 +391,6 @@ function itemCheckedToast(item: ShoppingListItemOut) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -358,7 +358,7 @@
|
|||||||
"aliases": [],
|
"aliases": [],
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": "λείο κεφαλωτό μαρούλι",
|
"name": "λείο κεφαλωτό μαρούλι",
|
||||||
"plural_name": "λείο κεφαλωτό μαρούλι"
|
"plural_name": "butter lettuce"
|
||||||
},
|
},
|
||||||
"hash brown": {
|
"hash brown": {
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|||||||
@@ -31,13 +31,7 @@ def serve_manifest():
|
|||||||
"action": "/r/create/url",
|
"action": "/r/create/url",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"enctype": "application/x-www-form-urlencoded",
|
"enctype": "application/x-www-form-urlencoded",
|
||||||
"params": {
|
"params": {"text": "recipe_import_url"},
|
||||||
# 'url' is the field Chrome Android populates when sharing a page URL
|
|
||||||
"url": "recipe_import_url",
|
|
||||||
# 'text' is used by apps that share URLs as plain text; mapped to a
|
|
||||||
# separate param so the page can fall back to it when 'url' is absent
|
|
||||||
"text": "recipe_import_text",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"icons": [
|
"icons": [
|
||||||
{"src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any"},
|
{"src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any"},
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ dependencies = [
|
|||||||
"pydantic-settings==2.14.0",
|
"pydantic-settings==2.14.0",
|
||||||
"pillow-heif==1.3.0",
|
"pillow-heif==1.3.0",
|
||||||
"pyjwt==2.12.1",
|
"pyjwt==2.12.1",
|
||||||
"openai==2.34.0",
|
"openai==2.33.0",
|
||||||
"typing-extensions==4.15.0",
|
"typing-extensions==4.15.0",
|
||||||
"itsdangerous==2.2.0",
|
"itsdangerous==2.2.0",
|
||||||
"yt-dlp==2026.3.17",
|
"yt-dlp==2026.3.17",
|
||||||
|
|||||||
8
uv.lock
generated
8
uv.lock
generated
@@ -995,7 +995,7 @@ requires-dist = [
|
|||||||
{ name = "itsdangerous", specifier = "==2.2.0" },
|
{ name = "itsdangerous", specifier = "==2.2.0" },
|
||||||
{ name = "jinja2", specifier = "==3.1.6" },
|
{ name = "jinja2", specifier = "==3.1.6" },
|
||||||
{ name = "lxml", specifier = "==6.1.0" },
|
{ name = "lxml", specifier = "==6.1.0" },
|
||||||
{ name = "openai", specifier = "==2.34.0" },
|
{ name = "openai", specifier = "==2.33.0" },
|
||||||
{ name = "orjson", specifier = "==3.11.8" },
|
{ name = "orjson", specifier = "==3.11.8" },
|
||||||
{ name = "paho-mqtt", specifier = "==1.6.1" },
|
{ name = "paho-mqtt", specifier = "==1.6.1" },
|
||||||
{ name = "pillow", specifier = "==12.2.0" },
|
{ name = "pillow", specifier = "==12.2.0" },
|
||||||
@@ -1224,7 +1224,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.34.0"
|
version = "2.33.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@@ -1236,9 +1236,9 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/89/f1e78f5f828f4e97a6ebca8f45c6b35667da12b074ac490dc8362b882279/openai-2.34.0.tar.gz", hash = "sha256:828b4efcbb126352c2b5eb97d33ae890c92a71ab72511aefc1b7fe64aeccb07b", size = 759556, upload-time = "2026-05-04T17:34:08.721Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f0/ee/d056c82f63c05f06baac0cffb4a90952d8274f90c49dfe244f20497b9bbd/openai-2.33.0.tar.gz", hash = "sha256:f850c435e2a4685bba3295bd54912dd26315d9c1b7733068186134d6e0599f9a", size = 693254, upload-time = "2026-04-28T14:04:42.428Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/40/f090499f10514515081d09cb9da09f25b821eb20497e9423afe4f07b4ecf/openai-2.34.0-py3-none-any.whl", hash = "sha256:c996a71b1a210f3569844572ad4c609307e978515fb76877cf449b72596e549e", size = 1316535, upload-time = "2026-05-04T17:34:06.773Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/32/37734d769bc8b42e4938785313cc05aade6cb0fa72479d3220a0d61a4e78/openai-2.33.0-py3-none-any.whl", hash = "sha256:03ac37d70e8c9e3a8124214e3afa785e2cbc12e627fbd98177a086ef2fd87ad5", size = 1162695, upload-time = "2026-04-28T14:04:40.482Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user