chromium/ash/webui/personalization_app/resources/js/wallpaper/wallpaper_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/wallpaper.css.js';
import 'chrome://resources/polymer/v3_0/iron-media-query/iron-media-query.js';

import {WallpaperGridItemSelectedEvent} from 'chrome://resources/ash/common/personalization/wallpaper_grid_item_element.js';
import {isNonEmptyArray} from 'chrome://resources/ash/common/sea_pen/sea_pen_utils.js';
import {assert} from 'chrome://resources/js/assert.js';

import {CurrentWallpaper, OnlineImageType, WallpaperCollection, WallpaperImage, WallpaperType} from '../../personalization_app.mojom-webui.js';
import {dismissTimeOfDayBanner} from '../ambient/ambient_controller.js';
import {isTimeOfDayWallpaperEnabled} from '../load_time_booleans.js';
import {PersonalizationRouterElement} from '../personalization_router_element.js';
import {WithPersonalizationStore} from '../personalization_store.js';
import {setColorModeAutoSchedule} from '../theme/theme_controller.js';
import {getThemeProvider} from '../theme/theme_interface_provider.js';
import {ThemeObserver} from '../theme/theme_observer.js';

import {ImageTile} from './constants.js';
import {getLoadingPlaceholderAnimationDelay, getLoadingPlaceholders, isWallpaperImage} from './utils.js';
import {getShouldShowTimeOfDayWallpaperDialog, selectWallpaper} from './wallpaper_controller.js';
import {getTemplate} from './wallpaper_images_element.html.js';
import {getWallpaperProvider} from './wallpaper_interface_provider.js';

(BigInt.prototype as any).toJSON = function() {
  return this.toString();
};

/**
 * If |current| is set and is an online wallpaper (include daily refresh
 * wallpaper), return the unitId of that image. Otherwise returns null.
 */
function getUnitId(current: CurrentWallpaper|null): bigint|null {
  if (current == null) {
    return null;
  }
  if (current.type !== WallpaperType.kOnline &&
      current.type !== WallpaperType.kDaily) {
    return null;
  }
  try {
    return BigInt(current.key);
  } catch (e) {
    console.warn('Required a BigInt value here', e);
    return null;
  }
}

/**
 * Return a list of tiles capturing units of image variants.
 */
