1/*
2 * Copyright (c) 2021-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 { FilesData } from '../../../databases/model/FileData';
17import FileAccessExec from '../../../base/utils/FileAccessExec';
18import { toast, isValidFileName } from '../../../base/utils/Common';
19import { TreeItem } from '../TreeItem';
20import { FileMkdirDialog } from './FileMkdirDialog';
21import { emit } from '../../../base/utils/EventBus';
22import { getResourceString } from '../../../base/utils/Tools';
23import { FILENAME_MAX_LENGTH, FILE_MANAGER_PREFERENCES, FOLDER_LEVEL } from '../../../base/constants/Constant';
24import StringUtil from '../../../base/utils/StringUtil';
25import { setPreferencesValue } from '../../../base/utils/PreferencesUtil';
26import { FileUtil } from '../../../base/utils/FileUtil';
27import Logger from '../../../base/log/Logger';
28import { ArrayUtil } from '../../../base/utils/ArrayUtil';
29import ErrorCodeConst from '../../../base/constants/ErrorCodeConst';
30import { StartModeOptions } from '../../../base/model/StartModeOptions';
31import { FilePickerUtil } from '../../../base/utils/FilePickerUtil';
32
33@Styles
34function pressedStyles() {
35  .borderRadius($r('app.float.common_borderRadius8'))
36  .backgroundColor($r('app.color.hicloud_hmos_bg'))
37}
38
39@Styles
40function normalStyles() {
41  .borderRadius($r('app.float.common_borderRadius8'))
42  .backgroundColor($r('app.color.transparent_color'))
43}
44
45const TAG = 'fileTree';
46
47@Component
48export struct fileTree {
49  private startModeOptions: StartModeOptions = FilePickerUtil.getStartOptionsFromStorage();
50  @State listLength: number = 0;
51  @State topRotate: boolean = false;
52  @State rootData: FilesData[] = [];
53  @State selectUri: string = '';
54  @State @Watch('nameChange') selectName: string = getResourceString($r('app.string.myPhone'));
55  public moveCallback: (e) => void;
56  @State @Watch('selectChange') chooseItem: FilesData = new FilesData({});
57  @State folderList: FilesData[] = [];
58  @State fileList: FilesData[] = [];
59  @State changeTitle: Resource = undefined;
60  @State fileName: string = '';
61  @State suffix: string = '';
62  fileMkdirDialog: CustomDialogController = new CustomDialogController({
63    builder: FileMkdirDialog({
64      fileItems: this.folderList,
65      getCurrentDir: this.selectUri,
66      confirm: this.fileMkdir.bind(this)
67    }),
68    autoCancel: true,
69    alignment: DialogAlignment.Bottom,
70    offset: { dx: 0, dy: -80 }
71  });
72  @State isSelectRootPath: boolean = true;
73  @State errorText: Resource = undefined;
74  @State isNeedLoadDefaultPath: boolean = false;
75  @State isClickExpand: boolean = false;
76  @Link @Watch('createFileFailTypeChange') createFileFailType: number;
77  lastSelectPath: string = AppStorage.Get<string>(FILE_MANAGER_PREFERENCES.lastSelectPath.key);
78  defaultExpandPath: string = '';
79  scroller: Scroller = new Scroller();
80  context: Context = globalThis.abilityContext;
81
82  async aboutToAppear() {
83    toast($r('app.string.select_location'))
84    const fileNameList = this.startModeOptions.newFileNames;
85    this.listLength = fileNameList.length;
86    const fileName: string = fileNameList[0];
87    if (fileName) {
88      const dotIndex = fileName.lastIndexOf('.');
89      let fileSuffix = this.startModeOptions.PhoneFileSuffixChoices;
90      if (!StringUtil.isEmpty(fileSuffix)) {
91        this.suffix = fileSuffix;
92        if (dotIndex > 0) {
93          this.fileName = fileName.substring(0, dotIndex) + fileSuffix;
94        } else {
95          this.fileName = fileName + fileSuffix;
96        }
97      } else {
98        this.fileName = fileName;
99      }
100    }
101    this.nameChange();
102    this.isSelectRootPath = true;
103    let { folderList, fileList } = FileAccessExec.getFileData();
104    this.fileList = fileList;
105    this.rootData = folderList;
106    this.folderList = this.rootData;
107    this.topRotate = !this.topRotate;
108    if (globalThis.documentInfo) {
109      this.selectUri = globalThis.documentInfo.uri;
110    }
111    this.loadDefaultExpandPath(this.startModeOptions);
112  }
113
114  aboutToDisappear() {
115    this.fileMkdirDialog = null;
116  }
117
118  fileMkdir(e) {
119    emit('fileMkdir', e)
120    if (this.isSelectRootPath) {
121      // 获取当前选中文件夹下的所有子文件
122      let { folderList, fileList } = FileAccessExec.getFileData();
123      this.rootData = folderList;
124      // 查找刚刚新建的文件夹index
125      const index = this.rootData.findIndex(item => item.fileName === e.mkdirName);
126      if (index !== -1) {
127        // 默认选中刚刚新建的文件夹
128        this.selectUri = this.rootData[index].uri;
129        this.selectName = this.rootData[index].fileName;
130        this.topRotate = true;
131        this.fileList = [];
132        this.folderList = [];
133      } else {
134        this.fileList = fileList;
135        this.folderList = folderList;
136      }
137    }
138  }
139
140  nameChange() {
141    this.changeTitle = this.listLength <= 1 ?
142    $r('app.string.to_save', this.selectName) : $r('app.string.to_save_plural', this.listLength, this.selectName)
143    if (this.isSelectRootPath) {
144      this.isSelectRootPath = false;
145    }
146  }
147
148  createFileFailTypeChange() {
149    if (this.createFileFailType === ErrorCodeConst.PICKER.FILE_NAME_EXIST) {
150      this.errorText = $r('app.string.save_file_has_same_file');
151      this.createFileFailType = ErrorCodeConst.PICKER.NORMAL;
152    }
153  }
154
155  selectChange() {
156    let autoShow: boolean = false;
157    if (this.chooseItem) {
158      autoShow = this.chooseItem.autoShow;
159      this.chooseItem.autoShow = false;
160    }
161    if (!this.isClickExpand || autoShow) {
162      let loadSubFinish = FileUtil.loadSubFinish(this.defaultExpandPath,
163        this.chooseItem.currentDir, FOLDER_LEVEL.MAX_LEVEL - 2);
164      if (loadSubFinish || autoShow) {
165        let allData: FilesData[] = [];
166        let pos = this.getSelectItemPos(this.rootData, allData);
167        let itemHeight: number = this.context.resourceManager.getNumber($r('app.float.common_size56'));
168        let scrollY: number = itemHeight * (pos - 1);
169        Logger.i(TAG, 'selectItemPos = ' + pos + ',itemHeight = ' + itemHeight + ' ; scrollY = ' + scrollY);
170        setTimeout(() => {
171          if (scrollY < 0) {
172            this.scroller.scrollEdge(Edge.Start);
173          } else {
174            this.scroller.scrollTo({ xOffset: 0, yOffset: scrollY });
175          }
176        }, 0);
177      }
178    }
179  }
180
181  private getSelectItemPos(fileList: FilesData[], allData: FilesData[]): number {
182    if (ArrayUtil.isEmpty(allData)) {
183      allData = [];
184    }
185    if (!ArrayUtil.isEmpty(fileList)) {
186      for (let index = 0; index < fileList.length; index++) {
187        const fileData: FilesData = fileList[index];
188        allData.push(fileData);
189        if (fileData.uri === this.selectUri) {
190          return allData.length;
191        }
192        if (fileData.hasSubFolderList()) {
193          let subFolderList: FilesData[] = fileData.getSubFolderList();
194          let result = this.getSelectItemPos(subFolderList, allData);
195          if (result > 0) {
196            return result;
197          }
198        }
199      }
200    }
201    return 0;
202  }
203
204  /**
205   * 加载默认展开目录,如果是路径选择器拉起的,优先使用三方指定的目录
206   */
207  async loadDefaultExpandPath(startModeOptions: StartModeOptions) {
208    let defaultPickDir = startModeOptions.defaultFilePathUri;
209    let loadUri = this.lastSelectPath;
210    if (!StringUtil.isEmpty(defaultPickDir)) {
211      loadUri = defaultPickDir;
212    }
213    if (!StringUtil.isEmpty(loadUri)) {
214      let fileHelper = await FileUtil.getFileAccessHelperAsync(startModeOptions.context);
215      let fileInfo = await FileUtil.getFileInfoByUri(loadUri, fileHelper);
216      if (fileInfo) {
217        this.defaultExpandPath = FileUtil.getCurrentFolderByFileInfo(fileInfo);
218        Logger.i(TAG, 'loadDefaultExpandPath = ' + this.defaultExpandPath);
219        // 值为true,说明需要刷新树布局,并且传入loadPath
220        this.isNeedLoadDefaultPath = !StringUtil.isEmpty(this.defaultExpandPath);
221      }
222    }
223  }
224
225  private canCreateFolder(): boolean {
226    if (this.chooseItem && this.chooseItem.layer) {
227      return this.chooseItem.layer < FOLDER_LEVEL.MAX_LEVEL;
228    }
229    return true;
230  }
231
232  build() {
233    Column() {
234      Row() {
235        Image($r('app.media.hidisk_cancel_normal'))
236          .width($r('app.float.common_size46'))
237          .height($r('app.float.common_size46'))
238          .objectFit(ImageFit.Contain)
239          .padding($r('app.float.common_padding10'))
240          .stateStyles({
241            pressed: pressedStyles,
242            normal: normalStyles
243          })
244          .interpolation(ImageInterpolation.Medium)
245          .onClick(() => {
246            this.moveCallback.call(this, {
247              cancel: true
248            });
249          })
250        Blank()
251        Image($r('app.media.hidisk_ic_add_folder'))
252          .width($r('app.float.common_size46'))
253          .height($r('app.float.common_size46'))
254          .objectFit(ImageFit.Contain)
255          .margin({ left: $r('app.float.common_margin2') })
256          .padding($r('app.float.common_padding10'))
257          .stateStyles({
258            pressed: pressedStyles,
259            normal: normalStyles
260          })
261          .enabled(this.canCreateFolder())
262          .opacity(this.canCreateFolder() ? $r('app.float.common_opacity10') : $r('app.float.common_opacity2'))
263          .interpolation(ImageInterpolation.Medium)
264          .onClick(() => {
265            this.fileMkdirDialog.open();
266          })
267        Image($r('app.media.ic_ok'))
268          .width($r('app.float.common_size46'))
269          .height($r('app.float.common_size46'))
270          .objectFit(ImageFit.Contain)
271          .objectFit(ImageFit.Contain)
272          .margin({ left: $r('app.float.common_margin2') })
273          .padding($r('app.float.common_padding10'))
274          .stateStyles({
275            pressed: pressedStyles,
276            normal: normalStyles
277          })
278          .onClick(async () => {
279            setPreferencesValue(FILE_MANAGER_PREFERENCES.name,
280              FILE_MANAGER_PREFERENCES.lastSelectPath.key, this.selectUri);
281            AppStorage.SetOrCreate<string>(FILE_MANAGER_PREFERENCES.lastSelectPath.key, this.selectUri);
282            const prefix = this.fileName.trim();
283            if (!prefix) {
284              this.errorText = $r('app.string.input_nothing');
285              return;
286            }
287            const fileName = this.fileName.trim();
288            if (StringUtil.getBytesCount(fileName) > FILENAME_MAX_LENGTH) {
289              this.errorText = $r('app.string.max_input_length');
290            } else if (!isValidFileName(fileName)) {
291              this.errorText = $r('app.string.input_invalid');
292            } else {
293              this.errorText = null;
294              this.moveCallback({
295                selectUri: this.selectUri,
296                fileName: fileName
297              });
298            }
299          })
300      }.width('100%')
301      .padding({
302        top: $r('app.float.common_padding5'),
303        right: $r('app.float.common_padding15'),
304        bottom: $r('app.float.common_padding30'),
305        left: $r('app.float.common_padding15')
306      })
307
308      Row() {
309        if (this.listLength > 1) {
310          Image($r('app.media.hidisk_icon_unknown'))
311            .objectFit(ImageFit.Contain)
312            .renderMode(ImageRenderMode.Original)
313            .aspectRatio(1)
314            .width($r('app.float.common_size52'))
315            .height($r('app.float.common_size52'))
316            .alignSelf(ItemAlign.Center)
317            .margin({ right: $r('app.float.common_margin10') })
318            .borderRadius($r('app.float.common_borderRadius8'))
319        }
320        Column() {
321          Text(this.changeTitle)
322            .fontSize($r('app.float.common_font_size16'))
323            .maxLines(1)
324            .textOverflow({ overflow: TextOverflow.Ellipsis })
325        }
326        .layoutWeight(1).alignItems(HorizontalAlign.Start)
327      }
328      .padding({
329        right: $r('app.float.common_padding15'),
330        left: $r('app.float.common_padding15'),
331        bottom: $r('app.float.common_padding15')
332      })
333
334      if (this.listLength <= 1) {
335        Column() {
336          TextInput({ text: this.fileName })
337            .fontSize($r('app.float.common_font_size16'))
338            .backgroundColor($r('app.color.text_input_bg_color'))
339            .onChange((newVal) => {
340              this.fileName = newVal
341              this.errorText = null
342            })
343          Divider().vertical(false).strokeWidth(1).color(Color.Gray)
344            .margin({
345              left: $r('app.float.common_margin20'),
346              right: $r('app.float.common_margin20'),
347              bottom: $r('app.float.common_margin2')
348            })
349
350          Text(this.errorText)
351            .margin({
352              left: $r('app.float.common_margin20'),
353              right: $r('app.float.common_margin20')
354            })
355            .padding({
356              top: $r('app.float.common_padding5'),
357              bottom: $r('app.float.common_padding10')
358            })
359            .fontSize($r('app.float.common_font_size14'))
360            .fontColor($r('app.color.error_message_color'))
361            .alignSelf(ItemAlign.Start)
362        }
363        .margin({ bottom: $r('app.float.common_size10') })
364      }
365
366      Row().width('100%').height($r('app.float.common_size4')).opacity(0.05).backgroundColor($r('app.color.black'))
367
368      Row() {
369        Image($r('app.media.hidisk_ic_classify_phone'))
370          .objectFit(ImageFit.Contain)
371          .renderMode(ImageRenderMode.Original)
372          .aspectRatio(1)
373          .width($r('app.float.common_size24'))
374          .alignSelf(ItemAlign.Center)
375          .margin({ right: $r('app.float.common_margin16') })
376        Text($r('app.string.myPhone'))
377          .fontSize($r('app.float.common_font_size16'))
378          .layoutWeight(1)
379        Image($r('app.media.ic_arrow_right'))
380          .objectFit(ImageFit.Contain)
381          .autoResize(true)
382          .height($r('app.float.common_size12'))
383          .width($r('app.float.common_size12'))
384          .interpolation(ImageInterpolation.Medium)
385          .rotate({ z: 90, angle: this.topRotate ? 90 : 0 })
386      }
387      .width('100%')
388      .padding({
389        top: $r('app.float.common_padding16'),
390        bottom: $r('app.float.common_padding16'),
391        left: $r('app.float.common_padding24'),
392        right: $r('app.float.common_padding24')
393      })
394      .backgroundColor(this.isSelectRootPath ? $r('app.color.path_pick_selected_bg') : '')
395      .onClick(async () => {
396        this.selectName = getResourceString($r('app.string.myPhone'));
397        this.selectUri = globalThis.documentInfo && globalThis.documentInfo?.uri;
398        this.isSelectRootPath = true;
399        let { folderList, fileList } = await FileAccessExec.getFileData();
400        this.topRotate = !this.topRotate;
401        this.fileList = fileList;
402        this.rootData = folderList;
403        this.folderList = this.rootData;
404        this.isClickExpand = true;
405        this.defaultExpandPath = '';
406      })
407
408      Scroll(this.scroller) {
409        Column() {
410          if (this.rootData.length && this.topRotate) {
411            ForEach(this.rootData, (item) => {
412              if (this.isNeedLoadDefaultPath) {
413                TreeItem({
414                  fileItem: item,
415                  loadPath: this.defaultExpandPath,
416                  selectUri: $selectUri,
417                  chooseItem: $chooseItem,
418                  selectName: $selectName,
419                  layer: 2,
420                  folderList: $folderList,
421                  fileList: $fileList,
422                  isClickExpand: $isClickExpand
423                })
424              } else {
425                TreeItem({
426                  fileItem: item,
427                  selectUri: $selectUri,
428                  chooseItem: $chooseItem,
429                  selectName: $selectName,
430                  layer: 2,
431                  folderList: $folderList,
432                  fileList: $fileList,
433                  isClickExpand: $isClickExpand
434                })
435              }
436            })
437          }
438        }
439      }
440      .width('100%')
441      .scrollBar(BarState.Off)
442      .layoutWeight(1)
443      .padding({ bottom: $r('app.float.common_padding10') })
444      .align(Alignment.TopStart)
445    }
446    .width('100%')
447    .height('100%')
448    .backgroundColor($r('app.color.white'))
449    .borderRadius({ topLeft: $r('app.float.common_size24'), topRight: $r('app.float.common_size24') })
450  }
451}
452