<commit_before>import DEBUG_ON from '../debug_flg';
import lang from '../lang';
import ContentScripts from '../content/content_scripts';
import TrailCanvas from '../content/trail_canvas';
import LibGesture from '../content/lib_gesture';
import LibOption from '../lib_option';

const gestureForOption = new LibGesture();
const option = new LibOption();
const canvasForOption = new TrailCanvas('gestureOptionCanvas', '10002');

(async () => {
  await option.load();
  canvasForOption.setCanvasSize(500, 500);

  /**
   * entry point (jQuery.ready)
   */
  $(() => {
    initializeAndRegisterEventForTab();

    // 言語設定の変更
    reflectSelectedLanguageToScreen();
    reflectOptionSettingsOnScreen();

    setCanvasStyle(canvasForOption);

    // オプションデータの表示
    // type: テキスト
    option.OPTION_ID_LIST.forEach((idName) => {
      $('#' + idName).on('change', (event) => {
        option.setParam(idName, $(event.target).val());
        saveOptions();
      });
    });

    // type: チェックボックス
    const checkIds = ['command_text_on', 'action_text_on', 'trail_on'];
    checkIds.forEach((idName) => {
      $('#' + idName).on('change', (event) => {
        option.setParam(idName, $(event.target).prop('checked'));
        saveOptions();
      });
    });

    // type: ラジオボタン
    const radioIds = ['language'];
    radioIds.forEach((idName) => {
      $('[name=' + idName + ']').on('change', (event) => {
        option.setParam(idName, $(event.currentTarget).attr('value'));
        saveOptions();
        reflectSelectedLanguageToScreen();
      });
    });

    registerEventForGesture();
    registerEventForAllReset();

    setupColorWheel();
  });
})();

/**
 * コンテキストメニューの呼び出しをされたときに実行されるイベント。
 * falseを返すと、コンテキストメニューを無効にする。
 */
document.addEventListener('contextmenu', (event) => {
  event.preventDefault();
});

/**
 * create canvas & update style
 *
 * @param {Object} canvas
 */
const setCanvasStyle = (canvas) => {
  canvas.setLineStyle('#000000', 1);

  const ctx = canvas.getCanvas().getContext('2d');
  ctx.font = 'bold 30px \'Arial\'';
  ctx.textBaseline = 'top';
  // ctx.fillStyle = '#FF0000';
};

/**
 * オプションの設定内容を画面に反映する
 */
const reflectOptionSettingsOnScreen = () => {
  const checkValues = {
    'command_text_on': option.isCommandTextOn(),
    'action_text_on': option.isActionTextOn(),
    'trail_on': option.isTrailOn(),
  };
  const radioValues = {
    'language': option.getLanguage(),
  };
  const textValues = {
    'color_code': option.getColorCode(),
    'line_width': option.getLineWidth(),
  };

  // 各DOMに設定値を適用
  Object.keys(textValues).forEach((key) => {
    const $inputTextElement = $('#' + key);
    const setText = textValues[key];

    $inputTextElement.val(setText);
  });

  Object.keys(checkValues).forEach((key) => {
    $('#' + key).prop('checked', checkValues[key]);
  });

  Object.keys(radioValues).forEach((key) => {
    const value = radioValues[key];
    $('[name=' + key + '][value=' + value + ']').prop('checked', true);
  });

  // ジェスチャー
  option.GESTURE_ID_LIST.forEach((key) => {
    const $inputTextElement = $('#' + key);
    const setText = option.getParam(key, '');

    $inputTextElement.val(setText);

    if (!$inputTextElement.hasClass('input_gesture')) {
      console.error('ジェスチャ設定に必須なDOM要素が見つかりません, {' + key + '}');
      return;
    }

    const setGestureText = setText ? ContentScripts.replaceCommandToArrow(setText) : ' ';
    const $viewsGestureElement = $inputTextElement.siblings('.views_gesture');

    $viewsGestureElement.html(setGestureText);
    $inputTextElement.hide();
  });
};

/**
 * タブに関する初期化と設定を行う
 */
const initializeAndRegisterEventForTab = () => {
  $('.changeTab').on('click', (event) => {
    const $clickedTab = $(event.currentTarget);
    reflectActiveTabToScreen($clickedTab);
  });

  const $defaultActiveTab = $('#tab_btn2.changeTab');
  reflectActiveTabToScreen($defaultActiveTab);
};

/**
 * アクティブなタブを画面に反映する
 */
let currentlySetColorClass = '';
const reflectActiveTabToScreen = ($target) => {
  const showBodyId = $target.data('show-body');
  const setColorClass = $target.data('set-color');

  $('.tabBody').hide();
  $('#' + showBodyId).show();

  const activeTabClass = 'is-active';
  $('.tabs li').removeClass(activeTabClass);
  $target.closest('li').addClass(activeTabClass);

  const $switchDom = $('.js-switch-color');
  const $allChangeableTab = $('.changeTab');
  if (currentlySetColorClass) {
    $switchDom.removeClass(currentlySetColorClass);
    $allChangeableTab.removeClass(currentlySetColorClass);
  }
  $switchDom.addClass(setColorClass);
  $target.addClass(setColorClass);
  currentlySetColorClass = setColorClass;
};

/**
 * 使用言語の設定を画面に反映する
 */
const reflectSelectedLanguageToScreen = () => {
  const $langEn = $('.class_English');
  const $langJa = $('.class_Japanese');
  const currentLanguage = option.getLanguage();

  switch (currentLanguage) {
    default:
      // no break;
    case 'English':
      $langEn.show();
      $langJa.hide();
      break;
    case 'Japanese':
      $langEn.hide();
      $langJa.show();
      break;
  }

  $('input.input_gesture').each((index, target) => {
    const id = target.id;
    const $target = $(target);
    const text = lang.gesture[id][currentLanguage];

    $target.parent().siblings('th').text(text);
  });
};

/**
 * @param {jQuery} $input
 */
const createGestureInputComponent = ($input) => {
  const drawCanvas = canvasForOption.getCanvas();
  const ctx = canvasForOption.getContext2d();

  if (!drawCanvas || !ctx) {
    return;
  }

  document.body.appendChild(drawCanvas);
  $input.data('prevValue', $input.val());
  gestureForOption.clear();
  canvasForOption.clearCanvas();
  ctx.globalAlpha = 0.5;
  ctx.fillRect(0, 0, drawCanvas.width, drawCanvas.height);
  ctx.globalAlpha = 1.0;

  [
    option.isJapanese() ? 'この枠内にジェスチャを' : 'Please draw a gesture',
    option.isJapanese() ? '　　描いてください　　' : '   with this frame   ',
  ].forEach((text, index) => {
    canvasForOption.drawText(text, 80, 180 + (index * 80), '#ECECEC');
  });

  const $canvas = $('#' + canvasForOption.getCanvasId());
  $canvas.off();
  $canvas.on('mousedown', (event) => {
    const tmpX = event.pageX - $canvas.offset().left;
    const tmpY = event.pageY - $canvas.offset().top;
    gestureForOption.startGesture(tmpX, tmpY, null);
    return false;
  }).on('mousemove', (event) => {
    const tmpX = event.pageX - $canvas.offset().left;
    const tmpY = event.pageY - $canvas.offset().top;
    if (gestureForOption.registerPoint(tmpX, tmpY)) {
      canvasForOption.drawLine(
          gestureForOption.getLastX(), gestureForOption.getLastY(),
          gestureForOption.getX(), gestureForOption.getY(),
      );
    }
    return false;
  }).on('mouseup', () => {
    const removeCanvas = document.getElementById(drawCanvas.id);
    if (removeCanvas) {
      document.body.removeChild(removeCanvas);
    }

    $input.val(gestureForOption.getGestureString()).triggerHandler('change');
    return false;
  });
};

const registerEventForGesture = () => {
  // ジェスチャ割り当てクリアボタン
  $('.reset_gesture').on('click', (event) => {
    const name = $(event.currentTarget).data('target');
    $('#' + name).val('').triggerHandler('change');
  });

  // ジェスチャ設定
  $('.views_gesture').on('click', (event) => {
    const $viewsGestureElement = $(event.currentTarget);
    $viewsGestureElement.siblings('.input_gesture').show().focus().trigger('click');
  });

  $('.input_gesture').on('blur', (event) => {
    const $input = $(event.currentTarget);
    const drawCanvasId = canvasForOption.getCanvasId();
    const removeCanvas = document.getElementById(drawCanvasId);
    if (removeCanvas) {
      document.body.removeChild(removeCanvas);
      const $canvas = $('#' + drawCanvasId);
      $canvas.off();
    }

    $input.hide();
  }).on('change', (event) => {
    const $input = $(event.target);
    const $viewsGestureElement = $input.siblings('.views_gesture');
    const inputGesture = $input.val();
    const targetActionName = $input.attr('id');

    // Validation
    if (!inputGesture.match(/^[DLUR]*$/)) {
      $input.val($input.data('prevValue'));
      return;
    }

    const registeredAction = inputGesture ? option.isGestureAlreadyExist(inputGesture) : false;
    if (registeredAction !== false && registeredAction !== targetActionName) {
      const gestureText = lang.gesture[registeredAction][option.getLanguage()];
      if (window.confirm('すでに「' + gestureText + '」に設定されています。入れ替えますか？')) {
        const $registeredAction = $('#' + registeredAction);
        $registeredAction.val('').hide();
        $registeredAction.siblings('.views_gesture').html(' ');
        option.setParam(registeredAction, '');
      } else {
        $input.val($input.data('prevValue'));
        return;
      }
    }

    const setGestureText = inputGesture
        ? ContentScripts.replaceCommandToArrow(inputGesture)
        : ' ';

    $viewsGestureElement.html(setGestureText);
    $input.hide();

    option.setParam(targetActionName, inputGesture);
    saveOptions();
  }).on('click', (event) => {
    createGestureInputComponent($(event.target));
  });
};

