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 \
python3 \
python3-dev \
openssl \
bash \
sudo \
# freetype-dev \
# fribidi-dev \
# harfbuzz-dev \
jpeg-dev \
lcms2-dev \
openjpeg-dev \
# tcl-dev \
# tiff-dev \
# tk-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
EXPOSE 80

View File

@ -1,18 +1,32 @@
<template>
<div>
<v-select
:items="allCategories"
v-model="selected"
label="Categories"
chips
deletable-chips
dense
:dense="dense"
item-text="name"
multiple
return-object
:solo="solo"
:return-object="returnObject"
:flat="flat"
@input="emitChange"
></v-select>
</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>
<script>
@ -20,6 +34,15 @@ const MOUNTED_EVENT = "mounted";
export default {
props: {
value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
},
data() {
return {
@ -34,6 +57,9 @@ export default {
allCategories() {
return this.$store.getters.getAllCategories;
},
flat() {
return this.selected.length > 0 && this.solo;
},
},
methods: {
emitChange() {
@ -42,6 +68,9 @@ export default {
setInit(val) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
},
};
</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-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-content class="align-self-start">
<v-list-item-title>

View File

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

View File

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

View File

@ -51,6 +51,11 @@ export default {
to: "/recipes/all",
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)"
>
<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-content
@click="
@ -136,6 +136,7 @@ export default {
this.fuseResults = this.result;
}
},
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
python mealie/db/init_db.py
python mealie/services/image/minify.py
## Migrations
# TODO