chromium/ash/webui/camera_app_ui/resources/js/views/settings/primary.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 {assert, assertInstanceof} from '../../assert.js';
import {CameraManager} from '../../device/index.js';
import {
  BaseSettingsOption,
  BaseSettingsOptionGroup,
} from '../../device/type.js';
import * as dom from '../../dom.js';
import {setExpertMode} from '../../expert.js';
import {I18nString} from '../../i18n_string.js';
import * as loadTimeData from '../../models/load_time_data.js';
import {ChromeHelper} from '../../mojo/chrome_helper.js';
import * as nav from '../../nav.js';
import * as scannerChip from '../../scanner_chip.js';
import * as state from '../../state.js';
import {Mode, ViewName} from '../../type.js';
import * as util from '../../util.js';
import {View} from '../view.js';

import {BaseSettings} from './base.js';
import {PhotoAspectRatioSettings} from './photo_aspect_ratio.js';
import {PhotoResolutionSettings} from './photo_resolution.js';
import {
  toAspectRatioAriaLabel,
  toAspectRatioLabel,
  toPhotoResolutionOptionLabel,
  toVideoResolutionOptionLabel,
} from './util.js';
import {VideoResolutionSettings} from './video_resolution.js';

const helpUrl =
    'https://support.google.com/chromebook/?p=camera_usage_on_chromebook';

function bindButton(openerId: string, callback: () => void): void {
  const opener = dom.get(`#${openerId}`, HTMLElement);
  opener.addEventListener('click', () => {
    callback();
  });
}

/**
 * Controller of primary settings view.
 */
export class PrimarySettings extends BaseSettings {
  private readonly subViews: BaseSettings[];

  private readonly header: HTMLElement;

  private readonly photoResolutionSettings: HTMLButtonElement;

  private readonly photoAspectRatioSettings: HTMLButtonElement;

  private readonly videoResolutionSettings: HTMLButtonElement;

  private headerClickedCount = 0;

  private headerClickedLastTime: number|null = null;

  constructor(readonly cameraManager: CameraManager) {
    super(ViewName.SETTINGS);

    bindButton(
        'settings-photo-resolution',
        () => this.openSubSettings(ViewName.PHOTO_RESOLUTION_SETTINGS));
    bindButton(
        'settings-photo-aspect-ratio',
        () => this.openSubSettings(ViewName.PHOTO_ASPECT_RATIO_SETTINGS));
    bindButton(
        'settings-video-resolution',
        () => this.openSubSettings(ViewName.VIDEO_RESOLUTION_SETTINGS));
    bindButton(
        'settings-expert',
        () => this.openSubSettings(ViewName.EXPERT_SETTINGS));
    bindButton('settings-feedback', () => {
      // Prevent setting view overlapping preview when sending app
      // window feedback screenshot b/155938542.
      this.leave();
      ChromeHelper.getInstance().openFeedbackDialog(loadTimeData.getI18nMessage(
          I18nString.FEEDBACK_DESCRIPTION_PLACEHOLDER));
    });
    bindButton('settings-help', () => {
      ChromeHelper.getInstance().openUrlInBrowser(helpUrl);
    });

    this.photoResolutionSettings =
        dom.get(`#settings-photo-resolution`, HTMLButtonElement);
    this.photoAspectRatioSettings =
        dom.get(`#settings-photo-aspect-ratio`, HTMLButtonElement);
    this.videoResolutionSettings =
        dom.get(`#settings-video-resolution`, HTMLButtonElement);

    this.subViews = [
      new PhotoResolutionSettings(this.cameraManager),
      new PhotoAspectRatioSettings(this.cameraManager),
      new VideoResolutionSettings(this.cameraManager),
      new BaseSettings(ViewName.EXPERT_SETTINGS),
    ];

    this.header = dom.get('#settings-header', HTMLElement);
    this.header.addEventListener('click', () => this.onHeaderClicked());

    const cameraSettings = [
      this.photoResolutionSettings,
      this.photoAspectRatioSettings,
      this.videoResolutionSettings,
    ];

    cameraManager.registerCameraUI({
      onCameraUnavailable: () => {
        for (const setting of cameraSettings) {
          setting.disabled = true;
        }
      },
      onCameraAvailable: () => {
        for (const setting of cameraSettings) {
          setting.disabled = false;
        }
      },
    });

    this.cameraManager.addPhotoResolutionOptionListener((groups) => {
      const option = this.getSelectedOption(groups);
      if (option === null) {
        return;
      }
      const span =
          dom.getFrom(this.photoResolutionSettings, 'span', HTMLSpanElement);
      span.textContent = toPhotoResolutionOptionLabel(option.resolutionLevel);
    });
    this.cameraManager.addPhotoAspectRatioOptionListener((groups) => {
      const option = this.getSelectedOption(groups);
      if (option === null) {
        return;
      }
      const span =
          dom.getFrom(this.photoAspectRatioSettings, 'span', HTMLSpanElement);
      span.textContent = toAspectRatioLabel(option.aspectRatioSet);
      span.setAttribute(
          'aria-label', toAspectRatioAriaLabel(option.aspectRatioSet));
    });
    this.cameraManager.addVideoResolutionOptionListener((groups) => {
      const option = this.getSelectedOption(groups);
      if (option === null) {
        return;
      }
      const span =
          dom.getFrom(this.videoResolutionSettings, 'span', HTMLSpanElement);
      span.textContent = toVideoResolutionOptionLabel(option.resolutionLevel);
    });

    state.addObserver(state.State.ENABLE_PREVIEW_OCR, (enabled) => {
      if (!enabled) {
        scannerChip.dismiss();
      }
    });
  }

  private getSelectedOption<T extends BaseSettingsOption>(
      groups: Array<BaseSettingsOptionGroup<T>>): T|null {
    const currentGroup = groups.find(
        (group) => group.deviceId === this.cameraManager.getDeviceId());
    // No camera is ready so no information to show.
    if (currentGroup === undefined) {
      return null;
    }
    const selectedOption =
        currentGroup.options.find((option) => option.checked);
    assert(selectedOption !== undefined);
    return selectedOption;
  }

  /**
   * Handle click on primary settings header (used to trigger expert mode).
   */
  private onHeaderClicked() {
    const reset = () => {
      this.headerClickedCount = 0;
      this.headerClickedLastTime = null;
    };

    // Reset the counter if last click is more than 1 second ago.
    if (this.headerClickedLastTime !== null &&
        (Date.now() - this.headerClickedLastTime) > 1000) {
      reset();
    }

    this.headerClickedCount++;
    this.headerClickedLastTime = Date.now();

    if (this.headerClickedCount === 5) {
      setExpertMode(true);
      reset();
    }
  }

  override getSubViews(): View[] {
    return this.subViews;
  }

  override entering(): void {
    this.updateHeader();
  }

  private updateHeader(): void {
    const headerString = state.get(Mode.VIDEO) ? I18nString.VIDEO_SETTINGS :
                                                 I18nString.PHOTO_SETTINGS;
    this.header.setAttribute('i18n-text', headerString);
    util.setupI18nElements(assertInstanceof(this.header, HTMLElement));
  }

  private async openSubSettings(name: ViewName): Promise<void> {
    // Dismiss primary-settings if sub-settings was dismissed by background
    // click.
    const cond = await nav.open(name).closed;
    if (cond.kind === 'BACKGROUND_CLICKED') {
      this.leave(cond);
    }
  }
}