mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
add print-view component (#407)
Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
35adc341e6
commit
3804e1d52c
@ -4,7 +4,9 @@
|
||||
<TheAppBar />
|
||||
<v-main>
|
||||
<v-banner v-if="demo" sticky>
|
||||
<div class="text-center"><b> This is a Demo of the v0.5.0 (BETA) </b> | Username: changeme@email.com | Password: demo</div>
|
||||
<div class="text-center">
|
||||
<b> This is a Demo of the v0.5.0 (BETA) </b> | Username: changeme@email.com | Password: demo
|
||||
</div>
|
||||
</v-banner>
|
||||
<GlobalSnackbar />
|
||||
<router-view></router-view>
|
||||
@ -76,4 +78,5 @@ export default {
|
||||
:root {
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -58,10 +58,10 @@ export default {
|
||||
defaultMenu() {
|
||||
return [
|
||||
{
|
||||
title: this.$t("general.download"),
|
||||
icon: "mdi-download",
|
||||
title: this.$t("general.print"),
|
||||
icon: "mdi-printer",
|
||||
color: "accent",
|
||||
action: "download",
|
||||
action: "print",
|
||||
},
|
||||
{
|
||||
title: this.$t("general.link"),
|
||||
@ -108,8 +108,8 @@ export default {
|
||||
case "edit":
|
||||
this.$router.push(`/recipe/${this.slug}` + "?edit=true");
|
||||
break;
|
||||
case "download":
|
||||
await this.downloadJson();
|
||||
case "print":
|
||||
this.$router.push(`/recipe/${this.slug}` + "?print=true");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -127,25 +127,6 @@ export default {
|
||||
() => console.log("Copied Failed", copyText)
|
||||
);
|
||||
},
|
||||
async downloadJson() {
|
||||
const recipe = await api.recipes.requestDetails(this.slug);
|
||||
this.downloadString(JSON.stringify(recipe, "", 4), "text/json", recipe.slug + ".json");
|
||||
},
|
||||
downloadString(text, fileType, fileName) {
|
||||
let blob = new Blob([text], { type: fileType });
|
||||
|
||||
let a = document.createElement("a");
|
||||
a.download = fileName;
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.dataset.downloadurl = [fileType, a.download, a.href].join(":");
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function() {
|
||||
URL.revokeObjectURL(a.href);
|
||||
}, 1500);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
155
frontend/src/components/Recipe/PrintView.vue
Normal file
155
frontend/src/components/Recipe/PrintView.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="container print">
|
||||
<div>
|
||||
<h1>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#E58325"
|
||||
d="M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.36,10.22L13.16,10C12.38,9.23 12.38,7.97 13.16,7.19L17.5,2.82L18.43,3.74L15.19,7L16.15,7.94L19.39,4.69L20.31,5.61L17.06,8.85L18,9.81L21.26,6.56L22.18,7.5L17.81,11.84C17.03,12.62 15.77,12.62 15,11.84L14.78,11.64L13.41,13Z"
|
||||
/>
|
||||
</svg>
|
||||
{{ recipe.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="time-container">
|
||||
<RecipeTimeCard :prepTime="recipe.prepTime" :totalTime="recipe.totalTime" :performTime="recipe.performTime" />
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
<div>
|
||||
<vue-markdown :source="recipe.description"> </vue-markdown>
|
||||
<h2>{{ $t("recipe.ingredients") }}</h2>
|
||||
<ul>
|
||||
<li v-for="(ingredient, index) in recipe.recipeIngredient" :key="index">
|
||||
<v-icon>
|
||||
mdi-checkbox-blank-outline
|
||||
</v-icon>
|
||||
<p>{{ ingredient }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ $t("recipe.instructions") }}</h2>
|
||||
<div v-for="(step, index) in recipe.recipeInstructions" :key="index">
|
||||
<h2 v-if="step.title">{{ step.title }}</h2>
|
||||
<div class="ml-5">
|
||||
<h3>{{ $t("recipe.step-index", { step: index + 1 }) }}</h3>
|
||||
<vue-markdown :source="step.text"> </vue-markdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<v-divider v-if="recipe.notes.length > 0" class="mb-5 mt-0"></v-divider>
|
||||
|
||||
<div v-for="(note, index) in recipe.notes" :key="index + 'note'">
|
||||
<h3>{{ note.title }}</h3>
|
||||
<vue-markdown :source="note.text"> </vue-markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue";
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
export default {
|
||||
components: {
|
||||
RecipeTimeCard,
|
||||
VueMarkdown,
|
||||
},
|
||||
props: {
|
||||
recipe: Object,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
body,
|
||||
html {
|
||||
margin-top: -40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0 !important;
|
||||
display: -webkit-box;
|
||||
display: flex;
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.015625em;
|
||||
font-weight: 300;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-left: 0.25rem;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.03125em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-right: 0.5rem;
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.time-container {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.time-chip {
|
||||
border-radius: 0.25rem;
|
||||
border-color: black;
|
||||
border: 1px;
|
||||
border-top: 1px;
|
||||
}
|
||||
|
||||
.print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.print {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card flat class="d-print-none">
|
||||
<v-card-text>
|
||||
<v-row align="center" justify="center">
|
||||
<v-btn left color="accent lighten-1 " class="ma-1 image-action" @click="$emit('exit')">
|
||||
<v-icon> mdi-arrow-left </v-icon>
|
||||
</v-btn>
|
||||
<v-card flat class="text-center" align-center>
|
||||
<v-card-text>Font Size</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn class="mx-2" fab dark x-small color="primary" @click="subtractFontSize">
|
||||
<v-icon dark> mdi-minus </v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="mx-2" fab dark x-small color="primary" @click="addFontSize">
|
||||
<v-icon dark> mdi-plus </v-icon>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card flat>
|
||||
<v-row dense align="center">
|
||||
<v-col md="10" sm="10">
|
||||
<v-card flat>
|
||||
<v-card-title> {{ recipe.name }} </v-card-title>
|
||||
|
||||
<v-card-text> {{ recipe.description }} </v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col md="1" sm="1" justify-end>
|
||||
<v-img :src="getImage(recipe.image)" max-height="200" max-width="300"> </v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
<v-card flat align>
|
||||
<v-card-text>
|
||||
<v-row class="mt-n6">
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-rating
|
||||
class="mr-2 align-end static"
|
||||
color="secondary darken-1"
|
||||
background-color="secondary lighten-3"
|
||||
length="5"
|
||||
:value="recipe.rating"
|
||||
></v-rating>
|
||||
</v-row>
|
||||
<h2 class="mt-1">Ingredients</h2>
|
||||
<v-row>
|
||||
<v-list dense class="column-wrapper align-start">
|
||||
<v-list-item
|
||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||
:key="generateKey('ingredient', index)"
|
||||
hide-details
|
||||
class="mb-n3 print-text"
|
||||
:label="ingredient"
|
||||
>
|
||||
<v-list-item-icon class="mr-1">
|
||||
<v-icon> mdi-minus </v-icon>
|
||||
</v-list-item-icon>
|
||||
{{ ingredient }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<div v-if="recipe.categories[0]">
|
||||
<h2 class="mt-4">Categories</h2>
|
||||
<v-chip class="ma-1" color="primary" dark v-for="category in recipe.categories" :key="category">
|
||||
{{ category }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="recipe.tags[0]">
|
||||
<h2 class="mt-4">Tags</h2>
|
||||
<v-chip class="ma-1" color="primary" dark v-for="tag in recipe.tags" :key="tag">
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<h2 v-if="recipe.notes[0]" class="my-2">Notes</h2>
|
||||
<v-card flat class="mt-1" v-for="(note, index) in recipe.notes" :key="generateKey('note', index)">
|
||||
<v-card-title> {{ note.title }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ note.text }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<h2 class="mb-4">Instructions</h2>
|
||||
|
||||
<v-card
|
||||
v-for="(step, index) in recipe.recipeInstructions"
|
||||
:key="generateKey('step', index)"
|
||||
class="my-n4"
|
||||
flat
|
||||
>
|
||||
<v-card-title class="my-n4">Step: {{ index + 1 }}</v-card-title>
|
||||
<v-card-text class="my-n4">{{ step.text }}</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { utils } from "@/utils";
|
||||
import { api } from "@/api";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
recipe: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fontSize: 1.0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getImage(image) {
|
||||
if (image) {
|
||||
return api.recipes.recipeImage(image) + "?rnd=" + this.imageKey;
|
||||
}
|
||||
},
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
addFontSize() {
|
||||
this.fontSize += 0.2;
|
||||
},
|
||||
subtractFontSize() {
|
||||
this.fontSize -= 0.2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.column-wrapper {
|
||||
column-count: 2;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<div class="text-center d-print-none">
|
||||
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
||||
<v-card :loading="processing">
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-print-none no-print">
|
||||
<v-navigation-drawer v-model="showSidebar" width="180px" clipped app>
|
||||
<template v-slot:prepend>
|
||||
<v-list-item two-line v-if="isLoggedIn" to="/admin/profile">
|
||||
@ -231,4 +231,10 @@ export default {
|
||||
bottom: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -79,6 +79,7 @@
|
||||
"no": "No",
|
||||
"ok": "OK",
|
||||
"options": "Options:",
|
||||
"print": "Print",
|
||||
"random": "Random",
|
||||
"rating": "Rating",
|
||||
"recent": "Recent",
|
||||
|
@ -3,7 +3,7 @@
|
||||
<v-card v-if="skeleton" :color="`white ${theme.isDark ? 'darken-2' : 'lighten-4'}`" class="pa-3">
|
||||
<v-skeleton-loader class="mx-auto" height="700px" type="card"></v-skeleton-loader>
|
||||
</v-card>
|
||||
<v-card v-else id="myRecipe">
|
||||
<v-card v-else id="myRecipe" class="d-print-none">
|
||||
<v-img height="400" :src="getImage(recipeDetails.slug)" class="d-print-none" :key="imageKey">
|
||||
<RecipeTimeCard
|
||||
:class="isMobile ? undefined : 'force-bottom'"
|
||||
@ -36,6 +36,7 @@
|
||||
/>
|
||||
<RecipeEditor v-else v-model="recipeDetails" ref="recipeEditor" @upload="getImageFile" />
|
||||
</v-card>
|
||||
<PrintView :recipe="recipeDetails" />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@ -43,6 +44,7 @@
|
||||
import { api } from "@/api";
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import RecipeViewer from "@/components/Recipe/RecipeViewer";
|
||||
import PrintView from "@/components/Recipe/PrintView";
|
||||
import RecipeEditor from "@/components/Recipe/RecipeEditor";
|
||||
import RecipeTimeCard from "@/components/Recipe/RecipeTimeCard.vue";
|
||||
import EditorButtonRow from "@/components/Recipe/EditorButtonRow";
|
||||
@ -56,6 +58,7 @@ export default {
|
||||
RecipeEditor,
|
||||
EditorButtonRow,
|
||||
RecipeTimeCard,
|
||||
PrintView,
|
||||
},
|
||||
mixins: [user],
|
||||
inject: {
|
||||
@ -93,10 +96,17 @@ export default {
|
||||
imageKey: 1,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getRecipeDetails();
|
||||
|
||||
async mounted() {
|
||||
await this.getRecipeDetails();
|
||||
this.jsonEditor = false;
|
||||
this.form = this.$route.query.edit === "true" && this.loggedIn;
|
||||
|
||||
console.log(this.$route.query.print);
|
||||
if (this.$route.query.print) {
|
||||
this.printPage();
|
||||
this.$router.push(this.$route.path);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -174,6 +184,9 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
printPage() {
|
||||
window.print();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user