From bdc7a5ea5c31e8b657704f367dd15143eb1c6716 Mon Sep 17 00:00:00 2001
From: melim
Date: Tue, 2 Jan 2024 20:00:40 +0100
Subject: [PATCH] Create a recipe app mentioned in issue #795
---
apps/followtherecipe/ChangeLog | 1 +
apps/followtherecipe/README.md | 32 ++++
apps/followtherecipe/app-icon.js | 1 +
apps/followtherecipe/app.js | 229 +++++++++++++++++++++++++++
apps/followtherecipe/icon.png | Bin 0 -> 931 bytes
apps/followtherecipe/interface.html | 146 +++++++++++++++++
apps/followtherecipe/metadata.json | 30 ++++
apps/followtherecipe/screenshot1.png | Bin 0 -> 414574 bytes
8 files changed, 439 insertions(+)
create mode 100644 apps/followtherecipe/ChangeLog
create mode 100644 apps/followtherecipe/README.md
create mode 100644 apps/followtherecipe/app-icon.js
create mode 100644 apps/followtherecipe/app.js
create mode 100644 apps/followtherecipe/icon.png
create mode 100644 apps/followtherecipe/interface.html
create mode 100644 apps/followtherecipe/metadata.json
create mode 100644 apps/followtherecipe/screenshot1.png
diff --git a/apps/followtherecipe/ChangeLog b/apps/followtherecipe/ChangeLog
new file mode 100644
index 000000000..9ba6a29bd
--- /dev/null
+++ b/apps/followtherecipe/ChangeLog
@@ -0,0 +1 @@
+0.01: New App
\ No newline at end of file
diff --git a/apps/followtherecipe/README.md b/apps/followtherecipe/README.md
new file mode 100644
index 000000000..ce80c680d
--- /dev/null
+++ b/apps/followtherecipe/README.md
@@ -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)
diff --git a/apps/followtherecipe/app-icon.js b/apps/followtherecipe/app-icon.js
new file mode 100644
index 000000000..95b75991b
--- /dev/null
+++ b/apps/followtherecipe/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEw4UA////e3uUt+cVEjEK0ALJlWqAAv4BYelqoAEBa/61ALRrQDCBY9q1ILCLYQLD0EKHZFawECAgILGFIYvHwAFBgQLGqwLDyoLGSwYLBI4gLFHYojFI4wdCMAJHGtEghEpBY9YkWIkoLNR4oLEHYwLMHYILJAoILIrWq1SzIBZYjE/gXKBYwAEEYwAEC67LGHQIABZY4jWF9FXBZVfBZX/BYmv/4AEBZ8KKIYACwALCACwA=="))
diff --git a/apps/followtherecipe/app.js b/apps/followtherecipe/app.js
new file mode 100644
index 000000000..8238a6c07
--- /dev/null
+++ b/apps/followtherecipe/app.js
@@ -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();
\ No newline at end of file
diff --git a/apps/followtherecipe/icon.png b/apps/followtherecipe/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..a2a94bb88ec64cd5119d9b3df42316617ad42cd9
GIT binary patch
literal 931
zcmV;U16=%xP)bPpaM6WszUzQEgLdJUk1`Zb
zEjKTIhdG#gYoxCYS@`8hxk@qOJa}lLi&I?c)5A;JIujctU23iirpGyEe((b4;H(Sbg9%q5
zvC}}hd-7Q0uaf=saAW;RjQu%&Ip
zn|N`IA1kmzc%6WBtPi$I#c;4(GrJlv7i(y55y%_2!Y)c-6F#ldtOm@*8roX~N~T`H
zHn|j1E&A}4=EfY%#Twd$fM2~@&zb;|-edsZyJEmSzAG^2wArxiJOaCbZ>h4ARRN^p
z*AQqX2V8)`XC*;J%G|;u0b@1dQz81CL;Y-}k`%`5}H^LRWtF@K-?l`z;*)UdvB`+_^zG
zI?=Fn@*WYmG~K=KoS%9INs~ER@sGw6iNpP5h-E~lDt9W
+
+
+
+
+
+ Choose your recipe
+
+
+
Recipe to be imported to BangleJs: -
+
+
+
+
+
+
+
+
+
+
+
+
+