bug/mobile-fixes (#426)

* search dialog rewrite

* lazy-load shopping list

* fit search bar

* event table

* set urls for static content

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-05-24 20:28:14 -08:00 committed by GitHub
parent 475cafae49
commit 8f8127a5fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 326 additions and 289 deletions

View file

@ -75,6 +75,9 @@ export default {
</script>
<style>
.top-dialog {
align-self: flex-start;
}
:root {
scrollbar-color: transparent transparent;
}

View file

@ -1,6 +1,6 @@
<template>
<v-row>
<SearchDialog ref="mealselect" @select="setSlug" />
<SearchDialog ref="mealselect" @selected="setSlug" />
<BaseDialog
title="Custom Meal"
:title-icon="$globals.icons.primary"
@ -78,7 +78,7 @@
</template>
<script>
import SearchDialog from "../UI/Search/SearchDialog";
import SearchDialog from "../UI/Dialogs/SearchDialog";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
import CardImage from "../Recipe/CardImage.vue";
@ -129,13 +129,13 @@ export default {
this.value[this.activeIndex]["meals"][0]["name"] = name;
this.value[this.activeIndex]["meals"][0]["description"] = description;
},
setSlug(name, slug) {
setSlug(recipe) {
switch (this.mode) {
case this.modes.primary:
this.setPrimary(name, slug);
this.setPrimary(recipe.name, recipe.slug);
break;
default:
this.setSide(name, slug);
this.setSide(recipe.name, recipe.slug);
break;
}
},

View file

@ -1,36 +1,44 @@
<template>
<v-card :ripple="false" class="mx-auto" hover :to="`/recipe/${slug}`" @click="$emit('selected')">
<v-list-item three-line>
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<v-img
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
></v-img>
<v-icon v-else color="primary" class="icon-position" size="100">
{{ $globals.icons.primary }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
<div class="d-flex justify-center align-center">
<v-rating
color="secondary"
class="ml-auto"
background-color="secondary lighten-3"
dense
length="5"
size="15"
:value="rating"
></v-rating>
<v-spacer></v-spacer>
<ContextMenu :slug="slug" menu-icon="mdi-dots-horizontal" />
</div>
</v-list-item-content>
</v-list-item>
</v-card>
<v-expand-transition>
<v-card
:ripple="false"
class="mx-auto"
hover
:to="this.$listeners.selected ? undefined : `/recipe/${slug}`"
@click="$emit('selected')"
>
<v-list-item three-line>
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<v-img
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
></v-img>
<v-icon v-else color="primary" class="icon-position" size="100">
{{ $globals.icons.primary }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
<div class="d-flex justify-center align-center">
<v-rating
color="secondary"
class="ml-auto"
background-color="secondary lighten-3"
dense
length="5"
size="15"
:value="rating"
></v-rating>
<v-spacer></v-spacer>
<ContextMenu :slug="slug" menu-icon="mdi-dots-horizontal" />
</div>
</v-list-item-content>
</v-list-item>
</v-card>
</v-expand-transition>
</template>
<script>

View file

@ -115,8 +115,5 @@ export default {
};
</script>
<style scoped>
.top-dialog {
align-self: flex-start;
}
<style>
</style>

View file

@ -0,0 +1,161 @@
<template>
<div>
<slot v-bind="{ open, close }"> </slot>
<v-dialog
v-model="dialog"
:width="isMobile ? undefined : '700'"
:height="isMobile ? undefined : '0'"
:fullscreen="isMobile"
content-class="top-dialog"
:scrollable="false"
>
<v-app-bar sticky dark color="primary lighten-1" :rounded="!isMobile">
<FuseSearchBar :raw-data="allItems" @results="filterItems" :search="searchString">
<v-text-field
id="arrow-search"
autofocus
v-model="searchString"
solo
flat
autocomplete="off"
background-color="primary lighten-1"
color="white"
dense
:clearable="!isMobile"
class="mx-2 arrow-search"
hide-details
single-line
:placeholder="$t('search.search')"
prepend-inner-icon="mdi-magnify"
>
</v-text-field>
</FuseSearchBar>
<v-btn v-if="isMobile" x-small fab light @click="dialog = false">
<v-icon>
mdi-close
</v-icon>
</v-btn>
</v-app-bar>
<v-card class="mt-1 pa-1" relative>
<v-card-actions>
<div class="mr-auto">
Results
</div>
<router-link to="/search"> Advanced Search </router-link>
</v-card-actions>
<MobileRecipeCard
v-for="(recipe, index) in results.slice(0, 10)"
:tabindex="index"
:key="index"
class="ma-1 arrow-nav"
:name="recipe.name"
:description="recipe.description"
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
:route="true"
v-on="$listeners.selected ? { selected: () => grabRecipe(recipe) } : {}"
/>
</v-card>
</v-dialog>
</div>
</template>
<script>
const SELECTED_EVENT = "selected";
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
export default {
components: {
FuseSearchBar,
MobileRecipeCard,
},
data() {
return {
selectedIndex: -1,
dialog: false,
searchString: "",
searchResults: [],
};
},
watch: {
$route() {
this.dialog = false;
},
dialog(val) {
if (!val) {
this.resetSelected();
}
},
},
mounted() {
this.$store.dispatch("requestAllRecipes");
document.addEventListener("keydown", this.onUpDown);
},
beforeDestroy() {
document.removeEventListener("keydown", this.onUpDown);
},
computed: {
isMobile() {
return this.$vuetify.breakpoint.name === "xs";
},
allItems() {
return this.$store.getters.getAllRecipes;
},
results() {
if (this.searchString != null && this.searchString.length >= 1) {
return this.searchResults;
}
return this.allItems;
},
},
methods: {
open() {
this.dialog = true;
},
close() {
this.dialog = false;
},
filterItems(val) {
this.searchResults = val.map(x => x.item);
},
grabRecipe(recipe) {
this.dialog = false;
this.$emit(SELECTED_EVENT, recipe);
},
onUpDown(e) {
if (e.keyCode === 38) {
e.preventDefault();
this.selectedIndex--;
} else if (e.keyCode === 40) {
e.preventDefault();
this.selectedIndex++;
} else {
return;
}
this.selectRecipe();
},
resetSelected() {
this.searchString = "";
this.selectedIndex = -1;
document.getElementsByClassName("arrow-nav")[0].focus();
},
selectRecipe() {
const recipeCards = document.getElementsByClassName("arrow-nav");
if (recipeCards) {
if (this.selectedIndex < 0) {
this.selectedIndex = -1;
document.getElementById("arrow-search").focus();
return;
}
this.selectedIndex >= recipeCards.length ? (this.selectedIndex = recipeCards.length - 1) : null;
recipeCards[this.selectedIndex].focus();
}
},
},
};
</script>
<style >
</style>

View file

@ -73,4 +73,8 @@ export default {
};
</script>
<style scoped></style>
<style scoped>
div {
width: 100%;
}
</style>

View file

@ -1,24 +1,20 @@
<template>
<v-menu v-model="menuModel" readonly offset-y offset-overflow max-height="75vh">
<template #activator="{ attrs }">
<SearchDialog ref="searchDialog">
<template v-slot="{ open }">
<v-text-field
readonly
@click="open"
ref="searchInput"
class="my-auto pt-1"
v-model="search"
v-bind="attrs"
:dense="dense"
class="my-auto mt-5 pt-1"
dense
light
dark
flat
:placeholder="$t('search.search-mealie')"
background-color="primary lighten-1"
color="white"
:solo="solo"
:style="`max-width: ${maxWidth};`"
@focus="onFocus"
@blur="isFocused = false"
autocomplete="off"
:autofocus="autofocus"
solo=""
:style="`max-width: 450;`"
>
<template #prepend-inner>
<v-icon color="grey lighten-3" size="29">
@ -27,122 +23,24 @@
</template>
</v-text-field>
</template>
<v-card v-if="showResults" max-height="75vh" :max-width="maxWidth" scrollable>
<v-card-text class="flex row mx-auto ">
<div class="mr-auto">
Results
</div>
<router-link to="/search"> Advanced Search </router-link>
</v-card-text>
<v-divider></v-divider>
<v-list scrollable v-if="autoResults">
<v-list-item
v-for="(item, index) in autoResults.slice(0, 15)"
:key="index"
:to="navOnClick ? `/recipe/${item.item.slug}` : null"
@click="navOnClick ? null : selected(item.item.slug, item.item.name)"
>
<v-list-item-avatar>
<v-img :src="getImage(item.item.slug)"></v-img>
</v-list-item-avatar>
<v-list-item-content @click="showResults ? null : selected(item.item.slug, item.item.name)">
<v-list-item-title v-html="highlight(item.item.name)"> </v-list-item-title>
<v-rating dense v-if="item.item.rating" :value="item.item.rating" size="12"> </v-rating>
<v-list-item-subtitle v-html="highlight(item.item.description)"> </v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</SearchDialog>
</template>
<script>
import Fuse from "fuse.js";
import { api } from "@/api";
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
export default {
props: {
showResults: {
default: false,
},
maxWidth: {
default: "450px",
},
dense: {
default: true,
},
navOnClick: {
default: true,
},
solo: {
default: true,
},
autofocus: {
default: false,
},
},
data() {
return {
isFocused: false,
searchSlug: "",
search: "",
menuModel: false,
result: [],
fuseResults: [],
options: {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
findAllMatches: true,
maxPatternLength: 32,
minMatchCharLength: 2,
keys: ["name", "description"],
},
};
components: {
SearchDialog,
},
mounted() {
document.addEventListener("keydown", this.onDocumentKeydown);
},
beforeDestroy() {
document.removeEventListener("keydown", this.onDocumentKeydown);
},
computed: {
data() {
return this.$store.getters.getAllRecipes;
},
autoResults() {
return this.fuseResults.length > 1 ? this.fuseResults : this.results;
},
fuse() {
return new Fuse(this.data, this.options);
},
isSearching() {
return this.search && this.search.length > 0;
},
},
watch: {
isSearching(val) {
val ? (this.menuModel = true) : this.resetSearch();
},
search() {
try {
this.result = this.fuse.search(this.search.trim());
} catch {
this.result = this.data.map(x => ({ item: x })).sort((a, b) => (a.name > b.name ? 1 : -1));
}
this.$emit("results", this.result);
if (this.showResults === true) {
this.fuseResults = this.result;
}
},
searchSlug() {
this.selected(this.searchSlug);
},
},
methods: {
highlight(string) {
if (!this.search) {
@ -150,23 +48,7 @@ export default {
}
return string.replace(new RegExp(this.search, "gi"), match => `<mark>${match}</mark>`);
},
getImage(image) {
return api.recipes.recipeTinyImage(image);
},
selected(slug, name) {
this.$emit("selected", slug, name);
},
async onFocus() {
this.$store.dispatch("requestAllRecipes");
this.isFocused = true;
},
resetSearch() {
this.$nextTick(() => {
this.search = "";
this.isFocused = false;
this.menuModel = false;
});
},
onDocumentKeydown(e) {
if (
e.key === "/" &&
@ -174,23 +56,10 @@ export default {
!document.activeElement.id.startsWith("input")
) {
e.preventDefault();
this.$refs.searchInput.focus();
this.$refs.searchDialog.open();
}
},
},
};
</script>
<style scoped>
.color-transition {
transition: background-color 0.3s ease;
}
</style>
<style lang="sass" scoped>
.v-menu__content
width: 100
&, & > *
display: flex
flex-direction: column
</style>

View file

@ -1,21 +1,23 @@
<template>
<div class="text-center ">
<v-dialog v-model="dialog" width="600px" height="0" :fullscreen="isMobile" content-class="top-dialog">
<v-card>
<v-dialog
v-model="dialog"
:width="isMobile ? undefined : '600'"
:height="isMobile ? undefined : '0'"
:fullscreen="isMobile"
content-class="top-dialog"
>
<v-card relative>
<v-app-bar dark color="primary lighten-1" rounded="0">
<SearchBar
ref="mealSearchBar"
@results="updateResults"
@selected="emitSelect"
:show-results="!isMobile"
max-width="568"
:dense="false"
:nav-on-click="false"
:autofocus="true"
/>
<v-btn icon @click="dialog = false" class="mt-1">
<v-icon> mdi-close </v-icon>
</v-btn>
</v-app-bar>
<v-card-text v-if="isMobile">
<div v-for="recipe in searchResults.slice(0, 7)" :key="recipe.name">
@ -31,6 +33,9 @@
/>
</div>
</v-card-text>
<v-btn v-if="isMobile" fab bottom @click="dialog = false" class="ma-2">
<v-icon> mdi-close </v-icon>
</v-btn>
</v-card>
</v-dialog>
</div>

View file

@ -46,7 +46,7 @@
<script>
import TheSiteMenu from "@/components/UI/TheSiteMenu";
import SearchBar from "@/components/UI/Search/SearchBar";
import SearchDialog from "@/components/UI/Search/SearchDialog";
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
import TheRecipeFab from "@/components/UI/TheRecipeFab";
import TheSidebar from "@/components/UI/TheSidebar";
import { user } from "@/mixins/user";

View file

@ -51,7 +51,7 @@
<v-spacer v-if="!isMobile"> </v-spacer>
<fuse-search-bar :raw-data="allItems" @results="filterItems" :search="searchString">
<fuse-search-bar class="fit-search mr-2" :raw-data="allItems" @results="filterItems" :search="searchString">
<v-text-field
v-model="searchString"
clearable
@ -218,4 +218,7 @@ export default {
height: auto !important;
flex-wrap: wrap;
}
.fit-search {
max-width: 300px;
}
</style>

View file

@ -62,85 +62,46 @@
</template>
</BaseDialog>
</v-card-actions>
<v-simple-table>
<template v-slot:default>
<thead>
<tr>
<th class="text-center">
{{ $t("general.type") }}
</th>
<th class="text-center">
{{ $t("general.name") }}
</th>
<th class="text-center">
{{ $t("general.general") }}
</th>
<th class="text-center">
{{ $t("general.recipe") }}
</th>
<th class="text-center">
{{ $t("events.database") }}
</th>
<th class="text-center">
{{ $t("events.scheduled") }}
</th>
<th class="text-center">
{{ $t("settings.migrations") }}
</th>
<th class="text-center">
{{ $t("group.group") }}
</th>
<th class="text-center">
{{ $t("user.user") }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in notifications" :key="index">
<td>
<v-avatar size="35" class="ma-1" :color="getIcon(item.type).icon ? 'primary' : undefined">
<v-icon dark v-if="getIcon(item.type).icon"> {{ getIcon(item.type).icon }}</v-icon>
<v-img v-else :src="getIcon(item.type).image"> </v-img>
</v-avatar>
{{ item.type }}
</td>
<td>
{{ item.name }}
</td>
<td class="text-center">
<v-icon color="success"> {{ item.general ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.recipe ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.backup ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.scheduled ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.migration ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.group ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.user ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<TheButton delete small minor @click="deleteNotification(item.id)"> </TheButton>
<TheButton edit small @click="testByID(item.id)">
<template v-slot:icon>
mdi-test-tube
</template>
{{ $t("general.test") }}
</TheButton>
</td>
</tr>
</tbody>
<v-data-table
disable-sort
:headers="headers"
:items="notifications"
class="elevation-1 text-center"
:footer-props="{
'items-per-page-options': [10, 20, 30, 40, -1],
}"
:items-per-page="10"
>
<template v-for="boolHeader in headers" v-slot:[`item.${boolHeader.value}`]="{ item }">
<div :key="boolHeader.value">
<div v-if="boolHeader.value === 'type'">
<v-avatar size="35" class="ma-1" :color="getIcon(item.type).icon ? 'primary' : undefined">
<v-icon dark v-if="getIcon(item.type).icon"> {{ getIcon(item.type).icon }}</v-icon>
<v-img v-else :src="getIcon(item.type).image"> </v-img>
</v-avatar>
{{ item[boolHeader.value] }}
</div>
<v-icon
v-else-if="item[boolHeader.value] === true || item[boolHeader.value] === false"
:color="item[boolHeader.value] ? 'success' : 'gray'"
>
{{ item[boolHeader.value] ? "mdi-check" : "mdi-close" }}
</v-icon>
<div v-else-if="boolHeader.text === 'Actions'">
<TheButton class="mr-1" delete x-small minor @click="deleteNotification(item.id)" />
<TheButton edit x-small @click="testByID(item.id)">
<template v-slot:icon>
mdi-test-tube
</template>
{{ $t("general.test") }}
</TheButton>
</div>
<div v-else>
{{ item[boolHeader.value] }}
</div>
</div>
</template>
</v-simple-table>
</v-data-table>
</v-card>
</div>
</template>
@ -179,19 +140,19 @@ export default {
},
{
text: "Discord",
image: "./static/discord.svg",
image: "/static/discord.svg",
},
{
text: "Gotify",
image: "./static/gotify.png",
image: "/static/gotify.png",
},
{
text: "Home Assistant",
image: "./static/home-assistant.png",
image: "/static/home-assistant.png",
},
{
text: "Pushover",
image: "./static/pushover.svg",
image: "/static/pushover.svg",
},
],
};
@ -199,6 +160,22 @@ export default {
mounted() {
this.getAllNotifications();
},
computed: {
headers() {
return [
{ text: this.$t("general.type"), value: "type" },
{ text: this.$t("general.name"), value: "name" },
{ text: this.$t("general.general"), value: "general", align: "center" },
{ text: this.$t("general.recipe"), value: "recipe", align: "center" },
{ text: this.$t("events.database"), value: "backup", align: "center" },
{ text: this.$t("events.scheduled"), value: "scheduled", align: "center" },
{ text: this.$t("settings.migrations"), value: "migration", align: "center" },
{ text: this.$t("group.group"), value: "group", align: "center" },
{ text: this.$t("user.user"), value: "user", align: "center" },
{ text: "Actions", align: "center" },
];
},
},
methods: {
getIcon(textValue) {
return this.notificationTypes.find(x => x.text === textValue);
@ -228,3 +205,9 @@ export default {
},
};
</script>
<style scoped>
th {
text-align: center !important;
}
</style>

View file

@ -92,20 +92,24 @@
<p v-if="!edit" class="mb-0">{{ item.quantity }}</p>
<v-icon v-if="!edit" small class="mx-3">
<v-icon v-if="!edit" x-small class="mx-3">
mdi-window-close
</v-icon>
<vue-markdown v-if="!edit" class="dense-markdown" :source="item.text"> </vue-markdown>
<v-textarea
single-line
rows="1"
auto-grow
class="mb-n2 pa-0"
dense
v-else
v-model="activeList.items[index].text"
></v-textarea>
<v-lazy>
<div>
<vue-markdown v-if="!edit" class="dense-markdown" :source="item.text"> </vue-markdown>
<v-textarea
single-line
rows="1"
auto-grow
class="mb-n2 pa-0"
dense
v-else
v-model="activeList.items[index].text"
></v-textarea>
</div>
</v-lazy>
</v-col>
<v-divider class="ma-1"></v-divider>
</v-row>