const registerEventForAllReset = () => {
  $('#reset_all').on('click', () => {
    const confirmOk = window.confirm(lang.confirmOptionReset[option.getLanguage()]);
    if (confirmOk) {
      option.reset();
      chrome.extension.sendMessage({msg: 'reload_option'}, () => {
      });
      reflectOptionSettingsOnScreen();
      reflectSelectedLanguageToScreen();
    }
  });
};

const setupColorWheel = () => {
  const colorWheel = Raphael.colorwheel($('#input_example')[0], 100);
  colorWheel.input($('#color_code')[0]);
  colorWheel.color(option.getColorCode());
  colorWheel.onchange(() => {
    $('#color_code').triggerHandler('change');
  });
};

const saveOptions = () => {
  option.save();
  chrome.extension.sendMessage({msg: 'reload_option'}, () => {
  });
};
<commit_msg>fix eslint<commit_after>import DEBUG_ON from '../debug_flg';
import lang from '../lang';
import ContentScripts from '../content/content_scripts';
import TrailCanvas from '../content/trail_canvas';
import LibGesture from '../content/lib_gesture';
import LibOption from '../lib_option';

const gestureForOption = new LibGesture();
const option = new LibOption();
const canvasForOption = new TrailCanvas('gestureOptionCanvas', '10002');

(async () => {
  await option.load();
  canvasForOption.setCanvasSize(500, 500);

  /**
   * entry point (jQuery.ready)
   */
  $(() => {
    initializeAndRegisterEventForTab();

    // 言語設定の変更
    reflectSelectedLanguageToScreen();
    reflectOptionSettingsOnScreen();

    setCanvasStyle(canvasForOption);

    // オプションデータの表示
    // type: テキスト
    option.OPTION_ID_LIST.forEach((idName) => {
      $('#' + idName).on('change', (event) => {
        option.setParam(idName, $(event.target).val());
        saveOptions();
      });
    });

    // type: チェックボックス
    const checkIds = ['command_text_on', 'action_text_on', 'trail_on'];
    checkIds.forEach((idName) => {
      $('#' + idName).on('change', (event) => {
        option.setParam(idName, $(event.target).prop('checked'));
        saveOptions();
      });
    });

    // type: ラジオボタン
    const radioIds = ['language'];
    radioIds.forEach((idName) => {
      $('[name=' + idName + ']').on('change', (event) => {
        option.setParam(idName, $(event.currentTarget).attr('value'));
        saveOptions();
        reflectSelectedLanguageToScreen();
      });
    });

    registerEventForGesture();
    registerEventForAllReset();

    setupColorWheel();
  });
})();

/**
 * コンテキストメニューの呼び出しをされたときに実行されるイベント。
 * falseを返すと、コンテキストメニューを無効にする。
 */
document.addEventListener('contextmenu', (event) => {
  event.preventDefault();
});

/**
 * create canvas & update style
 *
 * @param {Object} canvas
 */
const setCanvasStyle = (canvas) => {
  canvas.setLineStyle('#000000', 1);

  const ctx = canvas.getCanvas().getContext('2d');
  ctx.font = 'bold 30px \'Arial\'';
  ctx.textBaseline = 'top';
  // ctx.fillStyle = '#FF0000';
};

/**
 * オプションの設定内容を画面に反映する
 */
const reflectOptionSettingsOnScreen = () => {
  const checkValues = {
    'command_text_on': option.isCommandTextOn(),
    'action_text_on': option.isActionTextOn(),
    'trail_on': option.isTrailOn(),
  };
  const radioValues = {
    'language': option.getLanguage(),
  };
  const textValues = {
    'color_code': option.getColorCode(),
    'line_width': option.getLineWidth(),
  };

  // 各DOMに設定値を適用
  Object.keys(textValues).forEach((key) => {
    const $inputTextElement = $('#' + key);
    const setText = textValues[key];

    $inputTextElement.val(setText);
  });

  Object.keys(checkValues).forEach((key) => {
    $('#' + key).prop('checked', checkValues[key]);
  });

  Object.keys(radioValues).forEach((key) => {
    const value = radioValues[key];
    $('[name=' + key + '][value=' + value + ']').prop('checked', true);
  });

  // ジェスチャー
  option.GESTURE_ID_LIST.forEach((key) => {
    const $inputTextElement = $('#' + key);
    const setText = option.getParam(key, '');

    if (!$inputTextElement.hasClass('input_gesture')) {
      console.error('ジェスチャ設定に必須なDOM要素が見つかりません, {' + key + '}');
      return;
    }

    setGestureInputComponent($inputTextElement, setText);
    $inputTextElement.hide();
  });
};

/**
 * タブに関する初期化と設定を行う
 */
const initializeAndRegisterEventForTab = () => {
  $('.changeTab').on('click', (event) => {
    const $clickedTab = $(event.currentTarget);
    reflectActiveTabToScreen($clickedTab);
  });

  const $defaultActiveTab = $('#tab_btn2.changeTab');
  reflectActiveTabToScreen($defaultActiveTab);
};

/**
 * アクティブなタブを画面に反映する
 */
let currentlySetColorClass = '';
const reflectActiveTabToScreen = ($target) => {
  const showBodyId = $target.data('show-body');
  const setColorClass = $target.data('set-color');

  $('.tabBody').hide();
  $('#' + showBodyId).show();

  const activeTabClass = 'is-active';
  $('.tabs li').removeClass(activeTabClass);
  $target.closest('li').addClass(activeTabClass);

  const $switchDom = $('.js-switch-color');
  const $allChangeableTab = $('.changeTab');
  if (currentlySetColorClass) {
    $switchDom.removeClass(currentlySetColorClass);
    $allChangeableTab.removeClass(currentlySetColorClass);
  }
  $switchDom.addClass(setColorClass);
  $target.addClass(setColorClass);
  currentlySetColorClass = setColorClass;
};

/**
 * 使用言語の設定を画面に反映する
 */
const reflectSelectedLanguageToScreen = () => {
  const $langEn = $('.class_English');
  const $langJa = $('.class_Japanese');
  const currentLanguage = option.getLanguage();

  switch (currentLanguage) {
    default:
      // no break;
    case 'English':
      $langEn.show();
      $langJa.hide();
      break;
    case 'Japanese':
      $langEn.hide();
      $langJa.show();
      break;
  }

  $('input.input_gesture').each((index, target) => {
    const id = target.id;
    const $target = $(target);
    const text = lang.gesture[id][currentLanguage];

    $target.parent().siblings('th').text(text);
  });
};

/**
 * @param {jQuery} $input
 */
const createGestureInputComponent = ($input) => {
  const drawCanvas = canvasForOption.getCanvas();
  const ctx = canvasForOption.getContext2d();
  if (!drawCanvas || !ctx) {
    return;
  }

  document.body.appendChild(drawCanvas);
  $input.data('prevValue', $input.val());
  gestureForOption.clear();
  canvasForOption.clearCanvas();
  ctx.globalAlpha = 0.5;
  ctx.fillRect(0, 0, drawCanvas.width, drawCanvas.height);
  ctx.globalAlpha = 1.0;

  [
    option.isJapanese() ? 'この枠内にジェスチャを' : 'Please draw a gesture',
    option.isJapanese() ? '　　描いてください　　' : '   with this frame   ',
  ].forEach((text, index) => {
    canvasForOption.drawText(text, 80, 180 + (index * 80), '#ECECEC');
  });

  const $canvas = $('#' + canvasForOption.getCanvasId());
  $canvas.off();
  $canvas.on('mousedown', (event) => {
    const tmpX = event.pageX - $canvas.offset().left;
    const tmpY = event.pageY - $canvas.offset().top;
    gestureForOption.startGesture(tmpX, tmpY, null);
    return false;
  }).on('mousemove', (event) => {
    const tmpX = event.pageX - $canvas.offset().left;
    const tmpY = event.pageY - $canvas.offset().top;
    if (gestureForOption.registerPoint(tmpX, tmpY)) {
      canvasForOption.drawLine(
          gestureForOption.getLastX(), gestureForOption.getLastY(),
          gestureForOption.getX(), gestureForOption.getY(),
      );
    }
    return false;
  }).on('mouseup', () => {
    const removeCanvas = document.getElementById(drawCanvas.id);
    if (removeCanvas) {
      document.body.removeChild(removeCanvas);
    }

    setGestureInputComponent($input, gestureForOption.getGestureString());
    $input.triggerHandler('change');
    return false;
  });
};

const setGestureInputComponent = ($input, gestureText) => {
  const setGestureText = gestureText
      ? ContentScripts.replaceCommandToArrow(gestureText)
      : ' ';

  $input.val(gestureText);
  $input.siblings('.views_gesture').html(setGestureText);
};

const registerEventForGesture = () => {
  $('.reset_gesture').on('click', (event) => {
    const name = $(event.currentTarget).data('target');
    $('#' + name).val('').triggerHandler('change');
  });

  $('.views_gesture').on('click', (event) => {
    $(event.currentTarget).siblings('.input_gesture').show().focus().trigger('click');
  });

  $('.input_gesture').on('blur', (event) => {
    const $input = $(event.currentTarget);
    const drawCanvasId = canvasForOption.getCanvasId();
    const removeCanvas = document.getElementById(drawCanvasId);
    if (removeCanvas) {
      document.body.removeChild(removeCanvas);
      $('#' + drawCanvasId).off();
    }
    $input.hide();
  }).on('change', (event) => {
    const $input = $(event.target);
    const inputGesture = $input.val();
    const targetActionName = $input.attr('id');

    if (!inputGesture.match(/^[DLUR]*$/)) {
      setGestureInputComponent($input, $input.data('prevValue'));
      return;
    }

    const registeredAction = inputGesture ? option.isGestureAlreadyExist(inputGesture) : false;
    if (registeredAction !== false && registeredAction !== targetActionName) {
      const gestureLabel = lang.gesture[registeredAction][option.getLanguage()];
      if ( ! window.confirm('すでに「' + gestureLabel + '」に設定されています。入れ替えますか？')) {
        setGestureInputComponent($input, $input.data('prevValue'));
        return;
      }

      setGestureInputComponent($('#' + registeredAction), '');
      option.setParam(registeredAction, '');
    }

    setGestureInputComponent($input, inputGesture);
    $input.hide();

    option.setParam(targetActionName, inputGesture);
    saveOptions();
  }).on('click', (event) => {
    createGestureInputComponent($(event.target));
  });
};

