<commit_before>import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';

const messages = defineMessages({
  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
});

class Item extends React.PureComponent {

  static propTypes = {
    attachment: ImmutablePropTypes.map.isRequired,
    index: PropTypes.number.isRequired,
    size: PropTypes.number.isRequired,
    onClick: PropTypes.func.isRequired,
    autoPlayGif: PropTypes.bool.isRequired,
    expandMedia: PropTypes.bool.isRequired,
    lineMedia: PropTypes.bool,
    squareMedia: PropTypes.bool.isRequired,
  };

  handleClick = (e) => {
    const { index, onClick } = this.props;

    if (e.button === 0) {
      e.preventDefault();
      onClick(index);
    }

    e.stopPropagation();
  }

  render () {
    const { attachment, index, size, squareMedia, expandMedia, lineMedia } = this.props;

    let width  = 50;
    let height = 100;
    let top    = 'auto';
    let left   = 'auto';
    let bottom = 'auto';
    let right  = 'auto';

    if (size === 1 || expandMedia) {
      width = 100;
    } else if (lineMedia) {
      width = (100 - (size - 1)) / size;
      left = `${index}%`;
    }

    if (!expandMedia && !lineMedia) {
      if (size === 4 || (size === 3 && index > 0)) {
        height = 50;
      }

      if (size === 2) {
        if (index === 0) {
          right = '2px';
        } else {
          left = '2px';
        }
      } else if (size === 3) {
        if (index === 0) {
          right = '2px';
        } else if (index > 0) {
          left = '2px';
        }

        if (index === 1) {
          bottom = '2px';
        } else if (index > 1) {
          top = '2px';
        }
      } else if (size === 4) {
        if (index === 0 || index === 2) {
          right = '2px';
        }

        if (index === 1 || index === 3) {
          left = '2px';
        }

        if (index < 2) {
          bottom = '2px';
        } else {
          top = '2px';
        }
      }
    }

    let thumbnail = '';

    if (attachment.get('type') === 'image') {
      const previewUrl = attachment.get('preview_url');
      const previewWidth = attachment.getIn(['meta', 'small', 'width']);

      const originalUrl = attachment.get('url');
      const originalWidth = attachment.getIn(['meta', 'original', 'width']);

      const srcSet = attachment.has('meta') ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
      const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;

      if (expandMedia) {
        thumbnail = (

        );
      } else {
        thumbnail = (

            <img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' style={{ backgroundPosition: `50% ${squareMedia ? '0' : '20%'}` }} />

        );
      }
    } else if (attachment.get('type') === 'gifv') {
      const autoPlay = !isIOS() && this.props.autoPlayGif;

      thumbnail = (
        <div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
          <video
            className='media-gallery__item-gifv-thumbnail'
            role='application'
            src={attachment.get('url')}
            onClick={this.handleClick}
            autoPlay={autoPlay}
            loop
            muted
          />

          GIF

      );
    }

    return (
      <div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
        {thumbnail}

    );
  }

}

@injectIntl
export default class MediaGallery extends React.PureComponent {

  static propTypes = {
    sensitive: PropTypes.bool,
    media: ImmutablePropTypes.list.isRequired,
    height: PropTypes.number.isRequired,
    onOpenMedia: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
    autoPlayGif: PropTypes.bool.isRequired,
    expandMedia: PropTypes.bool.isRequired,
    lineMedia: PropTypes.bool,
    squareMedia: PropTypes.bool.isRequired,
  };

  static defaultProps = {
    expandMedia: false,
    lineMedia: false,
    squareMedia: false,
  };

  state = {
    visible: !this.props.sensitive,
  };

  componentWillReceiveProps(nextProps) {
    if (nextProps.sensitive !== this.props.sensitive) {
      this.setState({ visible: !nextProps.sensitive });
    }
  }

  handleOpen = () => {
    this.setState({ visible: !this.state.visible });
  }

  handleClick = (index) => {
    this.props.onOpenMedia(this.props.media, index);
  }

  render () {
    const { media, intl, sensitive, squareMedia, expandMedia, lineMedia } = this.props;

    let children;

    if (!this.state.visible) {
      let warning;

      if (sensitive) {
        warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
      } else {
        warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
      }

      children = (

          {warning}
          <FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' />

      );
    } else {
      const size = media.take(4).size;
      children = media.take(4).map((attachment, i) =>
        <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} squareMedia={squareMedia} expandMedia={expandMedia} lineMedia={!!lineMedia} />
      );
    }

    return (
      <div className='media-gallery' style={{ height: (expandMedia && this.state.visible) ? 'auto' : `${this.props.height}px` }}>
        <div className={`spoiler-button ${this.state.visible ? 'spoiler-button--visible' : ''}`}>
          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />

        {children}

    );
  }

}
<commit_msg>add space<commit_after>import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';

const messages = defineMessages({
  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
});

class Item extends React.PureComponent {

  static propTypes = {
    attachment: ImmutablePropTypes.map.isRequired,
    index: PropTypes.number.isRequired,
    size: PropTypes.number.isRequired,
    onClick: PropTypes.func.isRequired,
    autoPlayGif: PropTypes.bool.isRequired,
    expandMedia: PropTypes.bool.isRequired,
    lineMedia: PropTypes.bool,
    squareMedia: PropTypes.bool.isRequired,
  };

  handleClick = (e) => {
    const { index, onClick } = this.props;

    if (e.button === 0) {
      e.preventDefault();
      onClick(index);
    }

    e.stopPropagation();
  }

  render () {
    const { attachment, index, size, squareMedia, expandMedia, lineMedia } = this.props;

    let width  = 50;
    let height = 100;
    let top    = 'auto';
    let left   = 'auto';
    let bottom = 'auto';
    let right  = 'auto';

    if (size === 1 || expandMedia) {
      width = 100;
    } else if (lineMedia) {
      width = (100 - (size - 1)) / size;
      left = `${index}%`;
    }

    if (!expandMedia && !lineMedia) {
      if (size === 4 || (size === 3 && index > 0)) {
        height = 50;
      }

      if (size === 2) {
        if (index === 0) {
          right = '2px';
        } else {
          left = '2px';
        }
      } else if (size === 3) {
        if (index === 0) {
          right = '2px';
        } else if (index > 0) {
          left = '2px';
        }

        if (index === 1) {
          bottom = '2px';
        } else if (index > 1) {
          top = '2px';
        }
      } else if (size === 4) {
        if (index === 0 || index === 2) {
          right = '2px';
        }

        if (index === 1 || index === 3) {
          left = '2px';
        }

        if (index < 2) {
          bottom = '2px';
        } else {
          top = '2px';
        }
      }
    }

    let thumbnail = '';

    if (attachment.get('type') === 'image') {
      const previewUrl = attachment.get('preview_url');
      const previewWidth = attachment.getIn(['meta', 'small', 'width']);

      const originalUrl = attachment.get('url');
      const originalWidth = attachment.getIn(['meta', 'original', 'width']);

      const srcSet = attachment.has('meta') ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
      const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;

      if (expandMedia) {
        thumbnail = (

        );
      } else {
        thumbnail = (

            <img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' style={{ backgroundPosition: `50% ${squareMedia ? '0' : '20%'}` }} />

        );
      }
    } else if (attachment.get('type') === 'gifv') {
      const autoPlay = !isIOS() && this.props.autoPlayGif;

      thumbnail = (
        <div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
          <video
            className='media-gallery__item-gifv-thumbnail'
            role='application'
            src={attachment.get('url')}
            onClick={this.handleClick}
            autoPlay={autoPlay}
            loop
            muted
          />

          GIF

      );
    }

    return (
      <div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
        {thumbnail}

    );
  }

}

@injectIntl
export default class MediaGallery extends React.PureComponent {

  static propTypes = {
    sensitive: PropTypes.bool,
    media: ImmutablePropTypes.list.isRequired,
    height: PropTypes.number.isRequired,
    onOpenMedia: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
    autoPlayGif: PropTypes.bool.isRequired,
    expandMedia: PropTypes.bool.isRequired,
    lineMedia: PropTypes.bool,
    squareMedia: PropTypes.bool.isRequired,
  };

  static defaultProps = {
    expandMedia: false,
    lineMedia: false,
    squareMedia: false,
  };

  state = {
    visible: !this.props.sensitive,
  };

  componentWillReceiveProps (nextProps) {
    if (nextProps.sensitive !== this.props.sensitive) {
      this.setState({ visible: !nextProps.sensitive });
    }
  }

  handleOpen = () => {
    this.setState({ visible: !this.state.visible });
  }

  handleClick = (index) => {
    this.props.onOpenMedia(this.props.media, index);
  }

  render () {
    const { media, intl, sensitive, squareMedia, expandMedia, lineMedia } = this.props;

    let children;

    if (!this.state.visible) {
      let warning;

      if (sensitive) {
        warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
      } else {
        warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
      }

      children = (

          {warning}
          <FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' />

      );
    } else {
      const size = media.take(4).size;
      children = media.take(4).map((attachment, i) =>
        <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} squareMedia={squareMedia} expandMedia={expandMedia} lineMedia={!!lineMedia} />
      );
    }

    return (
      <div className='media-gallery' style={{ height: (expandMedia && this.state.visible) ? 'auto' : `${this.props.height}px` }}>
        <div className={`spoiler-button ${this.state.visible ? 'spoiler-button--visible' : ''}`}>
          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />

        {children}

    );
  }

}
<|endoftext|>


<commit_before>/**
 * Used for adding, editing, and copying earnings entries.
 * @module kocomponents/earnings/editor
 *
 */
import '../../client/editor';
import '../../client/list';
import '../../utilities/icon-dec';
import '../../input/duration';
import '../../../utils/autofocusBindingHandler';
import '../../input/date';
import * as suggestedPlatformsList from '../../../data/suggestedPlatforms';
import { ActionForValue } from '../../job-title-autocomplete';
import Komponent from '../../helpers/KnockoutComponent';
import UserEarningsEntry from '../../../models/UserEarningsEntry';
import ko from 'knockout';
import { show as showConfirm } from '../../../modals/confirm';
import { show as showError } from '../../../modals/error';
import template from './template.html';
import { item as userEarningsItem } from '../../../data/userEarnings';
import { list as userExternalListingsList } from '../../../data/userExternalListings';
import { list as userListingsList } from '../../../data/userListings';

const TAG_NAME = 'earnings-editor';

/**
 * @enum {string} Supported displaying modes
 */
const EditorMode = {
    add: 'add',
    edit: 'edit',
    copy: 'copy'
};