export function getImageTiles(
    isDarkModeActive: boolean, images: WallpaperImage[]): ImageTile[] {
  const tileMap = images.reduce((result, next) => {
    if (result.has(next.unitId)) {
      const tile = result.get(next.unitId)! as ImageTile;
      if (!tile.hasPreviewImage) {
        tile.preview.push(next.url);
      }
    } else {
      result.set(next.unitId, {
        preview: [next.url],
        unitId: next.unitId,
      } as ImageTile);
    }
    // Populate the assetId and attribution based on image type and system's
    // color mode.
    const tile = result.get(next.unitId)! as ImageTile;
    switch (next.type) {
      case OnlineImageType.kLight:
        if (!isDarkModeActive) {
          tile.assetId = next.assetId;
          tile.attribution = next.attribution;
        }
        break;
      case OnlineImageType.kDark:
        if (isDarkModeActive) {
          tile.assetId = next.assetId;
          tile.attribution = next.attribution;
        }
        break;
      case OnlineImageType.kMorning:
      case OnlineImageType.kLateAfternoon:
        tile.isTimeOfDayWallpaper = true;
        tile.assetId = next.assetId;
        tile.attribution = next.attribution;
        break;
      case OnlineImageType.kPreview:
        tile.hasPreviewImage = true;
        tile.preview = [next.url];
        tile.assetId = next.assetId;
        tile.attribution = next.attribution;
        break;
      case OnlineImageType.kUnknown:
        tile.assetId = next.assetId;
        tile.attribution = next.attribution;
        break;
    }
    return result;
  }, new Map() as Map<bigint, ImageTile>);
  return [...tileMap.values()];
}

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

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

  static get properties() {
    return {
      /**
       * Whether dark mode is the active preferred color scheme.
       */
      isDarkModeActive: {
        type: Boolean,
        value: false,
      },

      /**
       * The current collection id to display.
       */
      collectionId: String,

      collections_: Array,

      collectionsLoading_: Boolean,

      images_: Object,

      /**
       * Mapping of collection_id to boolean.
       */
      imagesLoading_: Object,

      selectedUnitId_: {
        type: BigInt,
        value: null,
      },

      /**
       * The pending selected image.
       */
      pendingSelectedUnitId_: {
        type: BigInt,
        value: null,
      },

      hasError_: {
        type: Boolean,
        computed:
            'computeHasError_(images_, imagesLoading_, collections_, collectionsLoading_, collectionId)',
        observer: 'onHasErrorChanged_',
      },

      tiles_: {
        type: Array,
        computed:
            'computeTiles_(images_, imagesLoading_, collectionId, isDarkModeActive)',
        observer: 'onTilesChanged_',
      },

      /**
       * The pending ToD wallpaper to be set when the dialog is displayed.
       */
      pendingTimeOfDayWallpaper_: Object,

      colorModeAutoScheduleEnabled_: Boolean,

      showTimeOfDayWallpaperDialog_: Boolean,
    };
  }

  collectionId: string;
  isDarkModeActive: boolean;
  private collections_: WallpaperCollection[]|null;
  private collectionsLoading_: boolean;
  private images_: Record<string, WallpaperImage[]|null>;
  private imagesLoading_: Record<string, boolean>;
  private selectedUnitId_: bigint|null;
  private pendingSelectedUnitId_: bigint|null;
  private hasError_: boolean;
  private tiles_: ImageTile[];
  private pendingTimeOfDayWallpaper_: WallpaperImage|null;
  private colorModeAutoScheduleEnabled_: boolean|null;
  private showTimeOfDayWallpaperDialog_: boolean;

  override connectedCallback() {
    super.connectedCallback();
    ThemeObserver.initThemeObserverIfNeeded();
    this.watch<WallpaperImagesElement['images_']>(
        'images_', state => state.wallpaper.backdrop.images);
    this.watch<WallpaperImagesElement['imagesLoading_']>(
        'imagesLoading_', state => state.wallpaper.loading.images);
    this.watch<WallpaperImagesElement['collections_']>(
        'collections_', state => state.wallpaper.backdrop.collections);
    this.watch<WallpaperImagesElement['collectionsLoading_']>(
        'collectionsLoading_', state => state.wallpaper.loading.collections);
    this.watch<WallpaperImagesElement['selectedUnitId_']>(
        'selectedUnitId_', state => getUnitId(state.wallpaper.currentSelected));
    this.watch<WallpaperImagesElement['pendingSelectedUnitId_']>(
        'pendingSelectedUnitId_',
        state => isWallpaperImage(state.wallpaper.pendingSelected) ?
            state.wallpaper.pendingSelected.unitId :
            null);
    this.watch<WallpaperImagesElement['colorModeAutoScheduleEnabled_']>(
        'colorModeAutoScheduleEnabled_',
        state => state.theme.colorModeAutoScheduleEnabled);
    this.watch<WallpaperImagesElement['showTimeOfDayWallpaperDialog_']>(
        'showTimeOfDayWallpaperDialog_',
        state => state.wallpaper.shouldShowTimeOfDayWallpaperDialog);
    this.updateFromStore();
  }

  /**
   * Determine whether the current collection failed to load or is not a valid
   * |collectionId|. Check that collections list loaded successfully, and that
   * the collection with id |collectionId| also loaded successfully.
   */
  private computeHasError_(
      images: Record<string, WallpaperImage>,
      imagesLoading: Record<string, boolean>,
      collections: WallpaperCollection[], collectionsLoading: boolean,
      collectionId: string): boolean {
    // Not yet initialized or still loading.
    if (!imagesLoading || !collectionId || collectionsLoading) {
      return false;
    }

    // Failed to load collections or unknown collectionId.
    if (!isNonEmptyArray(collections) ||
        !collections.some(collection => collection.id === collectionId)) {
      return true;
    }

    // Specifically check === false to guarantee that key is in the object and
    // set as false.
    return imagesLoading[collectionId] === false &&
        !isNonEmptyArray(images[collectionId]);
  }

  /** Kick the user back to wallpaper collections page if failed to load. */
  private onHasErrorChanged_(hasError: boolean) {
    if (hasError) {
      console.warn('An error occurred while loading collections or images');
      // Navigate back to main page and refresh.
      PersonalizationRouterElement.reloadAtWallpaper();
    }
  }

  /**
   * Send images if loading is ready and we have some images. Punt back to
   * main page if there is an error viewing this collection.
   */
  private computeTiles_(
      images: Record<string, WallpaperImage[]>,
      imagesLoading: Record<string, boolean>, collectionId: string,
      isDarkModeActive: boolean): ImageTile[]|number[] {
    const hasImages = !!images && !!imagesLoading && collectionId &&
        imagesLoading[collectionId] === false &&
        isNonEmptyArray(images[collectionId]);

    if (!hasImages) {
      return getLoadingPlaceholders(() => 1);
    }

    const imageArr = images[collectionId]!;
    return getImageTiles(isDarkModeActive, imageArr);
  }

  private onTilesChanged_(tiles: ImageTile[]) {
    if (tiles.some((tile => this.isTimeOfDayWallpaper_(tile)))) {
      // Dismisses the banner after the Time of Day collection images are
      // displayed.
      dismissTimeOfDayBanner(this.getStore());
    }
  }

  private getMainAriaLabel_(
      collectionId: string, collections: WallpaperCollection[]) {
    if (!collectionId || !Array.isArray(collections)) {
      return '';
    }
    const collection =
        collections.find(collection => collection.id === collectionId);

    if (!collection) {
      console.warn('Did not find collection matching collectionId');
      return '';
    }

    return collection.name;
  }

  private isLoadingTile_(tile: number|ImageTile): tile is number {
    return typeof tile === 'number';
  }

  private isImageTile_(tile: number|ImageTile): tile is ImageTile {
    return tile.hasOwnProperty('preview') &&
        Array.isArray((tile as any).preview);
  }

  private getLoadingPlaceholderAnimationDelay_(index: number): string {
    return getLoadingPlaceholderAnimationDelay(index);
  }

  private isTileSelected_(
      tile: ImageTile, selectedUnitId: bigint|null,
      pendingSelectedUnitId: bigint|null): boolean {
    // Make sure that both are bigint (not undefined) and equal.
    return (
        typeof selectedUnitId === 'bigint' && !!tile &&
            tile.unitId === selectedUnitId && !pendingSelectedUnitId ||
        typeof pendingSelectedUnitId === 'bigint' && !!tile &&
            tile.unitId === pendingSelectedUnitId);
  }

  private isTimeOfDayWallpaper_(tile: number|ImageTile): boolean {
    return this.isImageTile_(tile) && !!tile.isTimeOfDayWallpaper;
  }

  private async onImageSelected_(e: WallpaperGridItemSelectedEvent&
                                 {model: {item: ImageTile}}) {
    const unitId = e.model.item.unitId;
    assert(unitId && typeof unitId === 'bigint', 'unitId not found');
    const images = this.images_[this.collectionId]!;
    assert(isNonEmptyArray(images));
    const selectedImage = images.find(choice => choice.unitId === unitId);
    assert(selectedImage, 'could not find selected image');
    if (await this.shouldShowTimeOfDayWallpaperDialog_(e.model.item)) {
      this.pendingTimeOfDayWallpaper_ = selectedImage;
      return;
    }
    selectWallpaper(selectedImage, getWallpaperProvider(), this.getStore());
  }

  private async shouldShowTimeOfDayWallpaperDialog_(tile: ImageTile):
      Promise<boolean> {
    if (isTimeOfDayWallpaperEnabled()) {
      await getShouldShowTimeOfDayWallpaperDialog(
          getWallpaperProvider(), this.getStore());
    }
    return this.isTimeOfDayWallpaper_(tile) &&
        this.showTimeOfDayWallpaperDialog_ &&
        !this.colorModeAutoScheduleEnabled_;
  }

  private onCloseTimeOfDayDialog_() {
    assert(
        this.pendingTimeOfDayWallpaper_,
        'could not find the time of day wallpaper');
    selectWallpaper(
        this.pendingTimeOfDayWallpaper_, getWallpaperProvider(),
        this.getStore());
    this.pendingTimeOfDayWallpaper_ = null;
  }

  private onConfirmTimeOfDayDialog_() {
    setColorModeAutoSchedule(
        /*enabled=*/ true, getThemeProvider(), this.getStore());
    this.onCloseTimeOfDayDialog_();
  }

  private getAriaLabel_(tile: number|ImageTile): string {
    if (this.isLoadingTile_(tile)) {
      return this.i18n('ariaLabelLoading');
    }
    return tile.attribution!.join(' ');
  }

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

customElements.define(WallpaperImagesElement.is, WallpaperImagesElement);