Merge pull request #3143 from Mel-Levesque/RecipeApp_dev
Create a recipe app mentioned in issue #795master
commit
68e5eff15d
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App
|
||||||
|
|
@ -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".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4UA////e3uUt+cVEjEK0ALJlWqAAv4BYelqoAEBa/61ALRrQDCBY9q1ILCLYQLD0EKHZFawECAgILGFIYvHwAFBgQLGqwLDyoLGSwYLBI4gLFHYojFI4wdCMAJHGtEghEpBY9YkWIkoLNR4oLEHYwLMHYILJAoILIrWq1SzIBZYjE/gXKBYwAEEYwAEC67LGHQIABZY4jWF9FXBZVfBZX/BYmv/4AEBZ8KKIYACwALCACwA=="))
|
||||||
|
|
@ -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 |
|
|
@ -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>
|
||||||
|
|
@ -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 |
Loading…
Reference in New Issue