/**
 * Component
 */
export default class EarningsEditor extends Komponent {

    static get template() { return template; }

    /**
     * Parameters allowed are 'input only' when the value given is read at constructor
     * and keeps constant internally. If is an observable, any change from outside is
     * not read.
     * @param {object} params
     * @param {KnockoutObservable<EditorMode>} [params.editorMode] Input only value setting-up the mode in use
     * @param {(number|KnockoutObservable<number>)} [params.earningsEntryID] Input only ID to be edited or copied, or zero for new.
     * @param {(number|KnockoutObservable<number>)} [params.userExternalListingID] Input only ID to preset the external listing,
     * this let's add an earning from a listing component as a shortcut.
     * @param {KnockoutObservable<integer>} [params.startAtStep] Input only value setting-up the initial step for the component.
     * @param {function} [params.onSaved] Callback to notify after save the item, with the updated data included
     * @param {function} [params.onDeleted] Callback to notify after delete the item
     */
    constructor(params) {
        super();

        /**
         * Editable earnings entry. Same instance is used all the time, just
         * updating content, simplifying working with the form and summary.
         * @member {UserEarningsEntry}
         */
        this.earningsEntry = new UserEarningsEntry({
            earningsEntryID: ko.unwrap(params.earningsEntryID) || 0,
            userExternalListingID: ko.unwrap(params.userExternalListingID)
        });

        /**
         * Captures from the activity which "mode" the editor
         * component is to be used.
         * @member {EditorMode}
         */
        this.editorMode = ko.observable(ko.unwrap(params.editorMode));

        /**
         * @member {KnockoutObservable<object>}
         */
        this.selectedClient = ko.observable(null);

        /**
         * Callback executed when the form is saved successfully, giving
         * a copy of the server data
         * @member {function}
         */
        this.onSaved = params.onSaved;

        /**
         * Callback executed when the entry was deleted successfully
         * @member {function}
         */
        this.onDeleted = params.onDeleted;

        /**
         * Holds a list of the user external listings, available to be selected
         * as the earnings entry listing.
         */
        this.userExternalListings = ko.observableArray([]);

        /**
         * Holds a list of the user listings at Loconomics, available to allow
         * quick selection of job title.
         */
        this.userListings = ko.observableArray([]);

        /**
         * Holds a list of suggested platforms, available to be selected
         * as the earnigns entry listing, but while is not a listing it will
         * create a listing for that platform automatically.
         */
        this.suggestedPlatforms = ko.observableArray([]);

        // Starts in review mode when we are editing or copying an entry,
        // so anything else but 'add'
        this.__setupStepsManagement(params.editorMode !== EditorMode.add);

        this.__setupStatusFlags();

        /**
         * Label text for the 'delete' button
         * @member {KnockoutComputed<string>}
         */
        this.deleteButtonText = ko.pureComputed(() => {
            var itIs = this.isDeleting();
            return itIs ? 'Deleting..' : 'Delete entry';
        });

        /**
         * Label text for the 'save' button
         * @member {KnockoutComputed<string>}
         */
        this.saveButtonText = ko.pureComputed(() => {
            var itIs = this.isDeleting();
            return itIs ? 'Submitting..' : 'Submit';
        });

        this.__setupDataOperations();
    }

    /**
     * Define members to implement a step based interface.
     * @param {boolean} [startInReview=false] Whether the interface must start at the
     * summary step with review enabled.
     * @private
     */
    __setupStepsManagement(startInReview) {
        /**
         * Magic number for the step where is the summary.
         * @const {number}
         * @private
         */
        const SUMMARY_STEP = 0;
        /**
         * Keeps track of the current step being displayed.
         * By default is 1, first step, except when starting at review that will
         * be 0 (the summary).
         * This default is in sync with isAtReview initialization.
         * @member {KnockoutObservable<integer>}
         */
        this.currentStep = ko.observable(startInReview ? SUMMARY_STEP : 1);

        /**
         * Returns which step the user is on in the form.
         * @member {KnockoutComputed<boolean>}
         */
        this.isAtStep = function(number) {
            return ko.pureComputed(() => this.currentStep() === number);
        };

        /**
         * Whether the user has completed the steps almost once and is free
         * to jump between the summary and steps in order to do touch ups.
         * By default is false, which means the user is restricted to follow
         * the steps in order until finalize and reach the summary, when
         * this flag switchs to true.
         * Can be set.
         * By default is false, except when starting at review that will be true.
         * This default is in sync with currentStep initialization.
         * @member {KnockoutObservable<boolean>}
         */
        this.isAtReview = ko.observable(!!startInReview);

        /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<number>}
         */
        this.goNextStep = () => {
            this.currentStep(this.currentStep() + 1);
        };

        /**
         * Takes the user to the specified step
         * @param {number} step Step number
         */
        this.goToStep = (step) => {
            this.currentStep(step);
        };

        /**
         * Takes the user to the summary.
         * Will enable review mode too.
         */
        this.goToSummary = () => {
            this.currentStep(SUMMARY_STEP);
            // Reached the summary, all steps done so enters review mode
            this.isAtReview(true);
        };

         /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<string>}
         */
        this.stepButtonLabel = ko.pureComputed(() => {
            if (this.isAtReview()) {
                return 'Save';
            }
            else {
                return 'Save and Continue';
            }
        });

        /**
         * Takes the user to the next step in the form (in standard mode),
         * or to the summary (when in review mode).
         * @member {KnockoutComputed<number>}
         */
        this.saveStep = () => {
            if (this.isAtReview()) {
                this.currentStep(SUMMARY_STEP);
            }
            else {
                this.goNextStep();
            }
        };
    }

    /**
     * Define members for all the status flags needed.
     * @private
     */
    __setupStatusFlags() {
        /**
         * When a loading request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isLoading = ko.observable(false);

        /**
         * When a saving request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isSaving = ko.observable(false);

        /**
         * When a deletion request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isDeleting = ko.observable(false);

        /**
         * When edition must be locked because of in progress
         * operations.
         * @member {KnockoutComputed<boolean>}
         */
        this.isLocked = ko.pureComputed(() => this.isSaving() || this.isLoading() || this.isDeleting());

        /**
         * Whether the item is a new record or is being edited.
         * @member {KnockoutObservable<boolean>}
         */
        this.isNew = ko.pureComputed(() => this.editorMode() !== EditorMode.edit);
    }

    /**
     * Define members, prepare subscriptions to work with the code
     * and start any initial request for data
     * @private
     */
    __setupDataOperations() {
        /**
         * We create an item manager to operate on the data for the requested ID
         * (allows to load, save, delete).
         */
        this.dataManager = userEarningsItem(this.earningsEntry.earningsEntryID());

        /**
         * Suscribe to data coming for the list and put them in our
         * userListings propery.
         */
        this.subscribeTo(userListingsList.onData, this.userListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your listings',
                error: err
            });
        });

        /**
         * Suscribe to data coming for the list and put them in our
         * userExternalListings propery.
         */
        this.subscribeTo(userExternalListingsList.onData, this.userExternalListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userExternalListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your external listings',
                error: err
            });
        });

        /**
         * Load suggestions.
         */
        this.subscribeTo(suggestedPlatformsList.onData, this.suggestedPlatforms);

        /// Notify data load errors
        this.subscribeTo(suggestedPlatformsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading the platforms',
                error: err
            });
        });

        // When we have an ID, we need to load it first
        if (this.earningsEntry.earningsEntryID()) {

            this.isLoading(true);

            this.dataManager.onceLoaded()
            .then((data) => {
                this.isLoading(false);
                if (this.editorMode() === EditorMode.copy) {
                    // On copy mode, we need to reset the ID and dataManager
                    // so it forces to create a new entry (otherwise it will
                    // actually update the original one)
                    data.earningsEntryID = 0;
                    this.dataManager = userEarningsItem(0);
                }
                this.earningsEntry.model.updateWith(data);
            })
            .catch((error) => {
                this.isLoading(false);
                showError({
                    title: 'There was an error loading the earnings entry',
                    error
                });
            });
        }
    }

    /**
     * Makes given client current one selected.
     * @param {Object} client
     * @method
     */
    selectClient(client) {
        // Updates edited entry with the client ID selected
        this.earningsEntry.clientUserID(client.clientID);
        // Save a reference, to display name and other info
        this.selectedClient(client);
        this.goToSummary();
    }

    /**
     * Makes given listing the one selected
     * @param {Object} listing
     * @method
     */
    selectListing(listing) {
        this.earningsEntry.userExternalListingID(listing.userExternalListingID);
        this.earningsEntry.listingTitle(listing.title);
        // for integrity, makes platform matches listing (not needed to save the data)
        this.earningsEntry.platformID(listing.platformID);
        this.saveStep();
    }

    /**
     * Gets the job title from the given user listings and use it as the
     * selected job title of the earnings entry
     * @param {Object} listing
     * @method
     */
    selectListingJobTitle(listing) {
        this.earningsEntry.jobTitleID(listing.jobTitleID);
        this.earningsEntry.jobTitleName(listing.jobTitleSingularName);
        this.goToSummary();
    }

    /**
     * Gets the job title from a job title selected from the autocomplete
     * and use it as the selected job title of the earnings entry
     * @param {string} textValue User input text searching a job title
     * @param {models/JobTitle} data Selected job title model
     */
    selectJobTitle(textValue, data) {
        if (!data || !data.jobTitleID) return;

        const id = data.jobTitleID();
        this.earningsEntry.jobTitleID(id);
        this.earningsEntry.jobTitleName(data.singularName());
        this.goToSummary();
        return {
            value: ActionForValue.clear
        };
    }

    /**
     * Makes given platform as the selected listing, which means a listing
     * will be created for that platform
     * @param {rest/Platform} platform
     */
    selectPlatform(platform) {
        this.earningsEntry.platformID(platform.platformID);
        // resets listingID or will not take effect
        this.earningsEntry.userExternalListingID(null);
        // Something to display to the user
        this.earningsEntry.listingTitle(`My ${platform.name} listing`);
        this.saveStep();
    }

    /**
     * Save data in the server
     * @returns {Promise<object>}
     */
    save() {
        if (this.isSaving()) return Promise.reject();

        this.isSaving(true);

        const data = this.earningsEntry.model.toPlainObject(true);

        return this.dataManager
        .save(data)
        .then((freshData) => {
            this.isSaving(false);
            if (this.onSaved) {
                // Notify
                this.onSaved(freshData);
            }
            else {
                // Use updated/created data
                this.earningsEntry.model.updateWith(freshData);
            }
        })
        .catch((error) => {
            this.isSaving(false);
            showError({
                title: 'There was an error saving the earnings entry',
                error
            });
        });
    }

    /**
     * Delete the entry being edited after confirmation
     * @returns {Promise}
     */
    confirmDelete() {
        if (this.isDeleting()) return Promise.reject();

        this.isDeleting(true);
        const id = this.earningsEntry.earningsEntryID();

        return showConfirm({
            title: 'Are you sure',
            message: 'Delete ernings entry #' + id,
            yes: 'Delete',
            no: 'Keep'
        })
        .then(() =>  this.dataManager.delete())
        .then(() => {
            this.isDeleting(false);
            if (this.onDeleted) {
                // Notify
                this.onDeleted();
            }
            else {
                // Reset to new item
                this.earningsEntry.model.reset();
                this.editorMode(EditorMode.add);
            }
        })
        .catch((error) => {
            this.isDeleting(false);
            if (error) {
                showError({
                    title: 'There was an error deleting the earnings entry',
                    error
                });
            }
        });
    }
}

ko.components.register(TAG_NAME, EarningsEditor);

<commit_msg>Minor: save button text changed on deleting, question mark on confirm deletion #776<commit_after>/**
 * Used for adding, editing, and copying earnings entries.
 * @module kocomponents/earnings/editor
 *
 */
import '../../client/editor';
import '../../client/list';
import '../../utilities/icon-dec';
import '../../input/duration';
import '../../../utils/autofocusBindingHandler';
import '../../input/date';
import * as suggestedPlatformsList from '../../../data/suggestedPlatforms';
import { ActionForValue } from '../../job-title-autocomplete';
import Komponent from '../../helpers/KnockoutComponent';
import UserEarningsEntry from '../../../models/UserEarningsEntry';
import ko from 'knockout';
import { show as showConfirm } from '../../../modals/confirm';
import { show as showError } from '../../../modals/error';
import template from './template.html';
import { item as userEarningsItem } from '../../../data/userEarnings';
import { list as userExternalListingsList } from '../../../data/userExternalListings';
import { list as userListingsList } from '../../../data/userListings';

const TAG_NAME = 'earnings-editor';

/**
 * @enum {string} Supported displaying modes
 */
const EditorMode = {
    add: 'add',
    edit: 'edit',
    copy: 'copy'
};

/**
 * Component
 */
export default class EarningsEditor extends Komponent {

    static get template() { return template; }

    /**
     * Parameters allowed are 'input only' when the value given is read at constructor
     * and keeps constant internally. If is an observable, any change from outside is
     * not read.
     * @param {object} params
     * @param {KnockoutObservable<EditorMode>} [params.editorMode] Input only value setting-up the mode in use
     * @param {(number|KnockoutObservable<number>)} [params.earningsEntryID] Input only ID to be edited or copied, or zero for new.
     * @param {(number|KnockoutObservable<number>)} [params.userExternalListingID] Input only ID to preset the external listing,
     * this let's add an earning from a listing component as a shortcut.
     * @param {KnockoutObservable<integer>} [params.startAtStep] Input only value setting-up the initial step for the component.
     * @param {function} [params.onSaved] Callback to notify after save the item, with the updated data included
     * @param {function} [params.onDeleted] Callback to notify after delete the item
     */
    constructor(params) {
        super();

        /**
         * Editable earnings entry. Same instance is used all the time, just
         * updating content, simplifying working with the form and summary.
         * @member {UserEarningsEntry}
         */
        this.earningsEntry = new UserEarningsEntry({
            earningsEntryID: ko.unwrap(params.earningsEntryID) || 0,
            userExternalListingID: ko.unwrap(params.userExternalListingID)
        });

        /**
         * Captures from the activity which "mode" the editor
         * component is to be used.
         * @member {EditorMode}
         */
        this.editorMode = ko.observable(ko.unwrap(params.editorMode));

        /**
         * @member {KnockoutObservable<object>}
         */
        this.selectedClient = ko.observable(null);

        /**
         * Callback executed when the form is saved successfully, giving
         * a copy of the server data
         * @member {function}
         */
        this.onSaved = params.onSaved;

        /**
         * Callback executed when the entry was deleted successfully
         * @member {function}
         */
        this.onDeleted = params.onDeleted;

        /**
         * Holds a list of the user external listings, available to be selected
         * as the earnings entry listing.
         */
        this.userExternalListings = ko.observableArray([]);

        /**
         * Holds a list of the user listings at Loconomics, available to allow
         * quick selection of job title.
         */
        this.userListings = ko.observableArray([]);

        /**
         * Holds a list of suggested platforms, available to be selected
         * as the earnigns entry listing, but while is not a listing it will
         * create a listing for that platform automatically.
         */
        this.suggestedPlatforms = ko.observableArray([]);

        // Starts in review mode when we are editing or copying an entry,
        // so anything else but 'add'
        this.__setupStepsManagement(params.editorMode !== EditorMode.add);

        this.__setupStatusFlags();

        /**
         * Label text for the 'delete' button
         * @member {KnockoutComputed<string>}
         */
        this.deleteButtonText = ko.pureComputed(() => {
            var itIs = this.isDeleting();
            return itIs ? 'Deleting..' : 'Delete entry';
        });

        /**
         * Label text for the 'save' button
         * @member {KnockoutComputed<string>}
         */
        this.saveButtonText = ko.pureComputed(() => {
            var itIs = this.isSaving();
            return itIs ? 'Submitting..' : 'Submit';
        });

        this.__setupDataOperations();
    }

    /**
     * Define members to implement a step based interface.
     * @param {boolean} [startInReview=false] Whether the interface must start at the
     * summary step with review enabled.
     * @private
     */
    __setupStepsManagement(startInReview) {
        /**
         * Magic number for the step where is the summary.
         * @const {number}
         * @private
         */
        const SUMMARY_STEP = 0;
        /**
         * Keeps track of the current step being displayed.
         * By default is 1, first step, except when starting at review that will
         * be 0 (the summary).
         * This default is in sync with isAtReview initialization.
         * @member {KnockoutObservable<integer>}
         */
        this.currentStep = ko.observable(startInReview ? SUMMARY_STEP : 1);

        /**
         * Returns which step the user is on in the form.
         * @member {KnockoutComputed<boolean>}
         */
        this.isAtStep = function(number) {
            return ko.pureComputed(() => this.currentStep() === number);
        };

        /**
         * Whether the user has completed the steps almost once and is free
         * to jump between the summary and steps in order to do touch ups.
         * By default is false, which means the user is restricted to follow
         * the steps in order until finalize and reach the summary, when
         * this flag switchs to true.
         * Can be set.
         * By default is false, except when starting at review that will be true.
         * This default is in sync with currentStep initialization.
         * @member {KnockoutObservable<boolean>}
         */
        this.isAtReview = ko.observable(!!startInReview);

        /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<number>}
         */
        this.goNextStep = () => {
            this.currentStep(this.currentStep() + 1);
        };

        /**
         * Takes the user to the specified step
         * @param {number} step Step number
         */
        this.goToStep = (step) => {
            this.currentStep(step);
        };

        /**
         * Takes the user to the summary.
         * Will enable review mode too.
         */
        this.goToSummary = () => {
            this.currentStep(SUMMARY_STEP);
            // Reached the summary, all steps done so enters review mode
            this.isAtReview(true);
        };

         /**
         * Takes the user to the next step in the form.
         * @member {KnockoutComputed<string>}
         */
        this.stepButtonLabel = ko.pureComputed(() => {
            if (this.isAtReview()) {
                return 'Save';
            }
            else {
                return 'Save and Continue';
            }
        });

        /**
         * Takes the user to the next step in the form (in standard mode),
         * or to the summary (when in review mode).
         * @member {KnockoutComputed<number>}
         */
        this.saveStep = () => {
            if (this.isAtReview()) {
                this.currentStep(SUMMARY_STEP);
            }
            else {
                this.goNextStep();
            }
        };
    }

    /**
     * Define members for all the status flags needed.
     * @private
     */
    __setupStatusFlags() {
        /**
         * When a loading request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isLoading = ko.observable(false);

        /**
         * When a saving request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isSaving = ko.observable(false);

        /**
         * When a deletion request it's on the works
         * @member {KnockoutObservable<boolean>}
         */
        this.isDeleting = ko.observable(false);

        /**
         * When edition must be locked because of in progress
         * operations.
         * @member {KnockoutComputed<boolean>}
         */
        this.isLocked = ko.pureComputed(() => this.isSaving() || this.isLoading() || this.isDeleting());

        /**
         * Whether the item is a new record or is being edited.
         * @member {KnockoutObservable<boolean>}
         */
        this.isNew = ko.pureComputed(() => this.editorMode() !== EditorMode.edit);
    }

    /**
     * Define members, prepare subscriptions to work with the code
     * and start any initial request for data
     * @private
     */
    __setupDataOperations() {
        /**
         * We create an item manager to operate on the data for the requested ID
         * (allows to load, save, delete).
         */
        this.dataManager = userEarningsItem(this.earningsEntry.earningsEntryID());

        /**
         * Suscribe to data coming for the list and put them in our
         * userListings propery.
         */
        this.subscribeTo(userListingsList.onData, this.userListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your listings',
                error: err
            });
        });

        /**
         * Suscribe to data coming for the list and put them in our
         * userExternalListings propery.
         */
        this.subscribeTo(userExternalListingsList.onData, this.userExternalListings);

        /**
         * Notify data load errors
         */
        this.subscribeTo(userExternalListingsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading your external listings',
                error: err
            });
        });

        /**
         * Load suggestions.
         */
        this.subscribeTo(suggestedPlatformsList.onData, this.suggestedPlatforms);

        /// Notify data load errors
        this.subscribeTo(suggestedPlatformsList.onDataError, (err) => {
            showError({
                title: 'There was an error loading the platforms',
                error: err
            });
        });

        // When we have an ID, we need to load it first
        if (this.earningsEntry.earningsEntryID()) {

            this.isLoading(true);

            this.dataManager.onceLoaded()
            .then((data) => {
                this.isLoading(false);
                if (this.editorMode() === EditorMode.copy) {
                    // On copy mode, we need to reset the ID and dataManager
                    // so it forces to create a new entry (otherwise it will
                    // actually update the original one)
                    data.earningsEntryID = 0;
                    this.dataManager = userEarningsItem(0);
                }
                this.earningsEntry.model.updateWith(data);
            })
            .catch((error) => {
                this.isLoading(false);
                showError({
                    title: 'There was an error loading the earnings entry',
                    error
                });
            });
        }
    }

    /**
     * Makes given client current one selected.
     * @param {Object} client
     * @method
     */
    selectClient(client) {
        // Updates edited entry with the client ID selected
        this.earningsEntry.clientUserID(client.clientID);
        // Save a reference, to display name and other info
        this.selectedClient(client);
        this.goToSummary();
    }

    /**
     * Makes given listing the one selected
     * @param {Object} listing
     * @method
     */
    selectListing(listing) {
        this.earningsEntry.userExternalListingID(listing.userExternalListingID);
        this.earningsEntry.listingTitle(listing.title);
        // for integrity, makes platform matches listing (not needed to save the data)
        this.earningsEntry.platformID(listing.platformID);
        this.saveStep();
    }

    /**
     * Gets the job title from the given user listings and use it as the
     * selected job title of the earnings entry
     * @param {Object} listing
     * @method
     */
    selectListingJobTitle(listing) {
        this.earningsEntry.jobTitleID(listing.jobTitleID);
        this.earningsEntry.jobTitleName(listing.jobTitleSingularName);
        this.goToSummary();
    }

    /**
     * Gets the job title from a job title selected from the autocomplete
     * and use it as the selected job title of the earnings entry
     * @param {string} textValue User input text searching a job title
     * @param {models/JobTitle} data Selected job title model
     */
    selectJobTitle(textValue, data) {
        if (!data || !data.jobTitleID) return;

        const id = data.jobTitleID();
        this.earningsEntry.jobTitleID(id);
        this.earningsEntry.jobTitleName(data.singularName());
        this.goToSummary();
        return {
            value: ActionForValue.clear
        };
    }

    /**
     * Makes given platform as the selected listing, which means a listing
     * will be created for that platform
     * @param {rest/Platform} platform
     */
    selectPlatform(platform) {
        this.earningsEntry.platformID(platform.platformID);
        // resets listingID or will not take effect
        this.earningsEntry.userExternalListingID(null);
        // Something to display to the user
        this.earningsEntry.listingTitle(`My ${platform.name} listing`);
        this.saveStep();
    }

    /**
     * Save data in the server
     * @returns {Promise<object>}
     */
    save() {
        if (this.isSaving()) return Promise.reject();

        this.isSaving(true);

        const data = this.earningsEntry.model.toPlainObject(true);

        return this.dataManager
        .save(data)
        .then((freshData) => {
            this.isSaving(false);
            if (this.onSaved) {
                // Notify
                this.onSaved(freshData);
            }
            else {
                // Use updated/created data
                this.earningsEntry.model.updateWith(freshData);
            }
        })
        .catch((error) => {
            this.isSaving(false);
            showError({
                title: 'There was an error saving the earnings entry',
                error
            });
        });
    }

    /**
     * Delete the entry being edited after confirmation
     * @returns {Promise}
     */
    confirmDelete() {
        if (this.isDeleting()) return Promise.reject();

        this.isDeleting(true);
        const id = this.earningsEntry.earningsEntryID();

        return showConfirm({
            title: 'Are you sure?',
            message: 'Delete ernings entry #' + id,
            yes: 'Delete',
            no: 'Keep'
        })
        .then(() =>  this.dataManager.delete())
        .then(() => {
            this.isDeleting(false);
            if (this.onDeleted) {
                // Notify
                this.onDeleted();
            }
            else {
                // Reset to new item
                this.earningsEntry.model.reset();
                this.editorMode(EditorMode.add);
            }
        })
        .catch((error) => {
            this.isDeleting(false);
            if (error) {
                showError({
                    title: 'There was an error deleting the earnings entry',
                    error
                });
            }
        });
    }
}

ko.components.register(TAG_NAME, EarningsEditor);

<|endoftext|>


<commit_before>var SM = require('source-map'),
	program = require('commander'),
	fs = require('fs');

var FUNCTIONS = {
	generated: 'generatedPositionFor',
	original: 'originalPositionFor'
};

function die(msg) {
	console.error('[ERROR] ' + (msg || ''));
	program.help();
}

program
	.version(module.exports.version, '-v, --version')
	.description('CLI tool for consuming and querying source-map source maps')
	.usage('<generated|original> <source_map_file> [options]')
	.option('-c, --column <column>', 'The column number to use in the source map query', 1)
	.option('-l, --line <line>', 'The line number to use in the source map query', 1)
	.option('-s, --source <source>', 'Filename of the original source file when querying for a generated position');

program.command('generated')
	.description('    Returns the generated line and column information for the original source, line, and column positions provided');

program.command('original')
	.description('    Returns the original source, line, and column information for the generated source\'s line and column positions provided');

program.on('--help', function(){
  console.log('  Examples:');
  console.log('');
  console.log('    # Get original position for line 5 from "Resources/app.js".');
  console.log('    $ node consumer.js original .map/Resources/app.js.map -l 5');
  console.log('');
  console.log('    # Get the generated position for line 123, column 5 of the');
  console.log('    # original file "app/controllers/index.js".');
  console.log('    $ node consumer.js generated "Resources/alloy/controllers/index.js" -l 123 -c 5 -s "app/controllers/index.js"');
  console.log('');
});

// TODO: add examples in help

program.parse(process.argv);

// validate arguments
if (program.args.length === 0) {
	die('Missing command and source map file');
} else if (program.args.length === 1) {
	die('Missing source map file');
}

var command = program.args[0];
var func = FUNCTIONS[command];
if (!func) {
	die('Invalid command "' + command + '". Must be "original" or "generated".');
}

var sourceMapFile = program.args[1];
if (!fs.existsSync(sourceMapFile)) {
	die('Source map file "' + sourceMapFile + '" does not exist');
}

// read the source map and generate the consumer
var sourceMap = fs.readFileSync(sourceMapFile, 'utf8');
var consumer = new SM.SourceMapConsumer(sourceMap);
var obj = {
	line: program.line,
	column: program.column
};
program.source && (obj.source = program.source);

// execute the source map query and output the info
var ret = consumer[func](obj);
console.log(ret);
<commit_msg>get rid of consumer.js, covered in debugger command<commit_after><|endoftext|>


<commit_before>var format = require('util').format

module.exports = function(chai, utils){
  function formatInspect(string){
    var formatArgs = [string]
    for(var i = 1; i < arguments.length; i++){
      formatArgs.push(utils.inspect(arguments[i]))
    }
    return format.apply(null, formatArgs)
  }

  chai.Assertion.addMethod('cause', function(eventable){
    this._eventable = eventable
  })

  chai.Assertion.addMethod('emit', function(eventName){
    if(arguments.length > 1){
      var expectedArgs = Array.prototype.slice.call(arguments, 1)
    }

    var trigger = this._obj
    var eventable = this._eventable

    var calledArgs
    eventable.once(eventName, function(){
      calledArgs = Array.prototype.slice.call(arguments)
    })

    trigger()

    var expectedEmit = [eventName].concat(expectedArgs || [])
    var pMsg
    if(calledArgs){
      var actualEmit = [eventName].concat(calledArgs)
      pMsg = formatInspect('expected #{this} to cause %s to emit %s but got %s',
                           eventable, expectedEmit, actualEmit)
    } else {
      pMsg = formatInspect('expected #{this} to cause %s to emit %s',
                           eventable, expectedEmit)
    }

    var nMsg = formatInspect('expected #{this} to not cause %s to emit %s',
                             eventable, expectedEmit)

    if(expectedArgs){
      this.assert(
        utils.eql(calledArgs, expectedArgs)
      , pMsg
      , nMsg
      )
    } else {
      this.assert(
        calledArgs != undefined
      , pMsg
      , nMsg
      )
    }
  })
}
<commit_msg>Refactoring<commit_after>var format = require('util').format

module.exports = function(chai, utils){
  var toArray = Function.call.bind(Array.prototype.slice)

  function messaging(eventable, eventName, expectedArgs, calledArgs){
    var expectedEmit = utils.inspect([eventName].concat(expectedArgs || []))

    if(calledArgs){
      var butGot = ' but got ' + utils.inspect([eventName].concat(calledArgs))
    }

    return {
      positive: format('expected #{this} to cause %s to emit %s' + butGot,
                        eventable, expectedEmit),
      negative: format('expected #{this} to not cause %s to emit %s',
                        eventable, expectedEmit),
    }
  }

  function calledCheck(calledArgs, expectedArgs){
    if(expectedArgs){
      return utils.eql(calledArgs, expectedArgs)
    } else {
      return calledArgs != undefined
    }
  }

  chai.Assertion.addMethod('cause', function(eventable){
    this._eventable = eventable
  })

  chai.Assertion.addMethod('emit', function(eventName){
    if(arguments.length > 1){
      var expectedArgs = toArray(arguments, 1)
    }

    var trigger = this._obj
    var eventable = this._eventable

    var calledArgs
    eventable.once(eventName, function(){
      calledArgs = toArray(arguments)
    })

    trigger()

    var message = messaging(eventable, eventName, expectedArgs, calledArgs)
    this.assert(
      calledCheck(calledArgs, expectedArgs)
    , message.positive
    , message.positive
    )
  })
}
<|endoftext|>


<commit_before>"use strict"

const dimensional = [[1,2,3], ["a", "b", "c"]]
const crazy = [1,2,[3],["a",["b","c"]],["Eloquent"],["JavaScript"]]
var l = console.log
function flatten(array) {
	return array.reduce( (b,c) => {
		let bIsArray = Array.isArray(b),
				cIsArray = Array.isArray(c)
		return (bIsArray && cIsArray) ? (l("both are arrays"),b.concat(c)) :
					(!bIsArray && cIsArray) ? (l("c is array"),[b].concat(c)) :
					(bIsArray && !cIsArray) ? (l("b is array"),b.concat([c])) :
																		(l("none is array"),[b].concat([c]))
	})
}
console.log(crazy)
console.log( flatten(crazy) )<commit_msg>second try with flatten - test case pass<commit_after>"use strict"

const dimensional = [[1,2,3], ["a", "b", "c"]]
const crazy = [1,2,[3],["a",["b","c"]],["Eloquent"],["JavaScript"]]
var l = console.log
function flatten(array) {
	return array.reduce( (b,c) => {
		let bIsArray = Array.isArray(b),
				cIsArray = Array.isArray(c)
		return (bIsArray && cIsArray) ? (l("both are arrays"),flatten(b.concat(c))) :
					(!bIsArray && cIsArray) ? (l("c is array"),[b].concat(c)) :
					(bIsArray && !cIsArray) ? (l("b is array"),b.concat([c])) :
																		(l("none is array"),[b].concat([c]))
	})
}
console.log(crazy)
console.log( flatten(crazy) )<|endoftext|>


