KanCraft/kanColleWidget

View on GitHub
src/js/Applications/Components/Options/Notifications/index.tsx

Summary

Maintainability
D
2 days
Test Coverage
import React, { createRef } from "react";
import cn from "classnames";
import Setting from "../../../Models/Settings/NotificationSetting";
import FileService from "../../../../Services/Files";
import NotificationService from "../../../../Services/Notification";
import { Kind } from "../../../Models/Queue/Queue";
import QuestAlertSetting from "../../../Models/Settings/QuestAlertSetting";
import SoundService from "../../../../Services/Sound";
import DisableMissionNotificationsSettingView from "./DisableMissionNotificationsSetting";

class NotificationSettingView extends React.Component<{
  label: string,
  kind: string,
}, {
  setting: Setting,
}> {
  constructor(props) {
    super(props);
    this.state = {
      setting: Setting.find<Setting>(this.props.kind),
    };
  }
  render() {
    const { setting } = this.state;
    const { kind } = this.props;
    return (
      <div className={`container notification-kind-setting ${this.props.kind}`}>
        <div className="columns">
          <div className={cn("column", "kind-label", kind == Kind.Tiredness ? "col-1" : "col-3" )}>
            <div>{this.props.label}</div>
          </div>
          {kind == Kind.Tiredness ? <div className="column col-2 form-group">
            {setting.enabled ? <select
              disabled={!setting.enabled}
              className="form-select"
              onChange={ev => this.setState({ setting: setting.update({ interval: parseInt(ev.currentTarget.value) }) })}
              value={setting.interval || 15}
            >
              <option key="10" value={10}>10分</option>
              <option key="15" value={15}>15分</option>
              <option key="20" value={20}>20分</option>
            </select> : null}
          </div> : null}
          <div className="column col-1">{this.renderEnabledSwitchView(setting)}</div>
          <div className="column col-2">{this.renderIconChangerView(setting)}</div>
          <div className="column">{this.renderSoundChangerView(setting)}</div>
          <div className="column col-1">{this.renderTesterView(setting)}</div>
        </div>
      </div>
    );
  }

  // MARK: - Enabled設定用
  renderEnabledSwitchView(setting: Setting) {
    if (this.props.kind == "default") return null; // defaultはいじれんようにしよ
    return (
      <div className="form-group">
        <label className="form-switch">
          <input type="checkbox"
            defaultChecked={setting.enabled}
            onChange={(ev) => this.setState({ setting: setting.update({ enabled: ev.target.checked }) })}
          /><i className="form-icon"></i>
        </label>
      </div>
    );
  }

  // MARK: - Icon設定用メソッド
  renderIconChangerView(setting: Setting) {
    if (setting.icon) return (
      <div className="icon-preview"
        style={{ backgroundImage: `url(${setting.icon})` }}
        onClick={() => window.confirm(`${this.props.label}のアイコンを削除?`) ? this.onIconFileDelete() : null}
      />
    );
    const ref = createRef<HTMLInputElement>();
    return (
      <div>
        <button className={cn("btn", { disabled: !setting.enabled })} onClick={() => ref.current.click()}>アイコン</button>
        <input type="file" hidden accept="image/*" ref={ref} onChange={ev => this.onIconFileChange(ev)} />
      </div>
    );
  }
  async onIconFileChange(ev: React.ChangeEvent<HTMLInputElement>) {
    if (ev.target.files.length == 0) return; // TODO: 既存のファイルを消す
    const { setting } = this.state;
    const file = ev.target.files[0];
    const fs = new FileService();
    await fs.init();
    const icon = await fs.save(setting.getFileSystemIconPath(), file);
    this.setState({ setting: this.state.setting.update({ icon }) });
  }
  async onIconFileDelete() {
    const { setting } = this.state;
    const fs = new FileService();
    await fs.init();
    await fs.delete(setting.getFileSystemIconPath(), false);
    this.setState({ setting: this.state.setting.update({ icon: undefined }) });
  }

  // MARK: - Sound設定用メソッド
  renderSoundChangerView(setting: Setting) {
    if (setting.sound) return (
      <div className="sound-preview">
        <audio src={setting.sound} controls={true}
          onVolumeChange={ev => {
            this.setState({ setting: this.state.setting.update({ volume: ev.currentTarget.volume }) });
          }}
        />
        <i className="icon icon-cross"
          onClick={() => window.confirm(`${this.props.label}の通知音を削除?`) ? this.onSoundFileDelete() : null}
        />
      </div>
    );
    const ref = createRef<HTMLInputElement>();
    return (
      <div>
        <button className="btn" onClick={() => ref.current.click()}>通知音</button>
        <input type="file" hidden accept="audio/*" ref={ref} onChange={ev => this.onSoundFileChange(ev)} />
      </div>
    );
  }
  async onSoundFileChange(ev: React.ChangeEvent<HTMLInputElement>) {
    if (ev.target.files.length == 0) return; // TODO: 既存のファイルを消す
    const file = ev.target.files[0];
    const fs = new FileService();
    await fs.init();
    const sound = await fs.save(this.state.setting.getFileSystemSoundPath(), file);
    this.setState({ setting: this.state.setting.update({ sound }) });
  }
  async onSoundFileDelete() {
    const { setting } = this.state;
    const fs = new FileService();
    await fs.init();
    await fs.delete(setting.getFileSystemSoundPath(), false);
    this.setState({ setting: this.state.setting.update({ sound: undefined }) });
  }

  // MARK: - テスト通知用
  renderTesterView(setting: Setting) {
    return <button
      className="btn btn-link"
      onClick={() => {
        const ns = new NotificationService();
        const opt: chrome.notifications.NotificationOptions<true> = {
          iconUrl: setting.getIcon(),
          message: `${this.props.label}通知のテスト`,
          type: "basic",
          title: "テスト",
        };
        const sound = setting.getSound();
        if (sound) new SoundService(sound.url, sound.volume).play();
        ns.create(`test_${this.props.kind}_${Date.now()}`, opt);
      }}
    >TEST</button>;
  }
}

// FIXME: ただの通知設定だと思ってると将来的に痛い目にあうかも...
class QuestAlertSettingView extends React.Component<{}, { setting: QuestAlertSetting }> {
  constructor(props) {
    super(props);
    this.state = { setting: QuestAlertSetting.user() };
  }
  render() {
    const { setting } = this.state;
    return (
      <div className={"container notification-kind-setting QuestAlert"}>
        <div className="columns">
          <div className="column col-3 kind-label"><div>未着手任務</div></div>
          <div className="column col-1">
            <div className="form-group">
              <label className="form-switch">
                <input type="checkbox"
                  defaultChecked={setting.enabled}
                  onChange={(ev) => this.setState({ setting: setting.update({ enabled: ev.target.checked }) })}
                /><i className="form-icon"></i>
              </label>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default class NotificationSettings extends React.Component {
  render() {
    return (
      <section className="category notification-setting">
        <h1>通知設定</h1>
        <blockquote className="description text-gray">
          通知音を設定した状態でオフにすると、通知音のみが流れることになります. // TODO: なんかもっと気の利いた説明をする
        </blockquote>
        <NotificationSettingView label="デフォルト" kind="default" />
        <NotificationSettingView label="遠征" kind={Kind.Mission} />
        <NotificationSettingView label="修復" kind={Kind.Recovery} />
        <NotificationSettingView label="建造" kind={Kind.Shipbuilding} />
        <NotificationSettingView label="疲労回復" kind={Kind.Tiredness} />
        <QuestAlertSettingView />
        <DisableMissionNotificationsSettingView />
      </section>
    );
  }
}