const registerEventForAllReset = () => {
  $('#reset_all').on('click', () => {
    const confirmOk = window.confirm(lang.confirmOptionReset[option.getLanguage()]);
    if (confirmOk) {
      option.reset();
      chrome.extension.sendMessage({msg: 'reload_option'}, () => {
      });
      reflectOptionSettingsOnScreen();
      reflectSelectedLanguageToScreen();
    }
  });
};

const setupColorWheel = () => {
  const colorWheel = Raphael.colorwheel($('#input_example')[0], 100);
  colorWheel.input($('#color_code')[0]);
  colorWheel.color(option.getColorCode());
  colorWheel.onchange(() => {
    $('#color_code').triggerHandler('change');
  });
};

const saveOptions = () => {
  option.save();
  chrome.extension.sendMessage({msg: 'reload_option'}, () => {
  });
};
<|endoftext|>


<commit_before>import React, {
  forwardRef,
  isValidElement,
  useCallback,
  useContext,
  useMemo,
  useState,
  useRef,
  useEffect,
} from 'react';
import styled, { ThemeContext } from 'styled-components';

import { controlBorderStyle, normalizeColor } from '../../utils';
import { defaultProps } from '../../default-props';

import { Box } from '../Box';
import { DropButton } from '../DropButton';
import { Keyboard } from '../Keyboard';
import { FormContext } from '../Form/FormContext';
import { TextInput } from '../TextInput';

import { SelectContainer } from './SelectContainer';
import { applyKey } from './utils';

const SelectTextInput = styled(TextInput)`
  cursor: ${props => (props.defaultCursor ? 'default' : 'pointer')};
`;

const StyledSelectDropButton = styled(DropButton)`
  ${props => !props.callerPlain && controlBorderStyle};
  ${props =>
    props.theme.select &&
    props.theme.select.control &&
    props.theme.select.control.extend};
  ${props => props.open && props.theme.select.control.open};
`;

StyledSelectDropButton.defaultProps = {};
Object.setPrototypeOf(StyledSelectDropButton.defaultProps, defaultProps);

const defaultDropAlign = { top: 'bottom', left: 'left' };
const defaultMessages = { multiple: 'multiple' };

const Select = forwardRef(
  (
    {
      a11yTitle,
      alignSelf,
      children,
      clear = false,
      closeOnChange = true,
      defaultValue,
      disabled,
      disabledKey,
      dropAlign = defaultDropAlign,
      dropHeight,
      dropProps,
      dropTarget,
      emptySearchMessage,
      focusIndicator,
      gridArea,
      id,
      icon,
      labelKey,
      margin,
      messages = defaultMessages,
      multiple,
      name,
      onChange,
      onClick,
      onClose,
      onKeyDown,
      onMore,
      onOpen,
      onSearch,
      open: propOpen,
      options: optionsProp,
      placeholder,
      plain,
      replace,
      searchPlaceholder,
      selected,
      size,
      value: valueProp,
      valueKey,
      valueLabel,
      ...rest
    },
    ref,
  ) => {
    const theme = useContext(ThemeContext) || defaultProps.theme;
    const inputRef = useRef();
    const formContext = useContext(FormContext);
    // value is used for what we receive in valueProp and the basis for
    // what we send with onChange
    const [value, setValue] = formContext.useFormInput(
      name,
      valueProp,
      defaultValue || '',
    );
    // valuedValue is the value mapped with any valueKey applied
    const valuedValue = useMemo(() => {
      if (Array.isArray(value))
        return value.map(v =>
          valueKey && valueKey.reduce ? v : applyKey(v, valueKey),
        );
      return valueKey && valueKey.reduce ? value : applyKey(value, valueKey);
    }, [value, valueKey]);
    // search input value
    const [search, setSearch] = useState();
    // All select option indices and values
    const [allOptions, setAllOptions] = useState(optionsProp);
    // Track changes to options property, except when options are being
    // updated due to search activity. Allows option's initial index value
    // to be referenced when filtered by search.
    useEffect(() => {
      if (!search) setAllOptions(optionsProp);
    }, [optionsProp, search]);

    // the option indexes present in the value
    const optionIndexesInValue = useMemo(() => {
      const result = [];
      allOptions.forEach((option, index) => {
        if (selected !== undefined) {
          if (Array.isArray(selected)) {
            if (selected.indexOf(index) !== -1) result.push(index);
          } else if (index === selected) {
            result.push(index);
          }
        } else if (Array.isArray(valuedValue)) {
          if (valuedValue.some(v => v === applyKey(option, valueKey))) {
            result.push(index);
          }
        } else if (valuedValue === applyKey(option, valueKey)) {
          result.push(index);
        }
      });
      return result;
    }, [allOptions, selected, valueKey, valuedValue]);

    const [open, setOpen] = useState(propOpen);
    useEffect(() => setOpen(propOpen), [propOpen]);

    const onRequestOpen = useCallback(() => {
      if (open) return;
      setOpen(true);
      if (onOpen) onOpen();
    }, [onOpen, open]);

    const onRequestClose = useCallback(() => {
      setOpen(false);
      if (onClose) onClose();
    }, [onClose]);

    const triggerChangeEvent = useCallback(nextValue => {
      // Calling set value function directly on input because React library
      // overrides setter `event.target.value =` and loses original event
      // target fidelity.
      // https://stackoverflow.com/a/46012210
      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype,
        'value',
      ).set;
      nativeInputValueSetter.call(inputRef.current, nextValue);
      const event = new Event('input', { bubbles: true });
      inputRef.current.dispatchEvent(event);
    }, []);

    const onSelectChange = useCallback(
      (event, { option, value: nextValue, selected: nextSelected }) => {
        if (closeOnChange) onRequestClose();
        // nextValue must not be of type object to set value directly on the
        // input. if it is an object, then the user has not provided necessary
        // props to reduce object option
        if (
          typeof nextValue !== 'object' &&
          nextValue !== event.target.value &&
          inputRef.current
        ) {
          // select registers changing option as a click event or keydown.
          // when in a form, we need to programatically trigger a change
          // event in order for the change event to be registered upstream
          // necessary for change validation in form
          triggerChangeEvent(nextValue);
        }
        setValue(nextValue);
        if (onChange) {
          event.persist();
          const adjustedEvent = event;
          adjustedEvent.target = inputRef.current;
          adjustedEvent.value = nextValue;
          adjustedEvent.option = option;
          adjustedEvent.selected = nextSelected;
          onChange(adjustedEvent);
        }
        setSearch();
      },
      [closeOnChange, onChange, onRequestClose, setValue, triggerChangeEvent],
    );

    let SelectIcon;
    switch (icon) {
      case false:
        break;
      case true:
      case undefined:
        SelectIcon =
          open && theme.select.icons.up
            ? theme.select.icons.up
            : theme.select.icons.down;
        break;
      default:
        SelectIcon = icon;
    }

    // element to show, trumps inputValue
    const selectValue = useMemo(() => {
      if (valueLabel) return valueLabel;
      if (React.isValidElement(value)) return value; // deprecated
      return undefined;
    }, [value, valueLabel]);

    // text to show
    const inputValue = useMemo(() => {
      if (!selectValue) {
        if (optionIndexesInValue.length === 0) return '';
        if (optionIndexesInValue.length === 1)
          return applyKey(allOptions[optionIndexesInValue[0]], labelKey);
        return messages.multiple;
      }
      return undefined;
    }, [labelKey, messages, optionIndexesInValue, allOptions, selectValue]);

    const iconColor = normalizeColor(
      theme.select.icons.color || 'control',
      theme,
    );

    return (
      <Keyboard onDown={onRequestOpen} onUp={onRequestOpen}>
        <StyledSelectDropButton
          ref={ref}
          id={id}
          disabled={disabled === true || undefined}
          dropAlign={dropAlign}
          dropTarget={dropTarget}
          open={open}
          alignSelf={alignSelf}
          focusIndicator={focusIndicator}
          gridArea={gridArea}
          margin={margin}
          onOpen={onRequestOpen}
          onClose={onRequestClose}
          onClick={onClick}
          dropContent={
            <SelectContainer
              clear={clear}
              disabled={disabled}
              disabledKey={disabledKey}
              dropHeight={dropHeight}
              emptySearchMessage={emptySearchMessage}
              id={id}
              labelKey={labelKey}
              multiple={multiple}
              name={name}
              onChange={onSelectChange}
              onKeyDown={onKeyDown}
              onMore={onMore}
              onSearch={onSearch}
              options={optionsProp}
              allOptions={allOptions}
              optionIndexesInValue={optionIndexesInValue}
              replace={replace}
              searchPlaceholder={searchPlaceholder}
              search={search}
              setSearch={setSearch}
              selected={selected}
              value={value}
              valueKey={valueKey}
            >
              {children}
            </SelectContainer>
          }
          // StyledDropButton needs to know if the border should be shown
          callerPlain={plain}
          plain // Button should be plain
          dropProps={dropProps}
          theme={theme}
        >
          <Box
            align="center"
            direction="row"
            justify="between"
            background={theme.select.background}
          >
            <Box direction="row" flex basis="auto">
              {selectValue || (
                <SelectTextInput
                  a11yTitle={
                    a11yTitle &&
                    `${a11yTitle}${
                      value && typeof value === 'string' ? `, ${value}` : ''
                    }`
                  }
                  // When Select is disabled, we want to show a default cursor
                  // but not have disabled styling come from TextInput
                  // Disabled can be a bool or an array of options to disable.
                  // We only want to disable the TextInput if the control
                  // button should be disabled which occurs when disabled
                  // equals true.
                  defaultCursor={disabled === true || undefined}
                  focusIndicator={false}
                  id={id ? `${id}__input` : undefined}
                  name={name}
                  ref={inputRef}
                  {...rest}
                  tabIndex="-1"
                  type="text"
                  placeholder={placeholder}
                  plain
                  readOnly
                  value={inputValue}
                  size={size}
                  theme={theme}
                />
              )}
            </Box>
            {SelectIcon && (
              <Box
                margin={theme.select.icons.margin}
                flex={false}
                style={{ minWidth: 'auto' }}
              >
                {isValidElement(SelectIcon) ? (
                  SelectIcon
                ) : (
                  <SelectIcon color={iconColor} size={size} />
                )}
              </Box>
            )}
          </Box>
        </StyledSelectDropButton>
      </Keyboard>
    );
  },
);