<commit_before>$(document).ready(function(){
    var box = document.getElementsByTagName('a-box');

    var changeColor = function() {
        this.setAttribute('color', 'green');
    };
    var returnColor = function() {
        this.setAttribute('color', '#4CC3D9');
    };

    for (var i = 0; i < box.length; i++) {
        box[i].addEventListener('mouseenter', changeColor, false);
        box[i].addEventListener('mouseleave', returnColor, false);
    }
});<commit_msg>Use hover method to increase code readability<commit_after>$(document).ready(function(){
    /*var box = document.getElementsByTagName('a-box');

    var changeColor = function() {
        this.setAttribute('color', 'green');
    };
    var returnColor = function() {
        this.setAttribute('color', '#4CC3D9');
    };

    for (var i = 0; i < box.length; i++) {
        box[i].addEventListener('mouseenter', changeColor, false);
        box[i].addEventListener('mouseleave', returnColor, false);
    }*/

    $( "a-box" ).hover(
        function(){
            $(this).setAttribute('color', 'green');
        },
        function(){
            this.setAttribute('color', '#4CC3D9');
        }
    );
});<|endoftext|>


<commit_before>/********************
* Seen plugin by jd *
********************/

var db = new sqlite3.Database('config/seen.db', function() {
	db.run("CREATE TABLE if not exists users (userid TEXT, lastonline INTEGER)");
});

function updateSeen (user) {
	var userid = toId(user);
	var now = Date.now();

	db.all("SELECT * FROM users WHERE userid=$userid", {$userid: userid}, function (err,rows) {
    	if (rows.length < 1) {
    		db.run("INSERT INTO users(userid, lastonline) VALUES ($userid, $date)", {$userid: userid, $date: now}, function(err) {
    			if (err) return console.log(err);
    		});
    	} else {
    		db.run("UPDATE users SET lastonline=$date WHERE userid=$userid", {$date: now, $userid: userid}, function(err) {
    			if (err) return console.log(err);
    		});
    	}
    });
}
global.updateSeen = updateSeen;

function lastSeen (user, callback) {
 	var self = this;
	db.all("SELECT * FROM users WHERE userid=$userid", {$userid: toId(user)}, function (err,rows) {
		callback((rows[0] ? rows[0].lastonline : false));
	});
}

exports.commands = {
	seen: function(target, room, user) {
		if (!target) return this.sendReply('Usage: /seen [user]');
		if (toId(target).length > 18) return this.sendReply('/seen - Usernames can\'t be longer than 18 characters.');
		if (!this.canBroadcast()) return;
		if (toId(target) == user.userid) return this.sendReplyBox('Have you looked in a mirror lately?');
		var self = this;
		var targetUser = Users(target);
		var name = target;
		var userid = toId(target);
		if (targetUser && targetUser.connected) return this.sendReplyBox("" + Tools.escapeHTML(targetUser.name) + " is online right now");
		var output = '';

		lastSeen(userid, function(lastOnline) {
			if (!lastOnline) output = "" +
				(targetUser ? Tools.escapeHTML(targetUser.name) : Tools.escapeHTML(name)) + " has never been seen online.";
			var date = new Date(lastOnline);
			if (lastOnline) {
				output = "" +
					(targetUser ? Tools.escapeHTML(targetUser.name) : Tools.escapeHTML(name)) + " was last seen online at " + date.toUTCString() + ".";
				var seconds = Math.floor(((Date.now() - lastOnline) / 1000));
				var minutes = Math.floor((seconds / 60));
				var hours = Math.floor((minutes / 60));
				var days = Math.floor((hours / 24));

				var secondsWord = (((seconds % 60) > 1 || (seconds % 60) == 0) ? 'seconds' : 'second');
				var minutesWord = (((minutes % 60) > 1 || (minutes % 60) == 0) ? 'minutes' : 'minute');
				var hoursWord = ((hours > 1 || hours == 0) ? 'hours' : 'hour');
				var daysWord = ((days === 1) ? 'day' : 'days');

				if (minutes < 1) {
					output += " (" + seconds + " " + secondsWord + " ago)";
				}
				if (minutes > 0 && minutes < 60) {
					output += " (" + minutes + " " + minutesWord + " ago)";
				}
				if (hours > 0 && days < 1) {
					output += " (" + hours + " " + hoursWord + " " + (minutes % 60) + " " + minutesWord + " ago)";
				}
				if (days > 0) {
					output += " (" + days + " " + daysWord + " ago)";
				}

			}
			self.sendReplyBox(output);
			room.update();
		});
	}
}
<commit_msg>Delete seen.js<commit_after><|endoftext|>


<commit_before>const ourshd = async context => {
  context.sendMessageContent.push({
    imageUrl: 'https://imgur.com/ygsx5S3',
    options: {
      reply_markup: {
        inline_keyboard: [
          [
            {
              text: '🆓 PPAV X 奧視免費專區',
              url: `https://www.ppav.xyz/redirect/?url=${encodeURIComponent(
                'http://ourshdtv.com/ad/click?code=1551ef9cbae61a7d089c49979b4fac97'
              )}`,
            },
          ],
        ],
      },
      parse_mode: 'Markdown',
      disable_web_page_preview: false,
      caption: `
PPAV x 奧視 今日免費看

PPAV 獨家取得奧視影片，每日一部，千萬不要錯過！
立刻點擊觀看👇`,
    },
  });
};

export default ourshd;
<commit_msg>Fix ourshd image url (#133)<commit_after>const ourshd = async context => {
  context.sendMessageContent.push({
    imageUrl: 'https://i.imgur.com/ygsx5S3.jpg',
    options: {
      reply_markup: {
        inline_keyboard: [
          [
            {
              text: '🆓 PPAV X 奧視免費專區',
              url: `https://www.ppav.xyz/redirect/?url=${encodeURIComponent(
                'http://ourshdtv.com/ad/click?code=1551ef9cbae61a7d089c49979b4fac97'
              )}`,
            },
          ],
        ],
      },
      parse_mode: 'Markdown',
      disable_web_page_preview: false,
      caption: `
PPAV x 奧視 今日免費看

PPAV 獨家取得奧視影片，每日一部，千萬不要錯過！
立刻點擊觀看👇`,
    },
  });
};

export default ourshd;
<|endoftext|>


<commit_before>'use strict';

const phoneFormatter = require('phone-formatter');
const config = require('../config/config');

/**
 * Verifies new webhook when setting up Messenger app
 *
 * @param req
 * @param res
 */
function verify(req, res) {
    console.log("Receiving webhook verification from FB.");
    var verifyToken = config.facebookVerifyToken;
    if (req.query['hub.verify_token'] === verifyToken) {
        res.send(req.query['hub.challenge']);
    } else {
      res.send('Error, wrong token');
    };
}

/**
 * Makes array of generic request objects from incoming context
 *
 * @param context
 * @return {genericRequests}
 */
function translateRequestToGenericFormats(context) {
  var genericRequests = [];
  // iterate over messaging events - FBM uses batch processing
  var messaging_events = context.userRequest.entry[0].messaging;
  for (let i = 0; i < messaging_events.length; i++) {
    let singleGenericRequest = {
      reqType: '',
      userInput: '',
      context: context
    };
    var event = context.userRequest.entry[0].messaging[i];
    if (event.message && event.message.text) {
      console.log("User input Post detected from FB");
      var userInput = event.message.text;

      if ((userInput === 'help') || (userInput === 'bot help')) {
        singleGenericRequest.userInput = userInput;
        singleGenericRequest.reqType = 'help';
      }
      else if ((userInput === 'hi') || (userInput === 'hello')) {
        singleGenericRequest.userInput = userInput;
        singleGenericRequest.reqType = 'greeting';
      }
      else if (userInput.slice(0, 4) === 'bot ') {
        singleGenericRequest.userInput = userInput.slice(4);
        singleGenericRequest.reqType = 'user-input';
      }
      else {
        singleGenericRequest.userInput = userInput;
        singleGenericRequest.reqType = 'ignore';
      };
    } else if (event.postback) {
      console.log("Postback Post detected from FB");
    // Later: determine if this is triggering a new search, or displaying more Companies - right now just triggers new search
      singleGenericRequest.userInput = event.postback.payload;
      singleGenericRequest.reqType = 'postback';
    } else {
      console.log("Non-text-input Post detected from FB");
      singleGenericRequest.reqType = 'ignore';
    };
    genericRequests.push(singleGenericRequest);
  }
  return genericRequests;
}

/**
 * Takes generic response data and structures payloads for Messenger sending
 *
 * @param genericResponse
 * @return {payloads}
 */
function generateResponsePayloads(genericResponse) {
  // if a False object passed in, pass through false
  if (!genericResponse) {
    return false;
  };

  console.log("About to begin generating payloads from genericResponse.");
  var payloads = [];
  var token = config.facebookAccessToken;
  var url = 'https://graph.facebook.com/v2.6/me/messages';
  var sender = genericResponse.context.userRequest.entry[0].messaging[0].sender.id;
  // var userInput = genericResponse.userInput;
  var type = genericResponse.type;

  // Case: nothing returned from Companies search / junk input
  if (type === 'nothing-found') {
    console.log('No Company Results flag detected in genericResponse.');
    let elements = loadNothingFoundElements();
    payloads.push(makePayload(token, url, sender, elements));
  }
  else if (type === 'help') {
    console.log('Help flag detected in genericResponse.');
    let elements = loadHelpElements();
    payloads.push(makePayload(token, url, sender, elements));
  }
  else if (type === 'greeting') {
    console.log('Greeting flag detected in genericResponse.');
    let elements = loadGreetingElements();
    payloads.push(makePayload(token, url, sender, elements));
  }
  else if (type === 'standard') {
    // Refactor this to compress into another function
    console.log('Standard type flag detected in genericResponse.');
    var name = genericResponse.data.name || '';
    var posts = genericResponse.data.posts || [];
    var contactMethods = genericResponse.data.contactMethods || [];
    var otherCompanies = genericResponse.data.otherCompanies || [];
    var elements = [];

    // load Payload cards for Posts
    if (posts && posts.length) {
      var postElements = loadPostElements(posts, name);
      elements = elements.concat(postElements);
    }

    // load Contact Methods card to Payload
    if (contactMethods) {
      var contactMethodsElements = loadContactMethodsElements(contactMethods, name);
      // only push in if at least one button exists:
      if (contactMethodsElements[0].subtitle || contactMethodsElements[0].buttons.length) {
          elements = elements.concat(contactMethodsElements);
      };
    }

    // make Other Companies Card
    if (otherCompanies && otherCompanies.length) {
      var otherCompaniesElements = loadOtherCompaniesElements(otherCompanies, name);
      elements = elements.concat(otherCompaniesElements);
    }

    // if no other payload loaded up, send a Nothing-Found reponse
    if (!elements.length) {
      let nothingFoundElements = loadNothingFoundElements();
      elements = elements.concat(nothingFoundElements);
    }

    payloads.push(makePayload(token, url, sender, elements));
  }
  console.log('Payloads prepared by Messenger bot: ' + JSON.stringify(payloads));
  return payloads;
}

