App Bar Rewrite (#347)

* Dummy Commit

* consolidate sidebar and app bar

* fix image error

* consolidate sidebar

* new icon for user menu

* fixes #329

* fix double click on mobile

* swap to computed properties

* fix open/close bug

* rewrite search for mobile

* fix ingredient checkbox

* cleanup console.logs

* set default lang + bump version

* draft changelog

* reword

* update env variables

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-04-25 13:47:08 -08:00 committed by GitHub
parent 7e6f3c9310
commit d5a340bde1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 384 additions and 370 deletions

View File

@ -0,0 +1,30 @@
# vx.x.x COOL TITLE GOES HERE
**App Version: vx.x.x**
**Database Version: vx.x.x**
## Breaking Changes
!!! error "Breaking Changes"
#### Database
#### ENV Variables
## Bug Fixes
- Fixed ...
## Features and Improvements
### General
- New Thing 1
### UI Improvements
-
### Behind the Scenes
- Refactoring...

View File

@ -0,0 +1,35 @@
# v0.5.0 COOL TITLE GOES HERE
**App Version: v0.5.0**
**Database Version: v0.5.0**
## Breaking Changes
!!! error "Breaking Changes"
#### Database
Database version has been bumped from v0.4.x -> v0.5.0. You will need to export and import your data.
## Bug Fixes
- Fixed #332 - Language settings are saved for one browser
- Fixes #281 - Slow Handling of Large Sets of Recipes
## Features and Improvements
### General
- More localization
- Start date for Week is now selectable
- Languages are now managed through Crowdin
- The main App bar went through a major overhaul
- Sidebar can now be toggled everywhere.
- New and improved mobile friendly bottom bar
- Improved styling for search bar in desktop
- Improved search layout on mobile
- Profile image now shown on all sidebars
### Behind the Scenes
- Unified Sidebar Components
- Refactor UI components to fit Vue best practices (WIP)

View File

@ -10,12 +10,15 @@ To deploy docker on your local network it is highly recommended to use docker to
- linux/arm/v7 - linux/arm/v7
- linux/arm64 - linux/arm64
!!! tip "Fix for linux/arm/v7 container on Raspberry Pi 4: 'Fatal Python error: init_interp_main: can't initialize time'" !!! tip "Fatal Python error: init_interp_main: can't initialize time"
Some users experience an problem with running the linux/arm/v7 container on Raspberry Pi 4. This is not a problem with the Mealie container, but with a bug in the hosts Docker installation.
Update the host RP4 using [instructions](linuxserver/docker-papermerge#4 (comment)), summarized here: Update the host RP4 using [instructions](linuxserver/docker-papermerge#4 (comment)), summarized here:
```shell
```shell
wget http://ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.1-1_armhf.deb wget http://ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.1-1_armhf.deb
sudo dpkg -i libseccomp2_2.5.1-1_armhf.deb sudo dpkg -i libseccomp2_2.5.1-1_armhf.deb
``` ```
## Quick Start - Docker CLI ## Quick Start - Docker CLI
Deployment with the Docker CLI can be done with `docker run` and specify the database type, in this case `sqlite`, setting the exposed port `9925`, mounting the current directory, and pull the latest image. After the image is up an running you can navigate to http://your.ip.addres:9925 and you'll should see mealie up and running! Deployment with the Docker CLI can be done with `docker run` and specify the database type, in this case `sqlite`, setting the exposed port `9925`, mounting the current directory, and pull the latest image. After the image is up an running you can navigate to http://your.ip.addres:9925 and you'll should see mealie up and running!
@ -60,7 +63,7 @@ services:
| ---------------- | ------------------ | ----------------------------------------------------------------------------------- | | ---------------- | ------------------ | ----------------------------------------------------------------------------------- |
| DB_TYPE | sqlite | The database type to be used. Current Options 'sqlite' | | DB_TYPE | sqlite | The database type to be used. Current Options 'sqlite' |
| DEFAULT_GROUP | Home | The default group for users | | DEFAULT_GROUP | Home | The default group for users |
| DEFAULT_USERNAME | changeme@email.com | The default username for the superuser | | DEFAULT_EMAIL | changeme@email.com | The default username for the superuser |
| DEFAULT_PASSWORD | MyPassword | The default password for the superuser | | DEFAULT_PASSWORD | MyPassword | The default password for the superuser |
| TOKEN_TIME | 2 | The time in hours that a login/auth token is valid | | TOKEN_TIME | 2 | The time in hours that a login/auth token is valid |
| API_PORT | 9000 | The port exposed by backend API. **do not change this if you're running in docker** | | API_PORT | 9000 | The port exposed by backend API. **do not change this if you're running in docker** |

View File

@ -77,6 +77,7 @@ nav:
- Guidelines: "contributors/developers-guide/general-guidelines.md" - Guidelines: "contributors/developers-guide/general-guidelines.md"
- Development Road Map: "roadmap.md" - Development Road Map: "roadmap.md"
- Change Log: - Change Log:
- v0.5.0 General Upgrades: "changelog/v0.5.0.md"
- v0.4.3 Hot Fix: "changelog/v0.4.3.md" - v0.4.3 Hot Fix: "changelog/v0.4.3.md"
- v0.4.2 Backend/Migrations: "changelog/v0.4.2.md" - v0.4.2 Backend/Migrations: "changelog/v0.4.2.md"
- v0.4.1 Frontend/UI: "changelog/v0.4.1.md" - v0.4.1 Frontend/UI: "changelog/v0.4.1.md"

View File

@ -1,5 +1,6 @@
<template> <template>
<v-app> <v-app>
<!-- Dummpy Comment -->
<TheAppBar /> <TheAppBar />
<v-main> <v-main>
<v-banner v-if="demo" sticky <v-banner v-if="demo" sticky
@ -7,10 +8,6 @@
<b> This is a Demo</b> | Username: changeme@email.com | Password: demo <b> This is a Demo</b> | Username: changeme@email.com | Password: demo
</div></v-banner </div></v-banner
> >
<v-slide-x-reverse-transition>
<TheRecipeFab v-if="loggedIn" />
</v-slide-x-reverse-transition>
<router-view></router-view> <router-view></router-view>
</v-main> </v-main>
<FlashMessage :position="'right bottom'"></FlashMessage> <FlashMessage :position="'right bottom'"></FlashMessage>
@ -19,7 +16,6 @@
<script> <script>
import TheAppBar from "@/components/UI/TheAppBar"; import TheAppBar from "@/components/UI/TheAppBar";
import TheRecipeFab from "@/components/UI/TheRecipeFab";
import Vuetify from "./plugins/vuetify"; import Vuetify from "./plugins/vuetify";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
@ -28,7 +24,6 @@ export default {
components: { components: {
TheAppBar, TheAppBar,
TheRecipeFab,
}, },
mixins: [user], mixins: [user],
@ -40,14 +35,6 @@ export default {
}, },
}, },
async created() {
window.addEventListener("keyup", e => {
if (e.key == "/" && !document.activeElement.id.startsWith("input")) {
this.search = !this.search;
}
});
},
async mounted() { async mounted() {
this.$store.dispatch("initTheme"); this.$store.dispatch("initTheme");
this.$store.dispatch("requestRecentRecipes"); this.$store.dispatch("requestRecentRecipes");
@ -58,6 +45,7 @@ export default {
this.darkModeSystemCheck(); this.darkModeSystemCheck();
this.darkModeAddEventListener(); this.darkModeAddEventListener();
this.$store.dispatch("requestAppInfo"); this.$store.dispatch("requestAppInfo");
this.$store.dispatch("requestCustomPages");
}, },
methods: { methods: {

View File

@ -3,23 +3,22 @@
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<v-list-item <v-list-item
dense dense
v-for="(ingredient, index) in displayIngredients" v-for="(ingredient, index) in ingredients"
:key="generateKey('ingredient', index)" :key="generateKey('ingredient', index)"
@click="ingredient.checked = !ingredient.checked" @click="toggleChecked(index)"
> >
<v-checkbox <v-checkbox
hide-details hide-details
v-model="ingredient.checked" :value="checked[index]"
class="pt-0 my-auto py-auto" class="pt-0 my-auto py-auto"
color="secondary" color="secondary"
:readonly="true"
> >
</v-checkbox> </v-checkbox>
<v-list-item-content> <v-list-item-content>
<vue-markdown <vue-markdown
class="ma-0 pa-0 text-subtitle-1 dense-markdown" class="ma-0 pa-0 text-subtitle-1 dense-markdown"
:source="ingredient.text" :source="ingredient"
> >
</vue-markdown> </vue-markdown>
</v-list-item-content> </v-list-item-content>
@ -37,18 +36,21 @@ export default {
props: { props: {
ingredients: Array, ingredients: Array,
}, },
computed: { data() {
displayIngredients() { return {
return this.ingredients.map(x => ({ checked: [],
text: x, };
checked: false,
}));
}, },
mounted() {
this.checked = this.ingredients.map(() => false);
}, },
methods: { methods: {
generateKey(item, index) { generateKey(item, index) {
return utils.generateUniqueKey(item, index); return utils.generateUniqueKey(item, index);
}, },
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
}, },
}; };
</script> </script>

View File

@ -1,110 +0,0 @@
<template>
<div>
<v-btn
class="mt-9 ml-n1"
fixed
left
bottom
fab
small
color="primary"
@click="showSidebar = !showSidebar"
>
<v-icon>mdi-tag</v-icon></v-btn
>
<v-navigation-drawer
:value="mobile ? showSidebar : true"
v-model="showSidebar"
width="175px"
clipped
app
>
<v-list nav dense>
<v-list-item v-for="nav in links" :key="nav.title" link :to="nav.to">
<v-list-item-icon>
<v-icon>{{ nav.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ nav.title | titleCase }}</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
</div>
</template>
<script>
import { api } from "@/api";
export default {
data() {
return {
showSidebar: false,
mobile: false,
links: [],
baseLinks: [
{
icon: "mdi-home",
to: "/",
title: this.$t("page.home-page"),
},
{
icon: "mdi-view-module",
to: "/recipes/all",
title: this.$t("page.all-recipes"),
},
{
icon: "mdi-magnify",
to: "/search",
title: this.$t('search.search'),
},
],
};
},
mounted() {
this.buildSidebar();
this.mobile = this.viewScale();
this.showSidebar = !this.viewScale();
},
methods: {
async buildSidebar() {
this.links = [];
this.links.push(...this.baseLinks);
const pages = await api.siteSettings.getPages();
if(pages.length > 0) {
pages.sort((a, b) => a.position - b.position);
pages.forEach(async element => {
this.links.push({
title: element.name,
to: `/pages/${element.slug}`,
icon: "mdi-tag",
});
});
}
else {
const categories = await api.categories.getAll();
categories.forEach(async element => {
this.links.push({
title: element.name,
to: `/recipes/category/${element.slug}`,
icon: "mdi-tag",
});
});
}
},
viewScale() {
switch (this.$vuetify.breakpoint.name) {
case "xs":
return true;
case "sm":
return true;
default:
return false;
}
},
},
};
</script>
<style>
</style>

View File

@ -1,29 +1,49 @@
<template> <template>
<v-menu v-model="menuModel" offset-y readonly :width="maxWidth"> <v-menu
v-model="menuModel"
readonly
offset-y
offset-overflow
max-height="75vh"
>
<template #activator="{ attrs }"> <template #activator="{ attrs }">
<v-text-field <v-text-field
class="mt-6" ref="searchInput"
class="my-auto pt-1"
v-model="search" v-model="search"
v-bind="attrs" v-bind="attrs"
:dense="dense" :dense="dense"
light light
:label="$t('search.search-mealie')" dark
autofocus flat
:placeholder="$t('search.search-mealie')"
background-color="primary lighten-1"
color="white"
:solo="solo" :solo="solo"
:style="`max-width: ${maxWidth};`" :style="`max-width: ${maxWidth};`"
@focus="onFocus" @focus="onFocus"
@blur="isFocused = false"
autocomplete="off" autocomplete="off"
:autofocus="autofocus"
> >
<template #prepend-inner>
<v-icon color="grey lighten-3" size="29">
mdi-magnify
</v-icon>
</template>
</v-text-field> </v-text-field>
</template> </template>
<v-card v-if="showResults" max-height="500" :max-width="maxWidth"> <v-card
<v-card-text class="flex row mx-auto"> v-if="showResults"
max-height="75vh"
:max-width="maxWidth"
scrollable
>
<v-card-text class="flex row mx-auto ">
<div class="mr-auto"> <div class="mr-auto">
Results Results
</div> </div>
<router-link to="/search"> <router-link to="/search"> Advanced Search </router-link>
Advanced Search
</router-link>
</v-card-text> </v-card-text>
<v-divider></v-divider> <v-divider></v-divider>
<v-list scrollable v-if="autoResults"> <v-list scrollable v-if="autoResults">
@ -77,21 +97,21 @@ export default {
navOnClick: { navOnClick: {
default: true, default: true,
}, },
resetSearch: {
default: false,
},
solo: { solo: {
default: true, default: true,
}, },
autofocus: {
default: false,
},
}, },
data() { data() {
return { return {
isFocused: false,
searchSlug: "", searchSlug: "",
search: "", search: "",
menuModel: false, menuModel: false,
result: [], result: [],
fuseResults: [], fuseResults: [],
isDark: false,
options: { options: {
shouldSort: true, shouldSort: true,
threshold: 0.6, threshold: 0.6,
@ -105,8 +125,10 @@ export default {
}; };
}, },
mounted() { mounted() {
this.isDark = this.$store.getters.getIsDark; document.addEventListener("keydown", this.onDocumentKeydown);
this.$store.dispatch("requestAllRecipes"); },
beforeDestroy() {
document.removeEventListener("keydown", this.onDocumentKeydown);
}, },
computed: { computed: {
data() { data() {
@ -124,11 +146,7 @@ export default {
}, },
watch: { watch: {
isSearching(val) { isSearching(val) {
val ? (this.menuModel = true) : null; val ? (this.menuModel = true) : this.resetSearch();
},
resetSearch(val) {
val ? (this.search = "") : null;
}, },
search() { search() {
@ -167,9 +185,26 @@ export default {
this.$emit("selected", slug, name); this.$emit("selected", slug, name);
}, },
async onFocus() { async onFocus() {
clearTimeout(this.timeout); this.$store.dispatch("requestAllRecipes");
this.isFocused = true; this.isFocused = true;
}, },
resetSearch() {
this.$nextTick(() => {
this.search = "";
this.isFocused = false;
this.menuModel = false;
});
},
onDocumentKeydown(e) {
if (
e.key === "/" &&
e.target !== this.$refs.searchInput.$refs.input &&
!document.activeElement.id.startsWith("input")
) {
e.preventDefault();
this.$refs.searchInput.focus();
}
},
}, },
}; };
</script> </script>
@ -181,4 +216,9 @@ export default {
</style> </style>
<style lang="sass" scoped> <style lang="sass" scoped>
.v-menu__content
width: 100
&, & > *
display: flex
flex-direction: column
</style> </style>

View File

@ -1,22 +1,29 @@
<template> <template>
<div class="text-center "> <div class="text-center ">
<v-dialog v-model="dialog" width="600px" height="0" :fullscreen="isMobile"> <v-dialog
v-model="dialog"
width="600px"
height="0"
:fullscreen="isMobile"
content-class="top-dialog"
>
<v-card> <v-card>
<v-app-bar dark color="primary"> <v-app-bar dark color="primary lighten-1" rounded="0">
<v-toolbar-title class="headline">Search a Recipe</v-toolbar-title>
</v-app-bar>
<v-card-text>
<SearchBar <SearchBar
ref="mealSearchBar"
@results="updateResults" @results="updateResults"
@selected="emitSelect" @selected="emitSelect"
:show-results="!isMobile" :show-results="!isMobile"
max-width="550px" max-width="568"
:dense="false" :dense="false"
:nav-on-click="false" :nav-on-click="false"
:reset-search="dialog" :autofocus="true"
:solo="false"
/> />
<div v-if="isMobile"> <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"> <div v-for="recipe in searchResults.slice(0, 7)" :key="recipe.name">
<MobileRecipeCard <MobileRecipeCard
class="ma-1 px-0" class="ma-1 px-0"
@ -29,7 +36,6 @@
@selected="dialog = false" @selected="dialog = false"
/> />
</div> </div>
</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -74,11 +80,12 @@ export default {
}, },
open() { open() {
this.dialog = true; this.dialog = true;
this.$router.push("#mobile-search"); this.$refs.mealSearchBar.resetSearch();
this.$router.push("#search");
}, },
toggleDialog(open) { toggleDialog(open) {
if (open) { if (open) {
this.$router.push("#mobile-search"); this.$router.push("#search");
} else { } else {
this.$router.back(); // 😎 back button click this.$router.back(); // 😎 back button click
} }
@ -92,4 +99,8 @@ export default {
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
} }
.top-dialog {
align-self: flex-start;
}
</style> </style>

View File

@ -1,52 +1,18 @@
<template> <template>
<div> <div>
<TheSidebar ref="theSidebar" />
<v-app-bar <v-app-bar
v-if="!isMobile"
clipped-left clipped-left
dense dense
app app
color="primary" color="primary"
dark dark
class="d-print-none" class="d-print-none"
:bottom="isMobile"
> >
<router-link v-if="!(isMobile && search)" to="/"> <v-btn icon @click="openSidebar">
<v-btn icon> <v-icon> mdi-menu </v-icon>
<v-icon size="40"> mdi-silverware-variant </v-icon>
</v-btn> </v-btn>
</router-link>
<div v-if="!isMobile" btn class="pl-2">
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
>Mealie
</v-toolbar-title>
</div>
<v-spacer></v-spacer>
<v-expand-x-transition>
<SearchBar
ref="mainSearchBar"
v-if="search"
:show-results="true"
@selected="navigateFromSearch"
:max-width="isMobile ? '100%' : '450px'"
/>
</v-expand-x-transition>
<v-btn icon @click="search = !search">
<v-icon>mdi-magnify</v-icon>
</v-btn>
<TheSiteMenu />
</v-app-bar>
<v-app-bar
v-else
bottom
clipped-left
dense
app
color="primary"
dark
class="d-print-none"
>
<router-link to="/"> <router-link to="/">
<v-btn icon> <v-btn icon>
<v-icon size="40"> mdi-silverware-variant </v-icon> <v-icon size="40"> mdi-silverware-variant </v-icon>
@ -54,21 +20,34 @@
</router-link> </router-link>
<div v-if="!isMobile" btn class="pl-2"> <div v-if="!isMobile" btn class="pl-2">
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')" <v-toolbar-title style="cursor: pointer" @click="$router.push('/')">
>Mealie Mealie
</v-toolbar-title> </v-toolbar-title>
</div> </div>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-expand-x-transition> <SearchBar
<SearchDialog ref="mainSearchDialog" /> v-if="!isMobile"
</v-expand-x-transition> :show-results="true"
<v-btn icon @click="$refs.mainSearchDialog.open()"> @selected="navigateFromSearch"
<v-icon>mdi-magnify</v-icon> :max-width="isMobile ? '100%' : '450px'"
/>
<div v-else>
<v-btn icon @click="$refs.recipeSearch.open()">
<v-icon> mdi-magnify </v-icon>
</v-btn> </v-btn>
<SearchDialog ref="recipeSearch"/>
</div>
<TheSiteMenu /> <TheSiteMenu />
<v-slide-x-reverse-transition>
<TheRecipeFab v-if="loggedIn && isMobile" />
</v-slide-x-reverse-transition>
</v-app-bar> </v-app-bar>
<v-slide-x-reverse-transition>
<TheRecipeFab v-if="loggedIn && !isMobile" :absolute="true" />
</v-slide-x-reverse-transition>
</div> </div>
</template> </template>
@ -76,39 +55,40 @@
import TheSiteMenu from "@/components/UI/TheSiteMenu"; import TheSiteMenu from "@/components/UI/TheSiteMenu";
import SearchBar from "@/components/UI/Search/SearchBar"; import SearchBar from "@/components/UI/Search/SearchBar";
import SearchDialog from "@/components/UI/Search/SearchDialog"; import SearchDialog from "@/components/UI/Search/SearchDialog";
import TheRecipeFab from "@/components/UI/TheRecipeFab";
import TheSidebar from "@/components/UI/TheSidebar";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
export default { export default {
name: "AppBar", name: "AppBar",
mixins: [user], mixins: [user],
components: { components: {
SearchDialog,
TheRecipeFab,
TheSidebar,
TheSiteMenu, TheSiteMenu,
SearchBar, SearchBar,
SearchDialog,
}, },
data() { data() {
return { return {
search: false, showSidebar: false,
isMobile: false,
}; };
}, },
watch: {
$route() {
this.search = false;
},
},
computed: { computed: {
// isMobile() { isMobile() {
// return this.$vuetify.breakpoint.name === "xs"; return this.$vuetify.breakpoint.name === "xs";
// }, },
}, },
methods: { methods: {
navigateFromSearch(slug) { navigateFromSearch(slug) {
this.$router.push(`/recipe/${slug}`); this.$router.push(`/recipe/${slug}`);
}, },
openSidebar() {
this.$refs.theSidebar.toggleSidebar();
},
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style scoped>
</style> </style>

View File

@ -54,16 +54,28 @@
</v-form> </v-form>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-speed-dial v-model="fab" fixed right bottom open-on-hover> <v-speed-dial
v-model="fab"
:open-on-hover="absolute"
:fixed="absolute"
:bottom="absolute"
:right="absolute"
>
<template v-slot:activator> <template v-slot:activator>
<v-btn v-model="fab" color="accent" dark fab> <v-btn
v-model="fab"
:color="absolute ? 'accent' : 'white'"
dark
:icon="!absolute"
:fab="absolute"
>
<v-icon> mdi-plus </v-icon> <v-icon> mdi-plus </v-icon>
</v-btn> </v-btn>
</template> </template>
<v-btn fab dark small color="primary" @click="addRecipe = true"> <v-btn fab dark small color="primary" @click="addRecipe = true">
<v-icon>mdi-link</v-icon> <v-icon>mdi-link</v-icon>
</v-btn> </v-btn>
<v-btn fab dark small color="accent" @click="navCreate"> <v-btn fab dark small color="accent" @click="$router.push('/new')">
<v-icon>mdi-square-edit-outline</v-icon> <v-icon>mdi-square-edit-outline</v-icon>
</v-btn> </v-btn>
</v-speed-dial> </v-speed-dial>
@ -74,6 +86,11 @@
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
props: {
absolute: {
default: false,
},
},
data() { data() {
return { return {
error: false, error: false,
@ -102,10 +119,6 @@ export default {
} }
}, },
navCreate() {
this.$router.push("/new");
},
reset() { reset() {
this.fab = false; this.fab = false;
this.error = false; this.error = false;

View File

@ -1,27 +1,8 @@
<template> <template>
<div> <div>
<v-btn <v-navigation-drawer v-model="showSidebar" width="180px" clipped app>
class="mt-9 ml-n1"
fixed
left
bottom
fab
small
color="primary"
@click="showSidebar = !showSidebar"
>
<v-icon>mdi-cog</v-icon></v-btn
>
<v-navigation-drawer
:value="mobile ? showSidebar : true"
v-model="showSidebar"
width="180px"
clipped
app
>
<template v-slot:prepend> <template v-slot:prepend>
<v-list-item two-line> <v-list-item two-line v-if="isLoggedIn">
<v-list-item-avatar color="accent" class="white--text"> <v-list-item-avatar color="accent" class="white--text">
<img <img
:src="userProfileImage" :src="userProfileImage"
@ -41,12 +22,11 @@
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</template> </template>
<v-divider></v-divider> <v-divider></v-divider>
<v-list nav dense> <v-list nav dense>
<v-list-item <v-list-item
v-for="nav in baseLinks" v-for="nav in effectiveMenu"
:key="nav.title" :key="nav.title"
link link
:to="nav.to" :to="nav.to"
@ -58,22 +38,8 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-divider></v-divider> <!-- Version List Item -->
<v-list nav dense v-if="user.admin"> <v-list nav dense class="fixedBottom" v-if="!isMain">
<v-list-item
v-for="nav in superLinks"
:key="nav.title"
link
:to="nav.to"
>
<v-list-item-icon>
<v-icon>{{ nav.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ nav.title }}</v-list-item-title>
</v-list-item>
</v-list>
<v-list nav dense class="fixedBottom">
<v-list-item to="/admin/about"> <v-list-item to="/admin/about">
<v-list-item-icon class="mr-3 pt-1"> <v-list-item-icon class="mr-3 pt-1">
<v-icon :color="newVersionAvailable ? 'red--text' : ''"> <v-icon :color="newVersionAvailable ? 'red--text' : ''">
@ -104,20 +70,94 @@
</template> </template>
<script> <script>
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials"; import { initials } from "@/mixins/initials";
import { user } from "@/mixins/user"; import { user } from "@/mixins/user";
import axios from "axios"; import axios from "axios";
export default { export default {
mixins: [validators, initials, user], mixins: [initials, user],
data() { data() {
return { return {
showSidebar: false,
links: [],
latestVersion: null, latestVersion: null,
hideImage: false, hideImage: false,
showSidebar: false, };
mobile: false, },
links: [], mounted() {
superLinks: [ this.getVersion();
this.resetView();
},
computed: {
isMain() {
const testVal = this.$route.path.split("/");
if (testVal[1] === "recipe") this.closeSidebar();
else this.resetView();
return !(testVal[1] === "admin");
},
baseMainLinks() {
return [
{
icon: "mdi-home",
to: "/",
title: this.$t("page.home-page"),
},
{
icon: "mdi-view-module",
to: "/recipes/all",
title: this.$t("page.all-recipes"),
},
{
icon: "mdi-magnify",
to: "/search",
title: this.$t("search.search"),
},
];
},
customPages() {
const pages = this.$store.getters.getCustomPages;
if (pages.length > 0) {
pages.sort((a, b) => a.position - b.position);
return pages.map(x => ({
title: x.name,
to: `/pages/${x.slug}`,
icon: "mdi-tag",
}));
} else {
const categories = this.$store.getters.getAllCategories;
return categories.map(x => ({
title: x.name,
to: `/recipes/category/${x.slug}`,
icon: "mdi-tag",
}));
}
},
mainMenu() {
return [...this.baseMainLinks, ...this.customPages];
},
settingsLinks() {
return [
{
icon: "mdi-account",
to: "/admin/profile",
title: this.$t("settings.profile"),
},
{
icon: "mdi-format-color-fill",
to: "/admin/themes",
title: this.$t("general.themes"),
},
{
icon: "mdi-food",
to: "/admin/meal-planner",
title: this.$t("meal-plan.meal-planner"),
},
];
},
adminLinks() {
return [
{ {
icon: "mdi-cog", icon: "mdi-cog",
to: "/admin/settings", to: "/admin/settings",
@ -138,34 +178,20 @@ export default {
to: "/admin/migrations", to: "/admin/migrations",
title: this.$t("settings.migrations"), title: this.$t("settings.migrations"),
}, },
], ];
baseLinks: [
{
icon: "mdi-account",
to: "/admin/profile",
title: this.$t("settings.profile"),
}, },
{ adminMenu() {
icon: "mdi-format-color-fill", if (this.user.admin) {
to: "/admin/themes", return [...this.settingsLinks, ...this.adminLinks];
title: this.$t("general.themes"), } else {
return this.settingsLinks;
}
}, },
{ effectiveMenu() {
icon: "mdi-food", return this.isMain ? this.mainMenu : this.adminMenu;
to: "/admin/meal-planner",
title: this.$t("meal-plan.meal-planner"),
}, },
],
};
},
async mounted() {
this.mobile = this.viewScale();
this.showSidebar = !this.viewScale();
this.getVersion();
},
computed: {
userProfileImage() { userProfileImage() {
this.resetImage();
return `api/users/${this.user.id}/image`; return `api/users/${this.user.id}/image`;
}, },
newVersionAvailable() { newVersionAvailable() {
@ -175,18 +201,26 @@ export default {
const appInfo = this.$store.getters.getAppInfo; const appInfo = this.$store.getters.getAppInfo;
return appInfo.version; return appInfo.version;
}, },
isLoggedIn() {
return this.$store.getters.getIsLoggedIn;
},
isMobile() {
return this.$vuetify.breakpoint.name === "xs";
},
}, },
methods: { methods: {
viewScale() { resetImage() {
switch (this.$vuetify.breakpoint.name) { this.hideImage == false;
case "xs": },
return true; resetView() {
case "sm": this.showSidebar = !this.isMobile;
return true; },
default: toggleSidebar() {
return false; this.showSidebar = !this.showSidebar;
} },
closeSidebar() {
this.showSidebar = false;
}, },
async getVersion() { async getVersion() {
let response = await axios.get( let response = await axios.get(
@ -198,6 +232,7 @@ export default {
}, },
} }
); );
this.latestVersion = response.data.tag_name; this.latestVersion = response.data.tag_name;
}, },
}, },

View File

@ -11,7 +11,7 @@
> >
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" icon> <v-btn v-bind="attrs" v-on="on" icon>
<v-icon>mdi-menu</v-icon> <v-icon>mdi-account</v-icon>
</v-btn> </v-btn>
</template> </template>

View File

@ -145,15 +145,14 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {

View File

@ -4,19 +4,12 @@
<v-slide-x-transition hide-on-leave> <v-slide-x-transition hide-on-leave>
<router-view></router-view> <router-view></router-view>
</v-slide-x-transition> </v-slide-x-transition>
<AdminSidebar />
</v-container> </v-container>
</div> </div>
</template> </template>
<script> <script>
import AdminSidebar from "@/components/Admin/AdminSidebar"; export default {};
export default {
components: {
AdminSidebar,
},
};
</script> </script>
<style> <style>

View File

@ -1,6 +1,6 @@
<template> <template>
<v-container> <v-container>
<CategorySidebar />
<CardSection <CardSection
v-if="siteSettings.showRecent" v-if="siteSettings.showRecent"
:title="$t('page.recent')" :title="$t('page.recent')"
@ -23,11 +23,10 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import CardSection from "../components/UI/CardSection"; import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar";
export default { export default {
components: { components: {
CardSection, CardSection,
CategorySidebar,
}, },
data() { data() {
return { return {

View File

@ -39,7 +39,7 @@
color="primary" color="primary"
class="headline font-weight-light white--text" class="headline font-weight-light white--text"
> >
<v-img :src="getImage(meal.image)"></v-img> <v-img :src="getImage(meal.slug)"></v-img>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title v-text="meal.name"></v-list-item-title> <v-list-item-title v-text="meal.name"></v-list-item-title>

View File

@ -40,7 +40,7 @@
</v-col> </v-col>
<v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12"> <v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12">
<v-card flat> <v-card flat>
<v-img :src="getImage(meal.image)" max-height="300"> </v-img> <v-img :src="getImage(meal.slug)" max-height="300"> </v-img>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>

View File

@ -1,6 +1,5 @@
<template> <template>
<v-container> <v-container>
<CategorySidebar />
<CardSection <CardSection
:sortable="true" :sortable="true"
:title="$t('page.all-recipes')" :title="$t('page.all-recipes')"
@ -13,11 +12,10 @@
<script> <script>
import CardSection from "@/components/UI/CardSection"; import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
export default { export default {
components: { components: {
CardSection, CardSection,
CategorySidebar,
}, },
data() { data() {
return {}; return {};

View File

@ -1,6 +1,5 @@
<template> <template>
<v-container> <v-container>
<CategorySidebar />
<CardSection <CardSection
:sortable="true" :sortable="true"
:title="title" :title="title"
@ -15,11 +14,9 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import CardSection from "@/components/UI/CardSection"; import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
export default { export default {
components: { components: {
CardSection, CardSection,
CategorySidebar,
}, },
data() { data() {
return { return {

View File

@ -1,6 +1,5 @@
<template> <template>
<v-container> <v-container>
<CategorySidebar />
<v-card flat height="100%"> <v-card flat height="100%">
<v-app-bar flat> <v-app-bar flat>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -32,13 +31,11 @@
<script> <script>
import CardSection from "@/components/UI/CardSection"; import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
import { api } from "@/api"; import { api } from "@/api";
export default { export default {
components: { components: {
CardSection, CardSection,
CategorySidebar,
}, },
data() { data() {
return { return {

View File

@ -1,6 +1,5 @@
<template> <template>
<v-container> <v-container>
<CategorySidebar />
<CardSection <CardSection
:sortable="true" :sortable="true"
:title="title" :title="title"
@ -15,11 +14,9 @@
<script> <script>
import { api } from "@/api"; import { api } from "@/api";
import CardSection from "@/components/UI/CardSection"; import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
export default { export default {
components: { components: {
CardSection, CardSection,
CategorySidebar,
}, },
data() { data() {
return { return {

View File

@ -1,6 +1,5 @@
<template> <template>
<v-container> <v-container>
<CategorySidebar />
<v-card flat> <v-card flat>
<v-row dense> <v-row dense>
<v-col> <v-col>
@ -79,14 +78,12 @@
<script> <script>
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import RecipeCard from "@/components/Recipe/RecipeCard"; import RecipeCard from "@/components/Recipe/RecipeCard";
import CategorySidebar from "@/components/UI/CategorySidebar";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import FilterSelector from "./FilterSelector.vue"; import FilterSelector from "./FilterSelector.vue";
export default { export default {
components: { components: {
RecipeCard, RecipeCard,
CategorySidebar,
CategoryTagSelector, CategoryTagSelector,
FilterSelector, FilterSelector,
}, },

View File

@ -10,6 +10,7 @@ const state = {
cardsPerSection: 9, cardsPerSection: 9,
categories: [], categories: [],
}, },
customPages: [],
}; };
const mutations = { const mutations = {
@ -18,6 +19,9 @@ const mutations = {
VueI18n.locale = payload.language; VueI18n.locale = payload.language;
Vuetify.framework.lang.current = payload.language; Vuetify.framework.lang.current = payload.language;
}, },
setCustomPages(state, payload) {
state.customPages = payload;
},
}; };
const actions = { const actions = {
@ -25,11 +29,16 @@ const actions = {
let settings = await api.siteSettings.get(); let settings = await api.siteSettings.get();
commit("setSettings", settings); commit("setSettings", settings);
}, },
async requestCustomPages({commit }) {
const customPages = await api.siteSettings.getPages()
commit("setCustomPages", customPages)
}
}; };
const getters = { const getters = {
getActiveLang: state => state.siteSettings.language, getActiveLang: state => state.siteSettings.language,
getSiteSettings: state => state.siteSettings, getSiteSettings: state => state.siteSettings,
getCustomPages: state => state.customPages,
}; };
export default { export default {

View File

@ -6,8 +6,8 @@ from typing import Optional, Union
import dotenv import dotenv
from pydantic import BaseSettings, Field, validator from pydantic import BaseSettings, Field, validator
APP_VERSION = "v0.4.3" APP_VERSION = "v0.5.0beta"
DB_VERSION = "v0.4.0" DB_VERSION = "v0.5.0"
CWD = Path(__file__).parent CWD = Path(__file__).parent
BASE_DIR = CWD.parent.parent BASE_DIR = CWD.parent.parent

View File

@ -7,7 +7,7 @@ from slugify import slugify
class SiteSettings(CamelModel): class SiteSettings(CamelModel):
language: str = "en" language: str = "en-US"
first_day_of_week: int = 0 first_day_of_week: int = 0
show_recent: bool = True show_recent: bool = True
cards_per_section: int = 9 cards_per_section: int = 9