Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/better print #237

Merged
merged 4 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 65 additions & 44 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,49 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<title>Sharp Cooking</title>
<meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="description" content="Your personal cooking book app" />
<meta name="keywords" content="PWA,App,Cooking,Recipes,Book" />
<meta name="author" content="LPains" />
<meta name="theme-color" content="#ffffff" />

<meta name="robots" content="index, follow" />
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<title>Sharp Cooking</title>
<meta name="viewport" content="width=device-width, viewport-fit=cover, initial-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="description" content="Your personal cooking book app" />
<meta name="keywords" content="PWA,App,Cooking,Recipes,Book" />
<meta name="author" content="LPains" />
<meta name="theme-color" content="#ffffff" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Zeyada&display=swap" rel="stylesheet">

<meta name="robots" content="index, follow" />

<meta property="og:title" content="Sharp Cooking" />
<meta property="og:description" content="Your personal cooking book app" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://app.sharpcooking.net/" />
<meta property="og:image" content="https://app.sharpcooking.net/android-chrome-512x512.png" />
<meta property="og:site_name" content="Sharp Cooking" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://app.sharpcooking.net/android-chrome-512x512.png" />
<meta name="twitter:title" content="Sharp Cooking" />
<meta name="twitter:description" content="Your personal cooking book app" />

<link rel="apple-touch-startup-image" href="/apple-splash-2048-2732.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1668-2388.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1536-2048.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1668-2224.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1620-2160.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1290-2796.jpg"
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1179-2556.jpg"
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1284-2778.jpg"
media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1170-2532.jpg"
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1125-2436.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1242-2688.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-828-1792.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1242-2208.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-750-1334.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-640-1136.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
</head>

<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>

<meta property="og:title" content="Sharp Cooking" />
<meta property="og:description" content="Your personal cooking book app" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://app.sharpcooking.net/" />
<meta property="og:image" content="https://app.sharpcooking.net/android-chrome-512x512.png" />
<meta property="og:site_name" content="Sharp Cooking" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://app.sharpcooking.net/android-chrome-512x512.png" />
<meta name="twitter:title" content="Sharp Cooking" />
<meta name="twitter:description" content="Your personal cooking book app" />

