mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 21:45:25 +02:00
Release v0.1.0 Candidate (#85)
* Changed uvicorn port to 80 * Changed port in docker-compose to match dockerfile * Readded environment variables in docker-compose * production image rework * Use opengraph metadata to make basic recipe cards when full recipe metadata is not available * fixed instrucitons on parse * add last_recipe * automated testing * roadmap update * Sqlite (#75) * file structure * auto-test * take 2 * refactor ap scheduler and startup process * fixed scraper error * database abstraction * database abstraction * port recipes over to new schema * meal migration * start settings migration * finale mongo port * backup improvements * migration imports to new DB structure * unused import cleanup * docs strings * settings and theme import logic * cleanup * fixed tinydb error * requirements * fuzzy search * remove scratch file * sqlalchemy models * improved search ui * recipe models almost done * sql modal population * del scratch * rewrite database model mixins * mostly grabage * recipe updates * working sqllite * remove old files and reorganize * final cleanup Co-authored-by: Hayden <hay-kot@pm.me> * Backup card (#78) * backup / import dialog * upgrade to new tag method * New import card * rename settings.py to app_config.py * migrate to poetry for development * fix failing test Co-authored-by: Hayden <hay-kot@pm.me> * added mkdocs to docker-compose * Translations (#72) * Translations + danish * changed back proxy target to use ENV * Resolved more merge conflicts * Removed test in translation * Documentation of translations * Updated translations * removed old packages Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com> * fail to start bug fixes * feature: prep/cook/total time slots (#80) Co-authored-by: Hayden <hay-kot@pm.me> * missing bind attributes * Bug fixes (#81) * fix: url remains after succesful import * docs: changelog + update todos * arm image * arm compose * compose updates * update poetry * arm support Co-authored-by: Hayden <hay-kot@pm.me> * dockerfile hotfix * dockerfile hotfix * Version Release Final Touches (#84) * Remove slim * bug: opacity issues * bug: startup failure with no database * ci/cd on dev branch * formatting * v0.1.0 documentation Co-authored-by: Hayden <hay-kot@pm.me> * db init hotfix * bug: fix crash in mongo * fix mongo bug * fixed version notifier * finale changelog Co-authored-by: kentora <=> Co-authored-by: Hayden <hay-kot@pm.me> Co-authored-by: Richard Mitic <richard.h.mitic@gmail.com> Co-authored-by: kentora <kentora@kentora.dk>
This commit is contained in:
parent
f6c1fa0e8b
commit
88dfd40b8d
173 changed files with 10273 additions and 3735 deletions
5221
frontend/package-lock.json
generated
5221
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,29 +5,34 @@
|
|||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"lint": "vue-cli-service lint",
|
||||
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.8.2",
|
||||
"qs": "^6.9.4",
|
||||
"fuse.js": "^6.4.6",
|
||||
"qs": "^6.9.6",
|
||||
"v-jsoneditor": "^1.4.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-i18n": "^8.22.4",
|
||||
"vue-router": "^3.4.9",
|
||||
"vuetify": "^2.4.2",
|
||||
"vuex": "^3.6.0",
|
||||
"vuex-persistedstate": "^4.0.0-beta.2"
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@intlify/vue-i18n-loader": "^1.0.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.10",
|
||||
"@vue/cli-plugin-eslint": "^4.5.10",
|
||||
"@vue/cli-service": "^4.5.10",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"sass": "^1.32.0",
|
||||
"sass": "^1.32.4",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-cli-plugin-vuetify": "^2.0.9",
|
||||
"vue-cli-plugin-i18n": "~1.0.1",
|
||||
"vue-cli-plugin-vuetify": "^2.0.8",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.3.0"
|
||||
},
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<v-app-bar dense app color="primary" dark class="d-print-none">
|
||||
<v-btn @click="$router.push('/')" icon class="d-flex align-center">
|
||||
<v-icon size="40">
|
||||
mdi-silverware-variant
|
||||
</v-icon>
|
||||
<v-btn @click="$router.push('/')" icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
<div btn class="pl-2">
|
||||
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-expand-x-transition>
|
||||
<SearchBar
|
||||
class="mt-7"
|
||||
v-if="search"
|
||||
:show-results="true"
|
||||
@selected="navigateFromSearch"
|
||||
/>
|
||||
</v-expand-x-transition>
|
||||
<v-btn icon @click="toggleSearch">
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
|
@ -22,10 +27,6 @@
|
|||
<v-container>
|
||||
<AddRecipeFab />
|
||||
<SnackBar />
|
||||
<v-expand-transition>
|
||||
<SearchHeader v-show="search" />
|
||||
</v-expand-transition>
|
||||
|
||||
<router-view></router-view>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
@ -34,7 +35,7 @@
|
|||
|
||||
<script>
|
||||
import Menu from "./components/UI/Menu";
|
||||
import SearchHeader from "./components/UI/SearchHeader";
|
||||
import SearchBar from "./components/UI/SearchBar";
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
||||
import SnackBar from "./components/UI/SnackBar";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
|
@ -44,14 +45,14 @@ export default {
|
|||
components: {
|
||||
Menu,
|
||||
AddRecipeFab,
|
||||
SearchHeader,
|
||||
SnackBar
|
||||
SnackBar,
|
||||
SearchBar,
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -62,7 +63,7 @@ export default {
|
|||
},
|
||||
|
||||
data: () => ({
|
||||
search: false
|
||||
search: false,
|
||||
}),
|
||||
methods: {
|
||||
/**
|
||||
|
@ -90,8 +91,11 @@ export default {
|
|||
} else {
|
||||
this.search = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ export default {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
async import(fileName) {
|
||||
let response = await apiReq.post(backupURLs.importBackup(fileName));
|
||||
async import(fileName, data) {
|
||||
let response = await apiReq.post(backupURLs.importBackup(fileName), data);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response;
|
||||
},
|
||||
|
@ -29,6 +29,10 @@ export default {
|
|||
},
|
||||
|
||||
async create(tag, template) {
|
||||
if (typeof template == String) {
|
||||
template = [template];
|
||||
}
|
||||
console.log(tag, template);
|
||||
let response = apiReq.post(backupURLs.createBackup, {
|
||||
tag: tag,
|
||||
template: template,
|
||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
|||
let response = await apiReq.post(recipeURLs.createByURL, {
|
||||
url: recipeURL,
|
||||
});
|
||||
|
||||
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response;
|
||||
},
|
||||
|
@ -51,8 +51,9 @@ export default {
|
|||
async update(data) {
|
||||
const recipeSlug = data.slug;
|
||||
|
||||
apiReq.post(recipeURLs.update(recipeSlug), data);
|
||||
let response = await apiReq.post(recipeURLs.update(recipeSlug), data);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async delete(recipeSlug) {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Edit Meal Plan </v-card-title>
|
||||
<v-card-title class="headline"> {{$t('meal-plan.edit-meal-plan')}} </v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<MealPlanCard v-model="mealPlan.meals" />
|
||||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<v-btn color="success" text @click="update"> Update </v-btn>
|
||||
<v-btn color="success" text @click="update"> {{$t('general.update')}} </v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Create a New Meal Plan </v-card-title>
|
||||
<v-card-title class="headline">
|
||||
{{$t('meal-plan.create-a-new-meal-plan')}}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
|
@ -17,7 +19,7 @@
|
|||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="startComputedDateFormatted"
|
||||
label="Start Date"
|
||||
:label="$t('meal-plan.start-date')"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-calendar"
|
||||
readonly
|
||||
|
@ -45,7 +47,7 @@
|
|||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="endComputedDateFormatted"
|
||||
label="End Date"
|
||||
:label="$t('meal-plan.end-date')"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-calendar"
|
||||
readonly
|
||||
|
@ -69,9 +71,9 @@
|
|||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<v-btn color="success" @click="random" v-if="meals[1]" text>
|
||||
Random
|
||||
{{$t('general.random')}}
|
||||
</v-btn>
|
||||
<v-btn color="success" @click="save" text> Save </v-btn>
|
||||
<v-btn color="success" @click="save" text> {{$t('general.save')}} </v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="show = !show"> </v-btn>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-row justify="center">
|
||||
<v-dialog v-model="dialog" persistent max-width="800">
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Choose a Recipe </v-card-title>
|
||||
<v-card-title class="headline"> {{$t('meal-plan.choose-a-recipe')}} </v-card-title>
|
||||
<v-card-text>
|
||||
<v-autocomplete
|
||||
:items="availableRecipes"
|
||||
|
@ -13,14 +13,12 @@
|
|||
hide-details
|
||||
hide-selected
|
||||
item-text="slug"
|
||||
label="Search for a Recipe"
|
||||
:label="$t('search.search-for-a-recipe')"
|
||||
single-line
|
||||
>
|
||||
<template v-slot:no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
Search for your Favorite
|
||||
<strong>Recipe</strong>
|
||||
<v-list-item-title :v-html="$t('search.search-for-your-favorite-recipe')">
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
@ -44,8 +42,8 @@
|
|||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="secondary" text @click="dialog = false"> Close </v-btn>
|
||||
<v-btn color="secondary" text @click="dialog = false"> Select </v-btn>
|
||||
<v-btn color="secondary" text @click="dialog = false"> {{$t('general.close')}} </v-btn>
|
||||
<v-btn color="secondary" text @click="dialog = false"> {{$t('general.select')}} </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
|
@ -10,17 +10,16 @@
|
|||
v-on="on"
|
||||
@click="inputText = ''"
|
||||
>
|
||||
Bulk Add
|
||||
{{$t('new-recipe.bulk-add')}}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Bulk Add </v-card-title>
|
||||
<v-card-title class="headline"> {{$t('new-recipe.bulk-add')}} </v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p>
|
||||
Paste in your recipe data. Each line will be treated as an item in a
|
||||
list
|
||||
{{$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')}}
|
||||
</p>
|
||||
<v-textarea v-model="inputText"> </v-textarea>
|
||||
</v-card-text>
|
||||
|
@ -29,7 +28,7 @@
|
|||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" text @click="save"> Save </v-btn>
|
||||
<v-btn color="success" text @click="save"> {{$t('general.save')}} </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
|
@ -6,21 +6,41 @@
|
|||
<v-col>
|
||||
<v-file-input
|
||||
v-model="fileObject"
|
||||
label="Image File"
|
||||
:label="$t('general.image-file')"
|
||||
truncate-length="30"
|
||||
@change="uploadImage"
|
||||
></v-file-input>
|
||||
</v-col>
|
||||
<v-col cols="3"></v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
label="Total Time"
|
||||
v-model="value.totalTime"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col
|
||||
><v-text-field
|
||||
label="Prep Time"
|
||||
v-model="value.prepTime"
|
||||
></v-text-field
|
||||
></v-col>
|
||||
<v-col
|
||||
><v-text-field
|
||||
label="Cook Time / Perform Time"
|
||||
v-model="value.performTime"
|
||||
></v-text-field
|
||||
></v-col>
|
||||
</v-row>
|
||||
</v-row>
|
||||
<v-text-field class="my-3" label="Recipe Name" v-model="value.name">
|
||||
<v-text-field class="my-3" :label="$t('recipe.recipe-name')" v-model="value.name">
|
||||
</v-text-field>
|
||||
<v-textarea height="100" label="Description" v-model="value.description">
|
||||
<v-textarea height="100" :label="$t('recipe.description')" v-model="value.description">
|
||||
</v-textarea>
|
||||
<div class="my-2"></div>
|
||||
<v-row dense disabled>
|
||||
<v-col sm="5">
|
||||
<v-text-field label="Servings" v-model="value.recipeYield">
|
||||
<v-text-field :label="$t('recipe.servings')" v-model="value.recipeYield">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
|
@ -34,7 +54,7 @@
|
|||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<h2 class="mb-4">Ingredients</h2>
|
||||
<h2 class="mb-4">{{$t('recipe.ingredients')}}</h2>
|
||||
<div
|
||||
v-for="(ingredient, index) in value.recipeIngredient"
|
||||
:key="generateKey('ingredient', index)"
|
||||
|
@ -51,7 +71,7 @@
|
|||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
label="Ingredient"
|
||||
:label="$t('recipe.ingredient')"
|
||||
v-model="value.recipeIngredient[index]"
|
||||
></v-text-field>
|
||||
</v-row>
|
||||
|
@ -61,7 +81,7 @@
|
|||
</v-btn>
|
||||
<BulkAdd @bulk-data="appendIngredients" />
|
||||
|
||||
<h2 class="mt-6">Categories</h2>
|
||||
<h2 class="mt-6">{{$t('recipe.categories')}}</h2>
|
||||
<v-combobox
|
||||
dense
|
||||
multiple
|
||||
|
@ -83,7 +103,7 @@
|
|||
</template>
|
||||
</v-combobox>
|
||||
|
||||
<h2 class="mt-4">Tags</h2>
|
||||
<h2 class="mt-4">{{$t('recipe.tags')}}</h2>
|
||||
<v-combobox dense multiple chips deletable-chips v-model="value.tags">
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
|
@ -98,7 +118,7 @@
|
|||
</template>
|
||||
</v-combobox>
|
||||
|
||||
<h2 class="my-4">Notes</h2>
|
||||
<h2 class="my-4">{{$t('recipe.notes')}}</h2>
|
||||
<v-card
|
||||
class="mt-1"
|
||||
v-for="(note, index) in value.notes"
|
||||
|
@ -122,7 +142,7 @@
|
|||
></v-text-field>
|
||||
</v-row>
|
||||
|
||||
<v-textarea label="Note" v-model="value.notes[index]['text']">
|
||||
<v-textarea :label="$t('recipe.note')" v-model="value.notes[index]['text']">
|
||||
</v-textarea>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -135,7 +155,7 @@
|
|||
<v-divider class="my-divider" :vertical="true"></v-divider>
|
||||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<h2 class="mb-4">Instructions</h2>
|
||||
<h2 class="mb-4">{{$t('recipe.instructions')}}</h2>
|
||||
<div v-for="(step, index) in value.recipeInstructions" :key="index">
|
||||
<v-hover v-slot="{ hover }">
|
||||
<v-card
|
||||
|
@ -153,7 +173,7 @@
|
|||
@click="removeStep(index)"
|
||||
>
|
||||
<v-icon color="error">mdi-delete</v-icon> </v-btn
|
||||
>Step: {{ index + 1 }}</v-card-title
|
||||
>{{ $t('recipe.step-index', {step: index + 1}) }}</v-card-title
|
||||
>
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
|
|
107
frontend/src/components/Recipe/RecipeTimeCard.vue
Normal file
107
frontend/src/components/Recipe/RecipeTimeCard.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<v-card
|
||||
color="accent"
|
||||
class="custom-transparent"
|
||||
tile
|
||||
:width="`${timeCardWidth}`"
|
||||
>
|
||||
<v-card-text
|
||||
class="text-caption white--text"
|
||||
v-if="totalTime || prepTime || performTime"
|
||||
>
|
||||
<v-row align="center" dense>
|
||||
<v-col :cols="iconColumn">
|
||||
<v-icon large color="white"> mdi-clock-outline </v-icon>
|
||||
</v-col>
|
||||
<v-divider
|
||||
vertical
|
||||
color="white"
|
||||
class="my-1"
|
||||
v-if="totalTime"
|
||||
></v-divider>
|
||||
<v-col v-if="totalTime">
|
||||
<div><strong> Total Time </strong></div>
|
||||
<div>{{ totalTime }}</div>
|
||||
</v-col>
|
||||
<v-divider
|
||||
vertical
|
||||
color="white"
|
||||
class="my-1"
|
||||
v-if="prepTime"
|
||||
></v-divider>
|
||||
<v-col v-if="prepTime">
|
||||
<div><strong> Prep Time </strong></div>
|
||||
<div>{{ prepTime }}</div>
|
||||
</v-col>
|
||||
<v-divider
|
||||
vertical
|
||||
color="white"
|
||||
class="my-1"
|
||||
v-if="performTime"
|
||||
></v-divider>
|
||||
<v-col v-if="performTime">
|
||||
<div><strong> Cook Time </strong></div>
|
||||
<div>{{ performTime }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
prepTime: String,
|
||||
totalTime: String,
|
||||
performTime: String,
|
||||
},
|
||||
computed: {
|
||||
timeLength() {
|
||||
let times = [];
|
||||
let timeArray = [this.totalTime, this.prepTime, this.performTime];
|
||||
timeArray.forEach((element) => {
|
||||
if (element) {
|
||||
times.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
return times.length;
|
||||
},
|
||||
iconColumn() {
|
||||
switch (this.timeLength) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return 4;
|
||||
case 2:
|
||||
return 3;
|
||||
case 3:
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
timeCardWidth() {
|
||||
let timeArray = [this.totalTime, this.prepTime, this.performTime];
|
||||
let width = 120;
|
||||
timeArray.forEach((element) => {
|
||||
if (element) {
|
||||
width += 70;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.$vuetify.breakpoint.name === "xs") {
|
||||
return "100%";
|
||||
}
|
||||
|
||||
return `${width}px`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-transparent {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
|
@ -32,7 +32,7 @@
|
|||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<h2 class="mb-4">Ingredients</h2>
|
||||
<h2 class="mb-4">{{$t('recipe.ingredients')}}</h2>
|
||||
<div
|
||||
v-for="(ingredient, index) in ingredients"
|
||||
:key="generateKey('ingredient', index)"
|
||||
|
@ -47,7 +47,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="categories[0]">
|
||||
<h2 class="mt-4">Categories</h2>
|
||||
<h2 class="mt-4">{{$t('recipe.categories')}}</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
|
@ -60,7 +60,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="tags[0]">
|
||||
<h2 class="mt-4">Tags</h2>
|
||||
<h2 class="mt-4">{{$t('recipe.tags')}}</h2>
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
|
@ -72,7 +72,7 @@
|
|||
</v-chip>
|
||||
</div>
|
||||
|
||||
<h2 v-if="notes[0]" class="my-4">Notes</h2>
|
||||
<h2 v-if="notes[0]" class="my-4">{{$t('recipe.notes')}}</h2>
|
||||
<v-card
|
||||
class="mt-1"
|
||||
v-for="(note, index) in notes"
|
||||
|
@ -87,7 +87,7 @@
|
|||
<v-divider class="my-divider" :vertical="true"></v-divider>
|
||||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<h2 class="mb-4">Instructions</h2>
|
||||
<h2 class="mb-4">{{$t('recipe.instructions')}}</h2>
|
||||
<v-hover
|
||||
v-for="(step, index) in instructions"
|
||||
:key="generateKey('step', index)"
|
||||
|
@ -99,7 +99,7 @@
|
|||
:elevation="hover ? 12 : 2"
|
||||
@click="toggleDisabled(index)"
|
||||
>
|
||||
<v-card-title>Step: {{ index + 1 }}</v-card-title>
|
||||
<v-card-title>{{ $t('recipe.step-index', {step: index + 1}) }}</v-card-title>
|
||||
<v-card-text>{{ step.text }}</v-card-text>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
|
@ -121,7 +121,7 @@
|
|||
target="_blank"
|
||||
class="rounded-sm mr-4"
|
||||
>
|
||||
Original Recipe
|
||||
{{$t('recipe.original-recipe')}}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
|
90
frontend/src/components/Settings/Backup/BackupCard.vue
Normal file
90
frontend/src/components/Settings/Backup/BackupCard.vue
Normal file
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div>
|
||||
<ImportDialog
|
||||
:name="selectedName"
|
||||
:date="selectedDate"
|
||||
ref="import_dialog"
|
||||
@import="importBackup"
|
||||
@delete="deleteBackup"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="4"
|
||||
v-for="backup in backups"
|
||||
:key="backup.name"
|
||||
>
|
||||
<v-card @click="openDialog(backup)">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" sm="2">
|
||||
<v-icon color="primary"> mdi-backup-restore </v-icon>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="10">
|
||||
<div>
|
||||
<strong>{{ backup.name }}</strong>
|
||||
</div>
|
||||
<div>{{ readableTime(backup.date) }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImportDialog from "./ImportDialog";
|
||||
import api from "../../../api";
|
||||
import utils from "../../../utils";
|
||||
export default {
|
||||
props: {
|
||||
backups: Array,
|
||||
},
|
||||
components: {
|
||||
ImportDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedName: "",
|
||||
selectedDate: "",
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openDialog(backup) {
|
||||
this.selectedDate = this.readableTime(backup.date);
|
||||
this.selectedName = backup.name;
|
||||
this.$refs.import_dialog.open();
|
||||
},
|
||||
readableTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
return utils.getDateAsText(date);
|
||||
},
|
||||
async importBackup(data) {
|
||||
this.$emit("loading");
|
||||
let response = await api.backups.import(data.name, data);
|
||||
|
||||
let failed = response.data.failed;
|
||||
let succesful = response.data.successful;
|
||||
|
||||
this.$emit("finished", succesful, failed);
|
||||
},
|
||||
deleteBackup(data) {
|
||||
this.$emit("loading");
|
||||
|
||||
api.backups.delete(data.name);
|
||||
this.selectedBackup = null;
|
||||
this.backupLoading = false;
|
||||
|
||||
this.$emit("finished");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
129
frontend/src/components/Settings/Backup/ImportDialog.vue
Normal file
129
frontend/src/components/Settings/Backup/ImportDialog.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-card>
|
||||
<v-card-title> {{ name }} </v-card-title>
|
||||
<v-card-subtitle class="mb-n3"> {{ date }} </v-card-subtitle>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mb-n4 mt-1"
|
||||
dense
|
||||
:label="$t('settings.backup.import-recipes')"
|
||||
v-model="importRecipes"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
class="my-n4"
|
||||
dense
|
||||
:label="$t('settings.backup.import-themes')"
|
||||
v-model="importThemes"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
class="my-n4"
|
||||
dense
|
||||
:label="$t('settings.backup.import-settings')"
|
||||
v-model="importSettings"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
<!-- <v-col>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-on="on" v-bind="attrs">
|
||||
<v-checkbox
|
||||
class="mb-n4 mt-1"
|
||||
dense
|
||||
label="Force"
|
||||
v-model="forceImport"
|
||||
></v-checkbox>
|
||||
</span>
|
||||
</template>
|
||||
<span>Force update existing recipes</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip top>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-on="on" v-bind="attrs">
|
||||
<v-checkbox
|
||||
class="mb-n4 mt-1"
|
||||
dense
|
||||
label="Rebase"
|
||||
v-model="rebaseImport"
|
||||
></v-checkbox>
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
>Removes all recipes, and then imports recipes from the
|
||||
backup</span
|
||||
>
|
||||
</v-tooltip>
|
||||
</v-col> -->
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn disabled color="success" text @click="raiseEvent('download')">
|
||||
{{$t('general.download')}}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="raiseEvent('delete')">
|
||||
{{$t('general.delete')}}
|
||||
</v-btn>
|
||||
<v-btn color="success" text @click="raiseEvent('import')">
|
||||
{{$t('general.import')}}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
default: "Backup Name",
|
||||
},
|
||||
date: {
|
||||
default: "Backup Date",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
importRecipes: true,
|
||||
forceImport: false,
|
||||
rebaseImport: false,
|
||||
importThemes: false,
|
||||
importSettings: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
close() {
|
||||
this.dialog = false;
|
||||
},
|
||||
raiseEvent(event) {
|
||||
let eventData = {
|
||||
name: this.name,
|
||||
recipes: this.importRecipes,
|
||||
force: this.forceImport,
|
||||
rebase: this.rebaseImport,
|
||||
themes: this.importThemes,
|
||||
settings: this.importSettings,
|
||||
};
|
||||
this.close();
|
||||
this.$emit(event, eventData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,61 +1,38 @@
|
|||
<template>
|
||||
<v-card :loading="backupLoading">
|
||||
<v-card-title class="headline"> Backup and Exports </v-card-title>
|
||||
<v-card :loading="backupLoading" class="mt-3">
|
||||
<v-card-title class="headline">
|
||||
{{$t('settings.backup-and-exports')}}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<p>
|
||||
Backups are exported in standard JSON format along with all the images
|
||||
stored on the file system. In your backup folder you'll find a .zip file
|
||||
that contains all of the recipe JSON and images from the database.
|
||||
Additionally, if you selected a markdown file, those will also be stored
|
||||
in the .zip file. To import a backup, it must be located in your backups
|
||||
folder. Automated backups are done each day at 3:00 AM.
|
||||
{{$t('settings.backup-info')}}
|
||||
</p>
|
||||
|
||||
<v-row dense align="center">
|
||||
<v-col dense cols="12" sm="12" md="4">
|
||||
<v-text-field v-model="backupTag" label="Backup Tag"></v-text-field>
|
||||
<v-text-field v-model="backupTag" :label="$t('settings.backup-tag')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-combobox
|
||||
auto-select-first
|
||||
label="Markdown Template"
|
||||
:label="$t('settings.markdown-template')"
|
||||
:items="availableTemplates"
|
||||
v-model="selectedTemplate"
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col dense cols="12" sm="12" md="2">
|
||||
<v-btn block color="accent" @click="createBackup" width="165">
|
||||
Backup Recipes
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row dense align="center">
|
||||
<v-col dense cols="12" sm="12" md="4">
|
||||
<v-form ref="form">
|
||||
<v-combobox
|
||||
auto-select-first
|
||||
label="Select a Backup for Import"
|
||||
:items="availableBackups"
|
||||
v-model="selectedBackup"
|
||||
:rules="[(v) => !!v || 'Backup Selection is Required']"
|
||||
required
|
||||
></v-combobox>
|
||||
</v-form>
|
||||
</v-col>
|
||||
<v-col dense cols="12" sm="12" md="3" lg="2">
|
||||
<v-btn block color="accent" @click="importBackup">
|
||||
Import Backup
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col dense cols="12" sm="12" md="2" lg="2">
|
||||
<v-btn block color="error" @click="deleteBackup">
|
||||
Delete Backup
|
||||
<v-btn block text color="accent" @click="createBackup" width="165">
|
||||
{{$t('settings.backup-recipes')}}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<BackupCard
|
||||
@loading="backupLoading = true"
|
||||
@finished="processFinished"
|
||||
:backups="availableBackups"
|
||||
/>
|
||||
<SuccessFailureAlert
|
||||
success-header="Successfully Imported"
|
||||
:success="successfulImports"
|
||||
|
@ -69,10 +46,12 @@
|
|||
<script>
|
||||
import api from "../../../api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import BackupCard from "./BackupCard";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SuccessFailureAlert,
|
||||
BackupCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -95,18 +74,6 @@ export default {
|
|||
this.availableBackups = response.imports;
|
||||
this.availableTemplates = response.templates;
|
||||
},
|
||||
async importBackup() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.backupLoading = true;
|
||||
|
||||
let response = await api.backups.import(this.selectedBackup);
|
||||
console.log(response.data);
|
||||
this.failedImports = response.data.failed;
|
||||
this.successfulImports = response.data.successful;
|
||||
|
||||
this.backupLoading = false;
|
||||
}
|
||||
},
|
||||
deleteBackup() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.backupLoading = true;
|
||||
|
@ -121,10 +88,7 @@ export default {
|
|||
async createBackup() {
|
||||
this.backupLoading = true;
|
||||
|
||||
let response = await api.backups.create(
|
||||
this.backupTag,
|
||||
this.selectedTemplate
|
||||
);
|
||||
let response = await api.backups.create(this.backupTag, this.templates);
|
||||
|
||||
if (response.status == 201) {
|
||||
this.selectedBackup = null;
|
||||
|
@ -132,6 +96,12 @@ export default {
|
|||
this.backupLoading = false;
|
||||
}
|
||||
},
|
||||
processFinished(successful = null, failed = null) {
|
||||
this.getAvailableBackups();
|
||||
this.backupLoading = false;
|
||||
this.successfulImports = successful;
|
||||
this.failedImports = failed;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
Currently Chowdown via public Repo URL is the only supported type of
|
||||
migration
|
||||
{{$t('migration.currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration')}}
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="5" sm="5">
|
||||
<v-text-field
|
||||
v-model="repo"
|
||||
label="Chowdown Repo URL"
|
||||
:label="$t('migration.chowdown-repo-url')"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="importRepo"> Migrate </v-btn>
|
||||
<v-btn text color="info" @click="importRepo"> {{$t('migration.migrate')}} </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-alert v-if="failedRecipes[1]" outlined dense type="error">
|
||||
<h4>Failed Recipes</h4>
|
||||
<h4>{{$t('migration.failed-recipes')}}</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedRecipes" :key="fail">
|
||||
{{ fail }}
|
||||
|
@ -28,7 +27,7 @@
|
|||
</v-list>
|
||||
</v-alert>
|
||||
<v-alert v-if="failedImages[1]" outlined dense type="error">
|
||||
<h4>Failed Images</h4>
|
||||
<h4>{{$t('migration.failed-images')}}</h4>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="fail in this.failedImages" :key="fail">
|
||||
{{ fail }}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<template>
|
||||
<v-card-text>
|
||||
<p>
|
||||
You can import recipes from either a zip file or a directory located in
|
||||
the /app/data/migraiton/ folder. Please review the documentation to ensure
|
||||
your directory structure matches what is expected
|
||||
{{$t('migration.you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected')}}
|
||||
</p>
|
||||
<v-form ref="form">
|
||||
<v-row align="center">
|
||||
|
@ -11,20 +9,20 @@
|
|||
<v-select
|
||||
:items="availableImports"
|
||||
v-model="selectedImport"
|
||||
label="Nextcloud Data"
|
||||
:label="$t('migration.nextcloud-data')"
|
||||
:rules="[rules.required]"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" sm="12">
|
||||
<v-btn text color="info" @click="importRecipes"> Migrate </v-btn>
|
||||
<v-btn text color="info" @click="importRecipes"> {{$t('migration.migrate')}} </v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="1" sm="12">
|
||||
<v-btn text color="error" @click="deleteImportValidation">
|
||||
Delete
|
||||
{{$t('general.delete')}}
|
||||
</v-btn>
|
||||
<Confirmation
|
||||
title="Delete Data"
|
||||
message="Are you sure you want to delete this migration data?"
|
||||
:title="$t('general.delete-data')"
|
||||
:message="$t('migration.delete-confirmation')"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
|
@ -39,9 +37,9 @@
|
|||
</v-row>
|
||||
</v-form>
|
||||
<SuccessFailureAlert
|
||||
success-header="Successfully Imported from Nextcloud"
|
||||
:success-header="$t('migration.successfully-imported-from-nextcloud')"
|
||||
:success="successfulImports"
|
||||
failed-header="Failed Imports"
|
||||
failed-header="$t('migration.failed-imports')"
|
||||
:failed="failedImports"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-form ref="file">
|
||||
<v-file-input
|
||||
:loading="loading"
|
||||
label="Upload an Archive"
|
||||
:label="$t('migration.upload-an-archive')"
|
||||
v-model="file"
|
||||
accept=".zip"
|
||||
@change="upload"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<v-card :loading="loading">
|
||||
<v-card-title class="headline"> Recipe Migration </v-card-title>
|
||||
<v-card-title class="headline"> {{$t('migration.recipe-migration')}} </v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-tabs v-model="tab">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</v-btn>
|
||||
<v-dialog v-model="dialog" width="400">
|
||||
<v-card>
|
||||
<v-card-title> {{ buttonText }} Color </v-card-title>
|
||||
<v-card-title> {{ buttonText }} {{$t('settings.color')}} </v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="color"> </v-text-field>
|
||||
<v-row>
|
||||
|
@ -26,8 +26,8 @@
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text @click="toggleSwatches"> Swatches </v-btn>
|
||||
<v-btn text @click="dialog = false"> Select </v-btn>
|
||||
<v-btn text @click="toggleSwatches"> {{$t('settings.swatches')}} </v-btn>
|
||||
<v-btn text @click="dialog = false"> {{$t('general.select')}} </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-btn text color="info" @click="dialog = true"> New </v-btn>
|
||||
<v-btn text color="info" @click="dialog = true"> {{$t('general.new')}} </v-btn>
|
||||
<v-dialog v-model="dialog" width="400">
|
||||
<v-card>
|
||||
<v-card-title> Add a New Theme </v-card-title>
|
||||
<v-card-title> {{$t('settings.add-a-new-theme')}} </v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
label="Theme Name"
|
||||
|
@ -13,9 +13,9 @@
|
|||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="dialog = false"> Cancel </v-btn>
|
||||
<v-btn color="grey" text @click="dialog = false"> {{$t('general.cancel')}} </v-btn>
|
||||
<v-btn color="success" text @click="Select" :disabled="!themeName">
|
||||
Create
|
||||
{{$t('general.create')}}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline"> Theme Settings </v-card-title>
|
||||
<v-card-title class="headline">
|
||||
{{ $t("settings.theme.theme-settings") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<h2 class="mt-4 mb-1">Dark Mode</h2>
|
||||
<h2 class="mt-4 mb-1">{{ $t("settings.theme.dark-mode") }}</h2>
|
||||
<p>
|
||||
Choose how Mealie looks to you. Set your theme preference to follow your
|
||||
system settings, or choose to use the light or dark theme.
|
||||
{{
|
||||
$t(
|
||||
"settings.theme.choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12">
|
||||
|
@ -18,33 +23,35 @@
|
|||
>
|
||||
<v-btn value="system"> Default to system </v-btn>
|
||||
|
||||
<v-btn value="light"> Light </v-btn>
|
||||
<v-btn value="light"> {{ $t("settings.theme.light") }} </v-btn>
|
||||
|
||||
<v-btn value="dark"> Dark </v-btn>
|
||||
<v-btn value="dark"> {{ $t("settings.theme.dark") }} </v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row></v-card-text
|
||||
>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<h2 class="mt-1 mb-1">Theme</h2>
|
||||
<h2 class="mt-1 mb-1">{{ $t("settings.theme.theme") }}</h2>
|
||||
<p>
|
||||
Select a theme from the dropdown or create a new theme. Note that the
|
||||
default theme will be served to all users who have not set a theme
|
||||
preference.
|
||||
{{
|
||||
$t(
|
||||
"settings.theme.select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<v-form ref="form" lazy-validation>
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="4" sm="3">
|
||||
<v-select
|
||||
label="Saved Color Theme"
|
||||
:label="$t('settings.theme.saved-color-theme')"
|
||||
:items="availableThemes"
|
||||
item-text="name"
|
||||
return-object
|
||||
v-model="selectedTheme"
|
||||
@change="themeSelected"
|
||||
:rules="[(v) => !!v || 'Theme is required']"
|
||||
:rules="[(v) => !!v || $t('settings.theme.theme-is-required')]"
|
||||
required
|
||||
>
|
||||
</v-select>
|
||||
|
@ -57,8 +64,10 @@
|
|||
Delete
|
||||
</v-btn>
|
||||
<Confirmation
|
||||
title="Delete Theme"
|
||||
message="Are you sure you want to delete this theme?"
|
||||
:title="$t('settings.theme.delete-theme')"
|
||||
:message="
|
||||
$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')
|
||||
"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
|
@ -70,43 +79,43 @@
|
|||
<v-row dense align-content="center" v-if="selectedTheme.colors">
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Primary"
|
||||
:button-text="$t('settings.theme.primary')"
|
||||
v-model="selectedTheme.colors.primary"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Secondary"
|
||||
:button-text="$t('settings.theme.secondary')"
|
||||
v-model="selectedTheme.colors.secondary"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Accent"
|
||||
:button-text="$t('settings.theme.accent')"
|
||||
v-model="selectedTheme.colors.accent"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Success"
|
||||
:button-text="$t('settings.theme.success')"
|
||||
v-model="selectedTheme.colors.success"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Info"
|
||||
:button-text="$t('settings.theme.info')"
|
||||
v-model="selectedTheme.colors.info"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Warning"
|
||||
:button-text="$t('settings.theme.warning')"
|
||||
v-model="selectedTheme.colors.warning"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
button-text="Error"
|
||||
:button-text="$t('settings.theme.error')"
|
||||
v-model="selectedTheme.colors.error"
|
||||
/>
|
||||
</v-col>
|
||||
|
@ -119,7 +128,7 @@
|
|||
<v-col></v-col>
|
||||
<v-col align="end">
|
||||
<v-btn text color="success" @click="saveThemes">
|
||||
Save Colors and Apply Theme
|
||||
{{ $t("settings.theme.save-colors-and-apply-theme") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
label="Set New Time"
|
||||
:label="$t('settings.set-new-time')"
|
||||
prepend-icon="mdi-clock-time-four-outline"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
|
@ -18,8 +18,8 @@
|
|||
</template>
|
||||
<v-time-picker v-if="modal2" v-model="time" full-width>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="modal2 = false"> Cancel </v-btn>
|
||||
<v-btn text color="primary" @click="saveTime"> OK </v-btn>
|
||||
<v-btn text color="primary" @click="modal2 = false"> {{$t('general.cancel')}} </v-btn>
|
||||
<v-btn text color="primary" @click="saveTime"> {{$t('general.ok')}} </v-btn>
|
||||
</v-time-picker>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Meal Planner Webhooks
|
||||
{{$t('settings.webhooks.meal-planner-webhooks')}}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p>
|
||||
The URLs listed below will recieve webhooks containing the recipe data
|
||||
for the meal plan on it's scheduled day. Currently Webhooks will execute
|
||||
at <strong>{{ time }}</strong>
|
||||
</p>
|
||||
<p v-html="$t('settings.webhooks.the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at', {time: time})"></p>
|
||||
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="2" sm="5">
|
||||
<v-switch
|
||||
v-model="enabled"
|
||||
inset
|
||||
label="Enabled"
|
||||
:label="$t('general.enabled')"
|
||||
class="my-n3"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
|
@ -23,7 +19,7 @@
|
|||
<TimePickerDialog @save-time="saveTime" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="testWebhooks"> Test Webhooks </v-btn>
|
||||
<v-btn text color="info" @click="testWebhooks"> {{$t('settings.webhooks.test-webhooks')}} </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
@ -36,7 +32,7 @@
|
|||
<v-col>
|
||||
<v-text-field
|
||||
v-model="webhooks[index]"
|
||||
label="Webhook URL"
|
||||
:label="$t('settings.webhooks.webhook-url')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -51,7 +47,7 @@
|
|||
<v-col> </v-col>
|
||||
<v-col align="end">
|
||||
<v-btn text color="success" @click="saveWebhooks">
|
||||
Save Webhooks
|
||||
{{$t('settings.webhooks.save-webhooks')}}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
|
@ -2,16 +2,20 @@
|
|||
<div class="text-center">
|
||||
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
||||
<v-card :loading="processing">
|
||||
<v-card-title class="headline"> From URL </v-card-title>
|
||||
<v-card-title class="headline"
|
||||
>{{ $t("new-recipe.from-url") }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field v-model="recipeURL" label="Recipe URL"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="recipeURL"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
|
||||
<v-alert v-if="error" color="red" outlined type="success">
|
||||
Looks like there was an error parsing the URL. Check the log and
|
||||
debug/last_recipe.json to see what went wrong.
|
||||
{{ $t("new-recipe.error-message") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
|
@ -19,8 +23,12 @@
|
|||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="reset"> Close </v-btn>
|
||||
<v-btn color="success" text @click="createRecipe"> Submit </v-btn>
|
||||
<v-btn color="grey" text @click="reset">
|
||||
{{ $t("general.close") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text @click="createRecipe">
|
||||
{{ $t("general.submit") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
@ -66,6 +74,7 @@ export default {
|
|||
|
||||
this.addRecipe = false;
|
||||
this.processing = false;
|
||||
this.recipeURL = "";
|
||||
this.$router.push(`/recipe/${response.data}`);
|
||||
},
|
||||
|
||||
|
|
|
@ -13,20 +13,20 @@
|
|||
v-model="user.name"
|
||||
light="light"
|
||||
prepend-icon="person"
|
||||
label="Name"
|
||||
:label="$t('general.name')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
label="Email"
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
prepend-icon="mdi-lock"
|
||||
label="Password"
|
||||
:label="$t('login.password')"
|
||||
type="password"
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
|
@ -34,7 +34,7 @@
|
|||
v-if="options.isLoggingIn"
|
||||
v-model="options.shouldStayLoggedIn"
|
||||
light="light"
|
||||
label="Stay logged in?"
|
||||
:label="$t('login.stay-logged-in')"
|
||||
hide-details="hide-details"
|
||||
></v-checkbox>
|
||||
<v-btn
|
||||
|
@ -44,14 +44,14 @@
|
|||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
>Sign in</v-btn
|
||||
>{{$t('login.sign-in')}}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else
|
||||
block="block"
|
||||
type="submit"
|
||||
@click.prevent="options.isLoggingIn = true"
|
||||
>Sign up</v-btn
|
||||
>{{$t('login.sign-up')}}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
|
|
@ -32,26 +32,32 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
items: [
|
||||
{
|
||||
icon: "mdi-calendar-week",
|
||||
title: "Dinner This Week",
|
||||
nav: "/meal-plan/this-week",
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-today",
|
||||
title: "Dinner Today",
|
||||
nav: "/meal-plan/today",
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-multiselect",
|
||||
title: "Planner",
|
||||
nav: "/meal-plan/planner",
|
||||
},
|
||||
{ icon: "mdi-cog", title: "Settings", nav: "/settings/site" },
|
||||
],
|
||||
}),
|
||||
data: function () {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
icon: "mdi-calendar-week",
|
||||
title: this.$i18n.t("meal-plan.dinner-this-week"),
|
||||
nav: "/meal-plan/this-week",
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-today",
|
||||
title: this.$i18n.t("meal-plan.dinner-today"),
|
||||
nav: "/meal-plan/today",
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-multiselect",
|
||||
title: this.$i18n.t("meal-plan.planner"),
|
||||
nav: "/meal-plan/planner",
|
||||
},
|
||||
{
|
||||
icon: "mdi-cog",
|
||||
title: this.$i18n.t("general.settings"),
|
||||
nav: "/settings/site",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
navRouter(route) {
|
||||
this.$router.push(route);
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<v-tooltip top color="secondary" max-width="400" open-delay="50">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn color="secondary" v-on="on" v-bind="attrs" text
|
||||
>Description</v-btn
|
||||
>{{$t('recipe.description')}}</v-btn
|
||||
>
|
||||
</template>
|
||||
<span>{{ description }}</span>
|
||||
|
|
|
@ -9,15 +9,13 @@
|
|||
hide-details
|
||||
hide-selected
|
||||
item-text="slug"
|
||||
label="Search for a Recipe"
|
||||
:label="$t('search.search-for-a-recipe')"
|
||||
single-line
|
||||
@keyup.enter.native="moreInfo(selected)"
|
||||
>
|
||||
<template v-slot:no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
Search for your Favorite
|
||||
<strong>Recipe</strong>
|
||||
<v-list-item-title :v-html="$t('search.search-for-your-favorite-recipe')">
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
|
108
frontend/src/components/UI/SearchBar.vue
Normal file
108
frontend/src/components/UI/SearchBar.vue
Normal file
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-autocomplete
|
||||
:items="autoResults"
|
||||
item-value="item.slug"
|
||||
item-text="item.name"
|
||||
dense
|
||||
light
|
||||
label="Search Mealie"
|
||||
:search-input.sync="search"
|
||||
hide-no-data
|
||||
cache-items
|
||||
solo
|
||||
>
|
||||
<template
|
||||
v-if="showResults"
|
||||
v-slot:item="{ item }"
|
||||
style="max-width: 750px"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.image)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content @click="selected(item.item.slug)">
|
||||
<v-list-item-title>
|
||||
{{ item.item.name }}
|
||||
<v-rating
|
||||
dense
|
||||
v-if="item.item.rating"
|
||||
:value="item.item.rating"
|
||||
size="12"
|
||||
>
|
||||
</v-rating>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.item.description }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import utils from "../../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showResults: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: "",
|
||||
result: [],
|
||||
autoResults: [],
|
||||
isDark: false,
|
||||
options: {
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: ["name", "slug"],
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.isDark = this.$store.getters.getIsDark;
|
||||
},
|
||||
computed: {
|
||||
data() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
fuse() {
|
||||
return new Fuse(this.data, this.options);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search() {
|
||||
if (this.search.trim() === "") this.result = this.list;
|
||||
else this.result = this.fuse.search(this.search.trim());
|
||||
console.log("test");
|
||||
|
||||
this.$emit("results", this.result);
|
||||
if (this.showResults === true) {
|
||||
this.autoResults = this.result;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
selected(slug) {
|
||||
this.$emit("selected", slug);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.color-transition {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn color="white" text v-bind="attrs" @click="close(false)">
|
||||
Close
|
||||
{{$t('general.close')}}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
|
23
frontend/src/i18n.js
Normal file
23
frontend/src/i18n.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
function loadLocaleMessages () {
|
||||
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const messages = {}
|
||||
locales.keys().forEach(key => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1]
|
||||
messages[locale] = locales(key)
|
||||
}
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
export default new VueI18n({
|
||||
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
|
||||
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
|
||||
messages: loadLocaleMessages()
|
||||
})
|
134
frontend/src/locales/da.json
Normal file
134
frontend/src/locales/da.json
Normal file
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"404": {
|
||||
"page-not-found": "404 side blev ikke fundet",
|
||||
"take-me-home": "Tag mig hjem"
|
||||
},
|
||||
"new-recipe": {
|
||||
"from-url": "Fra URL",
|
||||
"recipe-url": "URL på opskrift",
|
||||
"error-message": "Der opstod en fejl under indlæsning af opskriften. Tjek loggen og debug/last_recipe.json for at fejlsøge problemet.",
|
||||
"bulk-add": "Bulk Tilføj",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Indsæt dine opskriftsdata. \nHver linje behandles som et element på en liste"
|
||||
},
|
||||
"general": {
|
||||
"submit": "Indsend",
|
||||
"name": "Navn",
|
||||
"settings": "Indstillinger",
|
||||
"cancel": "Annuller",
|
||||
"close": "Luk",
|
||||
"create": "Opret",
|
||||
"delete": "Slet",
|
||||
"edit": "Rediger",
|
||||
"enabled": "Aktiveret",
|
||||
"image-file": "Billedfil",
|
||||
"new": "Ny",
|
||||
"ok": "Ok",
|
||||
"random": "Tilfældig",
|
||||
"save": "Gem",
|
||||
"select": "Vælg",
|
||||
"update": "Opdater",
|
||||
"delete-data": "Slet data",
|
||||
"download": "Hent",
|
||||
"import": "Importere"
|
||||
},
|
||||
"login": {
|
||||
"email": "E-mail",
|
||||
"password": "Adgangskode",
|
||||
"sign-in": "Log ind",
|
||||
"sign-up": "Opret bruger",
|
||||
"stay-logged-in": "Forbliv logget ind"
|
||||
},
|
||||
"meal-plan": {
|
||||
"dinner-this-week": "Madplan denne uge",
|
||||
"dinner-today": "Madplan i dag",
|
||||
"planner": "Planlægger",
|
||||
"choose-a-recipe": "Vælg en opskrift",
|
||||
"create-a-new-meal-plan": "Opret en ny måltidsplan",
|
||||
"edit-meal-plan": "Rediger måltidsplan",
|
||||
"end-date": "Slutdato",
|
||||
"meal-plans": "Måltidsplaner",
|
||||
"start-date": "Start dato"
|
||||
},
|
||||
"recipe": {
|
||||
"description": "Beskrivelse",
|
||||
"categories": "Kategorier",
|
||||
"ingredient": "Ingrediens",
|
||||
"ingredients": "Ingredienser",
|
||||
"instructions": "Instruktioner",
|
||||
"note": "Bemærk",
|
||||
"notes": "Bemærkninger",
|
||||
"original-recipe": "Oprindelig opskrift",
|
||||
"recipe-name": "Opskriftens navn",
|
||||
"servings": "Portioner",
|
||||
"step-index": "Trin: {step}",
|
||||
"tags": "Mærker",
|
||||
"view-recipe": "Se opskrift"
|
||||
},
|
||||
"search": {
|
||||
"search-for-a-recipe": "Søg efter en opskrift",
|
||||
"search-for-your-favorite-recipe": "Søg efter din foretrukne <strong>opskrift</strong>"
|
||||
},
|
||||
"migration": {
|
||||
"chowdown-repo-url": "Chowdown Repo URL",
|
||||
"currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration": "I øjeblikket er Chowdown via offentlig Repo URL den eneste understøttede migreringstype",
|
||||
"failed-images": "Mislykkede billeder",
|
||||
"failed-recipes": "Mislykkede opskrifter",
|
||||
"migrate": "Migrere",
|
||||
"recipe-migration": "Migrering af opskrifter",
|
||||
"delete-confirmation": "Er du sikker på, at du vil slette disse migrationsdata?",
|
||||
"failed-imports": "Mislykket import",
|
||||
"nextcloud-data": "Nextcloud data",
|
||||
"successfully-imported-from-nextcloud": "Importeret fra Nextcloud",
|
||||
"upload-an-archive": "Upload et arkiv",
|
||||
"you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected": "Du kan importere opskrifter fra enten en zip-fil eller et bibliotek i /app/data/migraiton/ folderen. \nGennemse dokumentationen for at sikre, at din bibliotekstruktur svarer til det, der forventes"
|
||||
},
|
||||
"settings": {
|
||||
"add-a-new-theme": "Tilføj et nyt tema",
|
||||
"backup-and-exports": "Backup og eksport",
|
||||
"backup-info": "Sikkerhedskopier eksporteres i standard JSON-format sammen med alle de billeder, der er gemt på filsystemet. \nI din sikkerhedskopimappe finder du en .zip-fil, der indeholder alle opskrifterne JSON og billeder fra databasen. \nDerudover, hvis du valgte en markdown-fil, gemmes disse også i .zip-filen. \nFor at importere en sikkerhedskopi skal den være placeret i din sikkerhedskopimappe. \nAutomatiske sikkerhedskopier udføres hver dag kl. 3:00.",
|
||||
"backup-recipes": "Sikkerhedskopier opksrifter",
|
||||
"backup-tag": "Sikkerhedskopier tags",
|
||||
"color": "Farve",
|
||||
"contribute": "Bidrag",
|
||||
"explore-the-docs": "Udforsk dokumentation",
|
||||
"markdown-template": "Markdown skabelon",
|
||||
"new-version-available": "En ny version af Mealie er tilgængelig. <a {aContents}> Besøg repoen </a>",
|
||||
"set-new-time": "Indstil ny tid",
|
||||
"swatches": "Prøver",
|
||||
"current": "Version:",
|
||||
"latest": "Seneste:",
|
||||
"theme": {
|
||||
"accent": "Accent",
|
||||
"dark-mode": "Mørk tilstand",
|
||||
"error": "Fejl",
|
||||
"info": "Info",
|
||||
"primary": "Primær",
|
||||
"secondary": "Sekundær",
|
||||
"select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference": "Vælg et tema i rullemenuen, eller opret et nyt tema. \nBemærk, at standardtemaet serveres til alle brugere, der ikke har angivet en temapræference.",
|
||||
"success": "Succes",
|
||||
"theme-is-required": "Tema er påkrævet",
|
||||
"theme-settings": "Temaindstillinger",
|
||||
"warning": "Advarsel",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "Er du sikker på, at du vil slette dette tema?",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Vælg, hvordan Mealie ser ud for dig. \nIndstil dit tema til at følge dine systemindstillinger, eller vælg at bruge det lyse eller mørke tema.",
|
||||
"dark": "Mørkt",
|
||||
"delete-theme": "Slet tema",
|
||||
"light": "Lyst",
|
||||
"save-colors-and-apply-theme": "Gem farver og anvend tema",
|
||||
"saved-color-theme": "Gemt farvetema",
|
||||
"theme": "Tema"
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "Måltidsplanlægning Webhooks",
|
||||
"save-webhooks": "Gem Webhooks",
|
||||
"test-webhooks": "Test Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Webadresserne, der er anført nedenfor, modtager webhooks, der indeholder opskriftsdataene for måltidsplanen på den planlagte dag. \nWebhooks udføres i øjeblikket på <strong> {time} </strong>",
|
||||
"webhook-url": "Webhook adresse"
|
||||
},
|
||||
"backup": {
|
||||
"import-recipes": "Importer opskrifter",
|
||||
"import-settings": "Importindstillinger",
|
||||
"import-themes": "Importer temaer"
|
||||
}
|
||||
}
|
||||
}
|
134
frontend/src/locales/en.json
Normal file
134
frontend/src/locales/en.json
Normal file
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"404": {
|
||||
"page-not-found": "404 Page Not Found",
|
||||
"take-me-home": "Take me Home"
|
||||
},
|
||||
"new-recipe": {
|
||||
"from-url": "From URL",
|
||||
"recipe-url": "Recipe URL",
|
||||
"error-message": "Looks like there was an error parsing the URL. Check the log and debug/last_recipe.json to see what went wrong.",
|
||||
"bulk-add": "Bulk Add",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list"
|
||||
},
|
||||
"general": {
|
||||
"submit": "Submit",
|
||||
"name": "Name",
|
||||
"settings": "Settings",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"image-file": "Image File",
|
||||
"update": "Update",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"select": "Select",
|
||||
"random": "Random",
|
||||
"new": "New",
|
||||
"create": "Create",
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK",
|
||||
"enabled": "Enabled",
|
||||
"download": "Download",
|
||||
"import": "Import",
|
||||
"delete-data": "Delete Data"
|
||||
},
|
||||
"login": {
|
||||
"stay-logged-in": "Stay logged in?",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"sign-in": "Sign in",
|
||||
"sign-up": "Sign up"
|
||||
},
|
||||
"meal-plan": {
|
||||
"dinner-this-week": "Dinner This Week",
|
||||
"dinner-today": "Dinner Today",
|
||||
"planner": "Planner",
|
||||
"edit-meal-plan": "Edit Meal Plan",
|
||||
"meal-plans": "Meal Plans",
|
||||
"choose-a-recipe": "Choose a Recipe",
|
||||
"create-a-new-meal-plan": "Create a New Meal Plan",
|
||||
"start-date": "Start Date",
|
||||
"end-date": "End Date"
|
||||
},
|
||||
"recipe": {
|
||||
"description": "Description",
|
||||
"ingredients": "Ingredients",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"instructions": "Instructions",
|
||||
"step-index": "Step: {step}",
|
||||
"recipe-name": "Recipe Name",
|
||||
"servings": "Servings",
|
||||
"ingredient": "Ingredient",
|
||||
"notes": "Notes",
|
||||
"note": "Note",
|
||||
"original-recipe": "Original Recipe",
|
||||
"view-recipe": "View Recipe"
|
||||
},
|
||||
"search": {
|
||||
"search-for-a-recipe": "Search for a Recipe",
|
||||
"search-for-your-favorite-recipe": "Search for your Favorite <strong>Recipe</strong>"
|
||||
},
|
||||
"settings": {
|
||||
"color": "Color",
|
||||
"swatches": "Swatches",
|
||||
"add-a-new-theme": "Add a New Theme",
|
||||
"set-new-time": "Set New Time",
|
||||
"current": "Version:",
|
||||
"latest": "Latest",
|
||||
"explore-the-docs": "Explore the Docs",
|
||||
"contribute": "Contribute",
|
||||
"backup-and-exports": "Backup and Exports",
|
||||
"backup-info": "Backups are exported in standard JSON format along with all the images stored on the file system. In your backup folder you'll find a .zip file that contains all of the recipe JSON and images from the database. Additionally, if you selected a markdown file, those will also be stored in the .zip file. To import a backup, it must be located in your backups folder. Automated backups are done each day at 3:00 AM.",
|
||||
"backup-tag": "Backup Tag",
|
||||
"markdown-template": "Markdown Template",
|
||||
"backup-recipes": "Backup Recipes",
|
||||
"theme": {
|
||||
"theme-settings": "Theme Settings",
|
||||
"select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference": "Select a theme from the dropdown or create a new theme. Note that the default theme will be served to all users who have not set a theme preference.",
|
||||
"dark-mode": "Dark Mode",
|
||||
"theme-is-required": "Theme is required",
|
||||
"primary": "Primary",
|
||||
"secondary": "Secondary",
|
||||
"accent": "Accent",
|
||||
"success": "Success",
|
||||
"info": "Info",
|
||||
"warning": "Warning",
|
||||
"error": "Error",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"theme": "Theme",
|
||||
"saved-color-theme": "Saved Color Theme",
|
||||
"delete-theme": "Delete Theme",
|
||||
"are-you-sure-you-want-to-delete-this-theme": "Are you sure you want to delete this theme?",
|
||||
"save-colors-and-apply-theme": "Save Colors and Apply Theme",
|
||||
"choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme": "Choose how Mealie looks to you. Set your theme preference to follow your system settings, or choose to use the light or dark theme."
|
||||
},
|
||||
"webhooks": {
|
||||
"meal-planner-webhooks": "Meal Planner Webhooks",
|
||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will recieve webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at <strong>{ time }</strong>",
|
||||
"test-webhooks": "Test Webhooks",
|
||||
"webhook-url": "Webhook URL",
|
||||
"save-webhooks": "Save Webhooks"
|
||||
},
|
||||
"new-version-available": "A New Version of Mealie is Avaiable, <a {aContents}> Visit the Repo </a>",
|
||||
"backup": {
|
||||
"import-recipes": "Import Recipes",
|
||||
"import-themes": "Import Themes",
|
||||
"import-settings": "Import Settings"
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"recipe-migration": "Recipe Migration",
|
||||
"currently-chowdown-via-public-repo-url-is-the-only-supported-type-of-migration": "Currently Chowdown via public Repo URL is the only supported type of migration",
|
||||
"chowdown-repo-url": "Chowdown Repo URL",
|
||||
"migrate": "Migrate",
|
||||
"failed-recipes": "Failed Recipes",
|
||||
"failed-images": "Failed Images",
|
||||
"you-can-import-recipes-from-either-a-zip-file-or-a-directory-located-in-the-app-data-migraiton-folder-please-review-the-documentation-to-ensure-your-directory-structure-matches-what-is-expected": "You can import recipes from either a zip file or a directory located in the /app/data/migraiton/ folder. Please review the documentation to ensure your directory structure matches what is expected",
|
||||
"nextcloud-data": "Nextcloud Data",
|
||||
"delete-confirmation": "Are you sure you want to delete this migration data?",
|
||||
"successfully-imported-from-nextcloud": "Successfully Imported from Nextcloud",
|
||||
"failed-imports": "Failed Imports",
|
||||
"upload-an-archive": "Upload an Archive"
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import vuetify from "./plugins/vuetify";
|
|||
import store from "./store/store";
|
||||
import VueRouter from "vue-router";
|
||||
import { routes } from "./routes";
|
||||
import i18n from './i18n'
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(VueRouter);
|
||||
|
@ -17,11 +18,12 @@ new Vue({
|
|||
vuetify,
|
||||
store,
|
||||
router,
|
||||
render: (h) => h(App),
|
||||
i18n,
|
||||
render: (h) => h(App)
|
||||
}).$mount("#app");
|
||||
|
||||
// Truncate
|
||||
let filter = function (text, length, clamp) {
|
||||
let filter = function(text, length, clamp) {
|
||||
clamp = clamp || "...";
|
||||
let node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
|
@ -32,5 +34,3 @@ let filter = function (text, length, clamp) {
|
|||
Vue.filter("truncate", filter);
|
||||
|
||||
export { router };
|
||||
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<v-col>
|
||||
<v-card height="">
|
||||
<v-card-text>
|
||||
<h1>404 No Page Found</h1>
|
||||
<h1>{{$t('404.page-not-found')}}</h1>
|
||||
</v-card-text>
|
||||
<v-btn text block @click="$router.push('/')"> Take me Home </v-btn>
|
||||
<v-btn text block @click="$router.push('/')"> {{$t('404.take-me-home')}} </v-btn>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="2"></v-col>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<NewMeal v-else @created="requestMeals" />
|
||||
|
||||
<v-card class="my-1">
|
||||
<v-card-title class="headline"> Meal Plans </v-card-title>
|
||||
<v-card-title class="headline"> {{$t('meal-plan.meal-plans')}} </v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-timeline align-top :dense="$vuetify.breakpoint.smAndDown">
|
||||
|
@ -50,7 +50,7 @@
|
|||
text
|
||||
@click="editPlan(mealplan.uid)"
|
||||
>
|
||||
Edit
|
||||
{{$t('general.edit')}}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error lighten-2"
|
||||
|
@ -58,7 +58,7 @@
|
|||
text
|
||||
@click="deletePlan(mealplan.uid)"
|
||||
>
|
||||
Delete
|
||||
{{$t('general.delete')}}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
|
|
@ -22,14 +22,16 @@
|
|||
|
||||
<v-card-text> {{ meal.description }} </v-card-text>
|
||||
|
||||
<v-btn
|
||||
align="center"
|
||||
color="secondary"
|
||||
text
|
||||
@click="$router.push(`/recipe/${meal.slug}`)"
|
||||
>
|
||||
View Recipe
|
||||
</v-btn>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
align="center"
|
||||
color="secondary"
|
||||
text
|
||||
@click="$router.push(`/recipe/${meal.slug}`)"
|
||||
>
|
||||
{{$t('recipe.view-recipe')}}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12">
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
class="d-print-none"
|
||||
:key="imageKey"
|
||||
>
|
||||
<RecipeTimeCard
|
||||
class="force-bottom"
|
||||
:prepTime="recipeDetails.prepTime"
|
||||
:totalTime="recipeDetails.totalTime"
|
||||
:performTime="recipeDetails.performTime"
|
||||
/>
|
||||
</v-img>
|
||||
<ButtonRow
|
||||
:open="showIcons"
|
||||
|
@ -49,6 +55,7 @@ import utils from "../utils";
|
|||
import VJsoneditor from "v-jsoneditor";
|
||||
import RecipeViewer from "../components/Recipe/RecipeViewer";
|
||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||
import RecipeTimeCard from "../components/Recipe/RecipeTimeCard.vue";
|
||||
import ButtonRow from "../components/UI/ButtonRow";
|
||||
|
||||
export default {
|
||||
|
@ -57,10 +64,11 @@ export default {
|
|||
RecipeViewer,
|
||||
RecipeEditor,
|
||||
ButtonRow,
|
||||
RecipeTimeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// CurrentRecipe: this.$route.params.recipe,
|
||||
// currentRecipe: this.$route.params.recipe,
|
||||
form: false,
|
||||
jsonEditor: false,
|
||||
jsonEditorOptions: {
|
||||
|
@ -99,7 +107,7 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
CurrentRecipe() {
|
||||
currentRecipe() {
|
||||
return this.$route.params.recipe;
|
||||
},
|
||||
showIcons() {
|
||||
|
@ -118,7 +126,7 @@ export default {
|
|||
this.fileObject = fileObject;
|
||||
},
|
||||
async getRecipeDetails() {
|
||||
this.recipeDetails = await api.recipes.requestDetails(this.CurrentRecipe);
|
||||
this.recipeDetails = await api.recipes.requestDetails(this.currentRecipe);
|
||||
this.form = false;
|
||||
},
|
||||
getImage(image) {
|
||||
|
@ -130,7 +138,7 @@ export default {
|
|||
api.recipes.delete(this.recipeDetails.slug);
|
||||
},
|
||||
async saveRecipe() {
|
||||
await api.recipes.update(this.recipeDetails);
|
||||
let slug = await api.recipes.update(this.recipeDetails);
|
||||
|
||||
if (this.fileObject) {
|
||||
await api.recipes.updateImage(this.recipeDetails.slug, this.fileObject);
|
||||
|
@ -138,6 +146,7 @@ export default {
|
|||
|
||||
this.form = false;
|
||||
this.imageKey += 1;
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
showForm() {
|
||||
this.form = true;
|
||||
|
@ -154,4 +163,10 @@ export default {
|
|||
.disabled-card {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.force-bottom {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
58
frontend/src/pages/SearchPage.vue
Normal file
58
frontend/src/pages/SearchPage.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-row justify="center">
|
||||
<v-col cols="1"> </v-col>
|
||||
<v-col>
|
||||
<SearchBar @results="updateResults" :show-results="false" />
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn icon>
|
||||
<v-icon large> mdi-filter </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="searchResults">
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="item in searchResults.slice(0, 10)"
|
||||
:key="item.item.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:name="item.item.name"
|
||||
:description="item.item.description"
|
||||
:slug="item.item.slug"
|
||||
:rating="item.item.rating"
|
||||
:image="item.item.image"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchBar from "../components/UI/SearchBar";
|
||||
import RecipeCard from "../components/UI/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
RecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateResults(results) {
|
||||
this.searchResults = results;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,30 +1,37 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-alert v-if="newVersion" color="green" type="success" outlined>
|
||||
A New Version of Mealie is Avaiable,
|
||||
<a
|
||||
href="https://github.com/hay-kot/mealie/releases/latest"
|
||||
target="_blank"
|
||||
class="green--text"
|
||||
>
|
||||
Visit the Repo
|
||||
</a>
|
||||
<v-alert
|
||||
v-if="newVersion"
|
||||
color="green"
|
||||
type="success"
|
||||
outlined
|
||||
v-html="
|
||||
$t('settings.new-version-available', {
|
||||
aContents:
|
||||
'target=\'_blank\' href=\'https://github.com/hay-kot/mealie\' class=\'green--text\'',
|
||||
})
|
||||
"
|
||||
>
|
||||
</v-alert>
|
||||
<Theme />
|
||||
<Backup class="mt-2" />
|
||||
<Webhooks class="mt-2" />
|
||||
<Migration class="mt-2" />
|
||||
<p class="text-center my-2">
|
||||
Version: {{ version }} | Latest: {{ latestVersion }} ·
|
||||
{{ $t("settings.current") }}
|
||||
{{ version }} |
|
||||
{{ $t("settings.latest") }}
|
||||
{{ latestVersion }}
|
||||
·
|
||||
<a href="https://hay-kot.github.io/mealie/" target="_blank">
|
||||
Explore the Docs
|
||||
{{ $t("settings.explore-the-docs") }}
|
||||
</a>
|
||||
·
|
||||
<a
|
||||
href="https://hay-kot.github.io/mealie/2.1%20-%20Contributions/"
|
||||
href="https://hay-kot.github.io/mealie/contributors/non-coders/"
|
||||
target="_blank"
|
||||
>
|
||||
Contribute
|
||||
{{ $t("settings.contribute") }}
|
||||
</a>
|
||||
</p>
|
||||
</v-container>
|
||||
|
@ -47,7 +54,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
latestVersion: null,
|
||||
version: "v0.0.2",
|
||||
version: "v0.1.0",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import HomePage from "./pages/HomePage";
|
||||
import Page404 from "./pages/404Page";
|
||||
import SearchPage from "./pages/SearchPage";
|
||||
import RecipePage from "./pages/RecipePage";
|
||||
import RecipeNewPage from "./pages/RecipeNewPage";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
|
@ -10,6 +11,7 @@ import api from "./api";
|
|||
export const routes = [
|
||||
{ path: "/", component: HomePage },
|
||||
{ path: "/mealie", component: HomePage },
|
||||
{ path: "/search", component: SearchPage },
|
||||
{ path: "/recipe/:recipe", component: RecipePage },
|
||||
{ path: "/new/", component: RecipeNewPage },
|
||||
{ path: "/settings/site", component: SettingsPage },
|
||||
|
|
|
@ -17,6 +17,7 @@ function inDarkMode(payload) {
|
|||
const state = {
|
||||
activeTheme: {},
|
||||
darkMode: "system",
|
||||
isDark: false,
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
@ -30,6 +31,7 @@ const mutations = {
|
|||
|
||||
if (isDark !== null) {
|
||||
Vuetify.framework.theme.dark = isDark;
|
||||
state.isDark = isDark;
|
||||
state.darkMode = payload;
|
||||
}
|
||||
},
|
||||
|
@ -60,6 +62,7 @@ const actions = {
|
|||
const getters = {
|
||||
getActiveTheme: (state) => state.activeTheme,
|
||||
getDarkMode: (state) => state.darkMode,
|
||||
getIsDark: (state) => state.isDark,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -10,4 +10,12 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
pluginOptions: {
|
||||
i18n: {
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
localeDir: 'locales',
|
||||
enableInSFC: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue