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 router from '@ohos.router';
17import {
18  Action,
19  AlbumInfo,
20  BroadCast,
21  BroadCastConstants,
22  BroadCastManager,
23  Constants,
24  JumpSourceToMain,
25  Log,
26  MediaDataSource,
27  MediaItem,
28  ScreenManager,
29  SelectManager,
30  TraceControllerUtils,
31  UiUtil,
32  ViewData,
33} from '@ohos/common';
34import {
35  BrowserController,
36  CustomDialogView,
37  GridScrollBar,
38  ImageGridItemComponent,
39  MoveOrCopyBroadCastProp,
40  NoPhotoComponent
41} from '@ohos/common/CommonComponents';
42import { AlbumSelectActionBar } from '@ohos/browser/BrowserComponents';
43import { PhotoBrowserComponent } from '../view/PhotoBrowserComponent';
44import { SelectPhotoBrowserView } from '../view/SelectPhotoBrowserView';
45
46const TAG: string = 'NewAlbumPage';
47AppStorage.setOrCreate('photoGridPageIndex', Constants.INVALID);
48
49interface Params {
50  item: string;
51}
52
53@Entry
54@Component
55export struct NewAlbumPage {
56  @State isEmpty: boolean = false;
57  @State isShowScrollBar: boolean = false;
58  @State gridRowCount: number = 0;
59  @Provide isSelectedMode: boolean = true;
60  @Provide isAllSelected: boolean = false;
61  @State totalSelectedCount: number = 0;
62  @StorageLink('isHorizontal') isHorizontal: boolean = ScreenManager.getInstance().isHorizontal();
63  @Provide broadCast: BroadCast = new BroadCast();
64  @Provide isShow: boolean = true;
65  @Provide isShowBar: boolean = true;
66  @State moreMenuList: Action[] = [];
67  @Provide rightClickMenuList: Action[] = [];
68  @State isClickScrollBar: boolean = false;
69  @StorageLink('photoGridPageIndex') @Watch('onIndexChange') photoGridPageIndex: number = Constants.INVALID;
70  @StorageLink('isSplitMode') isSplitMode: boolean = ScreenManager.getInstance().isSplitMode();
71  @StorageLink('leftBlank') leftBlank: number[] =
72    [0, ScreenManager.getInstance().getStatusBarHeight(), 0, ScreenManager.getInstance().getNaviBarHeight()];
73  title: string = '';
74  @StorageLink('placeholderIndex') @Watch('onPlaceholderChanged') placeholderIndex: number = -1;
75  @State pageStatus: boolean = false;
76  @State isRunningAnimation: boolean = false;
77  @State @Watch('updateAnimationStatus') browserController: BrowserController = new BrowserController(true);
78  private dataSource: MediaDataSource = new MediaDataSource(Constants.DEFAULT_SLIDING_WIN_SIZE);
79  private scroller: Scroller = new Scroller();
80  private isDataFreeze = false;
81  private mSelectManager: SelectManager | null = null;
82  private isActive = false;
83  private appBroadCast: BroadCast = BroadCastManager.getInstance().getBroadCast();
84  private isNewAlbum: boolean = AppStorage.get<boolean>(Constants.APP_KEY_NEW_ALBUM) as boolean;
85  private onWindowSizeChangeCallBack: Function = () => {
86    // 后续phone缩略图支持横竖屏后再放开
87  }
88  private onUpdateFavorStateFunc: Function = (item: MediaItem): void => this.onUpdateFavorState(item);
89  private selectFunc: Function = (position: number, key: string, value: boolean, callback: Function): void =>
90  this.select(position, key, value, callback);
91  private jumpPhotoBrowserFunc: Function = (name: string, item: MediaItem): void => this.jumpPhotoBrowser(name, item);
92  private jumpThirdPhotoBrowserFunc: Function = (name: string, item: MediaItem,
93                                                 geometryTapIndex: number, geometryTransitionString: string): void =>
94  this.jumpThirdPhotoBrowser(name, item, geometryTapIndex, geometryTransitionString);
95  private onDataReloadedFunc: Function = (size: number): void => this.onDataReloaded(size);
96  private onLoadingFinishedFunc: Function = (): void => this.onLoadingFinished();
97
98  private select(position: number, key: string, value: boolean, callback: Function): void {
99      if (this.mSelectManager?.toggle(key, value, position)) {
100      Log.info(TAG, 'enter event process');
101      callback();
102    }
103  }
104
105  private jumpPhotoBrowser(name: string, item: MediaItem): void {
106    let targetIndex = this.dataSource.getDataIndex(item);
107    if (targetIndex == Constants.NOT_FOUND) {
108      Log.error(TAG, 'targetIndex is not found');
109      return;
110    }
111    Log.info(TAG, `jump to photo browser at index: ${targetIndex}`);
112    AppStorage.setOrCreate(Constants.APP_KEY_PHOTO_BROWSER, this.dataSource);
113    interface Params {
114      position: number;
115      transition: string;
116      leftBlank: number[];
117    }
118    let params: Params = {
119      position: targetIndex,
120      transition: name,
121      leftBlank: this.leftBlank,
122    }
123    this.browserController.showBrowserWithNoAnimation(params);
124  }
125
126  private jumpThirdPhotoBrowser(name: string, item: MediaItem,
127                                geometryTapIndex: number, geometryTransitionString: string): void {
128    let targetIndex = this.dataSource.getDataIndex(item);
129    Log.info(TAG, `jump to photo browser, index: ${targetIndex}, transition: ${name}`);
130    AppStorage.setOrCreate(Constants.PHOTO_GRID_SELECT_MANAGER, this.mSelectManager);
131    AppStorage.setOrCreate(Constants.APP_KEY_PHOTO_BROWSER, this.dataSource);
132    interface Params {
133      position: number;
134      transition: string;
135      leftBlank: number[];
136    }
137
138    const params: Params = {
139      position: targetIndex,
140      transition: name,
141      leftBlank: this.leftBlank,
142    };
143    if (geometryTapIndex && geometryTransitionString) {
144      this.browserController.showSelectBrowser(geometryTapIndex, geometryTransitionString, TAG, params);
145    } else {
146      this.browserController.showSelectBrowserWithNoAnimation(params);
147    }
148  }
149
150  private onDataReloaded(size: number): void {
151    Log.info(TAG, `ON_LOADING_FINISHED size: ${size}`);
152    this.isEmpty = size == 0;
153    Log.info(TAG, `isEmpty: ${this.isEmpty}`);
154  }
155
156  private onLoadingFinished(): void {
157    Log.info(TAG, 'ON_DATA_RELOADED');
158    this.dataSource.onDataReloaded();
159  }
160
161  onIndexChange() {
162    Log.info(TAG, `onIndexChange ${this.photoGridPageIndex}`)
163    if (this.photoGridPageIndex != Constants.INVALID) {
164      this.scroller.scrollToIndex(this.photoGridPageIndex);
165    }
166  }
167
168  onPlaceholderChanged() {
169    Log.debug(TAG, 'onPlaceholderChanged placeholderIndex is ' + this.placeholderIndex);
170    if (this.placeholderIndex != -1) {
171      this.scroller.scrollToIndex(this.placeholderIndex);
172    }
173  }
174
175  onMenuClicked(action: Action) {
176    Log.info(TAG, `onMenuClicked, action: ${action.actionID}`);
177    if (action.actionID === Action.CANCEL.actionID) {
178      router.back();
179    } else if (action.actionID === Action.OK.actionID) {
180      if (this.mSelectManager?.getSelectedCount() == 0) {
181        Log.info(TAG, `onMenuClicked, action: ${action.actionID}, count = 0`);
182      }
183      Log.info(TAG, `onMenuClicked, action: ${action.actionID} newAlbum: ${this.isNewAlbum}`);
184      if (this.isNewAlbum) {
185        AppStorage.setOrCreate(Constants.IS_SHOW_MOVE_COPY_DIALOG, true);
186        let url = 'pages/index';
187        router.back({
188          url: url,
189          params: {
190            jumpSource: JumpSourceToMain.ALBUM,
191          }
192        })
193      } else {
194        MoveOrCopyBroadCastProp.getInstance().doAddOperation(this.broadCast);
195      }
196    }
197  }
198
199  onModeChange() {
200    Log.info(TAG, 'onModeChange');
201  }
202
203  onPageShow() {
204    this.appBroadCast.emit(BroadCastConstants.THIRD_ROUTE_PAGE, []);
205    this.isShow = true;
206    this.pageStatus = this.isShow;
207    this.onActive();
208  }
209
210  onPageHide() {
211    this.isShow = false;
212    this.pageStatus = this.isShow;
213    this.onInActive();
214  }
215
216  onActive() {
217    if (!this.isActive) {
218      Log.info(TAG, 'onActive');
219      this.isActive = true;
220
221      this.dataSource && this.dataSource.onActive();
222      if (this.isSelectedMode && this.mSelectManager) {
223        this.totalSelectedCount = this.mSelectManager.getSelectedCount();
224        this.dataSource.forceUpdate();
225      }
226    }
227  }
228
229  onInActive() {
230    if (this.isActive) {
231      Log.info(TAG, 'onInActive');
232      this.isActive = false;
233      this.dataSource && this.dataSource.onInActive();
234    }
235  }
236
237  onUpdateFavorState(item: MediaItem) {
238    Log.debug(TAG, 'onUpdateFavorState');
239    let index = this.dataSource.getIndexByMediaItem(item);
240    if (index != -1) {
241      this.dataSource.onDataChanged(index);
242    }
243  }
244
245  onBackPress() {
246    if (this.browserController.isBrowserShow) {
247      this.doPhotoBrowserViewBack();
248      return true;
249    }
250    if (this.browserController.isSelectBrowserShow) {
251      this.doSelectPhotoBrowserViewBack();
252      return true;
253    }
254    return false;
255  }
256
257  doSelectPhotoBrowserViewBack() {
258    this.appBroadCast.emit(BroadCastConstants.SELECT_PHOTO_BROWSER_BACK_PRESS_EVENT, []);
259  }
260
261  doPhotoBrowserViewBack() {
262    this.appBroadCast.emit(BroadCastConstants.PHOTO_BROWSER_BACK_PRESS_EVENT, []);
263  }
264
265  aboutToAppear(): void {
266    TraceControllerUtils.startTrace('PhotoGridPageAboutToAppear');
267    this.mSelectManager = AppStorage.Get<SelectManager>(Constants.APP_KEY_NEW_ALBUM_SELECTED) as SelectManager;
268    if (this.mSelectManager == null) {
269      this.mSelectManager = new SelectManager();
270      AppStorage.setOrCreate(Constants.APP_KEY_NEW_ALBUM_SELECTED, this.mSelectManager);
271    }
272    let param: Params = router.getParams() as Params;
273    if (param != null) {
274      Log.debug(TAG, `After router.getParams, param is: ${JSON.stringify(param)}`);
275      let item: AlbumInfo = JSON.parse(param.item) as AlbumInfo;
276      this.title = item.albumName;
277      this.dataSource.setAlbumUri(item.uri);
278      AppStorage.setOrCreate(Constants.APP_KEY_NEW_ALBUM_SOURCE, item.uri);
279    } else {
280      this.title = '';
281      this.dataSource.setAlbumUri('');
282    }
283
284    let self = this;
285    this.dataSource.setBroadCast(this.broadCast)
286    this.mSelectManager.setPhotoDataImpl();
287    this.initGridRowCount();
288    ScreenManager.getInstance().on(ScreenManager.ON_WIN_SIZE_CHANGED, this.onWindowSizeChangeCallBack);
289    this.broadCast.on(BroadCastConstants.SELECT, this.selectFunc);
290    this.broadCast.on(BroadCastConstants.JUMP_PHOTO_BROWSER, this.jumpPhotoBrowserFunc);
291    this.broadCast.on(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER, this.jumpThirdPhotoBrowserFunc);
292    this.broadCast.on(Constants.ON_LOADING_FINISHED, this.onDataReloadedFunc);
293    this.broadCast.on(BroadCastConstants.ON_DATA_RELOADED, this.onLoadingFinishedFunc);
294
295    this.appBroadCast.on(BroadCastConstants.UPDATE_DATA_SOURCE, this.onUpdateFavorStateFunc);
296    AppStorage.setOrCreate(Constants.PHOTO_GRID_SELECT_MANAGER, this.mSelectManager);
297    this.mSelectManager.registerCallback('allSelect', (newState: boolean) => {
298      Log.info(TAG, `allSelect ${newState}`);
299      this.isDataFreeze = AppStorage.get<boolean>(Constants.IS_DATA_FREEZE) as boolean;
300      if (this.isDataFreeze) {
301        return;
302      }
303      this.isAllSelected = newState;
304      this.dataSource.forceUpdate();
305    });
306    this.mSelectManager.registerCallback('select', (newState: number) => {
307      Log.info(TAG, `select ${newState}`);
308      this.isDataFreeze = AppStorage.get<boolean>(Constants.IS_DATA_FREEZE) as boolean;
309      if (this.isDataFreeze) {
310        return;
311      }
312      this.dataSource.onDataChanged(newState);
313    });
314    this.mSelectManager.registerCallback('updateCount', (newState: number) => {
315      Log.info(TAG, `updateSelectedCount ${newState}`);
316      this.isDataFreeze = AppStorage.get<boolean>(Constants.IS_DATA_FREEZE) as boolean;
317      if (this.isDataFreeze) {
318        return;
319      }
320      this.moreMenuList = [];
321      this.moreMenuList.push(Boolean(newState) ? Action.INFO : Action.INFO_INVALID);
322      this.totalSelectedCount = newState;
323    });
324    this.dataSource.registerCallback('updateCount', (newState: number) => {
325      Log.info(TAG, `updateTotalCount ${newState}`);
326      self.isShowScrollBar = (newState > Constants.PHOTOS_CNT_FOR_HIDE_SCROLL_BAR);
327      self.mSelectManager?.setTotalCount(newState);
328    })
329
330    this.moreMenuList = [];
331    this.moreMenuList.push(Action.INFO);
332    TraceControllerUtils.finishTrace('PhotoGridPageAboutToAppear');
333  }
334
335  aboutToDisappear(): void {
336    if (this.broadCast) {
337      this.broadCast.off(BroadCastConstants.SELECT, this.selectFunc);
338      this.broadCast.off(BroadCastConstants.JUMP_PHOTO_BROWSER, this.jumpPhotoBrowserFunc);
339      this.broadCast.off(BroadCastConstants.JUMP_THIRD_PHOTO_BROWSER, this.jumpThirdPhotoBrowserFunc);
340      this.broadCast.off(Constants.ON_LOADING_FINISHED, this.onDataReloadedFunc);
341      this.broadCast.off(BroadCastConstants.ON_DATA_RELOADED, this.onLoadingFinishedFunc);
342    }
343    this.appBroadCast.off(BroadCastConstants.UPDATE_DATA_SOURCE, this.onUpdateFavorStateFunc);
344    this.dataSource.releaseBroadCast();
345    ScreenManager.getInstance().off(ScreenManager.ON_WIN_SIZE_CHANGED, this.onWindowSizeChangeCallBack);
346    AppStorage.delete(Constants.PHOTO_GRID_SELECT_MANAGER);
347  }
348
349  build() {
350    Stack() {
351      Column() {
352        AlbumSelectActionBar({
353          onMenuClicked: (action: Action): void => this.onMenuClicked(action),
354          totalSelectedCount: $totalSelectedCount,
355          menuList: $moreMenuList
356        })
357        if (this.isEmpty) {
358          NoPhotoComponent({
359            title: $r('app.string.no_distributed_photo_head_title_album')
360          })
361        } else {
362          Stack() {
363            Grid(this.scroller) {
364              LazyForEach(this.dataSource, (item: ViewData, index?: number) => {
365                if (!!item) {
366                  GridItem() {
367                    ImageGridItemComponent({
368                      dataSource: this.dataSource,
369                      item: item.mediaItem,
370                      isSelected: this.isSelectedMode ?
371                      this.mSelectManager?.isItemSelected((item.mediaItem as MediaItem).uri as string, item.viewIndex) :
372                        false,
373                      pageName: Constants.PHOTO_TRANSITION_ALBUM,
374                      mPosition: index,
375                      geometryTransitionString: this.getGeometryTransitionId(item, index as number),
376                      selectedCount: $totalSelectedCount
377                    })
378                  }
379                  .aspectRatio(1)
380                  .columnStart(item.viewIndex % this.gridRowCount)
381                  .columnEnd(item.viewIndex % this.gridRowCount)
382                  .key('NewAlbumPageImage' + index)
383                  .zIndex(index === this.placeholderIndex ? 1 : 0)
384                }
385              }, (item: ViewData, index?: number) => {
386                if (item == null || item == undefined) {
387                  return JSON.stringify(item) + index;
388                }
389                return this.getGeometryTransitionId(item, index as number);
390              })
391            }
392            .edgeEffect(EdgeEffect.Spring)
393            .columnsTemplate('1fr '.repeat(this.gridRowCount))
394            .columnsGap(Constants.GRID_GUTTER)
395            .rowsGap(Constants.GRID_GUTTER)
396            .cachedCount(Constants.GRID_CACHE_ROW_COUNT)
397
398            if (this.isShowScrollBar) {
399              GridScrollBar({ scroller: this.scroller });
400            }
401          }
402          .layoutWeight(1)
403        }
404        CustomDialogView({ broadCast: $broadCast })
405      }
406      .backgroundColor($r('app.color.default_background_color'))
407      .margin({
408        top: this.leftBlank[1],
409        bottom: this.leftBlank[3]
410      })
411
412      if (this.browserController.isBrowserShow) {
413        Column() {
414          PhotoBrowserComponent({
415            pageStatus: this.pageStatus,
416            geometryTransitionEnable: true,
417            isRunningAnimation: $isRunningAnimation,
418            browserController: this.browserController
419          })
420        }
421        .width('100%')
422        .height('100%')
423
424        // Opacity must change for TransitionEffect taking effect
425        .transition(TransitionEffect.asymmetric(TransitionEffect.opacity(0.99), TransitionEffect.opacity(0.99)))
426      }
427
428      if (this.browserController.isSelectBrowserShow) {
429        Column() {
430          SelectPhotoBrowserView({
431            pageStatus: this.pageStatus,
432            geometryTransitionEnable: true,
433            isRunningAnimation: $isRunningAnimation,
434            browserController: this.browserController
435          })
436        }
437        .width('100%')
438        .height('100%')
439
440        // Opacity must change for TransitionEffect taking effect
441        .transition(TransitionEffect.asymmetric(TransitionEffect.opacity(0.99), TransitionEffect.opacity(0.99)))
442      }
443    }
444  }
445
446  pageTransition() {
447    PageTransitionEnter({ type: RouteType.Pop, duration: 300 })
448      .opacity(1)
449    PageTransitionExit({ type: RouteType.Push, duration: 300 })
450      .opacity(1)
451  }
452
453  private updateAnimationStatus() {
454    this.isRunningAnimation = this.browserController.isAnimating;
455  }
456
457  private getGeometryTransitionId(item: ViewData, index: number): string {
458    let mediaItem = item.mediaItem as MediaItem;
459    if (mediaItem) {
460      return TAG + mediaItem.getHashCode() + (this.mSelectManager?.isItemSelected(mediaItem.uri as string) ?? false);
461    } else {
462      return TAG + item.viewIndex;
463    }
464  }
465
466  private initGridRowCount(): void {
467    let contentWidth = ScreenManager.getInstance().getWinWidth();
468    let margin = 0;
469    let maxThumbWidth = px2vp(Constants.GRID_IMAGE_SIZE) * Constants.GRID_MAX_SIZE_RATIO;
470    this.gridRowCount = Math.max(Math.round(((contentWidth - Constants.NUMBER_2 * margin) +
471    Constants.GRID_GUTTER) / (maxThumbWidth + Constants.GRID_GUTTER)),
472      Constants.DEFAULT_ALBUM_GRID_COLUMN_MIN_COUNT);
473    Log.info(TAG, `initGridRowCount contentWidth: ${contentWidth}`);
474  }
475}