Select.defaultProps = { ...defaultProps };

Select.displayName = 'Select';

let SelectDoc;
if (process.env.NODE_ENV !== 'production') {
  // eslint-disable-next-line global-require
  SelectDoc = require('./doc').doc(Select);
}
const SelectWrapper = SelectDoc || Select;

export { SelectWrapper as Select };
<commit_msg>Fix for native event in Select component (Preact support) (#5098)<commit_after>import React, {
  forwardRef,
  isValidElement,
  useCallback,
  useContext,
  useMemo,
  useState,
  useRef,
  useEffect,
} from 'react';
import styled, { ThemeContext } from 'styled-components';

import { controlBorderStyle, normalizeColor } from '../../utils';
import { defaultProps } from '../../default-props';

import { Box } from '../Box';
import { DropButton } from '../DropButton';
import { Keyboard } from '../Keyboard';
import { FormContext } from '../Form/FormContext';
import { TextInput } from '../TextInput';

import { SelectContainer } from './SelectContainer';
import { applyKey } from './utils';

const SelectTextInput = styled(TextInput)`
  cursor: ${props => (props.defaultCursor ? 'default' : 'pointer')};
`;

const StyledSelectDropButton = styled(DropButton)`
  ${props => !props.callerPlain && controlBorderStyle};
  ${props =>
    props.theme.select &&
    props.theme.select.control &&
    props.theme.select.control.extend};
  ${props => props.open && props.theme.select.control.open};
`;

StyledSelectDropButton.defaultProps = {};
Object.setPrototypeOf(StyledSelectDropButton.defaultProps, defaultProps);

const defaultDropAlign = { top: 'bottom', left: 'left' };
const defaultMessages = { multiple: 'multiple' };

const Select = forwardRef(
  (
    {
      a11yTitle,
      alignSelf,
      children,
      clear = false,
      closeOnChange = true,
      defaultValue,
      disabled,
      disabledKey,
      dropAlign = defaultDropAlign,
      dropHeight,
      dropProps,
      dropTarget,
      emptySearchMessage,
      focusIndicator,
      gridArea,
      id,
      icon,
      labelKey,
      margin,
      messages = defaultMessages,
      multiple,
      name,
      onChange,
      onClick,
      onClose,
      onKeyDown,
      onMore,
      onOpen,
      onSearch,
      open: propOpen,
      options: optionsProp,
      placeholder,
      plain,
      replace,
      searchPlaceholder,
      selected,
      size,
      value: valueProp,
      valueKey,
      valueLabel,
      ...rest
    },
    ref,
  ) => {
    const theme = useContext(ThemeContext) || defaultProps.theme;
    const inputRef = useRef();
    const formContext = useContext(FormContext);
    // value is used for what we receive in valueProp and the basis for
    // what we send with onChange
    const [value, setValue] = formContext.useFormInput(
      name,
      valueProp,
      defaultValue || '',
    );
    // valuedValue is the value mapped with any valueKey applied
    const valuedValue = useMemo(() => {
      if (Array.isArray(value))
        return value.map(v =>
          valueKey && valueKey.reduce ? v : applyKey(v, valueKey),
        );
      return valueKey && valueKey.reduce ? value : applyKey(value, valueKey);
    }, [value, valueKey]);
    // search input value
    const [search, setSearch] = useState();
    // All select option indices and values
    const [allOptions, setAllOptions] = useState(optionsProp);
    // Track changes to options property, except when options are being
    // updated due to search activity. Allows option's initial index value
    // to be referenced when filtered by search.
    useEffect(() => {
      if (!search) setAllOptions(optionsProp);
    }, [optionsProp, search]);

    // the option indexes present in the value
    const optionIndexesInValue = useMemo(() => {
      const result = [];
      allOptions.forEach((option, index) => {
        if (selected !== undefined) {
          if (Array.isArray(selected)) {
            if (selected.indexOf(index) !== -1) result.push(index);
          } else if (index === selected) {
            result.push(index);
          }
        } else if (Array.isArray(valuedValue)) {
          if (valuedValue.some(v => v === applyKey(option, valueKey))) {
            result.push(index);
          }
        } else if (valuedValue === applyKey(option, valueKey)) {
          result.push(index);
        }
      });
      return result;
    }, [allOptions, selected, valueKey, valuedValue]);

    const [open, setOpen] = useState(propOpen);
    useEffect(() => setOpen(propOpen), [propOpen]);

    const onRequestOpen = useCallback(() => {
      if (open) return;
      setOpen(true);
      if (onOpen) onOpen();
    }, [onOpen, open]);

    const onRequestClose = useCallback(() => {
      setOpen(false);
      if (onClose) onClose();
    }, [onClose]);

    const triggerChangeEvent = useCallback(nextValue => {
      // Calling set value function directly on input because React library
      // overrides setter `event.target.value =` and loses original event
      // target fidelity.
      // https://stackoverflow.com/a/46012210
      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype,
        'value',
      ).set;
      nativeInputValueSetter.call(inputRef.current, nextValue);
      const event = new Event('input', { bubbles: true });
      inputRef.current.dispatchEvent(event);
    }, []);

    const onSelectChange = useCallback(
      (event, { option, value: nextValue, selected: nextSelected }) => {
        if (closeOnChange) onRequestClose();
        // nextValue must not be of type object to set value directly on the
        // input. if it is an object, then the user has not provided necessary
        // props to reduce object option
        if (
          typeof nextValue !== 'object' &&
          nextValue !== event.target.value &&
          inputRef.current
        ) {
          // select registers changing option as a click event or keydown.
          // when in a form, we need to programatically trigger a change
          // event in order for the change event to be registered upstream
          // necessary for change validation in form
          triggerChangeEvent(nextValue);
        }
        setValue(nextValue);
        if (onChange) {
          event.persist();
          let adjustedEvent;
          // support for native event used by Preact
          if (event instanceof Event) {
            adjustedEvent = new event.constructor(event.type, event);
            Object.defineProperties(adjustedEvent, {
              target: { value: inputRef.current },
              value: { value: nextValue },
              option: { value: option },
              selected: { value: nextSelected },
            });
          } else {
            adjustedEvent = event;
            adjustedEvent.target = inputRef.current;
            adjustedEvent.value = nextValue;
            adjustedEvent.option = option;
            adjustedEvent.selected = nextSelected;
          }
          onChange(adjustedEvent);
        }
        setSearch();
      },
      [closeOnChange, onChange, onRequestClose, setValue, triggerChangeEvent],
    );

    let SelectIcon;
    switch (icon) {
      case false:
        break;
      case true:
      case undefined:
        SelectIcon =
          open && theme.select.icons.up
            ? theme.select.icons.up
            : theme.select.icons.down;
        break;
      default:
        SelectIcon = icon;
    }

    // element to show, trumps inputValue
    const selectValue = useMemo(() => {
      if (valueLabel) return valueLabel;
      if (React.isValidElement(value)) return value; // deprecated
      return undefined;
    }, [value, valueLabel]);

    // text to show
    const inputValue = useMemo(() => {
      if (!selectValue) {
        if (optionIndexesInValue.length === 0) return '';
        if (optionIndexesInValue.length === 1)
          return applyKey(allOptions[optionIndexesInValue[0]], labelKey);
        return messages.multiple;
      }
      return undefined;
    }, [labelKey, messages, optionIndexesInValue, allOptions, selectValue]);

    const iconColor = normalizeColor(
      theme.select.icons.color || 'control',
      theme,
    );

    return (
      <Keyboard onDown={onRequestOpen} onUp={onRequestOpen}>
        <StyledSelectDropButton
          ref={ref}
          id={id}
          disabled={disabled === true || undefined}
          dropAlign={dropAlign}
          dropTarget={dropTarget}
          open={open}
          alignSelf={alignSelf}
          focusIndicator={focusIndicator}
          gridArea={gridArea}
          margin={margin}
          onOpen={onRequestOpen}
          onClose={onRequestClose}
          onClick={onClick}
          dropContent={
            <SelectContainer
              clear={clear}
              disabled={disabled}
              disabledKey={disabledKey}
              dropHeight={dropHeight}
              emptySearchMessage={emptySearchMessage}
              id={id}
              labelKey={labelKey}
              multiple={multiple}
              name={name}
              onChange={onSelectChange}
              onKeyDown={onKeyDown}
              onMore={onMore}
              onSearch={onSearch}
              options={optionsProp}
              allOptions={allOptions}
              optionIndexesInValue={optionIndexesInValue}
              replace={replace}
              searchPlaceholder={searchPlaceholder}
              search={search}
              setSearch={setSearch}
              selected={selected}
              value={value}
              valueKey={valueKey}
            >
              {children}
            </SelectContainer>
          }
          // StyledDropButton needs to know if the border should be shown
          callerPlain={plain}
          plain // Button should be plain
          dropProps={dropProps}
          theme={theme}
        >
          <Box
            align="center"
            direction="row"
            justify="between"
            background={theme.select.background}
          >
            <Box direction="row" flex basis="auto">
              {selectValue || (
                <SelectTextInput
                  a11yTitle={
                    a11yTitle &&
                    `${a11yTitle}${
                      value && typeof value === 'string' ? `, ${value}` : ''
                    }`
                  }
                  // When Select is disabled, we want to show a default cursor
                  // but not have disabled styling come from TextInput
                  // Disabled can be a bool or an array of options to disable.
                  // We only want to disable the TextInput if the control
                  // button should be disabled which occurs when disabled
                  // equals true.
                  defaultCursor={disabled === true || undefined}
                  focusIndicator={false}
                  id={id ? `${id}__input` : undefined}
                  name={name}
                  ref={inputRef}
                  {...rest}
                  tabIndex="-1"
                  type="text"
                  placeholder={placeholder}
                  plain
                  readOnly
                  value={inputValue}
                  size={size}
                  theme={theme}
                />
              )}
            </Box>
            {SelectIcon && (
              <Box
                margin={theme.select.icons.margin}
                flex={false}
                style={{ minWidth: 'auto' }}
              >
                {isValidElement(SelectIcon) ? (
                  SelectIcon
                ) : (
                  <SelectIcon color={iconColor} size={size} />
                )}
              </Box>
            )}
          </Box>
        </StyledSelectDropButton>
      </Keyboard>
    );
  },
);

