chromium/chrome/browser/resources/side_panel/customize_chrome/themes.ts

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

import 'chrome://customize-chrome-side-panel.top-chrome/shared/sp_heading.js';
import 'chrome://resources/cr_elements/cr_auto_img/cr_auto_img.js';
import 'chrome://resources/cr_elements/cr_grid/cr_grid.js';
import 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js';
import './check_mark_wrapper.js';

import type {SpHeadingElement} from 'chrome://customize-chrome-side-panel.top-chrome/shared/sp_heading.js';
import {HelpBubbleMixinLit} from 'chrome://resources/cr_components/help_bubble/help_bubble_mixin_lit.js';
import type {CrToggleElement} from 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js';
import {assert} from 'chrome://resources/js/assert.js';
import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import {CustomizeChromeAction, recordCustomizeChromeAction} from './common.js';
import type {BackgroundCollection, CollectionImage, CustomizeChromePageCallbackRouter, CustomizeChromePageHandlerInterface, Theme} from './customize_chrome.mojom-webui.js';
import {CustomizeChromeApiProxy} from './customize_chrome_api_proxy.js';
import {getCss} from './themes.css.js';
import {getHtml} from './themes.html.js';
import {WindowProxy} from './window_proxy.js';

export const CHROME_THEME_ELEMENT_ID =
    'CustomizeChromeUI::kChromeThemeElementId';
export const CHROME_THEME_BACK_ELEMENT_ID =
    'CustomizeChromeUI::kChromeThemeBackElementId';

const ThemesElementBase = HelpBubbleMixinLit(CrLitElement);

export interface ThemesElement {
  $: {
    refreshDailyToggle: CrToggleElement,
    heading: SpHeadingElement,
  };
}

export class ThemesElement extends ThemesElementBase {
  static get is() {
    return 'customize-chrome-themes';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      selectedCollection: {type: Object},
      header_: {type: String},
      isRefreshToggleChecked_: {type: Boolean},
      theme_: {type: Object},
      themes_: {type: Array},
    };
  }

  selectedCollection: BackgroundCollection|null = null;

  protected header_: string = '';
  protected isRefreshToggleChecked_: boolean = false;
  private theme_?: Theme;
  protected themes_: CollectionImage[] = [];

  private callbackRouter_: CustomizeChromePageCallbackRouter;
  private pageHandler_: CustomizeChromePageHandlerInterface;
  private previewImageLoadStartEpoch_: number = -1;
  private setThemeListenerId_: number|null = null;

  constructor() {
    super();
    this.pageHandler_ = CustomizeChromeApiProxy.getInstance().handler;
    this.callbackRouter_ = CustomizeChromeApiProxy.getInstance().callbackRouter;
  }

  override connectedCallback() {
    super.connectedCallback();
    this.setThemeListenerId_ =
        this.callbackRouter_.setTheme.addListener((theme: Theme) => {
          this.theme_ = theme;
        });
    this.pageHandler_.updateTheme();
    FocusOutlineManager.forDocument(document);
  }

  override disconnectedCallback() {
    super.disconnectedCallback();
    assert(this.setThemeListenerId_);
    this.callbackRouter_.removeListener(this.setThemeListenerId_);
  }

  override willUpdate(changedProperties: PropertyValues<this>) {
    super.willUpdate(changedProperties);

    if (changedProperties.has('selectedCollection')) {
      this.onCollectionChange_();
    }

    const changedPrivateProperties =
        changedProperties as Map<PropertyKey, unknown>;

    if (changedPrivateProperties.has('theme_') ||
        changedProperties.has('selectedCollection')) {
      this.isRefreshToggleChecked_ = this.computeIsRefreshToggleChecked_();
    }
  }

  override updated(changedProperties: PropertyValues<this>) {
    super.updated(changedProperties);

    const changedPrivateProperties =
        changedProperties as Map<PropertyKey, unknown>;

    if (changedPrivateProperties.has('themes_') && this.themes_.length > 0) {
      this.onThemesRendered_();
    }
  }

  override firstUpdated() {
    this.registerHelpBubble(
        CHROME_THEME_BACK_ELEMENT_ID, this.$.heading.getBackButton());
  }

  focusOnBackButton() {
    this.$.heading.getBackButton().focus();
  }

  private onThemesRendered_() {
    const firstTile = this.shadowRoot!.querySelector('.tile.theme');
    if (firstTile) {
      this.registerHelpBubble(CHROME_THEME_ELEMENT_ID, firstTile);
    }
  }

  protected onPreviewImageLoad_() {
    chrome.metricsPrivate.recordValue(
        {
          metricName: 'NewTabPage.Images.ShownTime.ThemePreviewImage',
          type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LOG,
          min: 1,
          max: 60000,  // 60 seconds.
          buckets: 100,
        },
        Math.floor(
            WindowProxy.getInstance().now() -
            this.previewImageLoadStartEpoch_));
  }

  private onCollectionChange_() {
    this.header_ = '';
    this.themes_ = [];
    if (this.selectedCollection) {
      this.previewImageLoadStartEpoch_ = WindowProxy.getInstance().now();
      this.pageHandler_.getBackgroundImages(this.selectedCollection!.id)
          .then(({images}) => {
            this.themes_ = images;
          });
      this.header_ = this.selectedCollection.label;
    }
  }

  protected onBackClick_() {
    this.dispatchEvent(new Event('back-click'));
  }

  protected onSelectTheme_(e: Event) {
    const index = Number((e.currentTarget as HTMLElement).dataset['index']);
    const theme = this.themes_[index]!;

    recordCustomizeChromeAction(
        CustomizeChromeAction.FIRST_PARTY_COLLECTION_THEME_SELECTED);
    const {
      attribution1,
      attribution2,
      attributionUrl,
      imageUrl,
      previewImageUrl,
      collectionId,
    } = theme;
    this.pageHandler_.setBackgroundImage(
        attribution1, attribution2, attributionUrl, imageUrl, previewImageUrl,
        collectionId);
  }

  private computeIsRefreshToggleChecked_(): boolean {
    if (!this.selectedCollection) {
      return false;
    }
    return !!this.theme_ && !!this.theme_.backgroundImage &&
        this.theme_.backgroundImage.dailyRefreshEnabled &&
        this.selectedCollection!.id ===
        this.theme_.backgroundImage.collectionId;
  }

  protected onRefreshDailyToggleChange_(e: CustomEvent<boolean>) {
    this.pageHandler_.setDailyRefreshCollectionId(
        e.detail ? this.selectedCollection!.id : '');
  }

  protected isThemeSelected_(url: string): boolean {
    return !!this.theme_ && !this.theme_.thirdPartyThemeInfo &&
        !!this.theme_.backgroundImage &&
        this.theme_?.backgroundImage.url.url === url &&
        !this.isRefreshToggleChecked_;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'customize-chrome-themes': ThemesElement;
  }
}

customElements.define(ThemesElement.is, ThemesElement);