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 */
15
16import photoAccessHelper from '@ohos.file.photoAccessHelper';
17const FILTER_MEDIA_TYPE_ALL = 'FILTER_MEDIA_TYPE_ALL';
18const FILTER_MEDIA_TYPE_IMAGE = 'FILTER_MEDIA_TYPE_IMAGE';
19const FILTER_MEDIA_TYPE_VIDEO = 'FILTER_MEDIA_TYPE_VIDEO';
20
21@Component
22export struct PhotoPickerComponent {
23  pickerOptions?: PickerOptions | undefined;
24  onSelect?: (uri: string) => void;
25  onDeselect?: (uri: string) => void;
26  onItemClicked?: (itemInfo: ItemInfo, clickType: ClickType) => boolean;
27  onEnterPhotoBrowser?: (photoBrowserInfo: PhotoBrowserInfo) => boolean;
28  onExitPhotoBrowser?: (photoBrowserInfo: PhotoBrowserInfo) => boolean;
29  onPickerControllerReady?: () => void;
30  onPhotoBrowserChanged?: (browserItemInfo: BaseItemInfo) => boolean;
31  onSelectedItemsDeleted?: ItemsDeletedCallback;
32  onExceedMaxSelected?: ExceedMaxSelectedCallback;
33  onCurrentAlbumDeleted?: CurrentAlbumDeletedCallback;
34  onVideoPlayStateChanged?: VideoPlayStateChangedCallback;
35  @ObjectLink @Watch('onChanged') pickerController: PickerController;
36  private proxy: UIExtensionProxy | undefined;
37
38  private onChanged(): void {
39    if (!this.proxy) {
40      return;
41    }
42    let data = this.pickerController?.data;
43    if (data?.has('SET_SELECTED_URIS')) {
44      this.proxy.send({'selectUris': data?.get('SET_SELECTED_URIS') as Array<string>});
45      console.info('PhotoPickerComponent onChanged: SET_SELECTED_URIS');
46    } else if (data?.has('SET_ALBUM_URI')) {
47      this.proxy.send({'albumUri': data?.get('SET_ALBUM_URI') as string});
48      console.info('PhotoPickerComponent onChanged: SET_ALBUM_URI');
49    } else if (data?.has('SET_MAX_SELECT_COUNT')) {
50      this.onSetMaxSelectCount(data);
51    } else if (data?.has('SET_PHOTO_BROWSER_ITEM')) {
52      this.onSetPhotoBrowserItem(data);
53    } else if (data?.has('EXIT_PHOTO_BROWSER')) {
54      this.handleExitPhotoBrowser();
55    } else if (data?.has('SET_PHOTO_BROWSER_UI_ELEMENT_VISIBILITY')) {
56      this.onSetPhotoBrowserUIElementVisibility(data);
57    } else {
58      console.info('PhotoPickerComponent onChanged: other case');
59    }
60  }
61
62  private onSetMaxSelectCount(data?: Map<string, Object>): void {
63    let maxSelected: MaxSelected = data?.get('SET_MAX_SELECT_COUNT') as MaxSelected;
64    let map: Map<MaxCountType, number> | undefined = maxSelected?.data;
65    this.proxy.send({
66      'totalCount': map?.get(MaxCountType.TOTAL_MAX_COUNT),
67      'photoCount': map?.get(MaxCountType.PHOTO_MAX_COUNT),
68      'videoCount': map?.get(MaxCountType.VIDEO_MAX_COUNT)
69    });
70    console.info('PhotoPickerComponent onChanged: SET_MAX_SELECT_COUNT');
71  }
72
73  private onSetPhotoBrowserItem(data?: Map<string, Object>): void {
74    let photoBrowserRangeInfo: PhotoBrowserRangeInfo = data?.get('SET_PHOTO_BROWSER_ITEM') as PhotoBrowserRangeInfo;
75    this.proxy?.send({
76      'itemUri': photoBrowserRangeInfo?.uri,
77      'photoBrowserRange': photoBrowserRangeInfo?.photoBrowserRange
78    });
79    console.info('PhotoPickerComponent onChanged: SET_PHOTO_BROWSER_ITEM');
80  }
81
82  private handleExitPhotoBrowser(): void {
83    this.proxy.send({'exitPhotoBrowser': true});
84    console.info('PhotoPickerComponent onChanged: EXIT_PHOTO_BROWSER');
85  }
86
87  private onSetPhotoBrowserUIElementVisibility(data?: Map<string, Object>): void {
88    let photoBrowserUIElementVisibility: PhotoBrowserUIElementVisibility =
89      data?.get('SET_PHOTO_BROWSER_UI_ELEMENT_VISIBILITY') as PhotoBrowserUIElementVisibility;
90    this.proxy?.send({
91      'elements': photoBrowserUIElementVisibility?.elements,
92      'isVisible': photoBrowserUIElementVisibility?.isVisible
93    });
94    console.info('PhotoPickerComponent onChanged: SET_PHOTO_BROWSER_UI_ELEMENT_VISIBILITY');
95  }
96
97  build() {
98    Row() {
99      Column() {
100        SecurityUIExtensionComponent({
101          parameters: {
102            "ability.want.params.uiExtensionTargetType":"photoPicker",
103            uri: "multipleselect",
104            targetPage: "photoPage",
105            filterMediaType: this.convertMIMETypeToFilterType(this.pickerOptions?.MIMEType),
106            maxSelectNumber: this.pickerOptions?.maxSelectNumber as number,
107            isPhotoTakingSupported: this.pickerOptions?.isPhotoTakingSupported as boolean,
108            isEditSupported: false,
109            recommendationOptions: this.pickerOptions?.recommendationOptions as photoAccessHelper.RecommendationOptions,
110            preselectedUri: this.pickerOptions?.preselectedUris as Array<string>,
111            isFromPickerView: true,
112            isNeedActionBar: false,
113            isNeedSelectBar: false,
114            isSearchSupported: this.pickerOptions?.isSearchSupported as boolean,
115            checkBoxColor: this.pickerOptions?.checkBoxColor as string,
116            backgroundColor: this.pickerOptions?.backgroundColor as string,
117            checkboxTextColor: this.pickerOptions?.checkboxTextColor as string,
118            photoBrowserBackgroundColorMode: this.pickerOptions?.photoBrowserBackgroundColorMode as PickerColorMode,
119            isRepeatSelectSupported: this.pickerOptions?.isRepeatSelectSupported as boolean,
120            maxSelectedReminderMode: this.pickerOptions?.maxSelectedReminderMode as ReminderMode,
121            orientation: this.pickerOptions?.orientation as PickerOrientation,
122            selectMode: this.pickerOptions?.selectMode as SelectMode,
123            maxPhotoSelectNumber: this.pickerOptions?.maxPhotoSelectNumber as number,
124            maxVideoSelectNumber: this.pickerOptions?.maxVideoSelectNumber as number,
125            isOnItemClickedSet: this.onItemClicked? true : false,
126            isPreviewForSingleSelectionSupported: this.pickerOptions?.isPreviewForSingleSelectionSupported as boolean,
127            isSlidingSelectionSupported: this.pickerOptions?.isSlidingSelectionSupported as boolean,
128            photoBrowserCheckboxPosition: this.pickerOptions?.photoBrowserCheckboxPosition as [number, number],
129            gridMargin: this.pickerOptions?.gridMargin as Margin,
130            photoBrowserMargin: this.pickerOptions?.photoBrowserMargin as Margin
131          }
132        }).height('100%').width('100%').onRemoteReady((proxy) => {
133          this.proxy = proxy;
134          console.info('PhotoPickerComponent onRemoteReady');
135        }).onReceive((data) => {
136          let wantParam: Record<string, Object> = data as Record<string, Object>;
137          this.handleOnReceive(wantParam);
138        }).onError(() => {
139          console.info('PhotoPickerComponent onError');
140        });
141      }
142      .width('100%')
143    }
144    .height('100%')
145  }
146
147  private handleOnReceive(wantParam: Record<string, Object>): void {
148    let dataType = wantParam['dataType'] as string;
149    console.info('PhotoPickerComponent onReceive: dataType = ' + dataType);
150    if (dataType === 'selectOrDeselect') {
151      this.handleSelectOrDeselect(wantParam);
152    } else if (dataType === 'itemClick') {
153      this.handleItemClick(wantParam);
154    } else if (dataType === 'onPhotoBrowserStateChanged') {
155      this.handleEnterOrExitPhotoBrowser(wantParam);
156    } else if (dataType === 'remoteReady') {
157      if (this.onPickerControllerReady) {
158        this.onPickerControllerReady();
159        console.info('PhotoPickerComponent onReceive: onPickerControllerReady');
160      }
161    } else if (dataType === 'onPhotoBrowserChanged') {
162      this.handlePhotoBrowserChange(wantParam);
163    } else if (dataType === 'onVideoPlayStateChanged') {
164      this.handleVideoPlayStateChange(wantParam)
165    } else {
166      this.handleOtherOnReceive(wantParam);
167      console.info('PhotoPickerComponent onReceive: other case');
168    }
169    console.info('PhotoPickerComponent onReceive' + JSON.stringify(wantParam));
170  }
171
172  private handleOtherOnReceive(wantParam: Record<string, Object>): void{
173    let dataType = wantParam.dataType as string;
174    if (dataType === 'exceedMaxSelected') {
175      if (this.onExceedMaxSelected) {
176        this.onExceedMaxSelected(wantParam.maxCountType as MaxCountType);
177      }
178    } else if (dataType === 'selectedItemsDeleted') {
179      if (this.onSelectedItemsDeleted) {
180        this.onSelectedItemsDeleted(wantParam.selectedItemInfos as Array<BaseItemInfo>);
181      }
182    } else if (dataType === 'currentAlbumDeleted') {
183      if (this.onCurrentAlbumDeleted) {
184        this.onCurrentAlbumDeleted();
185      }
186    } else {
187      console.info('PhotoPickerComponent onReceive: other case');
188    }
189  }
190
191  private handleSelectOrDeselect(wantParam: Record<string, Object>): void {
192    let isSelect: boolean = wantParam['isSelect'] as boolean;
193    if (isSelect) {
194      if (this.onSelect) {
195        this.onSelect(wantParam['select-item-list'] as string);
196        console.info('PhotoPickerComponent onReceive: onSelect');
197      }
198    } else {
199      if (this.onDeselect) {
200        this.onDeselect(wantParam['select-item-list'] as string);
201        console.info('PhotoPickerComponent onReceive: onDeselect');
202      }
203    }
204  }
205
206  private handleItemClick(wantParam: Record<string, Object>): void {
207    if (this.onItemClicked) {
208      let clickType: ClickType = ClickType.SELECTED;
209      let type = wantParam['clickType'] as string;
210      if (type === 'select') {
211        clickType = ClickType.SELECTED;
212      } else if (type === 'deselect') {
213        clickType = ClickType.DESELECTED;
214      } else {
215        console.info('PhotoPickerComponent onReceive: other clickType');
216      }
217      let itemInfo: ItemInfo = new ItemInfo();
218      let itemType: string = wantParam['itemType'] as string;
219      if (itemType === 'thumbnail') {
220        itemInfo.itemType = ItemType.THUMBNAIL;
221      } else if (itemType === 'camera') {
222        itemInfo.itemType = ItemType.CAMERA;
223      } else {
224        console.info('PhotoPickerComponent onReceive: other itemType');
225      }
226      itemInfo.uri = wantParam['uri'] as string;
227      itemInfo.mimeType = wantParam['mimeType'] as string;
228      itemInfo.width = wantParam['width'] as number;
229      itemInfo.height = wantParam['height'] as number;
230      itemInfo.size = wantParam['size'] as number;
231      itemInfo.duration = wantParam['duration'] as number;
232      let result: boolean = this.onItemClicked(itemInfo, clickType);
233      console.info('PhotoPickerComponent onReceive: onItemClicked = ' + clickType);
234      if (this.proxy) {
235        if (itemType === 'thumbnail' && clickType === ClickType.SELECTED) {
236          this.proxy.send({'clickConfirm': itemInfo.uri, 'isConfirm': result});
237          console.info('PhotoPickerComponent onReceive: click confirm: uri = ' + itemInfo.uri + 'isConfirm = ' + result);
238        }
239        if (itemType === 'camera') {
240          this.proxy.send({'enterCamera': result});
241          console.info('PhotoPickerComponent onReceive: enter camera ' + result);
242        }
243      }
244    }
245  }
246
247  private handleEnterOrExitPhotoBrowser(wantParam: Record<string, Object>): void {
248    let isEnter: boolean = wantParam['isEnter'] as boolean;
249    let photoBrowserInfo: PhotoBrowserInfo = new PhotoBrowserInfo();
250    photoBrowserInfo.animatorParams = new AnimatorParams();
251    photoBrowserInfo.animatorParams.duration = wantParam['duration'] as number;
252    photoBrowserInfo.animatorParams.curve = wantParam['curve'] as Curve | ICurve | string;
253    if (isEnter) {
254      if (this.onEnterPhotoBrowser) {
255        this.onEnterPhotoBrowser(photoBrowserInfo);
256      }
257    } else {
258      if (this.onExitPhotoBrowser) {
259        this.onExitPhotoBrowser(photoBrowserInfo);
260      }
261    }
262    console.info('PhotoPickerComponent onReceive: onPhotoBrowserStateChanged = ' + isEnter);
263  }
264
265  private handlePhotoBrowserChange(wantParam: Record<string, Object>): void {
266    let browserItemInfo: BaseItemInfo = new BaseItemInfo();
267    browserItemInfo.uri = wantParam['uri'] as string;
268    if (this.onPhotoBrowserChanged) {
269      this.onPhotoBrowserChanged(browserItemInfo);
270    }
271    console.info('PhotoPickerComponent onReceive: onPhotoBrowserChanged = ' + browserItemInfo.uri);
272  }
273
274  private handleVideoPlayStateChange(wantParam: Record<string, Object>): void {
275    if(this.onVideoPlayStateChanged) {
276      this.onVideoPlayStateChanged(wantParam.state as VideoPlayerState)
277    }
278    console.info('PhotoPickerComponent onReceive: onVideoPlayStateChanged = ' + JSON.stringify(wantParam));
279  }
280
281  private convertMIMETypeToFilterType(mimeType: photoAccessHelper.PhotoViewMIMETypes): string {
282    let filterType: string;
283    if (mimeType === photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE) {
284      filterType = FILTER_MEDIA_TYPE_IMAGE;
285    } else if (mimeType === photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE) {
286      filterType = FILTER_MEDIA_TYPE_VIDEO;
287    } else {
288      filterType = FILTER_MEDIA_TYPE_ALL;
289    }
290    console.info('PhotoPickerComponent convertMIMETypeToFilterType' + JSON.stringify(filterType));
291    return filterType;
292  }
293}
294
295export type ItemsDeletedCallback = (baseItemInfos: Array<BaseItemInfo>) => void;
296
297export type ExceedMaxSelectedCallback = (exceedMaxCountType: MaxCountType) => void;
298
299export type CurrentAlbumDeletedCallback = () => void;
300
301export type VideoPlayStateChanged = (state: VideoPlayerState) => {} void;
302
303@Observed
304export class PickerController {
305  data?: Map<string, Object>;
306
307  setData(type: DataType, data: Object) {
308    if (data === undefined) {
309      return;
310    }
311    if (type === DataType.SET_SELECTED_URIS) {
312      if (data instanceof Array) {
313        let uriLists: Array<string> = data as Array<string>;
314        if (uriLists) {
315          this.data = new Map([['SET_SELECTED_URIS', [...uriLists]]]);
316          console.info('PhotoPickerComponent SET_SELECTED_URIS' + JSON.stringify(uriLists));
317        }
318      }
319    } else if (type === DataType.SET_ALBUM_URI) {
320      let albumUri: string = data as string;
321      if (albumUri !== undefined) {
322        this.data = new Map([['SET_ALBUM_URI', albumUri]]);
323        console.info('PhotoPickerComponent SET_ALBUM_URI' + JSON.stringify(albumUri));
324      }
325    } else {
326      console.info('PhotoPickerComponent setData: other case');
327    }
328  }
329
330  setMaxSelected(maxSelected: MaxSelected) {
331    if (maxSelected) {
332      this.data = new Map([['SET_MAX_SELECT_COUNT', maxSelected]]);
333      console.info('PhotoPickerComponent SET_MAX_SELECT_COUNT' + JSON.stringify(maxSelected));
334    }
335  }
336
337  setPhotoBrowserItem(uri: string, photoBrowserRange?: PhotoBrowserRange) {
338    let photoBrowserRangeInfo: PhotoBrowserRangeInfo = new PhotoBrowserRangeInfo();
339    photoBrowserRangeInfo.uri = uri;
340    let newPhotoBrowserRange = photoBrowserRange ? photoBrowserRange : PhotoBrowserRange.ALL;
341    photoBrowserRangeInfo.photoBrowserRange = newPhotoBrowserRange;
342    this.data = new Map([['SET_PHOTO_BROWSER_ITEM', photoBrowserRangeInfo]]);
343    console.info('PhotoPickerComponent SET_PHOTO_BROWSER_ITEM' + JSON.stringify(photoBrowserRangeInfo));
344  }
345
346  exitPhotoBrowser() {
347    this.data = new Map([['EXIT_PHOTO_BROWSER', true]]);
348    console.info('PhotoPickerComponent EXIT_PHOTO_BROWSER');
349  }
350
351  setPhotoBrowserUIElementVisibility(elements: Array<PhotoBrowserUIElement>, isVisible?: boolean) {
352    let photoBrowserUIElementVisibility: PhotoBrowserUIElementVisibility = new PhotoBrowserUIElementVisibility();
353    photoBrowserUIElementVisibility.elements = elements;
354    photoBrowserUIElementVisibility.isVisible = isVisible;
355    this.data = new Map([['SET_PHOTO_BROWSER_UI_ELEMENT_VISIBILITY', photoBrowserUIElementVisibility]]);
356    console.info('PhotoPickerComponent SET_PHOTO_BROWSER_UI_ELEMENT_VISIBILITY ' +
357      JSON.stringify(photoBrowserUIElementVisibility));
358  }
359}
360
361export class PickerOptions extends photoAccessHelper.BaseSelectOptions {
362  checkBoxColor?: string;
363  backgroundColor?: string;
364  isRepeatSelectSupported?: boolean;
365  checkboxTextColor?: string;
366  photoBrowserBackgroundColorMode?: PickerColorMode;
367  maxSelectedReminderMode?: ReminderMode;
368  orientation?: PickerOrientation;
369  selectMode?: SelectMode;
370  maxPhotoSelectNumber?: number;
371  maxVideoSelectNumber?: number;
372  isSlidingSelectionSupported?: boolean;
373  photoBrowserCheckboxPosition?: [number, number];
374  gridMargin?: Margin;
375  photoBrowserMargin?: Margin;
376}
377
378export class BaseItemInfo {
379  uri?: string;
380  mimeType?: string;
381  width?: number;
382  height?: number;
383  size?: number;
384  duration?: number;
385}
386
387export class ItemInfo extends BaseItemInfo {
388  itemType?: ItemType;
389}
390
391export class PhotoBrowserInfo {
392  animatorParams?: AnimatorParams;
393}
394
395export class AnimatorParams {
396  duration?: number;
397  curve?: Curve | ICurve | string;
398}
399
400export class MaxSelected {
401  data?: Map<MaxCountType, number>;
402}
403
404class PhotoBrowserRangeInfo {
405  uri?: string;
406  photoBrowserRange?: PhotoBrowserRange;
407}
408
409class PhotoBrowserUIElementVisibility {
410  elements?: Array<PhotoBrowserUIElement>;
411  isVisible?: boolean;
412}
413
414export enum DataType {
415  SET_SELECTED_URIS = 1,
416  SET_ALBUM_URI = 2
417}
418
419export enum ItemType {
420  THUMBNAIL = 0,
421  CAMERA = 1
422}
423
424export enum ClickType {
425  SELECTED = 0,
426  DESELECTED = 1
427}
428
429export enum PickerOrientation {
430  VERTICAL = 0,
431  HORIZONTAL = 1
432}
433
434export enum SelectMode {
435  SINGLE_SELECT = 0,
436  MULTI_SELECT = 1
437}
438
439export enum PickerColorMode {
440  AUTO = 0,
441  LIGHT = 1,
442  DARK = 2
443}
444
445export enum ReminderMode {
446  NONE = 0,
447  TOAST = 1,
448  MASK = 2
449}
450
451export enum MaxCountType {
452  TOTAL_MAX_COUNT = 0,
453  PHOTO_MAX_COUNT = 1,
454  VIDEO_MAX_COUNT = 2
455}
456
457export enum PhotoBrowserRange {
458  ALL = 0,
459  SELECTED_ONLY = 1
460}
461
462export enum PhotoBrowserUIElement {
463  CHECKBOX = 0,
464  BACK_BUTTON = 1
465}
466
467export enum VideoPlayerState {
468  PLAYING = 0,
469  PAUSED = 1,
470  STOPPED = 3,
471  SEEK_START = 4,
472  SEEK_FINISH = 5
473}