/**
 * Loads response elements for Case: nothing found
 *
 * @param userInput
 * @return {elements}
 */
function loadNothingFoundElements() {
  return [{
      "title": "I didn\’t understand.",
      "subtitle": 'Please start by telling me the company name. (ex: \'bot verizon\')',
      "buttons": [{
          "type": "web_url",
          "url": "https://gethuman.com",
          "title": "Go to GetHuman"
      }],
  }];
}

/**
 * Loads response elements for Case: Help input
 *
 * @return {elements}
 */
function loadHelpElements() {
  return [{
      "title": "It sounds like you need help.",
      "subtitle": 'Start by saying \'bot\', followed by the company name. (ex: \'bot verizon\')',
      "buttons": [{
          "type": "web_url",
          "url": "https://gethuman.com",
          "title": "Go to GetHuman"
      }],
  }];
}

/**
 * Loads response elements for Case: Help input
 *
 * @return {elements}
 */
function loadHelpElements() {
  return [{
      "title": "Hi there! One of our reps will be with you shortly.",
      "buttons": [{
          "type": "web_url",
          "url": "https://gethuman.com",
          "title": "Go to GetHuman"
      }],
  },
  {
      "title": "Want to talk to our bot?",
      "subtitle": 'Start with \'bot\', followed by the company name. (ex: \'bot verizon\')'
  }];
}

/**
 * Loads response elements for Posts cards
 *
 * @param posts
 * @param name
 * @return {postElements}
 */
function loadPostElements(posts, name) {
  var postElements = [];
  for (let i = 0; i < posts.length; i++) {
    let title = posts[i].title || '';
    let urlId = posts[i].urlId || '';
    let singleElement = {
        "title": title,
        "subtitle": '#' + (i+1) + ' most common ' + name + ' issue',
        "buttons": [{
            "type": "web_url",
            "url": "https://gethuman.com?company=" + encodeURIComponent(name),
            "title": "Fix this issue for me"
        },
        {
            "type": "web_url",
            "url": "https://answers.gethuman.co/_" + encodeURIComponent(urlId),
            "title": "More info ..."
        }],
    };
    postElements.push(singleElement);
  }
  return postElements;
};

/**
 * Loads response elements for Contact Methods cards
 *
 * @param contactMethods
 * @param name
 * @return {contactMethodsElements}
 */
function loadContactMethodsElements(contactMethods, name) {
  var contactMethodsElements = [{
      title: 'Best ways to contact ' + name + ':',
      buttons: formatContactButtons(contactMethods)
  }];
  if (contactMethods.email) {
      contactMethodsElements[0].subtitle = contactMethods.email;
  };
  return contactMethodsElements;
};

/**
 * Loads response elements for Other Companies cards
 *
 * @param otherCompanies
 * @param name
 * @return {otherCompaniesElements}
 */
function loadOtherCompaniesElements(otherCompanies, name) {
  var otherCompaniesElements = [{
      "title": "Were you trying to reach " + name + "?",
      "subtitle": "Perhaps you would like to ask me about this company:",
      "buttons": []
  }];
  var altCompany = otherCompanies[0];
  otherCompaniesElements[0].buttons.push({
      "type": "postback",
      "title": altCompany,
      "payload": altCompany
  });
  otherCompaniesElements[0].buttons.push({
      "type": "web_url",
      "url": "https://gethuman.com/",
      "title": "Something else"
  });
  return otherCompaniesElements;
};

/**
 * Takes contact methods, formats associated buttons for Messenger card
 *
 * @param contactMethods
 * @return {buttons}
 */
function formatContactButtons(contactMethods) {
    var buttons = [];
    var counter = 1;
    for(var key in contactMethods) {
        if ((counter <= 2) && (contactMethods[key])) {
            var button = {};
            switch(key) {
                case 'twitter':
                    button = {
                        "type": "web_url",
                        "url": 'https://twitter.com/' + contactMethods[key],
                        "title": "Twitter"
                    };
                    break;
                case 'web':
                    button = {
                        "type": "web_url",
                        "url": contactMethods[key],
                        "title": "Web"
                    };
                    break;
                case 'chat':
                    button = {
                        "type": "web_url",
                        "url": contactMethods[key],
                        "title": "Chat"
                    };
                    break;
                case 'facebook':
                    button = {
                        "type": "web_url",
                        "url": contactMethods[key],
                        "title": "Facebook"
                    };
                    break;
                case 'phone':
                    button = {
                        "type": "phone_number",
                        "payload": phoneFormatter.format(contactMethods[key], "+1NNNNNNNNNN"),
                        "title": contactMethods[key]
                    };
                    break;
                case 'email':
                    console.log("Email detected, not creating button for it because Messenger won't let us.");
                    break;
                default:
                    console.log("Something went wrong in Facebook contact button formatting");
            }
            if (button.type) {
                buttons.push(button);
                counter += 1;
            }
        }
    }
    console.log("Buttons formatted from contact methods: " + JSON.stringify(buttons));
    return buttons;
}

/**
 * Forms payload for sending to Messenger
 *
 * @param token
 * @param url
 * @param sender
 * @param elements
 * @return {payload}
 */
function makePayload(token, url, sender, elements) {
  return {
    url: url,
    qs: {access_token: token},
    method: 'POST',
    json: {
        recipient: {id: sender},
        message: {
            "attachment": {
                "type": "template",
                "payload": {
                    "template_type": "generic",
                    "elements": elements
                }
            }
        },
    }
  };
};

module.exports = {
  verify: verify,
  translateRequestToGenericFormats: translateRequestToGenericFormats,
  generateResponsePayloads: generateResponsePayloads,
  loadOtherCompaniesElements: loadOtherCompaniesElements,
  loadContactMethodsElements: loadContactMethodsElements,
  loadPostElements: loadPostElements,
  loadNothingFoundElements: loadNothingFoundElements,
  formatContactButtons: formatContactButtons,
  makePayload: makePayload
};<commit_msg>debug<commit_after>'use strict';

const phoneFormatter = require('phone-formatter');
const config = require('../config/config');

/**
 * Verifies new webhook when setting up Messenger app
 *
 * @param req
 * @param res
 */
function verify(req, res) {
    console.log("Receiving webhook verification from FB.");
    var verifyToken = config.facebookVerifyToken;
    if (req.query['hub.verify_token'] === verifyToken) {
        res.send(req.query['hub.challenge']);
    } else {
      res.send('Error, wrong token');
    };
}

/**
 * Makes array of generic request objects from incoming context
 *
 * @param context
 * @return {genericRequests}
 */
function translateRequestToGenericFormats(context) {
  var genericRequests = [];
  // iterate over messaging events - FBM uses batch processing
  var messaging_events = context.userRequest.entry[0].messaging;
  for (let i = 0; i < messaging_events.length; i++) {
    let singleGenericRequest = {
      reqType: '',
      userInput: '',
      context: context
    };
    var event = context.userRequest.entry[0].messaging[i];
    if (event.message && event.message.text) {
      console.log("User input Post detected from FB");
      var userInput = event.message.text;

      if ((userInput === 'help') || (userInput === 'bot help')) {
        singleGenericRequest.userInput = userInput;
        singleGenericRequest.reqType = 'help';
      }
      else if ((userInput === 'hi') || (userInput === 'hello')) {
        singleGenericRequest.userInput = userInput;
        singleGenericRequest.reqType = 'greeting';
      }
      else if (userInput.slice(0, 4) === 'bot ') {
        singleGenericRequest.userInput = userInput.slice(4);
        singleGenericRequest.reqType = 'user-input';
      }
      else {
        singleGenericRequest.userInput = userInput;
        singleGenericRequest.reqType = 'ignore';
      };
    } else if (event.postback) {
      console.log("Postback Post detected from FB");
    // Later: determine if this is triggering a new search, or displaying more Companies - right now just triggers new search
      singleGenericRequest.userInput = event.postback.payload;
      singleGenericRequest.reqType = 'postback';
    } else {
      console.log("Non-text-input Post detected from FB");
      singleGenericRequest.reqType = 'ignore';
    };
    genericRequests.push(singleGenericRequest);
  }
  return genericRequests;
}

/**
 * Takes generic response data and structures payloads for Messenger sending
 *
 * @param genericResponse
 * @return {payloads}
 */
function generateResponsePayloads(genericResponse) {
  // if a False object passed in, pass through false
  if (!genericResponse) {
    return false;
  };

  console.log("About to begin generating payloads from genericResponse.");
  var payloads = [];
  var token = config.facebookAccessToken;
  var url = 'https://graph.facebook.com/v2.6/me/messages';
  var sender = genericResponse.context.userRequest.entry[0].messaging[0].sender.id;
  // var userInput = genericResponse.userInput;
  var type = genericResponse.type;

  // Case: nothing returned from Companies search / junk input
  if (type === 'nothing-found') {
    console.log('No Company Results flag detected in genericResponse.');
    let elements = loadNothingFoundElements();
    payloads.push(makePayload(token, url, sender, elements));
  }
  else if (type === 'help') {
    console.log('Help flag detected in genericResponse.');
    let elements = loadHelpElements();
    payloads.push(makePayload(token, url, sender, elements));
  }
  else if (type === 'greeting') {
    console.log('Greeting flag detected in genericResponse.');
    let elements = loadGreetingElements();
    payloads.push(makePayload(token, url, sender, elements));
  }
  else if (type === 'standard') {
    // Refactor this to compress into another function
    console.log('Standard type flag detected in genericResponse.');
    var name = genericResponse.data.name || '';
    var posts = genericResponse.data.posts || [];
    var contactMethods = genericResponse.data.contactMethods || [];
    var otherCompanies = genericResponse.data.otherCompanies || [];
    var elements = [];

    // load Payload cards for Posts
    if (posts && posts.length) {
      var postElements = loadPostElements(posts, name);
      elements = elements.concat(postElements);
    }

    // load Contact Methods card to Payload
    if (contactMethods) {
      var contactMethodsElements = loadContactMethodsElements(contactMethods, name);
      // only push in if at least one button exists:
      if (contactMethodsElements[0].subtitle || contactMethodsElements[0].buttons.length) {
          elements = elements.concat(contactMethodsElements);
      };
    }

    // make Other Companies Card
    if (otherCompanies && otherCompanies.length) {
      var otherCompaniesElements = loadOtherCompaniesElements(otherCompanies, name);
      elements = elements.concat(otherCompaniesElements);
    }

    // if no other payload loaded up, send a Nothing-Found reponse
    if (!elements.length) {
      let nothingFoundElements = loadNothingFoundElements();
      elements = elements.concat(nothingFoundElements);
    }

    payloads.push(makePayload(token, url, sender, elements));
  }
  console.log('Payloads prepared by Messenger bot: ' + JSON.stringify(payloads));
  return payloads;
}

