1/**
2 * Copyright (c) 2022-2023 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 { MenuOperation, WindowUtil } from '@ohos/common';
18import {
19  Action,
20  AddMenuOperation,
21  BatchDeleteMenuOperation,
22  BroadCast,
23  BroadCastConstants,
24  BroadCastManager,
25  Constants,
26  DateUtil,
27  DeleteMenuOperation,
28  Log,
29  MediaItem,
30  MediaOperationType,
31  MenuContext,
32  MenuOperationFactory,
33  ScreenManager,
34  ShareMenuOperation,
35  TimelineData,
36  TimelineSelectManager,
37  TraceControllerUtils,
38  UiUtil,
39  ViewData,
40  ViewType
41} from '@ohos/common';
42import {
43  BrowserController,
44  CustomDialogView,
45  ImageGridItemComponent,
46  NoPhotoIndexComponent,
47} from '@ohos/common/CommonComponents';
48import { TimelineDataSource } from '../model/TimelineDataSource';
49import { TimelineTitleComponent } from './TimelineTitleComponent';
50import { TimelinePageActionBar } from './TimelinePageActionBar';
51import { TimelinePageToolBar } from './TimelinePageToolBar';
52import router from '@ohos.router';
53import { TimelineDataSourceManager } from '../model/TimelineDataSourceManager';
54import { TimelineScrollBar } from './TimelineScrollBar';
55
56const TAG: string = 'TimelinePage';
57AppStorage.setOrCreate('timelinePageIndex', Constants.INVALID);
58
59interface Params {
60  albumName: string;
61  albumUri: string;
62  pageType: string;
63  pageFrom: number;
64};
65
66// PHOTO Page
67@Component
68export struct TimelinePage {
69  @Provide isEmpty: boolean = false;
70  @State isShowScrollBar: boolean = false;
71  @StorageLink('isHorizontal') isHorizontal: boolean = ScreenManager.getInstance().isHorizontal();
72  @State gridRowCount: number = 0;
73  @Consume @Watch('updateRightClickMenuList') isSelectedMode: boolean;
74  @Consume('isShowSideBar') @Watch('initGridRowCount') isSidebar: boolean;
75  @Provide isAllSelected: boolean = false;
76  @State totalSelectedCount: number = 0;
77  @Provide broadCast: BroadCast = TimelineDataSourceManager.getInstance().getBroadCast();
78  @Consume @Watch('onIndexPageShow') isShow: boolean;
79  @StorageLink('timelinePageIndex') @Watch('onIndexChange') timelinePageIndex: number = Constants.INVALID;
80  @StorageLink('isSplitMode') isSplitMode: boolean = ScreenManager.getInstance().isSplitMode();
81  @StorageLink('leftBlank') leftBlank: number[] =
82    [0, ScreenManager.getInstance().getStatusBarHeight(), 0, ScreenManager.getInstance().getNaviBarHeight()];
83  dataSource: TimelineDataSource | null = null;
84  mSelectManager: TimelineSelectManager = new TimelineSelectManager();
85  scroller: Scroller = new Scroller();
86  appBroadCast: BroadCast = BroadCastManager.getInstance().getBroadCast();
87  isInCurrentTab = true; // Is it on the current tab page
88  isDataFreeze = false; // Is the page data frozen
89  deleteMode = false; // Is delete mode
90  isActive = false; // Is the page active
91  routerStart = false; // Is move or copy router page
92  @Provide moreMenuList: Action[] = [];
93  @Provide rightClickMenuList: Action[] = [];
94  @State groupSelectMode: boolean[] = [];
95  @Provide yearData: TimelineData[] = [];
96  @Provide dateText: string = '';
97  @Provide isShowBar: boolean = true;
98  @StorageLink('placeholderIndex') @Watch('onPlaceholderChanged') placeholderIndex: number = -1;
99  @ObjectLink browserController: BrowserController;
100  @Provide hidePopup: boolean = false;
101  // 选择模式下,鼠标对着未勾选项按右键弹框时,移动和复制菜单点击事件的标识位
102  private isMvOrCpSeparatesItem: boolean = false;
103  private mvOrCpSeparatesItem?: MediaItem;
104  private onWindowSizeChangeCallBack: Function = (): void => this.initGridRowCount();
105  private backPressEventFunc: Function = (callback: Function): void => this.onIndexBackPress(callback);
106  private onTableChangedFunc: Function = (index: number): void => this.onTabChanged(index);
107  private resetStateEventFunc: Function = (index: number): void => this.onStateReset(index);
108  private updateDataSourceFunc: Function = (item: MediaItem): void => this.onUpdateFavorState(item);
109  private resetZeroFunc: Function = (pageNumber: number): void => this.resetZero(pageNumber);
110  private onLoadingFinishedFunc: Function = (size: number): void => this.onLoadingFinished(size);
111  private selectFunc: Function = (index: number, id: string, isSelected: boolean, callback?: Function): void =>
112  this.select(index, id, isSelected, callback);
113  private groupSelectFunc: Function = (position: number): void => this.groupSelect(position);
114  private jumpPhotoBrowserFunc: Function = (name: string, item: MediaItem,
115                                            geometryTapIndex: number, geometryTransitionString: string): void =>
116  this.jumpPhotoBrowser(name, item, geometryTapIndex, geometryTransitionString);
117  private jumpThirdPhotoBrowserFunc: Function = (name: string, item: MediaItem,
118                                                 geometryTapIndex: number, geometryTransitionString: string): void =>
119  this.jumpThirdPhotoBrowser(name, item, geometryTapIndex, geometryTransitionString);
120  private onDataReloadedFunc: Function = (): void => this.onDataReloaded();
121  private initDateTextFunc: Function = (): void => this.initDateText();
122  @State layoutOptions: GridLayoutOptions = {
123    regularSize: [1, 1],
124    irregularIndexes: [],
125  }
126
127  onPlaceholderChanged() {
128    Log.debug(TAG, 'onPlaceholderChanged placeholderIndex is ' + this.placeholderIndex);
129    if (this.placeholderIndex != -1) {
130      this.scroller.scrollToIndex(this.placeholderIndex);
131    }
132  }
133
134  aboutToAppear(): void {
135    TraceControllerUtils.startTrace('TimelinePageAboutToAppear');
136    Log.info(TAG, 'aboutToAppear begin');
137    let self = this;
138    this.dataSource = TimelineDataSourceManager.getInstance().getDataSource();
139    let params: Params = router.getParams() as Params;
140    if (params != null && params.pageFrom && params.pageFrom == Constants.ENTRY_FROM.CAMERA) {
141      this.dataSource.initData();
142    }
143    this.mSelectManager.setGroupData(this.dataSource.getGroupData());
144    this.mSelectManager.setTotalCount(this.dataSource.getMediaCount());
145    this.moreMenuList = [Action.ADD, Action.INFO];
146    this.updateRightClickMenuList();
147    this.dataSource.registerCallback('updateGroupData', (newState: TimelineData[]) => {
148      self.mSelectManager.updateGroupData(newState);
149      self.updateYearMap();
150      self.updateLayoutOptions(newState);
151    });
152    this.dataSource.registerCallback('updateCount', (newState: number) => {
153      Log.info(TAG, `updateCount ${newState}`);
154      self.isEmpty = !Boolean(newState);
155      self.isShowScrollBar = (newState > Constants.PHOTOS_CNT_FOR_HIDE_SCROLL_BAR);
156      self.mSelectManager.setTotalCount(newState);
157    });
158
159    // 后续phone缩略图支持横竖屏后再放开
160    if (AppStorage.Get('deviceType') as string !== Constants.DEFAULT_DEVICE_TYPE) {
161      ScreenManager.getInstance().on(ScreenManager.ON_WIN_SIZE_CHANGED, this.onWindowSizeChangeCallBack);
162    }
163
164    this.appBroadCast.on(BroadCastConstants.BACK_PRESS_EVENT, this.backPressEventFunc);
165    this.appBroadCast.on(BroadCastConstants.ON_TAB_CHANGED, this.onTableChangedFunc);
166    this.appBroadCast.on(BroadCastConstants.RESET_STATE_EVENT, this.resetStateEventFunc);
167    this.appBroadCast.on(BroadCastConstants.UPDATE_DATA_SOURCE, this.updateDataSourceFunc);
168    this.appBroadCast.on(BroadCastConstants.RESET_ZERO, this.resetZeroFunc);
169    this.broadCast.on(Constants.ON_LOADING_FINISHED, this.onLoadingFinishedFunc);
170
171    Log.info(TAG, 'aboutToAppear doing');
172    this.mSelectManager.setPhotoDataImpl();
173
174    this.broadCast.on(BroadCastConstants.SELECT, this.selectFunc);
175    this.broadCast.on(BroadCastConstants.GROUP_SELECT, this.groupSelectFunc);
176    this.broadCast.on(BroadCastConstants.JUMP_PHOTO_BROWSER, this.jumpPhotoBrowserFunc);
177    this.broadCast.on(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER, this.jumpThirdPhotoBrowserFunc);
178    this.broadCast.on(BroadCastConstants.ON_DATA_RELOADED, this.onDataReloadedFunc);
179    this.broadCast.on(BroadCastConstants.INIT_DATE_TEXT, this.initDateTextFunc);
180
181    this.mSelectManager.registerCallback('allSelect', (newState: boolean) => {
182      Log.info(TAG, `allSelect ${newState}`);
183      if (this.isDataFreeze) {
184        return;
185      }
186      this.isAllSelected = newState;
187      if (this.dataSource != null) {
188          this.dataSource.forceUpdate();
189      }
190    });
191
192    this.mSelectManager.registerCallback('updateGroupCount', () => {
193      Log.info(TAG, 'updateGroupCount');
194      if (this.isDataFreeze) {
195        return;
196      }
197      this.updateGroupSelectMode();
198    });
199
200    this.mSelectManager.registerCallback('updateCount', (newState: number) => {
201      Log.info(TAG, `mSelectManager updateCount ${newState}`);
202      if (this.isDataFreeze) {
203        return;
204      }
205      this.totalSelectedCount = newState;
206      this.moreMenuList = Boolean(newState) ? [Action.ADD, Action.INFO]
207        : [Action.ADD_INVALID, Action.INFO_INVALID];
208    });
209    this.initGridRowCount();
210    this.updateGroupSelectMode();
211    this.updateYearMap();
212    this.onActive();
213    this.updateLayoutOptions(this.dataSource.groups);
214    TraceControllerUtils.finishTrace('TimelinePageAboutToAppear');
215  }
216
217  updateLayoutOptions(groups: TimelineData[]): void {
218    this.layoutOptions.irregularIndexes && this.layoutOptions.irregularIndexes.pop();
219    let currentTitleIndex = 0;
220    let count: number[] = [];
221    count.push(currentTitleIndex);
222    for (let index = 0; index < groups.length - 1; index++) {
223      currentTitleIndex = currentTitleIndex + groups[index].count + 1;
224      count.push(currentTitleIndex);
225    }
226    this.layoutOptions = {
227      regularSize: [1, 1],
228      irregularIndexes: count,
229    };
230  }
231
232  private onLoadingFinished(size: number): void {
233    Log.info(TAG, `ON_LOADING_FINISHED size: ${size}`);
234  }
235
236  private select(index: number, id: string, isSelected: boolean, callback?: Function): void {
237    if (this.mSelectManager.toggle(id, isSelected, index)) {
238      if (!this.isSelectedMode) {
239        this.isSelectedMode = true;
240      }
241    }
242    if (callback) {
243      callback();
244    }
245  }
246
247  private groupSelect(position: number): void {
248    Log.info(TAG, `GROUP_SELECT ${position}`);
249    if (this.mSelectManager.toggleGroup(this.mSelectManager.getTitleCoordinate(position))) {
250      this.totalSelectedCount = this.mSelectManager.getSelectedCount();
251    }
252  }
253
254  private jumpPhotoBrowser(name: string, item: MediaItem,
255                           geometryTapIndex: number, geometryTransitionString: string): void {
256    let targetIndex = this.dataSource == null ? Constants.NOT_FOUND : this.dataSource.getDataIndex(item);
257    if (targetIndex == Constants.NOT_FOUND) {
258      Log.error(TAG, 'targetIndex is not found');
259      return;
260    }
261    AppStorage.setOrCreate(Constants.APP_KEY_PHOTO_BROWSER, this.dataSource);
262    if (geometryTapIndex !== undefined && geometryTransitionString !== undefined) {
263      this.jumpToPhotoBrowserGeometryTransition(targetIndex, name, item, geometryTapIndex, geometryTransitionString);
264    } else {
265      this.jumpToPhotoBrowserNormal(targetIndex, name, item);
266    }
267  }
268
269  private jumpThirdPhotoBrowser(name: string, item: MediaItem,
270                                geometryTapIndex: number, geometryTransitionString: string): void {
271    let targetIndex = this.dataSource == null ? Constants.NOT_FOUND : this.dataSource.getDataIndex(item);
272    if (targetIndex == Constants.NOT_FOUND) {
273      Log.error(TAG, 'targetIndex is not found');
274      return;
275    }
276    Log.info(TAG, `JUMP_THIRD_PHOTO_BROWSER.index: ${targetIndex} transition: ${name}`);
277    AppStorage.setOrCreate(Constants.PHOTO_GRID_SELECT_MANAGER, this.mSelectManager);
278    AppStorage.setOrCreate(Constants.APP_KEY_PHOTO_BROWSER, this.dataSource);
279    if (geometryTapIndex !== undefined && geometryTransitionString !== undefined) {
280      this.jumpToSelectPhotoBrowserGeometryTransition(
281        targetIndex, name, item, geometryTapIndex, geometryTransitionString);
282    } else {
283      this.jumpToSelectPhotoBrowserNormal(targetIndex, name, item);
284    }
285  }
286
287  private onDataReloaded(): void {
288    Log.info(TAG, 'ON_DATA_RELOADED');
289    if (this.deleteMode) {
290      animateTo({
291        duration: 300, // 删除动画时长
292        curve: Curves.cubicBezier(0.0, 0.0, 0.2, 1.0) // 减速曲线参数
293      }, (): void => {
294        this.dataSource?.onDataReloaded();
295      })
296      this.deleteMode = false;
297    } else {
298      if (this.dataSource != null) {
299          this.dataSource.onDataReloaded();
300      }
301    }
302  }
303
304  private initDateText(): void {
305    let scrollMediaItem: MediaItem = this.dataSource == null ? new MediaItem() :
306      this.dataSource.getMediaItemByPosition(0) as MediaItem;
307    this.dateText = DateUtil.getLocalizedYearAndMonth(scrollMediaItem.getDataTaken());
308  }
309
310  jumpToPhotoBrowserNormal(targetIndex: number, name: string, item: MediaItem) {
311    router.pushUrl({
312      url: 'pages/PhotoBrowser',
313      params: {
314        position: targetIndex,
315        transition: name,
316        leftBlank: this.leftBlank,
317      }
318    });
319  }
320
321  jumpToPhotoBrowserGeometryTransition(targetIndex: number, name: string, item: MediaItem, geometryTapIndex: number,
322                                       geometryTransitionString: string) {
323    interface Msg {
324      position: number;
325      transition: string;
326      leftBlank: number[];
327    }
328
329    const params: Msg = {
330      position: targetIndex,
331      transition: name,
332      leftBlank: this.leftBlank,
333    };
334    this.browserController.showBrowser(geometryTapIndex, geometryTransitionString, TAG, params);
335  }
336
337  jumpToSelectPhotoBrowserNormal(targetIndex: number, name: string, item: MediaItem) {
338    router.pushUrl({
339      url: 'pages/SelectPhotoBrowser',
340      params: {
341        position: targetIndex,
342        transition: name,
343      }
344    });
345  }
346
347  jumpToSelectPhotoBrowserGeometryTransition(targetIndex: number, name: string, item: MediaItem,
348                                             geometryTapIndex: number, geometryTransitionString: string) {
349    interface Params {
350      position: number;
351      transition: string;
352      leftBlank: number[];
353    }
354
355    const params: Params = {
356      position: targetIndex,
357      transition: name,
358      leftBlank: this.leftBlank,
359    };
360    this.browserController.showSelectBrowser(geometryTapIndex, geometryTransitionString, TAG, params);
361  }
362
363  onPageShow() {
364  }
365
366  aboutToDisappear(): void {
367    Log.debug(TAG, 'aboutToDisappear');
368    ScreenManager.getInstance().off(ScreenManager.ON_WIN_SIZE_CHANGED, this.onWindowSizeChangeCallBack);
369    this.dataSource?.unregisterCallback('updateGroupData');
370    this.dataSource?.unregisterCallback('updateCount');
371    if (this.appBroadCast != null) {
372      this.appBroadCast.off(BroadCastConstants.BACK_PRESS_EVENT, this.backPressEventFunc);
373      this.appBroadCast.off(BroadCastConstants.ON_TAB_CHANGED, this.onTableChangedFunc);
374      this.appBroadCast.off(BroadCastConstants.RESET_STATE_EVENT, this.resetStateEventFunc);
375      this.appBroadCast.off(BroadCastConstants.UPDATE_DATA_SOURCE, this.updateDataSourceFunc);
376      this.appBroadCast.off(BroadCastConstants.RESET_ZERO, this.resetZeroFunc);
377    }
378    if (this.broadCast != null) {
379      this.broadCast.off(Constants.ON_LOADING_FINISHED, this.onLoadingFinishedFunc);
380      this.broadCast.off(BroadCastConstants.SELECT, this.selectFunc);
381      this.broadCast.off(BroadCastConstants.GROUP_SELECT, this.groupSelectFunc);
382      this.broadCast.off(BroadCastConstants.JUMP_PHOTO_BROWSER, this.jumpPhotoBrowserFunc);
383      this.broadCast.off(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER, this.jumpThirdPhotoBrowserFunc);
384      this.broadCast.off(BroadCastConstants.ON_DATA_RELOADED, this.onDataReloadedFunc);
385      this.broadCast.off(BroadCastConstants.INIT_DATE_TEXT, this.initDateTextFunc);
386    }
387  }
388
389  updateGroupSelectMode(): void {
390    let groups: TimelineData[] = this.dataSource == null ? [] : this.dataSource.groups;
391    if (this.groupSelectMode.length == 0) {
392      Log.info(TAG, 'first updateGroupSelectMode');
393      for (let i = 0; i < groups.length; i++) {
394        this.groupSelectMode.push(this.mSelectManager.isGroupSelected(i));
395      }
396    } else {
397      Log.info(TAG, 'no first updateGroupSelectMode');
398      for (let i = 0; i < groups.length; i++) {
399        Log.info(TAG, 'update one');
400        this.groupSelectMode[i] = this.mSelectManager.isGroupSelected(i);
401      }
402    }
403  }
404
405  updateRightClickMenuList() {
406    this.rightClickMenuList = this.isSelectedMode
407      ? [Action.DELETE, Action.ADD, Action.INFO]
408      : [Action.MULTISELECT, Action.DELETE, Action.ADD, Action.INFO];
409  }
410
411  updateYearMap(): void {
412    Log.info(TAG, 'updateYearMap');
413    let groups: TimelineData[] = this.dataSource == null ? [] : this.dataSource.groups;
414    if (groups.length == 0) {
415      Log.error(TAG, 'year length is 0');
416      return;
417    }
418    this.yearData = [];
419
420    let startGroup: TimelineData = groups[0];
421    let count: number = startGroup.count as number;
422    let startTime: number = startGroup.startDate as number;
423    let endTime: number = startGroup.startDate as number;
424
425    for (let i = 1; i < groups.length; i++) {
426      let dateTaken: number = groups[i].startDate as number;
427      if (DateUtil.isTheSameYear(startTime, dateTaken)) {
428        count = count + groups[i].count as number;
429        endTime = dateTaken;
430      } else {
431        let groupData = new TimelineData(startTime, endTime, count);
432        this.yearData.push(groupData);
433        count = groups[i].count as number;
434        startTime = dateTaken;
435        endTime = dateTaken;
436      }
437    }
438    let groupData = new TimelineData(startTime, endTime, count);
439    this.yearData.push(groupData);
440    Log.info(TAG, 'updateYearMap end');
441  }
442
443  onIndexChange(): void {
444    Log.info(TAG, `onIndexChange ${this.timelinePageIndex}`);
445    if (this.timelinePageIndex != Constants.INVALID && this.dataSource != null) {
446      this.scroller.scrollToIndex(this.dataSource.getPositionByIndex(this.timelinePageIndex));
447    }
448  }
449
450  onIndexBackPress(callback: Function): void {
451    if (this.isInCurrentTab) {
452      callback(this.onModeChange());
453    }
454  }
455
456  onTabChanged(index: number): void {
457    if (index == Constants.TIMELINE_PAGE_INDEX) {
458      this.isInCurrentTab = true;
459      this.onActive();
460    } else {
461      this.isInCurrentTab = false;
462      this.onModeChange();
463      this.onInActive();
464    }
465  }
466
467  onStateReset(index: number): void {
468    if (index == Constants.TIMELINE_PAGE_INDEX) {
469      this.onModeChange();
470    }
471  }
472
473  resetZero(pageNumber: number): void {
474    if (pageNumber == Constants.TIMELINE_PAGE_INDEX) {
475      this.scroller.scrollEdge(Edge.Top);
476    }
477  }
478
479  onMenuClicked(action: Action): void {
480    Log.info(TAG, `onMenuClicked, actionID: ${action.actionID}`);
481    let menuContext: MenuContext;
482    let menuOperation: MenuOperation;
483    if (action === Action.CANCEL) {
484      this.onModeChange();
485    } else if (action === Action.MULTISELECT) {
486      this.isSelectedMode = true;
487    } else if (action === Action.SELECT_ALL) {
488      this.mSelectManager.selectAll(true);
489    } else if (action === Action.DESELECT_ALL) {
490      this.mSelectManager.deSelectAll();
491    } else if (action === Action.DELETE) {
492      menuContext = new MenuContext();
493      menuContext
494        .withSelectManager(this.mSelectManager)
495        .withFromSelectMode(this.isSelectedMode)
496        .withOperationStartCallback((): void => this.onDeleteStart())
497        .withOperationEndCallback((): void => this.onDeleteEnd())
498        .withBroadCast(this.broadCast);
499      menuOperation = MenuOperationFactory.getInstance()
500        .createMenuOperation(BatchDeleteMenuOperation, menuContext);
501      menuOperation.doAction();
502    } else if (action === Action.SHARE) {
503      menuContext = new MenuContext();
504      menuContext.withFromSelectMode(true).withSelectManager(this.mSelectManager);
505      menuOperation = MenuOperationFactory.getInstance()
506        .createMenuOperation(ShareMenuOperation, menuContext);
507      menuOperation.doAction();
508    } else if (action === Action.INFO) {
509      this.hidePopup = true;
510      this.openDetailsDialog();
511    } else if (action === Action.ADD) {
512      this.mSelectManager.getSelectedItems((selectedItems: Array<MediaItem>) => {
513        Log.info(TAG, `Get selected items success, size: ${selectedItems.length}`);
514        this.routeToSelectAlbumPage(MediaOperationType.Add, selectedItems);
515      })
516    }
517  }
518
519  routeToSelectAlbumPage(pageType: string, selectedItems: Array<MediaItem>): void {
520    Log.info(TAG, 'Route to select album page');
521    router.pushUrl({
522      url: 'pages/MediaOperationPage',
523      params: {
524        pageFrom: Constants.MEDIA_OPERATION_FROM_TIMELINE,
525        pageType: pageType,
526        selectedItems: selectedItems
527      }
528    });
529    this.routerStart = true;
530  }
531
532  async openDetailsDialog(): Promise<void> {
533    if (this.totalSelectedCount == 0) {
534      Log.error(TAG, 'no select error');
535      return;
536    } else if (this.totalSelectedCount == 1) {
537      Log.info(TAG, 'totalSelectedCount is 1');
538      await this.mSelectManager.getSelectedItems((selectItems: MediaItem[]) => {
539        Log.info(TAG, `openDetailsDialog selectItems.length: ${selectItems.length}`);
540        if (selectItems.length != 1) {
541          Log.error(TAG, 'get selectItems is error');
542          return;
543        }
544        this.broadCast.emit(BroadCastConstants.SHOW_DETAIL_DIALOG, [selectItems[0], false]);
545      });
546    } else {
547      await this.mSelectManager.getSelectedItems((selectItems: MediaItem[]) => {
548        Log.info(TAG, `openDetailsDialog selectItems.length: ${selectItems.length}`);
549        if (selectItems.length <= 1) {
550          Log.error(TAG, 'get selectItems is error');
551          return;
552        }
553        let size = 0;
554        selectItems.forEach((item) => {
555          Log.info(TAG, `openDetailsDialog item.size: ${item.size}`);
556          size = size + item.size;
557        })
558
559        Log.info(TAG, `openDetailsDialog size: ${size}`);
560        this.broadCast.emit(BroadCastConstants.SHOW_MULTI_SELECT_DIALOG, [this.totalSelectedCount, size]);
561      });
562      return;
563    }
564  }
565
566  onDeleteStart(): void {
567    Log.info(TAG, `onDeleteStart`);
568    this.deleteMode = true;
569    this.isDataFreeze = true;
570    if (this.dataSource != null) {
571      this.dataSource.unregisterTimelineObserver();
572      this.dataSource.freeze();
573    }
574  }
575
576  onDeleteEnd(): void {
577    Log.info(TAG, `onDeleteEnd`);
578    this.isDataFreeze = false;
579    this.onModeChange();
580    if (this.dataSource != null) {
581      this.dataSource.registerTimelineObserver();
582      this.dataSource.onChange('image');
583      this.dataSource.unfreeze();
584    }
585  }
586
587  onCopyStart(): void {
588    Log.info(TAG, `onCopyStart`);
589    this.isDataFreeze = true;
590    if (this.dataSource != null) {
591      this.dataSource.unregisterTimelineObserver();
592      this.dataSource.freeze();
593    }
594
595  }
596
597  onCopyEnd(err: Object, count: number, total: number): void {
598    Log.info(TAG, `onCopyEnd count: ${count}, total: ${total}`);
599    this.isDataFreeze = false;
600    this.onModeChange();
601    if (this.dataSource != null) {
602      this.dataSource.registerTimelineObserver();
603      this.dataSource.onChange('image');
604      this.dataSource.unfreeze();
605    }
606    if (err) {
607      UiUtil.showToast($r('app.string.copy_failed_single'));
608    }
609  }
610
611  onMoveStart(): void {
612    Log.info(TAG, `onMoveStart`);
613    this.isDataFreeze = true;
614    if (this.dataSource != null) {
615      this.dataSource.unregisterTimelineObserver();
616      this.dataSource.freeze();
617    }
618  }
619
620  onMoveEnd(err: Object, count: number, total: number): void {
621    Log.info(TAG, `onMoveEnd count: ${count}, total: ${total}`);
622    this.isDataFreeze = false;
623    this.onModeChange();
624    if (this.dataSource != null) {
625      this.dataSource.registerTimelineObserver();
626      this.dataSource.unfreeze();
627      this.dataSource.switchRefreshOn();
628      this.dataSource.onChange('image');
629    }
630    if (err) {
631      UiUtil.showToast($r('app.string.move_failed_single'));
632    }
633  }
634
635  onModeChange() {
636    Log.debug(TAG, `onModeChange current mode ${this.isSelectedMode}`);
637    if (this.isSelectedMode) {
638      this.isSelectedMode = false;
639      this.isAllSelected = false;
640      this.mSelectManager.onModeChange(false);
641      this.updateGroupSelectMode();
642      AppStorage.delete(Constants.PHOTO_GRID_SELECT_MANAGER);
643      return true;
644    }
645    return false;
646  }
647
648  // The callbacks after index page shows
649  onIndexPageShow() {
650    Log.info(TAG, `[onIndexPageShow] isShow=${this.isShow}, isInCurrentTab=${this.isInCurrentTab}`);
651    if (this.isShow && this.isInCurrentTab) {
652      let params: Params = router.getParams() as Params;
653      if (this.routerStart && params != null && params.pageType != null) {
654        Log.info(TAG, `MediaOperation back ${JSON.stringify(params)}`)
655        if (params.pageType === MediaOperationType.Add) {
656          this.addOperation(params.albumName, params.albumUri);
657        }
658      }
659      this.routerStart = false;
660      this.onActive();
661    } else if (!this.isShow && this.isInCurrentTab) {
662      this.onInActive();
663    } else {
664    }
665  }
666
667  // The callback when current page is in the foreground
668  onActive() {
669    if (!this.isActive) {
670      Log.info(TAG, 'onActive');
671      this.isActive = true;
672
673      this.dataSource?.onActive();
674      if (this.isSelectedMode) {
675        this.totalSelectedCount = this.mSelectManager.getSelectedCount();
676        this.dataSource?.forceUpdate();
677      }
678    }
679  }
680
681  // The callback when current page is in the background
682  onInActive() {
683    if (this.isActive) {
684      Log.info(TAG, 'onInActive');
685      this.isActive = false;
686      this.dataSource?.onInActive();
687    }
688  }
689
690  getGeometryTransitionId(item: ViewData, index: number): string {
691    let mediaItem = item.mediaItem as MediaItem;
692    if (mediaItem) {
693      return TAG + mediaItem.getHashCode() + this.mSelectManager.isItemSelected(mediaItem.uri, item.viewIndex);
694    } else {
695      return TAG + item.viewIndex;
696    }
697  }
698
699  build() {
700    Stack() {
701      Column() {
702        if (this.isEmpty) {
703          NoPhotoIndexComponent({ index: Constants.TIMELINE_PAGE_INDEX, hasBarSpace: true })
704        } else {
705          TimelinePageActionBar({
706            onMenuClicked: (action: Action): void => this.onMenuClicked(action),
707            totalSelectedCount: $totalSelectedCount
708          });
709
710          Stack() {
711            Grid(this.scroller, this.layoutOptions) {
712              LazyForEach(this.dataSource as TimelineDataSource, (item: ViewData, index?: number) => {
713                if (!!item) {
714                  if (item.viewType == ViewType.GROUP_TITLE) {
715                    GridItem() {
716                      TimelineTitleComponent({
717                        groupData: item.viewData,
718                        mPosition: item.viewIndex,
719                        isSelected: this.groupSelectMode[item.viewIndex]
720                      })
721                    }
722                    .key('TimelinePage_GridItem' + index)
723                  } else if (item.viewType == ViewType.ITEM) {
724                    GridItem() {
725                      ImageGridItemComponent({
726                        dataSource: this.dataSource,
727                        item: item.mediaItem,
728                        mPosition: item.viewIndex,
729                        isSelected: this.isSelectedMode ?
730                        this.mSelectManager.isItemSelected((item.mediaItem as MediaItem).uri as string,
731                          item.viewIndex) : false,
732                        pageName: Constants.PHOTO_TRANSITION_TIMELINE,
733                        onMenuClicked: (action: Action): void => this.onMenuClicked(action),
734                        onMenuClickedForSingleItem: (action: Action, currentPhoto: MediaItem): void =>
735                        this.onMenuClickedForSingleItem(action, currentPhoto),
736                        geometryTransitionString: this.getGeometryTransitionId(item, index as number),
737                        selectedCount: $totalSelectedCount
738                      })
739                    }
740                    .aspectRatio(1)
741                    .key('TimelinePage_GridItem' + index)
742                    .zIndex(index === this.placeholderIndex ? 1 : 0)
743                  }
744                }
745              }, (item: ViewData, index?: number) => {
746                if (item == null || item == undefined) {
747                  return (JSON.stringify(item) + index) as string;
748                }
749                if (item.viewType == ViewType.GROUP_TITLE) {
750                  return (JSON.stringify(item.viewData) + this.groupSelectMode[item.viewIndex]) as string;
751                } else {
752                  return this.getGeometryTransitionId(item, index as number) as string;
753                }
754              })
755            }
756            .edgeEffect(EdgeEffect.Spring)
757            .columnsTemplate('1fr '.repeat(this.gridRowCount))
758            .scrollBar(BarState.Off)
759            .columnsGap(Constants.GRID_GUTTER)
760            .rowsGap(Constants.GRID_GUTTER)
761            .cachedCount(Constants.GRID_CACHE_ROW_COUNT)
762            .onScrollIndex((first) => {
763              let scrollMediaItem: MediaItem | null = this.dataSource == null ?
764                null : this.dataSource.getMediaItemByPosition(first) as MediaItem;
765              if (scrollMediaItem?.getDataTaken()) {
766                this.dateText = DateUtil.getLocalizedYearAndMonth(scrollMediaItem.getDataTaken());
767                Log.debug(TAG, `scrollIndex=${first}, dateTaken=${scrollMediaItem.getDataTaken()}`);
768              } else {
769                Log.warn(TAG, `scrollIndex ${first} out of active window`);
770              }
771            })
772
773            if (this.isShowScrollBar) {
774              TimelineScrollBar({ scroller: this.scroller })
775            }
776          }
777          .layoutWeight(1)
778        }
779      }
780      .alignItems(HorizontalAlign.Start)
781      .justifyContent(FlexAlign.Start)
782      .margin({
783        bottom: this.isHorizontal ? 0 : $r('app.float.tab_bar_vertical_height')
784      })
785
786      if (this.isSelectedMode) {
787        TimelinePageToolBar({
788          onMenuClicked: (action: Action): void => this.onMenuClicked(action),
789          totalSelectedCount: $totalSelectedCount
790        })
791      }
792      CustomDialogView({ broadCast: $broadCast })
793    }
794  }
795
796  private onMenuClickedForSingleItem(action: Action, currentPhoto: MediaItem) {
797    Log.info(TAG, `single menu click, action: ${action?.actionID}, currentUri: ${currentPhoto?.uri}`);
798    if (currentPhoto == undefined) {
799      return;
800    }
801    let menuOperation: MenuOperation;
802    let menuContext: MenuContext;
803    if (action === Action.DELETE) {
804      menuContext = new MenuContext();
805      menuContext.withMediaItem(currentPhoto).withBroadCast(this.broadCast);
806      menuOperation = MenuOperationFactory.getInstance()
807        .createMenuOperation(DeleteMenuOperation, menuContext);
808      menuOperation.doAction();
809    } else if (action === Action.ADD) {
810      this.isMvOrCpSeparatesItem = true;
811      this.mvOrCpSeparatesItem = currentPhoto;
812      this.routeToSelectAlbumPage(MediaOperationType.Add, [currentPhoto]);
813    } else if (action === Action.INFO) {
814      this.broadCast.emit(BroadCastConstants.SHOW_DETAIL_DIALOG, [currentPhoto, false]);
815    }
816  }
817
818  // Calculate the number of squares per row
819  private initGridRowCount(): void {
820    let sideBarWidth = this.isSidebar ? Constants.TAB_BAR_WIDTH : 0;
821    let contentWidth = ScreenManager.getInstance().getWinWidth() - sideBarWidth;
822    let margin = 0;
823    let maxThumbWidth = px2vp(Constants.GRID_IMAGE_SIZE) * Constants.GRID_MAX_SIZE_RATIO;
824    let newCount = Math.max(Constants.GRID_MIN_COUNT,
825      Math.round(((contentWidth - Constants.NUMBER_2 * margin) +
826      Constants.GRID_GUTTER) / (maxThumbWidth + Constants.GRID_GUTTER)));
827    if (newCount != this.gridRowCount) {
828      this.gridRowCount = newCount;
829    }
830    Log.info(TAG, `initGridRowCount contentWidth: ${contentWidth}`);
831  }
832
833  private async addOperation(albumName: string, albumUri: string) {
834    let menuContext = new MenuContext();
835    let onCopyStartFunc = (): void => this.onCopyStart();
836    if (this.isMvOrCpSeparatesItem) {
837      menuContext.withMediaItem(this.mvOrCpSeparatesItem as MediaItem);
838      this.onCopyStart && this.onCopyStart();
839      this.isMvOrCpSeparatesItem = false;
840      this.mvOrCpSeparatesItem = undefined;
841    } else {
842      menuContext.withSelectManager(this.mSelectManager).withOperationStartCallback(onCopyStartFunc);
843    }
844    menuContext.withOperationEndCallback((err: Object, count: number, total: number): void =>
845    this.onCopyEnd(err as Object, count, total))
846      .withBroadCast(this.broadCast)
847      .withTargetAlbumName(albumName).withAlbumUri(albumUri);
848    let menuOperation = MenuOperationFactory.getInstance().createMenuOperation(AddMenuOperation, menuContext);
849    menuOperation.doAction();
850  }
851
852  private onUpdateFavorState(item: MediaItem): void {
853    Log.debug(TAG, 'onUpdateFavorState favor');
854    if (this.dataSource != null) {
855      let index = this.dataSource.getIndexByMediaItem(item);
856      if (index == Constants.NOT_FOUND) {
857          return;
858      }
859      let flushIndex = this.dataSource.getPositionByIndex(index);
860      Log.debug(TAG, `onUpdateFavorState favor flushIndex ${flushIndex}`);
861      this.dataSource.onDataChanged(flushIndex);
862    }
863  }
864}
865