chromium/ui/file_manager/file_manager/foreground/js/app_state_controller.ts

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';

import {isRecentFileData, isRecentRoot} from '../../common/js/entry_utils.js';
import {storage} from '../../common/js/storage.js';
import type {DialogType} from '../../state/state.js';

import type {DirectoryChangeEvent, DirectoryModel} from './directory_model.js';
import {GROUP_BY_FIELD_DIRECTORY, GROUP_BY_FIELD_MODIFICATION_TIME} from './file_list_model.js';
import type {FileManagerUI} from './ui/file_manager_ui.js';
import type {FileTableColumnModel} from './ui/file_table.js';
import {ListType} from './ui/list_container.js';

export class AppStateController {
  private readonly viewOptionStorageKey_: string;
  private directoryModel_: DirectoryModel|null = null;
  private ui_: FileManagerUI|null = null;
  private viewOptions_: any = null;

  /**
   * Preferred sort field of file list. This will be ignored in the Recent
   * folder, since it always uses descendant order of date-modified.
   */
  private fileListSortField_: string|null = DEFAULT_SORT_FIELD;

  /**
   * Preferred sort direction of file list. This will be ignored in the Recent
   * folder, since it always uses descendant order of date-modified.
   */
  private fileListSortDirection_: string|null = DEFAULT_SORT_DIRECTION;

  constructor(dialogType: DialogType) {
    this.viewOptionStorageKey_ = 'file-manager-' + dialogType;
  }

  async loadInitialViewOptions(): Promise<void> {
    // Load initial view option.
    try {
      const values = await storage.local.getAsync(this.viewOptionStorageKey_);

      this.viewOptions_ = {};

      const value = values[this.viewOptionStorageKey_];
      if (!value) {
        return;
      }

      // Load the global default options.
      try {
        this.viewOptions_ = JSON.parse(value);
      } catch (ignore) {
      }
    } catch (error) {
      this.viewOptions_ = {};
      console.warn(error);
    }
  }

  initialize(ui: FileManagerUI, directoryModel: DirectoryModel) {
    assert(this.viewOptions_);

    this.ui_ = ui;
    this.directoryModel_ = directoryModel;
    const {table} = ui.listContainer;

    // Restore preferences.
    ui.setCurrentListType(this.viewOptions_.listType || ListType.DETAIL);
    if (this.viewOptions_.sortField) {
      this.fileListSortField_ = this.viewOptions_.sortField;
    }
    if (this.viewOptions_.sortDirection) {
      this.fileListSortDirection_ = this.viewOptions_.sortDirection;
    }
    this.directoryModel_.getFileList().sort(
        this.fileListSortField_!, this.fileListSortDirection_!);
    if (this.viewOptions_.isAllAndroidFoldersVisible) {
      this.directoryModel_.getFileFilter().setAllAndroidFoldersVisible(true);
    }
    if (this.viewOptions_.columnConfig) {
      (table.columnModel as FileTableColumnModel)
          .restoreColumnConfig(this.viewOptions_.columnConfig);
      // The stored config might not match the current table width, do a
      // normalization here after restoration.
      table.columnModel.normalizeWidths(table.clientWidth);
    }

    // Register event listeners.
    table.addEventListener(
        'column-resize-end', this.saveViewOptions.bind(this));
    directoryModel.getFileList().addEventListener(
        'sorted', this.onFileListSorted_.bind(this));
    directoryModel.getFileFilter().addEventListener(
        'changed', this.onFileFilterChanged_.bind(this));
    directoryModel.addEventListener(
        'directory-changed', this.onDirectoryChanged_.bind(this));
  }

  /**
   * Saves current view option.
   */
  async saveViewOptions() {
    const prefs = {
      sortField: this.fileListSortField_,
      sortDirection: this.fileListSortDirection_,
      columnConfig: {},
      listType: this.ui_?.listContainer.currentListType,
      isAllAndroidFoldersVisible:
          this.directoryModel_?.getFileFilter().isAllAndroidFoldersVisible(),
    };
    assert(this.ui_);
    const cm = this.ui_.listContainer.table.columnModel as FileTableColumnModel;
    prefs.columnConfig = cm.exportColumnConfig();
    // Save the global default.
    const items: Record<string, string> = {};
    items[this.viewOptionStorageKey_] = JSON.stringify(prefs);
    storage.local.setAsync(items);
  }

  private async onFileListSorted_() {
    assert(this.directoryModel_);
    const currentDirectory = this.directoryModel_.getCurrentDirEntry();
    if (!currentDirectory) {
      return;
    }

    // Update preferred sort field and direction only when the current directory
    // is not Recent folder.
    if (!isRecentRoot(currentDirectory)) {
      const currentSortStatus = this.directoryModel_.getFileList().sortStatus;
      this.fileListSortField_ = currentSortStatus.field;
      this.fileListSortDirection_ = currentSortStatus.direction;
    }
    this.saveViewOptions();
  }

  private async onFileFilterChanged_() {
    assert(this.directoryModel_);
    const isAllAndroidFoldersVisible =
        this.directoryModel_.getFileFilter().isAllAndroidFoldersVisible();
    if (this.viewOptions_.isAllAndroidFoldersVisible !==
        isAllAndroidFoldersVisible) {
      this.viewOptions_.isAllAndroidFoldersVisible = isAllAndroidFoldersVisible;
      this.saveViewOptions();
    }
  }

  private onDirectoryChanged_(event: DirectoryChangeEvent) {
    assert(this.directoryModel_);
    assert(this.ui_);

    // Sort the file list by:
    // 1) 'date-modified' and 'desc' order on Recent folder.
    // 2) preferred field and direction on other folders.
    const fileData = this.directoryModel_.getCurrentFileData();
    if (!fileData) {
      return;
    }

    const isOnRecent = isRecentFileData(fileData);
    const fileListModel = this.directoryModel_.getFileList();
    this.ui_.listContainer.isOnRecent = isOnRecent;
    // TODO(b/354587005): Capture all recent categories in the store.
    // Currently only fake-entry://recent/all is in the store, but
    // `previousFileKey` can be other categories like fake-entry://recent/images
    // which is not in the store, we can't rely on store data to fetch the entry
    // by the file key here, hence matching the file key string directly here.
    const wasOnRecentBefore =
        event.detail.previousFileKey?.startsWith('fake-entry://recent/');
    if (isOnRecent !== wasOnRecentBefore) {
      if (isOnRecent) {
        fileListModel.groupByField = GROUP_BY_FIELD_MODIFICATION_TIME;
        fileListModel.sort(DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION);
      } else {
        const isGridView =
            this.ui_?.listContainer.currentListType === ListType.THUMBNAIL;
        fileListModel.groupByField =
            isGridView ? GROUP_BY_FIELD_DIRECTORY : null;
        fileListModel.sort(
            this.fileListSortField_!, this.fileListSortDirection_!);
      }
    }
  }
}

/**
 * Default sort field of the file list.
 */
const DEFAULT_SORT_FIELD = 'modificationTime';

/**
 * Default sort direction of the file list.
 */
const DEFAULT_SORT_DIRECTION = 'desc';