• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}