* fix links

* actually fix #238

* Feature/mkdocs version bump (#240)

* fix links (#239)

Co-authored-by: hay-kot <hay-kot@pm.me>

* fix #238

* bump mkdocs version

* light/dark toggle

* light/dark mode css

* API_DOCS defaults to True

* disable build on push for master

Co-authored-by: hay-kot <hay-kot@pm.me>

* Feature/recipe viewer (#244)

* fix dialog placement

* markdown support in ingredients

* fix line render issue

* fix tag rendering bug

* change ingredients to text area

* no slug error

* add tag pages

* remove console.logs

Co-authored-by: hay-kot <hay-kot@pm.me>

* changelog v0.4.1

* bug/backup-download (#245)

* fix blocked download

* + download blocked

Co-authored-by: hay-kot <hay-kot@pm.me>

* Feature/meal planner (#246)

* fixes duplicate recipes in meal-plan #221

* add quick week option

* scope css

* add mealplanner info

Co-authored-by: hay-kot <hay-kot@pm.me>

* Nextcloud Import Bugs - #248 (#250)

* parses datetime properly + clean category - #248

* add default credentials to docs

Co-authored-by: hay-kot <hay-kot@pm.me>

* Add bulk import examples to docs. (#252)

* Add bulk import examples to docs.

* Update api-usage.md

* Add Python example for bulk import.

* Change IP address in API example.

* Refactor/app settings (#251)

* fix env setup bugs

* remove unused import

* fix layout issues

* changelog

Co-authored-by: hay-kot <hay-kot@pm.me>

* env setup fixes

* Feature/about api (#253)

* fix settings

* app info cleanup

Co-authored-by: hay-kot <hay-kot@pm.me>

* Feature/image minify (#256)

* fix settings

* app info cleanup

* bottom-bar experiment

* remove dup key

* type hints

* add dependency

* updated image with query parameters

* read image options

* add image minification

* add image minification step

* alt image routes

* add image minification

* set mobile bar to top

Co-authored-by: hay-kot <hay-kot@pm.me>

* Feature/additional endpoints (#257)

* new recipe summary route

* add categories to cards

* add pillow

* show tags instead of categories

* additional debug info

* add todays meal image url

* about page

* fix reactive tag

* changelog + docs

* bump version

Co-authored-by: hay-kot <hay-kot@pm.me>

* add pillow dependencies (#258)

Co-authored-by: hay-kot <hay-kot@pm.me>

* Feature/search page (#259)

* add pillow dependencies

* advanced search page

* advanced search apge

* remove extra dependencies

* add pre-run script

Co-authored-by: hay-kot <hay-kot@pm.me>

* no image assignment

* advanced search

* fix docker dev build

* Do not force theme settings on login form (#260)

* Fix docker dev db persistence (#264)

* Fix docker dev db persistence

* Make run.sh the only startup script for prod + dev

Credits to @hay-kot for run.sh script logic

* Restore dev backend initialization in non-docker setup

* Make run.sh POSIX-friendly

* Allow dev backend to auto-reload in Docker

* Frontend Refactor + Bug Fixes

* merge category and tag selector

* unifiy category selector

* add hint

* spacing

* fix nextcloud migration

* simplify email validator #261

* formatting

* cleanup

* auto-gen

* format

* update run script

* unified category/tag selector

* rename component

* Add advanced search link

* remove old code

* convert keywords to tags

* add proper behavior on rename

* proper image name association on rename

* fix test cleanup

* changelog

* set docker comppand

* minify on migration

Co-authored-by: hay-kot <hay-kot@pm.me>

* bug-fixes/category-tag-creator (#266)

* fix category labels

* set loader for migration

* v0.4.1

Co-authored-by: hay-kot <hay-kot@pm.me>

* Hot Fix (#269)

* fix category labels

* set loader for migration

* v0.4.1

* reorganize API docs

Co-authored-by: hay-kot <hay-kot@pm.me>

* Fix some pytests (#265)

* Fix encoding issue in cleaner unit test

* Add VS Code task to run pytests

* Fix FileExistsError when running Windows

* Add Portuguese Translation (#232)

* Add Portuguese Translation

* add portuguese translation option

* formatting

* add missing }

* specify absolute path

* Feature/migration-rewrite (#278)

* start

* migration rewrite

* update name

* convert chowdown to new methods

* refactor/remove duplicate code

* refactor to unify logger + log to file

* remove toolbox

* Display report on UI

Co-authored-by: hay-kot <hay-kot@pm.me>

* refactor/image-minification (#285)

* refactor image minification calls

* remove nusiance logs

Co-authored-by: hay-kot <hay-kot@pm.me>

* feature/debug-info (#286)

* rename 'ENV' to 'PRODUCTION' and default to true

* set env PRODUCTION

* refactor file download process

* add last_recipe.json and log downloads

* changelog + version bump

* set env on workflows

* bump version

Co-authored-by: hay-kot <hay-kot@pm.me>

* Basic nutrition editor (#288)

* Basic nutrition editor

* fix no image on scrape

* nutrition display

* add recipe images

* update by url

* new upload options

Co-authored-by: hay-kot <hay-kot@pm.me>

* Sanitize recipe backup filenames (#287)

Fixes #275

* fix page creation fixes #290

* Display categories in sidebar if no pages set (#292)

Fixes  #291

* Enrich page title with context (#296)

- Static pages have their own titles
- The name of the recipe is displayed when viewing it

* fix: translates phrase for locale de (#298)

Co-authored-by: Jonas  Schubert <jonas.schubert.1990@web.de>

* Fix ingredient checkbox click (#305)

Fixes #304
v-list-item already flips the checkbox

* Localize custom pages and search page (#299)

* Localize custom pages and search page

* Fix FR translation for step

* fixes #306

* fixes #297

* Update changelog

* generate docs

Co-authored-by: hay-kot <hay-kot@pm.me>
Co-authored-by: Nat <nathanynath@yahoo.fr>
Co-authored-by: sephrat <34862846+sephrat@users.noreply.github.com>
Co-authored-by: Pedro Mata Rodrigues <pmmatarodrigues@gmail.com>
Co-authored-by: JonasSchubert <jonas.schubert.projects@web.de>
Co-authored-by: Jonas  Schubert <jonas.schubert.1990@web.de>
This commit is contained in:
Hayden
2021-04-17 12:57:47 -08:00
committed by GitHub
parent e11577f786
commit 0f5a564ff3
68 changed files with 3357 additions and 2521 deletions

View File

@@ -61,9 +61,16 @@ const apiReq = {
processResponse(response);
return response;
},
async download(url) {
const response = await this.get(url);
const token = response.data.fileToken;
const tokenURL = baseURL + "utils/download?token=" + token;
window.open(tokenURL, "_blank");
return response.data;
},
};
export { apiReq };
export { baseURL };

View File

@@ -4,7 +4,7 @@ import { store } from "@/store";
const backupBase = baseURL + "backups/";
const backupURLs = {
export const backupURLs = {
// Backup
available: `${backupBase}available`,
createBackup: `${backupBase}export/database`,
@@ -13,6 +13,8 @@ const backupURLs = {
downloadBackup: fileName => `${backupBase}${fileName}/download`,
};
export const backupAPI = {
/**
* Request all backups available on the server
@@ -43,19 +45,19 @@ export const backupAPI = {
/**
* Creates a backup on the serve given a set of options
* @param {object} data
* @returns
* @returns
*/
async create(options) {
let response = apiReq.post(backupURLs.createBackup, options);
return response;
},
/**
* Downloads a file from the server. I don't actually think this is used?
* @param {string} fileName
* Downloads a file from the server. I don't actually think this is used?
* @param {string} fileName
* @returns Download URL
*/
async download(fileName) {
let response = await apiReq.get(backupURLs.downloadBackup(fileName));
return response.data;
const url = backupURLs.downloadBackup(fileName);
apiReq.download(url);
},
};

View File

@@ -61,6 +61,11 @@ export const recipeAPI = {
return response;
},
async updateImagebyURL(slug, url) {
const response = apiReq.post(recipeURLs.updateImage(slug), { url: url });
return response;
},
async update(data) {
let response = await apiReq.put(recipeURLs.update(data.slug), data);
store.dispatch("requestRecentRecipes");

View File

@@ -37,14 +37,7 @@
<v-divider></v-divider>
<v-card-actions>
<v-btn
color="accent"
text
:loading="downloading"
@click="downloadFile(`/api/backups/${name}/download`)"
>
{{ $t("general.download") }}
</v-btn>
<TheDownloadBtn :download-url="downloadUrl" />
<v-spacer></v-spacer>
<v-btn color="error" text @click="raiseEvent('delete')">
{{ $t("general.delete") }}
@@ -66,9 +59,10 @@
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
import axios from "axios";
import TheDownloadBtn from "@/components/UI/TheDownloadBtn.vue";
import { backupURLs } from "@/api/backup";
export default {
components: { ImportOptions },
components: { ImportOptions, TheDownloadBtn },
props: {
name: {
default: "Backup Name",
@@ -92,6 +86,11 @@ export default {
downloading: false,
};
},
computed: {
downloadUrl() {
return backupURLs.downloadBackup(this.name);
},
},
methods: {
updateOptions(options) {
this.options = options;
@@ -116,23 +115,6 @@ export default {
this.close();
this.$emit(event, eventData);
},
async downloadFile(downloadURL) {
this.downloading = true;
const response = await axios({
url: downloadURL,
method: "GET",
responseType: "blob", // important
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `${this.name}.zip`);
document.body.appendChild(link);
link.click();
this.downloading = false;
},
},
};
</script>

View File

@@ -45,7 +45,7 @@
</template>
<script>
import DataTable from "./DataTable";
import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable";
export default {
components: {
DataTable,
@@ -145,4 +145,4 @@ export default {
</script>
<style>
</style>
</style>

View File

@@ -17,13 +17,13 @@
<v-text-field
autofocus
v-model="page.name"
label="Page Name"
:label="$t('settings.page-name')"
></v-text-field>
<CategoryTagSelector
v-model="page.categories"
ref="categoryFormSelector"
@mounted="catMounted = true"
tag-selector="false"
:tag-selector="false"
/>
</v-card-text>

View File

@@ -3,10 +3,10 @@
<CreatePageDialog ref="createDialog" @refresh-page="getPages" />
<v-card-text>
<h2 class="mt-1 mb-1 ">
Custom Pages
{{$t('settings.custom-pages')}}
<span>
<v-btn color="success" @click="newPage" small class="ml-3">
Create
{{$t('general.create')}}
</v-btn>
</span>
</h2>
@@ -41,11 +41,11 @@
<v-card-actions>
<v-btn text small color="error" @click="deletePage(item.id)">
Delete
{{$t('general.delete')}}
</v-btn>
<v-spacer> </v-spacer>
<v-btn small text color="success" @click="editPage(index)">
Edit
{{$t('general.edit')}}
</v-btn>
</v-card-actions>
</v-card>
@@ -55,7 +55,7 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" @click="savePages">
Save
{{$t('general.save')}}
</v-btn>
</v-card-actions>
</v-card>
@@ -76,8 +76,8 @@ export default {
customPages: [],
newPageData: {
create: true,
title: "New Page",
buttonText: "Create",
title: this.$t('settings.new-page'),
buttonText: this.$t('general.create'),
data: {
name: "",
categories: [],
@@ -86,8 +86,8 @@ export default {
},
editPageData: {
create: false,
title: "Edit Page",
buttonText: "Update",
title: this.$t('settings.edit-page'),
buttonText: this.$t('general.update'),
data: {},
},
};

View File

@@ -1,5 +1,6 @@
<template>
<v-card outlined class="my-2" :loading="loading">
<MigrationDialog ref="migrationDialog" />
<v-card-title>
{{ title }}
<v-spacer></v-spacer>
@@ -67,6 +68,7 @@
import UploadBtn from "../../UI/UploadBtn";
import utils from "@/utils";
import { api } from "@/api";
import MigrationDialog from "@/components/Admin/Migration/MigrationDialog.vue";
export default {
props: {
folder: String,
@@ -76,6 +78,7 @@ export default {
},
components: {
UploadBtn,
MigrationDialog,
},
data() {
return {
@@ -90,7 +93,8 @@ export default {
async importMigration(file_name) {
this.loading = true;
let response = await api.migrations.import(this.folder, file_name);
this.$emit("imported", response.successful, response.failed);
this.$refs.migrationDialog.open(response);
// this.$emit("imported", response.successful, response.failed);
this.loading = false;
},
readableTime(timestamp) {

View File

@@ -0,0 +1,109 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="70%">
<v-card>
<v-app-bar dark color="primary mb-2">
<v-icon large left>
mdi-import
</v-icon>
<v-toolbar-title class="headline">
Migration Summary
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text class="mb-n4">
<v-row>
<div v-for="values in allNumbers" :key="values.title">
<v-card-text>
<div>
<h3>{{ values.title }}</h3>
</div>
<div class="success--text">Success: {{ values.success }}</div>
<div class="error--text">Failed: {{ values.failure }}</div>
</v-card-text>
</div>
</v-row>
</v-card-text>
<v-tabs v-model="tab">
<v-tab>{{ $t("general.recipes") }}</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="(table, index) in allTables" :key="index">
<v-card flat>
<DataTable :data-headers="importHeaders" :data-set="table" />
</v-card>
</v-tab-item>
</v-tabs-items>
</v-card>
</v-dialog>
</div>
</template>
<script>
import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable";
export default {
components: {
DataTable,
},
data: () => ({
tab: null,
dialog: false,
recipeData: [],
themeData: [],
settingsData: [],
userData: [],
groupData: [],
pageData: [],
importHeaders: [
{
text: "Status",
value: "status",
},
{
text: "Name",
align: "start",
sortable: true,
value: "name",
},
{ text: "Exception", value: "data-table-expand", align: "center" },
],
allDataTables: [],
}),
computed: {
recipeNumbers() {
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
},
allNumbers() {
return [this.recipeNumbers];
},
allTables() {
return [this.recipeData];
},
},
methods: {
calculateNumbers(title, list_array) {
if (!list_array) return;
let numbers = { title: title, success: 0, failure: 0 };
list_array.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
},
open(importData) {
this.recipeData = importData;
this.dialog = true;
},
},
};
</script>
<style>
</style>

View File

@@ -90,7 +90,7 @@ export default {
computed: {
inputLabel() {
if (!this.showLabel) return null;
return this.tagSelector ? "Tags" : "Categories";
return this.tagSelector ? this.$t('recipe.tags') : this.$t('recipe.categories');
},
activeItems() {
let ItemObjects = [];

View File

@@ -0,0 +1,76 @@
<template>
<div class="text-center">
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
<template v-slot:activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on">
Image
</v-btn>
</template>
<v-card width="400">
<v-card-title class="headline flex mb-0">
<div>
Recipe Image
</div>
<UploadBtn
class="ml-auto"
url="none"
file-name="image"
:text-btn="false"
@uploaded="uploadImage"
/>
</v-card-title>
<v-card-text class="mt-n5">
<div>
<v-text-field label="URL" class="pt-5" clearable v-model="url">
<template v-slot:append-outer>
<v-btn
class="ml-2"
color="primary"
@click="getImageFromURL"
:loading="loading"
>
Get
</v-btn>
</template>
</v-text-field>
</div>
</v-card-text>
</v-card>
</v-menu>
</div>
</template>
<script>
const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload";
import UploadBtn from "@/components/UI/UploadBtn";
import { api } from "@/api";
// import axios from "axios";
export default {
components: {
UploadBtn,
},
props: {
slug: String,
},
data: () => ({
items: [{ title: "Upload Image" }, { title: "From URL" }],
url: "",
loading: false,
}),
methods: {
uploadImage(fileObject) {
this.$emit(UPLOAD_EVENT, fileObject);
},
async getImageFromURL() {
this.loading = true;
const response = await api.recipes.updateImagebyURL(this.slug, this.url);
if (response) this.$emit(REFRESH_EVENT);
this.loading = false;
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div v-if="valueNotNull || edit">
<h2 class="my-4">Nutrition</h2>
<div v-if="edit">
<div v-for="(item, key, index) in value" :key="index">
<v-text-field
dense
:value="value[key]"
:label="labels[key].label"
:suffix="labels[key].suffix"
type="number"
autocomplete="off"
@input="updateValue(key, $event)"
></v-text-field>
</div>
</div>
<div v-if="showViewer">
<v-list dense>
<v-list-item-group color="primary">
<v-list-item v-for="(item, key, index) in labels" :key="index">
<v-list-item-content>
<v-list-item-title class="pl-4 text-subtitle-1 flex row ">
<div>{{ item.label }}</div>
<div class="ml-auto mr-1">{{ value[key] }}</div>
<div>{{ item.suffix }}</div>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</div>
</div>
</template>
<script>
export default {
props: {
value: {},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
labels: {
calories: {
label: "Calories",
suffix: "calories",
},
fatContent: { label: "Fat Content", suffix: "grams" },
fiberContent: { label: "Fiber Content", suffix: "grams" },
proteinContent: { label: "Protein Content", suffix: "grams" },
sodiumContent: { label: "Sodium Content", suffix: "milligrams" },
sugarContent: { label: "Sugar Content", suffix: "grams" },
},
};
},
computed: {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
if (valueProperty && valueProperty !== "") return true;
}
return false;
},
},
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -2,16 +2,12 @@
<v-form ref="form">
<v-card-text>
<v-row dense>
<v-col cols="3"></v-col>
<v-col>
<v-file-input
v-model="fileObject"
:label="$t('general.image-file')"
truncate-length="30"
@change="uploadImage"
></v-file-input>
</v-col>
<v-col cols="3"></v-col>
<ImageUploadBtn
class="mt-2"
@upload="uploadImage"
:slug="value.slug"
@refresh="$emit('upload')"
/>
</v-row>
<v-row dense>
<v-col>
@@ -92,7 +88,7 @@
auto-grow
solo
dense
rows="2"
rows="1"
>
<v-icon
class="mr-n1"
@@ -165,6 +161,7 @@
<v-btn class="mt-1" color="secondary" fab dark small @click="addNote">
<v-icon>mdi-plus</v-icon>
</v-btn>
<NutritionEditor v-model="value.nutrition" :edit="true" />
<ExtrasEditor :extras="value.extras" @save="saveExtras" />
</v-col>
@@ -222,17 +219,20 @@
<script>
import draggable from "vuedraggable";
import { api } from "@/api";
import utils from "@/utils";
import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import NutritionEditor from "./NutritionEditor";
import ImageUploadBtn from "./ImageUploadBtn.vue";
export default {
components: {
BulkAdd,
ExtrasEditor,
draggable,
CategoryTagSelector,
NutritionEditor,
ImageUploadBtn,
},
props: {
value: Object,
@@ -251,12 +251,8 @@ export default {
};
},
methods: {
uploadImage() {
this.$emit("upload", this.fileObject);
},
async updateImage() {
const slug = this.value.slug;
api.recipes.updateImage(slug, this.fileObject);
uploadImage(fileObject) {
this.$emit("upload", fileObject);
},
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {

View File

@@ -12,6 +12,7 @@
v-model="ingredient.checked"
class="pt-0 my-auto py-auto"
color="secondary"
:readonly="true"
>
</v-checkbox>

View File

@@ -40,6 +40,7 @@
:isCategory="false"
/>
<Notes :notes="notes" />
<NutritionEditor :value="nutrition" :edit="false" />
</div>
</v-col>
<v-divider
@@ -56,6 +57,7 @@
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
<Notes :notes="notes" />
<NutritionEditor :value="nutrition" :edit="false" />
</div>
<v-row class="mt-2 mb-1">
<v-col></v-col>
@@ -80,6 +82,7 @@
</template>
<script>
import NutritionEditor from "@/components/Recipe/RecipeEditor/NutritionEditor";
import VueMarkdown from "@adapttive/vue-markdown";
import utils from "@/utils";
import RecipeChips from "./RecipeChips";
@@ -93,6 +96,7 @@ export default {
Steps,
Notes,
Ingredients,
NutritionEditor,
},
props: {
name: String,
@@ -105,6 +109,7 @@ export default {
rating: Number,
yields: String,
orgURL: String,
nutrition: Object,
},
data() {
return {

View File

@@ -5,7 +5,7 @@
<v-row v-if="title != null">
<v-col>
<v-btn-toggle group>
<v-btn text :to="`/recipes/${title.toLowerCase()}`">
<v-btn text>
{{ title.toUpperCase() }}
</v-btn>
</v-btn-toggle>

View File

@@ -54,22 +54,11 @@ export default {
{
icon: "mdi-magnify",
to: "/search",
title: "search",
title: this.$t('search.search'),
},
],
};
},
computed: {
allCategories() {
return this.$store.getters.getCategories;
},
},
watch: {
allCategories() {
this.buildSidebar();
},
showSidebar() {},
},
mounted() {
this.buildSidebar();
this.mobile = this.viewScale();
@@ -81,14 +70,27 @@ export default {
this.links = [];
this.links.push(...this.baseLinks);
const pages = await api.siteSettings.getPages();
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",
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) {

View File

@@ -0,0 +1,51 @@
<template>
<v-btn color="accent" text :loading="downloading" @click="downloadFile">
{{ showButtonText }}
</v-btn>
</template>
<script>
/**
* The download button used for the entire site
* pass a URL to the endpoint that will return a
* file_token which will then be used to request the file
* from the server and open that link in a new tab
*/
import { apiReq } from "@/api/api-utils";
export default {
props: {
/**
* URL to get token from
*/
downloadUrl: {
default: "",
},
/**
* Override button text. Defaults to "Download"
*/
buttonText: {
default: null,
},
},
data() {
return {
downloading: false,
};
},
computed: {
showButtonText() {
return this.buttonText || this.$t("general.download");
},
},
methods: {
async downloadFile() {
this.downloading = true;
await apiReq.download(this.downloadUrl);
this.downloading = false;
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,7 +1,12 @@
<template>
<v-form ref="file">
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
<v-btn :loading="isSelecting" @click="onButtonClick" color="accent" text>
<v-btn
:loading="isSelecting"
@click="onButtonClick"
color="accent"
:text="textBtn"
>
<v-icon left> {{ icon }}</v-icon>
{{ text ? text : defaultText }}
</v-btn>
@@ -13,10 +18,17 @@ const UPLOAD_EVENT = "uploaded";
import { api } from "@/api";
export default {
props: {
post: {
type: Boolean,
default: true,
},
url: String,
text: { default: "Upload" },
icon: { default: "mdi-cloud-upload" },
fileName: { default: "archive" },
textBtn: {
default: true,
},
},
data: () => ({
file: null,
@@ -33,6 +45,12 @@ export default {
async upload() {
if (this.file != null) {
this.isSelecting = true;
if (this.post) {
this.$emit(UPLOAD_EVENT, this.file);
this.isSelecting = false;
return;
}
let formData = new FormData();
formData.append(this.fileName, this.file);

View File

@@ -93,7 +93,7 @@
"groups": "Gruppen",
"could-not-validate-credentials": "Anmeldeinformationen konnten nicht validiert werden",
"login": "Anmeldung",
"groups-can-only-be-set-by-administrators": "Groups can only be set by administrators",
"groups-can-only-be-set-by-administrators": "Gruppen können nur durch einen Administrator gesetzt werden",
"upload-photo": "Foto hochladen",
"reset-password": "Passwort zurücksetzen",
"current-password": "Aktuelles Passwort",

View File

@@ -48,7 +48,8 @@
"apply": "Apply",
"current-parenthesis": "(Current)",
"users": "Users",
"groups": "Groups"
"groups": "Groups",
"about": "About"
},
"page": {
"home-page": "Home Page",
@@ -145,7 +146,16 @@
"delete-confirmation": "Are you sure you want to delete this recipe?"
},
"search": {
"search-mealie": "Search Mealie"
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter",
"tag-filter": "Tag Filter",
"include": "Include",
"exclude": "Exclude",
"and": "And",
"or": "Or",
"search": "Search"
},
"settings": {
"general-settings": "General Settings",
@@ -215,7 +225,11 @@
"site-settings": "Site Settings",
"manage-users": "Manage Users",
"migrations": "Migrations",
"profile": "Profile"
"profile": "Profile",
"custom-pages": "Custom Pages",
"new-page": "New Page",
"edit-page": "Edit Page",
"page-name": "Page Name"
},
"migration": {
"recipe-migration": "Recipe Migration",

View File

@@ -46,7 +46,10 @@
"token": "Jeton",
"field-required": "Champ obligatoire",
"apply": "Appliquer",
"current-parenthesis": "(Actuel)"
"current-parenthesis": "(Actuel)",
"groups": "Groupes",
"users": "Utilisateurs",
"about": "À propos"
},
"page": {
"home-page": "Accueil",
@@ -120,7 +123,7 @@
"categories": "Catégories",
"tags": "Tags",
"instructions": "Instructions",
"step-index": "Etape: {step}",
"step-index": "Étape : {step}",
"recipe-name": "Nom de la recette",
"servings": "Portions",
"ingredient": "Ingrédient",
@@ -143,7 +146,16 @@
"delete-confirmation": "Êtes-vous sûr(e) de vouloir supprimer cette recette ?"
},
"search": {
"search-mealie": "Rechercher dans Mealie"
"search-mealie": "Rechercher dans Mealie",
"search-placeholder": "Rechercher...",
"and": "Et",
"category-filter": "Filtre par catégories",
"exclude": "Exclure",
"include": "Inclure",
"max-results": "Résultats max",
"or": "Ou",
"tag-filter": "Filtre par tags",
"search": "Rechercher"
},
"settings": {
"general-settings": "Paramètres généraux",
@@ -185,7 +197,7 @@
},
"webhooks": {
"meal-planner-webhooks": "Webhooks du planificateur de repas",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Les liens dans cette liste recevront les webhooks contenant les recettes pour le plan de menu du jour défini. Actuellement, les webhooks s'executeront à <strong>{ time }</strong>",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Les liens dans cette liste recevront les webhooks contenant les recettes pour le plan de menu du jour défini. Actuellement, les webhooks s'exécuteront à",
"test-webhooks": "Tester les webhooks",
"webhook-url": "Lien du webhook"
},
@@ -213,7 +225,11 @@
"manage-users": "Utilisateurs",
"migrations": "Migrations",
"profile": "Profil",
"site-settings": "Paramètres site"
"site-settings": "Paramètres site",
"custom-pages": "Pages personnalisées",
"edit-page": "Modifier la page",
"new-page": "Nouvelle page",
"page-name": "Nom de la page"
},
"migration": {
"recipe-migration": "Migrer les recettes",

View File

@@ -18,6 +18,20 @@ const router = new VueRouter({
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
});
const DEFAULT_TITLE = 'Mealie';
const TITLE_SEPARATOR = '🍴';
const TITLE_SUFFIX = " " + TITLE_SEPARATOR + " " + DEFAULT_TITLE;
router.afterEach( (to) => {
Vue.nextTick( async () => {
if(typeof to.meta.title === 'function' ) {
const title = await to.meta.title(to);
document.title = title + TITLE_SUFFIX;
} else {
document.title = to.meta.title ? to.meta.title + TITLE_SUFFIX : DEFAULT_TITLE;
}
});
});
const vueApp = new Vue({
vuetify,
store,

View File

@@ -20,6 +20,17 @@
</v-list-item>
</v-list-item-group>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<TheDownloadBtn
button-text="Download Recipe JSON"
download-url="/api/debug/last-recipe-json"
/>
<TheDownloadBtn
button-text="Download Log"
download-url="/api/debug/log"
/>
</v-card-actions>
<v-divider></v-divider>
</v-card>
</div>
@@ -27,7 +38,9 @@
<script>
import { api } from "@/api";
import TheDownloadBtn from "@/components/UI/TheDownloadBtn";
export default {
components: { TheDownloadBtn },
data() {
return {
prettyInfo: [],

View File

@@ -50,6 +50,7 @@
:rating="recipeDetails.rating"
:yields="recipeDetails.recipeYield"
:orgURL="recipeDetails.orgURL"
:nutrition="recipeDetails.nutrition"
/>
<VJsoneditor
@error="logError()"
@@ -151,6 +152,7 @@ export default {
methods: {
getImageFile(fileObject) {
this.fileObject = fileObject;
this.saveImage();
},
async getRecipeDetails() {
this.recipeDetails = await api.recipes.requestDetails(this.currentRecipe);
@@ -172,19 +174,21 @@ export default {
return this.$refs.recipeEditor.validateRecipe();
}
},
async saveImage() {
if (this.fileObject) {
await api.recipes.updateImage(this.recipeDetails.slug, this.fileObject);
}
this.imageKey += 1;
},
async saveRecipe() {
if (this.validateRecipe()) {
let slug = await api.recipes.update(this.recipeDetails);
if (this.fileObject) {
await api.recipes.updateImage(
this.recipeDetails.slug,
this.fileObject
);
this.saveImage();
}
this.form = false;
this.imageKey += 1;
if (slug != this.recipeDetails.slug) {
this.$router.push(`/recipe/${slug}`);
}

View File

@@ -10,11 +10,11 @@
mandatory
>
<v-btn :value="false">
Include
{{$t('search.include')}}
</v-btn>
<v-btn :value="true">
Exclude
{{$t('search.exclude')}}
</v-btn>
</v-btn-toggle>
<v-spacer></v-spacer>
@@ -28,10 +28,10 @@
mandatory
>
<v-btn :value="false">
And
{{$t('search.and')}}
</v-btn>
<v-btn :value="true">
Or
{{$t('search.or')}}
</v-btn>
</v-btn-toggle>
</v-toolbar>

View File

@@ -8,7 +8,7 @@
v-model="searchString"
outlined
color="primary accent-3"
placeholder="Placeholder"
:placeholder="$t('search.search-placeholder')"
append-icon="mdi-magnify"
>
</v-text-field>
@@ -16,7 +16,7 @@
<v-col cols="12" md="2" sm="12">
<v-text-field
class="mt-0 pt-0"
label="Max Results"
:label="$t('search.max-results')"
v-model="maxResults"
type="number"
outlined
@@ -26,7 +26,7 @@
<v-row dense class="mt-0 flex-row align-center justify-space-around">
<v-col>
<h3 class="pl-2 text-center headline">Category Filter</h3>
<h3 class="pl-2 text-center headline">{{$t('search.category-filter')}}</h3>
<FilterSelector class="mb-1" @update="updateCatParams" />
<CategoryTagSelector
:solo="true"
@@ -36,7 +36,7 @@
/>
</v-col>
<v-col>
<h3 class="pl-2 text-center headline">Tag Filter</h3>
<h3 class="pl-2 text-center headline">{{$t('search.tag-filter')}}</h3>
<FilterSelector class="mb-1" @update="updateTagParams" />
<CategoryTagSelector

View File

@@ -8,6 +8,7 @@ import ManageUsers from "@/pages/Admin/ManageUsers";
import Settings from "@/pages/Admin/Settings";
import About from "@/pages/Admin/About";
import { store } from "../store";
import i18n from '@/i18n.js';
export default {
path: "/admin",
@@ -25,35 +26,59 @@ export default {
{
path: "profile",
component: Profile,
meta: {
title: i18n.t('settings.profile'),
},
},
{
path: "backups",
component: Backup,
meta: {
title: i18n.t('settings.backup-and-exports'),
},
},
{
path: "themes",
component: Theme,
meta: {
title: i18n.t('general.themes'),
},
},
{
path: "meal-planner",
component: MealPlanner,
meta: {
title: i18n.t('meal-plan.meal-planner'),
},
},
{
path: "migrations",
component: Migration,
meta: {
title: i18n.t('settings.migrations'),
},
},
{
path: "manage-users",
component: ManageUsers,
meta: {
title: i18n.t('settings.manage-users'),
},
},
{
path: "settings",
component: Settings,
meta: {
title: i18n.t('settings.site-settings'),
},
},
{
path: "about",
component: About,
meta: {
title: i18n.t('general.about'),
},
},
],
};

View File

@@ -15,6 +15,7 @@ import ThisWeek from "@/pages/MealPlan/ThisWeek";
import { api } from "@/api";
import Admin from "./admin";
import { store } from "../store";
import i18n from '@/i18n.js';
export const routes = [
{ path: "/", name: "home", component: HomePage },
@@ -31,15 +32,43 @@ export const routes = [
{ path: "/sign-up", redirect: "/" },
{ path: "/sign-up/:token", component: SignUpPage },
{ path: "/debug", component: Debug },
{ path: "/search", component: SearchPage },
{
path: "/search",
component: SearchPage,
meta: {
title: i18n.t('search.search'),
},
},
{ path: "/recipes/all", component: AllRecipes },
{ path: "/pages/:customPage", component: CustomPage },
{ path: "/recipes/tag/:tag", component: TagPage },
{ path: "/recipes/category/:category", component: CategoryPage },
{ path: "/recipe/:recipe", component: ViewRecipe },
{
path: "/recipe/:recipe",
component: ViewRecipe,
meta: {
title: async route => {
const recipe = await api.recipes.requestDetails(route.params.recipe);
return recipe.name;
},
}
},
{ path: "/new/", component: NewRecipe },
{ path: "/meal-plan/planner", component: Planner },
{ path: "/meal-plan/this-week", component: ThisWeek },
{
path: "/meal-plan/planner",
component: Planner,
meta: {
title: i18n.t('meal-plan.meal-planner'),
}
},
{
path: "/meal-plan/this-week",
component: ThisWeek,
meta: {
title: i18n.t('meal-plan.dinner-this-week'),
}
},
Admin,
{
path: "/meal-plan/today",