1/* 2 * Copyright (c) 2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import curves from '@ohos.curves' 17import router from '@ohos.router' 18import { CIRCLE_RADIUS } from '../common/Constants' 19import { BreakPointType } from '../common/BreakpointSystem' 20import { FoodInfo, CategoryId, MealTime, MealTimeId, DietRecord } from '../model/DataModels' 21import { getFoodInfo, initDietRecords, getMileTimes } from '../model/DataUtil' 22 23@Styles function cardStyle () { 24 .height('100%') 25 .padding({ top: 20, right: 20, left: 20 }) 26 .backgroundColor(Color.White) 27 .borderRadius(12) 28} 29 30@Component 31struct CardTitle { 32 private title: Resource 33 private subtitle: Resource 34 35 build() { 36 Row() { 37 Text(this.title) 38 .fontSize(26) 39 Blank() 40 Text(this.subtitle) 41 .fontSize(13) 42 .fontColor('rgba(0,0,0,0.6)') 43 } 44 .width('100%') 45 .height(26) 46 } 47} 48 49@Component 50struct PageTitle { 51 private foodName: Resource = $r('app.string.title_food_detail') 52 53 build() { 54 Row() { 55 Image($r('app.media.back')) 56 .width(20) 57 .height(20) 58 .onClick(() => { 59 router.back() 60 }) 61 Text(this.foodName) 62 .fontSize(22) 63 .margin({ left: 20 }) 64 } 65 .padding(12) 66 .width('100%') 67 } 68} 69 70@Component 71struct FoodImageDisplay { 72 private foodInfo: FoodInfo 73 @State imageBgColorA: number = 0 74 @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm' 75 76 build() { 77 Stack({ alignContent: Alignment.BottomStart }) { 78 Image(this.foodInfo.image) 79 .sharedTransition(this.foodInfo.letter, { 80 duration: 400, 81 curve: curves.cubicBezier(0.2, 0.2, 0.1, 1.0), 82 delay: 100 83 }) 84 .backgroundColor(`rgba(255, 255, 255, ${this.imageBgColorA})`) 85 .objectFit(ImageFit.Contain) 86 Text(this.foodInfo.name) 87 .fontSize(26) 88 .fontWeight(FontWeight.Bold) 89 .margin({ left: 26, bottom: 18 }) 90 } 91 .height(this.currentBreakpoint == 'lg' ? 166 : 280) 92 } 93} 94 95@Component 96struct ContentTable { 97 private foodInfo: FoodInfo 98 99 @Builder IngredientItem(title: Resource, colorValue: string, name: Resource, value: Resource) { 100 Row() { 101 Text(title) 102 .fontSize(18) 103 .fontWeight(FontWeight.Bold) 104 .layoutWeight(1) 105 .align(Alignment.Start) 106 Row() { 107 Circle({ width: 6, height: 6 }) 108 .margin({ right: 12 }) 109 .fill(colorValue) 110 Text(name) 111 .fontSize(18) 112 Blank() 113 Text(value) 114 .fontSize(18) 115 } 116 .width('100%') 117 .layoutWeight(2) 118 } 119 .margin({ bottom: 20 }) 120 } 121 122 build() { 123 Column() { 124 this.IngredientItem($r('app.string.diet_record_calorie'), '#F54040', $r('app.string.diet_record_calorie'), $r('app.string.calorie_with_kcal_unit', this.foodInfo.calories.toString())) 125 Row().height(20) 126 this.IngredientItem($r('app.string.nutrition_element'), '#CCC', $r('app.string.nutrition_element'), $r('app.string.weight_with_gram_unit', this.foodInfo.protein.toString())) 127 this.IngredientItem(null, '#F5D640', $r('app.string.diet_record_fat'), $r('app.string.weight_with_gram_unit', this.foodInfo.fat.toString())) 128 this.IngredientItem(null, '#9E9EFF', $r('app.string.diet_record_carbohydrates'), $r('app.string.weight_with_gram_unit', this.foodInfo.carbohydrates.toString())) 129 this.IngredientItem(null, '#53F540', $r('app.string.diet_record_vitaminC'), $r('app.string.weight_with_milligram_unit', this.foodInfo.vitaminC.toString())) 130 } 131 .cardStyle() 132 } 133} 134 135@Component 136struct CaloriesProgress { 137 private foodInfo: FoodInfo 138 private averageCalories: number = 0 139 private totalCalories: number = 0 140 private highCalories: boolean = false 141 142 aboutToAppear() { 143 switch (this.foodInfo.categoryId) { 144 case CategoryId.Vegetable: 145 this.averageCalories = 26 146 break 147 case CategoryId.Fruit: 148 this.averageCalories = 60 149 break 150 case CategoryId.Nut: 151 this.averageCalories = 606 152 break 153 case CategoryId.Seafood: 154 this.averageCalories = 56 155 break 156 case CategoryId.Dessert: 157 this.averageCalories = 365 158 break 159 } 160 this.totalCalories = this.averageCalories * 2 161 this.highCalories = this.foodInfo.calories < this.averageCalories 162 } 163 164 build() { 165 Column() { 166 CardTitle({ title: $r('app.string.diet_record_calorie'), subtitle: $r('app.string.unit_weight') }) 167 168 Row() { 169 Text(this.foodInfo.calories.toString()) 170 .fontColor(this.getCalorieColor()) 171 .fontSize(65) 172 Text($r('app.string.calorie_with_kcal_unit', '')) 173 .fontSize(20) 174 .margin({ bottom: 10 }) 175 } 176 .margin({ top: 25, bottom: 25 }) 177 .alignItems(VerticalAlign.Bottom) 178 179 Text(this.highCalories ? $r('app.string.high_calorie_food') : $r('app.string.low_calorie_food')) 180 .fontSize(13) 181 .fontColor('#313131') 182 183 Progress({ value: this.foodInfo.calories, total: this.totalCalories, style: ProgressStyle.Linear }) 184 .style({ strokeWidth: 24 }) 185 .color(this.getCalorieColor()) 186 .margin({ top: 18 }) 187 } 188 .cardStyle() 189 } 190 191 getCalorieColor() { 192 return this.highCalories ? $r('app.color.high_calorie') : $r('app.color.low_calorie') 193 } 194} 195 196class NutritionElement { 197 element: Resource 198 weight: number 199 percent: number 200 beginAngle: number 201 endAngle: number 202 color: string 203 204 constructor(element: Resource, weight: number, color: string) { 205 this.element = element 206 this.weight = weight 207 this.color = color 208 } 209} 210 211@Component 212struct NutritionPieChart { 213 private foodInfo: FoodInfo 214 private nutritionElements: NutritionElement[] 215 private settings: RenderingContextSettings = new RenderingContextSettings(true) 216 private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 217 218 build() { 219 Column() { 220 CardTitle({ title: $r('app.string.nutrition_element'), subtitle: $r('app.string.unit_weight') }) 221 Canvas(this.context) 222 .height(CIRCLE_RADIUS * 2) 223 .aspectRatio(1) 224 .margin({ top: 30, bottom: 32 }) 225 .onReady(() => { 226 this.nutritionElements.forEach((item) => { 227 this.context.beginPath() 228 this.context.moveTo(CIRCLE_RADIUS, CIRCLE_RADIUS) 229 this.context.arc(CIRCLE_RADIUS, CIRCLE_RADIUS, CIRCLE_RADIUS, item.beginAngle, item.endAngle) 230 this.context.fillStyle = item.color 231 this.context.fill() 232 }) 233 }) 234 Row() { 235 ForEach(this.nutritionElements, (item: NutritionElement) => { 236 Row({ space: 4 }) { 237 Circle({ width: 8, height: 8 }).fill(item.color) 238 Text(item.element).fontSize(12) 239 Text($r('app.string.weight_with_gram_unit', item.weight.toString())).fontSize(12) 240 } 241 }) 242 } 243 .width('100%') 244 .justifyContent(FlexAlign.SpaceAround) 245 } 246 .cardStyle() 247 } 248} 249 250@Component 251struct NutritionPercent { 252 private foodInfo: FoodInfo 253 private nutritionElements: NutritionElement[] 254 255 build() { 256 Column() { 257 CardTitle({ title: $r('app.string.nutrition_element'), subtitle: $r('app.string.unit_weight') }) 258 259 Row() { 260 ForEach(this.nutritionElements, (item: NutritionElement) => { 261 Column() { 262 Stack({ alignContent: Alignment.Center }) { 263 Progress({ value: item.percent, type: ProgressType.Ring }) 264 .style({ strokeWidth: 10 }) 265 .color(item.color) 266 .margin(4) 267 Text(item.percent + '%').fontSize(17) 268 } 269 270 Text(item.element) 271 .fontSize(13) 272 .margin({ top: 24 }) 273 Text($r('app.string.weight_with_gram_unit', item.weight.toString())) 274 .fontSize(13) 275 }.layoutWeight(1) 276 277 }) 278 } 279 .width('100%') 280 .margin({ top: 50 }) 281 } 282 .cardStyle() 283 } 284} 285 286@CustomDialog 287struct Record { 288 private foodInfo: FoodInfo 289 private controller: CustomDialogController 290 private select: number = 1 291 private mileTime: string[] = getMileTimes() 292 private foodWeight: string[] = ['25', '50', '100', '150', '200', '250', '300', '350', '400', '450', '500'] 293 private mealTimeId: MealTimeId = MealTimeId.Lunch 294 private mealWeight: number = Number(this.foodWeight[this.select]) 295 296 build() { 297 Column() { 298 Row({ space: 6 }) { 299 Column() { 300 Text(this.foodInfo.name) 301 .minFontSize(18) 302 .maxFontSize(30) 303 .maxLines(1) 304 Text($r('app.string.calorie_with_kcal_unit', this.foodInfo.calories.toString())) 305 .fontSize(16) 306 .fontColor('rgba(0,0,0,0.4)') 307 .margin({ top: 2 }) 308 } 309 .layoutWeight(1) 310 .justifyContent(FlexAlign.Center) 311 312 TextPicker({ range: this.mileTime, selected: this.select }) 313 .layoutWeight(1) 314 .linearGradient({ 315 angle: 0, 316 direction: GradientDirection.Top, 317 colors: [[0xfdfdfd, 0.0], [0xe0e0e0, 0.5], [0xfdfdfd, 1]], 318 }) 319 .onChange((value: string, index: number) => { 320 this.mealTimeId = index 321 }) 322 323 TextPicker({ range: this.foodWeight, selected: this.select }) 324 .layoutWeight(1) 325 .linearGradient({ 326 angle: 0, 327 direction: GradientDirection.Top, 328 colors: [[0xfdfdfd, 0.0], [0xe0e0e0, 0.5], [0xfdfdfd, 1]], 329 }) 330 .onChange((value: string) => { 331 this.mealWeight = Number(value) 332 }) 333 } 334 .height(128) 335 336 Button($r('app.string.button_food_detail_complete'), { type: ButtonType.Capsule, stateEffect: true }) 337 .height(43) 338 .width('100%') 339 .margin({ top: 33, left: 72, right: 72 }) 340 .backgroundColor($r('app.color.theme_color_green')) 341 .onClick(() => { 342 let dietRecordsList = AppStorage.Get<Array<DietRecord>>('dietRecords') 343 if (dietRecordsList == undefined || dietRecordsList.length === 0) { 344 dietRecordsList = initDietRecords 345 } 346 let dietRecordData = new DietRecord(dietRecordsList.length, this.foodInfo.id, new MealTime(this.mealTimeId), this.mealWeight) 347 dietRecordsList.push(dietRecordData) 348 AppStorage.SetOrCreate<Array<DietRecord>>('dietRecords', dietRecordsList) 349 this.controller.close() 350 }) 351 } 352 .cardStyle() 353 .height(254) 354 .width('90%') 355 } 356} 357 358@Entry 359@Component 360struct FoodDetail { 361 @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm' 362 private foodInfo: FoodInfo = getFoodInfo() 363 private nutritionElements: NutritionElement[] 364 dialogController: CustomDialogController = new CustomDialogController({ 365 builder: Record({ foodInfo: this.foodInfo }), 366 autoCancel: true, 367 alignment: DialogAlignment.Bottom, 368 offset: { dx: 0, dy: -20 }, 369 customStyle: true 370 }) 371 372 aboutToAppear() { 373 let total = this.foodInfo.protein + this.foodInfo.fat + this.foodInfo.carbohydrates 374 this.nutritionElements = [ 375 new NutritionElement($r('app.string.diet_record_protein'), this.foodInfo.protein, '#ff9421'), 376 new NutritionElement($r('app.string.diet_record_fat'), this.foodInfo.fat, '#ffd100'), 377 new NutritionElement($r('app.string.diet_record_carbohydrates'), this.foodInfo.carbohydrates, '#4cd041') 378 ] 379 let lastEndAngle = -0.5 * Math.PI 380 this.nutritionElements.forEach((value) => { 381 let percent = value.weight / total 382 value.percent = Math.round(percent * 100) 383 value.beginAngle = lastEndAngle 384 value.endAngle = (percent * 2 * Math.PI) + lastEndAngle 385 lastEndAngle = value.endAngle 386 return value 387 }) 388 } 389 390 build() { 391 Scroll() { 392 Column() { 393 PageTitle() 394 FoodImageDisplay({ foodInfo: this.foodInfo }) 395 Swiper() { 396 ContentTable({ foodInfo: this.foodInfo }) 397 CaloriesProgress({ foodInfo: this.foodInfo }) 398 NutritionPercent({ foodInfo: this.foodInfo, nutritionElements: this.nutritionElements }) 399 NutritionPieChart({ foodInfo: this.foodInfo, nutritionElements: this.nutritionElements }) 400 } 401 .indicator(new BreakPointType({ sm: true, md: false, lg: false }).getValue(this.currentBreakpoint)) 402 .displayCount(new BreakPointType({ sm: 1, md: 2, lg: 3 }).getValue(this.currentBreakpoint)) 403 .clip(new Rect().width('100%').height('100%').radiusWidth(15).radiusHeight(15)) 404 .itemSpace(20) 405 .height(330) 406 .indicatorStyle({ selectedColor: $r('app.color.theme_color_green') }) 407 .margin({ top: 10, right: 10, left: 10 }) 408 409 Button($r('app.string.button_food_detail_record'), { type: ButtonType.Capsule, stateEffect: true }) 410 .height(42) 411 .width('80%') 412 .margin({ top: 32, bottom: 32 }) 413 .backgroundColor($r('app.color.theme_color_green')) 414 .onClick(() => { 415 this.dialogController.open() 416 }) 417 } 418 .alignItems(HorizontalAlign.Center) 419 } 420 .backgroundColor('#EDF2F5') 421 .height('100%') 422 .align(Alignment.Top) 423 } 424}