mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-30 19:54:44 -04:00
feat: implement local storage for sorting and dynamic sort icons on the new recipe sort card (#1506)
* added new sort icons * added dynamic sort icons * implemented local storage for sorting and mobile card view * fixed bug with local storage booleans * added type hints * bum vue use to use merge defaults * use reactive localstorage * add $vuetify type * sort returns * fix type error Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
parent
34f52c06a6
commit
1b83c82997
@ -61,7 +61,7 @@
|
|||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
<v-btn text :icon="$vuetify.breakpoint.xsOnly" v-bind="attrs" :loading="sortLoading" v-on="on">
|
||||||
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
<v-icon :left="!$vuetify.breakpoint.xsOnly">
|
||||||
{{ $globals.icons.sort }}
|
{{ preferences.sortIcon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
|
{{ $vuetify.breakpoint.xsOnly ? null : $t("general.sort") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -102,11 +102,11 @@
|
|||||||
event: 'toggle-dense-view',
|
event: 'toggle-dense-view',
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
@toggle-dense-view="mobileCards = !mobileCards"
|
@toggle-dense-view="toggleMobileCards()"
|
||||||
/>
|
/>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<div v-if="recipes" class="mt-2">
|
<div v-if="recipes" class="mt-2">
|
||||||
<v-row v-if="!viewScale">
|
<v-row v-if="!useMobileCards">
|
||||||
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
<v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
|
||||||
<v-lazy>
|
<v-lazy>
|
||||||
<RecipeCard
|
<RecipeCard
|
||||||
@ -174,6 +174,7 @@ import RecipeCardMobile from "./RecipeCardMobile.vue";
|
|||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
import { useLazyRecipes, useSorter } from "~/composables/recipes";
|
import { useLazyRecipes, useSorter } from "~/composables/recipes";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
import { useUserSortPreferences } from "~/composables/use-users/preferences";
|
||||||
|
|
||||||
const SORT_EVENT = "sort";
|
const SORT_EVENT = "sort";
|
||||||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||||
@ -211,7 +212,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
const mobileCards = ref(false);
|
const preferences = useUserSortPreferences();
|
||||||
|
|
||||||
const utils = useSorter();
|
const utils = useSorter();
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
@ -223,8 +225,8 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { $globals, $vuetify } = useContext();
|
const { $globals, $vuetify } = useContext();
|
||||||
const viewScale = computed(() => {
|
const useMobileCards = computed(() => {
|
||||||
return mobileCards.value || $vuetify.breakpoint.smAndDown;
|
return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayTitleIcon = computed(() => {
|
const displayTitleIcon = computed(() => {
|
||||||
@ -247,18 +249,20 @@ export default defineComponent({
|
|||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const perPage = ref(30);
|
const perPage = ref(30);
|
||||||
const orderBy = ref("name");
|
|
||||||
const orderDirection = ref("asc");
|
|
||||||
const hasMore = ref(true);
|
const hasMore = ref(true);
|
||||||
|
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const { recipes, fetchMore } = useLazyRecipes();
|
const { fetchMore } = useLazyRecipes();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.usePagination) {
|
if (props.usePagination) {
|
||||||
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
|
const newRecipes = await fetchMore(
|
||||||
|
page.value,
|
||||||
|
perPage.value,
|
||||||
|
preferences.value.orderBy,
|
||||||
|
preferences.value.orderDirection
|
||||||
|
);
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
@ -273,7 +277,12 @@ export default defineComponent({
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
page.value = page.value + 1;
|
page.value = page.value + 1;
|
||||||
|
|
||||||
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
|
const newRecipes = await fetchMore(
|
||||||
|
page.value,
|
||||||
|
perPage.value,
|
||||||
|
preferences.value.orderBy,
|
||||||
|
preferences.value.orderDirection
|
||||||
|
);
|
||||||
if (!newRecipes.length) {
|
if (!newRecipes.length) {
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
} else {
|
} else {
|
||||||
@ -284,51 +293,39 @@ export default defineComponent({
|
|||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes
|
* sortRecipes helps filter using the API. This will eventually replace the sortRecipesFrontend function which pulls all recipes
|
||||||
(without pagination) and does the sorting in the frontend.
|
* (without pagination) and does the sorting in the frontend.
|
||||||
|
* TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above)
|
||||||
TODO: remove sortRecipesFrontend and remove duplicate "sortRecipes" section in the template (above)
|
* @param sortType
|
||||||
TODO: use indicator to show asc / desc order
|
*/
|
||||||
*/
|
|
||||||
|
|
||||||
function sortRecipes(sortType: string) {
|
function sortRecipes(sortType: string) {
|
||||||
if (state.sortLoading || loading.value) {
|
if (state.sortLoading || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setter(orderBy: string, ascIcon: string, descIcon: string) {
|
||||||
|
if (preferences.value.orderBy !== orderBy) {
|
||||||
|
preferences.value.orderBy = orderBy;
|
||||||
|
preferences.value.orderDirection = "asc";
|
||||||
|
} else {
|
||||||
|
preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc";
|
||||||
|
}
|
||||||
|
preferences.value.sortIcon = preferences.value.orderDirection === "asc" ? ascIcon : descIcon;
|
||||||
|
}
|
||||||
|
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case EVENTS.az:
|
case EVENTS.az:
|
||||||
if (orderBy.value !== "name") {
|
setter("name", $globals.icons.sortAlphabeticalAscending, $globals.icons.sortAlphabeticalDescending);
|
||||||
orderBy.value = "name";
|
|
||||||
orderDirection.value = "asc";
|
|
||||||
} else {
|
|
||||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case EVENTS.rating:
|
case EVENTS.rating:
|
||||||
if (orderBy.value !== "rating") {
|
setter("rating", $globals.icons.sortAscending, $globals.icons.sortDescending);
|
||||||
orderBy.value = "rating";
|
|
||||||
orderDirection.value = "desc";
|
|
||||||
} else {
|
|
||||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case EVENTS.created:
|
case EVENTS.created:
|
||||||
if (orderBy.value !== "created_at") {
|
setter("created_at", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending);
|
||||||
orderBy.value = "created_at";
|
|
||||||
orderDirection.value = "desc";
|
|
||||||
} else {
|
|
||||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case EVENTS.updated:
|
case EVENTS.updated:
|
||||||
if (orderBy.value !== "update_at") {
|
setter("updated_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending);
|
||||||
orderBy.value = "update_at";
|
|
||||||
orderDirection.value = "desc";
|
|
||||||
} else {
|
|
||||||
orderDirection.value = orderDirection.value === "asc" ? "desc" : "asc";
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown Event", sortType);
|
console.log("Unknown Event", sortType);
|
||||||
@ -344,7 +341,12 @@ export default defineComponent({
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
// fetch new recipes
|
// fetch new recipes
|
||||||
const newRecipes = await fetchMore(page.value, perPage.value, orderBy.value, orderDirection.value);
|
const newRecipes = await fetchMore(
|
||||||
|
page.value,
|
||||||
|
perPage.value,
|
||||||
|
preferences.value.orderBy,
|
||||||
|
preferences.value.orderDirection
|
||||||
|
);
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
|
|
||||||
state.sortLoading = false;
|
state.sortLoading = false;
|
||||||
@ -379,17 +381,22 @@ export default defineComponent({
|
|||||||
state.sortLoading = false;
|
state.sortLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMobileCards() {
|
||||||
|
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mobileCards,
|
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
EVENTS,
|
|
||||||
viewScale,
|
|
||||||
displayTitleIcon,
|
displayTitleIcon,
|
||||||
|
EVENTS,
|
||||||
infiniteScroll,
|
infiniteScroll,
|
||||||
loading,
|
loading,
|
||||||
navigateRandom,
|
navigateRandom,
|
||||||
|
preferences,
|
||||||
sortRecipes,
|
sortRecipes,
|
||||||
sortRecipesFrontend,
|
sortRecipesFrontend,
|
||||||
|
toggleMobileCards,
|
||||||
|
useMobileCards,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
28
frontend/composables/use-users/preferences.ts
Normal file
28
frontend/composables/use-users/preferences.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Ref, useContext } from "@nuxtjs/composition-api";
|
||||||
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
|
||||||
|
export interface UserRecipePreferences {
|
||||||
|
orderBy: string;
|
||||||
|
orderDirection: string;
|
||||||
|
sortIcon: string;
|
||||||
|
useMobileCards: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserSortPreferences(): Ref<UserRecipePreferences> {
|
||||||
|
const { $globals } = useContext();
|
||||||
|
|
||||||
|
const fromStorage = useLocalStorage(
|
||||||
|
"recipe-section-preferences",
|
||||||
|
{
|
||||||
|
orderBy: "name",
|
||||||
|
orderDirection: "asc",
|
||||||
|
sortIcon: $globals.icons.sortAlphabeticalAscending,
|
||||||
|
useMobileCards: false,
|
||||||
|
},
|
||||||
|
{ mergeDefaults: true }
|
||||||
|
// we cast to a Ref because by default it will return an optional type ref
|
||||||
|
// but since we pass defaults we know all properties are set.
|
||||||
|
) as Ref<UserRecipePreferences>;
|
||||||
|
|
||||||
|
return fromStorage;
|
||||||
|
}
|
@ -22,7 +22,7 @@
|
|||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@vue/composition-api": "^1.6.2",
|
"@vue/composition-api": "^1.6.2",
|
||||||
"@vueuse/core": "^8.5.0",
|
"@vueuse/core": "^9.0.2",
|
||||||
"core-js": "^3.23.1",
|
"core-js": "^3.23.1",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Plugin } from "@nuxt/types"
|
import { Plugin } from "@nuxt/types";
|
||||||
|
import { Framework } from "vuetify";
|
||||||
import { icons } from "~/utils/icons";
|
import { icons } from "~/utils/icons";
|
||||||
import { Icon } from "~/utils/icons/icon-type";
|
import { Icon } from "~/utils/icons/icon-type";
|
||||||
|
|
||||||
@ -15,13 +16,14 @@ declare module "vue/types/vue" {
|
|||||||
declare module "@nuxt/types" {
|
declare module "@nuxt/types" {
|
||||||
interface Context {
|
interface Context {
|
||||||
$globals: Globals;
|
$globals: Globals;
|
||||||
|
$vuetify: Framework;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalsPlugin: Plugin = (_, inject) => {
|
const globalsPlugin: Plugin = (_, inject) => {
|
||||||
inject("globals", {
|
inject("globals", {
|
||||||
icons
|
icons,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default globalsPlugin
|
export default globalsPlugin;
|
||||||
|
@ -78,6 +78,14 @@ export interface Icon {
|
|||||||
shareVariant: string;
|
shareVariant: string;
|
||||||
shuffleVariant: string;
|
shuffleVariant: string;
|
||||||
sort: string;
|
sort: string;
|
||||||
|
sortAscending: string;
|
||||||
|
sortDescending: string;
|
||||||
|
sortAlphabeticalAscending: string;
|
||||||
|
sortAlphabeticalDescending: string;
|
||||||
|
sortCalendarAscending: string;
|
||||||
|
sortCalendarDescending: string;
|
||||||
|
sortClockAscending: string;
|
||||||
|
sortClockDescending: string;
|
||||||
star: string;
|
star: string;
|
||||||
testTube: string;
|
testTube: string;
|
||||||
tools: string;
|
tools: string;
|
||||||
|
@ -45,6 +45,14 @@ import {
|
|||||||
mdiCodeJson,
|
mdiCodeJson,
|
||||||
mdiCog,
|
mdiCog,
|
||||||
mdiSort,
|
mdiSort,
|
||||||
|
mdiSortAscending,
|
||||||
|
mdiSortDescending,
|
||||||
|
mdiSortAlphabeticalAscending,
|
||||||
|
mdiSortAlphabeticalDescending,
|
||||||
|
mdiSortCalendarAscending,
|
||||||
|
mdiSortCalendarDescending,
|
||||||
|
mdiSortClockAscending,
|
||||||
|
mdiSortClockDescending,
|
||||||
mdiOrderAlphabeticalAscending,
|
mdiOrderAlphabeticalAscending,
|
||||||
mdiStar,
|
mdiStar,
|
||||||
mdiNewBox,
|
mdiNewBox,
|
||||||
@ -194,6 +202,14 @@ export const icons = {
|
|||||||
shareVariant: mdiShareVariant,
|
shareVariant: mdiShareVariant,
|
||||||
shuffleVariant: mdiShuffleVariant,
|
shuffleVariant: mdiShuffleVariant,
|
||||||
sort: mdiSort,
|
sort: mdiSort,
|
||||||
|
sortAscending: mdiSortAscending,
|
||||||
|
sortDescending: mdiSortDescending,
|
||||||
|
sortAlphabeticalAscending: mdiSortAlphabeticalAscending,
|
||||||
|
sortAlphabeticalDescending: mdiSortAlphabeticalDescending,
|
||||||
|
sortCalendarAscending: mdiSortCalendarAscending,
|
||||||
|
sortCalendarDescending: mdiSortCalendarDescending,
|
||||||
|
sortClockAscending: mdiSortClockAscending,
|
||||||
|
sortClockDescending: mdiSortClockDescending,
|
||||||
star: mdiStar,
|
star: mdiStar,
|
||||||
testTube: mdiTestTube,
|
testTube: mdiTestTube,
|
||||||
tools: mdiTools,
|
tools: mdiTools,
|
||||||
|
@ -2227,6 +2227,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
|
"@types/web-bluetooth@^0.0.15":
|
||||||
|
version "0.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72"
|
||||||
|
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
|
||||||
|
|
||||||
"@types/webpack-bundle-analyzer@3.9.3":
|
"@types/webpack-bundle-analyzer@3.9.3":
|
||||||
version "3.9.3"
|
version "3.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz#3a12025eb5d86069c30b47a157e62c0aca6e39a1"
|
resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz#3a12025eb5d86069c30b47a157e62c0aca6e39a1"
|
||||||
@ -2550,24 +2555,25 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
|
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
|
||||||
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
|
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
|
||||||
|
|
||||||
"@vueuse/core@^8.5.0":
|
"@vueuse/core@^9.0.2":
|
||||||
version "8.5.0"
|
version "9.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-8.5.0.tgz#2b7548e52165c88e1463756c36188e105d806543"
|
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.0.2.tgz#e6e59c45b43c1ffe4ad5149853a1ea4e8f1e3b12"
|
||||||
integrity sha512-VEJ6sGNsPlUp0o9BGda2YISvDZbhWJSOJu5zlp2TufRGVrLcYUKr31jyFEOj6RXzG3k/H4aCYeZyjpItfU8glw==
|
integrity sha512-kOIqaQPSs7OSByWg1ulEKRUJbsq3FmbJiUr0RhEKpt3O1Uhl4DrDj85DUbQBABVYgPvSaY6AE/fP3/FOcRIOoQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vueuse/metadata" "8.5.0"
|
"@types/web-bluetooth" "^0.0.15"
|
||||||
"@vueuse/shared" "8.5.0"
|
"@vueuse/metadata" "9.0.2"
|
||||||
|
"@vueuse/shared" "9.0.2"
|
||||||
vue-demi "*"
|
vue-demi "*"
|
||||||
|
|
||||||
"@vueuse/metadata@8.5.0":
|
"@vueuse/metadata@9.0.2":
|
||||||
version "8.5.0"
|
version "9.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-8.5.0.tgz#1aaa3787922cfda0f38243aaa7779366a669b4db"
|
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.0.2.tgz#373fdeb552f2a002ddc1f36c4027c936d78f8cb5"
|
||||||
integrity sha512-WxsD+Cd+bn+HcjpY6Dl9FJ8ywTRTT9pTwk3bCQpzEhXVYAyNczKDSahk50fCfIJKeWHhyI4B2+/ZEOxQAkUr0g==
|
integrity sha512-TRh+TNUYXiodatSAxd0xZc7sh4RfktVVgNFIN7TCQXKyancbCAcWfHvKfgdlX8LcqSBxKoHVa90n0XdUbboTkw==
|
||||||
|
|
||||||
"@vueuse/shared@8.5.0":
|
"@vueuse/shared@9.0.2":
|
||||||
version "8.5.0"
|
version "9.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-8.5.0.tgz#fa01ecd3161933f521dd6428b9ef8015ded1bbd3"
|
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.0.2.tgz#d3fb03594a9482d264702c67efe71d691ce67084"
|
||||||
integrity sha512-qKG+SZb44VvGD4dU5cQ63z4JE2Yk39hQUecR0a9sEdJA01cx+XrxAvFKJfPooxwoiqalAVw/ktWK6xbyc/jS3g==
|
integrity sha512-KwBDefK2ljLESpt0ffe2w8EGUCb3IaMfTzeytB/uHHjHOGOEIHLHHyn8W2C48uGQEvoe5iwaW4Bfp8cRUM6IFA==
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi "*"
|
vue-demi "*"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user