<link rel="apple-touch-startup-image" href="/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1290-2796.jpg" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1179-2556.jpg" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1284-2778.jpg" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1170-2532.jpg" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
8 changes: 7 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@
"ingredientDetailsIngredient": "Ingredient:",
"ingredientDetailsAlternativeUOMs": "Alternative UOMs:"
},
"print": {}
"print": {
"action": "Print",
"seconds": "seconds ",
"minutes": "minutes ",
"hours": "hours ",
"days": "days "
}
},
"importBackup": {
"title": "Import Backup",
Expand Down
8 changes: 7 additions & 1 deletion public/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@
"ingredientDetailsIngredient": "Ingrediente:",
"ingredientDetailsAlternativeUOMs": "UDMs alternativas:"
},
"print": {}
"print": {
"action": "Imprimir",
"seconds": "segundos ",
"minutes": "minutos ",
"hours": "horas ",
"days": "dias "
}
},
"importBackup": {
"title": "Importar backup",
Expand Down
2 changes: 1 addition & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ onMounted(async () => {

<template>
<TopBar />
<div class="container mx-auto">
<div :class="{'container mx-auto': state.useContainer}">
<InstallPrompt />
<div class="mt-16 mx-4 mb-10 dark:text-white">
<router-view v-slot="{ Component }">
Expand Down
2 changes: 1 addition & 1 deletion src/pages/recipe/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ async function applyMultiplier() {
}

function printItem() {
window.print();
router.push(`/recipe/${id.value}/print`);
}

function changeTime() {
Expand Down
167 changes: 167 additions & 0 deletions src/pages/recipe/[id]/print.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import { useRoute, onBeforeRouteLeave } from "vue-router";
import {
getRecipe,
getRecipeImage,
getSetting
} from "../../../services/dataService";
import { useTranslation } from "i18next-vue";
import { useState } from "../../../services/store";
import { RecipeViewModel } from "../recipeViewModel";
import { parseInstruction } from "@jlucaspains/sharp-recipe-parser";
import i18next from "i18next";

const { t } = useTranslation();
const route = useRoute();
const state = useState()!;

const id = computed(() => parseInt(route.params.id as string));
const item = ref({
id: 1,
title: "",
score: 3,
ingredients: [] as string[],
steps: [] as string[],
notes: "",
multiplier: 1,
changedOn: "",
image: "",
imageAvailable: false,
hasNotes: false
} as RecipeViewModel);
const displayTime = ref("");

onBeforeRouteLeave((to, from, next) => {
state.useContainer = true;
next();
});

onMounted(async () => {
state.useContainer = false;
state.menuOptions = [
{
text: t("pages.recipe.id.print.action"),
action: () => {
window.print();
},
svg: `<svg class="h-5 w-5 text-white m-auto" width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" />
<path d="M17 17h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-14a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h2" />
<path d="M17 9v-4a2 2 0 0 0 -2 -2h-6a2 2 0 0 0 -2 2v4" />
<rect x="7" y="13" width="10" height="8" rx="2" />
</svg>`,
},
];

const recipe = (await getRecipe(id.value)) as RecipeViewModel;
const image = await getRecipeImage(id.value);
const defaultTimeSetting = await getSetting("StepsInterval", "5");

if (recipe) {
state.title = recipe.title;

item.value = recipe;
item.value.image = image?.url;
displayTime.value = getRecipeDisplayTime(recipe, parseInt(defaultTimeSetting));
}

// delay print by 500 millisseconds to allow the page to render
// vue nextTick sometimes fail
setTimeout(() => {
window.print();
}, 500);
});


function getRecipeDisplayTime(
recipe: RecipeViewModel,
defaultTime: number
): string {
let totalTimeInSeconds = 0;
for (const step of recipe.steps) {
const result = parseInstruction(step, i18next.language);
const time = result?.totalTimeInSeconds || (defaultTime * 60);
totalTimeInSeconds += time;
}

return secondsToString(totalTimeInSeconds);
}

function secondsToString(totalSeconds: number) {
const days = Math.floor((totalSeconds % 31536000) / 86400);
const hours = Math.floor(((totalSeconds % 31536000) % 86400) / 3600);
const minutes = Math.floor((((totalSeconds % 31536000) % 86400) % 3600) / 60);
const seconds = (((totalSeconds % 31536000) % 86400) % 3600) % 60;

let result = "";
if (days > 0) {
result += `${days} ${t("pages.recipe.id.print.days")} `;
}
if (hours > 0) {
result += `${hours} ${t("pages.recipe.id.print.hours")} `;
}
if (minutes > 0) {
result += `${minutes} ${t("pages.recipe.id.print.minutes")} `;
}
if (seconds > 0) {
result += `${seconds} ${t("pages.recipe.id.print.seconds")}`;
}

return result;
}

</script>

<template>
<div class="mx-auto print-page print:-mt-16 print:-mx-4">
<div class="grid grid-cols-12 w-full">
<section class="col-span-6">
<img data-testid="recipe-img" :src="item.image" alt="" />
</section>
<section class="col-span-6 mt-4">
<h1 data-testid="recipe-title" class="text-center text-4xl print-title">
{{ item.title }}
</h1>
<ul class="my-2 flex flex-wrap items-center justify-center">
<li data-testid="display-time">🕛 {{ displayTime }}</li>
</ul>
<hr class="w-60 my-2 mx-auto border-theme-primary">
<ul class="p-2 text-center">
<li v-for="ingredient in item.ingredients">{{ ingredient }}</li>
</ul>
</section>
</div>
<div class="mt-5 mx-16">
<section class="h-96 p-2">
<h2 class="text-center text-2xl">Instructions</h2>
<ul class="list-decimal list-outside pl-3">
<li class="list-item list-item-lg-number mb-2 text-justify" v-for="step in item.steps">{{ step }}</li>
</ul>
</section>
</div>
</div>
</template>

<style>
@page :first {
margin-top: 0;
}

@page {
size: auto;
margin-left: 0mm;
margin-right: 0mm;
margin-bottom: 1in;
margin-top: 1in;
}

.print-title {
font-family: 'Zeyada', cursive;
}

.print-page {
width: 8.5in;
}
</style>
3 changes: 2 additions & 1 deletion src/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ interface State {
menuOptions?: MenuOption[];
indexScrollY: number;
message: any;
useContainer: boolean;
}

export const stateSymbol = Symbol('state') as InjectionKey<State>;
export const createState = () => reactive({ title: "", menuOptions: [], indexScrollY: 0, message: null });
export const createState = () => reactive({ title: "", menuOptions: [], indexScrollY: 0, message: null, useContainer: true });

export const useState = () => inject(stateSymbol);
export const provideState = () => provide(
Expand Down
23 changes: 21 additions & 2 deletions tests/display.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ test('change time', async ({ page, browserName }) => {
expect(await page.getByText('10:35 AM').textContent()).toMatch(/10:35.*/);
});

test('print recipe', async ({ page }) => {
await createRecipe(page, 2, "Print Bread", 5, ["100g flour"], ["Bake it for 30 min"]);

page.on("load", (pg) => {
pg.evaluate("window.print = function() { console.log('Print was triggered'); };")
});

await page.goto('/');
await page.getByText('Print Bread').first().click();
await page.getByTestId('print-button').click();

await expect(page).toHaveURL(new RegExp(/.*\/recipe\/2\/print/));
await expect(page.getByTestId('recipe-title')).toHaveText('Print Bread');
await expect(page.getByTestId('display-time')).toContainText('30 minutes');
await expect(page.getByText('100g flour')).toHaveText('100g flour');
await expect(page.getByText('Bake it for 30 min')).toHaveText('Bake it for 30 min');
await page.waitForEvent("console", item => item.text() == "Print was triggered")
});

// test('change time webkit', async ({ page, browserName }) => {
// test.skip(browserName !== 'webkit', 'not applicable');

Expand Down Expand Up @@ -100,7 +119,7 @@ Bake it for 30 min`;
await consoleWaiter;
});

test('share as file', async ({ page, browserName }) => {
test('share as file', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'not applicable');
await page.addInitScript(() => {
const comparer = '[{"id":2,"title":"New Bread","score":5,"ingredients":["100g flour",""],"steps":["Bake it for 30 min"],"multiplier":1,"images":[]}]';
Expand All @@ -112,7 +131,7 @@ test('share as file', async ({ page, browserName }) => {
const json = JSON.parse(result);
delete json[0].changedOn;

if(JSON.stringify(json) !== comparer) {
if (JSON.stringify(json) !== comparer) {
console.error("File doesn't match expectation");
} else {
console.info("All good");
Expand Down
Loading