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