/**
 * Loads response elements for Case: nothing found
 *
 * @param userInput
 * @return {elements}
 */
function loadNothingFoundElements() {
  return [{
      "title": "I didn\’t understand.",
      "subtitle": 'Please start by telling me the company name. (ex: \'bot verizon\')',
      "buttons": [{
          "type": "web_url",
          "url": "https://gethuman.com",
          "title": "Go to GetHuman"
      }],
  }];
}

/**
 * Loads response elements for Case: Help input
 *
 * @return {elements}
 */
function loadHelpElements() {
  return [{
      "title": "It sounds like you need help.",
      "subtitle": 'Start by saying \'bot\', followed by the company name. (ex: \'bot verizon\')',
      "buttons": [{
          "type": "web_url",
          "url": "https://gethuman.com",
          "title": "Go to GetHuman"
      }],
  }];
}

/**
 * Loads response elements for Case: Help input
 *
 * @return {elements}
 */
function loadGreetingElements() {
  return [{
      "title": "Hi there! One of our reps will be with you shortly.",
      "buttons": [{
          "type": "web_url",
          "url": "https://gethuman.com",
          "title": "Go to GetHuman"
      }],
  },
  {
      "title": "Want to talk to our bot?",
      "subtitle": 'Start with \'bot\', followed by the company name. (ex: \'bot verizon\')'
  }];
}

/**
 * Loads response elements for Posts cards
 *
 * @param posts
 * @param name
 * @return {postElements}
 */
function loadPostElements(posts, name) {
  var postElements = [];
  for (let i = 0; i < posts.length; i++) {
    let title = posts[i].title || '';
    let urlId = posts[i].urlId || '';
    let singleElement = {
        "title": title,
        "subtitle": '#' + (i+1) + ' most common ' + name + ' issue',
        "buttons": [{
            "type": "web_url",
            "url": "https://gethuman.com?company=" + encodeURIComponent(name),
            "title": "Fix this issue for me"
        },
        {
            "type": "web_url",
            "url": "https://answers.gethuman.co/_" + encodeURIComponent(urlId),
            "title": "More info ..."
        }],
    };
    postElements.push(singleElement);
  }
  return postElements;
};

/**
 * Loads response elements for Contact Methods cards
 *
 * @param contactMethods
 * @param name
 * @return {contactMethodsElements}
 */
function loadContactMethodsElements(contactMethods, name) {
  var contactMethodsElements = [{
      title: 'Best ways to contact ' + name + ':',
      buttons: formatContactButtons(contactMethods)
  }];
  if (contactMethods.email) {
      contactMethodsElements[0].subtitle = contactMethods.email;
  };
  return contactMethodsElements;
};

/**
 * Loads response elements for Other Companies cards
 *
 * @param otherCompanies
 * @param name
 * @return {otherCompaniesElements}
 */
function loadOtherCompaniesElements(otherCompanies, name) {
  var otherCompaniesElements = [{
      "title": "Were you trying to reach " + name + "?",
      "subtitle": "Perhaps you would like to ask me about this company:",
      "buttons": []
  }];
  var altCompany = otherCompanies[0];
  otherCompaniesElements[0].buttons.push({
      "type": "postback",
      "title": altCompany,
      "payload": altCompany
  });
  otherCompaniesElements[0].buttons.push({
      "type": "web_url",
      "url": "https://gethuman.com/",
      "title": "Something else"
  });
  return otherCompaniesElements;
};

/**
 * Takes contact methods, formats associated buttons for Messenger card
 *
 * @param contactMethods
 * @return {buttons}
 */
function formatContactButtons(contactMethods) {
    var buttons = [];
    var counter = 1;
    for(var key in contactMethods) {
        if ((counter <= 2) && (contactMethods[key])) {
            var button = {};
            switch(key) {
                case 'twitter':
                    button = {
                        "type": "web_url",
                        "url": 'https://twitter.com/' + contactMethods[key],
                        "title": "Twitter"
                    };
                    break;
                case 'web':
                    button = {
                        "type": "web_url",
                        "url": contactMethods[key],
                        "title": "Web"
                    };
                    break;
                case 'chat':
                    button = {
                        "type": "web_url",
                        "url": contactMethods[key],
                        "title": "Chat"
                    };
                    break;
                case 'facebook':
                    button = {
                        "type": "web_url",
                        "url": contactMethods[key],
                        "title": "Facebook"
                    };
                    break;
                case 'phone':
                    button = {
                        "type": "phone_number",
                        "payload": phoneFormatter.format(contactMethods[key], "+1NNNNNNNNNN"),
                        "title": contactMethods[key]
                    };
                    break;
                case 'email':
                    console.log("Email detected, not creating button for it because Messenger won't let us.");
                    break;
                default:
                    console.log("Something went wrong in Facebook contact button formatting");
            }
            if (button.type) {
                buttons.push(button);
                counter += 1;
            }
        }
    }
    console.log("Buttons formatted from contact methods: " + JSON.stringify(buttons));
    return buttons;
}

/**
 * Forms payload for sending to Messenger
 *
 * @param token
 * @param url
 * @param sender
 * @param elements
 * @return {payload}
 */
function makePayload(token, url, sender, elements) {
  return {
    url: url,
    qs: {access_token: token},
    method: 'POST',
    json: {
        recipient: {id: sender},
        message: {
            "attachment": {
                "type": "template",
                "payload": {
                    "template_type": "generic",
                    "elements": elements
                }
            }
        },
    }
  };
};

module.exports = {
  verify: verify,
  translateRequestToGenericFormats: translateRequestToGenericFormats,
  generateResponsePayloads: generateResponsePayloads,
  loadOtherCompaniesElements: loadOtherCompaniesElements,
  loadContactMethodsElements: loadContactMethodsElements,
  loadPostElements: loadPostElements,
  loadNothingFoundElements: loadNothingFoundElements,
  formatContactButtons: formatContactButtons,
  makePayload: makePayload
};<|endoftext|>


<commit_before>const encoder = require('./encoder');
const signatures = require('./signatures');
const transform = require('./transform');
const sizes = require('./sizes');

class Writer {

    _setIntegerFrame(name, value) {
        const integer = parseInt(value, 10);

        this.frames.push({
            name,
            value: integer,
            size: sizes.getNumericFrameSize(integer.toString().length)
        });
    }

    _setStringFrame(name, value) {
        const stringValue = value.toString();

        this.frames.push({
            name,
            value: stringValue,
            size: sizes.getStringFrameSize(stringValue.length)
        });
    }

    _setPictureFrame(name, buffer) {
        const mimeType = signatures.getMimeType(new Uint8Array(buffer), 0, 12);

        if (!mimeType) {
            throw new Error('Unknown picture MIME type');
        }
        this.frames.push({
            name,
            value: buffer,
            mimeType,
            size: sizes.getPictureFrameSize(buffer.byteLength, mimeType.length)
        });
    }

    _setLyricsFrame(name, lyrics) {
        const stringValue = lyrics.toString();

        this.frames.push({
            name,
            value: stringValue,
            size: sizes.getLyricsFrameSize(stringValue.length)
        });
    }

    constructor(buffer) {
        if (!buffer || typeof buffer !== 'object' || !('byteLength' in buffer)) {
            throw new Error('First argument should be an instance of ArrayBuffer or Buffer');
        }

        this.arrayBuffer = buffer;
        this.padding = 4096;
        this.frames = [];
        this.url = '';
    }

    setFrame(frameName, frameValue) {
        switch (frameName) {
            case 'TPE1': // song artists
            case 'TCOM': // song composers
            {
                if (!Array.isArray(frameValue)) {
                    throw new Error(`${frameName} frame value should be an array of strings`);
                }
                const artists = frameValue
                    .map((artist) => artist.toString())
                    .join('/');

                this._setStringFrame(frameName, artists);
                break;
            }
            case 'TCON': // song genre
            {
                if (!Array.isArray(frameValue)) {
                    throw new Error(`${frameName} frame value should be an array of strings`);
                }
                const genres = frameValue
                    .map((frame) => frame.toString())
                    .join(';');

                this._setStringFrame(frameName, genres);
                break;
            }
            case 'TIT2': // song title
            case 'TALB': // album title
            case 'TPE2': // album artist // spec doesn't say anything about separator, so it is a string, not array
            case 'TRCK': // song number in album: 5 or 5/10
            case 'TPOS': // album disc number: 1 or 1/3
            case 'TPUB': // label name
            {
                this._setStringFrame(frameName, frameValue);
                break;
            }
            case 'TLEN': // song duration
            case 'TYER': // album release year
            {
                this._setIntegerFrame(frameName, frameValue);
                break;
            }
            case 'USLT': // unsychronised lyrics
            {
                this._setLyricsFrame(frameName, frameValue);
                break;
            }
            case 'APIC': // song cover
            {
                if (typeof frameValue !== 'object' || !('byteLength' in frameValue)) {
                    throw new Error('APIC frame value should be an instance of ArrayBuffer or Buffer');
                }
                this._setPictureFrame(frameName, frameValue);
                break;
            }
            default:
            {
                throw new Error(`Unsupported frame ${frameName}`);
            }
        }
        return this;
    }

    removeTag() {
        const headerLength = 10;
        const bufferLength = this.arrayBuffer.byteLength;

        if (bufferLength < headerLength) {
            return;
        }
        const firstTenBytes = new Uint8Array(this.arrayBuffer, 0, headerLength);
        const version = firstTenBytes[3];
        const tagSize = transform.uint7ArrayToUint28([
                firstTenBytes[6], firstTenBytes[7],
                firstTenBytes[8], firstTenBytes[9]
            ]) + headerLength;

        if (!signatures.isId3v2(firstTenBytes) || version < 2 || version > 4) {
            return;
        }

        this.arrayBuffer = this.arrayBuffer.slice(tagSize);
    }