Select.defaultProps = { ...defaultProps };

Select.displayName = 'Select';

let SelectDoc;
if (process.env.NODE_ENV !== 'production') {
  // eslint-disable-next-line global-require
  SelectDoc = require('./doc').doc(Select);
}
const SelectWrapper = SelectDoc || Select;

export { SelectWrapper as Select };
<|endoftext|>


<commit_before>import mixin from 'reactjs-mixin';
import React from 'react';

import InternalStorageMixin from '../mixins/InternalStorageMixin';
import Service from '../structs/Service';
import ServiceActionItem from '../constants/ServiceActionItem';
import ServiceDetailConfigurationTab from './ServiceDetailConfigurationTab';
import ServiceDetailTaskTab from './ServiceDetailTaskTab';
import ServiceFormModal from './modals/ServiceFormModal';
import ServiceInfo from './ServiceInfo';
import ServicesBreadcrumb from './ServicesBreadcrumb';
import TabsMixin from '../mixins/TabsMixin';

class ServiceDetail extends mixin(InternalStorageMixin, TabsMixin) {

  constructor() {
    super(...arguments);

    this.tabs_tabs = {
      tasks: 'Tasks',
      configuration: 'Configuration',
      debug: 'Debug',
      logs: 'Logs'
    };

    this.state = {
      currentTab: Object.keys(this.tabs_tabs).shift(),
      isServiceFormModalShown: false
    };

    this.onActionsItemSelection = this.onActionsItemSelection.bind(this);
    this.onCloseServiceFormModal = this.onCloseServiceFormModal.bind(this);
  }

  onActionsItemSelection(item) {
    if (item.id === ServiceActionItem.EDIT) {
      this.setState({isServiceFormModalShown: true});
    }
  }

  onCloseServiceFormModal() {
    this.setState({isServiceFormModalShown: false});
  }

  renderConfigurationTabView() {
    return (
      <ServiceDetailConfigurationTab service={this.props.service}/>
    );
  }

  renderDebugTabView() {
    return (Debug Placeholder);
  }

  renderLogsTabView() {
    return (Logs Placeholder);
  }

  renderTasksTabView() {
    return (
      <ServiceDetailTaskTab service={this.props.service}/>
    );
  }

  render() {
    const {service} = this.props;

    return (

          <ServicesBreadcrumb serviceTreeItem={service} />
          <ServiceInfo onActionsItemSelection={this.onActionsItemSelection}
            service={service} tabs={this.tabs_getUnroutedTabs()} />
          {this.tabs_getTabView()}

        <ServiceFormModal isEdit={true}
          open={this.state.isServiceFormModalShown}
          service={service}
          onClose={this.onCloseServiceFormModal} />

    );
  }
}

ServiceDetail.contextTypes = {
  router: React.PropTypes.func
};

ServiceDetail.propTypes = {
  service: React.PropTypes.instanceOf(Service)
};

module.exports = ServiceDetail;
<commit_msg>Use methos to bind automatism<commit_after>import mixin from 'reactjs-mixin';
import React from 'react';

import InternalStorageMixin from '../mixins/InternalStorageMixin';
import Service from '../structs/Service';
import ServiceActionItem from '../constants/ServiceActionItem';
import ServiceDetailConfigurationTab from './ServiceDetailConfigurationTab';
import ServiceDetailTaskTab from './ServiceDetailTaskTab';
import ServiceFormModal from './modals/ServiceFormModal';
import ServiceInfo from './ServiceInfo';
import ServicesBreadcrumb from './ServicesBreadcrumb';
import TabsMixin from '../mixins/TabsMixin';

const METHODS_TO_BIND = [
  'onActionsItemSelection',
  'onCloseServiceFormModal'
];

class ServiceDetail extends mixin(InternalStorageMixin, TabsMixin) {

  constructor() {
    super(...arguments);

    this.tabs_tabs = {
      tasks: 'Tasks',
      configuration: 'Configuration',
      debug: 'Debug',
      logs: 'Logs'
    };

    this.state = {
      currentTab: Object.keys(this.tabs_tabs).shift(),
      isServiceFormModalShown: false
    };

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    });
  }

  onActionsItemSelection(item) {
    if (item.id === ServiceActionItem.EDIT) {
      this.setState({isServiceFormModalShown: true});
    }
  }

  onCloseServiceFormModal() {
    this.setState({isServiceFormModalShown: false});
  }

  renderConfigurationTabView() {
    return (
      <ServiceDetailConfigurationTab service={this.props.service}/>
    );
  }

  renderDebugTabView() {
    return (Debug Placeholder);
  }

  renderLogsTabView() {
    return (Logs Placeholder);
  }

  renderTasksTabView() {
    return (
      <ServiceDetailTaskTab service={this.props.service}/>
    );
  }

  render() {
    const {service} = this.props;

    return (

          <ServicesBreadcrumb serviceTreeItem={service} />
          <ServiceInfo onActionsItemSelection={this.onActionsItemSelection}
            service={service} tabs={this.tabs_getUnroutedTabs()} />
          {this.tabs_getTabView()}

        <ServiceFormModal isEdit={true}
          open={this.state.isServiceFormModalShown}
          service={service}
          onClose={this.onCloseServiceFormModal} />

    );
  }
}

ServiceDetail.contextTypes = {
  router: React.PropTypes.func
};

ServiceDetail.propTypes = {
  service: React.PropTypes.instanceOf(Service)
};

module.exports = ServiceDetail;
<|endoftext|>


<commit_before>
import ReactGA from 'react-ga'

var coreActions = require('./actions.js');
var uiActions = require('../ui/actions.js');
var pusherActions = require('../pusher/actions.js');
var mopidyActions = require('../mopidy/actions.js');
var spotifyActions = require('../spotify/actions.js');
var lastfmActions = require('../lastfm/actions.js');
var helpers = require('../../helpers.js');

const CoreMiddleware = (function(){

    /**
     * The actual middleware inteceptor
     **/
    return store => next => action => {

        switch(action.type){

            case 'HANDLE_EXCEPTION':

                // Construct meaningful message and description
                var message = action.message;
                if (action.description){
                    var description = action.description;
                } else if (action.data.xhr && action.data.xhr.responseText){
                    var xhr_response = JSON.parse(action.data.xhr.responseText);
                    if (xhr_response.error && xhr_response.error.message){
                        var description = xhr_response.error.message;
                    }
                } else {
                    var description = null;
                }

                // Prepare a summary dump of our state
                var state = store.getState();
                var exported_state = {
                    core: Object.assign({},state.core),
                    ui: Object.assign({},state.ui),
                    spotify: Object.assign({},state.spotify),
                    mopidy: Object.assign({},state.mopidy),
                    pusher: Object.assign({},state.pusher)
                }

                // Strip out non-essential store info
                delete exported_state.core.albums
                delete exported_state.core.artists
                delete exported_state.core.playlists
                delete exported_state.core.users
                delete exported_state.core.queue_metadata
                delete exported_state.core.current_tracklist
                delete exported_state.spotify.library_albums
                delete exported_state.spotify.library_artists
                delete exported_state.spotify.library_playlists
                delete exported_state.spotify.autocomplete_results
                delete exported_state.mopidy.library_albums
                delete exported_state.mopidy.library_artists
                delete exported_state.mopidy.library_playlists

                var data = Object.assign(
                    {},
                    action.data,
                    {
                        message: message,
                        description: description,
                        state: exported_state
                    }
                );

                // Log with Raven Sentry
                Raven.captureException(
                    new Error(message),
                    {
                        extra: data
                    }
                );

                // Log with Analytics
                ReactGA.event({
                    category: "Error",
                    action: message,
                    label: description,
                    nonInteraction: true
                });

                store.dispatch(uiActions.createNotification(
                    message,
                    'bad',
                    null,
                    null,
                    description
                ));
                console.error(message, description, data);
                break;

            case 'CORE_START_SERVICES':
                store.dispatch(mopidyActions.connect());
                store.dispatch(pusherActions.connect());
                store.dispatch(lastfmActions.connect());

                next(action)
                break

            case 'TRACK_LOADED':
                if (action.data) ReactGA.event({ category: 'Track', action: 'Load', label: action.key });

                if (action.track.album && action.track.album.images && action.track.album.images.length > 0){
                    action.track.album.images = helpers.digestMopidyImages(store.getState().mopidy, action.track.album.images);
                }

                next(action)
                break

            case 'ALBUM_LOADED':
                if (action.data) ReactGA.event({ category: 'Album', action: 'Load', label: action.key })

                if (action.album.images && action.album.images.length > 0){
                    action.album.images = helpers.digestMopidyImages(store.getState().mopidy, action.album.images);
                }

                // Load our tracks
                if (action.album.tracks){
                    store.dispatch({
                        type: 'TRACKS_LOADED',
                        tracks: action.album.tracks
                    });
                }

                next(action)
                break

            case 'ALBUMS_LOADED':
                if (action.data) ReactGA.event({ category: 'Albums', action: 'Load', label: action.albums.length+' items' })

                for (var i = 0; i < action.albums.length; i++){
                    if (action.albums[i].images && action.albums[i].images.length > 0){
                        action.albums[i].images = helpers.digestMopidyImages(store.getState().mopidy, action.albums[i].images);
                    }

                    // Load our tracks
                    if (action.albums[i].tracks){
                        store.dispatch({
                            type: 'TRACKS_LOADED',
                            tracks: action.albums[i].tracks
                        });
                    }
                }

                next(action)
                break

            case 'ARTIST_LOADED':
                if (action.data) ReactGA.event({ category: 'Artist', action: 'Load', label: action.artist.uri });

                // Load our tracks
                if (action.artist.tracks){
                    store.dispatch({
                        type: 'TRACKS_LOADED',
                        tracks: action.artist.tracks
                    });
                }

                next(action)
                break

            case 'PLAY_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Play', label: action.uri })
                next(action)
                break

            case 'SAVE_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Save', label: action.key })
                next(action)
                break

            case 'CREATE_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Create', label: +action.name })
                next(action)
                break

            case 'REORDER_PLAYLIST_TRACKS':
                ReactGA.event({ category: 'Playlist', action: 'Reorder tracks', label: action.key })
                next(action)
                break

            case 'ADD_PLAYLIST_TRACKS':
                ReactGA.event({ category: 'Playlist', action: 'Add tracks', label: action.playlist_uri })
                next(action)
                break

            case 'REMOVE_PLAYLIST_TRACKS':
                ReactGA.event({ category: 'Playlist', action: 'Remove tracks', label: action.playlist_uri })
                next(action)
                break

            case 'DELETE_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Delete', label: action.uri })
                next(action)
                break

            case 'SEARCH_STARTED':
                ReactGA.event({ category: 'Search', action: 'Started', label: action.type+': '+action.query })
                next(action)

                var state = store.getState()
                if (state.ui.search_uri_schemes){
                    var uri_schemes = state.ui.search_uri_schemes
                } else {
                    var uri_schemes = state.mopidy.uri_schemes
                }

                // backends that can handle more than just track results
                // make sure they are available and respect our settings
                var available_full_uri_schemes = ['local:','file:','gmusic:']
                var full_uri_schemes = []
                for (var i = 0; i < available_full_uri_schemes.length; i++){
                    var index = uri_schemes.indexOf(available_full_uri_schemes[i])
                    if (index > -1){
                        full_uri_schemes.push(available_full_uri_schemes[i])
                    }
                }

                // initiate spotify searching
                if (!action.only_mopidy){
                    if (!state.ui.search_settings || state.ui.search_settings.spotify){
                        store.dispatch(spotifyActions.getSearchResults(action.query))
                    }
                }

                // backend searching (mopidy)
                if (state.mopidy.connected){
                    store.dispatch(mopidyActions.getSearchResults(action.search_type, action.query, 100, full_uri_schemes))
                }

                break

            case 'PLAYLIST_TRACKS_ADDED':
                store.dispatch(uiActions.createNotification('Added '+action.tracks_uris.length+' tracks to playlist'))
                switch(helpers.uriSource(action.key)){
                    case 'spotify':
                        store.dispatch(spotifyActions.getPlaylist(action.key))
                        break
                    case 'm3u':
                        if (store.getState().mopidy.connected ) store.dispatch(mopidyActions.getPlaylist(action.key))
                        break
                }
                next(action)
                break

            case 'PLAYLIST_LOADED':
                if (action.data) ReactGA.event({ category: 'Playlist', action: 'Load', label: action.playlist.uri })

                var playlist = Object.assign({}, action.playlist)
                switch (helpers.uriSource(playlist.uri)){

                    case 'm3u':
                        playlist.can_edit = true
                        break

                    case 'spotify':
                        if (store.getState().spotify.authorization && store.getState().spotify.me){
                            playlist.can_edit = (helpers.getFromUri('playlistowner',playlist.uri) == store.getState().spotify.me.id)
                        }
                }

                // Load our tracks
                if (action.playlist.tracks){
                    store.dispatch({
                        type: 'TRACKS_LOADED',
                        tracks: action.playlist.tracks
                    });
                }

                // proceed as usual
                action.playlist = playlist;
                next(action);
                break

            case 'PLAYLISTS_LOADED':
                if (action.data) ReactGA.event({ category: 'Playlists', action: 'Load', label: action.playlists.length+' items' })

                var playlists = []
                for (var i = 0; i < action.playlists.length; i++){
                    var playlist = Object.assign({}, action.playlists[i])

                    switch (helpers.uriSource(playlist.uri)){

                        case 'm3u':
                            playlist.can_edit = true
                            break

                        case 'spotify':
                            if (store.getState().spotify.authorization && store.getState().spotify.me){
                                playlist.can_edit = (helpers.getFromUri('playlistowner',playlist.uri) == store.getState().spotify.me.id)
                            }
                    }

                    playlists.push(playlist)
                }

                // proceed as usual
                action.playlists = playlists
                next(action)
                break

            case 'MOPIDY_CURRENTTLTRACK':
                if (action.data && action.data.track){
                    helpers.setWindowTitle(action.data.track, store.getState().mopidy.play_state);

                    // make sure our images use mopidy host:port
                    if (action.data.track.album && action.data.track.album.images && action.data.track.album.images.length > 0){
                        var images = Object.assign([], action.data.track.album.images)
                        for (var i = 0; i < images.length; i++){
                            if (typeof(images[i]) === 'string' && images[i].startsWith('/images/')){
                                images[i] = '//'+store.getState().mopidy.host+':'+store.getState().mopidy.port+images[i]
                            }
                        }
                        action.data.track.album.images = images
                    }
                }

                next(action)
                break

            case 'MOPIDY_TLTRACKS':

                var core = store.getState().core;
                var tracklist = []
                for (var i = 0; i < action.data.length; i++){

                    var tltrack = helpers.formatTracks(action.data[i]);

                    // load our metadata (if we have any for that tlid)
                    if (core.queue_metadata !== undefined && core.queue_metadata['tlid_'+tltrack.tlid] !== undefined){
                        var metadata = core.queue_metadata['tlid_'+tltrack.tlid]
                    } else {
                        var metadata = {}
                    }

                    var current_tlid = null;
                    if (core.current_track && core.tracks && core.tracks[core.current_track] !== undefined && core.tracks[core.current_track].tlid !== undefined){
                        current_tlid = core.tracks[core.current_track].tlid;
                    }

                    var track = Object.assign(
                        {},
                        tltrack,
                        metadata,
                        {
                            playing: (tltrack.tlid == current_tlid)
                        })
                    tracklist.push(track)
                }

                // Append to our action
                tracklist = helpers.formatTracks(tracklist);
                action.tracklist = tracklist;

                // Load our tracks into index
                store.dispatch({
                    type: 'TRACKS_LOADED',
                    tracks: tracklist
                });

                next(action);
                break

            // Get assets from all of our providers
            case 'GET_LIBRARY_PLAYLISTS':
                if (store.getState().spotify.connected){
                    store.dispatch(spotifyActions.getLibraryPlaylists())
                }
                if (store.getState().mopidy.connected){
                    store.dispatch(mopidyActions.getLibraryPlaylists())
                }
                next(action)
                break

            // Get assets from all of our providers
            case 'GET_LIBRARY_ALBUMS':
                if (store.getState().spotify.connected){
                    store.dispatch(spotifyActions.getLibraryAlbums())
                }
                if (store.getState().mopidy.connected){
                    store.dispatch(mopidyActions.getLibraryAlbums())
                }
                next(action)
                break

            // Get assets from all of our providers
            case 'GET_LIBRARY_ARTISTS':
                if (store.getState().spotify.connected){
                    store.dispatch(spotifyActions.getLibraryArtists())
                }
                if (store.getState().mopidy.connected){
                    store.dispatch(mopidyActions.getLibraryArtists())
                }
                next(action)
                break

            case 'RESTART':
                location.reload()
                break

            // This action is irrelevant to us, pass it on to the next middleware
            default:
                return next(action)
        }
    }

})();

export default CoreMiddleware<commit_msg>Tracks index<commit_after>
import ReactGA from 'react-ga'

var coreActions = require('./actions.js');
var uiActions = require('../ui/actions.js');
var pusherActions = require('../pusher/actions.js');
var mopidyActions = require('../mopidy/actions.js');
var spotifyActions = require('../spotify/actions.js');
var lastfmActions = require('../lastfm/actions.js');
var helpers = require('../../helpers.js');

