1/*
2 * Copyright (c) 2024 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 */
15import { UIContext } from '@ohos.arkui.UIContext';
16import { ComponentContent } from '@ohos.arkui.node';
17import { BusinessError } from '@ohos.base';
18import { curves } from '@kit.ArkUI';
19
20const DIALOG_BORDER_RADIUS: Resource = $r('sys.float.ohos_id_corner_radius_default_m');
21const DIALOG_INNER_PADDING_SIZE: number = 16;
22const DIALOG_MAX_WIDTH: number = 480;
23const DIALOG_OFFSET_X: number = 0;
24const DIALOG_OFFSET_Y_FOR_BAR: number = -88;
25const DIALOG_OFFSET_Y_FOR_NONE: number = -44;
26
27const STANDARD_MIN_COMPONENT_HEIGHT: number = 82;
28const STANDARD_MAX_COMPONENT_HEIGHT: number = 94;
29
30const DIALOG_SHADOW_RADIUS: number = 16;
31const DIALOG_SHADOW_OFFSET_Y: number = 10;
32const DIALOG_SHADOW_COLOR: ResourceStr = '#19000000';
33
34const TITLE_LINE_DISTANCE: number = 2;
35const TITLE_MAX_LINE: number = 2;
36const SUBTITLE_MAX_LINE: number = 1;
37const TEXT_LEFT_MARGIN_SIZE: number = 16;
38const SUBTITLE_DEFAULT_COLOR: Resource = $r('sys.color.ohos_id_color_text_secondary_contrary');
39const TITLE_DEFAULT_COLOR: Resource = $r('sys.color.ohos_id_color_text_primary_contrary');
40
41const OPERATE_AREA_AVOID_WIDTH: number = 28;
42
43const CLOSE_ICON_DARK_RESOURCE: Resource = $r('sys.color.ohos_id_color_tertiary');
44const CLOSE_ICON_LIGHT_RESOURCE: Resource = $r('sys.color.ohos_id_color_primary_contrary');
45
46const CLOSE_BUTTON_BORDER_RADIUS: number = 8;
47const CLOSE_BUTTON_ICON_SIZE: number = 16;
48const CLOSE_BUTTON_HOT_SPOT_SIZE: number = 32;
49const CLOSE_BUTTON_MARGIN: number = 8;
50const CLOSE_BUTTON_ICON_OPACITY = 0.6;
51const CLOSE_BUTTON_RESPONSE_REGION_OFFSET_X: number = -8;
52const CLOSE_BUTTON_RESPONSE_REGION_OFFSET_Y: number = -8;
53const CLOSE_BUTTON_OFFSET_X: number = 0;
54const CLOSE_BUTTON_OFFSET_Y: number = -50;
55
56const FOREGROUND_IMAGE_OFFSET_X: number = 4;
57const FOREGROUND_IMAGE_OFFSET_Y: number = 0;
58
59export enum IconStyle {
60  DARK = 0,
61  LIGHT = 1
62}
63
64export enum TitlePosition {
65  TOP = 0,
66  BOTTOM = 1
67}
68
69export enum BottomOffset {
70  OFFSET_FOR_BAR = 0,
71  OFFSET_FOR_NONE = 1
72}
73
74class DialogParams {
75  public options: DialogOptions;
76  public defaultCloseAction: Callback<void>;
77
78  constructor(
79    options: DialogOptions,
80    defaultCloseAction: Callback<void>,
81  ) {
82    this.options = options;
83    this.defaultCloseAction = defaultCloseAction;
84  }
85}
86
87@Builder
88function dialogBuilder(params: DialogParams) {
89  Row() {
90    Flex() {
91      Row() {
92        SymbolGlyph($r('sys.symbol.xmark_circle_fill'))
93          .fontColor([params.options.iconStyle === IconStyle.DARK ?
94            CLOSE_ICON_DARK_RESOURCE : CLOSE_ICON_LIGHT_RESOURCE])
95          .borderRadius(CLOSE_BUTTON_BORDER_RADIUS)
96          .width(CLOSE_BUTTON_ICON_SIZE)
97          .height(CLOSE_BUTTON_ICON_SIZE)
98          .opacity(CLOSE_BUTTON_ICON_OPACITY)
99          .draggable(false)
100          .focusable(true)
101          .responseRegion({
102            x: CLOSE_BUTTON_RESPONSE_REGION_OFFSET_X,
103            y: CLOSE_BUTTON_RESPONSE_REGION_OFFSET_Y,
104            width: CLOSE_BUTTON_HOT_SPOT_SIZE,
105            height: CLOSE_BUTTON_HOT_SPOT_SIZE
106          })
107          .margin(CLOSE_BUTTON_MARGIN)
108          .alignSelf(ItemAlign.End)
109          .offset({
110            x: CLOSE_BUTTON_OFFSET_X,
111            y: CLOSE_BUTTON_OFFSET_Y
112          })
113          .onClick(() => {
114            if (params.options.onDialogClose !== undefined) {
115              params.options.onDialogClose()
116            }
117            params.defaultCloseAction()
118          })
119
120        Image(params.options.foregroundImage)
121          .height(STANDARD_MAX_COMPONENT_HEIGHT)
122          .objectFit(ImageFit.Contain)
123          .fitOriginalSize(true)
124          .offset({
125            x: FOREGROUND_IMAGE_OFFSET_X,
126            y: FOREGROUND_IMAGE_OFFSET_Y
127          })
128          .alignSelf(ItemAlign.End)
129      }
130      .padding({ left: OPERATE_AREA_AVOID_WIDTH })
131      .direction(Direction.Rtl)
132      .defaultFocus(true)
133      .align(Alignment.End)
134      .alignSelf(ItemAlign.End)
135      .constraintSize({
136        maxWidth: '50%',
137        minWidth: '40%'
138      })
139
140      Flex({
141        direction: params.options.titlePosition === TitlePosition.BOTTOM ?
142          FlexDirection.ColumnReverse : FlexDirection.Column,
143        justifyContent: FlexAlign.Center
144      }) {
145        Text(params.options.title)
146          .alignSelf(ItemAlign.Start)
147          .maxFontSize($r('sys.float.ohos_id_text_size_sub_title1'))
148          .minFontSize(16)
149          .fontColor(params.options.titleColor !== undefined ? params.options.titleColor : TITLE_DEFAULT_COLOR)
150          .fontWeight(FontWeight.Bold)
151          .margin(params.options.titlePosition ? { top: TITLE_LINE_DISTANCE } : { bottom: TITLE_LINE_DISTANCE })
152          .maxLines(TITLE_MAX_LINE)
153          .wordBreak(WordBreak.BREAK_WORD)
154          .textOverflow({ overflow: TextOverflow.Ellipsis })
155        Text(params.options.subtitle)
156          .alignSelf(ItemAlign.Start)
157          .maxFontSize($r('sys.float.ohos_id_text_size_caption'))
158          .minFontSize(9)
159          .fontColor(params.options.subtitleColor !== undefined ? params.options.subtitleColor : SUBTITLE_DEFAULT_COLOR)
160          .maxLines(SUBTITLE_MAX_LINE)
161          .wordBreak(WordBreak.BREAK_WORD)
162          .textOverflow({ overflow: TextOverflow.Ellipsis })
163      }
164      .constraintSize({
165        maxWidth: '60%',
166        minWidth: '50%'
167      })
168      .flexGrow(1)
169      .margin({ left: TEXT_LEFT_MARGIN_SIZE })
170    }
171    .backgroundColor(params.options.backgroundImage === undefined ? '#EBEEF5' : 'rgba(0,0,0,0)')
172    .shadow({
173      radius: DIALOG_SHADOW_RADIUS,
174      offsetX: 0,
175      offsetY: DIALOG_SHADOW_OFFSET_Y,
176      color: DIALOG_SHADOW_COLOR
177    })
178    .height(STANDARD_MIN_COMPONENT_HEIGHT)
179    .width('100%')
180    .alignSelf(ItemAlign.End)
181    .direction(Direction.Rtl)
182    .zIndex(1)
183    .borderRadius({
184      topLeft: DIALOG_BORDER_RADIUS,
185      topRight: DIALOG_BORDER_RADIUS,
186      bottomLeft: DIALOG_BORDER_RADIUS,
187      bottomRight: DIALOG_BORDER_RADIUS
188    })
189    .onClick(() => {
190      if (params.options.onDialogClick !== undefined) {
191        params.options.onDialogClick()
192      }
193      params.defaultCloseAction()
194    })
195
196    Image(params.options.backgroundImage)
197      .width('100%')
198      .height(STANDARD_MIN_COMPONENT_HEIGHT)
199      .offset({ x: '-100%', y: 0 })
200      .borderRadius(DIALOG_BORDER_RADIUS)
201      .zIndex(0)
202      .alignSelf(ItemAlign.End)
203      .onClick(() => {
204        if (params.options.onDialogClose !== undefined) {
205          params.options.onDialogClose()
206        }
207        params.defaultCloseAction()
208      })
209  }
210  .backgroundColor('rgba(0,0,0,0)')
211  .width('100%')
212  .height(STANDARD_MAX_COMPONENT_HEIGHT)
213  .padding({
214    left: DIALOG_INNER_PADDING_SIZE,
215    right: DIALOG_INNER_PADDING_SIZE
216  })
217  .constraintSize({
218    minHeight: STANDARD_MIN_COMPONENT_HEIGHT,
219    maxHeight: STANDARD_MAX_COMPONENT_HEIGHT,
220    maxWidth: DIALOG_MAX_WIDTH
221  })
222}
223
224declare interface DialogOptions {
225  uiContext: UIContext,
226  bottomOffsetType?: BottomOffset,
227  title?: ResourceStr,
228  subtitle?: ResourceStr,
229  titleColor?: ResourceStr | Color,
230  subtitleColor?: ResourceStr | Color,
231  backgroundImage?: Resource,
232  foregroundImage?: Resource,
233  iconStyle?: IconStyle,
234  titlePosition?: TitlePosition,
235  onDialogClick?: Callback<void>,
236  onDialogClose?: Callback<void>
237}
238
239export class InterstitialDialogAction {
240  private uiContext: UIContext;
241  private contentNode: ComponentContent<Object>;
242  private dialogParam: DialogParams;
243  private bottomOffsetType?: BottomOffset;
244
245  constructor(dialogOptions: DialogOptions) {
246    this.uiContext = dialogOptions.uiContext;
247    this.bottomOffsetType = dialogOptions.bottomOffsetType;
248    this.dialogParam = new DialogParams(
249      dialogOptions,
250      () => {
251        this.closeDialog()
252      }
253    );
254    this.contentNode = new ComponentContent(this.uiContext, wrapBuilder(dialogBuilder), this.dialogParam)
255  }
256
257  openDialog() {
258    if (this.contentNode !== null) {
259      this.uiContext.getPromptAction().openCustomDialog(this.contentNode, {
260        isModal: false,
261        autoCancel: false,
262        offset: {
263          dx: DIALOG_OFFSET_X,
264          dy: this.bottomOffsetType === BottomOffset.OFFSET_FOR_BAR ?
265            DIALOG_OFFSET_Y_FOR_BAR : DIALOG_OFFSET_Y_FOR_NONE
266        },
267        alignment: DialogAlignment.Bottom,
268        transition: TransitionEffect.asymmetric(
269          TransitionEffect.OPACITY.animation({ duration: 150, curve: Curve.Sharp })
270            .combine(TransitionEffect.scale({ x: 0.85, y: 0.85, centerX: '50%', centerY: '85%' })
271              .animation({ curve: curves.interpolatingSpring(0, 1, 228, 24)}))
272          ,
273          TransitionEffect.OPACITY.animation({ duration: 250, curve: Curve.Sharp })
274            .combine(TransitionEffect.scale({ x: 0.85, y: 0.85, centerX: '50%', centerY: '85%' })
275              .animation({ duration: 250, curve: Curve.Friction }))
276        )
277      })
278        .catch((error: BusinessError) => {
279          let message = (error as BusinessError).message
280          let code = (error as BusinessError).code
281          console.error(`${code}: ${message}`);
282        })
283    }
284  }
285
286  closeDialog() {
287    if (this.contentNode !== null) {
288      this.uiContext.getPromptAction().closeCustomDialog(this.contentNode)
289        .catch((error: BusinessError) => {
290          let message = (error as BusinessError).message
291          let code = (error as BusinessError).code
292          console.error(`${code}: ${message}`);
293        })
294    }
295  }
296}