Merge pull request #3143 from Mel-Levesque/RecipeApp_dev

Create a recipe app mentioned in issue #795
pull/3150/head
Rob Pilling 2024-01-03 23:09:59 +00:00 committed by GitHub
commit 68e5eff15d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 439 additions and 0 deletions

View File

@ -0,0 +1 @@
0.01: New App

View File

@ -0,0 +1,32 @@
# Follow the recipe
A simple app using Gadgetbridge internet access to fetch a recipe and follow it step by step.
For now, if you are connected to Gadgetbridge, it display a random recipe whenever you restart the app.
Else, a stored recipe is displayed.
You can go to the next screen via tab right and go the previous screen via tab left.
You can choose a recipe via the App Loader:
Select the recipe then click on "Save recipe onto BangleJs".
![](screenshot1.png)
Make sure that you allowed 'Internet Access' via the Gadgetbridge app before using Follow The Recipe.
If you run the app via web IDE, connect your Banglejs via Gadgetbridge app then in the web IDE connect via Android.
For more informations, [see the documentation about Gadgetbridge](https://www.espruino.com/Gadgetbridge)
TO-DOs:
- [X] Display random recipe on start
- [ ] Choose between some recipe previously saved or random on start
- [ ] Edit the recipe and save it to BangleJs
- [ ] improve GUI (color, fonts, ...)
## Contributors
Written by [Mel-Levesque](https://github.com/Mel-Levesque)
## Thanks To
- Design taken from the [Info application](https://github.com/espruino/BangleApps/tree/master/apps/info) by [David Peer](https://github.com/peerdavid)
- App icon from [icons8.com](https://icons8.com)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA////e3uUt+cVEjEK0ALJlWqAAv4BYelqoAEBa/61ALRrQDCBY9q1ILCLYQLD0EKHZFawECAgILGFIYvHwAFBgQLGqwLDyoLGSwYLBI4gLFHYojFI4wdCMAJHGtEghEpBY9YkWIkoLNR4oLEHYwLMHYILJAoILIrWq1SzIBZYjE/gXKBYwAEEYwAEC67LGHQIABZY4jWF9FXBZVfBZX/BYmv/4AEBZ8KKIYACwALCACwA=="))

229
apps/followtherecipe/app.js Normal file
View File

@ -0,0 +1,229 @@
const storage = require("Storage");
const settings = require("Storage").readJSON("followtherecipe.json");
const locale = require('locale');
var ENV = process.env;
var W = g.getWidth(), H = g.getHeight();
var screen = 0;
var Layout = require("Layout");
let maxLenghtHorizontal = 16;
let maxLenghtvertical = 6;
let uri = "https://www.themealdb.com/api/json/v1/1/random.php";
var colors = {0: "#70f", 1:"#70d", 2: "#70g", 3: "#20f", 4: "#30f"};
var screens = [];
function drawData(name, value, y){
g.drawString(name, 10, y);
g.drawString(value, 100, y);
}
function drawInfo() {
g.reset().clearRect(Bangle.appRect);
var h=18, y = h;
// Header
g.drawLine(0,25,W,25);
g.drawLine(0,26,W,26);
// Info body depending on screen
g.setFont("Vector",15).setFontAlign(-1,-1).setColor("#0ff");
screens[screen].items.forEach(function (item, index){
g.setColor(colors[index]);
drawData(item.name, item.fun, y+=h);
});
// Bottom
g.setColor(g.theme.fg);
g.drawLine(0,H-h-3,W,H-h-3);
g.drawLine(0,H-h-2,W,H-h-2);
g.setFont("Vector",h-2).setFontAlign(-1,-1);
g.drawString(screens[screen].name, 2, H-h+2);
g.setFont("Vector",h-2).setFontAlign(1,-1);
g.drawString((screen+1) + "/" + screens.length, W, H-h+2);
}
// Change page if user touch the left or the right of the screen
Bangle.on('touch', function(btn, e){
var left = parseInt(g.getWidth() * 0.3);
var right = g.getWidth() - left;
var isLeft = e.x < left;
var isRight = e.x > right;
if(isRight){
screen = (screen + 1) % screens.length;
}
if(isLeft){
screen -= 1;
screen = screen < 0 ? screens.length-1 : screen;
}
Bangle.buzz(40, 0.6);
drawInfo();
});
function infoIngredients(ingredients, measures){
let combinedList = [];
let listOfString = [];
let lineBreaks = 0;
// Iterate through the arrays and combine the ingredients and measures
for (let i = 0; i < ingredients.length; i++) {
const combinedString = `${ingredients[i]}: ${measures[i]}`;
lineBreaks += 1;
// Check if the line is more than 16 characters
if (combinedString.length > maxLenghtHorizontal) {
// Add line break and update lineBreaks counter
combinedList.push(`${ingredients[i]}:\n${measures[i]}`);
lineBreaks += 1;
} else {
// Add to the combinedList array
combinedList.push(combinedString);
}
// Check the total line breaks
if (lineBreaks >= maxLenghtvertical) {
const resultString = combinedList.join('\n');
listOfString.push(resultString);
combinedList = [];
lineBreaks = 0;
}
if(i == ingredients.length){
listOfString.push(combinedList.join('\n'));
}
}
for(let i = 0; i < listOfString.length; i++){
let screen = {
name: "Ingredients",
items: [
{name: listOfString[i], fun: ""},
]
};
screens.push(screen);
}
}
// Format instructions to display on screen
function infoInstructions(instructionsString){
let item = [];
let chunkSize = 22;
//remove all space line and other to avoid problem with text
instructionsString = instructionsString.replace(/[\n\r]/g, '');
for (let i = 0; i < instructionsString.length; i += chunkSize) {
const chunk = instructionsString.substring(i, i + chunkSize).trim();
item.push({ name: chunk, fun: "" });
if (item.length === maxLenghtvertical) {
let screen = {
name: "Instructions",
items: item,
};
screens.push(screen);
item = [];
}
}
if (item.length > 0) {
let screen = {
name: "Instructions",
items: item,
};
screens.push(screen);
}
}
// Get json format and parse it into Strings
function getRecipeData(data) {
let mealName = data.strMeal;
let category = data.strCategory;
let area = data.strArea;
let instructions = data.strInstructions;
const ingredients = [];
const measures = [];
for (let i = 1; i <= 20; i++) {
const ingredient = data["strIngredient" + i];
const measure = data["strMeasure" + i];
if (ingredient && ingredient.trim() !== "") {
ingredients.push(ingredient);
if (measure && measure.trim() !== ""){
measures.push(measure);
}else{
measures.push("¯\\_(ツ)_/¯");
}
} else { // If no more ingredients are found
screens = [
{
name: "General",
items: [
{name: mealName, fun: ""},
{name: "", fun: ""},
{name: "Category", fun: category},
{name: "", fun: ""},
{name: "Area: ", fun: area},
]
}
];
infoIngredients(ingredients, measures);
infoInstructions(instructions);
drawInfo();
break;
}
}
}
function jsonData(){
let json = '{"meals":[{"idMeal":"52771","strMeal":"Spicy Arrabiata Penne","strDrinkAlternate":null,"strCategory":"Vegetarian","strArea":"Italian","strInstructions":"Bring a large pot of water to a boil. Add kosher salt to the boiling water, then add the pasta. Cook according to the package instructions, about 9 minutes.\\r\\nIn a large skillet over medium-high heat, add the olive oil and heat until the oil starts to shimmer. Add the garlic and cook, stirring, until fragrant, 1 to 2 minutes. Add the chopped tomatoes, red chile flakes, Italian seasoning and salt and pepper to taste. Bring to a boil and cook for 5 minutes. Remove from the heat and add the chopped basil.\\r\\nDrain the pasta and add it to the sauce. Garnish with Parmigiano-Reggiano flakes and more basil and serve warm.","strMealThumb":"https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg","strTags":"Pasta,Curry","strYoutube":"https://www.youtube.com/watch?v=1IszT_guI08","strIngredient1":"penne rigate","strIngredient2":"olive oil","strIngredient3":"garlic","strIngredient4":"chopped tomatoes","strIngredient5":"red chile flakes","strIngredient6":"italian seasoning","strIngredient7":"basil","strIngredient8":"Parmigiano-Reggiano","strIngredient9":"","strIngredient10":"","strIngredient11":"","strIngredient12":"","strIngredient13":"","strIngredient14":"","strIngredient15":"","strIngredient16":null,"strIngredient17":null,"strIngredient18":null,"strIngredient19":null,"strIngredient20":null,"strMeasure1":"1 pound","strMeasure2":"1/4 cup","strMeasure3":"3 cloves","strMeasure4":"1 tin ","strMeasure5":"1/2 teaspoon","strMeasure6":"1/2 teaspoon","strMeasure7":"6 leaves","strMeasure8":"spinkling","strMeasure9":"","strMeasure10":"","strMeasure11":"","strMeasure12":"","strMeasure13":"","strMeasure14":"","strMeasure15":"","strMeasure16":null,"strMeasure17":null,"strMeasure18":null,"strMeasure19":null,"strMeasure20":null,"strSource":null,"strImageSource":null,"strCreativeCommonsConfirmed":null,"dateModified":null}]}';
if(settings != null){
json = JSON.stringify({ meals: [settings] });
}
const obj = JSON.parse(json);
getRecipeData(obj.meals[0]);
}
function initData(retryCount) {
if (!Bangle.http) {
console.log("No http method found");
jsonData();
return;
}
jsonData();
Bangle.http(uri, { timeout: 1000 })
.then(event => {
try {
const obj = JSON.parse(event.resp);
if (obj.meals && obj.meals.length > 0) {
getRecipeData(obj.meals[0]);
} else {
console.log("Invalid JSON structure: meals array is missing or empty");
}
} catch (error) {
console.log("JSON Parse Error: " + error.message);
}
})
.catch(e => {
console.log("Request Error:", e);
if (e === "Timeout" && retryCount > 0) {
setTimeout(() => initData(retryCount - 1), 1000); // Optional: Add a delay before retrying
}else{
jsonData();
}
});
}
initData(3);
Bangle.on('lock', function(isLocked) {
drawInfo();
});
Bangle.loadWidgets();
Bangle.drawWidgets();

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

View File

@ -0,0 +1,146 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<style>
#responseContainer {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.meal {
flex: 1 0 calc(33.333% - 20px);
box-sizing: border-box;
}
.meal:hover {
background-color: cornflowerblue;
}
.meal img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<h3>Choose your recipe</h3>
<p>
<input id="recipeLink" type="text" autocomplete="off" placeholder="Search a Recipe" onkeyup="checkInput()" style="width:90%; margin: 3px"></input>
<p>Recipe to be imported to BangleJs: <span id="mealSelected">-</span></p>
<button id="upload" class="btn btn-primary">Save recipe into BangleJs</button>
</p>
<p id="testUtil">
</p>
<div id="responseContainer">
</div>
<script src="../../core/lib/interface.js"></script>
<script>
let uri = "";
let recipe = null;
const fileRecipeJson = "followtherecipe.json";
function checkInput(){
let inputStr = document.getElementById("recipeLink").value;
if(inputStr != "") {
getRecipe(inputStr);
}
}
function getRecipe(inputStr){
const Http = new XMLHttpRequest();
const url='https://www.themealdb.com/api/json/v1/1/search.php?s='+inputStr;
Http.open("GET", url);
Http.send();
Http.onreadystatechange = (e) => {
try{
const obj = JSON.parse(Http.response);
console.log("debug");
console.log(obj);
displayResponseData(obj)
}catch(e){
console.log("Error: "+e);
}
}
}
function displayResponseData(data){
const mealsContainer = document.getElementById('responseContainer');
while (mealsContainer.firstChild) {
mealsContainer.removeChild(mealsContainer.firstChild);
}
data.meals.forEach((meal) => {
const mealDiv = document.createElement('div');
mealDiv.classList.add('meal');
const imgElement = document.createElement('img');
imgElement.src = meal.strMealThumb;
imgElement.alt = meal.strMeal;
const titleP = document.createElement('p');
titleP.textContent = meal.strMeal;
// Append the image and title to the meal div
mealDiv.appendChild(imgElement);
mealDiv.appendChild(titleP);
mealDiv.onclick = function () {
document.getElementById("mealSelected").innerText = meal.strMeal;
let linkMeal = meal.strMeal.replaceAll(" ", "_");
uri = 'https://www.themealdb.com/api/json/v1/1/search.php?s='+linkMeal;
recipe = meal;
};
// Append the meal div to the container
mealsContainer.appendChild(mealDiv);
});
}
var settings = {};
function loadRecipe(){
try {
Util.showModal("Loading...");
Util.readStorageJSON(`${fileRecipeJson}`, data=>{
if(data){
settings = data;
document.getElementById("mealSelected").innerHTML = settings.strMeal;
checkInput();
}else{
console.log("NO data found");
}
});
} catch(ex) {
console.log("(Warning) Could not load data from BangleJs.");
console.log(ex);
}
Util.hideModal();
}
document.getElementById("upload").addEventListener("click", function() {
if(recipe != null){
try {
settings = recipe;
Util.showModal("Saving...");
Util.writeStorage("followtherecipe.json", JSON.stringify(settings), ()=>{
Util.hideModal();
});
console.log("Sent settings!");
} catch(ex) {
console.log("(Warning) Could not write settings to BangleJs.");
console.log(ex);
}
}
});
function onInit() {
loadRecipe();
}
onInit();
</script>
</body>
</html>

View File

@ -0,0 +1,30 @@
{
"id": "followtherecipe",
"name": "Follow The Recipe",
"shortName":"FTR",
"icon": "icon.png",
"version": "0.01",
"description": "Follow The Recipe (FTR) is a bangle.js app to follow a recipe step by step",
"type": "app",
"tags": "tool, tools, cook",
"supports": [
"BANGLEJS2"
],
"allow_emulator": true,
"interface": "interface.html",
"readme": "README.md",
"data": [
{"name":"followtherecipe.json"}
],
"storage": [
{
"name": "followtherecipe.app.js",
"url": "app.js"
},
{
"name": "followtherecipe.img",
"url": "app-icon.js",
"evaluate": true
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB