Feature/search page (#259)

* add pillow dependencies

* advanced search page

* advanced search apge

* remove extra dependencies

* add pre-run script

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-04-03 17:18:01 -08:00 committed by GitHub
parent ec7fa6332d
commit 6d5f3e7496
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 383 additions and 91 deletions

View File

@ -14,25 +14,11 @@ RUN apk add --no-cache libxml2-dev \
libffi-dev \ libffi-dev \
python3 \ python3 \
python3-dev \ python3-dev \
openssl \
bash \
sudo \
# freetype-dev \
# fribidi-dev \
# harfbuzz-dev \
jpeg-dev \ jpeg-dev \
lcms2-dev \ lcms2-dev \
openjpeg-dev \ openjpeg-dev \
# tcl-dev \
# tiff-dev \
# tk-dev \
zlib-dev zlib-dev
ADD depends /depends
RUN cd /depends \
&& chmod +x install_webp.sh \
&& chmod +x download-and-extract.sh \
&& ./install_webp.sh
ENV ENV True ENV ENV True
EXPOSE 80 EXPOSE 80

View File

@ -1,18 +1,32 @@
<template> <template>
<div> <v-select
<v-select :items="allCategories"
:items="allCategories" v-model="selected"
v-model="selected" label="Categories"
label="Categories" chips
chips deletable-chips
deletable-chips :dense="dense"
dense item-text="name"
item-text="name" multiple
multiple :solo="solo"
return-object :return-object="returnObject"
@input="emitChange" :flat="flat"
></v-select> @input="emitChange"
</div> >
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeByIndex(data.index)"
label
color="accent"
dark
>
{{ data.item.name }}
</v-chip>
</template></v-select
>
</template> </template>
<script> <script>
@ -20,6 +34,15 @@ const MOUNTED_EVENT = "mounted";
export default { export default {
props: { props: {
value: Array, value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
}, },
data() { data() {
return { return {
@ -34,6 +57,9 @@ export default {
allCategories() { allCategories() {
return this.$store.getters.getAllCategories; return this.$store.getters.getAllCategories;
}, },
flat() {
return this.selected.length > 0 && this.solo;
},
}, },
methods: { methods: {
emitChange() { emitChange() {
@ -42,6 +68,9 @@ export default {
setInit(val) { setInit(val) {
this.selected = val; this.selected = val;
}, },
removeByIndex(index) {
this.selected.splice(index, 1);
},
}, },
}; };
</script> </script>

View File

@ -0,0 +1,79 @@
<template>
<v-select
:items="allTags"
v-model="selected"
label="Tags"
chips
deletable-chips
:dense="dense"
:solo="solo"
:flat="flat"
item-text="name"
multiple
:return-object="returnObject"
@input="emitChange"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeByIndex(data.index)"
label
color="accent"
dark
>
{{ data.item.name }}
</v-chip>
</template>
</v-select>
</template>
<script>
const MOUNTED_EVENT = "mounted";
export default {
props: {
value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
},
data() {
return {
selected: [],
};
},
mounted() {
this.$emit(MOUNTED_EVENT);
},
computed: {
allTags() {
return this.$store.getters.getAllTags;
},
flat() {
return this.selected.length > 0 && this.solo;
},
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
setInit(val) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -7,7 +7,7 @@
> >
<v-list-item> <v-list-item>
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4"> <v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
<v-img :src="getImage(image)"> </v-img> <v-img :src="getImage(slug)"> </v-img>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content class="align-self-start"> <v-list-item-content class="align-self-start">
<v-list-item-title> <v-list-item-title>

View File

@ -7,7 +7,7 @@
@click="$emit('click')" @click="$emit('click')"
min-height="275" min-height="275"
> >
<v-img height="200" :src="getImage(image)"> <v-img height="200" :src="getImage(slug)">
<v-expand-transition v-if="description"> <v-expand-transition v-if="description">
<div <div
v-if="hover" v-if="hover"

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="items && items.length > 0"> <div v-if="items.length > 0">
<h2 v-if="title" class="mt-4">{{ title }}</h2> <h2 v-if="title" class="mt-4">{{ title }}</h2>
<v-chip <v-chip
label label
@ -19,7 +19,9 @@
<script> <script>
export default { export default {
props: { props: {
items: Array, items: {
default: [],
},
title: { title: {
default: null, default: null,
}, },

View File

@ -51,6 +51,11 @@ export default {
to: "/recipes/all", to: "/recipes/all",
title: this.$t("page.all-recipes"), title: this.$t("page.all-recipes"),
}, },
{
icon: "mdi-magnify",
to: "/search",
title: "search",
},
], ],
}; };
}, },

View File

@ -27,7 +27,7 @@
@click="navOnClick ? null : selected(item.item.slug, item.item.name)" @click="navOnClick ? null : selected(item.item.slug, item.item.name)"
> >
<v-list-item-avatar> <v-list-item-avatar>
<v-img :src="getImage(item.item.image)"></v-img> <v-img :src="getImage(item.item.slug)"></v-img>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content <v-list-item-content
@click=" @click="
@ -136,6 +136,7 @@ export default {
this.fuseResults = this.result; this.fuseResults = this.result;
} }
}, },
searchSlug() { searchSlug() {
this.selected(this.searchSlug); this.selected(this.searchSlug);
}, },

View File

@ -1,58 +0,0 @@
<template>
<v-container>
<v-row justify="center">
<v-col cols="1"> </v-col>
<v-col>
<SearchBar @results="updateResults" :show-results="false" />
</v-col>
<v-col cols="2">
<v-btn icon>
<v-icon large> mdi-filter </v-icon>
</v-btn>
</v-col>
</v-row>
<v-row v-if="searchResults">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="item in searchResults.slice(0, 10)"
:key="item.item.name"
>
<RecipeCard
:name="item.item.name"
:description="item.item.description"
:slug="item.item.slug"
:rating="item.item.rating"
:image="item.item.image"
/>
</v-col>
</v-row>
</v-container>
</template>
<script>
import SearchBar from "../components/UI/Search/SearchBar";
import RecipeCard from "../components/Recipe/RecipeCard";
export default {
components: {
SearchBar,
RecipeCard,
},
data() {
return {
searchResults: [],
};
},
methods: {
updateResults(results) {
this.searchResults = results;
},
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,69 @@
<template>
<v-toolbar dense flat>
<v-btn-toggle
dense
v-model="selected"
tile
color="primary accent-3"
@change="emitMulti"
group
mandatory
>
<v-btn :value="false">
Include
</v-btn>
<v-btn :value="true">
Exclude
</v-btn>
</v-btn-toggle>
<v-spacer></v-spacer>
<v-btn-toggle
dense
v-model="match"
tile
color="primary accent-3"
@change="emitMulti"
group
mandatory
>
<v-btn :value="false">
And
</v-btn>
<v-btn :value="true">
Or
</v-btn>
</v-btn-toggle>
</v-toolbar>
</template>
<script>
export default {
props: {
value: {
default: "include", // Optionas: "include", "exclude", "any"
},
},
data() {
return {
selected: false,
match: false,
};
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
emitMulti() {
const updateData = {
exclude: this.selected,
matchAny: this.match,
};
this.$emit("update", updateData);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,178 @@
<template>
<v-container>
<CategorySidebar />
<v-card flat>
<v-row dense>
<v-col>
<v-text-field
v-model="searchString"
outlined
color="primary accent-3"
placeholder="Placeholder"
append-icon="mdi-magnify"
>
</v-text-field>
</v-col>
<v-col cols="12" md="2" sm="12">
<v-text-field
class="mt-0 pt-0"
label="Max Results"
v-model="maxResults"
type="number"
outlined
/>
</v-col>
</v-row>
<v-row dense class="mt-0 flex-row align-center justify-space-around">
<v-col>
<h3 class="pl-2 text-center headline">Category Filter</h3>
<FilterSelector class="mb-1" @update="updateCatParams" />
<CategorySelector
:solo="true"
:dense="false"
v-model="includeCategories"
:return-object="false"
/>
</v-col>
<v-col>
<h3 class="pl-2 text-center headline">Tag Filter</h3>
<FilterSelector class="mb-1" @update="updateTagParams" />
<TagSelector
:solo="true"
:dense="false"
v-model="includeTags"
:return-object="false"
/>
</v-col>
</v-row>
<v-row v-if="fuzzyRecipes">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="item in fuzzyRecipes.slice(0, maxResults)"
:key="item.name"
>
<RecipeCard
:name="item.item.name"
:description="item.item.description"
:slug="item.item.slug"
:rating="item.item.rating"
:image="item.item.image"
:tags="item.item.tags"
/>
</v-col>
</v-row>
</v-card>
</v-container>
</template>
<script>
import Fuse from "fuse.js";
import RecipeCard from "@/components/Recipe/RecipeCard";
import CategorySidebar from "@/components/UI/CategorySidebar";
import CategorySelector from "@/components/FormHelpers/CategorySelector";
import TagSelector from "@/components/FormHelpers/TagSelector";
import FilterSelector from "./FilterSelector.vue";
export default {
components: {
RecipeCard,
CategorySidebar,
CategorySelector,
TagSelector,
FilterSelector,
},
data() {
return {
searchString: "",
maxResults: 21,
searchResults: [],
catFilter: {
exclude: false,
matchAny: false,
},
tagFilter: {
exclude: false,
matchAny: false,
},
includeCategories: [],
includeTags: [],
options: {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
findAllMatches: true,
maxPatternLength: 32,
minMatchCharLength: 2,
keys: ["name", "description"],
},
};
},
computed: {
allRecipes() {
return this.$store.getters.getRecentRecipes;
},
filteredRecipes() {
return this.allRecipes.filter(recipe => {
const includesTags = this.check(
this.includeTags,
recipe.tags,
this.tagFilter.matchAny,
this.tagFilter.exclude
);
const includesCats = this.check(
this.includeCategories,
recipe.recipeCategory,
this.catFilter.matchAny,
this.catFilter.exclude
);
return [includesTags, includesCats].every(x => x === true);
});
},
fuse() {
return new Fuse(this.filteredRecipes, this.options);
},
fuzzyRecipes() {
if (this.searchString.trim() === "") {
return this.filteredRecipes.map(x => ({ item: x }));
}
const result = this.fuse.search(this.searchString.trim());
return result;
},
isSearching() {
return this.searchString && this.searchString.length > 0;
},
},
methods: {
check(filterBy, recipeList, matchAny, exclude) {
let isMatch = true;
if (filterBy.length === 0) return isMatch;
if (recipeList) {
if (matchAny) {
isMatch = filterBy.some(t => recipeList.includes(t)); // Checks if some items are a match
} else {
isMatch = filterBy.every(t => recipeList.includes(t)); // Checks if every items is a match
}
return exclude ? !isMatch : isMatch;
} else;
return false;
},
updateTagParams(params) {
this.tagFilter = params;
},
updateCatParams(params) {
this.catFilter = params;
},
},
};
</script>
<style>
</style>

View File

@ -2,6 +2,7 @@
# Initialize Database Prerun # Initialize Database Prerun
python mealie/db/init_db.py python mealie/db/init_db.py
python mealie/services/image/minify.py
## Migrations ## Migrations
# TODO # TODO