chromium/ash/webui/personalization_app/resources/js/wallpaper/local_images_element.ts

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

/**
 * @fileoverview WallpaperImages displays a list of wallpaper images from a
 * wallpaper collection. It requires a parameter collection-id to fetch
 * and display the images. It also caches the list of wallpaper images by
 * wallpaper collection id to avoid refetching data unnecessarily.
 */

import 'chrome://resources/ash/common/personalization/common.css.js';
import 'chrome://resources/ash/common/personalization/wallpaper.css.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import '../../common/icons.html.js';

import {WallpaperGridItemSelectedEvent} from 'chrome://resources/ash/common/personalization/wallpaper_grid_item_element.js';
import {isImageDataUrl, isNonEmptyFilePath} from 'chrome://resources/ash/common/sea_pen/sea_pen_utils.js';
import {assert} from 'chrome://resources/js/assert.js';
import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
import {afterNextRender} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {CurrentWallpaper, WallpaperProviderInterface, WallpaperType} from '../../personalization_app.mojom-webui.js';
import {WithPersonalizationStore} from '../personalization_store.js';

import {DefaultImageSymbol, DisplayableImage, kDefaultImageSymbol} from './constants.js';
import {getTemplate} from './local_images_element.html.js';
import {getPathOrSymbol, isDefaultImage} from './utils.js';
import {selectWallpaper} from './wallpaper_controller.js';
import {getWallpaperProvider} from './wallpaper_interface_provider.js';


export class LocalImagesElement extends WithPersonalizationStore {
  static get is() {
    return 'local-images';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      images_: {
        type: Array,
        observer: 'onImagesChanged_',
      },

      /** Mapping of local image path to data url. */
      imageData_: Object,

      /** Mapping of local image path to boolean. */
      imageDataLoading_: Object,

      currentSelected_: Object,

      /** The pending selected image. */
      pendingSelected_: Object,

      imagesToDisplay_: {
        type: Array,
        value: [],
      },
    };
  }

  static get observers() {
    return ['onImageLoaded_(imageData_, imageDataLoading_)'];
  }

  private wallpaperProvider_: WallpaperProviderInterface;
  private images_: Array<FilePath|DefaultImageSymbol>|null;
  private imageData_: Record<FilePath['path']|DefaultImageSymbol, Url>;
  private imageDataLoading_:
      Record<FilePath['path']|DefaultImageSymbol, boolean>;
  private currentSelected_: CurrentWallpaper|null;
  private pendingSelected_: DisplayableImage|null;
  private imagesToDisplay_: Array<FilePath|DefaultImageSymbol>;

  constructor() {
    super();
    this.wallpaperProvider_ = getWallpaperProvider();
  }

  override ready() {
    super.ready();
    afterNextRender(this, () => {
      this.shadowRoot!.getElementById('main')!.focus();
    });
  }

  override connectedCallback() {
    super.connectedCallback();
    this.watch<LocalImagesElement['images_']>(
        'images_', state => state.wallpaper.local.images);
    this.watch<LocalImagesElement['imageData_']>(
        'imageData_', state => state.wallpaper.local.data);
    this.watch<LocalImagesElement['imageDataLoading_']>(
        'imageDataLoading_', state => state.wallpaper.loading.local.data);
    this.watch<LocalImagesElement['currentSelected_']>(
        'currentSelected_', state => state.wallpaper.currentSelected);
    this.watch<LocalImagesElement['pendingSelected_']>(
        'pendingSelected_', state => state.wallpaper.pendingSelected);
    this.updateFromStore();
  }

  /** Sets |imagesToDisplay| when a new set of local images loads. */
  private onImagesChanged_(images: LocalImagesElement['images_']) {
    this.imagesToDisplay_ = (images || []).filter(image => {
      const key = getPathOrSymbol(image);
      if (this.imageDataLoading_[key] === false) {
        return isImageDataUrl(this.imageData_[key]);
      }
      return true;
    });
  }

  /**
   * Called each time a new image thumbnail is loaded. Removes images
   * from the list of displayed images if it has failed to load.
   */
  private onImageLoaded_(
      imageData: LocalImagesElement['imageData_'],
      imageDataLoading: LocalImagesElement['imageDataLoading_']) {
    if (!imageData || !imageDataLoading) {
      return;
    }
    // Iterate backwards in case we need to splice to remove from
    // |imagesToDisplay| while iterating.
    for (let i = this.imagesToDisplay_.length - 1; i >= 0; i--) {
      const image = this.imagesToDisplay_[i];
      const key = getPathOrSymbol(image);
      const failed =
          imageDataLoading[key] === false && !isImageDataUrl(imageData[key]);
      if (failed) {
        this.splice('imagesToDisplay_', i, 1);
      }
    }
  }

  private isImageSelected_(
      image: FilePath|DefaultImageSymbol|null,
      currentSelected: LocalImagesElement['currentSelected_'],
      pendingSelected: LocalImagesElement['pendingSelected_']): boolean {
    if (!image || (!currentSelected && !pendingSelected)) {
      return false;
    }
    if (isDefaultImage(image)) {
      return (
          (isDefaultImage(pendingSelected)) ||
          (!pendingSelected && !!currentSelected &&
           currentSelected.type === WallpaperType.kDefault));
    }
    return (
        isNonEmptyFilePath(pendingSelected) &&
            image.path === pendingSelected.path ||
        !!currentSelected && image.path === currentSelected.key &&
            !pendingSelected);
  }

  private getAriaLabel_(
      image: FilePath|DefaultImageSymbol|null,
      imageDataLoading: LocalImagesElement['imageDataLoading_']): string {
    if (this.isImageLoading_(image, imageDataLoading)) {
      return this.i18n('ariaLabelLoading');
    }
    if (isDefaultImage(image)) {
      return this.i18n('defaultWallpaper');
    }
    if (!isNonEmptyFilePath(image)) {
      return '';
    }
    const path = image.path;
    return path.substring(path.lastIndexOf('/') + 1);
  }

  private isImageLoading_(
      image: FilePath|DefaultImageSymbol|null,
      imageDataLoading: LocalImagesElement['imageDataLoading_']): boolean {
    if (!image || !imageDataLoading) {
      return true;
    }
    const key = getPathOrSymbol(image);
    // If key is not present, then loading has not yet started. Still show a
    // loading tile in this case.
    return !imageDataLoading.hasOwnProperty(key) ||
        imageDataLoading[key] === true;
  }

  private getImageData_(
      image: FilePath|DefaultImageSymbol|null,
      imageData: LocalImagesElement['imageData_'],
      imageDataLoading: LocalImagesElement['imageDataLoading_']): Url|null {
    if (!image || this.isImageLoading_(image, imageDataLoading)) {
      return null;
    }
    const data = imageData[getPathOrSymbol(image)];
    // Return a "fail" url that will not load.
    if (!isImageDataUrl(data)) {
      return {url: ''};
    }
    return data;
  }

  private getImageDataId_(image: FilePath|DefaultImageSymbol|null): string {
    if (!image) {
      return '';
    }
    return isNonEmptyFilePath(image) ? image.path : image.toString();
  }

  private onImageSelected_(event: WallpaperGridItemSelectedEvent&
                           {model: {item: FilePath | DefaultImageSymbol}}) {
    assert(
        event.model.item === kDefaultImageSymbol ||
            isNonEmptyFilePath(event.model.item),
        'local image is a file path or default image');
    selectWallpaper(event.model.item, this.wallpaperProvider_, this.getStore());
  }

  private getAriaIndex_(i: number): number {
    return i + 1;
  }
}

customElements.define(LocalImagesElement.is, LocalImagesElement);