MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-

View on GitHub
src/renderer/ui/components/generic/LyricsViewer.js

Summary

Maintainability
C
1 day
Test Coverage
import $ from 'jquery';
import React, { Component, PropTypes } from 'react';
import { findDOMNode } from 'react-dom';
import { shell, remote } from 'electron';
import { requireSettings } from './SettingsProvider';

class LyricsViewer extends Component {
  static propTypes = {
    theme: PropTypes.bool.isRequired,
    themeColor: PropTypes.string.isRequired,
    themeType: PropTypes.string.isRequired,
  };

  static openGoogleSearch(event) {
    event.stopPropagation();
    const { artist, title } = remote.getGlobal('PlaybackAPI').currentSong(true);
    const lyricsTranslation = TranslationProvider.query('lyrics-lyrics');
    const query = encodeURIComponent(`${title} - ${artist} ${lyricsTranslation}`);

    shell.openExternal(`https://www.google.com/search?q=${query}`);
  }

  constructor(...args) {
    super(...args);

    this.state = {
      visible: false,
      noLyricsFound: false,
    };
  }

  // I hate this so much
  // But I also don't know how to do this with React so meh
  // TODO: Clean this rubbish up
  componentDidMount() {
    let animate = false;
    let animationTimer;
    let noLyricsTimer;
    let jumpDetect;
    let isPlaying = false;

    // Handle new lyrics strings
    this.lyricsHandler = (e, lyrics) => {
      if (!lyrics) {
        $(findDOMNode(this)).find('#lyrics').html('<h1><span is="translation-key">lyrics-loading-message</span></h1>');
        $(findDOMNode(this)).find('#lyrics p').stop();
        animate = false;
        clearTimeout(noLyricsTimer);
        noLyricsTimer = setTimeout(() => {
          this.handleLyricsNotFound();
          $(findDOMNode(this)).find('#lyrics').html('<h1><span is="translation-key">lyrics-failed-message</span></h1>');
        }, 4000);
      } else {
        clearTimeout(noLyricsTimer);
        const scroll = Settings.get('scrollLyrics', true);
        const lyricsHTML = lyrics.replace(/\n/g, '<br />');
        $(findDOMNode(this)).find('#lyrics').html(`<p ${scroll ? 'data-scroll' : ''}>${lyricsHTML}</p>`);
        animate = scroll;
      }
    };
    // Handle playing and pausing
    this.stateHandler = (e, remoteIsPlaying) => {
      isPlaying = remoteIsPlaying;
      if (!isPlaying) return $(findDOMNode(this)).find('#lyrics p').stop();
      animate = Settings.get('scrollLyrics', true);
    };
    // Handle time progression of a song
    this.timeHandler = (e, timeObj) => {
      $(findDOMNode(this)).find('#lyrics_bar').width(`${(timeObj.total === 0 ? 0 : timeObj.current / timeObj.total) * 100}%`);
      let jumped = false;
      if (Math.abs(timeObj.current - jumpDetect) > 1000) {
        animate = true;
        jumped = true;
      }

      jumpDetect = timeObj.current;
      if (!isPlaying || !animate || !timeObj.total || !$(findDOMNode(this)).find('#lyrics p').get(0)) return;
      const lyricsP = $(findDOMNode(this)).find('#lyrics p');
      const maxHeight = parseInt(lyricsP.get(0).scrollHeight, 10);
      const viewPortHeight = parseInt(lyricsP.innerHeight(), 10);
      const waitTime = (viewPortHeight / maxHeight) * timeObj.total * 0.3;
      const actualWaitTime = Math.max(0, waitTime - timeObj.current);
      clearTimeout(animationTimer);
      if (jumped) {
        lyricsP.stop();
        lyricsP.scrollTop(maxHeight * (Math.max(0, timeObj.current - actualWaitTime) / timeObj.total));

        // Reset animate after jump to prevent unwanted scrolling
        animate = Settings.get('scrollLyrics', true);
        animationTimer.setTimeout();
      }
      animationTimer = setTimeout(() => {
        lyricsP.stop().animate({
          scrollTop: maxHeight - viewPortHeight,
        }, timeObj.total - timeObj.current - actualWaitTime - waitTime, 'linear');
      }, actualWaitTime);
      animate = false;
    };

    this.scrollSettingsHandler = (e, state) => {
      const lyricsP = $(findDOMNode(this)).find('#lyrics p');
      animate = state;
      if (state) {
        lyricsP.attr('data-scroll', true);
      } else {
        lyricsP.removeAttr('data-scroll');
        clearTimeout(animationTimer);
        lyricsP.stop();
      }
    };

    this.startAnimating = () => {
      if (Settings.get('scrollLyrics', true)) {
        animate = true;
      }
    };

    this._hook();
  }

  componentWillUnmount() {
    this._unhook();
  }

  _hook() {
    Emitter.on('lyrics:show', this.show);
    Emitter.on('PlaybackAPI:change:lyrics', this.lyricsHandler);
    Emitter.on('PlaybackAPI:change:state', this.stateHandler);
    Emitter.on('settings:set:scrollLyrics', this.scrollSettingsHandler);
  }

  _unhook() {
    Emitter.off('lyrics:show', this.show);
    Emitter.off('PlaybackAPI:change:lyrics', this.lyricsHandler);
    Emitter.off('PlaybackAPI:change:state', this.stateHandler);
    Emitter.off('settings:set:scrollLyrics', this.scrollSettingsHandler);
  }

  handleLyricsNotFound() {
    this.setState(prevState => Object.assign(prevState, { noLyricsFound: true }));
  }


  hide = () => {
    this.setState({
      visible: false,
    });
    Emitter.off('PlaybackAPI:change:time', this.timeHandler);
    window.removeEventListener('resize', this.startAnimating);
  }

  show = () => {
    Emitter.on('PlaybackAPI:change:time', this.timeHandler);
    window.addEventListener('resize', this.startAnimating);

    this.setState({
      visible: true,
    });
  }

  render() {
    const barStyle = {};
    barStyle.backgroundColor = 'rgb(252, 88, 37)';

    const progressStyle = {};
    progressStyle.backgroundColor = 'rgb(34, 35, 38)';

    if (this.props.theme) {
      barStyle.backgroundColor = this.props.themeColor;
    }

    return (
      <div id="lyrics_back" className={this.state.visible ? 'vis' : ''} onClick={this.hide}>
        <div id="lyrics_container">
          <div id="lyrics">
            <h1>
              {TranslationProvider.query('lyrics-no-song-message')}
            </h1>
          </div>
          {this.state.noLyricsFound &&
            <a id="search-link" onClick={LyricsViewer.openGoogleSearch}>{TranslationProvider.query('lyrics-search-in')} Google.com</a>
           }
          <div id="shadow"></div>
        </div>
        <div id="lyrics_progress" style={progressStyle}>
          <div id="lyrics_bar" className="lyrics-progress" style={barStyle}></div>
        </div>
      </div>
    );
  }
}

export default requireSettings(LyricsViewer, ['theme', 'themeColor', 'themeType']);