1"use strict"; 2 3const STORAGE_KEY = "myDietTracker.v1"; 4const CLOUD_CONFIG_KEY = "myDietTracker.cloudConfig"; 5const CLOUD_SESSION_KEY = "myDietTracker.cloudSession"; 6const LOCAL_STATE_PRESENT_AT_LAUNCH = localStorage.getItem(STORAGE_KEY) !== null; 7 8const mealLabels = { 9 breakfast: "اÙÙØ·Ø§Ø±", 10 snack1: "Ø³ÙØ§Ù 1", 11 lunch: "Ø§ÙØºØ¯Ø§Ø¡", 12 snack2: "Ø³ÙØ§Ù 2", 13 dinner: "Ø§ÙØ¹Ø´Ø§Ø¡" 14}; 15 16const unitLabels = { 17 g: "ج٠", 18 ml: "Ù Ù", 19 piece: "ÙØ·Ø¹Ø©", 20 serving: "ØØµØ©", 21 tbsp: "Ù ÙØ¹ÙØ© ÙØ¨Ùرة", 22 cup: "ÙÙØ¨" 23}; 24 25const viewTitles = { 26 dashboard: "اÙÙÙÙ ", 27 add: "تسجÙÙ ÙØ¬Ø¨Ø©", 28 mealplanner: "ØØ§Ø³Ø¨Ø© اÙÙØ¬Ø¨Ø©", 29 explore: "Ø§Ø³ØªÙØ´Ù Ø§ÙØ£Ø·Ø¹Ù Ø©", 30 foods: "Ù ÙØªØ¨ØªÙ", 31 calculator: "ØØ§Ø³Ø¨Ø© Ø§ÙØ³Ø¹Ø±Ø§Øª", 32 history: "Ø§ÙØ³Ø¬Ù", 33 progress: "اÙÙØ²Ù ÙØ§ÙØªÙØ¯Ù ", 34 settings: "Ø§ÙØ¥Ø¹Ø¯Ø§Ø¯Ø§Øª" 35}; 36 37const defaultCategories = [ 38 { id: "protein", name: "Ø¨Ø±ÙØªÙÙØ§Øª", icon: "P", custom: false }, 39 { id: "carbs", name: "ÙØ´ÙÙØ§Øª", icon: "C", custom: false }, 40 { id: "dairy", name: "Ø£ÙØ¨Ø§Ù ÙØ£Ø¬Ø¨Ø§Ù", icon: "D", custom: false }, 41 { id: "legumes", name: "بÙÙÙÙØ§Øª", icon: "L", custom: false }, 42 { id: "vegetables", name: "خضار", icon: "V", custom: false }, 43 { id: "fruits", name: "ÙØ§ÙÙØ©", icon: "F", custom: false }, 44 { id: "fats", name: "دÙÙÙ ÙÙ ÙØ³Ø±Ø§Øª", icon: "N", custom: false }, 45 { id: "drinks", name: "Ù Ø´Ø±ÙØ¨Ø§Øª", icon: "B", custom: false }, 46 { id: "egyptian", name: "Ø£ÙÙØ§Øª Ù ØµØ±ÙØ©", icon: "E", custom: false }, 47 { id: "meals", name: "ÙØ¬Ø¨Ø§Øª ÙØ£ÙÙ Ø¨ÙØªÙ", icon: "M", custom: false }, 48 { id: "supplements", name: "Ù ÙÙ ÙØ§Øª", icon: "S", custom: false }, 49 { id: "uncategorized", name: "ØºÙØ± ٠صÙÙ", icon: "?", custom: false } 50]; 51 52const defaultFoods = [ 53 food("base-qareesh", "Ø¬Ø¨ÙØ© ÙØ±ÙØ´", 100, "g", 100, 12, 4, 4, 0, "dairy", true), 54 food("base-egg-boiled", "Ø¨ÙØ¶ ٠سÙÙÙ", 1, "piece", 78, 6.3, 0.6, 5.3, 0, "protein", true), 55 food("base-egg-white", "Ø¨ÙØ§Ø¶ Ø¨ÙØ¶Ø©", 1, "piece", 17, 3.6, 0.2, 0.1, 0, "protein"), 56 food("base-rice-cooked", "رز Ø£Ø¨ÙØ¶ Ù Ø·Ø¨ÙØ®", 100, "g", 130, 2.7, 28.2, 0.3, 0.4, "carbs", true), 57 food("base-rice-raw", "أرز Ø£Ø¨ÙØ¶ ÙÙ", 100, "g", 365, 7.1, 80, 0.7, 1.3, "carbs"), 58 food("base-brown-rice", "أرز بÙÙ Ù Ø·Ø¨ÙØ®", 100, "g", 123, 2.7, 25.6, 1, 1.6, "carbs"), 59 researchedFood("egy-rice-sayadiya", "رز ØµÙØ§Ø¯ÙØ©", 100, "g", 170, 3.2, 31, 3.6, 1.1, "egyptian", "ØªÙØ¯Ùر ÙØµÙØ© Ù ØµØ±ÙØ© ٠بÙ٠عÙÙ Ù ÙÙÙØ§Øª USDA", "Ù ØªÙØ³Ø· رز Ù Ø·Ù٠ببص٠٠ØÙ ر ÙÙÙ ÙØ© Ø²ÙØª Ù Ø¹ØªØ¯ÙØ©Ø ÙÙ ÙØ© Ø§ÙØ²Ùت ÙØ§Ùبص٠تغÙÙØ± اÙÙÙÙ Ø©."), 60 researchedFood("egy-rice-koshari-lentils", "رز ÙØ´Ø±Ù (رز ÙØ¹Ø¯Ø³)", 100, "g", 158, 5.1, 29, 2.4, 3.2, "egyptian", "ØªÙØ¯Ùر ÙØµÙØ© Ù ØµØ±ÙØ© ٠بÙ٠عÙÙ Ù ÙÙÙØ§Øª USDA", "رز ÙØ¹Ø¯Ø³ Ù Ø·Ø¨ÙØ®Ø§Ù بÙÙ ÙØ© Ø²ÙØª Ù Ø¹ØªØ¯ÙØ©Ø ÙØ§ ÙØ´Ù Ù Ù ÙØ±ÙÙØ© Ø£Ù ØÙ ص Ø£Ù Ø¯ÙØ© اÙÙØ´Ø±Ù اÙÙØ§Ù Ù."), 61 researchedFood("egy-rice-tomato", "رز Ø¨Ø§ÙØ·Ù اط٠", 100, "g", 148, 2.8, 28, 2.9, 1.2, "egyptian", "ØªÙØ¯Ùر ÙØµÙØ© Ù ÙØ²ÙÙØ© ٠بÙ٠عÙÙ Ù ÙÙÙØ§Øª USDA", "Ù ØªÙØ³Ø· رز Ù Ø·ÙÙ Ø¨Ø§ÙØ·Ù اط٠ÙÙÙ ÙØ© Ø²ÙØª Ù Ø¹ØªØ¯ÙØ©."), 62 researchedFood("egy-rice-tomato-potato", "رز Ø¨Ø§ÙØ·Ù Ø§Ø·Ù ÙØ§Ùبطاطس", 100, "g", 137, 2.7, 25.5, 2.8, 1.6, "egyptian", "ØªÙØ¯Ùر ÙØµÙØ© Ù ÙØ²ÙÙØ© ٠بÙ٠عÙÙ Ù ÙÙÙØ§Øª USDA", "اÙÙÙÙ Ø© ÙÙÙ 100 Ø¬Ù Ù Ù Ø§ÙØ·Ø¨Ù اÙÙÙØ§Ø¦ÙØ ØªØ®ØªÙÙ ØØ³Ø¨ ÙØ³Ø¨Ø© Ø§ÙØ±Ø² ÙÙØ¨Ø·Ø§Ø·Ø³ ÙÙÙ ÙØ© Ø§ÙØ²Ùت."), 63 researchedFood("base-chicken-cooked", "صدر ÙØ±Ø§Ø® Ù Ø´Ù٠بدÙÙ Ø¬ÙØ¯", 100, "g", 165, 31, 0, 3.6, 0, "protein", "USDA FoodData Central", "اÙÙÙÙ Ø© ÙÙÙ 100 ج٠بعد Ø§ÙØ·ÙÙØ بدÙÙ Ø¬ÙØ¯ Ø£Ù ØµÙØµ.", true), 64 researchedFood("base-chicken-raw", "صدر ÙØ±Ø§Ø® Ù٠بدÙÙ Ø¬ÙØ¯", 100, "g", 120, 22.5, 0, 2.6, 0, "protein", "USDA FoodData Central", "اÙÙÙÙ Ø© ÙÙÙ 100 Ø¬Ù ÙØ¨Ù Ø§ÙØ·ÙÙ.") , 65 researchedFood("base-chicken-thigh", "ÙØ±Ù ÙØ±Ø§Ø® Ù ÙØ²Ùع Ø§ÙØ¬Ùد Ù Ø·ÙÙ", 100, "g", 209, 26, 0, 10.9, 0, "protein", "USDA FoodData Central", "اÙÙÙÙ Ø© ÙÙÙ 100 ج٠٠٠اÙÙØÙ Ø§ÙÙ Ø·Ù٠دÙÙ Ø¬ÙØ¯."), 66 food("base-beef", "ÙØÙ Ø¨ÙØ±Ù ÙÙÙÙ Ø§ÙØ¯ÙÙ Ù Ø·ÙÙ", 100, "g", 217, 26, 0, 12, 0, "protein"), 67 food("base-minced-beef", "ÙØÙ Ù ÙØ±ÙÙ Ù ØªÙØ³Ø· Ø§ÙØ¯ÙÙ Ù Ø·ÙÙ", 100, "g", 250, 26, 0, 17, 0, "protein"), 68 food("base-tuna", "تÙÙØ© Ù ØµÙØ§Ø© باÙ٠اء", 100, "g", 116, 25.5, 0, 0.8, 0, "protein", true), 69 food("base-salmon", "سÙÙ ÙÙ Ù Ø´ÙÙ", 100, "g", 206, 22, 0, 12, 0, "protein"), 70 food("base-shrimp", "ج٠بر٠٠طÙÙ", 100, "g", 99, 24, 0.2, 0.3, 0, "protein"), 71 food("base-baladi", "رغÙÙ Ø¹ÙØ´ Ø¨ÙØ¯Ù Ù ØªÙØ³Ø·", 1, "piece", 250, 8, 50, 2, 4, "carbs", true), 72 food("base-half-baladi", "ÙØµÙ رغÙÙ Ø¹ÙØ´ Ø¨ÙØ¯Ù", 1, "piece", 125, 4, 25, 1, 2, "carbs"), 73 food("base-toast", "ØªÙØ³Øª أس٠ر", 1, "piece", 75, 3, 13, 1.1, 2, "carbs"), 74 food("base-oats", "Ø´ÙÙØ§Ù جاÙ", 100, "g", 389, 16.9, 66.3, 6.9, 10.6, "carbs", true), 75 food("base-yogurt", "Ø²Ø¨Ø§Ø¯Ù Ø·Ø¨ÙØ¹Ù ÙØ§Ù Ù Ø§ÙØ¯Ø³Ù ", 100, "g", 61, 3.5, 4.7, 3.3, 0, "dairy"), 76 food("base-greek-yogurt", "زباد٠ÙÙÙØ§ÙÙ ÙÙÙÙ Ø§ÙØ¯Ø³Ù ", 100, "g", 73, 9.5, 3.9, 2, 0, "dairy", true), 77 food("base-milk", "ÙØ¨Ù ÙØµÙ دس٠", 100, "ml", 50, 3.4, 4.8, 1.8, 0, "dairy"), 78 food("base-skim-milk", "ÙØ¨Ù خاÙÙ Ø§ÙØ¯Ø³Ù ", 100, "ml", 35, 3.4, 5, 0.1, 0, "dairy"), 79 food("base-cheddar", "Ø¬Ø¨ÙØ© Ø´ÙØ¯Ø±", 100, "g", 403, 25, 1.3, 33, 0, "dairy"), 80 researchedFood("base-potato", "بطاطس ٠سÙÙÙØ© بدÙÙ Ø¥Ø¶Ø§ÙØ§Øª", 100, "g", 87, 1.9, 20.1, 0.1, 1.8, "carbs", "USDA FoodData Central", "اÙÙÙÙ Ø© ÙÙÙ 100 ج٠بعد Ø§ÙØ³ÙÙØ دÙÙ Ø²ÙØª أ٠زبدة."), 81 researchedFood("potato-fried", "بطاطس Ù ØÙ رة / Ù ÙÙÙØ©", 100, "g", 312, 3.4, 41.4, 14.7, 3.8, "carbs", "USDA FoodData Central", "Ù ØªÙØ³Ø· بطاطس Ù ÙÙÙØ© ÙÙ Ø§ÙØ²ÙØªØ ÙÙØ¹ Ø§ÙØ²Ùت Ù٠دة اÙÙÙÙ ÙØºÙرا٠اÙÙÙÙ Ø©."), 82 researchedFood("potato-baked", "بطاطس Ù Ø´ÙÙØ© بدÙÙ Ø²ÙØª", 100, "g", 93, 2.5, 21.2, 0.1, 2.2, "carbs", "USDA FoodData Central", "اÙÙÙÙ Ø© ÙÙÙ 100 Ø¬Ù Ù Ù Ø§ÙØ¨Ø·Ø§Ø·Ø³ اÙÙ Ø´ÙÙØ© دÙÙ Ø¥Ø¶Ø§ÙØ§Øª."), 83 egyptianFood("potato-wedges-light", "بطاطس ÙØ¯Ø¬Ø² باÙÙØ±Ù - Ø²ÙØª Ø®ÙÙÙ", 145, 2.5, 24, 4.5, 2.5), 84 egyptianFood("potato-mashed-milk", "بطاطس بÙÙØ±Ù٠باÙÙØ¨Ù", 113, 1.9, 16.8, 4.2, 1.5), 85 egyptianFood("egy-potato-tomato", "بطاطس Ù Ø·Ø¨ÙØ®Ø© Ø¨Ø§ÙØµÙصة", 105, 2, 15, 4.5, 2), 86 researchedFood("potato-chips", "Ø´ÙØ¨Ø³Ù بطاطس", 100, "g", 536, 7, 53, 35, 4.8, "carbs", "USDA FoodData Central", "Ù ØªÙØ³Ø· Ø±ÙØ§Ø¦Ù Ø§ÙØ¨Ø·Ø§Ø·Ø³ اÙÙ ÙÙÙØ©Ø راجع Ø¹Ø¨ÙØ© اÙÙ ÙØªØ¬ Ø¹ÙØ¯ تÙÙØ±Ùا."), 87 food("base-sweet-potato", "بطاطا ØÙÙØ© Ù Ø´ÙÙØ©", 100, "g", 90, 2, 20.7, 0.2, 3.3, "carbs"), 88 food("base-pasta", "Ù ÙØ±ÙÙØ© ٠سÙÙÙØ©", 100, "g", 158, 5.8, 30.9, 0.9, 1.8, "carbs"), 89 food("base-bulgur", "Ø¨Ø±ØºÙ Ù Ø·Ø¨ÙØ®", 100, "g", 83, 3.1, 18.6, 0.2, 4.5, "carbs"), 90 food("base-beans", "ÙÙ٠٠د٠س بدÙÙ Ø¥Ø¶Ø§ÙØ§Øª", 100, "g", 110, 7.6, 19.7, 0.4, 5.4, "legumes"), 91 food("base-lentils", "عدس Ù Ø·Ø¨ÙØ®", 100, "g", 116, 9, 20.1, 0.4, 7.9, "legumes"), 92 food("base-chickpeas", "ØÙ ص Ù Ø·Ø¨ÙØ®", 100, "g", 164, 8.9, 27.4, 2.6, 7.6, "legumes"), 93 fruitFood("base-banana", "Ù ÙØ²", 89, 1.1, 22.8, 0.3, 2.6), 94 fruitFood("base-apple", "ØªÙØ§Ø باÙÙØ´Ø±", 52, 0.3, 13.8, 0.2, 2.4), 95 fruitFood("base-orange", "Ø¨Ø±ØªÙØ§Ù", 47, 0.9, 11.8, 0.1, 2.4), 96 fruitFood("base-strawberry", "ÙØ±Ø§ÙÙØ©", 32, 0.7, 7.7, 0.3, 2), 97 researchedFood("base-dates", "ت٠رة ٠جÙÙØ© Ù ØªÙØ³Ø·Ø©", 1, "piece", 23, 0.2, 6.2,
970, 0.6, "fruits", "USDA FoodData Central", "ØØµØ© ØªÙØ±ÙØ¨ÙØ© ÙØªÙ رة Ù ØªÙØ³Ø·Ø©Ø ÙØ²Ù Ø§ÙØØ¨Ø© ÙØ®ØªÙÙ ØØ³Ø¨ اÙÙÙØ¹."), 98 fruitFood("fruit-pear", "Ù٠ثرÙ", 57, 0.4, 15.2, 0.1, 3.1), 99 fruitFood("fruit-peach", "Ø®ÙØ®", 39, 0.9, 9.5, 0.3, 1.5), 100 fruitFood("fruit-apricot", "Ù Ø´Ù Ø´", 48, 1.4, 11.1, 0.4, 2), 101 fruitFood("fruit-mango", "Ù Ø§ÙØ¬Ù", 60, 0.8, 15, 0.4, 1.6), 102 fruitFood("fruit-guava", "Ø¬ÙØ§ÙØ©", 68, 2.6, 14.3, 1, 5.4), 103 fruitFood("fruit-grapes", "Ø¹ÙØ¨", 69, 0.7, 18.1, 0.2, 0.9), 104 fruitFood("fruit-watermelon", "Ø¨Ø·ÙØ®", 30, 0.6, 7.6, 0.2, 0.4), 105 fruitFood("fruit-cantaloupe", "ش٠ا٠/ ÙÙØªØ§ÙÙØ¨", 34, 0.8, 8.2, 0.2, 0.9), 106 fruitFood("fruit-pomegranate", "ر٠ا٠- ØØ¨Ùب", 83, 1.7, 18.7, 1.2, 4), 107 fruitFood("fruit-kiwi", "ÙÙÙÙ", 61, 1.1, 14.7, 0.5, 3), 108 fruitFood("fruit-pineapple", "Ø£ÙØ§Ùاس", 50, 0.5, 13.1, 0.1, 1.4), 109 fruitFood("fruit-plum", "برÙÙÙ", 46, 0.7, 11.4, 0.3, 1.4), 110 fruitFood("fruit-cherry", "ÙØ±Ø² ØÙÙ", 63, 1.1, 16, 0.2, 2.1), 111 fruitFood("fruit-fig", "تÙ٠طازج", 74, 0.8, 19.2, 0.3, 2.9), 112 fruitFood("fruit-date-dried-100", "ت٠ر ٠جÙÙ", 277, 1.8, 75, 0.2, 6.7), 113 researchedFood("fruit-date-fresh", "Ø¨ÙØ رطب", 100, "g", 142, 1.3, 37.2, 0.2, 3.5, "fruits", "جداÙ٠ترÙÙØ¨ Ø£ØºØ°ÙØ© Ø¥ÙÙÙÙ ÙØ©", "ÙÙÙ Ø© Ù ØªÙØ³Ø·Ø©Ø تختÙÙ ÙØ«ÙØ±ÙØ§ Ø¨Ø§Ø®ØªÙØ§Ù درجة اÙÙØ¶Ø¬ ÙØ§ÙÙÙØ¹."), 114 fruitFood("fruit-tangerine", "ÙÙØ³ÙÙ", 53, 0.8, 13.3, 0.3, 1.8), 115 fruitFood("fruit-grapefruit", "Ø¬Ø±ÙØ¨ ÙØ±Ùت", 42, 0.8, 10.7, 0.1, 1.6), 116 fruitFood("fruit-lemon", "ÙÙÙ ÙÙ", 29, 1.1, 9.3, 0.3, 2.8), 117 fruitFood("fruit-avocado", "Ø£ÙÙÙØ§Ø¯Ù", 160, 2, 8.5, 14.7, 6.7), 118 fruitFood("fruit-coconut", "Ø¬ÙØ² ÙÙØ¯ طازج", 354, 3.3, 15.2, 33.5, 9), 119 fruitFood("fruit-prickly-pear", "تÙÙ Ø´ÙÙÙ", 41, 0.7, 9.6, 0.5, 3.6), 120 fruitFood("fruit-persimmon", "ÙØ§Ùا", 70, 0.6, 18.6, 0.2, 3.6), 121 fruitFood("fruit-mulberry", "ØªÙØª", 43, 1.4, 9.8, 0.4, 1.7), 122 fruitFood("fruit-blueberry", "ØªÙØª أزرÙ", 57, 0.7, 14.5, 0.3, 2.4), 123 fruitFood("fruit-blackberry", "ØªÙØª Ø£Ø³ÙØ¯", 43, 1.4, 9.6, 0.5, 5.3), 124 fruitFood("fruit-raspberry", "ØªÙØª Ø§ÙØ¹ÙÙÙ", 52, 1.2, 11.9, 0.7, 6.5), 125 fruitFood("fruit-papaya", "Ø¨Ø§Ø¨Ø§ÙØ§", 43, 0.5, 10.8, 0.3, 1.7), 126 fruitFood("fruit-passion", "Ø¨Ø§Ø´Ù ÙØ±Ùت", 97, 2.2, 23.4, 0.7, 10.4), 127 fruitFood("fruit-lychee", "ÙÙØªØ´Ù", 66, 0.8, 16.5, 0.4, 1.3), 128 fruitFood("fruit-loquat", "Ø¨Ø´Ù ÙØ© / Ø£Ø³ÙØ¯ÙÙØ§", 47, 0.4, 12.1, 0.2, 1.7), 129 fruitFood("fruit-quince", "Ø³ÙØ±Ø¬Ù", 57, 0.4, 15.3, 0.1, 1.9), 130 researchedFood("fruit-custard-apple", "ÙØ´Ø·Ø© / ØªÙØ§Ø Ø³ÙØ±Ù", 100, "g", 94, 2.1, 23.6, 0.3, 4.4, "fruits", "جداÙ٠ترÙÙØ¨ Ø£ØºØ°ÙØ© دÙÙÙØ©", "اÙÙÙÙ ÙÙØ¬Ø²Ø¡ اÙ٠أÙÙÙ ÙÙØ¯ تختÙÙ ØØ³Ø¨ Ø§ÙØµÙÙ."), 131 food("base-cucumber", "Ø®ÙØ§Ø±", 100, "g", 15, 0.7, 3.6, 0.1, 0.5, "vegetables"), 132 food("base-tomato", "ط٠اط٠", 100, "g", 18, 0.9, 3.9, 0.2, 1.2, "vegetables"), 133 food("base-lettuce", "خس", 100, "g", 15, 1.4, 2.9, 0.2, 1.3, "vegetables"), 134 food("base-broccoli", "برÙÙÙÙ Ù Ø·ÙÙ", 100, "g", 35, 2.4, 7.2, 0.4, 3.3, "vegetables"), 135 food("base-mixed-salad", "Ø³ÙØ·Ø© خضراء بدÙÙ Ø²ÙØª", 100, "g", 25, 1.2, 5, 0.2, 2, "vegetables", true), 136 food("base-olive-oil", "Ø²ÙØª Ø²ÙØªÙÙ", 1, "tbsp", 119, 0, 0, 13.5, 0, "fats"), 137 food("base-peanut-butter", "زبدة ÙÙÙ Ø³ÙØ¯Ø§ÙÙ", 100, "g", 588, 25, 20, 50, 6, "fats"), 138 food("base-almonds", "ÙÙØ²", 100, "g", 579, 21.2, 21.6, 49.9, 12.5, "fats"), 139 food("base-peanuts", "ÙÙÙ Ø³ÙØ¯Ø§ÙÙ Ù ØÙ ص", 100, "g", 585, 24.4, 21.5, 49.7, 8.4, "fats"), 140 food("base-whey", "ÙØ§Ù Ø¨Ø±ÙØªÙÙ - سÙÙØ¨ ØªÙØ±ÙبÙ", 1, "serving", 120, 24, 3, 2, 0, "supplements"), 141 food("base-coffee", "ÙÙÙØ© سادة", 1, "cup", 3, 0.3, 0, 0, 0, "drinks"), 142 food("base-green-tea", "شا٠أخضر سادة", 1, "cup", 2, 0, 0.4, 0, 0, "drinks"), 143 food("base-herbal-tea", "Ù Ø´Ø±ÙØ¨ أعشاب سادة", 1, "cup", 2, 0, 0.4, 0, 0, "drinks"), 144 food("base-americano", "ÙÙÙØ© أ٠رÙÙØ§Ù٠سادة", 1, "cup", 5, 0.3, 0.7, 0, 0, "drinks"), 145 food("base-espresso", "Ø¥Ø³Ø¨Ø±ÙØ³Ù سادة", 1, "cup", 3, 0.2,
1450.5, 0, 0, "drinks"), 146 egyptianFood("egy-chicken-pane-fried", "ÙØ±Ø§Ø® باÙÙÙ Ù ÙÙÙØ©", 270, 22, 17, 13, 1), 147 egyptianFood("egy-chicken-pane-oven", "ÙØ±Ø§Ø® باÙÙÙ Ù٠اÙÙØ±Ù Ø£Ù Ø¥ÙØ± ÙØ±Ø§Ùر", 220, 24, 15, 7.5, 1), 148 researchedFood("chicken-grilled-skin", "ÙØ±Ø§Ø® Ù Ø´ÙÙØ© Ø¨Ø§ÙØ¬Ùد - ÙØÙ Ù Ø®ØªÙØ·", 100, "g", 239, 27, 0, 14, 0, "protein", "USDA FoodData Central", "Ù ØªÙØ³Ø· ÙØÙ Ø§ÙÙØ±Ø§Ø® اÙÙ Ø´Ù٠٠ع Ø§ÙØ¬ÙØ¯Ø Ø§ÙÙØ·Ø¹Ø© اÙ٠ختارة تؤثر عÙÙ Ø§ÙØ¯ÙÙÙ."), 149 egyptianFood("egy-chicken-oven-noskin", "ÙØ±Ø§Ø® Ù٠اÙÙØ±Ù بدÙÙ Ø¬ÙØ¯", 185, 28, 0, 7.5, 0), 150 egyptianFood("egy-chicken-boiled-noskin", "ÙØ±Ø§Ø® ٠سÙÙÙØ© بدÙÙ Ø¬ÙØ¯", 170, 28, 0, 6, 0), 151 egyptianFood("egy-chicken-potato-stew", "ÙØ±Ø§Ø® ٠ع بطاطس Ù Ø·Ø¨ÙØ®Ø© Ø¨Ø§ÙØµÙصة", 145, 10, 14, 5.5, 2), 152 egyptianFood("egy-chicken-shawarma", "Ø´Ø§ÙØ±Ù ا ÙØ±Ø§Ø® Ù ÙØ²ÙÙØ©", 210, 24, 4, 11, 0.5), 153 egyptianFood("egy-chicken-kofta", "ÙÙØªØ© ÙØ±Ø§Ø®", 190, 22, 5, 9, 0.5), 154 egyptianFood("egy-shish-tawook", "Ø´ÙØ´ طاÙÙÙ Ù Ø´ÙÙ", 175, 27, 3, 6, 0.3), 155 researchedFood("fish-tilapia-grilled", "Ø³Ù Ù Ø¨ÙØ·Ù Ù Ø´ÙÙ", 100, "g", 128, 26, 0, 2.7, 0, "protein", "USDA FoodData Central", "اÙÙÙÙ Ø© ÙÙÙ 100 ج٠٠٠اÙÙØÙ Ø§ÙÙ Ø·Ù٠دÙÙ Ø²ÙØª إضاÙÙ."), 156 egyptianFood("egy-tilapia-fried", "Ø³Ù Ù Ø¨ÙØ·Ù Ù ÙÙÙ", 215, 23, 8, 10.5, 0.5), 157 egyptianFood("egy-mullet-grilled", "Ø³Ù Ù Ø¨ÙØ±Ù Ù Ø´ÙÙ", 160, 24, 0, 7, 0), 158 egyptianFood("egy-fish-sayadiya", "Ø³Ù Ù ØµÙØ§Ø¯ÙØ© Ø¨Ø§ÙØ£Ø±Ø²", 175, 12, 25, 3.5, 1), 159 egyptianFood("egy-green-beans", "ÙØ§ØµÙÙÙØ§ خضراء Ø¨Ø§ÙØµÙصة", 80, 2.5, 10, 3.5, 3.5), 160 egyptianFood("egy-zucchini-tomato", "ÙÙØ³Ø© Ù Ø·Ø¨ÙØ®Ø© Ø¨Ø§ÙØµÙصة", 65, 2, 7, 3.5, 2), 161 egyptianFood("egy-white-beans", "ÙØ§ØµÙÙÙØ§ Ø¨ÙØ¶Ø§Ø¡ Ø¨Ø§ÙØµÙصة", 125, 7, 20, 2.5, 6), 162 egyptianFood("egy-pasta-red-sauce", "Ù ÙØ±ÙÙØ© Ø¨Ø§ÙØµÙصة Ø§ÙØÙ Ø±Ø§Ø¡", 150, 5, 28, 2.5, 2), 163 egyptianFood("egy-shakshuka", "Ø´ÙØ´ÙÙØ© Ù ØµØ±ÙØ©", 110, 6, 5, 7, 1.5), 164 egyptianFood("egy-eggs-pastrami", "Ø¨ÙØ¶ Ø¨Ø§ÙØ¨Ø³Ø·Ø±Ù Ø©", 210, 15, 3, 15, 0), 165 egyptianFood("egy-liver-grilled", "ÙØ¨Ø¯Ø© Ù Ø´ÙÙØ©", 175, 26, 3, 6, 0), 166 egyptianFood("egy-beef-boiled", "ÙØÙ Ø© ٠سÙÙÙØ©", 230, 26, 0, 14, 0), 167 egyptianFood("egy-beef-vegetable-stew", "ÙØ®ÙØ© ÙØÙ Ø© Ø¨Ø§ÙØ®Ø¶Ø§Ø±", 150, 14, 7, 7, 2), 168 egyptianFood("egy-rice-kofta", "ÙÙØªØ© أرز", 195, 11, 20, 8, 1), 169 egyptianFood("egy-okra-plain", "Ø¨Ø§Ù ÙØ© سادة Ù Ø·Ø¨ÙØ®Ø© - Ø²ÙØª Ù ØªÙØ³Ø·", 75, 2, 8, 4, 3), 170 egyptianFood("egy-okra-meat", "Ø¨Ø§Ù ÙØ© باÙÙØÙ Ø©", 115, 7, 7, 7, 2.5), 171 egyptianFood("egy-peas-carrots", "Ø¨Ø³ÙØ© Ø¨Ø§ÙØ¬Ø²Ø± - Ø²ÙØª Ù ØªÙØ³Ø·", 95, 4, 15, 2.5, 4), 172 egyptianFood("egy-peas-meat", "Ø¨Ø³ÙØ© باÙÙØÙ Ø©", 125, 8, 12, 5, 3.5), 173 egyptianFood("egy-taro", "ÙÙÙØ§Ø³ Ø¨Ø§ÙØ³ÙÙ", 110, 2.5, 18, 3.5, 3), 174 egyptianFood("egy-molokhia", "Ù ÙÙØ®ÙØ© Ù ØµØ±ÙØ© Ø¨Ø§ÙØªÙÙÙØ© - Ø²ÙØª Ù ØªÙØ³Ø·", 70, 3, 7, 4, 2.5), 175 egyptianFood("egy-spinach", "Ø³Ø¨Ø§ÙØ® Ù Ø·Ø¨ÙØ®Ø© Ø¨Ø§ÙØ·Ù اط٠", 65, 3, 7, 3, 3), 176 egyptianFood("egy-moussaka", "Ù Ø³ÙØ¹Ø© Ù ØµØ±ÙØ©", 120, 3, 12, 7, 4), 177 egyptianFood("egy-moussaka-meat", "Ù Ø³ÙØ¹Ø© باÙÙØÙ Ø© اÙÙ ÙØ±ÙÙ Ø©", 155, 8, 10, 10, 3), 178 egyptianFood("egy-mahshi-grape", "Ù ØØ´Ù ÙØ±Ù Ø¹ÙØ¨", 165, 3.5, 25, 6, 3), 179 egyptianFood("egy-mahshi-cabbage", "Ù ØØ´Ù ÙØ±Ùب", 145, 3.5, 23, 5, 3), 180 egyptianFood("egy-mahshi-zucchini", "Ù ØØ´Ù ÙÙØ³Ø©", 130, 3, 20, 4.5, 2.5), 181 egyptianFood("egy-mahshi-eggplant", "Ù ØØ´Ù Ø¨Ø§Ø°ÙØ¬Ø§Ù", 135, 3, 20, 5, 3), 182 egyptianFood("egy-mahshi-pepper", "Ù ØØ´Ù ÙÙÙÙ", 130, 3, 20, 4.5, 2.5), 183 egyptianFood("egy-mahshi-mixed", "Ù ØØ´Ù Ù Ø´ÙÙ", 145, 3.5, 23, 5, 3), 184 egyptianFood("egy-mombar", "٠٠بار (Ù ÙØ¨Ø§Ø±)", 240, 10, 22, 13, 1.5), 185 egyptianFood("egy-macaroni-bechamel", "Ù ÙØ±ÙÙØ© بشا٠ÙÙ", 200, 8, 23, 8.5, 1.5), 186 egyptianFood("egy-lasagna-meat", "ÙØ§Ø²Ø§ÙÙØ§ باÙÙØÙ Ø©", 190, 10, 21, 8, 1.5), 187 egyptianFood("egy-roqaq-meat", "Ø±ÙØ§Ù باÙÙØÙ Ø©", 260, 10, 28, 12, 1), 188 egyptianFood("egy-fatta-meat", "ÙØªØ© Ù ØµØ±ÙØ© باÙÙØÙ Ø©", 220, 10, 27, 8, 1), 189 egyptianFood("egy-kebab-halla", "ÙØ¨Ø§Ø¨ ØÙØ©", 180, 20, 4, 9, 1), 190 egyptianFood("egy-dawood-basha", "ÙÙØªØ© Ø¯Ø§ÙØ¯ باشا", 185, 15, 8, 10, 1.5), 191 egyptianFood("egy-liver-alex", "ÙØ¨Ø¯Ø© إسÙÙØ¯Ø±Ø§ÙÙ - Ø²ÙØª Ù ØªÙØ³Ø·", 185, 22, 4, 9, 1), 192 egyptianFood("egy-liver-onion", "ÙØ¨Ø¯Ø© Ù Ø´ÙØØ© Ø¨Ø§ÙØ¨ØµÙ", 175, 23, 5, 7.5, 1), 193 egyptianFood("egy-sausage-alex", "سج٠إسÙÙØ¯Ø±Ø§ÙÙ", 280, 15, 7, 22, 1.5), 194 egyptianFood("egy-hawawshi", "ØÙØ§ÙØ´Ù", 275, 13, 28, 12, 2), 195 egyptianFood("egy-potato-meat", "صÙÙÙØ© بطاطس باÙÙØÙ Ø©", 145, 8, 15, 6, 2), 196 egyptianFood("egy-potato-chicken", "صÙÙÙØ© بطاطس باÙÙØ±Ø§Ø®", 155, 11, 14, 6.5, 2), 197 egyptianFood("egy-orzo-soup", "Ø´ÙØ±Ø¨Ø© ÙØ³Ø§Ù عصÙÙØ±", 85, 2.5, 13, 2.5, 1), 198 egyptianFood("egy-lentil-soup", "Ø´ÙØ±Ø¨Ø© عدس Ù ØµØ±ÙØ©", 105, 5, 17, 2.5, 4), 199 egyptianFood("egy-rice-vermicelli", "رز Ø¨Ø§ÙØ´Ø¹Ø±ÙØ© Ù Ø·Ø¨ÙØ®", 155, 3, 30, 2.5, 0.7), 200 egyptianFood("egy-roz-moammar", "أرز ٠ع٠ر", 190, 5, 25, 8, 0.5), 201 egyptianFood("egy-bessara", "بصارة", 115, 6, 14, 4, 5), 202 egyptianFood("egy-fava-oil", "ÙÙÙ Ø¨Ø§ÙØ²Ùت ÙØ§ÙÙÙÙ ÙÙ", 145, 7.5, 18, 5, 5), 203 egyptianFood("egy-pigeon-stuffed", "ØÙ Ø§Ù Ù ØØ´Ù", 240, 17, 13, 13, 0.5), 204 egyptianFood("egy-trotters", "ÙÙØ§Ø±Ø¹ Ù Ø·Ø¨ÙØ®Ø©", 220, 25, 2, 13, 0), 205 food("base-tea", "شا٠سادة بدÙÙ Ø³ÙØ±", 1, "cup", 2, 0, 0.5, 0, 0, "drinks"), 206 researchedFood("drink-tea-milk", "Ø´Ø§Ù Ø¨ÙØ¨Ù - بدÙÙ Ø³ÙØ±", 1, "cup", 52, 3.4, 5.3, 1.8, 0, "drinks", "ØªÙØ¯Ùر ٠٠شا٠سادة + 100 Ù Ù ÙØ¨Ù ÙØµÙ دس٠", "اÙÙÙØ¨ Ù ØØ³Ùب بشا٠Ù٠اء ٠ع ÙØÙ 100 Ù Ù ÙØ¨Ù ÙØµÙ دس٠. اختر Ù ÙØ§Ø¹Ù Ø§ÙØ³Ùر Ø¹ÙØ¯ تسجÙ٠اÙÙ Ø´Ø±ÙØ¨ ÙØªÙضا٠تÙÙØ§Ø¦ÙÙØ§."), 207 food("base-instant-coffee", "ÙØ³ÙاÙÙ٠سادة بدÙÙ Ø¥Ø¶Ø§ÙØ§Øª", 1, "cup", 4, 0.2, 0.7, 0, 0, "drinks"), 208 food("base-sugar-tsp", "Ø³ÙØ± Ø£Ø¨ÙØ¶ - Ù ÙØ¹ÙØ© ØµØºÙØ±Ø©", 1, "serving", 16, 0, 4, 0, 0, "drinks"), 209 food("base-koshari", "ÙØ´Ø±Ù Ù ØªÙØ³Ø· بدÙÙ Ø¥Ø¶Ø§ÙØ§Øª زائدة", 100, "g", 170, 6, 31, 2.5, 4, "meals"), 210 food("base-falafel", "Ø·Ø¹Ù ÙØ© Ù ÙÙÙØ© Ù ØªÙØ³Ø·Ø©", 1, "piece", 65, 2.5, 5.5, 3.8, 1.5, "meals"), 211 food("base-grilled-kofta", "ÙÙØªØ© Ù Ø´ÙÙØ©", 100, "g", 245, 23, 2, 16, 0, "meals") 212]; 213 214function food(id, name, basis, unit, calories, protein, carbs, fat, fiber, categoryId, favorite = false) { 215 return { 216 id, name, basis, unit, calories, protein, carbs, fat, fiber, 217 categoryId, favorite, custom: false, brand: "", notes: "", source: "ÙØ§Ø¹Ø¯Ø© Ø§ÙØ¨Ø±Ùا٠ج" 218 }; 219} 220 221function researchedFood(id, name, basis, unit, calories, protein, carbs, fat, fiber, categoryId, source, notes = "", favorite = false) { 222 const item = food(id, name, basis, unit, calories, protein, carbs, fat, fiber, categoryId, favorite); 223 item.source = source; 224 item.notes = notes; 225 return item; 226} 227 228function fruitFood(id, name, calories, protein, carbs, fat, fiber) { 229 return researchedFood( 230 id, name, 100, "g", calories, protein, carbs, fat, fiber, "fruits", 231 "USDA FoodData Central", 232 "اÙÙÙÙ ÙÙÙ 100 Ø¬Ù Ù Ù Ø§ÙØ¬Ø²Ø¡ اÙ٠أÙÙÙØ ٠٠دÙ٠اÙÙØ´Ø± Ø£Ù Ø§ÙØ¨Ø°Ùر ØºÙØ± اÙ٠أÙÙÙØ© ØÙØ« ÙÙØ·Ø¨Ù." 233 ); 234} 235 236function egyptianFood(id, name, calories, protein, carbs, fat, fiber) { 237 const item = food(id, name, 100, "g", calories, protein, carbs, fat, fiber, "egyptian"); 238 item.notes = "ØªÙØ¯Ùر Ù ÙØ²ÙÙ Ù ØªÙØ³Ø· ÙÙÙ 100 Ø¬Ù Ø ÙØ¯ ÙØªØºÙر Ø¨ÙØÙ 20â35% ØØ³Ø¨ Ø§ÙØ²Ùت ÙØ§Ùس٠٠ÙÙØ³Ø¨Ø© اÙÙØÙ Ø© ÙØ§Ù٠اء ÙØ·Ø±ÙÙØ© Ø§ÙØªØ³ÙÙØ©. Ø§Ø³ØªØ®Ø¯Ù ÙØµÙØ© ٠خصصة Ù٠تعر٠٠ÙÙÙØ§Øª طبختÙ."; 239 item.source = "ÙØµÙØ© Ù ØµØ±ÙØ© Ù Ø¹ÙØ§Ø±ÙØ© + ٠راجع Ù ÙÙÙØ§Øª"; 240 return item; 241} 242 243const initialState = { 244 version: 7, 245 updatedAt: new Date().toISOString(), 246 theme: "light", 247 profile: { 248 age: 19, 249 sex: "unspecified", 250 height: 168, 251 weight: 120, 252 workouts: 4, 253 dailyActivity: "sedentary" 254 }, 255 goalProfiles: { 256 default: { calories: 2400, protein: 160, carbs: 250, fat: 80, water: 3 }, 257 training: { calories: 2500, protein: 170, carbs: 280, fat: 75, water: 3.5 }, 258 rest: { calories: 2300, protein: 170, carbs: 210, fat: 80, water: 3 } 259 }, 260 dayModes: {}, 261 dayGoalOverrides: {}, 262 categories: defaultCategories, 263 foods: defaultFoods, 264 logs: {}, 265 water: {}, 266 weights: [{ id: makeId(), date: todayISO(), value: 120, note: "اÙÙØ²Ù اÙ٠بدئÙ" }] 267}; 268 269let state = loadState(); 270let deferredInstallPrompt = null; 271let toastTimer = null; 272let cloudSyncTimer = null; 273let isApplyingRemote = false;
274let compareIds = []; 275let usdaSearchResults = []; 276let lastCalculatorResult = null; 277let mealDraftEntries = []; 278let customDrinkTarget = "log"; 279let cloudConfig = loadJson(CLOUD_CONFIG_KEY, { url: "", anonKey: "" }); 280let cloudSession = loadJson(CLOUD_SESSION_KEY, null); 281 282const $ = (selector) => document.querySelector(selector); 283const $$ = (selector) => [...document.querySelectorAll(selector)]; 284 285function makeId() { 286 return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`; 287} 288 289function todayISO() { 290 const d = new Date(); 291 const offset = d.getTimezoneOffset(); 292 return new Date(d.getTime() - offset * 60_000).toISOString().slice(0, 10); 293} 294 295function addDays(dateISO, amount) { 296 const d = new Date(`${dateISO}T12:00:00`); 297 d.setDate(d.getDate() + amount); 298 return d.toISOString().slice(0, 10); 299} 300 301function formatNumber(value, maximumFractionDigits = 0) { 302 return new Intl.NumberFormat("ar-EG", { maximumFractionDigits }).format(Number(value) || 0); 303} 304 305function formatDate(dateISO, options = { weekday: "long", day: "numeric", month: "long" }) { 306 return new Intl.DateTimeFormat("ar-EG", options).format(new Date(`${dateISO}T12:00:00`)); 307} 308 309function deepClone(value) { 310 return JSON.parse(JSON.stringify(value)); 311} 312 313function escapeHtml(value) { 314 return String(value ?? "") 315 .replaceAll("&", "&") 316 .replaceAll("<", "<") 317 .replaceAll(">", ">") 318 .replaceAll('"', """) 319 .replaceAll("'", "'"); 320} 321 322function optionalNumber(value) { 323 if (value === "" || value === null || value === undefined) return null; 324 const num = Number(value); 325 return Number.isFinite(num) ? num : null; 326} 327 328function loadJson(key, fallback) { 329 try { 330 const raw = localStorage.getItem(key); 331 return raw ? JSON.parse(raw) : fallback; 332 } catch { 333 return fallback; 334 } 335} 336 337function loadState() { 338 try { 339 const raw = localStorage.getItem(STORAGE_KEY); 340 if (!raw) return deepClone(initialState); 341 return normalizeState(JSON.parse(raw)); 342 } catch (error) { 343 console.error("Could not load saved data", error); 344 return deepClone(initialState); 345 } 346} 347 348function normalizeState(saved) { 349 const merged = deepClone(initialState); 350 const previousVersion = Number(saved?.version) || 1; 351 merged.version = 7; 352 merged.updatedAt = saved.updatedAt || new Date().toISOString(); 353 merged.theme = saved.theme === "dark" ? "dark" : "light"; 354 merged.profile = { ...merged.profile, ...(saved.profile || {}) }; 355 356 if (saved.goalProfiles) { 357 merged.goalProfiles = { 358 default: { ...merged.goalProfiles.default, ...(saved.goalProfiles.default || {}) }, 359 training: { ...merged.goalProfiles.training, ...(saved.goalProfiles.training || {}) }, 360 rest: { ...merged.goalProfiles.rest, ...(saved.goalProfiles.rest || {}) } 361 }; 362 } else if (saved.goals) { 363 merged.goalProfiles.default = { ...merged.goalProfiles.default, ...saved.goals }; 364 } 365 366 merged.dayModes = saved.dayModes && typeof saved.dayModes === "object" ? saved.dayModes : {}; 367 merged.dayGoalOverrides = saved.dayGoalOverrides && typeof saved.dayGoalOverrides === "object" ? saved.dayGoalOverrides : {}; 368 merged.logs = saved.logs && typeof saved.logs === "object" ? saved.logs : {}; 369 merged.water = saved.water && typeof saved.water === "object" ? saved.water : {}; 370 merged.weights = Array.isArray(saved.weights) ? saved.weights : merged.weights; 371 372 // اÙÙØ³Ø®ØªØ§Ù Ø§ÙØ³Ø§Ø¨ÙØªØ§Ù ÙØ§Ùتا تØÙظا٠اÙ٠اء بعدد Ø§ÙØ£ÙÙØ§Ø¨. Ù Ù Ø§ÙØ¥ØµØ¯Ø§Ø± 3 Ø£ØµØ¨ØØª اÙÙØØ¯Ø© ÙØªØ±. 373 if (previousVersion < 3) { 374 const minimumTargets = { default: 3, training: 3.5, rest: 3 }; 375 Object.keys(merged.goalProfiles).forEach((profileName) => { 376 const oldValue = Number(merged.goalProfiles[profileName].water) || 0; 377 const liters = oldValue > 5 ? oldValue / 4 : oldValue; 378 merged.goalProfiles[profileName].water = Math.max(minimumTargets[profileName] || 3, liters || 0); 379 }); 380 Object.keys(merged.dayGoalOverrides).forEach((date) => { 381 const oldValue = Number(merged.dayGoalOverrides[date]?.water) || 0; 382 if (oldValue) merged.dayGoalOverrides[date].water = oldValue > 5 ? oldValue / 4 : oldValue; 383 }); 384 merged.water = Object.fromEntries(Object.entries(merged.water).map(([date, value]) => [date, Math.round(((Number(value) || 0) / 4) * 100) / 100])); 385 } 386 387 const savedCategories = Array.isArray(saved.categories) ? saved.categories : []; 388 const customCategories = savedCategories.filter((category) => category.custom && !defaultCategories.some((base) => base.id === category.id)); 389 merged.categories = defaultCategories.map((base) => ({ ...base, ...(savedCategories.find((category) => category.id === base.id) || {}) })).concat(customCategories); 390 391 const savedFoods = Array.isArray(saved.foods) ? saved.foods : []; 392 const customFoods = savedFoods.filter((item) => item.custom).map(normalizeFood); 393 const savedBaseMap = new Map(savedFoods.filter((item) => !item.custom).map((item) => [item.id, item])); 394 merged.foods = defaultFoods.map((base) => { 395 const savedBase = savedBaseMap.get(base.id) || {}; 396 return normalizeFood({ ...base, favorite: savedBase.favorite ?? base.favorite }); 397 }).concat(customFoods); 398 return merged; 399} 400 401function normalizeFood(item) { 402 return { 403 id: item.id || makeId(), 404 name: item.name || "طعا٠بدÙ٠اس٠", 405 basis: Number(item.basis) > 0 ? Number(item.basis) : 100, 406 unit: unitLabels[item.unit] ? item.unit : "g", 407 calories: Number(item.calories) || 0, 408 protein: optionalNumber(item.protein), 409 carbs: optionalNumber(item.carbs), 410 fat: optionalNumber(item.fat), 411 fiber: optionalNumber(item.fiber), 412 categoryId: item.categoryId || guessCategory(item.name), 413 favorite: Boolean(item.favorite), 414 custom: Boolean(item.custom), 415 brand: item.brand || "", 416 notes: item.notes || "", 417 source: item.source || (item.custom ? "أضاÙ٠اÙ٠ستخد٠" : "ÙØ§Ø¹Ø¯Ø© Ø§ÙØ¨Ø±Ùا٠ج"), 418 sourceId: item.sourceId || "" 419 }; 420} 421 422function guessCategory(name = "") { 423 const text = name.toLowerCase(); 424 if (/ÙØ±Ø§Ø®|ÙØÙ |تÙÙØ©|Ø¨ÙØ¶|س٠Ù|سÙÙ ÙÙ|ج٠برÙ|ÙÙØªØ©/.test(text)) return "protein"; 425 if (/جبÙ|زبادÙ|ÙØ¨Ù/.test(text)) return "dairy"; 426 if (/رز|أرز|Ø¹ÙØ´|ØªÙØ³Øª|Ù ÙØ±ÙÙØ©|بطاط|Ø´ÙÙØ§Ù|برغÙ/.test(text)) return "carbs"; 427 if (/ÙÙÙ|عدس|ØÙ ص/.test(text)) return "legumes"; 428 if (/Ø®ÙØ§Ø±|ط٠اط٠|Ø³ÙØ·Ø©|خس|برÙÙÙÙ/.test(text)) return "vegetables"; 429 if (/Ù ÙØ²|ØªÙØ§Ø|Ø¨Ø±ØªÙØ§Ù|ÙØ±Ø§ÙÙØ©|ت٠ر/.test(text)) return "fruits"; 430 if (/Ø²ÙØª|ÙÙØ²|Ø³ÙØ¯Ø§ÙÙ|Ù ÙØ³Ø±Ø§Øª/.test(text)) return "fats"; 431 if (/شاÙ|ÙÙÙØ©|ÙØ³ÙاÙÙÙ|Ø¥Ø³Ø¨Ø±ÙØ³Ù|ا٠رÙÙØ§ÙÙ|أ٠رÙÙØ§ÙÙ|أعشاب|Ù Ø´Ø±ÙØ¨/.test(text)) return "drinks"; 432 if (/Ø¨Ø§Ù ÙØ©|Ø¨Ø³ÙØ©|ÙÙÙØ§Ø³|Ù ÙÙØ®ÙØ©|Ù ØØ´Ù|٠٠بار|Ù ÙØ¨Ø§Ø±|بشا٠ÙÙ|ÙØ§Ø²Ø§ÙÙØ§|Ø±ÙØ§Ù|ÙØªØ©|ÙØ¨Ø§Ø¨ ØÙØ©|Ø¯Ø§ÙØ¯ باشا|ÙØ¨Ø¯Ø©|سجÙ|ØÙØ§ÙØ´Ù|صÙÙÙØ© بطاطس|ÙØ³Ø§Ù عصÙÙØ±|أرز ٠ع٠ر|رز ٠ع٠ر|رز ØµÙØ§Ø¯ÙØ©|رز ÙØ´Ø±Ù|رز Ø¨Ø§ÙØ·Ù اط٠|بصارة|ØÙ Ø§Ù Ù ØØ´Ù|ÙÙØ§Ø±Ø¹/.test(text)) return "egyptian"; 433 return "uncategorized"; 434} 435 436function saveState({ scheduleSync = true } = {}) { 437 state.updatedAt = new Date().toISOString(); 438 localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); 439 updateStorageStatus(); 440 if (scheduleSync && !isApplyingRemote) scheduleCloudPush(); 441} 442 443function showToast(message) { 444 const toast = $("#toast"); 445 toast.textContent = message; 446 toast.classList.add("show"); 447 clearTimeout(toastTimer); 448 toastTimer = setTimeout(() => toast.classList.remove("show"), 2800); 449} 450 451function getFood(foodId) { 452 return state.foods.find((item) => item.id === foodId); 453} 454 455function getCategory(categoryId) { 456 return state.categories.find((item) => item.id === categoryId) || state.categories.find((item) => item.id === "uncategorized"); 457} 458 459function calculateNutrition(item, quantity) { 460 const factor = Number(quantity) / Number(item?.basis || 1); 461 return { 462 calories: (Number(item?.calories) || 0) * factor, 463 protein: (Number(item?.protein) || 0) * factor, 464 carbs: (Number(item?.carbs) || 0) * factor, 465 fat: (Number(item?.fat) || 0) * factor, 466 fiber: (Number(item?.fiber) || 0) * factor 467 }; 468} 469 470function getEntries(dateISO) { 471 return Array.isArray(state.logs[dateISO]) ? state.logs[dateISO] : []; 472} 473 474function getDayTotals(dateISO) { 475 return getEntries(dateISO).reduce((total, entry) => {
476 const item = getFood(entry.foodId) || entry.foodSnapshot; 477 if (!item) return total; 478 const nutrition = calculateNutrition(item, entry.quantity); 479 Object.keys(total).forEach((key) => { total[key] += nutrition[key] || 0; }); 480 return total; 481 }, { calories: 0, protein: 0, carbs: 0, fat: 0, fiber: 0 }); 482} 483 484function getGoalsForDate(dateISO) { 485 if (state.dayGoalOverrides[dateISO]) { 486 return { ...state.dayGoalOverrides[dateISO], mode: "custom", label: "ÙØ¯Ù ٠خصص" }; 487 } 488 const mode = state.dayModes[dateISO] || "default"; 489 const labels = { default: "اÙÙØ¯Ù Ø§ÙØ£Ø³Ø§Ø³Ù", training: "ÙÙ٠ت٠رÙÙ", rest: "ÙÙÙ Ø±Ø§ØØ©" }; 490 return { ...state.goalProfiles[mode], mode, label: labels[mode] || labels.default }; 491} 492 493function percentage(value, goal) { 494 if (!goal) return 0; 495 return Math.max(0, Math.min(100, (value / goal) * 100)); 496} 497 498function setProgress(element, value) { 499 if (element) element.style.width = `${Math.min(100, Math.max(0, value))}%`; 500} 501 502function applyTheme() { 503 document.documentElement.dataset.theme = state.theme; 504 $("#themeToggle").textContent = state.theme === "dark" ? "اÙÙØ¶Ø¹ اÙÙØ§ØªØ" : "اÙÙØ¶Ø¹ Ø§ÙØ¯Ø§ÙÙ"; 505} 506 507function switchView(view) { 508 $$(".view").forEach((section) => section.classList.toggle("active-view", section.id === `${view}View`)); 509 $$('[data-view]').forEach((button) => button.classList.toggle("active", button.dataset.view === view)); 510 $("#pageTitle").textContent = viewTitles[view] || "My Diet Tracker"; 511 window.scrollTo({ top: 0, behavior: "smooth" }); 512 if (view === "mealplanner") renderMealPlanner(); 513 if (view === "explore") renderExplore(); 514 if (view === "calculator") populateCalculatorFromProfile(false); 515 if (view === "progress") renderProgress(); 516 if (view === "settings") renderSettings(); 517} 518 519function renderDashboard() { 520 const date = todayISO(); 521 const totals = getDayTotals(date); 522 const goals = getGoalsForDate(date); 523 const remaining = goals.calories - totals.calories; 524 const consumedPercent = percentage(totals.calories, goals.calories); 525 526 $("#heroRemaining").textContent = formatNumber(Math.abs(remaining)); 527 $("#heroRemainingLabel").textContent = remaining >= 0 ? " سعر ٠تبÙÙ" : " سعر ÙÙ٠اÙÙØ¯Ù"; 528 $("#heroSummary").textContent = totals.calories 529 ? `Ø£ÙÙØª ${formatNumber(totals.calories)} Ù Ù ${formatNumber(goals.calories)} سعر اÙÙÙÙ .` 530 : "ابدأ بتسجÙ٠أÙÙ ÙØ¬Ø¨Ø© اÙÙÙÙ ."; 531 $("#dayModeBadge").textContent = goals.label; 532 $("#ringCalories").textContent = formatNumber(totals.calories); 533 $("#ringGoal").textContent = formatNumber(goals.calories); 534 $("#calorieRing").style.setProperty("--percent", consumedPercent); 535 536 renderMacroStat("protein", totals.protein, goals.protein); 537 renderMacroStat("carbs", totals.carbs, goals.carbs); 538 renderMacroStat("fat", totals.fat, goals.fat); 539 540 const water = Number(state.water[date]) || 0; 541 $("#waterStat").textContent = `${formatNumber(water, 2)} / ${formatNumber(goals.water, 2)} ÙØªØ±`; 542 setProgress($("#waterBar"), percentage(water, goals.water)); 543 544 $("#todayMeals").innerHTML = renderMealGroups(date); 545 renderWeekSummary(); 546 renderRemainingSuggestions(); 547 renderDashboardEnergy(); 548} 549 550function renderMacroStat(key, value, goal) { 551 const ids = { protein: ["proteinStat", "proteinBar"], carbs: ["carbsStat", "carbsBar"], fat: ["fatStat", "fatBar"] }; 552 $(`#${ids[key][0]}`).textContent = `${formatNumber(value, 1)} / ${formatNumber(goal)} ج٠`; 553 setProgress($(`#${ids[key][1]}`), percentage(value, goal)); 554} 555 556function renderMealGroups(dateISO) { 557 const entries = getEntries(dateISO); 558 if (!entries.length) return `<div class="empty-state"><strong>ÙØ§ ØªÙØ¬Ø¯ ÙØ¬Ø¨Ø§Øª Ù Ø³Ø¬ÙØ©</strong><span>أض٠أÙÙ Ø·Ø¹Ø§Ù ÙØªØ¨Ø¯Ø£ ØØ³Ø§Ø¨ ÙÙÙ Ù.</span></div>`; 559 560 return Object.keys(mealLabels).map((mealType) => { 561 const mealEntries = entries.filter((entry) => entry.mealType === mealType); 562 if (!mealEntries.length) return ""; 563 const mealCalories = mealEntries.reduce((sum, entry) => {
564 const item = getFood(entry.foodId) || entry.foodSnapshot; 565 return sum + calculateNutrition(item, entry.quantity).calories; 566 }, 0); 567 const rows = mealEntries.map((entry) => { 568 const item = getFood(entry.foodId) || entry.foodSnapshot; 569 if (!item) return ""; 570 const nutrition = calculateNutrition(item, entry.quantity); 571 const incomplete = item.protein === null || item.carbs === null || item.fat === null; 572 return `<div class="food-entry"> 573 <div class="food-entry-info"><strong>${escapeHtml(item.name)}</strong><span>${formatNumber(entry.quantity, 1)} ${unitLabels[item.unit] || item.unit}${incomplete ? " ⢠اÙÙ Ø§ÙØ±Ùز ØºÙØ± Ù ÙØªÙ ÙØ©" : ` ⢠${formatNumber(nutrition.protein, 1)} Ø¬Ù Ø¨Ø±ÙØªÙÙ`}</span></div> 574 <span class="entry-kcal">${formatNumber(nutrition.calories)} سعر</span> 575 <button class="icon-button" data-delete-log="${entry.id}" data-date="${dateISO}" aria-label="ØØ°Ù">Ã</button> 576 </div>`; 577 }).join(""); 578 return `<section class="meal-group"><header class="meal-group-header"><h4>${mealLabels[mealType]}</h4><span>${formatNumber(mealCalories)} سعر</span></header>${rows}</section>`; 579 }).join(""); 580} 581 582function renderWeekSummary() { 583 const today = todayISO(); 584 const dates = Array.from({ length: 7 }, (_, index) => addDays(today, index - 6)); 585 const values = dates.map((date) => getDayTotals(date).calories); 586 const logged = values.filter((value) => value > 0); 587 const average = logged.length ? logged.reduce((sum, value) => sum + value, 0) / logged.length : 0; 588 const max = Math.max(...values, ...dates.map((date) => getGoalsForDate(date).calories), 1); 589 590 $("#weeklyAverage").textContent = formatNumber(average); 591 $("#daysLogged").textContent = formatNumber(logged.length); 592 const sortedWeights = [...state.weights].sort((a, b) => a.date.localeCompare(b.date)); 593 $("#latestWeight").textContent = sortedWeights.length ? `${formatNumber(sortedWeights.at(-1).value, 1)} ÙØ¬Ù ` : "â"; 594 595 $("#weekBars").innerHTML = dates.map((date, index) => { 596 const height = Math.max(3, (values[index] / max) * 145); 597 const label = new Intl.DateTimeFormat("ar-EG", { weekday: "short" }).format(new Date(`${date}T12:00:00`)); 598 return `<div class="week-bar"><b>${values[index] ? formatNumber(values[index]) : "â"}</b><i style="height:${height}px"></i><span>${label}</span></div>`; 599 }).join(""); 600} 601 602function renderRemainingSuggestions() { 603 const date = todayISO(); 604 const totals = getDayTotals(date); 605 const goals = getGoalsForDate(date); 606 const remainingCalories = Math.max(0, goals.calories - totals.calories); 607 const remainingProtein = Math.max(0, goals.protein - totals.protein); 608 const candidates = state.foods 609 .filter((item) => item.categoryId !== "drinks" && item.calories > 0 && item.calories <= Math.max(remainingCalories, 150) * 1.1) 610 .map((item) => { 611 const nutrition = calculateNutrition(item, item.basis); 612 const proteinScore = remainingProtein > 0 ? Math.min(nutrition.protein, remainingProtein) * 8 : nutrition.protein * 2; 613 const calorieFit = remainingCalories > 0 ? 100 - Math.min(100, Math.abs(remainingCalories - nutrition.calories) / Math.max(remainingCalories, 1) * 100) : 0; 614 return { item, nutrition, score: proteinScore + calorieFit + (item.favorite ? 15 : 0) }; 615 }) 616 .sort((a, b) => b.score - a.score) 617 .slice(0, 4); 618 619 if (!candidates.length || remainingCalories <= 0) { 620 $("#remainingSuggestions").innerHTML = `<div class="empty-state"><strong>${remainingCalories <= 0 ? "ÙØµÙت ÙÙØ¯Ù Ø§ÙØ³Ø¹Ø±Ø§Øª" : "ÙØ§ ØªÙØ¬Ø¯ Ø§ÙØªØ±Ø§ØØ§Øª Ø§ÙØ¢Ù"}</strong><span>راجع ÙÙÙ Ù Ø£Ù Ø§Ø¨ØØ« ÙÙ ÙØ³Ù Ø§ÙØ§Ø³ØªÙشاÙ.</span></div>`; 621 return; 622 } 623 624 $("#remainingSuggestions").innerHTML = candidates.map(({ item, nutrition }) => `<div class="suggestion-item"><div><strong>${escapeHtml(item.name)}</strong><span>${formatNumber(nutrition.calories)} سعر ⢠${formatNumber(nutrition.protein, 1)} Ø¬Ù Ø¨Ø±ÙØªÙÙ ÙÙÙ ${formatNumber(item.basis, 1)} ${unitLabels[item.unit]}</span></div><button class="ghost-button compact" data-quick-add="${item.id}">Ø¥Ø¶Ø§ÙØ©</button></div>`).join(""); 625} 626 627function renderDashboardEnergy() { 628 const result = calculateProfileEnergy(); 629 if (!result) { 630 $("#dashboardEnergySummary").innerHTML = `<div><strong>â</strong><span>BMR</span></div><div><strong>â</strong><span>Ø§ÙØ«Ø¨Ø§Øª</span></div><div><strong>${formatNumber(getGoalsForDate(todayISO()).calories)}</strong><span>ÙØ¯Ù اÙÙÙÙ </span></div>`; 631 return; 632 } 633 $("#dashboardEnergySummary").innerHTML = `<div><strong>${formatNumber(result.bmr)}</strong><span>BMR</span></div><div><strong>${formatNumber(result.tdee)}</strong><span>
633ثبات ØªÙØ¯ÙرÙ</span></div><div><strong>${formatNumber(getGoalsForDate(todayISO()).calories)}</strong><span>ÙØ¯Ù اÙÙÙÙ </span></div>`; 634} 635 636function calculateProfileEnergy() { 637 const { sex, age, height, weight, workouts, dailyActivity } = state.profile; 638 if (!['male', 'female'].includes(sex)) return null; 639 let activity = 1.2; 640 if (dailyActivity === "sedentary") activity = workouts >= 5 ? 1.5 : workouts >= 3 ? 1.45 : workouts >= 1 ? 1.35 : 1.2; 641 if (dailyActivity === "light") activity = workouts >= 5 ? 1.6 : workouts >= 3 ? 1.52 : 1.4; 642 if (dailyActivity === "active") activity = workouts >= 5 ? 1.75 : workouts >= 3 ? 1.65 : 1.55; 643 const bmr = calculateBmr(sex, age, height, weight); 644 return { bmr, tdee: bmr * activity, activity }; 645} 646 647function populateFoodSelect(search = "", selectedId = null) { 648 const normalized = search.trim().toLowerCase(); 649 const foods = [...state.foods] 650 .filter((item) => item.categoryId !== "drinks") 651 .filter((item) => !normalized || `${item.name} ${item.brand}`.toLowerCase().includes(normalized)) 652 .sort((a, b) => Number(b.favorite) - Number(a.favorite) || a.name.localeCompare(b.name, "ar")); 653 654 $("#foodSelect").innerHTML = foods.length 655 ? foods.map((item) => `<option value="${item.id}">${item.favorite ? "â " : ""}${escapeHtml(item.name)} â ${formatNumber(item.calories, 1)} سعر/${formatNumber(item.basis, 1)} ${unitLabels[item.unit]}</option>`).join("") 656 : `<option value="">ÙØ§ ØªÙØ¬Ø¯ ÙØªÙجة â Ø£Ø¶Ù Ø§ÙØ·Ø¹Ø§Ù Ù Ù Ù ÙØªØ¨ØªÙ</option>`; 657 658 if (selectedId && foods.some((item) => item.id === selectedId)) $("#foodSelect").value = selectedId; 659 updateQuantityHint(); 660 updateNutritionPreview(); 661} 662 663function updateQuantityHint() { 664 const item = getFood($("#foodSelect").value); 665 $("#quantityHint").textContent = item ? `(اÙÙØØ¯Ø©: ${unitLabels[item.unit]}Ø Ø§ÙÙÙÙ Ø© Ø§ÙØ£Ø³Ø§Ø³ÙØ©: ${formatNumber(item.basis, 1)})` : ""; 666 if (item && !$("#foodQuantity").dataset.userEdited) $("#foodQuantity").value = item.basis; 667} 668 669function updateNutritionPreview() { 670 const item = getFood($("#foodSelect").value); 671 const quantity = Number($("#foodQuantity").value); 672 const nutrition = item && quantity > 0 ? calculateNutrition(item, quantity) : { calories: 0, protein: 0, carbs: 0, fat: 0 }; 673 $("#previewCalories").textContent = formatNumber(nutrition.calories, 1); 674 $("#previewProtein").textContent = item?.protein === null ? "ØºÙØ± ٠عرÙÙ" : `${formatNumber(nutrition.protein, 1)} ج٠`; 675 $("#previewCarbs").textContent = item?.carbs === null ? "ØºÙØ± ٠عرÙÙ" : `${formatNumber(nutrition.carbs, 1)} ج٠`; 676 $("#previewFat").textContent = item?.fat === null ? "ØºÙØ± ٠عرÙÙ" : `${formatNumber(nutrition.fat, 1)} ج٠`; 677} 678 679function populateDraftFoodSelect(search = "", selectedId = null) { 680 const normalized = search.trim().toLowerCase(); 681 const foods = [...state.foods] 682 .filter((item) => item.categoryId !== "drinks") 683 .filter((item) => !normalized || `${item.name} ${item.brand}`.toLowerCase().includes(normalized)) 684 .sort((a, b) => Number(b.favorite) - Number(a.favorite) || a.name.localeCompare(b.name, "ar")); 685 686 $("#draftFoodSelect").innerHTML = foods.length 687 ? foods.map((item) => `<option value="${item.id}">${item.favorite ? "â " : ""}${escapeHtml(item.name)} â ${formatNumber(item.calories, 1)} سعر/${formatNumber(item.basis, 1)} ${unitLabels[item.unit]}</option>`).join("") 688 : `<option value="">ÙØ§ ØªÙØ¬Ø¯ ÙØªÙجة</option>`; 689 690 if (selectedId && foods.some((item) => item.id === selectedId)) $("#draftFoodSelect").value = selectedId; 691 updateDraftQuantityHint(); 692 updateDraftNutritionPreview(); 693} 694 695function updateDraftQuantityHint() { 696 const item = getFood($("#draftFoodSelect").value); 697 $("#draftQuantityHint").textContent = item ? `(اÙÙØØ¯Ø©: ${unitLabels[item.unit]}Ø Ø§ÙÙÙÙ Ø© Ø§ÙØ£Ø³Ø§Ø³ÙØ©: ${formatNumber(item.basis, 1)})` : ""; 698 if (item && !$("#draftFoodQuantity").dataset.userEdited) $("#draftFoodQuantity").value = item.basis; 699} 700 701function updateDraftNutritionPreview() { 702 const item = getFood($("#draftFoodSelect").value); 703 const quantity = Number($("#draftFoodQuantity").value); 704 const nutrition = item && quantity > 0 ? calculateNutrition(item, quantity) : { calories: 0, protein: 0, carbs: 0, fat: 0 }; 705 $("#draftPreviewCalories").textContent = formatNumber(nutrition.calories, 1); 706 $("#draftPreviewProtein").textContent = item?.protein === null ? "ØºÙØ± ٠عرÙÙ" : `${formatNumber(nutrition.protein, 1)} ج٠`; 707 $("#draftPreviewCarbs").textContent = item?.carbs === null ? "ØºÙØ± ٠عرÙÙ" : `${formatNumber(nutrition.carbs, 1)} ج٠`; 708 $("#draftPreviewFat").textContent = item?.fat === null ? "ØºÙØ± ٠عرÙÙ" : `${formatNumber(nutrition.fat, 1)} ج٠`; 709} 710 711function resetCustomDrinkForm() { 712 const form = $("#customDrinkForm"); 713 if (form) form.reset(); 714} 715 716function openCustomDrinkDialog(target = "log") { 717 customDrinkTarget = target === "draft" ? "draft" : "log"; 718 resetCustomDrinkForm(); 719 const dialog = $("#customDrinkDialog"); 720 if (dialog && typeof dialog.showModal === "function") dialog.showModal(); 721 setTimeout(() => $("#customDrinkName")?.focus(), 50); 722} 723 724function closeCustomDrinkDialog() { 725 const dialog = $("#customDrinkDialog"); 726 if (dialog?.open) dialog.close(); 727} 728 729function saveCustomDrink() { 730 const name = $("#customDrinkName").value.trim(); 731 const caloriesValue = $("#customDrinkCalories").value; 732 if (!name) return showToast("Ø§ÙØªØ¨ اس٠اÙÙ Ø´Ø±ÙØ¨."); 733 if (caloriesValue === "" || Number(caloriesValue) < 0) return showToast("Ø§ÙØªØ¨ سعرات اÙÙÙØ¨."); 734 735 const servingMl = optionalNumber($("#customDrinkMl").value); 736 const notesParts = []; 737 const userNotes = $("#customDrinkNotes").value.trim(); 738 if (servingMl !== null) notesParts.push(`ØØ¬Ù اÙÙÙØ¨ Ø§ÙØªÙØ±ÙØ¨Ù: ${formatNumber(servingMl, 0)} Ù Ù.`); 739 if (userNotes) notesParts.push(userNotes); 740 741 const item = normalizeFood({ 742 id: makeId(), 743 name, 744 calories: Number(caloriesValue), 745 basis: 1, 746 unit: "cup", 747 protein: optionalNumber($("#customDrinkProtein").value), 748 carbs: optionalNumber($("#customDrinkCarbs").value), 749 fat: optionalNumber($("#customDrinkFat").value), 750 fiber: optionalNumber($("#customDrinkFiber").value), 751 categoryId: "drinks", 752 brand: $("#customDrinkBrand").value.trim(), 753 notes: notesParts.join(" "), 754 favorite: $("#customDrinkFavorite").checked, 755 custom: true, 756 source: "أضاÙ٠اÙ٠ستخد٠" 757 }); 758 759 state.foods.push(item); 760 saveState(); 761 populateDrinkSelects(); 762 renderFoods(); 763 renderExplore(); 764 765 if (customDrinkTarget === "draft") { 766 $("#draftDrinkSelect").value = item.id; 767 updateDrinkPreview(true); 768 } else { 769 $("#drinkSelect").value = item.id; 770 updateDrinkPreview(false); 771 } 772 773 closeCustomDrinkDialog(); 774 resetCustomDrinkForm(); 775 showToast(`ت٠ØÙظ ${item.name} ÙÙ ÙØ§Ø¦Ù Ø© اÙÙ Ø´Ø±ÙØ¨Ø§Øª.`); 776} 777 778function getDrinkFoods() { 779 return state.foods 780 .filter((item) => item.categoryId === "drinks" && item.id !== "base-sugar-tsp") 781 .sort((a, b) => a.name.localeCompare(b.name, "ar")); 782} 783 784function populateDrinkSelects() { 785 const drinks = getDrinkFoods(); 786 const options = drinks.map((item) => `<option value="${item.id}">${escapeHtml(item.name)} â ${formatNumber(item.calories, 1)} سعر/ÙÙØ¨</option>`).join(""); 787 ["#drinkSelect", "#draftDrinkSelect"].forEach((selector) => { 788 const element = $(selector); 789 if (!element) return; 790 const current = element.value; 791 element.innerHTML = options || `<option value="">ÙØ§ ØªÙØ¬Ø¯ Ù Ø´Ø±ÙØ¨Ø§Øª</option>`; 792 if (drinks.some((item) => item.id === current)) element.value = current; 793 }); 794 updateDrinkPreview(false); 795 updateDrinkPreview(true); 796} 797 798function calculateDrinkNutrition(item, cups, sugarPerCup) { 799 const base = calculateNutrition(item, cups); 800 const sugarTsp = Math.max(0, Number(sugarPerCup) || 0) * Math.max(0, Number(cups) || 0); 801 return { 802 calories: base.calories + sugarTsp * 16, 803 protein: base.protein,
804 carbs: base.carbs + sugarTsp * 4, 805 fat: base.fat, 806 fiber: base.fiber, 807 sugarTsp 808 }; 809} 810 811function updateDrinkPreview(draft = false) { 812 const select = $(draft ? "#draftDrinkSelect" : "#drinkSelect"); 813 const cupsInput = $(draft ? "#draftDrinkCups" : "#drinkCups"); 814 const sugarInput = $(draft ? "#draftDrinkSugar" : "#drinkSugar"); 815 const preview = $(draft ? "#draftDrinkPreview" : "#drinkPreview"); 816 const item = getFood(select?.value); 817 const cups = Number(cupsInput?.value) || 0; 818 const sugar = Number(sugarInput?.value) || 0; 819 const nutrition = item ? calculateDrinkNutrition(item, cups, sugar) : { calories: 0, carbs: 0 }; 820 if (preview) preview.innerHTML = `<strong>${formatNumber(nutrition.calories, 1)} سعر</strong><span>${formatNumber(nutrition.carbs, 1)} Ø¬Ù ÙØ§Ø±Ø¨ ⢠${formatNumber(sugar, 1)} Ù ÙØ¹ÙØ© Ø³ÙØ± ÙÙÙ ÙÙØ¨</span>`; 821} 822 823function buildDrinkSnapshot(item, sugarPerCup) { 824 const sugar = Math.max(0, Number(sugarPerCup) || 0); 825 return normalizeFood({ 826 id: `drink-${item.id}-${String(sugar).replace(".", "-")}`, 827 name: `${item.name}${sugar > 0 ? ` + ${formatNumber(sugar, 1)} Ù ÙØ¹ÙØ© Ø³ÙØ±/ÙÙØ¨` : " بدÙÙ Ø³ÙØ±"}`, 828 basis: 1, 829 unit: "cup", 830 calories: Number(item.calories) + sugar * 16, 831 protein: item.protein, 832 carbs: (Number(item.carbs) || 0) + sugar * 4, 833 fat: item.fat, 834 fiber: item.fiber, 835 categoryId: "drinks", 836 custom: false, 837 source: "ØØ§Ø³Ø¨Ø© اÙÙ Ø´Ø±ÙØ¨Ø§Øª", 838 notes: "Ø§ÙØ³Ùر Ù ØØ³Ùب Ø¨Ù ÙØ¹ÙØ© ØµØºÙØ±Ø© ØªÙØ±ÙØ¨ÙØ§ 4 ج٠." 839 }); 840} 841 842function addDrinkToLog() { 843 const date = $("#logDate").value || todayISO(); 844 const item = getFood($("#drinkSelect").value); 845 const cups = Number($("#drinkCups").value); 846 const sugar = Number($("#drinkSugar").value) || 0; 847 if (!item || !cups || cups <= 0) return showToast("اختر اÙÙ Ø´Ø±ÙØ¨ ÙØ¹Ø¯Ø¯ Ø§ÙØ£ÙÙØ§Ø¨."); 848 const snapshot = buildDrinkSnapshot(item, sugar); 849 const entry = { id: makeId(), mealType: $("#mealType").value, foodId: "", foodSnapshot: snapshot, quantity: cups, createdAt: new Date().toISOString() }; 850 state.logs[date] = getEntries(date).concat(entry); 851 saveState(); 852 renderSelectedDay(); renderDashboard(); renderHistory(); 853 showToast(`ت٠ت Ø¥Ø¶Ø§ÙØ© ${snapshot.name}.`); 854} 855 856function addDrinkToDraft() { 857 const item = getFood($("#draftDrinkSelect").value); 858 const cups = Number($("#draftDrinkCups").value); 859 const sugar = Number($("#draftDrinkSugar").value) || 0; 860 if (!item || !cups || cups <= 0) return showToast("اختر اÙÙ Ø´Ø±ÙØ¨ ÙØ¹Ø¯Ø¯ Ø§ÙØ£ÙÙØ§Ø¨."); 861 const snapshot = buildDrinkSnapshot(item, sugar); 862 mealDraftEntries.push({ id: makeId(), foodId: "", foodSnapshot: snapshot, quantity: cups, temporary: true }); 863 renderMealPlanner(); 864 showToast("ت٠ت Ø¥Ø¶Ø§ÙØ© اÙÙ Ø´Ø±ÙØ¨ ÙÙÙØ¬Ø¨Ø© Ø§ÙØªØ¬Ø±ÙØ¨ÙØ© ÙÙØ·."); 865} 866 867function getDraftItem(entry) { 868 return (entry.foodId && getFood(entry.foodId)) || entry.foodSnapshot; 869} 870 871function getDraftTotals() { 872 return mealDraftEntries.reduce((total, entry) => { 873 const item = getDraftItem(entry); 874 if (!item) return total; 875 const nutrition = calculateNutrition(item, entry.quantity); 876 Object.keys(total).forEach((key) => { total[key] += nutrition[key] || 0; }); 877 return total; 878 }, { calories: 0, protein: 0, carbs: 0, fat: 0, fiber: 0 }); 879} 880 881function renderMealPlanner() { 882 if (!$("#draftFoodSelect")) return; 883 if (!$("#draftFoodSelect").options.length) populateDraftFoodSelect(); 884 const date = $("#draftCommitDate").value || todayISO(); 885 const dayTotals = getDayTotals(date); 886 const goals = getGoalsForDate(date); 887 const totals = getDraftTotals(); 888 const currentRemaining = goals.calories - dayTotals.calories; 889 const afterMeal = dayTotals.calories + totals.calories; 890 const remainingAfter = goals.calories - afterMeal; 891 const fitClass = totals.calories === 0 ? "" : remainingAfter >= 0 ? "good" : "warn"; 892 893 $("#draftGoalSummary").className = `meal-fit-card ${fitClass}`; 894 $("#draftGoalSummary").innerHTML = `<span>اÙÙ ØªØ§Ø ÙÙ ${formatDate(date, { day: "numeric", month: "short" })}</span><strong>${formatNumber(Math.max(0, currentRemaining))} سعر</strong><small>${totals.calories ? (remainingAfter >= 0 ? `بعد اÙÙØ¬Ø¨Ø© Ø³ÙØªØ¨ÙÙ ${formatNumber(remainingAfter)} سعر` : `اÙÙØ¬Ø¨Ø© Ø³ØªØªØ¬Ø§ÙØ² اÙÙØ¯Ù ب٠${formatNumber(Math.abs(remainingAfter))}
894 سعر`) : "ابدأ Ø¨Ø¥Ø¶Ø§ÙØ© Ù ÙÙÙØ§Øª ÙÙÙØ¬Ø¨Ø© Ø§ÙØªØ¬Ø±ÙØ¨ÙØ©"}</small>`; 895 896 $("#draftMealTotals").innerHTML = `<div><span>Ø§ÙØ³Ø¹Ø±Ø§Øª</span><strong>${formatNumber(totals.calories, 1)}</strong></div><div><span>Ø§ÙØ¨Ø±ÙتÙÙ</span><strong>${formatNumber(totals.protein, 1)} ج٠</strong></div><div><span>اÙÙØ§Ø±Ø¨</span><strong>${formatNumber(totals.carbs, 1)} ج٠</strong></div><div><span>Ø§ÙØ¯ÙÙÙ</span><strong>${formatNumber(totals.fat, 1)} ج٠</strong></div>`; 897 898 $("#draftMealItems").innerHTML = mealDraftEntries.length ? mealDraftEntries.map((entry) => { 899 const item = getDraftItem(entry); 900 const nutrition = calculateNutrition(item, entry.quantity); 901 const detail = item.protein === null && item.carbs === null && item.fat === null 902 ? "اÙÙÙÙ Ø§ÙØªÙصÙÙÙØ© ØºÙØ± ٠عرÙÙØ©" 903 : `${formatNumber(nutrition.protein, 1)} Ø¨Ø±ÙØªÙ٠⢠${formatNumber(nutrition.carbs, 1)} ÙØ§Ø±Ø¨ ⢠${formatNumber(nutrition.fat, 1)} دÙÙÙ`; 904 return `<div class="draft-item"><div><strong>${escapeHtml(item.name)}</strong><span>${formatNumber(entry.quantity, 1)} ${unitLabels[item.unit] || item.unit} ⢠${detail}</span></div><b>${formatNumber(nutrition.calories, 1)} سعر</b><button class="icon-button" data-delete-draft="${entry.id}" aria-label="ØØ°Ù Ù Ù Ø§ÙØªØ¬Ø±Ø¨Ø©">Ã</button></div>`; 905 }).join("") : `<div class="empty-state"><strong>اÙÙØ¬Ø¨Ø© Ø§ÙØªØ¬Ø±ÙØ¨ÙØ© ÙØ§Ø±ØºØ©</strong><span>أض٠٠ÙÙÙÙØ§ Ø£Ù Ø£ÙØ«Ø±Ø ÙÙÙ ÙÙØ³Ø¬Ù Ø£Ù Ø´ÙØ¡ ÙÙ ÙÙÙ Ù Ø§ÙØ¢Ù.</span></div>`; 906 907 $("#commitDraftMeal").disabled = mealDraftEntries.length === 0; 908} 909 910function addExistingFoodToDraft() { 911 const item = getFood($("#draftFoodSelect").value); 912 const quantity = Number($("#draftFoodQuantity").value); 913 if (!item || !quantity || quantity <= 0) return showToast("اختر Ø·Ø¹Ø§Ù ÙØ§ ÙÙÙ ÙØ© صØÙØØ©."); 914 mealDraftEntries.push({ id: makeId(), foodId: item.id, foodSnapshot: deepClone(item), quantity, temporary: false }); 915 renderMealPlanner(); 916 showToast(`ت٠ت Ø¥Ø¶Ø§ÙØ© ${item.name} ÙÙØªØ¬Ø±Ø¨Ø© ÙÙØ·.`); 917} 918 919function addCustomFoodToDraft() { 920 const name = $("#draftCustomName").value.trim(); 921 const calories = Number($("#draftCustomCalories").value); 922 if (!name) return showToast("Ø§ÙØªØ¨ اس٠اÙÙ ÙÙÙÙ."); 923 if (!Number.isFinite(calories) || calories < 0) return showToast("Ø§ÙØªØ¨ سعرات صØÙØØ©."); 924 const item = normalizeFood({ 925 id: `draft-${makeId()}`, name, basis: 1, unit: "serving", calories, 926 protein: optionalNumber($("#draftCustomProtein").value), 927 carbs: optionalNumber($("#draftCustomCarbs").value), 928 fat: optionalNumber($("#draftCustomFat").value), 929 fiber: null, categoryId: "uncategorized", custom: true, source: "Ù ÙÙÙÙ ØªØ¬Ø±ÙØ¨Ù" 930 }); 931 mealDraftEntries.push({ id: makeId(), foodId: "", foodSnapshot: item, quantity: 1, temporary: true }); 932 $("#draftCustomForm").reset(); 933 renderMealPlanner(); 934 showToast("ت٠ت Ø¥Ø¶Ø§ÙØ© اÙÙ ÙÙÙ٠اÙÙ Ø¤ÙØª ÙÙØªØ¬Ø±Ø¨Ø©."); 935} 936 937function commitDraftMeal() { 938 if (!mealDraftEntries.length) return showToast("أض٠٠ÙÙÙØ§Øª ÙÙÙØ¬Ø¨Ø© Ø£ÙÙÙØ§."); 939 const date = $("#draftCommitDate").value || todayISO(); 940 const mealType = $("#draftCommitMeal").value; 941 const entries = mealDraftEntries.map((draft) => ({ 942 id: makeId(), mealType, foodId: draft.foodId || "", foodSnapshot: deepClone(getDraftItem(draft)), 943 quantity: draft.quantity, createdAt: new Date().toISOString() 944 })); 945 state.logs[date] = getEntries(date).concat(entries); 946 saveState(); 947 const count = entries.length; 948 mealDraftEntries = []; 949 renderAll(); 950 showToast(`ت٠ت Ø¥Ø¶Ø§ÙØ© اÙÙØ¬Ø¨Ø© (${count} Ù ÙÙÙØ§Øª) Ø¥ÙÙ ÙÙÙ Ù.`); 951} 952
953function renderSelectedDay() { 954 const date = $("#logDate").value || todayISO(); 955 const totals = getDayTotals(date); 956 const goals = getGoalsForDate(date); 957 $("#selectedDayTitle").textContent = formatDate(date); 958 $("#selectedDaySummary").innerHTML = summaryCards(totals, goals); 959 $("#selectedDayEntries").innerHTML = renderMealGroups(date); 960} 961 962function summaryCards(totals, goals) { 963 return `<div><span>Ø§ÙØ³Ø¹Ø±Ø§Øª</span><strong>${formatNumber(totals.calories)} / ${formatNumber(goals.calories)}</strong></div><div><span>Ø§ÙØ¨Ø±ÙتÙÙ</span><strong>${formatNumber(totals.protein, 1)} ج٠</strong></div><div><span>اÙÙØ§Ø±Ø¨</span><strong>${formatNumber(totals.carbs, 1)} ج٠</strong></div><div><span>Ø§ÙØ¯ÙÙÙ</span><strong>${formatNumber(totals.fat, 1)} ج٠</strong></div>`; 964} 965 966function populateCategorySelects() { 967 const options = `<option value="all">ÙÙ Ø§ÙØªØµÙÙÙØ§Øª</option>${state.categories.map((category) => `<option value="${category.id}">${escapeHtml(category.name)}</option>`).join("")}`; 968 const noAllOptions = state.categories.map((category) => `<option value="${category.id}">${escapeHtml(category.name)}</option>`).join(""); 969 ["#exploreCategory", "#foodsListCategory"].forEach((selector) => { 970 const current = $(selector).value; 971 $(selector).innerHTML = options; 972 if ([...$(selector).options].some((option) => option.value === current)) $(selector).value = current; 973 }); 974 const customCurrent = $("#customCategory").value; 975 $("#customCategory").innerHTML = noAllOptions; 976 if ([...$("#customCategory").options].some((option) => option.value === customCurrent)) $("#customCategory").value = customCurrent; 977} 978 979function renderFoods() { 980 populateCategorySelects(); 981 const search = $("#foodsListSearch").value.trim().toLowerCase(); 982 const category = $("#foodsListCategory").value || "all"; 983 const items = [...state.foods] 984 .filter((item) => (!search || `${item.name} ${item.brand}`.toLowerCase().includes(search)) && (category === "all" || item.categoryId === category)) 985 .sort((a, b) => Number(b.favorite) - Number(a.favorite) || Number(b.custom) - Number(a.custom) || a.name.localeCompare(b.name, "ar")); 986 987 $("#foodsList").innerHTML = items.length ? items.map((item) => { 988 const categoryItem = getCategory(item.categoryId); 989 const macroText = [ 990 item.protein === null ? null : `Ø¨Ø±ÙØªÙÙ ${formatNumber(item.protein, 1)}`, 991 item.carbs === null ? null : `ÙØ§Ø±Ø¨ ${formatNumber(item.carbs, 1)}`, 992 item.fat === null ? null : `دÙÙÙ ${formatNumber(item.fat, 1)}` 993 ].filter(Boolean).join(" ⢠") || "اÙÙÙÙ Ø§ÙØªÙصÙÙÙØ© ØºÙØ± Ù Ø³Ø¬ÙØ©"; 994 return `<div class="food-row"><div class="food-row-main"><div class="food-icon">${escapeHtml(categoryItem.icon)}</div><div><strong>${item.favorite ? "â " : ""}${escapeHtml(item.name)}</strong><small>${formatNumber(item.calories, 1)} سعر ÙÙÙ ${formatNumber(item.basis, 1)} ${unitLabels[item.unit]} ⢠${macroText}</small><span class="badge">${escapeHtml(categoryItem.name)}${item.custom ? " ⢠٠خصص" : ""}</span></div></div><div class="food-actions"><button class="icon-button" data-quick-add="${item.id}" title="Ø¥Ø¶Ø§ÙØ© ÙÙÙÙÙ ">ï¼</button><button class="icon-button" data-toggle-favorite="${item.id}" title="اÙÙ ÙØ¶ÙØ©">${item.favorite ? "â " : "â"}</button>${item.custom ? `<button class="icon-button" data-edit-food="${item.id}" title="تعدÙÙ">â</button><button class="icon-button" data-delete-food="${item.id}" title="ØØ°Ù">Ã</button>` : ""}</div></div>`; 995 }).join("") : `<div class="empty-state"><strong>ÙØ§ ØªÙØ¬Ø¯ أطع٠ة Ù Ø·Ø§Ø¨ÙØ©</strong><span>غÙÙØ± Ø§ÙØ¨ØØ« Ø£Ù Ø£Ø¶Ù Ø·Ø¹Ø§Ù ÙØ§ Ø¬Ø¯ÙØ¯Ùا.</span></div>`; 996} 997 998function resetCustomFoodForm() { 999 $("#customFoodForm").reset(); 1000 $("#editingFoodId").value = ""; 1001 $("#customFoodBasis").value = 100; 1002 $("#customFoodUnit").value = "g"; 1003 $("#customCategory").value = "uncategorized"; 1004 $("#foodFormTitle").textContent = "Ø·Ø¹Ø§Ù Ø¬Ø¯ÙØ¯"; 1005 $("#cancelFoodEdit").hidden = true; 1006 $("#macroCheck").hidden = true; 1007} 1008 1009function editFood(foodId) { 1010 const item = getFood(foodId); 1011 if (!item || !item.custom) return; 1012 switchView("foods"); 1013 $("#editingFoodId").value = item.id; 1014 $("#customFoodName").value = item.name; 1015 $("#customCalories").value = item.calories; 1016 $("#customFoodBasis").value = item.basis; 1017 $("#customFoodUnit").value = item.unit; 1018 $("#customProtein").value = item.protein ?? ""; 1019 $("#customCarbs").value = item.carbs ?? ""; 1020 $("#customFat").value = item.fat ?? ""; 1021 $("#customFiber").value = item.fiber ?? ""; 1022 $("#customCategory").value = item.categoryId; 1023 $("#customBrand").value = item.brand || ""; 1024 $("#customNotes").value = item.notes || ""; 1025 $("#customFavorite").checked = item.favorite; 1026 $("#foodFormTitle").textContent = "تعدÙÙ Ø§ÙØ·Ø¹Ø§Ù "; 1027 $("#cancelFoodEdit").hidden = false;
1028 const details = $(".advanced-details"); 1029 if (details) details.open = true; 1030 window.scrollTo({ top: 0, behavior: "smooth" }); 1031 updateMacroCheck(); 1032} 1033 1034function updateMacroCheck() { 1035 const calories = optionalNumber($("#customCalories").value); 1036 const protein = optionalNumber($("#customProtein").value); 1037 const carbs = optionalNumber($("#customCarbs").value); 1038 const fat = optionalNumber($("#customFat").value); 1039 const box = $("#macroCheck"); 1040 if (calories === null || protein === null || carbs === null || fat === null) { 1041 box.hidden = true; 1042 return; 1043 } 1044 const calculated = protein * 4 + carbs * 4 + fat * 9; 1045 const difference = Math.abs(calories - calculated); 1046 if (difference < Math.max(20, calories * .12)) { 1047 box.hidden = true; 1048 return; 1049 } 1050 box.hidden = false; 1051 box.textContent = `ØªÙØ¨ÙÙ: Ø§ÙØ³Ø¹Ø±Ø§Øª اÙÙ ØØ³Ùبة ٠٠اÙÙ Ø§ÙØ±Ùز تساÙÙ ØªÙØ±ÙØ¨ÙØ§ ${formatNumber(calculated)} Ø³Ø¹Ø±Ø Ø¨ÙÙ٠ا Ø£Ø¯Ø®ÙØª ${formatNumber(calories)}. ÙÙ ÙÙ Ø§ÙØÙØ¸Ø ÙÙ٠راجع Ù ÙØµÙ اÙÙ ÙØªØ¬.`; 1052} 1053 1054function renderExplore() { 1055 populateCategorySelects(); 1056 const search = $("#exploreSearch").value.trim().toLowerCase(); 1057 const category = $("#exploreCategory").value || "all"; 1058 const filter = $("#exploreFilter").value; 1059 const sort = $("#exploreSort").value; 1060 const remaining = Math.max(0, getGoalsForDate(todayISO()).calories - getDayTotals(todayISO()).calories); 1061 1062 let items = state.foods.filter((item) => { 1063 if (search && !`${item.name} ${item.brand} ${getCategory(item.categoryId).name}`.toLowerCase().includes(search)) return false; 1064 if (category !== "all" && item.categoryId !== category) return false; 1065 const nutrition = calculateNutrition(item, item.basis); 1066 if (filter === "highProtein" && nutrition.protein < 15) return false; 1067 if (filter === "lowCalorie" && nutrition.calories > 150) return false; 1068 if (filter === "lowFat" && (item.fat === null || nutrition.fat > 5)) return false; 1069 if (filter === "highFiber" && (item.fiber === null || nutrition.fiber < 3)) return false; 1070 if (filter === "favorites" && !item.favorite) return false; 1071 return true; 1072 }); 1073 1074 const score = (item) => { 1075 const n = calculateNutrition(item, item.basis); 1076 const fit = remaining ? Math.max(0, 80 - Math.abs(remaining - n.calories) / Math.max(remaining, 1) * 80) : 0; 1077 return fit + n.protein * 4 + (item.favorite ? 12 : 0) - n.fat * .4; 1078 }; 1079 1080 if (sort === "recommended") items.sort((a, b) => score(b) - score(a)); 1081 if (sort === "proteinDesc") items.sort((a, b) => (b.protein || 0) - (a.protein || 0)); 1082 if (sort === "caloriesAsc") items.sort((a, b) => a.calories - b.calories); 1083 if (sort === "name") items.sort((a, b) => a.name.localeCompare(b.name, "ar")); 1084 1085 $("#exploreCount").textContent = `${formatNumber(items.length)} طعا٠٠تاØ`; 1086 $("#exploreFoods").innerHTML = items.length ? items.map(renderExploreCard).join("") : `<div class="empty-state"><strong>ÙØ§ ØªÙØ¬Ø¯ ÙØªØ§Ø¦Ø¬</strong><span>Ø¬Ø±ÙØ¨ Ø¨ØØ«Ùا Ø£Ù ÙÙØªØ±Ùا ٠ختÙÙÙØ§.</span></div>`; 1087 renderCompare(); 1088} 1089 1090function renderExploreCard(item) { 1091 const nutrition = calculateNutrition(item, item.basis); 1092 const tags = getFoodTags(item, nutrition); 1093 const compared = compareIds.includes(item.id); 1094 return `<article class="food-card"><div class="food-card-head"><div><h4>${item.favorite ? "â " : ""}${escapeHtml(item.name)}</h4><p>${escapeHtml(getCategory(item.categoryId).name)} ⢠ÙÙÙ ${formatNumber(item.basis, 1)} ${unitLabels[item.unit]}</p></div><span class="badge">${escapeHtml(item.source || "ÙØ§Ø¹Ø¯Ø© Ø§ÙØ¨Ø±Ùا٠ج")}</span></div><div class="food-card-macros"><div><span>سعرات</span><strong>${formatNumber(nutrition.calories)}</strong></div><div><span>Ø¨Ø±ÙØªÙÙ</span><strong>${item.protein === null ? "â" : formatNumber(nutrition.protein, 1)}</strong></div><div><span>ÙØ§Ø±Ø¨</span><strong>${item.carbs === null ? "â" : formatNumber(nutrition.carbs, 1)}</strong></div><div><span>دÙÙÙ</span><strong>${item.fat === null ? "â" : formatNumber(nutrition.fat, 1)}</strong></div></div>${item.notes ? `<p class="food-source-note">${escapeHtml(item.notes)}</p>` : ""}<div class="food-tags">${tags.map((tag) => `<span class="food-tag">${tag}</span>`).join("")}</div><div class="food-card-actions"><button class="primary-button compact" data-quick-add="${item.id}">أض٠ÙÙÙÙ Ù</button><button class="ghost-button compact" data-compare-food="${item.id}">${compared ? "Ø¥Ø²Ø§ÙØ©" : "ÙØ§Ø±Ù"}</button></div></article>`; 1095} 1096 1097function getFoodTags(item, nutrition) { 1098 const tags = []; 1099 if (nutrition.protein >= 20) tags.push("Ø¨Ø±ÙØªÙÙ Ù Ø±ØªÙØ¹"); 1100 if (nutrition.calories <= 100) tags.push("سعرات Ù ÙØ®Ùضة"); 1101 if (item.fat !== null && nutrition.fat <= 3) tags.push("ÙÙÙÙ Ø§ÙØ¯ÙÙÙ"); 1102 if (item.fiber !== null && nutrition.fiber >= 3) tags.push("Ø£ÙÙØ§Ù Ø¬ÙØ¯Ø©"); 1103 if (nutrition.calories >= 350) tags.push("ÙØ«Ø§ÙØ© Ø³Ø¹Ø±ÙØ© Ù Ø±ØªÙØ¹Ø©"); 1104 if (!tags.length) tags.push("ÙÙ ÙØ© Ù Ø±ÙØ©"); 1105 return tags.slice(0, 3); 1106} 1107 1108function renderCompare() {
1109 const items = compareIds.map(getFood).filter(Boolean); 1110 if (!items.length) { 1111 $("#compareFoods").innerHTML = `<div class="empty-state"><strong>Ù٠تختر أطع٠ة</strong><span>اضغط Â«ÙØ§Ø±Ù» عÙ٠أ٠طعا٠.</span></div>`; 1112 return; 1113 } 1114 $("#compareFoods").innerHTML = items.map((item) => { 1115 const n = calculateNutrition(item, item.basis); 1116 return `<div class="compare-item"><strong>${escapeHtml(item.name)}</strong><span>${formatNumber(n.calories)} سعر ⢠${item.protein === null ? "Ø¨Ø±ÙØªÙÙ ØºÙØ± ٠عرÙÙ" : `${formatNumber(n.protein, 1)} Ø¬Ù Ø¨Ø±ÙØªÙÙ`} ⢠${item.fat === null ? "دÙÙÙ ØºÙØ± ٠عرÙÙØ©" : `${formatNumber(n.fat, 1)} ج٠دÙÙÙ`}</span></div>`; 1117 }).join(""); 1118} 1119 1120function renderUsdaResults() { 1121 if (!usdaSearchResults.length) return; 1122 $("#usdaResults").innerHTML = usdaSearchResults.map((item, index) => `<div class="external-item"><strong>${escapeHtml(item.name)}</strong><small>${formatNumber(item.calories, 1)} سعر â¢ Ø¨Ø±ÙØªÙÙ ${formatNumber(item.protein, 1)} â¢ ÙØ§Ø±Ø¨ ${formatNumber(item.carbs, 1)} ⢠دÙÙÙ ${formatNumber(item.fat, 1)} ÙÙÙ 100 ج٠</small><button class="ghost-button compact" data-import-usda="${index}">أضÙÙ ÙÙ ÙØªØ¨ØªÙ</button></div>`).join(""); 1123} 1124 1125async function searchUsdaFoods() { 1126 const query = $("#usdaQuery").value.trim(); 1127 if (!query) return showToast("Ø§ÙØªØ¨ Ø§Ø³Ù Ø§ÙØ·Ø¹Ø§Ù Ø¨Ø§ÙØ¥ÙجÙÙØ²ÙØ© Ø£ÙÙÙØ§."); 1128 const key = $("#usdaApiKey").value.trim() || "DEMO_KEY"; 1129 $("#searchUsda").disabled = true; 1130 $("#searchUsda").textContent = "Ø¬Ø§Ø±Ù Ø§ÙØ¨ØØ«..."; 1131 try { 1132 const response = await fetch(`https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${encodeURIComponent(key)}&query=${encodeURIComponent(query)}&pageSize=10`); 1133 if (!response.ok) throw new Error(`USDA ${response.status}`); 1134 const data = await response.json(); 1135 usdaSearchResults = (data.foods || []).map((result) => { 1136 const nutrients = result.foodNutrients || []; 1137 const getNutrient = (number, names) => { 1138 const match = nutrients.find((n) => String(n.nutrientNumber || n.number || "") === String(number)) || nutrients.find((n) => names.some((name) => String(n.nutrientName || n.name || "").toLowerCase().includes(name))); 1139 return Number(match?.value) || 0; 1140 }; 1141 return { 1142 name: result.description || "USDA Food", 1143 calories: getNutrient(1008, ["energy"]), 1144 protein: getNutrient(1003, ["protein"]), 1145 carbs: getNutrient(1005, ["carbohydrate"]), 1146 fat: getNutrient(1004, ["total lipid", "total fat"]), 1147 fiber: getNutrient(1079, ["fiber"]), 1148 sourceId: String(result.fdcId || "") 1149 }; 1150 }).filter((item) => item.calories || item.protein || item.carbs || item.fat); 1151 $("#usdaResults").innerHTML = usdaSearchResults.length ? "" : `<div class="mini-note">ÙÙ ØªØ¸ÙØ± ÙØªØ§Ø¦Ø¬ ØºØ°Ø§Ø¦ÙØ© ÙØ§Ø¨ÙØ© ÙÙØ§Ø³ØªØ®Ø¯Ø§Ù .</div>`; 1152 renderUsdaResults(); 1153 } catch (error) { 1154 console.error(error); 1155 $("#usdaResults").innerHTML = `<div class="mini-note">تعذر Ø§ÙØ§ØªØµØ§Ù Ø¨ÙØ§Ø¹Ø¯Ø© USDA. Ø¬Ø±ÙØ¨ ÙØ§ØÙÙØ§ Ø£Ù Ø§Ø³ØªØ®Ø¯Ù Ù ÙØªØ§Ø API Ø®Ø§ØµÙØ§ بÙ.</div>`; 1156 } finally { 1157 $("#searchUsda").disabled = false; 1158 $("#searchUsda").textContent = "Ø¨ØØ« خارجÙ"; 1159 } 1160} 1161 1162function importUsdaFood(index) { 1163 const imported = usdaSearchResults[index]; 1164 if (!imported) return; 1165 state.foods.push(normalizeFood({ 1166 id: makeId(), name: imported.name, basis: 100, unit: "g", calories: imported.calories, 1167 protein: imported.protein, carbs: imported.carbs, fat: imported.fat, fiber: imported.fiber, 1168 categoryId: guessCategory(imported.name), favorite: false, custom: true, 1169 source: "USDA FoodData Central", sourceId: imported.sourceId, notes: "ØªÙ Ø§ÙØ§Ø³ØªÙراد Ù Ù Ø§ÙØ¨ØØ« Ø§ÙØ®Ø§Ø±Ø¬Ù" 1170 })); 1171 saveState(); 1172 renderFoods(); 1173 renderExplore(); 1174 populateFoodSelect(); 1175 showToast("ت٠ت Ø¥Ø¶Ø§ÙØ© Ø§ÙØ·Ø¹Ø§Ù Ø¥ÙÙ Ù ÙØªØ¨ØªÙ."); 1176} 1177 1178function calculateBmr(sex, age, height, weight) { 1179 const base = 10 * Number(weight) + 6.25 * Number(height) - 5 * Number(age);
1180 return sex === "male" ? base + 5 : base - 161; 1181} 1182 1183function populateCalculatorFromProfile(force = true) { 1184 if (!force && $("#calcAge").value) return; 1185 $("#calcSex").value = state.profile.sex === "female" ? "female" : "male"; 1186 $("#calcAge").value = state.profile.age; 1187 $("#calcHeight").value = state.profile.height; 1188 $("#calcWeight").value = state.profile.weight; 1189 let activity = 1.5; 1190 if (state.profile.dailyActivity === "sedentary" && state.profile.workouts < 3) activity = 1.375; 1191 if (state.profile.dailyActivity === "light") activity = state.profile.workouts >= 3 ? 1.55 : 1.375; 1192 if (state.profile.dailyActivity === "active") activity = state.profile.workouts >= 5 ? 1.725 : 1.55; 1193 $("#calcActivity").value = String(activity); 1194} 1195 1196function calculateEnergyFromForm() { 1197 const sex = $("#calcSex").value; 1198 const age = Number($("#calcAge").value); 1199 const height = Number($("#calcHeight").value); 1200 const weight = Number($("#calcWeight").value); 1201 const activity = Number($("#calcActivity").value); 1202 const goalType = $("#calcGoalType").value; 1203 const mode = $("#calcAdjustmentMode").value; 1204 const adjustmentInput = Math.max(0, Number($("#calcAdjustment").value) || 0); 1205 const bmr = calculateBmr(sex, age, height, weight); 1206 const tdee = bmr * activity; 1207 const customAdjustment = goalType === "maintain" ? 0 : mode === "percent" ? tdee * adjustmentInput / 100 : adjustmentInput; 1208 const customTarget = goalType === "deficit" ? tdee - customAdjustment : goalType === "surplus" ? tdee + customAdjustment : tdee; 1209 1210 // Ø§ÙØªØ±Ø§ØØ§Øª تÙÙØ§Ø¦ÙØ© Ù ØØ§Ùظة ÙØ¨Ø¯Ø§ÙØ©Ø Ù Ø¹ Ø¥Ø¸ÙØ§Ø± ÙØ·Ø§Ù بد٠ادعاء رÙÙ ÙØ·Ø¹Ù. 1211 const lossLow = tdee * 0.90; // عجز 10% 1212 const lossRecommended = tdee * 0.85; // عجز 15% 1213 const lossHigh = tdee * 0.80; // عجز 20% 1214 const gainLow = tdee * 1.05; // ÙØ§Ø¦Ø¶ 5% 1215 const gainRecommended = tdee * 1.075; // ÙØ§Ø¦Ø¶ 7.5% 1216 const gainHigh = tdee * 1.10; // ÙØ§Ø¦Ø¶ 10% 1217 1218 const targets = { 1219 maintain: tdee, 1220 loss: lossRecommended, 1221 gain: gainRecommended, 1222 custom: customTarget 1223 }; 1224 const selectedTargetKey = goalType === "deficit" ? "custom" : goalType === "surplus" ? "custom" : "maintain"; 1225 return { 1226 sex, age, height, weight, activity, goalType, mode, adjustmentInput, 1227 bmr, tdee, customAdjustment, customTarget, targets, selectedTargetKey, 1228 lossRange: { min: lossHigh, recommended: lossRecommended, max: lossLow }, 1229 gainRange: { min: gainLow, recommended: gainRecommended, max: gainHigh } 1230 }; 1231} 1232 1233function selectedCalculatorTarget(result = lastCalculatorResult) { 1234 if (!result) return 0; 1235 return Number(result.targets?.[result.selectedTargetKey]) || Number(result.customTarget) || Number(result.tdee) || 0; 1236} 1237 1238function targetMeta(result, key) { 1239 const target = Number(result.targets[key]); 1240 const delta = target - result.tdee; 1241 const weeklyKg = Math.abs(delta) * 7 / 7700; 1242 if (key === "loss") return { label: "ÙØ²ÙÙ Ù ÙØªØ±Ø", detail: `عجز 15% ØªÙØ±ÙØ¨ÙØ§ ⢠${formatNumber(weeklyKg, 2)} ÙØ¬Ù /Ø£Ø³Ø¨ÙØ¹ ÙØ¸Ø±ÙÙØ§` }; 1243 if (key === "gain") return { label: "Ø²ÙØ§Ø¯Ø© Ù ÙØªØ±ØØ©", detail: `ÙØ§Ø¦Ø¶ 7.5% ØªÙØ±ÙØ¨ÙØ§ ⢠${formatNumber(weeklyKg, 2)} ÙØ¬Ù /Ø£Ø³Ø¨ÙØ¹ ÙØ¸Ø±ÙÙØ§` }; 1244 if (key === "custom") return { label: "Ø§Ø®ØªÙØ§Ø±Ù اÙ٠خصص", detail: result.goalType === "maintain" ? "ثبات" : `${delta < 0 ? "عجز" : "ÙØ§Ø¦Ø¶"} ${formatNumber(Math.abs(delta))} سعر ⢠${formatNumber(weeklyKg, 2)} ÙØ¬Ù /Ø£Ø³Ø¨ÙØ¹ ÙØ¸Ø±ÙÙØ§` }; 1245 return { label: "سعرات Ø§ÙØ«Ø¨Ø§Øª", detail: "ØªÙØ¯Ùر اÙÙ ØØ§Ùظة عÙÙ Ù ØªÙØ³Ø· اÙÙØ²Ù" }; 1246} 1247 1248function renderCalculatorResults(result) { 1249 lastCalculatorResult = result; 1250 const selectedKey = result.selectedTargetKey || "maintain"; 1251 const selected = selectedCalculatorTarget(result); 1252 const selectedDelta = selected - result.tdee; 1253 const selectedPercent = result.tdee ? Math.abs(selectedDelta) / result.tdee * 100 : 0; 1254 let level = "good"; 1255 let title = "Ø§ÙØªØ±Ø§Ø تÙÙØ§Ø¦Ù ÙØ§Ø¨Ù ÙÙ٠راجعة"; 1256 let advice = "ابدأ Ø¨Ø§ÙØ±Ù٠اÙÙ ÙØªØ±Ø ÙØ±Ø§Ùب Ù ØªÙØ³Ø· اÙÙØ²Ù ÙØ§ÙØ·Ø§ÙØ© ÙØ§ÙØ¬ÙØ¹ Ù٠دة Ø£Ø³Ø¨ÙØ¹Ù٠إÙÙ Ø«ÙØ§Ø«Ø©Ø ث٠عدÙÙ 100â200 سعر ØØ³Ø¨ اÙÙØªÙجة اÙÙØ¹ÙÙØ©."; 1257 1258 if (selectedDelta < 0 && (selectedPercent > 25 || Math.abs(selectedDelta) > 1000)) { 1259 level = "danger"; 1260 title = "Ø§ÙØ¹Ø¬Ø² اÙ٠ختار Ø´Ø¯ÙØ¯"; 1261 advice = "ÙØ°Ø§ Ø§ÙØ¹Ø¬Ø² ÙØ¯ ÙØµØ¹Ø¨ Ø§ÙØ§Ø³ØªÙ رار عÙÙÙ ÙÙØ¤Ø«Ø± ÙÙ Ø§ÙØ£Ø¯Ø§Ø¡ ÙØ§ÙØªØºØ°ÙØ©. اختر Ø§ÙØªØ±Ø§Ø اÙÙØ²Ù٠اÙÙ ØØ§Ùظ أ٠راجع Ù Ø®ØªØµÙØ§Ø Ø®ØµÙØµÙا ٠ع Ø£Ù ØØ§ÙØ© صØÙØ©."; 1262 } else if (selectedDelta > 0 && selectedPercent > 15) { 1263 level = "warn"; 1264 title = "اÙÙØ§Ø¦Ø¶ اÙ٠ختار ÙØ¨Ùر ÙØ³Ø¨ÙÙØ§"; 1265 advice = "Ø²ÙØ§Ø¯Ø© Ø§ÙØ³Ø¹Ø±Ø§Øª Ø¨ÙØ°Ø§ اÙÙØ¯Ø± ÙØ¯ ØªØ±ÙØ¹ Ø§ÙØªØ³Ø§Ø¨ Ø§ÙØ¯ÙÙÙ. Ø§ÙØªØ±Ø§Ø Ø§ÙØ²Ùادة اÙÙ ØØ§Ùظ ÙØ¨Ø¯Ø£ Ø¹Ø§Ø¯Ø©Ù Ø¨ÙØ§Ø¦Ø¶ أصغر ث٠ÙÙØ¹Ø¯ÙÙ ØØ³Ø¨ Ù ØªÙØ³Ø· اÙÙØ²Ù ÙØ§Ùأداء."; 1266 } 1267 1268 const targetCard = (key, value, titleText, smallText) => ` 1269 <button type="button" class="result-card selectable ${selectedKey === key ? "featured" : ""}" data-select-calculator-target="${key}">
1270 <span>${titleText}</span><strong>${formatNumber(value)}</strong><small>${smallText}</small> 1271 </button>`; 1272 1273 const customMeta = targetMeta(result, "custom"); 1274 const selectedMeta = targetMeta(result, selectedKey); 1275 $("#calculatorResults").className = "calculator-results"; 1276 $("#calculatorResults").innerHTML = ` 1277 <div class="bmr-summary"><div><span>BMR Ø§ÙØØ±Ù Ø§ÙØ£Ø³Ø§Ø³Ù</span><strong>${formatNumber(result.bmr)}</strong><small>سعر/ÙÙÙ ÙÙ Ø§ÙØ±Ø§ØØ© Ø§ÙØªØ§Ù Ø©</small></div></div> 1278 <div class="recommendation-heading"><strong>Ø§ÙØªØ±Ø§ØØ§Øª Ø§ÙØªØ·Ø¨Ù٠تÙÙØ§Ø¦ÙÙØ§</strong><span>اضغط عÙÙ Ø§ÙØ®Ø·Ø© Ø§ÙØªÙ ØªØ±ÙØ¯ Ø§Ø¹ØªÙ Ø§Ø¯ÙØ§</span></div> 1279 <div class="result-grid recommendation-grid"> 1280 ${targetCard("maintain", result.targets.maintain, "Ø§ÙØ«Ø¨Ø§Øª", "ØØ§Ùظ عÙÙ Ù ØªÙØ³Ø· اÙÙØ²Ù")} 1281 ${targetCard("loss", result.targets.loss, "اÙÙØ²Ù٠اÙÙ ÙØªØ±Ø", `اÙÙØ·Ø§Ù ${formatNumber(result.lossRange.min)}â${formatNumber(result.lossRange.max)}`)} 1282 ${targetCard("gain", result.targets.gain, "Ø§ÙØ²Ùادة اÙÙ ÙØªØ±ØØ©", `اÙÙØ·Ø§Ù ${formatNumber(result.gainRange.min)}â${formatNumber(result.gainRange.max)}`)} 1283 ${targetCard("custom", result.targets.custom, customMeta.label, customMeta.detail)} 1284 </div> 1285 <div class="selected-target-line"><span>Ø§ÙØ®Ø·Ø© اÙ٠ختارة Ø§ÙØ¢Ù</span><strong>${selectedMeta.label}: ${formatNumber(selected)} سعر/ÙÙÙ </strong></div> 1286 <div class="advice-box ${level}"><strong>${title}</strong><p>${advice}</p><p>Ø§ÙØ«Ø¨Ø§Øª ÙØ§ÙØØ±ÙØ© اÙÙØ¹ÙÙØ© ØªÙØ¯ÙØ±Ø§ØªØ Ø§ÙÙØªÙجة Ø§ÙØ£Ø¯Ù ØªÙØ±Ø§Ø¬Ø¹ ٠٠اتجا٠اÙÙØ²Ù اÙÙ Ø³Ø¬ÙØ ÙÙÙØ³Øª ض٠اÙÙØ§ ÙÙ Ø¹Ø¯Ù ØªØºÙØ± Ù ØØ¯Ø¯.</p></div> 1287 <div class="result-actions"><button class="primary-button" data-apply-calculator="default">اعت٠د اÙ٠ختار ÙÙØ¯Ù أساسÙ</button><button class="secondary-button" data-apply-calculator="training">ÙÙØ¯Ù Ø§ÙØªÙ رÙÙ</button><button class="secondary-button" data-apply-calculator="rest">ÙÙØ¯Ù Ø§ÙØ±Ø§ØØ©</button></div>`; 1288} 1289 1290function selectCalculatorTarget(key) { 1291 if (!lastCalculatorResult?.targets || !(key in lastCalculatorResult.targets)) return; 1292 lastCalculatorResult.selectedTargetKey = key; 1293 renderCalculatorResults(lastCalculatorResult); 1294} 1295 1296function applyCalculatorResult(profileName) { 1297 if (!lastCalculatorResult) return; 1298 const target = selectedCalculatorTarget(lastCalculatorResult); 1299 state.goalProfiles[profileName].calories = Math.max(500, Math.round(target / 10) * 10); 1300 saveState(); 1301 renderSettings(profileName); 1302 renderDashboard(); 1303 showToast("ت٠اعت٠اد Ø§ÙØ®Ø·Ø© اÙ٠ختارة Ù٠اÙÙØ¯Ù اÙÙ ØØ¯Ø¯."); 1304} 1305 1306function renderHistory() { 1307 const date = $("#historyDate").value || todayISO(); 1308 const totals = getDayTotals(date); 1309 $("#historySummary").innerHTML = summaryCards(totals, getGoalsForDate(date)); 1310 $("#historyMeals").innerHTML = renderMealGroups(date); 1311} 1312 1313function renderProgress() { 1314 const entries = [...state.weights].sort((a, b) => a.date.localeCompare(b.date)); 1315 const start = entries[0]?.value; 1316 const current = entries.at(-1)?.value; 1317 const change = start !== undefined && current !== undefined ? current - start : null; 1318 $("#startWeight").textContent = start !== undefined ? `${formatNumber(start, 1)} ÙØ¬Ù ` : "â"; 1319 $("#currentWeight").textContent = current !== undefined ? `${formatNumber(current, 1)} ÙØ¬Ù ` : "â"; 1320 $("#weightChange").textContent = change !== null ? `${change > 0 ? "+" : ""}${formatNumber(change, 1)} ÙØ¬Ù ` : "â"; 1321 $("#weightEntries").innerHTML = entries.length ? [...entries].reverse().map((entry) => `<div class="weight-row"><div><strong>${formatNumber(entry.value, 1)} ÙØ¬Ù </strong><small>${formatDate(entry.date, { day: "numeric", month: "long", year: "numeric" })}${entry.note ? ` ⢠${escapeHtml(entry.note)}` : ""}</small></div><span>${entry.date}</span><button class="icon-button" data-delete-weight="${entry.id}">Ã</button></div>`).join("") : `<div class="empty-state"><strong>ÙØ§ ØªÙØ¬Ø¯ ÙÙØ§Ø³Ø§Øª</strong></div>`; 1322 drawWeightChart(entries); 1323} 1324 1325function drawWeightChart(entries) { 1326 const canvas = $("#weightChart"); 1327 const rect = canvas.getBoundingClientRect(); 1328 const ratio = window.devicePixelRatio || 1; 1329 const width = Math.max(300, rect.width || 600); 1330 const height = 260; 1331 canvas.width = width * ratio; 1332 canvas.height = height * ratio; 1333 const ctx = canvas.getContext("2d"); 1334 ctx.scale(ratio, ratio); 1335 ctx.clearRect(0, 0, width, height); 1336 const styles = getComputedStyle(document.documentElement); 1337 const muted = styles.getPropertyValue("--muted").trim(); 1338 const line = styles.getPropertyValue("--line").trim(); 1339 const primary = styles.getPropertyValue("--primary-2").trim(); 1340 ctx.font = "12px Segoe UI"; 1341 ctx.fillStyle = muted; 1342 if (entries.length < 2) { 1343 ctx.textAlign = "center"; 1344 ctx.fillText("أض٠ÙÙØ§Ø³ÙÙ Ø£Ù Ø£ÙØ«Ø± ÙØ¹Ø±Ø¶ Ø§ÙØ§ØªØ¬Ø§Ù", width / 2, height / 2); 1345 return; 1346 } 1347 const values = entries.map((entry) => Number(entry.value)); 1348 const min = Math.min(...values) - 1; 1349 const max = Math.max(...values) + 1; 1350 const pad = { top: 25, right: 20, bottom: 38, left: 46 }; 1351 const chartW = width - pad.left - pad.right; 1352 const chartH = height - pad.top - pad.bottom; 1353 ctx.strokeStyle = line; 1354 ctx.lineWidth = 1; 1355 for (let i = 0; i <= 4; i++) { 1356 const y = pad.top + chartH * i / 4; 1357 ctx.beginPath();
1357 ctx.moveTo(pad.left, y); ctx.lineTo(width - pad.right, y); ctx.stroke(); 1358 const label = max - (max - min) * i / 4; 1359 ctx.fillStyle = muted; ctx.textAlign = "right"; ctx.fillText(label.toFixed(1), pad.left - 7, y + 4); 1360 } 1361 const points = entries.map((entry, index) => ({ 1362 x: pad.left + chartW * index / Math.max(1, entries.length - 1), 1363 y: pad.top + (max - Number(entry.value)) / Math.max(.1, max - min) * chartH, 1364 entry 1365 })); 1366 ctx.strokeStyle = primary; ctx.lineWidth = 3; ctx.beginPath(); 1367 points.forEach((point, index) => index ? ctx.lineTo(point.x, point.y) : ctx.moveTo(point.x, point.y)); 1368 ctx.stroke(); 1369 points.forEach((point, index) => { 1370 ctx.fillStyle = primary; ctx.beginPath(); ctx.arc(point.x, point.y, 4, 0, Math.PI * 2); ctx.fill(); 1371 if (index === 0 || index === points.length - 1 || entries.length <= 5) { 1372 ctx.fillStyle = muted; ctx.textAlign = "center"; ctx.fillText(point.entry.date.slice(5), point.x, height - 14); 1373 } 1374 }); 1375} 1376 1377function renderSettings(preferredGoalProfile = null) { 1378 $("#profileAge").value = state.profile.age; 1379 $("#profileSex").value = state.profile.sex; 1380 $("#profileHeight").value = state.profile.height; 1381 $("#profileWeight").value = state.profile.weight; 1382 $("#profileWorkouts").value = state.profile.workouts; 1383 $("#profileDailyActivity").value = state.profile.dailyActivity; 1384 1385 const profileName = preferredGoalProfile || $("#activeGoalProfile").value || "default"; 1386 $("#activeGoalProfile").value = profileName; 1387 $$(".goal-tab").forEach((button) => button.classList.toggle("active", button.dataset.goalProfile === profileName)); 1388 const goals = state.goalProfiles[profileName]; 1389 $("#goalCalories").value = goals.calories; 1390 $("#goalProtein").value = goals.protein; 1391 $("#goalCarbs").value = goals.carbs; 1392 $("#goalFat").value = goals.fat; 1393 $("#goalWater").value = goals.water; 1394 1395 $("#supabaseUrl").value = cloudConfig.url || ""; 1396 $("#supabaseAnonKey").value = cloudConfig.anonKey || ""; 1397 $("#cloudEmail").value = cloudSession?.user?.email || ""; 1398 renderCloudStatus(); 1399} 1400 1401function deleteLog(date, id) { 1402 state.logs[date] = getEntries(date).filter((entry) => entry.id !== id); 1403 if (!state.logs[date].length) delete state.logs[date]; 1404 saveState(); 1405 renderDashboard(); renderSelectedDay(); renderHistory(); renderExplore(); 1406} 1407 1408function copyDay(source, target) { 1409 const sourceEntries = getEntries(source); 1410 if (!sourceEntries.length) { showToast("اÙÙÙÙ Ø§ÙØ³Ø§Ø¨Ù ÙØ§ ÙØØªÙ٠عÙÙ ÙØ¬Ø¨Ø§Øª."); return false; } 1411 const copies = sourceEntries.map((entry) => ({ ...deepClone(entry), id: makeId(), createdAt: new Date().toISOString() })); 1412 state.logs[target] = getEntries(target).concat(copies); 1413 saveState(); 1414 showToast("ØªÙ ÙØ³Ø® اÙÙØ¬Ø¨Ø§Øª Ø¨ÙØ¬Ø§Ø."); 1415 renderDashboard(); renderSelectedDay(); renderHistory(); 1416 return true; 1417} 1418 1419function prepareFoodForLog(foodId) { 1420 const item = getFood(foodId); 1421 if (!item) return; 1422 switchView("add"); 1423 if (item.categoryId === "drinks") { 1424 populateDrinkSelects(); 1425 $("#drinkSelect").value = item.id; 1426 $("#drinkCups").value = 1; 1427 $("#drinkSugar").value = 0; 1428 updateDrinkPreview(false); 1429 return; 1430 } 1431 $("#foodSearch").value = item.name; 1432 $("#foodQuantity").dataset.userEdited = ""; 1433 populateFoodSelect(item.name, item.id); 1434 $("#foodQuantity").value = item.basis; 1435 updateNutritionPreview(); 1436} 1437 1438function confirmAction(title, message) { 1439 const dialog = $("#confirmDialog"); 1440 $("#confirmTitle").textContent = title; 1441 $("#confirmMessage").textContent = message; 1442 dialog.showModal(); 1443 return new Promise((resolve) => dialog.addEventListener("close", () => resolve(dialog.returnValue === "confirm"), { once: true })); 1444} 1445 1446function downloadFile(filename, content, type) { 1447 const blob = new Blob([content], { type }); 1448 const url = URL.createObjectURL(blob); 1449 const link = document.createElement("a"); 1450 link.href = url; link.download = filename; link.click(); 1451 setTimeout(() => URL.revokeObjectURL(url), 1000); 1452} 1453 1454function exportCsv() { 1455 const rows = [["date", "meal", "food", "quantity", "unit", "calories", "protein", "carbs", "fat"]];
1456 Object.keys(state.logs).sort().forEach((date) => { 1457 getEntries(date).forEach((entry) => { 1458 const item = getFood(entry.foodId) || entry.foodSnapshot; 1459 if (!item) return; 1460 const n = calculateNutrition(item, entry.quantity); 1461 rows.push([date, mealLabels[entry.mealType], item.name, entry.quantity, unitLabels[item.unit], n.calories.toFixed(1), n.protein.toFixed(1), n.carbs.toFixed(1), n.fat.toFixed(1)]); 1462 }); 1463 }); 1464 const csv = "\uFEFF" + rows.map((row) => row.map((cell) => `"${String(cell ?? "").replaceAll('"', '""')}"`).join(",")).join("\n"); 1465 downloadFile(`my-diet-tracker-${todayISO()}.csv`, csv, "text/csv;charset=utf-8"); 1466} 1467 1468function openTodayGoalDialog() { 1469 const date = todayISO(); 1470 const override = state.dayGoalOverrides[date]; 1471 const mode = override ? "custom" : (state.dayModes[date] || "default"); 1472 $("#todayGoalMode").value = mode; 1473 const goals = override || state.goalProfiles[mode === "custom" ? "default" : mode]; 1474 $("#todayGoalCalories").value = goals.calories; 1475 $("#todayGoalProtein").value = goals.protein; 1476 $("#todayGoalCarbs").value = goals.carbs; 1477 $("#todayGoalFat").value = goals.fat; 1478 $("#todayGoalWater").value = goals.water; 1479 toggleTodayCustomFields(); 1480 $("#todayGoalDialog").showModal(); 1481} 1482 1483function toggleTodayCustomFields() { 1484 const custom = $("#todayGoalMode").value === "custom"; 1485 $("#todayCustomGoalFields").style.display = custom ? "grid" : "none"; 1486 if (!custom) { 1487 const goals = state.goalProfiles[$("#todayGoalMode").value]; 1488 if (goals) { 1489 $("#todayGoalCalories").value = goals.calories; 1490 $("#todayGoalProtein").value = goals.protein; 1491 $("#todayGoalCarbs").value = goals.carbs; 1492 $("#todayGoalFat").value = goals.fat; 1493 $("#todayGoalWater").value = goals.water; 1494 } 1495 } 1496} 1497 1498function saveTodayGoal() { 1499 const date = todayISO(); 1500 const mode = $("#todayGoalMode").value; 1501 if (mode === "custom") { 1502 state.dayGoalOverrides[date] = { 1503 calories: Number($("#todayGoalCalories").value), protein: Number($("#todayGoalProtein").value), 1504 carbs: Number($("#todayGoalCarbs").value), fat: Number($("#todayGoalFat").value), water: Number($("#todayGoalWater").value) 1505 }; 1506 delete state.dayModes[date]; 1507 } else { 1508 state.dayModes[date] = mode; 1509 delete state.dayGoalOverrides[date]; 1510 } 1511 saveState(); 1512 renderDashboard(); renderSelectedDay(); renderExplore(); 1513 showToast("ت٠تعدÙÙ ÙØ¯Ù اÙÙÙÙ ."); 1514} 1515 1516function resetTodayGoal() { 1517 const date = todayISO(); 1518 delete state.dayModes[date]; 1519 delete state.dayGoalOverrides[date]; 1520 saveState(); 1521 $("#todayGoalDialog").close(); 1522 renderDashboard(); renderSelectedDay(); renderExplore(); 1523 showToast("عاد اÙÙÙ٠إÙ٠اÙÙØ¯Ù Ø§ÙØ£Ø³Ø§Ø³Ù."); 1524} 1525 1526function updateStorageStatus() { 1527 const connected = Boolean(cloudSession?.access_token && cloudConfig.url && cloudConfig.anonKey); 1528 $("#storageStatus").textContent = connected ? "ØÙظ Ù ØÙÙ + Ù Ø²Ø§Ù ÙØ© Ø³ØØ§Ø¨ÙØ©" : "Ø§ÙØÙØ¸ اÙÙ ØÙÙ ÙØ¹Ù Ù"; 1529 $("#syncStatusButton").textContent = connected ? "Ø³ØØ§Ø¨Ù" : "Ù ØÙÙ"; 1530} 1531 1532function loadCloudConfigFromForm() { 1533 cloudConfig = { url: $("#supabaseUrl").value.trim().replace(/\/$/, ""), anonKey: $("#supabaseAnonKey").value.trim() }; 1534 localStorage.setItem(CLOUD_CONFIG_KEY, JSON.stringify(cloudConfig)); 1535 renderCloudStatus(); 1536 showToast("ت٠ØÙظ إعداد Ø§ÙØ§ØªØµØ§Ù."); 1537} 1538 1539function cloudConfigured() { 1540 return Boolean(cloudConfig.url && cloudConfig.anonKey); 1541} 1542 1543async function authRequest(path, body) { 1544 if (!cloudConfigured()) throw new Error("أدخ٠رابط Supabase ÙØ§ÙÙ ÙØªØ§Ø Ø£ÙÙÙØ§."); 1545 const response = await fetch(`${cloudConfig.url}${path}`, { 1546 method: "POST", 1547 headers: { "Content-Type": "application/json", apikey: cloudConfig.anonKey }, 1548 body: JSON.stringify(body) 1549 }); 1550 const data = await response.json().catch(() => ({})); 1551 if (!response.ok) throw new Error(data.msg || data.message || data.error_description || data.error || "تعذر Ø§ÙØ§ØªØµØ§Ù"); 1552 return data; 1553} 1554 1555async function cloudLogin() { 1556 try { 1557 const data = await authRequest("/auth/v1/token?grant_type=password", { email: $("#cloudEmail").value.trim(), password: $("#cloudPassword").value }); 1558 cloudSession = data; 1559 localStorage.setItem(CLOUD_SESSION_KEY, JSON.stringify(cloudSession)); 1560 $("#cloudPassword").value = ""; 1561 renderCloudStatus(); 1562 await reconcileCloudOnLogin(); 1563 showToast("ت٠تسجÙÙ Ø§ÙØ¯Ø®ÙÙ ÙØ§ÙÙ Ø²Ø§Ù ÙØ©."); 1564 } catch (error) { 1565 setCloudMessage(error.message, true); 1566 } 1567} 1568 1569async function cloudSignup() { 1570 try { 1571 const data = await authRequest("/auth/v1/signup", { email: $("#cloudEmail").value.trim(), password: $("#cloudPassword").value }); 1572 $("#cloudPassword").value = ""; 1573 if (data.access_token) { 1574 cloudSession = data; 1575 localStorage.setItem(CLOUD_SESSION_KEY, JSON.stringify(cloudSession)); 1576 await pushStateToCloud(false); 1577 showToast("ØªÙ Ø¥ÙØ´Ø§Ø¡ Ø§ÙØØ³Ø§Ø¨ ÙØ±Ø¨Ø· Ø§ÙØ¨ÙØ§ÙØ§Øª."); 1578 } else { 1579 showToast("ØªÙ Ø¥ÙØ´Ø§Ø¡ Ø§ÙØØ³Ø§Ø¨. Ø§ÙØØµ Ø¨Ø±ÙØ¯Ù ÙØªØ£ÙÙØ¯Ù ث٠سجÙÙ Ø§ÙØ¯Ø®ÙÙ."); 1580 } 1581 renderCloudStatus(); 1582 } catch (error) { 1583 setCloudMessage(error.message, true); 1584 } 1585} 1586 1587function cloudLogout() { 1588 cloudSession = null; 1589 localStorage.removeItem(CLOUD_SESSION_KEY); 1590 renderCloudStatus(); 1591 showToast("ت٠تسجÙÙ Ø§ÙØ®Ø±Ùج. Ø§ÙØÙØ¸ اÙÙ ØÙ٠٠ست٠ر."); 1592} 1593 1594async function refreshCloudSession() { 1595 if (!cloudSession?.refresh_token) throw new Error("Ø§ÙØªÙت Ø§ÙØ¬Ùسة. سجÙÙ Ø§ÙØ¯Ø®ÙÙ Ù Ø¬Ø¯Ø¯ÙØ§."); 1596 const data = await authRequest("/auth/v1/token?grant_type=refresh_token", { refresh_token: cloudSession.refresh_token }); 1597 cloudSession = data; 1598 localStorage.setItem(CLOUD_SESSION_KEY, JSON.stringify(cloudSession)); 1599 renderCloudStatus(); 1600 return data; 1601} 1602 1603async function cloudRest(path, options = {}, retry = true) { 1604 if (!cloudConfigured() || !cloudSession?.access_token) throw new Error("سجÙÙ Ø§ÙØ¯Ø®Ù٠أÙÙÙØ§."); 1605 const response = await fetch(`${cloudConfig.url}/rest/v1/${path}`, { 1606 ...options, 1607 headers: { 1608 apikey: cloudConfig.anonKey, 1609 Authorization: `Bearer ${cloudSession.access_token}`, 1610 "Content-Type": "application/json", 1611 ...(options.headers || {}) 1612 } 1613 }); 1614 if (response.status === 401 && retry) { 1615 await refreshCloudSession(); 1616 return cloudRest(path, options, false); 1617 } 1618 const text = await response.text(); 1619 let data = null; 1620 try { data = text ? JSON.parse(text) : null; } catch { data = text ? { message: text } : null; } 1621 if (!response.ok) throw new Error(data?.message || data?.hint || `خطأ Ø³ØØ§Ø¨Ù ${response.status}`); 1622 return data; 1623} 1624 1625async function pushStateToCloud(silent = true) { 1626 if (!cloudConfigured() || !cloudSession?.user?.id) return; 1627 try { 1628 await cloudRest("user_data?on_conflict=user_id", { 1629 method: "POST", 1630 headers: { Prefer: "resolution=merge-duplicates,return=minimal" }, 1631 body: JSON.stringify([{ user_id: cloudSession.user.id, data: state, updated_at: state.updatedAt }]) 1632 }); 1633 setCloudMessage(`آخر Ø±ÙØ¹: ${new Date().toLocaleTimeString("ar-EG", { hour: "2-digit", minute: "2-digit" })}`); 1634 if (!silent) showToast("ØªÙ Ø±ÙØ¹ Ø§ÙØ¨ÙØ§ÙØ§Øª Ø¥ÙÙ Ø§ÙØ³ØØ§Ø¨Ø©."); 1635 } catch (error) { 1636 setCloudMessage(error.message, true); 1637 if (!silent) showToast("تعذر Ø±ÙØ¹ Ø§ÙØ¨ÙØ§ÙØ§Øª."); 1638 } 1639} 1640 1641async function fetchCloudState() { 1642 const rows = await cloudRest(`user_data?user_id=eq.${encodeURIComponent(cloudSession.user.id)}&select=data,updated_at`, { method: "GET" }); 1643 return Array.isArray(rows) && rows[0] ? rows[0] : null; 1644} 1645 1646async function pullStateFromCloud(confirmReplace = true) { 1647 try {
1648 const row = await fetchCloudState(); 1649 if (!row?.data) { 1650 showToast("ÙØ§ ØªÙØ¬Ø¯ Ø¨ÙØ§Ùات Ø³ØØ§Ø¨ÙØ© بعد. Ø§Ø±ÙØ¹ Ø¨ÙØ§Ùات ÙØ°Ø§ Ø§ÙØ¬Ùاز Ø£ÙÙÙØ§."); 1651 return; 1652 } 1653 if (confirmReplace) { 1654 const ok = await confirmAction("ØªÙØ²ÙÙ Ø¨ÙØ§Ùات Ø§ÙØ³ØØ§Ø¨Ø©", "Ø³ÙØªÙ Ø§Ø³ØªØ¨Ø¯Ø§Ù Ø§ÙØ¨ÙØ§ÙØ§Øª اÙÙ ØÙÙØ© Ø§ÙØØ§ÙÙØ© باÙÙØ³Ø®Ø© Ø§ÙØ³ØØ§Ø¨ÙØ©."); 1655 if (!ok) return; 1656 } 1657 isApplyingRemote = true; 1658 state = normalizeState(row.data); 1659 localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); 1660 initializeValues(); renderAll(); 1661 isApplyingRemote = false; 1662 setCloudMessage("ØªÙ ØªÙØ²ÙÙ Ø£ØØ¯Ø« ÙØ³Ø®Ø© Ø³ØØ§Ø¨ÙØ©."); 1663 showToast("ØªÙ ØªÙØ²ÙÙ Ø§ÙØ¨ÙØ§ÙØ§Øª Ø§ÙØ³ØØ§Ø¨ÙØ©."); 1664 } catch (error) { 1665 isApplyingRemote = false; 1666 setCloudMessage(error.message, true); 1667 } 1668} 1669 1670async function reconcileCloudOnLogin() { 1671 const row = await fetchCloudState(); 1672 if (!row?.data) { 1673 await pushStateToCloud(true); 1674 return; 1675 } 1676 1677 // On a genuinely new device, always download the existing cloud copy first. 1678 // This prevents the fresh default state from overwriting real cloud data. 1679 if (!LOCAL_STATE_PRESENT_AT_LAUNCH) { 1680 await pullStateFromCloud(false); 1681 return; 1682 } 1683 1684 const cloudTime = new Date(row.updated_at || row.data.updatedAt || 0).getTime(); 1685 const localTime = new Date(state.updatedAt || 0).getTime(); 1686 if (cloudTime > localTime) await pullStateFromCloud(false); 1687 else await pushStateToCloud(true); 1688} 1689 1690function scheduleCloudPush() { 1691 if (!cloudConfigured() || !cloudSession?.access_token) return; 1692 clearTimeout(cloudSyncTimer); 1693 cloudSyncTimer = setTimeout(() => pushStateToCloud(true), 1800); 1694} 1695 1696function setCloudMessage(message, error = false) { 1697 const element = $("#cloudMessage"); 1698 element.textContent = message; 1699 element.style.color = error ? "var(--danger)" : "var(--muted)"; 1700} 1701 1702function renderCloudStatus() { 1703 const connected = Boolean(cloudSession?.access_token && cloudSession?.user); 1704 $("#cloudBadge").textContent = connected ? `٠تصÙ: ${cloudSession.user.email || "ØØ³Ø§Ø¨"}` : "ØºÙØ± ٠تصÙ"; 1705 $("#cloudBadge").classList.toggle("connected", connected); 1706 $("#cloudLogout").hidden = !connected; 1707 $("#cloudLogin").hidden = connected; 1708 $("#cloudSignup").hidden = connected; 1709 $("#pushCloud").disabled = !connected; 1710 $("#pullCloud").disabled = !connected; 1711 updateStorageStatus(); 1712} 1713 1714function setupEventListeners() { 1715 $$('[data-view]').forEach((button) => button.addEventListener("click", () => switchView(button.dataset.view))); 1716 $$('[data-go]').forEach((button) => button.addEventListener("click", () => switchView(button.dataset.go))); 1717 1718 $("#themeToggle").addEventListener("click", () => { 1719 state.theme = state.theme === "dark" ? "light" : "dark"; 1720 applyTheme(); saveState(); renderProgress(); 1721 }); 1722 1723 $("#addWater").addEventListener("click", () => { const date = todayISO(); state.water[date] = Math.round(((Number(state.water[date]) || 0) + 0.25) * 100) / 100; saveState(); renderDashboard(); }); 1724 $("#removeWater").addEventListener("click", () => { const date = todayISO(); state.water[date] = Math.max(0, Math.round(((Number(state.water[date]) || 0) - 0.25) * 100) / 100); saveState(); renderDashboard(); }); 1725 $("#repeatYesterdayBtn").addEventListener("click", () => copyDay(addDays(todayISO(), -1), todayISO())); 1726 $("#editTodayGoal").addEventListener("click", openTodayGoalDialog); 1727 $("#todayGoalMode").addEventListener("change", toggleTodayCustomFields); 1728 $("#todayGoalForm").addEventListener("submit", (event) => { event.preventDefault(); saveTodayGoal(); $("#todayGoalDialog").close(); }); 1729 $("#resetTodayGoal").addEventListener("click", resetTodayGoal); 1730 1731 $("#foodSearch").addEventListener("input", (event) => populateFoodSelect(event.target.value)); 1732 $("#foodSelect").addEventListener("change", () => { $("#foodQuantity").dataset.userEdited = ""; updateQuantityHint(); updateNutritionPreview(); }); 1733 $("#foodQuantity").addEventListener("input", () => { $("#foodQuantity").dataset.userEdited = "1"; updateNutritionPreview(); }); 1734 $("#logDate").addEventListener("change", renderSelectedDay); 1735 $$('[data-open-custom-drink]').forEach((button) => button.addEventListener("click", () => openCustomDrinkDialog(button.dataset.openCustomDrink))); 1736 $("#customDrinkForm").addEventListener("submit", (event) => { event.preventDefault(); saveCustomDrink(); }); 1737 $("#closeCustomDrinkDialog").addEventListener("click", closeCustomDrinkDialog); 1738 $("#cancelCustomDrink").addEventListener("click", closeCustomDrinkDialog); 1739 ["#drinkSelect", "#drinkCups", "#drinkSugar"].forEach((selector) => $(selector).addEventListener(selector === "#drinkSelect" ? "change" : "input", () => updateDrinkPreview(false))); 1740 $("#logDrinkForm").addEventListener("submit", (event) => { event.preventDefault(); addDrinkToLog(); }); 1741 ["#draftDrinkSelect", "#draftDrinkCups", "#draftDrinkSugar"].forEach((selector) => $(selector).addEventListener(selector === "#draftDrinkSelect" ? "change" : "input", () => updateDrinkPreview(true))); 1742 $("#draftDrinkForm").addEventListener("submit", (event) => { event.preventDefault(); addDrinkToDraft(); }); 1743 $("#draftFoodSearch").addEventListener("input", (event) => populateDraftFoodSelect(event.target.value)); 1744 $("#draftFoodSelect").addEventListener("change", () => { $("#draftFoodQuantity").dataset.userEdited = "";
1744 updateDraftQuantityHint(); updateDraftNutritionPreview(); }); 1745 $("#draftFoodQuantity").addEventListener("input", () => { $("#draftFoodQuantity").dataset.userEdited = "1"; updateDraftNutritionPreview(); }); 1746 $("#draftFoodForm").addEventListener("submit", (event) => { event.preventDefault(); addExistingFoodToDraft(); }); 1747 $("#draftCustomForm").addEventListener("submit", (event) => { event.preventDefault(); addCustomFoodToDraft(); }); 1748 $("#draftCommitDate").addEventListener("change", renderMealPlanner); 1749 $("#clearDraftMeal").addEventListener("click", () => { mealDraftEntries = []; renderMealPlanner(); showToast("ØªÙ Ù Ø³Ø Ø§ÙÙØ¬Ø¨Ø© Ø§ÙØªØ¬Ø±ÙØ¨ÙØ©."); }); 1750 $("#commitDraftMeal").addEventListener("click", commitDraftMeal); 1751 1752 $("#logFoodForm").addEventListener("submit", (event) => { 1753 event.preventDefault(); 1754 const date = $("#logDate").value; 1755 const item = getFood($("#foodSelect").value); 1756 const quantity = Number($("#foodQuantity").value); 1757 if (!item || !date || !quantity || quantity <= 0) return; 1758 const entry = { id: makeId(), mealType: $("#mealType").value, foodId: item.id, foodSnapshot: deepClone(item), quantity, createdAt: new Date().toISOString() }; 1759 state.logs[date] = getEntries(date).concat(entry); 1760 saveState(); 1761 showToast(`ت٠ت Ø¥Ø¶Ø§ÙØ© ${item.name}.`); 1762 renderSelectedDay(); renderDashboard(); renderHistory(); renderExplore(); 1763 }); 1764 1765 ["#customCalories", "#customProtein", "#customCarbs", "#customFat"].forEach((selector) => $(selector).addEventListener("input", updateMacroCheck)); 1766 $("#customFoodForm").addEventListener("submit", (event) => { 1767 event.preventDefault(); 1768 const editingId = $("#editingFoodId").value; 1769 const data = normalizeFood({ 1770 id: editingId || makeId(), 1771 name: $("#customFoodName").value.trim(), 1772 calories: Number($("#customCalories").value), 1773 basis: Number($("#customFoodBasis").value) || 100, 1774 unit: $("#customFoodUnit").value || "g", 1775 protein: optionalNumber($("#customProtein").value), 1776 carbs: optionalNumber($("#customCarbs").value), 1777 fat: optionalNumber($("#customFat").value), 1778 fiber: optionalNumber($("#customFiber").value), 1779 categoryId: $("#customCategory").value || "uncategorized", 1780 brand: $("#customBrand").value.trim(), 1781 notes: $("#customNotes").value.trim(), 1782 favorite: $("#customFavorite").checked, 1783 custom: true, 1784 source: "أضاÙ٠اÙ٠ستخد٠" 1785 }); 1786 if (!data.name) return showToast("Ø§ÙØªØ¨ Ø§Ø³Ù Ø§ÙØ·Ø¹Ø§Ù ."); 1787 if (editingId) { 1788 const index = state.foods.findIndex((item) => item.id === editingId); 1789 if (index >= 0) state.foods[index] = data; 1790 } else state.foods.push(data); 1791 saveState(); resetCustomFoodForm(); renderFoods(); renderExplore(); populateFoodSelect(); 1792 showToast(editingId ? "ت٠تعدÙÙ Ø§ÙØ·Ø¹Ø§Ù ." : "ت٠ت Ø¥Ø¶Ø§ÙØ© Ø§ÙØ·Ø¹Ø§Ù . ÙÙ ÙÙÙ Ø§Ø³ØªØ®Ø¯Ø§Ù Ù Ø§ÙØ¢Ù."); 1793 }); 1794 $("#cancelFoodEdit").addEventListener("click", resetCustomFoodForm); 1795 $("#addCategoryButton").addEventListener("click", () => { 1796 const name = $("#newCategoryName").value.trim(); 1797 if (!name) return showToast("Ø§ÙØªØ¨ Ø§Ø³Ù Ø§ÙØªØµÙÙ٠أÙÙÙØ§."); 1798 const existing = state.categories.find((category) => category.name.toLowerCase() === name.toLowerCase()); 1799 if (existing) { $("#customCategory").value = existing.id; return showToast("Ø§ÙØªØµÙÙÙ Ù ÙØ¬Ùد باÙÙØ¹Ù."); } 1800 const id = `cat-${makeId()}`; 1801 state.categories.push({ id, name, icon: name.slice(0, 1), custom: true }); 1802 saveState(); populateCategorySelects(); $("#customCategory").value = id; $("#newCategoryName").value = ""; renderFoods(); renderExplore(); 1803 showToast("ت٠ت Ø¥Ø¶Ø§ÙØ© Ø§ÙØªØµÙÙÙ."); 1804 }); 1805 $("#foodsListSearch").addEventListener("input", renderFoods); 1806 $("#foodsListCategory").addEventListener("change", renderFoods); 1807 1808 ["#exploreSearch", "#exploreCategory", "#exploreFilter", "#exploreSort"].forEach((selector) => $(selector).addEventListener(selector === "#exploreSearch" ? "input" : "change", renderExplore)); 1809 $("#clearCompare").addEventListener("click", () => { compareIds = []; renderExplore(); }); 1810 $("#searchUsda").addEventListener("click", searchUsdaFoods); 1811 1812 $("#calculatorForm").addEventListener("submit", (event) => { event.preventDefault(); renderCalculatorResults(calculateEnergyFromForm()); }); 1813 $("#loadMyProfile").addEventListener("click", () => populateCalculatorFromProfile(true)); 1814 $("#calcGoalType").addEventListener("change", updateCalculatorAdjustmentUi); 1815 $("#calcAdjustmentMode").addEventListener("change", updateCalculatorAdjustmentUi); 1816 1817 $("#historyDate").addEventListener("change", renderHistory); 1818 $("#copyPreviousDay").addEventListener("click", () => { const target = $("#historyDate").value; if (target) copyDay(addDays(target, -1), target); }); 1819 1820 $("#weightForm").addEventListener("submit", (event) => { 1821 event.preventDefault(); 1822 const date = $("#weightDate").value;
1823 const value = Number($("#weightValue").value); 1824 const note = $("#weightNote").value.trim(); 1825 const existing = state.weights.find((entry) => entry.date === date); 1826 if (existing) { existing.value = value; existing.note = note; } 1827 else state.weights.push({ id: makeId(), date, value, note }); 1828 state.profile.weight = value; 1829 saveState(); renderProgress(); renderDashboard(); renderSettings(); showToast(existing ? "ØªÙ ØªØØ¯ÙØ« ÙØ²Ù اÙÙÙÙ ." : "ت٠ØÙظ اÙÙØ²Ù."); 1830 $("#weightNote").value = ""; 1831 }); 1832 1833 $("#profileForm").addEventListener("submit", (event) => { 1834 event.preventDefault(); 1835 state.profile = { 1836 age: Number($("#profileAge").value), sex: $("#profileSex").value, height: Number($("#profileHeight").value), 1837 weight: Number($("#profileWeight").value), workouts: Number($("#profileWorkouts").value), dailyActivity: $("#profileDailyActivity").value 1838 }; 1839 saveState(); renderDashboard(); showToast("ت٠ØÙظ Ø¨ÙØ§ÙاتÙ."); 1840 }); 1841 $$(".goal-tab").forEach((button) => button.addEventListener("click", () => renderSettings(button.dataset.goalProfile))); 1842 $("#goalsForm").addEventListener("submit", (event) => { 1843 event.preventDefault(); 1844 const profileName = $("#activeGoalProfile").value; 1845 state.goalProfiles[profileName] = { 1846 calories: Number($("#goalCalories").value), protein: Number($("#goalProtein").value), carbs: Number($("#goalCarbs").value), 1847 fat: Number($("#goalFat").value), water: Number($("#goalWater").value) 1848 }; 1849 saveState(); renderDashboard(); renderExplore(); showToast("ت٠ØÙظ اÙÙØ¯Ù."); 1850 }); 1851 1852 $("#saveCloudConfig").addEventListener("click", loadCloudConfigFromForm); 1853 $("#cloudLogin").addEventListener("click", cloudLogin); 1854 $("#cloudSignup").addEventListener("click", cloudSignup); 1855 $("#cloudLogout").addEventListener("click", cloudLogout); 1856 $("#pushCloud").addEventListener("click", () => pushStateToCloud(false)); 1857 $("#pullCloud").addEventListener("click", () => pullStateFromCloud(true)); 1858 1859 $("#exportBackup").addEventListener("click", () => downloadFile(`my-diet-tracker-backup-${todayISO()}.json`, JSON.stringify(state, null, 2), "application/json")); 1860 $("#exportCsv").addEventListener("click", exportCsv); 1861 $("#importBackup").addEventListener("change", async (event) => { 1862 const file = event.target.files[0]; 1863 if (!file) return; 1864 try { 1865 const parsed = JSON.parse(await file.text()); 1866 const ok = await confirmAction("استرجاع اÙÙØ³Ø®Ø©", "Ø³ÙØªÙ Ø§Ø³ØªØ¨Ø¯Ø§Ù Ø§ÙØ¨ÙØ§ÙØ§Øª Ø§ÙØØ§ÙÙØ© Ø¨Ù ØØªÙ٠اÙÙØ³Ø®Ø© Ø§ÙØ§ØØªÙØ§Ø·ÙØ©."); 1867 if (ok) { state = normalizeState(parsed); saveState(); initializeValues(); renderAll(); showToast("ت٠استرجاع اÙÙØ³Ø®Ø© Ø¨ÙØ¬Ø§Ø."); } 1868 } catch { showToast("اÙÙ ÙÙ ØºÙØ± ØµØ§ÙØ أ٠تاÙÙ."); } 1869 event.target.value = ""; 1870 }); 1871 $("#resetData").addEventListener("click", async () => { 1872 const ok = await confirmAction("Ù Ø³Ø ÙÙ Ø§ÙØ¨ÙØ§ÙØ§Øª", "Ø³ÙØªÙ Ù Ø³Ø Ø§ÙÙØ¬Ø¨Ø§Øª ÙØ§Ùأطع٠ة اÙ٠خصصة ÙØ§ÙØ£ÙØ²Ø§Ù Ù Ù ÙØ°Ø§ Ø§ÙØ¬Ùاز. إعداد Ø§ÙØ§ØªØµØ§Ù Ø§ÙØ³ØØ§Ø¨Ù ÙÙ ÙÙ٠سØ."); 1873 if (ok) { state = deepClone(initialState); saveState(); initializeValues(); renderAll(); showToast("ت٠ت إعادة Ø§ÙØªØ·Ø¨ÙÙ ÙØØ§ÙØªÙ Ø§ÙØ£ÙÙÙ."); } 1874 }); 1875 1876 document.addEventListener("click", async (event) => { 1877 const quickAdd = event.target.closest("[data-quick-add]"); 1878 if (quickAdd) return prepareFoodForLog(quickAdd.dataset.quickAdd); 1879 const deleteDraftButton = event.target.closest("[data-delete-draft]"); 1880 if (deleteDraftButton) { mealDraftEntries = mealDraftEntries.filter((entry) => entry.id !== deleteDraftButton.dataset.deleteDraft); renderMealPlanner(); return; } 1881 const deleteLogButton = event.target.closest("[data-delete-log]"); 1882 if (deleteLogButton) { const ok = await confirmAction("ØØ°Ù Ø§ÙØ·Ø¹Ø§Ù ", "ÙÙ ØªØ±ÙØ¯ ØØ°Ù ÙØ°Ø§ Ø§ÙØ·Ø¹Ø§Ù ٠٠اÙÙÙÙ Ø"); if (ok) deleteLog(deleteLogButton.dataset.date, deleteLogButton.dataset.deleteLog); return; } 1883 const favoriteButton = event.target.closest("[data-toggle-favorite]"); 1884 if (favoriteButton) { const item = getFood(favoriteButton.dataset.toggleFavorite); if (item) { item.favorite = !item.favorite; saveState(); renderFoods(); renderExplore(); populateFoodSelect($("#foodSearch").value); } return; } 1885 const editButton = event.target.closest("[data-edit-food]"); 1886 if (editButton) return editFood(editButton.dataset.editFood); 1887 const deleteFoodButton = event.target.closest("[data-delete-food]"); 1888 if (deleteFoodButton) { 1889 const item = getFood(deleteFoodButton.dataset.deleteFood); 1890 if (!item) return; 1891 const ok = await confirmAction("ØØ°Ù طعا٠٠خصص", `سÙÙØØ°Ù Â«${item.name}» ٠٠اÙÙØ§Ø¦Ù Ø©. Ø§ÙØ³Ø¬Ùات اÙÙØ¯ÙÙ Ø© Ø³ØªØ¸Ù ÙØ§Ø¨ÙØ© ÙÙØØ³Ø§Ø¨.`); 1892 if (ok) { state.foods = state.foods.filter((foodItem) => foodItem.id !== item.id);
1892 compareIds = compareIds.filter((id) => id !== item.id); saveState(); renderFoods(); renderExplore(); populateFoodSelect(); } 1893 return; 1894 } 1895 const compareButton = event.target.closest("[data-compare-food]"); 1896 if (compareButton) { 1897 const id = compareButton.dataset.compareFood; 1898 if (compareIds.includes(id)) compareIds = compareIds.filter((itemId) => itemId !== id); 1899 else if (compareIds.length < 3) compareIds.push(id); 1900 else return showToast("ÙÙ ÙÙ Ù ÙØ§Ø±ÙØ© 3 أطع٠ة Ø¨ØØ¯ Ø£ÙØµÙ."); 1901 renderExplore(); return; 1902 } 1903 const importButton = event.target.closest("[data-import-usda]"); 1904 if (importButton) return importUsdaFood(Number(importButton.dataset.importUsda)); 1905 const selectTargetButton = event.target.closest("[data-select-calculator-target]"); 1906 if (selectTargetButton) return selectCalculatorTarget(selectTargetButton.dataset.selectCalculatorTarget); 1907 const applyButton = event.target.closest("[data-apply-calculator]"); 1908 if (applyButton) return applyCalculatorResult(applyButton.dataset.applyCalculator); 1909 const deleteWeightButton = event.target.closest("[data-delete-weight]"); 1910 if (deleteWeightButton) { const ok = await confirmAction("ØØ°Ù ÙÙØ§Ø³ اÙÙØ²Ù", "ÙÙ ØªØ±ÙØ¯ ØØ°Ù ÙØ°Ø§ اÙÙÙØ§Ø³Ø"); if (ok) { state.weights = state.weights.filter((entry) => entry.id !== deleteWeightButton.dataset.deleteWeight); saveState(); renderProgress(); renderDashboard(); } } 1911 }); 1912 1913 window.addEventListener("resize", debounce(() => { if ($("#progressView").classList.contains("active-view")) renderProgress(); }, 120)); 1914 window.addEventListener("beforeinstallprompt", (event) => { event.preventDefault(); deferredInstallPrompt = event; $("#installButton").hidden = false; }); 1915 $("#installButton").addEventListener("click", async () => { if (!deferredInstallPrompt) return; deferredInstallPrompt.prompt(); await deferredInstallPrompt.userChoice; deferredInstallPrompt = null; $("#installButton").hidden = true; }); 1916} 1917 1918function updateCalculatorAdjustmentUi() { 1919 const goalType = $("#calcGoalType").value; 1920 const mode = $("#calcAdjustmentMode").value; 1921 $("#calcAdjustment").disabled = goalType === "maintain"; 1922 $("#calcAdjustmentLabel").firstChild.textContent = mode === "percent" ? "اÙÙØ³Ø¨Ø© %" : "عدد Ø§ÙØ³Ø¹Ø±Ø§Øª"; 1923 if (goalType === "maintain") $("#calcAdjustment").value = 0; 1924 else if (Number($("#calcAdjustment").value) === 0) $("#calcAdjustment").value = mode === "percent" ? (goalType === "surplus" ? 8 : 15) : (goalType === "surplus" ? 250 : 500); 1925 $("#calcAdjustment").max = mode === "percent" ? 40 : 2000; 1926 $("#calcAdjustment").step = mode === "percent" ? 1 : 50; 1927} 1928 1929function debounce(fn, delay) { 1930 let timer; 1931 return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; 1932} 1933 1934function initializeValues() { 1935 const today = todayISO(); 1936 $("#todayLabel").textContent = formatDate(today, { weekday: "long", day: "numeric", month: "long", year: "numeric" }); 1937 $("#logDate").value = today; 1938 $("#historyDate").value = today; 1939 $("#weightDate").value = today; 1940 $("#draftCommitDate").value = today; 1941 $("#weightValue").value = state.profile.weight; 1942 applyTheme(); 1943 populateCategorySelects(); 1944 populateDrinkSelects(); 1945 populateCalculatorFromProfile(true); 1946 updateCalculatorAdjustmentUi(); 1947 updateStorageStatus(); 1948} 1949 1950function renderAll() { 1951 renderDashboard(); 1952 populateFoodSelect(); 1953 populateDraftFoodSelect(); 1954 populateDrinkSelects(); 1955 renderSelectedDay(); 1956 renderMealPlanner(); 1957 renderFoods(); 1958 renderExplore(); 1959 renderHistory(); 1960 renderProgress(); 1961 renderSettings(); 1962} 1963 1964function registerServiceWorker() { 1965 if ("serviceWorker" in navigator && location.protocol.startsWith("http")) { 1966 navigator.serviceWorker.register("sw.js").catch((error) => console.warn("Service worker registration failed", error)); 1967 } 1968} 1969 1970function init() { 1971 initializeValues(); 1972 setupEventListeners(); 1973 renderAll(); 1974 registerServiceWorker(); 1975} 1976 1977document.addEventListener("DOMContentLoaded", init);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.