    addTag() {
        this.removeTag();

        const BOM = [0xff, 0xfe];
        const headerSize = 10;
        const totalFrameSize = sizes.getTotalFrameSize(this.frames);
        const totalTagSize = headerSize + totalFrameSize + this.padding;
        const buffer = new ArrayBuffer(this.arrayBuffer.byteLength + totalTagSize);
        const bufferWriter = new Uint8Array(buffer);

        let offset = 0;
        let writeBytes = [];

        writeBytes = [0x49, 0x44, 0x33, 3]; // ID3 tag and version
        bufferWriter.set(writeBytes, offset);
        offset += writeBytes.length;

        offset++; // version revision
        offset++; // flags

        writeBytes = transform.uint28ToUint7Array(totalTagSize - headerSize); // tag size (without header)
        bufferWriter.set(writeBytes, offset);
        offset += writeBytes.length;

        this.frames.forEach((frame) => {
            writeBytes = encoder.encodeUtf8Ascii(frame.name); // frame name
            bufferWriter.set(writeBytes, offset);
            offset += writeBytes.length;

            writeBytes = transform.uint32ToUint8Array(frame.size - headerSize); // frame size (without header)
            bufferWriter.set(writeBytes, offset);
            offset += writeBytes.length;

            offset += 2; // flags

            switch (frame.name) {
                case 'TPE1':
                case 'TCOM':
                case 'TCON':
                case 'TIT2':
                case 'TALB':
                case 'TPE2':
                case 'TRCK':
                case 'TPOS':
                case 'TPUB':
                {
                    writeBytes = [1].concat(BOM); // encoding, BOM
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    writeBytes = encoder.encodeUtf16le(frame.value); // frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;
                    break;
                }
                case 'USLT':
                {
                    const langEng = [101, 110, 103];

                    writeBytes = [1].concat(langEng, BOM); // encoding, language, BOM for content descriptor
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    offset += 2; // content descriptor

                    writeBytes = BOM; // BOM for frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    writeBytes = encoder.encodeUtf16le(frame.value); // frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;
                    break;
                }
                case 'TLEN':
                case 'TYER':
                {
                    offset++; // encoding

                    writeBytes = encoder.encodeUtf8Ascii(frame.value); // frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;
                    break;
                }
                case 'APIC':
                {
                    offset++; // encoding

                    writeBytes = encoder.encodeUtf8Ascii(frame.mimeType); // MIME type
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    writeBytes = [0, 3, 0]; // delemiter, pic type, delemiter
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    bufferWriter.set(new Uint8Array(frame.value), offset); // picture content
                    offset += frame.value.byteLength;
                    break;
                }
            }
        });

        offset += this.padding; // free space for rewriting
        bufferWriter.set(new Uint8Array(this.arrayBuffer), offset);
        this.arrayBuffer = buffer;
        return buffer;
    }

    getBlob() {
        return new Blob([this.arrayBuffer], {type: 'audio/mpeg'});
    }

    getURL() {
        if (!this.url) {
            this.url = URL.createObjectURL(this.getBlob());
        }
        return this.url;
    }

    revokeURL() {
        URL.revokeObjectURL(this.url);
    }

}

module.exports = Writer;
<commit_msg>refactor<commit_after>const encoder = require('./encoder');
const signatures = require('./signatures');
const transform = require('./transform');
const sizes = require('./sizes');

class Writer {

    _setIntegerFrame(name, value) {
        const integer = parseInt(value, 10);

        this.frames.push({
            name,
            value: integer,
            size: sizes.getNumericFrameSize(integer.toString().length)
        });
    }

    _setStringFrame(name, value) {
        const stringValue = value.toString();

        this.frames.push({
            name,
            value: stringValue,
            size: sizes.getStringFrameSize(stringValue.length)
        });
    }

    _setPictureFrame(name, buffer) {
        const mimeType = signatures.getMimeType(new Uint8Array(buffer), 0, 12);

        if (!mimeType) {
            throw new Error('Unknown picture MIME type');
        }
        this.frames.push({
            name,
            value: buffer,
            mimeType,
            size: sizes.getPictureFrameSize(buffer.byteLength, mimeType.length)
        });
    }

    _setLyricsFrame(name, lyrics) {
        const stringValue = lyrics.toString();

        this.frames.push({
            name,
            value: stringValue,
            size: sizes.getLyricsFrameSize(stringValue.length)
        });
    }

    constructor(buffer) {
        if (!buffer || typeof buffer !== 'object' || !('byteLength' in buffer)) {
            throw new Error('First argument should be an instance of ArrayBuffer or Buffer');
        }

        this.arrayBuffer = buffer;
        this.padding = 4096;
        this.frames = [];
        this.url = '';
    }

    setFrame(frameName, frameValue) {
        switch (frameName) {
            case 'TPE1': // song artists
            case 'TCOM': { // song composers
                if (!Array.isArray(frameValue)) {
                    throw new Error(`${frameName} frame value should be an array of strings`);
                }
                const artists = frameValue
                    .map((artist) => artist.toString())
                    .join('/');

                this._setStringFrame(frameName, artists);
                break;
            }
            case 'TCON': { // song genre
                if (!Array.isArray(frameValue)) {
                    throw new Error(`${frameName} frame value should be an array of strings`);
                }
                const genres = frameValue
                    .map((frame) => frame.toString())
                    .join(';');

                this._setStringFrame(frameName, genres);
                break;
            }
            case 'TIT2': // song title
            case 'TALB': // album title
            case 'TPE2': // album artist // spec doesn't say anything about separator, so it is a string, not array
            case 'TRCK': // song number in album: 5 or 5/10
            case 'TPOS': // album disc number: 1 or 1/3
            case 'TPUB': { // label name
                this._setStringFrame(frameName, frameValue);
                break;
            }
            case 'TLEN': // song duration
            case 'TYER': { // album release year
                this._setIntegerFrame(frameName, frameValue);
                break;
            }
            case 'USLT': { // unsychronised lyrics
                this._setLyricsFrame(frameName, frameValue);
                break;
            }
            case 'APIC': { // song cover
                if (typeof frameValue !== 'object' || !('byteLength' in frameValue)) {
                    throw new Error('APIC frame value should be an instance of ArrayBuffer or Buffer');
                }
                this._setPictureFrame(frameName, frameValue);
                break;
            }
            default: {
                throw new Error(`Unsupported frame ${frameName}`);
            }
        }
        return this;
    }

    removeTag() {
        const headerLength = 10;
        const bufferLength = this.arrayBuffer.byteLength;

        if (bufferLength < headerLength) {
            return;
        }
        const firstTenBytes = new Uint8Array(this.arrayBuffer, 0, headerLength);
        const version = firstTenBytes[3];
        const tagSize = transform.uint7ArrayToUint28([
                firstTenBytes[6], firstTenBytes[7],
                firstTenBytes[8], firstTenBytes[9]
            ]) + headerLength;

        if (!signatures.isId3v2(firstTenBytes) || version < 2 || version > 4) {
            return;
        }

        this.arrayBuffer = this.arrayBuffer.slice(tagSize);
    }

    addTag() {
        this.removeTag();

        const BOM = [0xff, 0xfe];
        const headerSize = 10;
        const totalFrameSize = sizes.getTotalFrameSize(this.frames);
        const totalTagSize = headerSize + totalFrameSize + this.padding;
        const buffer = new ArrayBuffer(this.arrayBuffer.byteLength + totalTagSize);
        const bufferWriter = new Uint8Array(buffer);

        let offset = 0;
        let writeBytes = [];

        writeBytes = [0x49, 0x44, 0x33, 3]; // ID3 tag and version
        bufferWriter.set(writeBytes, offset);
        offset += writeBytes.length;

        offset++; // version revision
        offset++; // flags

        writeBytes = transform.uint28ToUint7Array(totalTagSize - headerSize); // tag size (without header)
        bufferWriter.set(writeBytes, offset);
        offset += writeBytes.length;

        this.frames.forEach((frame) => {
            writeBytes = encoder.encodeUtf8Ascii(frame.name); // frame name
            bufferWriter.set(writeBytes, offset);
            offset += writeBytes.length;

            writeBytes = transform.uint32ToUint8Array(frame.size - headerSize); // frame size (without header)
            bufferWriter.set(writeBytes, offset);
            offset += writeBytes.length;

            offset += 2; // flags

            switch (frame.name) {
                case 'TPE1':
                case 'TCOM':
                case 'TCON':
                case 'TIT2':
                case 'TALB':
                case 'TPE2':
                case 'TRCK':
                case 'TPOS':
                case 'TPUB': {
                    writeBytes = [1].concat(BOM); // encoding, BOM
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    writeBytes = encoder.encodeUtf16le(frame.value); // frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;
                    break;
                }
                case 'USLT': {
                    const langEng = [101, 110, 103];

                    writeBytes = [1].concat(langEng, BOM); // encoding, language, BOM for content descriptor
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    offset += 2; // content descriptor

                    writeBytes = BOM; // BOM for frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    writeBytes = encoder.encodeUtf16le(frame.value); // frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;
                    break;
                }
                case 'TLEN':
                case 'TYER': {
                    offset++; // encoding

                    writeBytes = encoder.encodeUtf8Ascii(frame.value); // frame value
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;
                    break;
                }
                case 'APIC': {
                    offset++; // encoding

                    writeBytes = encoder.encodeUtf8Ascii(frame.mimeType); // MIME type
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    writeBytes = [0, 3, 0]; // delemiter, pic type, delemiter
                    bufferWriter.set(writeBytes, offset);
                    offset += writeBytes.length;

                    bufferWriter.set(new Uint8Array(frame.value), offset); // picture content
                    offset += frame.value.byteLength;
                    break;
                }
            }
        });

        offset += this.padding; // free space for rewriting
        bufferWriter.set(new Uint8Array(this.arrayBuffer), offset);
        this.arrayBuffer = buffer;
        return buffer;
    }

    getBlob() {
        return new Blob([this.arrayBuffer], {type: 'audio/mpeg'});
    }

    getURL() {
        if (!this.url) {
            this.url = URL.createObjectURL(this.getBlob());
        }
        return this.url;
    }

    revokeURL() {
        URL.revokeObjectURL(this.url);
    }

}

module.exports = Writer;
<|endoftext|>