const CoreMiddleware = (function(){

    /**
     * The actual middleware inteceptor
     **/
    return store => next => action => {

        switch(action.type){

            case 'HANDLE_EXCEPTION':

                // Construct meaningful message and description
                var message = action.message;
                if (action.description){
                    var description = action.description;
                } else if (action.data.xhr && action.data.xhr.responseText){
                    var xhr_response = JSON.parse(action.data.xhr.responseText);
                    if (xhr_response.error && xhr_response.error.message){
                        var description = xhr_response.error.message;
                    }
                } else {
                    var description = null;
                }

                // Prepare a summary dump of our state
                var state = store.getState();
                var exported_state = {
                    core: Object.assign({},state.core),
                    ui: Object.assign({},state.ui),
                    spotify: Object.assign({},state.spotify),
                    mopidy: Object.assign({},state.mopidy),
                    pusher: Object.assign({},state.pusher)
                }

                // Strip out non-essential store info
                delete exported_state.core.albums
                delete exported_state.core.artists
                delete exported_state.core.playlists
                delete exported_state.core.users
                delete exported_state.core.queue_metadata
                delete exported_state.core.current_tracklist
                delete exported_state.spotify.library_albums
                delete exported_state.spotify.library_artists
                delete exported_state.spotify.library_playlists
                delete exported_state.spotify.autocomplete_results
                delete exported_state.mopidy.library_albums
                delete exported_state.mopidy.library_artists
                delete exported_state.mopidy.library_playlists

                var data = Object.assign(
                    {},
                    action.data,
                    {
                        message: message,
                        description: description,
                        state: exported_state
                    }
                );

                // Log with Raven Sentry
                Raven.captureException(
                    new Error(message),
                    {
                        extra: data
                    }
                );

                // Log with Analytics
                ReactGA.event({
                    category: "Error",
                    action: message,
                    label: description,
                    nonInteraction: true
                });

                store.dispatch(uiActions.createNotification(
                    message,
                    'bad',
                    null,
                    null,
                    description
                ));
                console.error(message, description, data);
                break;

            case 'CORE_START_SERVICES':
                store.dispatch(mopidyActions.connect());
                store.dispatch(pusherActions.connect());
                store.dispatch(lastfmActions.connect());

                next(action)
                break

            case 'TRACK_LOADED':
                if (action.data) ReactGA.event({ category: 'Track', action: 'Load', label: action.key });

                if (action.track.album && action.track.album.images && action.track.album.images.length > 0){
                    action.track.album.images = helpers.digestMopidyImages(store.getState().mopidy, action.track.album.images);
                }

                next(action)
                break

            case 'ALBUM_LOADED':
                if (action.data) ReactGA.event({ category: 'Album', action: 'Load', label: action.key })

                if (action.album.images && action.album.images.length > 0){
                    action.album.images = helpers.digestMopidyImages(store.getState().mopidy, action.album.images);
                }

                // Load our tracks
                if (action.album.tracks){
                    store.dispatch({
                        type: 'TRACKS_LOADED',
                        tracks: action.album.tracks
                    });
                }

                next(action)
                break

            case 'ALBUMS_LOADED':
                if (action.data) ReactGA.event({ category: 'Albums', action: 'Load', label: action.albums.length+' items' })

                for (var i = 0; i < action.albums.length; i++){
                    if (action.albums[i].images && action.albums[i].images.length > 0){
                        action.albums[i].images = helpers.digestMopidyImages(store.getState().mopidy, action.albums[i].images);
                    }

                    // Load our tracks
                    if (action.albums[i].tracks){
                        store.dispatch({
                            type: 'TRACKS_LOADED',
                            tracks: action.albums[i].tracks
                        });
                    }
                }

                next(action)
                break

            case 'ARTIST_LOADED':
                if (action.data) ReactGA.event({ category: 'Artist', action: 'Load', label: action.artist.uri });

                // Load our tracks
                if (action.artist.tracks){
                    store.dispatch({
                        type: 'TRACKS_LOADED',
                        tracks: action.artist.tracks
                    });
                }

                next(action)
                break

            case 'PLAY_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Play', label: action.uri })
                next(action)
                break

            case 'SAVE_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Save', label: action.key })
                next(action)
                break

            case 'CREATE_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Create', label: +action.name })
                next(action)
                break

            case 'REORDER_PLAYLIST_TRACKS':
                ReactGA.event({ category: 'Playlist', action: 'Reorder tracks', label: action.key })
                next(action)
                break

            case 'ADD_PLAYLIST_TRACKS':
                ReactGA.event({ category: 'Playlist', action: 'Add tracks', label: action.playlist_uri })
                next(action)
                break

            case 'REMOVE_PLAYLIST_TRACKS':
                ReactGA.event({ category: 'Playlist', action: 'Remove tracks', label: action.playlist_uri })
                next(action)
                break

            case 'DELETE_PLAYLIST':
                ReactGA.event({ category: 'Playlist', action: 'Delete', label: action.uri })
                next(action)
                break

            case 'SEARCH_STARTED':
                ReactGA.event({ category: 'Search', action: 'Started', label: action.type+': '+action.query })
                next(action)

                var state = store.getState()
                if (state.ui.search_uri_schemes){
                    var uri_schemes = state.ui.search_uri_schemes
                } else {
                    var uri_schemes = state.mopidy.uri_schemes
                }

                // backends that can handle more than just track results
                // make sure they are available and respect our settings
                var available_full_uri_schemes = ['local:','file:','gmusic:']
                var full_uri_schemes = []
                for (var i = 0; i < available_full_uri_schemes.length; i++){
                    var index = uri_schemes.indexOf(available_full_uri_schemes[i])
                    if (index > -1){
                        full_uri_schemes.push(available_full_uri_schemes[i])
                    }
                }

                // initiate spotify searching
                if (!action.only_mopidy){
                    if (!state.ui.search_settings || state.ui.search_settings.spotify){
                        store.dispatch(spotifyActions.getSearchResults(action.query))
                    }
                }

                // backend searching (mopidy)
                if (state.mopidy.connected){
                    store.dispatch(mopidyActions.getSearchResults(action.search_type, action.query, 100, full_uri_schemes))
                }

                break

            case 'PLAYLIST_TRACKS_ADDED':
                store.dispatch(uiActions.createNotification('Added '+action.tracks_uris.length+' tracks to playlist'))
                switch(helpers.uriSource(action.key)){
                    case 'spotify':
                        store.dispatch(spotifyActions.getPlaylist(action.key))
                        break
                    case 'm3u':
                        if (store.getState().mopidy.connected ) store.dispatch(mopidyActions.getPlaylist(action.key))
                        break
                }
                next(action)
                break

            case 'PLAYLIST_LOADED':
                if (action.data) ReactGA.event({ category: 'Playlist', action: 'Load', label: action.playlist.uri })

                var playlist = Object.assign({}, action.playlist)
                switch (helpers.uriSource(playlist.uri)){

                    case 'm3u':
                        playlist.can_edit = true
                        break

                    case 'spotify':
                        if (store.getState().spotify.authorization && store.getState().spotify.me){
                            playlist.can_edit = (helpers.getFromUri('playlistowner',playlist.uri) == store.getState().spotify.me.id)
                        }
                }

                // Load our tracks
                if (action.playlist.tracks){
                    store.dispatch({
                        type: 'TRACKS_LOADED',
                        tracks: action.playlist.tracks
                    });
                }

                // proceed as usual
                action.playlist = playlist;
                next(action);
                break

            case 'MOPIDY_CURRENTTLTRACK':
                if (action.data && action.data.track){
                    helpers.setWindowTitle(action.data.track, store.getState().mopidy.play_state);

                    // make sure our images use mopidy host:port
                    if (action.data.track.album && action.data.track.album.images && action.data.track.album.images.length > 0){
                        var images = Object.assign([], action.data.track.album.images)
                        for (var i = 0; i < images.length; i++){
                            if (typeof(images[i]) === 'string' && images[i].startsWith('/images/')){
                                images[i] = '//'+store.getState().mopidy.host+':'+store.getState().mopidy.port+images[i]
                            }
                        }
                        action.data.track.album.images = images
                    }
                }

                next(action)
                break

            case 'MOPIDY_TLTRACKS':

                var core = store.getState().core;
                var tracklist = []
                for (var i = 0; i < action.data.length; i++){

                    var tltrack = helpers.formatTracks(action.data[i]);

                    // load our metadata (if we have any for that tlid)
                    if (core.queue_metadata !== undefined && core.queue_metadata['tlid_'+tltrack.tlid] !== undefined){
                        var metadata = core.queue_metadata['tlid_'+tltrack.tlid]
                    } else {
                        var metadata = {}
                    }

                    var current_tlid = null;
                    if (core.current_track && core.tracks && core.tracks[core.current_track] !== undefined && core.tracks[core.current_track].tlid !== undefined){
                        current_tlid = core.tracks[core.current_track].tlid;
                    }

                    var track = Object.assign(
                        {},
                        tltrack,
                        metadata,
                        {
                            playing: (tltrack.tlid == current_tlid)
                        })
                    tracklist.push(track)
                }

                // Append to our action
                tracklist = helpers.formatTracks(tracklist);
                action.tracklist = tracklist;

                // Load our tracks into index
                store.dispatch({
                    type: 'TRACKS_LOADED',
                    tracks: tracklist
                });

                next(action);
                break

            // Get assets from all of our providers
            case 'GET_LIBRARY_PLAYLISTS':
                if (store.getState().spotify.connected){
                    store.dispatch(spotifyActions.getLibraryPlaylists())
                }
                if (store.getState().mopidy.connected){
                    store.dispatch(mopidyActions.getLibraryPlaylists())
                }
                next(action)
                break

            // Get assets from all of our providers
            case 'GET_LIBRARY_ALBUMS':
                if (store.getState().spotify.connected){
                    store.dispatch(spotifyActions.getLibraryAlbums())
                }
                if (store.getState().mopidy.connected){
                    store.dispatch(mopidyActions.getLibraryAlbums())
                }
                next(action)
                break

            // Get assets from all of our providers
            case 'GET_LIBRARY_ARTISTS':
                if (store.getState().spotify.connected){
                    store.dispatch(spotifyActions.getLibraryArtists())
                }
                if (store.getState().mopidy.connected){
                    store.dispatch(mopidyActions.getLibraryArtists())
                }
                next(action)
                break

            case 'RESTART':
                location.reload()
                break

            // This action is irrelevant to us, pass it on to the next middleware
            default:
                return next(action)
        }
    }

})();

