Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
0883e3ca13 fix(deps): update dependency authlib to v1.7.1 2026-05-09 13:13:18 +00:00
20 changed files with 70 additions and 197 deletions

View File

@@ -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)"

View File

@@ -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>

View File

@@ -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[]>(() => {

View File

@@ -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") {

View File

@@ -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>

View File

@@ -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",
}, },

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 });
} }
}); });

View File

@@ -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"));
} }

View File

@@ -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.
} }
}); });

View File

@@ -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
} }
} }

View File

@@ -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"));
} }

View File

@@ -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 {

View File

@@ -358,7 +358,7 @@
"aliases": [], "aliases": [],
"description": "", "description": "",
"name": "λείο κεφαλωτό μαρούλι", "name": "λείο κεφαλωτό μαρούλι",
"plural_name": "λείο κεφαλωτό μαρούλι" "plural_name": "butter lettuce"
}, },
"hash brown": { "hash brown": {
"aliases": [], "aliases": [],

View File

@@ -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"},

View File

@@ -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
View File

@@ -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]]