mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-27 16:24:31 -04:00 
			
		
		
		
	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:
		
							
								
								
									
										30
									
								
								docs/docs/changelog/template.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								docs/docs/changelog/template.md
									
									
									
									
									
										Normal 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... | ||||
							
								
								
									
										35
									
								
								docs/docs/changelog/v0.5.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/docs/changelog/v0.5.0.md
									
									
									
									
									
										Normal 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) | ||||
| @@ -10,12 +10,15 @@ To deploy docker on your local network it is highly recommended to use docker to | ||||
|  - linux/arm/v7 | ||||
|  - 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: | ||||
| ```shell | ||||
|  | ||||
|     ```shell | ||||
|     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 | ||||
| ``` | ||||
|     ``` | ||||
|  | ||||
| ## 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! | ||||
| @@ -60,7 +63,7 @@ services: | ||||
| | ---------------- | ------------------ | ----------------------------------------------------------------------------------- | | ||||
| | DB_TYPE          | sqlite             | The database type to be used. Current Options 'sqlite'                              | | ||||
| | 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                                              | | ||||
| | 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** | | ||||
|   | ||||
| @@ -77,6 +77,7 @@ nav: | ||||
|           - Guidelines: "contributors/developers-guide/general-guidelines.md" | ||||
|   - Development Road Map: "roadmap.md" | ||||
|   - Change Log: | ||||
|       - v0.5.0 General Upgrades: "changelog/v0.5.0.md" | ||||
|       - v0.4.3 Hot Fix: "changelog/v0.4.3.md" | ||||
|       - v0.4.2 Backend/Migrations: "changelog/v0.4.2.md" | ||||
|       - v0.4.1 Frontend/UI: "changelog/v0.4.1.md" | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <template> | ||||
|   <v-app> | ||||
|     <!-- Dummpy Comment --> | ||||
|     <TheAppBar /> | ||||
|     <v-main> | ||||
|       <v-banner v-if="demo" sticky | ||||
| @@ -7,10 +8,6 @@ | ||||
|           <b> This is a Demo</b> | Username: changeme@email.com | Password: demo | ||||
|         </div></v-banner | ||||
|       > | ||||
|  | ||||
|       <v-slide-x-reverse-transition> | ||||
|         <TheRecipeFab v-if="loggedIn" /> | ||||
|       </v-slide-x-reverse-transition> | ||||
|       <router-view></router-view> | ||||
|     </v-main> | ||||
|     <FlashMessage :position="'right bottom'"></FlashMessage> | ||||
| @@ -19,7 +16,6 @@ | ||||
|  | ||||
| <script> | ||||
| import TheAppBar from "@/components/UI/TheAppBar"; | ||||
| import TheRecipeFab from "@/components/UI/TheRecipeFab"; | ||||
| import Vuetify from "./plugins/vuetify"; | ||||
| import { user } from "@/mixins/user"; | ||||
|  | ||||
| @@ -28,7 +24,6 @@ export default { | ||||
|  | ||||
|   components: { | ||||
|     TheAppBar, | ||||
|     TheRecipeFab, | ||||
|   }, | ||||
|  | ||||
|   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() { | ||||
|     this.$store.dispatch("initTheme"); | ||||
|     this.$store.dispatch("requestRecentRecipes"); | ||||
| @@ -58,6 +45,7 @@ export default { | ||||
|     this.darkModeSystemCheck(); | ||||
|     this.darkModeAddEventListener(); | ||||
|     this.$store.dispatch("requestAppInfo"); | ||||
|     this.$store.dispatch("requestCustomPages"); | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|   | ||||
| @@ -3,23 +3,22 @@ | ||||
|     <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> | ||||
|     <v-list-item | ||||
|       dense | ||||
|       v-for="(ingredient, index) in displayIngredients" | ||||
|       v-for="(ingredient, index) in ingredients" | ||||
|       :key="generateKey('ingredient', index)" | ||||
|       @click="ingredient.checked = !ingredient.checked" | ||||
|       @click="toggleChecked(index)" | ||||
|     > | ||||
|       <v-checkbox | ||||
|         hide-details | ||||
|         v-model="ingredient.checked" | ||||
|         :value="checked[index]" | ||||
|         class="pt-0 my-auto py-auto" | ||||
|         color="secondary" | ||||
|         :readonly="true" | ||||
|       > | ||||
|       </v-checkbox> | ||||
|  | ||||
|       <v-list-item-content> | ||||
|         <vue-markdown | ||||
|           class="ma-0 pa-0 text-subtitle-1 dense-markdown" | ||||
|           :source="ingredient.text" | ||||
|           :source="ingredient" | ||||
|         > | ||||
|         </vue-markdown> | ||||
|       </v-list-item-content> | ||||
| @@ -37,18 +36,21 @@ export default { | ||||
|   props: { | ||||
|     ingredients: Array, | ||||
|   }, | ||||
|   computed: { | ||||
|     displayIngredients() { | ||||
|       return this.ingredients.map(x => ({ | ||||
|         text: x, | ||||
|         checked: false, | ||||
|       })); | ||||
|   data() { | ||||
|     return { | ||||
|       checked: [], | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.checked = this.ingredients.map(() => false); | ||||
|   }, | ||||
|   methods: { | ||||
|     generateKey(item, index) { | ||||
|       return utils.generateUniqueKey(item, index); | ||||
|     }, | ||||
|     toggleChecked(index) { | ||||
|       this.$set(this.checked, index, !this.checked[index]); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -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> | ||||
| @@ -1,29 +1,49 @@ | ||||
| <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 }"> | ||||
|       <v-text-field | ||||
|         class="mt-6" | ||||
|         ref="searchInput" | ||||
|         class="my-auto pt-1" | ||||
|         v-model="search" | ||||
|         v-bind="attrs" | ||||
|         :dense="dense" | ||||
|         light | ||||
|         :label="$t('search.search-mealie')" | ||||
|         autofocus | ||||
|         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" | ||||
|       > | ||||
|         <template #prepend-inner> | ||||
|           <v-icon color="grey lighten-3" size="29"> | ||||
|             mdi-magnify | ||||
|           </v-icon> | ||||
|         </template> | ||||
|       </v-text-field> | ||||
|     </template> | ||||
|     <v-card v-if="showResults" max-height="500" :max-width="maxWidth"> | ||||
|       <v-card-text class="flex row mx-auto"> | ||||
|     <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> | ||||
|         <router-link to="/search"> Advanced Search </router-link> | ||||
|       </v-card-text> | ||||
|       <v-divider></v-divider> | ||||
|       <v-list scrollable v-if="autoResults"> | ||||
| @@ -77,21 +97,21 @@ export default { | ||||
|     navOnClick: { | ||||
|       default: true, | ||||
|     }, | ||||
|     resetSearch: { | ||||
|       default: false, | ||||
|     }, | ||||
|     solo: { | ||||
|       default: true, | ||||
|     }, | ||||
|     autofocus: { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       isFocused: false, | ||||
|       searchSlug: "", | ||||
|       search: "", | ||||
|       menuModel: false, | ||||
|       result: [], | ||||
|       fuseResults: [], | ||||
|       isDark: false, | ||||
|       options: { | ||||
|         shouldSort: true, | ||||
|         threshold: 0.6, | ||||
| @@ -105,8 +125,10 @@ export default { | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.isDark = this.$store.getters.getIsDark; | ||||
|     this.$store.dispatch("requestAllRecipes"); | ||||
|     document.addEventListener("keydown", this.onDocumentKeydown); | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     document.removeEventListener("keydown", this.onDocumentKeydown); | ||||
|   }, | ||||
|   computed: { | ||||
|     data() { | ||||
| @@ -124,11 +146,7 @@ export default { | ||||
|   }, | ||||
|   watch: { | ||||
|     isSearching(val) { | ||||
|       val ? (this.menuModel = true) : null; | ||||
|     }, | ||||
|  | ||||
|     resetSearch(val) { | ||||
|       val ? (this.search = "") : null; | ||||
|       val ? (this.menuModel = true) : this.resetSearch(); | ||||
|     }, | ||||
|  | ||||
|     search() { | ||||
| @@ -167,9 +185,26 @@ export default { | ||||
|       this.$emit("selected", slug, name); | ||||
|     }, | ||||
|     async onFocus() { | ||||
|       clearTimeout(this.timeout); | ||||
|       this.$store.dispatch("requestAllRecipes"); | ||||
|       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> | ||||
| @@ -181,4 +216,9 @@ export default { | ||||
| </style> | ||||
|  | ||||
| <style lang="sass" scoped> | ||||
| .v-menu__content | ||||
|   width: 100 | ||||
|   &, & > * | ||||
|     display: flex | ||||
|     flex-direction: column | ||||
| </style> | ||||
| @@ -1,22 +1,29 @@ | ||||
| <template> | ||||
|   <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-app-bar dark color="primary"> | ||||
|           <v-toolbar-title class="headline">Search a Recipe</v-toolbar-title> | ||||
|         </v-app-bar> | ||||
|         <v-card-text> | ||||
|         <v-app-bar dark color="primary lighten-1" rounded="0"> | ||||
|           <SearchBar | ||||
|             ref="mealSearchBar" | ||||
|             @results="updateResults" | ||||
|             @selected="emitSelect" | ||||
|             :show-results="!isMobile" | ||||
|             max-width="550px" | ||||
|             max-width="568" | ||||
|             :dense="false" | ||||
|             :nav-on-click="false" | ||||
|             :reset-search="dialog" | ||||
|             :solo="false" | ||||
|             :autofocus="true" | ||||
|           /> | ||||
|           <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"> | ||||
|             <MobileRecipeCard | ||||
|               class="ma-1 px-0" | ||||
| @@ -29,7 +36,6 @@ | ||||
|               @selected="dialog = false" | ||||
|             /> | ||||
|           </div> | ||||
|           </div> | ||||
|         </v-card-text> | ||||
|       </v-card> | ||||
|     </v-dialog> | ||||
| @@ -74,11 +80,12 @@ export default { | ||||
|     }, | ||||
|     open() { | ||||
|       this.dialog = true; | ||||
|       this.$router.push("#mobile-search"); | ||||
|       this.$refs.mealSearchBar.resetSearch(); | ||||
|       this.$router.push("#search"); | ||||
|     }, | ||||
|     toggleDialog(open) { | ||||
|       if (open) { | ||||
|         this.$router.push("#mobile-search"); | ||||
|         this.$router.push("#search"); | ||||
|       } else { | ||||
|         this.$router.back(); // 😎 back button click | ||||
|       } | ||||
| @@ -92,4 +99,8 @@ export default { | ||||
|   align-items: flex-start; | ||||
|   justify-content: flex-start; | ||||
| } | ||||
|  | ||||
| .top-dialog { | ||||
|   align-self: flex-start; | ||||
| } | ||||
| </style> | ||||
| @@ -1,52 +1,18 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <TheSidebar ref="theSidebar" /> | ||||
|     <v-app-bar | ||||
|       v-if="!isMobile" | ||||
|       clipped-left | ||||
|       dense | ||||
|       app | ||||
|       color="primary" | ||||
|       dark | ||||
|       class="d-print-none" | ||||
|       :bottom="isMobile" | ||||
|     > | ||||
|       <router-link v-if="!(isMobile && search)" to="/"> | ||||
|         <v-btn icon> | ||||
|           <v-icon size="40"> mdi-silverware-variant </v-icon> | ||||
|       <v-btn icon @click="openSidebar"> | ||||
|         <v-icon> mdi-menu </v-icon> | ||||
|       </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="/"> | ||||
|         <v-btn icon> | ||||
|           <v-icon size="40"> mdi-silverware-variant </v-icon> | ||||
| @@ -54,21 +20,34 @@ | ||||
|       </router-link> | ||||
|  | ||||
|       <div v-if="!isMobile" btn class="pl-2"> | ||||
|         <v-toolbar-title style="cursor: pointer" @click="$router.push('/')" | ||||
|           >Mealie | ||||
|         <v-toolbar-title style="cursor: pointer" @click="$router.push('/')"> | ||||
|           Mealie | ||||
|         </v-toolbar-title> | ||||
|       </div> | ||||
|  | ||||
|       <v-spacer></v-spacer> | ||||
|       <v-expand-x-transition> | ||||
|         <SearchDialog ref="mainSearchDialog" /> | ||||
|       </v-expand-x-transition> | ||||
|       <v-btn icon @click="$refs.mainSearchDialog.open()"> | ||||
|         <v-icon>mdi-magnify</v-icon> | ||||
|       <SearchBar | ||||
|         v-if="!isMobile" | ||||
|         :show-results="true" | ||||
|         @selected="navigateFromSearch" | ||||
|         :max-width="isMobile ? '100%' : '450px'" | ||||
|       /> | ||||
|       <div v-else> | ||||
|         <v-btn icon @click="$refs.recipeSearch.open()"> | ||||
|           <v-icon> mdi-magnify </v-icon> | ||||
|         </v-btn> | ||||
|         <SearchDialog ref="recipeSearch"/> | ||||
|       </div> | ||||
|  | ||||
|       <TheSiteMenu /> | ||||
|  | ||||
|       <v-slide-x-reverse-transition> | ||||
|         <TheRecipeFab v-if="loggedIn && isMobile" /> | ||||
|       </v-slide-x-reverse-transition> | ||||
|     </v-app-bar> | ||||
|     <v-slide-x-reverse-transition> | ||||
|       <TheRecipeFab v-if="loggedIn && !isMobile" :absolute="true" /> | ||||
|     </v-slide-x-reverse-transition> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @@ -76,39 +55,40 @@ | ||||
| import TheSiteMenu from "@/components/UI/TheSiteMenu"; | ||||
| import SearchBar from "@/components/UI/Search/SearchBar"; | ||||
| import SearchDialog from "@/components/UI/Search/SearchDialog"; | ||||
| import TheRecipeFab from "@/components/UI/TheRecipeFab"; | ||||
| import TheSidebar from "@/components/UI/TheSidebar"; | ||||
| import { user } from "@/mixins/user"; | ||||
| export default { | ||||
|   name: "AppBar", | ||||
|  | ||||
|   mixins: [user], | ||||
|   components: { | ||||
|     SearchDialog, | ||||
|     TheRecipeFab, | ||||
|     TheSidebar, | ||||
|     TheSiteMenu, | ||||
|     SearchBar, | ||||
|     SearchDialog, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       search: false, | ||||
|       isMobile: false, | ||||
|       showSidebar: false, | ||||
|     }; | ||||
|   }, | ||||
|   watch: { | ||||
|     $route() { | ||||
|       this.search = false; | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     // isMobile() { | ||||
|     //   return this.$vuetify.breakpoint.name === "xs"; | ||||
|     // }, | ||||
|     isMobile() { | ||||
|       return this.$vuetify.breakpoint.name === "xs"; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     navigateFromSearch(slug) { | ||||
|       this.$router.push(`/recipe/${slug}`); | ||||
|     }, | ||||
|     openSidebar() { | ||||
|       this.$refs.theSidebar.toggleSidebar(); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| <style scoped> | ||||
| </style> | ||||
| @@ -54,16 +54,28 @@ | ||||
|         </v-form> | ||||
|       </v-card> | ||||
|     </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> | ||||
|         <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-btn> | ||||
|       </template> | ||||
|       <v-btn fab dark small color="primary" @click="addRecipe = true"> | ||||
|         <v-icon>mdi-link</v-icon> | ||||
|       </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-btn> | ||||
|     </v-speed-dial> | ||||
| @@ -74,6 +86,11 @@ | ||||
| import { api } from "@/api"; | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
|     absolute: { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       error: false, | ||||
| @@ -102,10 +119,6 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     navCreate() { | ||||
|       this.$router.push("/new"); | ||||
|     }, | ||||
|  | ||||
|     reset() { | ||||
|       this.fab = false; | ||||
|       this.error = false; | ||||
|   | ||||
| @@ -1,27 +1,8 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-btn | ||||
|       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 | ||||
|     > | ||||
|     <v-navigation-drawer v-model="showSidebar" width="180px" clipped app> | ||||
|       <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"> | ||||
|             <img | ||||
|               :src="userProfileImage" | ||||
| @@ -41,12 +22,11 @@ | ||||
|           </v-list-item-content> | ||||
|         </v-list-item> | ||||
|       </template> | ||||
| 
 | ||||
|       <v-divider></v-divider> | ||||
| 
 | ||||
|       <v-list nav dense> | ||||
|         <v-list-item | ||||
|           v-for="nav in baseLinks" | ||||
|           v-for="nav in effectiveMenu" | ||||
|           :key="nav.title" | ||||
|           link | ||||
|           :to="nav.to" | ||||
| @@ -58,22 +38,8 @@ | ||||
|         </v-list-item> | ||||
|       </v-list> | ||||
| 
 | ||||
|       <v-divider></v-divider> | ||||
|       <v-list nav dense v-if="user.admin"> | ||||
|         <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"> | ||||
|       <!-- Version List Item --> | ||||
|       <v-list nav dense class="fixedBottom" v-if="!isMain"> | ||||
|         <v-list-item to="/admin/about"> | ||||
|           <v-list-item-icon class="mr-3 pt-1"> | ||||
|             <v-icon :color="newVersionAvailable ? 'red--text' : ''"> | ||||
| @@ -104,20 +70,94 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { validators } from "@/mixins/validators"; | ||||
| import { initials } from "@/mixins/initials"; | ||||
| import { user } from "@/mixins/user"; | ||||
| import axios from "axios"; | ||||
| export default { | ||||
|   mixins: [validators, initials, user], | ||||
|   mixins: [initials, user], | ||||
|   data() { | ||||
|     return { | ||||
|       showSidebar: false, | ||||
|       links: [], | ||||
| 
 | ||||
|       latestVersion: null, | ||||
|       hideImage: false, | ||||
|       showSidebar: false, | ||||
|       mobile: false, | ||||
|       links: [], | ||||
|       superLinks: [ | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|     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", | ||||
|           to: "/admin/settings", | ||||
| @@ -138,34 +178,20 @@ export default { | ||||
|           to: "/admin/migrations", | ||||
|           title: this.$t("settings.migrations"), | ||||
|         }, | ||||
|       ], | ||||
|       baseLinks: [ | ||||
|         { | ||||
|           icon: "mdi-account", | ||||
|           to: "/admin/profile", | ||||
|           title: this.$t("settings.profile"), | ||||
|       ]; | ||||
|     }, | ||||
|         { | ||||
|           icon: "mdi-format-color-fill", | ||||
|           to: "/admin/themes", | ||||
|           title: this.$t("general.themes"), | ||||
|     adminMenu() { | ||||
|       if (this.user.admin) { | ||||
|         return [...this.settingsLinks, ...this.adminLinks]; | ||||
|       } else { | ||||
|         return this.settingsLinks; | ||||
|       } | ||||
|     }, | ||||
|         { | ||||
|           icon: "mdi-food", | ||||
|           to: "/admin/meal-planner", | ||||
|           title: this.$t("meal-plan.meal-planner"), | ||||
|     effectiveMenu() { | ||||
|       return this.isMain ? this.mainMenu : this.adminMenu; | ||||
|     }, | ||||
|       ], | ||||
|     }; | ||||
|   }, | ||||
|   async mounted() { | ||||
|     this.mobile = this.viewScale(); | ||||
|     this.showSidebar = !this.viewScale(); | ||||
|     this.getVersion(); | ||||
|   }, | ||||
| 
 | ||||
|   computed: { | ||||
|     userProfileImage() { | ||||
|       this.resetImage(); | ||||
|       return `api/users/${this.user.id}/image`; | ||||
|     }, | ||||
|     newVersionAvailable() { | ||||
| @@ -175,18 +201,26 @@ export default { | ||||
|       const appInfo = this.$store.getters.getAppInfo; | ||||
|       return appInfo.version; | ||||
|     }, | ||||
|     isLoggedIn() { | ||||
|       return this.$store.getters.getIsLoggedIn; | ||||
|     }, | ||||
|     isMobile() { | ||||
|       return this.$vuetify.breakpoint.name === "xs"; | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   methods: { | ||||
|     viewScale() { | ||||
|       switch (this.$vuetify.breakpoint.name) { | ||||
|         case "xs": | ||||
|           return true; | ||||
|         case "sm": | ||||
|           return true; | ||||
|         default: | ||||
|           return false; | ||||
|       } | ||||
|     resetImage() { | ||||
|       this.hideImage == false; | ||||
|     }, | ||||
|     resetView() { | ||||
|       this.showSidebar = !this.isMobile; | ||||
|     }, | ||||
|     toggleSidebar() { | ||||
|       this.showSidebar = !this.showSidebar; | ||||
|     }, | ||||
|     closeSidebar() { | ||||
|       this.showSidebar = false; | ||||
|     }, | ||||
|     async getVersion() { | ||||
|       let response = await axios.get( | ||||
| @@ -198,6 +232,7 @@ export default { | ||||
|           }, | ||||
|         } | ||||
|       ); | ||||
| 
 | ||||
|       this.latestVersion = response.data.tag_name; | ||||
|     }, | ||||
|   }, | ||||
| @@ -11,7 +11,7 @@ | ||||
|     > | ||||
|       <template v-slot:activator="{ on, attrs }"> | ||||
|         <v-btn v-bind="attrs" v-on="on" icon> | ||||
|           <v-icon>mdi-menu</v-icon> | ||||
|           <v-icon>mdi-account</v-icon> | ||||
|         </v-btn> | ||||
|       </template> | ||||
|  | ||||
|   | ||||
| @@ -145,15 +145,14 @@ | ||||
|     "view-recipe": "View Recipe" | ||||
|   }, | ||||
|   "search": { | ||||
|     "and": "And", | ||||
|     "search-mealie": "Search Mealie (press /)", | ||||
|     "search-placeholder": "Search...", | ||||
|     "max-results": "Max Results", | ||||
|     "category-filter": "Category Filter", | ||||
|     "exclude": "Exclude", | ||||
|     "include": "Include", | ||||
|     "max-results": "Max Results", | ||||
|     "or": "Or", | ||||
|     "search": "Search", | ||||
|     "search-mealie": "Search Mealie", | ||||
|     "search-placeholder": "Search...", | ||||
|     "tag-filter": "Tag Filter" | ||||
|   }, | ||||
|   "settings": { | ||||
|   | ||||
| @@ -4,19 +4,12 @@ | ||||
|       <v-slide-x-transition hide-on-leave> | ||||
|         <router-view></router-view> | ||||
|       </v-slide-x-transition> | ||||
|       <AdminSidebar /> | ||||
|     </v-container> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import AdminSidebar from "@/components/Admin/AdminSidebar"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     AdminSidebar, | ||||
|   }, | ||||
| }; | ||||
| export default {}; | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CategorySidebar /> | ||||
|      | ||||
|     <CardSection | ||||
|       v-if="siteSettings.showRecent" | ||||
|       :title="$t('page.recent')" | ||||
| @@ -23,11 +23,10 @@ | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import CardSection from "../components/UI/CardSection"; | ||||
| import CategorySidebar from "../components/UI/CategorySidebar"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     CardSection, | ||||
|     CategorySidebar, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
|                   color="primary" | ||||
|                   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-content> | ||||
|                   <v-list-item-title v-text="meal.name"></v-list-item-title> | ||||
|   | ||||
| @@ -40,7 +40,7 @@ | ||||
|             </v-col> | ||||
|             <v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12"> | ||||
|               <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-col> | ||||
|           </v-row> | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CategorySidebar /> | ||||
|     <CardSection | ||||
|       :sortable="true" | ||||
|       :title="$t('page.all-recipes')" | ||||
| @@ -13,11 +12,10 @@ | ||||
|  | ||||
| <script> | ||||
| import CardSection from "@/components/UI/CardSection"; | ||||
| import CategorySidebar from "@/components/UI/CategorySidebar"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     CardSection, | ||||
|     CategorySidebar, | ||||
|   }, | ||||
|   data() { | ||||
|     return {}; | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CategorySidebar /> | ||||
|     <CardSection | ||||
|       :sortable="true" | ||||
|       :title="title" | ||||
| @@ -15,11 +14,9 @@ | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import CardSection from "@/components/UI/CardSection"; | ||||
| import CategorySidebar from "@/components/UI/CategorySidebar"; | ||||
| export default { | ||||
|   components: { | ||||
|     CardSection, | ||||
|     CategorySidebar, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CategorySidebar /> | ||||
|     <v-card flat height="100%"> | ||||
|       <v-app-bar flat> | ||||
|         <v-spacer></v-spacer> | ||||
| @@ -32,13 +31,11 @@ | ||||
|  | ||||
| <script> | ||||
| import CardSection from "@/components/UI/CardSection"; | ||||
| import CategorySidebar from "@/components/UI/CategorySidebar"; | ||||
| import { api } from "@/api"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     CardSection, | ||||
|     CategorySidebar, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CategorySidebar /> | ||||
|     <CardSection | ||||
|       :sortable="true" | ||||
|       :title="title" | ||||
| @@ -15,11 +14,9 @@ | ||||
| <script> | ||||
| import { api } from "@/api"; | ||||
| import CardSection from "@/components/UI/CardSection"; | ||||
| import CategorySidebar from "@/components/UI/CategorySidebar"; | ||||
| export default { | ||||
|   components: { | ||||
|     CardSection, | ||||
|     CategorySidebar, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <template> | ||||
|   <v-container> | ||||
|     <CategorySidebar /> | ||||
|     <v-card flat> | ||||
|       <v-row dense> | ||||
|         <v-col> | ||||
| @@ -79,14 +78,12 @@ | ||||
| <script> | ||||
| import Fuse from "fuse.js"; | ||||
| import RecipeCard from "@/components/Recipe/RecipeCard"; | ||||
| import CategorySidebar from "@/components/UI/CategorySidebar"; | ||||
| import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector"; | ||||
| import FilterSelector from "./FilterSelector.vue"; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     RecipeCard, | ||||
|     CategorySidebar, | ||||
|     CategoryTagSelector, | ||||
|     FilterSelector, | ||||
|   }, | ||||
|   | ||||
| @@ -10,6 +10,7 @@ const state = { | ||||
|     cardsPerSection: 9, | ||||
|     categories: [], | ||||
|   }, | ||||
|   customPages: [], | ||||
| }; | ||||
|  | ||||
| const mutations = { | ||||
| @@ -18,6 +19,9 @@ const mutations = { | ||||
|     VueI18n.locale = payload.language; | ||||
|     Vuetify.framework.lang.current = payload.language; | ||||
|   }, | ||||
|   setCustomPages(state, payload) { | ||||
|     state.customPages = payload; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| const actions = { | ||||
| @@ -25,11 +29,16 @@ const actions = { | ||||
|     let settings = await api.siteSettings.get(); | ||||
|     commit("setSettings", settings); | ||||
|   }, | ||||
|   async requestCustomPages({commit }) { | ||||
|     const customPages = await api.siteSettings.getPages() | ||||
|     commit("setCustomPages", customPages) | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const getters = { | ||||
|   getActiveLang: state => state.siteSettings.language, | ||||
|   getSiteSettings: state => state.siteSettings, | ||||
|   getCustomPages: state => state.customPages, | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   | ||||
| @@ -6,8 +6,8 @@ from typing import Optional, Union | ||||
| import dotenv | ||||
| from pydantic import BaseSettings, Field, validator | ||||
|  | ||||
| APP_VERSION = "v0.4.3" | ||||
| DB_VERSION = "v0.4.0" | ||||
| APP_VERSION = "v0.5.0beta" | ||||
| DB_VERSION = "v0.5.0" | ||||
|  | ||||
| CWD = Path(__file__).parent | ||||
| BASE_DIR = CWD.parent.parent | ||||
|   | ||||
| @@ -7,7 +7,7 @@ from slugify import slugify | ||||
|  | ||||
|  | ||||
| class SiteSettings(CamelModel): | ||||
|     language: str = "en" | ||||
|     language: str = "en-US" | ||||
|     first_day_of_week: int = 0 | ||||
|     show_recent: bool = True | ||||
|     cards_per_section: int = 9 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user