export default CoreMiddleware<|endoftext|>


<commit_before>// API
const express = require('express');
const app = express();
var http = require('http');
var socketio = require('socket.io');
var server = http.createServer(app);
var io = require('socket.io').listen(server);

server.listen(3000);

const socketExecute =  require("./socket-connection");

socketExecute(io);

app.use('/public', express.static('public'));

// app.listen(process.env.PORT || 3000);

console.log("server is running. go http://localhost:" + (~~process.env.PORT || 3000) + "/public/index.html\n");

<commit_msg>ルーティングの変更<commit_after>// API
const express = require('express');
const app = express();
var http = require('http');
var socketio = require('socket.io');
var server = http.createServer(app);
var io = require('socket.io').listen(server);

server.listen(~~process.env.PORT || 3000);

const socketExecute =  require("./socket-connection");

socketExecute(io);

app.use('/', express.static('public'));

// app.listen(process.env.PORT || 3000);

console.log("server is running. go http://localhost:" + (~~process.env.PORT || 3000) + "/public/index.html\n");

<|endoftext|>


<commit_before>/*
 * Copyright 2014-2015 Jiří Janoušek <janousek.jiri@gmail.com>
 * Copyright 2015 Rémi Benoit <r3m1.benoit@gmail.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

(function(Nuvola)
{

// Create media player component
var player = Nuvola.$object(Nuvola.MediaPlayer);

// Handy aliases
var PlaybackState = Nuvola.PlaybackState;
var PlayerAction = Nuvola.PlayerAction;

// Create new WebApp prototype
var WebApp = Nuvola.$WebApp();

// Initialization routines
WebApp._onInitWebWorker = function(emitter)
{
    Nuvola.WebApp._onInitWebWorker.call(this, emitter);

    var state = document.readyState;
    if (state === "interactive" || state === "complete")
        this._onPageReady();
    else
        document.addEventListener("DOMContentLoaded", this._onPageReady.bind(this));
}

// Page is ready for magic
WebApp._onPageReady = function()
{
    this.playButton = null;
    this.nextButton = null;
    this.previousButton = null;

    // Connect handler for signal ActionActivated
    Nuvola.actions.connect("ActionActivated", this);

    // Start update routine
    this.update();
}

// Extract data from the web page
WebApp.update = function()
{
    this.playButton = document.querySelector(".playControl");
    this.nextButton = document.querySelector(".skipControl__next");
    this.previousButton = document.querySelector(".skipControl__previous");

    if (!this.playButton || !this.nextButton || !this.previousButton) {
        setTimeout(this.update.bind(this), 500);
        return;
    }
    var track = {
        title: null,
        artist: null,
        album: null,
        artLocation: null
    }

    if (this.playButton.getAttribute("tabindex") == "-1") {
        player.setTrack(track);
        player.setPlaybackState(Nuvola.PlaybackState.UNKNOWN);
        player.setCanGoPrev(false);
        player.setCanGoNext(false);
        player.setCanPlay(false);
        player.setCanPause(false);

        setTimeout(this.update.bind(this), 500);
        return;
    }

    var canPrev = (this.previousButton.getAttribute("tabindex") == "-1");
    var canNext = (this.nextButton.getAttribute("tabindex") == "-1");
    var state = this.playButton.classList.contains("playing") ? PlaybackState.PLAYING : PlaybackState.PAUSED;

    var titleNode = document.querySelector(".playbackSoundBadge__title");
    if (titleNode) {
        track.title = titleNode.getAttribute("title");

        var artist = document.title.substring(track.title.length + 6);
        if (artist && artist.length > 0)
            track.artist = artist

        var artUrl = document.querySelector(".playbackSoundBadge__avatar").children[0].children[0].style.backgroundImage;
        if (artUrl)
            track.artLocation = artUrl.substring(4, artUrl.length-1);
    }

    player.setTrack(track);
    player.setPlaybackState(state);
    player.setCanGoPrev(canPrev);
    player.setCanGoNext(canNext);
    player.setCanPlay(state === PlaybackState.PAUSED);
    player.setCanPause(state === PlaybackState.PLAYING);

    // Schedule the next update
    setTimeout(this.update.bind(this), 500);
}

// Handler of playback actions
WebApp._onActionActivated = function(emitter, name, param)
{
    switch (name)
    {
    case PlayerAction.TOGGLE_PLAY:
    case PlayerAction.PLAY:
    case PlayerAction.PAUSE:
    case PlayerAction.STOP:
	if (this.playButton)
	    Nuvola.clickOnElement(this.playButton);
	else
	    Nuvola.warn("Play button not found for '{1}', section {2}", name, this.section);
        break;
    case PlayerAction.PREV_SONG:
        Nuvola.clickOnElement(document.querySelector(".skipControl__previous"));
        break;
    case PlayerAction.NEXT_SONG:
        Nuvola.clickOnElement(document.querySelector(".skipControl__next"));
        break;
    }
}

WebApp.start();

})(this);  // function(Nuvola)
<commit_msg>Treat different page title style<commit_after>/*
 * Copyright 2014-2015 Jiří Janoušek <janousek.jiri@gmail.com>
 * Copyright 2015 Rémi Benoit <r3m1.benoit@gmail.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

(function(Nuvola)
{

// Create media player component
var player = Nuvola.$object(Nuvola.MediaPlayer);

// Handy aliases
var PlaybackState = Nuvola.PlaybackState;
var PlayerAction = Nuvola.PlayerAction;

// Create new WebApp prototype
var WebApp = Nuvola.$WebApp();

// Initialization routines
WebApp._onInitWebWorker = function(emitter)
{
    Nuvola.WebApp._onInitWebWorker.call(this, emitter);

    var state = document.readyState;
    if (state === "interactive" || state === "complete")
        this._onPageReady();
    else
        document.addEventListener("DOMContentLoaded", this._onPageReady.bind(this));
}

// Page is ready for magic
WebApp._onPageReady = function()
{
    this.playButton = null;
    this.nextButton = null;
    this.previousButton = null;

    // Connect handler for signal ActionActivated
    Nuvola.actions.connect("ActionActivated", this);

    // Start update routine
    this.update();
}

// Extract data from the web page
WebApp.update = function()
{
    this.playButton = document.querySelector(".playControl");
    this.nextButton = document.querySelector(".skipControl__next");
    this.previousButton = document.querySelector(".skipControl__previous");

    if (!this.playButton || !this.nextButton || !this.previousButton) {
        setTimeout(this.update.bind(this), 500);
        return;
    }
    var track = {
        title: null,
        artist: null,
        album: null,
        artLocation: null
    }

    if (this.playButton.getAttribute("tabindex") == "-1") {
        player.setTrack(track);
        player.setPlaybackState(Nuvola.PlaybackState.UNKNOWN);
        player.setCanGoPrev(false);
        player.setCanGoNext(false);
        player.setCanPlay(false);
        player.setCanPause(false);

        setTimeout(this.update.bind(this), 500);
        return;
    }

    var canPrev = (this.previousButton.getAttribute("tabindex") == "-1");
    var canNext = (this.nextButton.getAttribute("tabindex") == "-1");
    var state = this.playButton.classList.contains("playing") ? PlaybackState.PLAYING : PlaybackState.PAUSED;

    var titleNode = document.querySelector(".playbackSoundBadge__title");
    if (titleNode) {
        track.title = titleNode.getAttribute("title");

        var artist = document.title.replace(" on SoundCloud - Listen to music", "");
        artist = artist.substring(track.title.length + 6);
        if (artist && artist.length > 0)
            track.artist = artist

        var artUrl = document.querySelector(".playbackSoundBadge__avatar").children[0].children[0].style.backgroundImage;
        if (artUrl)
            track.artLocation = artUrl.substring(4, artUrl.length-1);
    }

    player.setTrack(track);
    player.setPlaybackState(state);
    player.setCanGoPrev(canPrev);
    player.setCanGoNext(canNext);
    player.setCanPlay(state === PlaybackState.PAUSED);
    player.setCanPause(state === PlaybackState.PLAYING);

    // Schedule the next update
    setTimeout(this.update.bind(this), 500);
}

// Handler of playback actions
WebApp._onActionActivated = function(emitter, name, param)
{
    switch (name)
    {
    case PlayerAction.TOGGLE_PLAY:
    case PlayerAction.PLAY:
    case PlayerAction.PAUSE:
    case PlayerAction.STOP:
	if (this.playButton)
	    Nuvola.clickOnElement(this.playButton);
	else
	    Nuvola.warn("Play button not found for '{1}', section {2}", name, this.section);
        break;
    case PlayerAction.PREV_SONG:
        Nuvola.clickOnElement(document.querySelector(".skipControl__previous"));
        break;
    case PlayerAction.NEXT_SONG:
        Nuvola.clickOnElement(document.querySelector(".skipControl__next"));
        break;
    }
}

WebApp.start();

})(this);  // function(Nuvola)
<|endoftext|>