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 { MenuOperation, WindowUtil } from '@ohos/common';
17import {
18  Action,
19  AlbumInfo,
20  AlbumSetDataInfo,
21  AlbumSetDataSource,
22  AlbumSetSelectManager,
23  BroadCast,
24  BroadCastConstants,
25  BroadCastManager,
26  CommonObserverCallback,
27  Constants,
28  Log,
29  MediaObserver,
30  MenuContext,
31  MenuOperationFactory,
32  ScreenManager,
33  TraceControllerUtils,
34  UiUtil
35} from '@ohos/common';
36import {
37  AlbumSetNewMenuOperation,
38  CustomDialogView,
39  MoveOrCopyBroadCastProp,
40  NoPhotoIndexComponent,
41  TabItemWithText
42} from '@ohos/common/CommonComponents';
43import { TimelineDataSourceManager } from '../../../../../../../timeline';
44import { AlbumSetPageActionBar } from './AlbumSetPageActionBar';
45import { AlbumSetPageToolBar } from './AlbumSetPageToolBar';
46import { AlbumGridItemNewStyle } from './AlbumGridItemNewStyle';
47import { AlbumSetDeleteMenuOperation } from './AlbumSetDeleteMenuOperation';
48import { AlbumSetRenameMenuOperation } from './AlbumSetRenameMenuOperation';
49
50const TAG: string = 'AlbumSetPage';
51const TRANSITION_EFFECT_ALPHA: number = 0.99;
52
53// Album Set Page
54@Component
55export struct AlbumSetPage {
56  @Consume @Watch('onModeChange') isAlbumSetSelectedMode: boolean;
57  @Provide('selectedCount') @Watch('updateRightClickMenuList') selectedAlbumsCount: number = 0;
58  @Provide @Watch('updateRightClickMenuList') isDisableDelete: boolean = false;
59  @Provide @Watch('updateRightClickMenuList') isDisableRename: boolean = false;
60  @State isEmpty: boolean = TimelineDataSourceManager.getInstance().getDataSource().isEmpty();
61  @Provide gridColumnsCount: number = 0;
62  @Provide broadCast: BroadCast = new BroadCast();
63  @Consume @Watch('onIndexPageShow') isShow: boolean;
64  albums: AlbumSetDataSource = new AlbumSetDataSource(this.broadCast);
65  appBroadCast: BroadCast = BroadCastManager.getInstance().getBroadCast();
66  isInCurrentTab = false;
67  isActive = false; // Whether the page is in the foreground
68  scroller: Scroller = new Scroller();
69  @Consume('isShowSideBar') @Watch('initGridRowCount') isSidebar: boolean;
70  mSelectManager = new AlbumSetSelectManager();
71  isDataFreeze: boolean = false;
72  // the switch of distributed page
73  @Provide isTabBarShow: boolean = false;
74  @Provide rightClickMenuList: Action[] = [];
75  @StorageLink('isHorizontal') isHorizontal: boolean = ScreenManager.getInstance().isHorizontal();
76  @StorageLink('statusBarHeight') statusBarHeight: number = 0;
77  @StorageLink('deviceType') deviceType: string | undefined = AppStorage.get<string>('deviceType');
78  @State pageStatus: boolean = false;
79  @State bottomHeight: number = 0;
80  @StorageLink('selectedAlbumUri') selectedAlbumUri: string = '';
81  @StorageLink('albumActionBarOpacity') albumActionBarOpacity: number = 1;
82  @StorageLink('albumOpacity') albumOpacity: number = 1;
83  @StorageLink('albumOtherScale') albumOtherScale: number = 1;
84  private tabs: TabItemWithText[] = [
85    new TabItemWithText($r('app.string.local'), false),
86    new TabItemWithText($r('app.string.other_equipment'), false)
87  ];
88  private dataObserver: CommonObserverCallback = new CommonObserverCallback(this);
89  private tabsController: TabsController = new TabsController();
90  private currentIndex: number = Constants.LOCAL_TAB_INDEX;
91  private needNotify = false;
92  private ignoreLocalNotify = false;
93  private minGridColumnsCount: number = 2;
94  private onWinSizeChangedFunc: Function = (): void => this.initGridRowCount();
95  private onTabChangedFunc: Function = (index: number): void => this.onTabChanged(index);
96  private onStateResetFunc: Function = (index: number): void => this.onStateReset(index);
97  private onSendMoveCopyBroadCastFunc: Function = (index: number): void => this.onSendMoveCopyBroadCast(index);
98  private onLoadingFinishedFunc: Function = (size: number): void => this.onLoadingFinished(size);
99  private onResetZeroFunc: Function = (pageNumber: number): void => this.onResetZero(pageNumber);
100  private onMenuClickedFunc = (action: Action, arg: Object[]): void => this.onMenuClicked(action, arg);
101  private selectFunc = (
102    key: string, value: boolean, isDisableRename: boolean, isDisableDelete: boolean, callback: Function): void =>
103  this.select(key, value, isDisableRename, isDisableDelete, callback);
104
105  onMenuClicked(action: Action, arg: Object[]) {
106    Log.info(TAG, `onMenuClicked, action: ${action.actionID}`);
107    let menuContext: MenuContext;
108    let menuOperation: MenuOperation;
109    if (action.actionID === Action.NEW.actionID) {
110      menuContext = new MenuContext();
111      menuContext
112        .withOperationStartCallback((): void => this.onOperationStart())
113        .withOperationEndCallback((): void => this.onOperationEnd())
114        .withAlbumSetDataSource(this.albums)
115        .withBroadCast(this.broadCast);
116      menuOperation =
117        MenuOperationFactory.getInstance().createMenuOperation(AlbumSetNewMenuOperation, menuContext);
118      menuOperation.doAction();
119    } else if (action.actionID === Action.CANCEL.actionID) {
120      this.isAlbumSetSelectedMode = false;
121    } else if (action.actionID === Action.MULTISELECT.actionID) {
122      this.isAlbumSetSelectedMode = true;
123    } else if (action.actionID === Action.RENAME.actionID) {
124      menuContext = new MenuContext();
125      menuContext
126        .withFromSelectMode(true)
127        .withSelectManager(this.mSelectManager)
128        .withOperationStartCallback((): void => this.onOperationStart())
129        .withOperationEndCallback((): void => this.onOperationEnd())
130        .withBroadCast(this.broadCast);
131      menuOperation =
132        MenuOperationFactory.getInstance().createMenuOperation(AlbumSetRenameMenuOperation, menuContext);
133      menuOperation.doAction();
134    } else if (action.actionID === Action.DELETE.actionID) {
135      menuContext = new MenuContext();
136      menuContext
137        .withFromSelectMode(true)
138        .withSelectManager(this.mSelectManager)
139        .withOperationStartCallback((): void => this.onOperationStart())
140        .withOperationEndCallback((): void => this.onOperationEnd())
141        .withBroadCast(this.broadCast);
142      menuOperation =
143        MenuOperationFactory.getInstance().createMenuOperation(AlbumSetDeleteMenuOperation, menuContext);
144      menuOperation.doAction();
145    }
146  }
147
148  onOperationStart(): void {
149    this.isDataFreeze = true;
150    this.ignoreLocalNotify = true;
151    this.albums.freeze();
152  }
153
154  onOperationEnd(): void {
155    Log.debug(TAG, `onOperationEnd`);
156    this.isDataFreeze = false;
157    this.isAlbumSetSelectedMode = false
158    this.ignoreLocalNotify = false;
159    this.albums.onChange('image');
160    this.albums.unfreeze();
161  }
162
163  aboutToAppear(): void {
164    TraceControllerUtils.startTrace('AlbumSetPageAboutToAppear');
165    Log.info(TAG, `AlbumSetPageAboutToAppear`);
166    this.isEmpty = (this.albums.totalCount() == 0);
167    this.appBroadCast.on(BroadCastConstants.ON_TAB_CHANGED, this.onTabChangedFunc);
168    this.appBroadCast.on(BroadCastConstants.RESET_STATE_EVENT, this.onStateResetFunc);
169    this.appBroadCast.on(BroadCastConstants.SEND_COPY_OR_MOVE_BROADCAST, this.onSendMoveCopyBroadCastFunc);
170    AppStorage.setOrCreate('setSelectManagerToAnother', this.mSelectManager);
171    this.broadCast.on(Constants.ON_LOADING_FINISHED, this.onLoadingFinishedFunc);
172    this.appBroadCast.on(BroadCastConstants.RESET_ZERO, this.onResetZeroFunc);
173
174    MediaObserver.getInstance().registerObserver(this.dataObserver);
175
176    this.onActive();
177
178    this.updateRightClickMenuList();
179    this.initGridRowCount();
180    // 后续phone缩略图支持横竖屏后再放开
181    if (AppStorage.get('deviceType') as string !== Constants.DEFAULT_DEVICE_TYPE) {
182      ScreenManager.getInstance().on(ScreenManager.ON_WIN_SIZE_CHANGED, this.onWinSizeChangedFunc);
183    }
184
185    let self = this;
186    this.broadCast.on(BroadCastConstants.SELECT, this.selectFunc);
187    this.mSelectManager.registerCallback('updateCount', (newState: number) => {
188      Log.info(TAG, `updateSelectedCount ${newState}`);
189      if (this.isDataFreeze) {
190        return;
191      }
192      if (this.selectedAlbumsCount === 0 && newState === 0) {
193        this.selectedAlbumsCount--;
194      } else {
195        this.selectedAlbumsCount = newState;
196      }
197    });
198    this.mSelectManager.registerCallback('updateToolBarState',
199      (isDisableRename: boolean, isDisableDelete: boolean) => {
200        if (this.isDataFreeze) {
201          return;
202        }
203        Log.info(TAG, `updateToolBarState:\
204                    isDisableRename: ${isDisableRename}, isDisableDelete: ${isDisableDelete}`);
205        this.isDisableRename = isDisableRename
206        this.isDisableDelete = isDisableDelete
207      }
208    );
209
210    if (Constants.LOCAL_TAB_INDEX == this.currentIndex) {
211      this.tabs[Constants.LOCAL_TAB_INDEX].isSelected = true;
212      this.tabs[Constants.OTHER_EQUIPMENT_TAB_INDEX].isSelected = false;
213    } else {
214      this.tabs[Constants.LOCAL_TAB_INDEX].isSelected = false;
215      this.tabs[Constants.OTHER_EQUIPMENT_TAB_INDEX].isSelected = true;
216    }
217    TraceControllerUtils.finishTrace('AlbumSetPageAboutToAppear');
218  }
219
220  aboutToDisappear(): void {
221    this.onInActive();
222    this.broadCast.off(Constants.ON_LOADING_FINISHED, this.onLoadingFinishedFunc);
223    this.broadCast.off(BroadCastConstants.SELECT, this.selectFunc);
224    this.appBroadCast.off(BroadCastConstants.ON_TAB_CHANGED, this.onTabChangedFunc);
225    this.appBroadCast.off(BroadCastConstants.RESET_STATE_EVENT, this.onStateResetFunc);
226    this.appBroadCast.off(BroadCastConstants.SEND_COPY_OR_MOVE_BROADCAST, this.onSendMoveCopyBroadCastFunc);
227    this.appBroadCast.off(BroadCastConstants.RESET_ZERO, this.onResetZeroFunc);
228    MediaObserver.getInstance().unregisterObserver(this.dataObserver);
229    this.dataObserver.clearSource();
230    // 后续phone缩略图支持横竖屏后再放开
231    if (AppStorage.get('deviceType') as string !== Constants.DEFAULT_DEVICE_TYPE) {
232      ScreenManager.getInstance().off(ScreenManager.ON_WIN_SIZE_CHANGED, this.onWinSizeChangedFunc);
233    }
234  }
235
236  // Callback when the page is show.
237  onIndexPageShow() {
238    Log.info(TAG, `[onIndexPageShow] isShow=${this.isShow}, isInCurrentTab=${this.isInCurrentTab}`);
239    if (this.isShow && this.isInCurrentTab) {
240      this.onActive();
241    } else if (!this.isShow && this.isInCurrentTab) {
242      this.onInActive();
243    } else {
244    }
245  }
246
247  onModeChange() {
248    Log.info(TAG, `onModeChange ${this.isAlbumSetSelectedMode}`);
249    this.updateRightClickMenuList();
250    if (!this.isAlbumSetSelectedMode) {
251      this.mSelectManager.emitCallback('updateCount', [0]);
252      this.mSelectManager.onModeChange(false);
253    }
254  }
255
256  updateRightClickMenuList() {
257    if (this.isAlbumSetSelectedMode) {
258      this.rightClickMenuList = [];
259      if (!this.isDisableRename && this.selectedAlbumsCount == 1) {
260        this.rightClickMenuList.push(Action.RENAME);
261      }
262      if (!this.isDisableDelete && this.selectedAlbumsCount > 0) {
263        this.rightClickMenuList.push(Action.DELETE);
264      }
265    }
266  }
267
268  onStateReset(index: number): void {
269    if (index == Constants.ALBUM_PAGE_INDEX) {
270      this.isAlbumSetSelectedMode = false;
271    }
272  }
273
274  onTabChanged(index: number): void {
275    if (index == Constants.ALBUM_PAGE_INDEX) {
276      this.isInCurrentTab = true;
277      this.onActive();
278    } else {
279      this.isInCurrentTab = false;
280      this.isAlbumSetSelectedMode = false;
281      this.onInActive();
282    }
283  }
284
285  onLoadingFinished(size: number): void {
286    this.isEmpty = (size == 0);
287  }
288
289  select(key: string, value: boolean, isDisableRename: boolean, isDisableDelete: boolean, callback: Function): void {
290    this.mSelectManager.toolBarStateToggle(key, value, isDisableRename, isDisableDelete);
291    if (this.mSelectManager.toggle(key, value)) {
292      Log.info(TAG, 'enter event process');
293      callback();
294    }
295  }
296
297  onSendMoveCopyBroadCast(index: number): void {
298    if (index == Constants.ALBUM_PAGE_INDEX) {
299      MoveOrCopyBroadCastProp.getInstance().sendMoveOrAddBroadCast(this.broadCast);
300    }
301  }
302
303  // Callback when the page is in the foreground
304  onActive() {
305    if (!this.isActive) {
306      Log.info(TAG, 'onActive');
307      this.isActive = true;
308      if (this.currentIndex == Constants.LOCAL_TAB_INDEX) {
309        this.onLocalAlbumSetActive();
310      } else {
311        this.onLocalAlbumSetInActive();
312      }
313      this.showNotify();
314      if (this.albums.totalCount() == 0) {
315        this.albums.loadData();
316      }
317    }
318  }
319
320  // Callback when the page is in the background
321  onInActive() {
322    if (this.isActive) {
323      Log.info(TAG, 'onInActive');
324      this.isActive = false;
325      this.albums && this.albums.onInActive();
326    }
327  }
328
329  // Callback when the local albums' page is in the foreground
330  onLocalAlbumSetActive() {
331    if (this.currentIndex == Constants.LOCAL_TAB_INDEX) {
332      Log.info(TAG, 'Local album set is on active');
333      this.albums && this.albums.onActive();
334    }
335  }
336
337  // Callback when the local albums' page is in the background
338  onLocalAlbumSetInActive() {
339    if (this.currentIndex == Constants.OTHER_EQUIPMENT_TAB_INDEX) {
340      Log.info(TAG, 'Local album set is on inactive');
341      this.albums && this.albums.onInActive();
342    }
343  }
344
345  onResetZero(pageNumber: number) {
346    if (pageNumber == Constants.ALBUM_PAGE_INDEX) {
347      this.scroller.scrollEdge(Edge.Top);
348    }
349  }
350
351  initGridRowCount(): void {
352    Log.info(TAG, `get screen width is : ${ScreenManager.getInstance().getWinWidth()}`);
353    Log.info(TAG, `get screen height is : ${ScreenManager.getInstance().getWinHeight()}`);
354    let currentBreakpoint = AppStorage.get<string>('currentBreakpoint');
355    if (currentBreakpoint === Constants.BREAKPOINT_LG && this.deviceType == Constants.PAD_DEVICE_TYPE) {
356      this.gridColumnsCount = UiUtil.getAlbumGridCount(true);
357    } else {
358      this.gridColumnsCount = UiUtil.getAlbumGridCount(this.isSidebar);
359    }
360    Log.info(TAG, `the grid count in a line is: ${this.gridColumnsCount}`);
361  }
362
363  onMediaLibDataChange(changeType: string): void {
364    Log.info(TAG, `onMediaLibDataChange type: ${changeType}`);
365    if (!this.ignoreLocalNotify) {
366      this.albums.onChange(changeType);
367    }
368  }
369
370  isRecycleAlbumOfPhoneLikeDevice(item: AlbumInfo): boolean {
371    return this.deviceType != Constants.PC_DEVICE_TYPE && item.isTrashAlbum;
372  }
373
374  @Builder
375  LocalAlbumSet() {
376    Stack() {
377      if (!this.isEmpty) {
378        Grid(this.scroller) {
379          LazyForEach(this.albums, (item: AlbumSetDataInfo, index?: number) => {
380            GridItem() {
381              if (this.selectedAlbumUri === item.data.uri) {
382                Column() {
383                }
384                .width('100%')
385                .height(AppStorage.get<string>(Constants.KEY_OF_ALBUM_WIDTH) as string)
386                .key('' + index)
387              } else {
388                AlbumGridItemNewStyle({
389                  item: item.data,
390                  isSelected: this.isAlbumSetSelectedMode ?
391                  this.mSelectManager.isItemSelected(item.data.uri) : false,
392                  onMenuClicked: this.onMenuClickedFunc,
393                  onMenuClickedForSingleItem: (action: Action, currentAlbum: AlbumInfo): void =>
394                  this.onMenuClickedForSingleItem(action, currentAlbum),
395                  keyIndex: index,
396                  bottomHeight: $bottomHeight
397                })
398                  .transition(TransitionEffect.opacity(TRANSITION_EFFECT_ALPHA))
399              }
400            }
401            .margin({
402              bottom: this.bottomHeight
403            })
404            .clip(false)
405            .key('Album_' + index)
406          }, (item: AlbumSetDataInfo) => {
407            if (item.data.mediaItem) {
408              return item.data.getHashCode() + item.data.mediaItem.getHashCode();
409            }
410            return item.data.getHashCode();
411          })
412        }
413        .edgeEffect(EdgeEffect.Spring)
414        .clip(false)
415        .columnsTemplate('1fr '.repeat(this.gridColumnsCount))
416        .padding({
417          top: $r('app.float.album_set_page_padding_top'),
418          bottom: this.isHorizontal ? 0 : $r('app.float.tab_bar_vertical_height'),
419          left: $r('sys.float.ohos_id_card_margin_start'),
420          right: $r('sys.float.ohos_id_card_margin_end')
421        })
422        .columnsGap($r('sys.float.ohos_id_card_margin_middle'))
423        .rowsGap($r('sys.float.ohos_id_elements_margin_vertical_l'))
424        .scrollBar(BarState.Auto)
425      }
426    }
427  }
428
429  build() {
430    Stack() {
431      Flex({
432        direction: FlexDirection.Column,
433        justifyContent: FlexAlign.Start,
434        alignItems: ItemAlign.Start
435      }) {
436        if (!this.isEmpty || this.isTabBarShow) {
437          Column() {
438            AlbumSetPageActionBar({ onMenuClicked: this.onMenuClickedFunc })
439              .opacity(this.albumActionBarOpacity)
440          }
441          .backgroundColor($r('app.color.default_background_color'))
442          Column() {
443            this.LocalAlbumSet()
444          }
445          .zIndex(Constants.NEGATIVE_1)
446          .opacity(this.albumOpacity)
447          .scale({ x: this.albumOtherScale, y: this.albumOtherScale })
448          .layoutWeight(Constants.NUMBER_1)
449        }
450        if (this.isAlbumSetSelectedMode) {
451          AlbumSetPageToolBar({ onMenuClicked: this.onMenuClickedFunc })
452        }
453
454        if (this.isEmpty && !this.isTabBarShow) {
455          NoPhotoIndexComponent({ index: Constants.ALBUM_PAGE_INDEX, hasBarSpace: true })
456        }
457      }
458
459      CustomDialogView({ broadCast: $broadCast })
460    }
461  }
462
463  private onMenuClickedForSingleItem(action: Action, currentAlbum: AlbumInfo) {
464    Log.info(TAG, `single menu click, action: ${action?.actionID}, currentUri: ${currentAlbum?.albumName}`);
465    if (currentAlbum == undefined) {
466      return;
467    }
468    let menuContext: MenuContext;
469    let menuOperation: MenuOperation;
470    if (action.actionID === Action.RENAME.actionID) {
471      menuContext = new MenuContext();
472      menuContext
473        .withFromSelectMode(false)
474        .withAlbumInfo(currentAlbum)
475        .withOperationStartCallback((): void => this.onOperationStart())
476        .withOperationEndCallback((): void => this.onOperationEnd())
477        .withBroadCast(this.broadCast);
478      menuOperation = MenuOperationFactory.getInstance()
479        .createMenuOperation(AlbumSetRenameMenuOperation, menuContext);
480      menuOperation.doAction();
481    } else if (action.actionID === Action.DELETE.actionID) {
482      menuContext = new MenuContext();
483      menuContext
484        .withFromSelectMode(false)
485        .withAlbumInfo(currentAlbum)
486        .withOperationStartCallback((): void => this.onOperationStart())
487        .withOperationEndCallback((): void => this.onOperationEnd())
488        .withBroadCast(this.broadCast);
489      menuOperation = MenuOperationFactory.getInstance()
490        .createMenuOperation(AlbumSetDeleteMenuOperation, menuContext);
491      menuOperation.doAction();
492    }
493  }
494
495  private showNotify() {
496    if (this.needNotify) {
497      UiUtil.showToast($r('app.string.distributed_album_disconnected'));
498      this.needNotify = false;
499    }
500  }
501
502  private onDistributedTabChanged(index: number) {
503    this.currentIndex = index;
504    if (index == Constants.LOCAL_TAB_INDEX) {
505      this.onLocalAlbumSetActive();
506    } else {
507      this.onLocalAlbumSetInActive();
508    }
509  }
510
511  private onUpdateRemoteDevice(res: string, deviceId: string, size: number): void {
512    Log.info(TAG, `onUpdateRemoteDevice size: ${size} deviceId: ${deviceId} type: ${res}`);
513
514    if (res == 'offline') {
515      this.needNotify = true;
516    }
517
518    if (size <= 0) {
519      this.currentIndex = Constants.LOCAL_TAB_INDEX
520      try {
521        this.tabsController.changeIndex(this.currentIndex);
522      } catch (error) {
523        Log.debug(TAG, `change tab index failed: ${error}`);
524      }
525      this.tabs[Constants.LOCAL_TAB_INDEX].isSelected = true;
526      this.tabs[Constants.OTHER_EQUIPMENT_TAB_INDEX].isSelected = false;
527
528      if (this.isActive) {
529        this.showNotify();
530      }
531    }
532
533    this.isTabBarShow = (size > 0);
534  }
535}
536