<commit_before>import THREE from 'three';

import ADT from '../../pipeline/adt';
import Chunk from '../../pipeline/adt/chunk';
import DBC from '../../pipeline/dbc';
import M2 from '../../pipeline/m2';
import WDT from '../../pipeline/wdt';
import WMO from '../../pipeline/wmo';

class WorldMap extends THREE.Group {

  static ZEROPOINT = ADT.SIZE * 32;

  static CHUNKS_PER_ROW = 64 * 16;
  static CHUNK_RENDER_RADIUS = 10;

  constructor(data, wdt) {
    super();

    this.data = data;
    this.wdt = wdt;

    this.mapID = this.data.id;
    this.chunkX = null;
    this.chunkY = null;

    this.queuedChunks = new Map();
    this.chunks = new Map();

    this.queuedDoodads = new Map();
    this.doodads = new Map();

    this.queuedWMOs = new Map();
    this.wmos = new Map();
  }

  get internalName() {
    return this.data.internalName;
  }

  render(x, y) {
    const chunkX = Chunk.chunkFor(x);
    const chunkY = Chunk.chunkFor(y);

    if (this.chunkX === chunkX && this.chunkY === chunkY) {
      return;
    }

    this.chunkX = chunkX;
    this.chunkY = chunkY;

    const radius = this.constructor.CHUNK_RENDER_RADIUS;
    const indices = this.chunkIndicesAround(chunkX, chunkY, radius);

    indices.forEach((index) => {
      this.loadChunkByIndex(index);
    });

    this.chunks.forEach((_chunk, index) => {
      if (indices.indexOf(index) === -1) {
        this.unloadChunkByIndex(index);
      }
    });
  }

  chunkIndicesAround(chunkX, chunkY, radius) {
    const perRow = this.constructor.CHUNKS_PER_ROW;
    const sqrt = Math.sqrt;

    const base = this.indexFor(chunkX, chunkY);
    const indices = [];

    for (let y = -radius; y <= radius; ++y) {
      for (let x = -radius; x <= radius; ++x) {
        const distance = sqrt(y ** 2 + x ** 2);
        if (distance <= radius) {
          indices.push(base + y * perRow + x);
        }
      }
    }

    return indices;
  }

  loadChunkByIndex(index) {
    if (this.queuedChunks.has(index)) {
      return;
    }

    const perRow = this.constructor.CHUNKS_PER_ROW;
    const chunkX = (index / perRow) | 0;
    const chunkY = index % perRow;

    this.queuedChunks.set(index, Chunk.load(this, chunkX, chunkY).then((chunk) => {
      this.chunks.set(index, chunk);

      chunk.doodadEntries.forEach((entry) => {
        this.loadDoodad(entry);
      });

      chunk.wmoEntries.forEach((entry) => {
        this.loadWMO(entry);
      });

      this.add(chunk);
    }));
  }

  unloadChunkByIndex(index) {
    const chunk = this.chunks.get(index);
    if (!chunk) {
      return;
    }

    // TODO: Unload doodads and WMOs
    this.queuedChunks.delete(index);
    this.chunks.delete(index);
    this.remove(chunk);
  }

  indexFor(chunkX, chunkY) {
    return chunkX * 64 * 16 + chunkY;
  }

  loadDoodad(entry) {
    if (this.queuedDoodads.has(entry.id)) {
      return;
    }

    this.queuedDoodads.set(entry.id, M2.load(entry.filename).then((m2) => {
      m2.position.set(
        -(entry.position.z - this.constructor.ZEROPOINT),
        -(entry.position.x - this.constructor.ZEROPOINT),
        entry.position.y
      );

      m2.rotation.set(
        entry.rotation.x * Math.PI / 180,
        -entry.rotation.z * Math.PI / 180,
        entry.rotation.y * Math.PI / 180
      );

      if (entry.scale !== 1024) {
        const scale = entry.scale / 1024;
        m2.scale.set(scale, scale, scale);
      }

      this.add(m2);

      // TODO: Remove doodad from map on unload
      this.doodads.set(entry.id, m2);

      // Auto-play animation index 0 in doodad, if animations are present
      // TODO: Properly manage doodad animations
      if (m2.isAnimated && m2.animations.length > 0) {
        m2.animations.play(0);
      }
    }));
  }

  loadWMO(entry) {
    if (this.queuedWMOs.has(entry.id)) {
      return;
    }

    this.queuedWMOs.set(entry.id, WMO.load(entry.filename).then((wmo) => {
      wmo.position.set(
        -(entry.position.z - this.constructor.ZEROPOINT),
        -(entry.position.x - this.constructor.ZEROPOINT),
        entry.position.y
      );

      wmo.doodadSet = entry.doodadSet;

      // Provided as (X, Z, -Y)
      wmo.rotation.set(
        entry.rotation.x * Math.PI / 180,
        -entry.rotation.z * Math.PI / 180,
        entry.rotation.y * Math.PI / 180
      );

      this.add(wmo);

      // TODO: Remove WMO from map on unload
      this.wmos.set(entry.id, wmo);
    }));
  }

  animate(delta, camera, cameraRotated) {
    this.animateDoodads(delta, camera, cameraRotated);
  }

  animateDoodads(delta, camera, cameraRotated) {
    this.doodads.forEach((doodad) => {
      if (!doodad.isAnimated) {
        return;
      }

      if (doodad.animations.length > 0) {
        doodad.animations.update(delta);
      }

      if (cameraRotated && doodad.billboards.length > 0) {
        doodad.applyBillboards(camera);
      }

      if (doodad.skeletonHelper) {
        doodad.skeletonHelper.update();
      }
    });
  }

  static load(id) {
    return DBC.load('Map', id).then((data) => {
      const { internalName: name } = data;
      return WDT.load(`World\\Maps\\${name}\\${name}.wdt`).then((wdt) => {
        return new this(data, wdt);
      });
    });
  }

}

export default WorldMap;
<commit_msg>Render square radius for now<commit_after>import THREE from 'three';

import ADT from '../../pipeline/adt';
import Chunk from '../../pipeline/adt/chunk';
import DBC from '../../pipeline/dbc';
import M2 from '../../pipeline/m2';
import WDT from '../../pipeline/wdt';
import WMO from '../../pipeline/wmo';

class WorldMap extends THREE.Group {

  static ZEROPOINT = ADT.SIZE * 32;

  static CHUNKS_PER_ROW = 64 * 16;
  static CHUNK_RENDER_RADIUS = 10;

  constructor(data, wdt) {
    super();

    this.data = data;
    this.wdt = wdt;

    this.mapID = this.data.id;
    this.chunkX = null;
    this.chunkY = null;

    this.queuedChunks = new Map();
    this.chunks = new Map();

    this.queuedDoodads = new Map();
    this.doodads = new Map();

    this.queuedWMOs = new Map();
    this.wmos = new Map();
  }

  get internalName() {
    return this.data.internalName;
  }

  render(x, y) {
    const chunkX = Chunk.chunkFor(x);
    const chunkY = Chunk.chunkFor(y);

    if (this.chunkX === chunkX && this.chunkY === chunkY) {
      return;
    }

    this.chunkX = chunkX;
    this.chunkY = chunkY;

    const radius = this.constructor.CHUNK_RENDER_RADIUS;
    const indices = this.chunkIndicesAround(chunkX, chunkY, radius);

    indices.forEach((index) => {
      this.loadChunkByIndex(index);
    });

    this.chunks.forEach((_chunk, index) => {
      if (indices.indexOf(index) === -1) {
        this.unloadChunkByIndex(index);
      }
    });
  }

  chunkIndicesAround(chunkX, chunkY, radius) {
    const perRow = this.constructor.CHUNKS_PER_ROW;

    const base = this.indexFor(chunkX, chunkY);
    const indices = [];

    for (let y = -radius; y <= radius; ++y) {
      for (let x = -radius; x <= radius; ++x) {
        indices.push(base + y * perRow + x);
      }
    }

    return indices;
  }

  loadChunkByIndex(index) {
    if (this.queuedChunks.has(index)) {
      return;
    }

    const perRow = this.constructor.CHUNKS_PER_ROW;
    const chunkX = (index / perRow) | 0;
    const chunkY = index % perRow;

    this.queuedChunks.set(index, Chunk.load(this, chunkX, chunkY).then((chunk) => {
      this.chunks.set(index, chunk);

      chunk.doodadEntries.forEach((entry) => {
        this.loadDoodad(entry);
      });

      chunk.wmoEntries.forEach((entry) => {
        this.loadWMO(entry);
      });

      this.add(chunk);
    }));
  }

  unloadChunkByIndex(index) {
    const chunk = this.chunks.get(index);
    if (!chunk) {
      return;
    }

    // TODO: Unload doodads and WMOs
    this.queuedChunks.delete(index);
    this.chunks.delete(index);
    this.remove(chunk);
  }

  indexFor(chunkX, chunkY) {
    return chunkX * 64 * 16 + chunkY;
  }

  loadDoodad(entry) {
    if (this.queuedDoodads.has(entry.id)) {
      return;
    }

    this.queuedDoodads.set(entry.id, M2.load(entry.filename).then((m2) => {
      m2.position.set(
        -(entry.position.z - this.constructor.ZEROPOINT),
        -(entry.position.x - this.constructor.ZEROPOINT),
        entry.position.y
      );

      m2.rotation.set(
        entry.rotation.x * Math.PI / 180,
        -entry.rotation.z * Math.PI / 180,
        entry.rotation.y * Math.PI / 180
      );

      if (entry.scale !== 1024) {
        const scale = entry.scale / 1024;
        m2.scale.set(scale, scale, scale);
      }

      this.add(m2);

      // TODO: Remove doodad from map on unload
      this.doodads.set(entry.id, m2);

      // Auto-play animation index 0 in doodad, if animations are present
      // TODO: Properly manage doodad animations
      if (m2.isAnimated && m2.animations.length > 0) {
        m2.animations.play(0);
      }
    }));
  }

  loadWMO(entry) {
    if (this.queuedWMOs.has(entry.id)) {
      return;
    }

    this.queuedWMOs.set(entry.id, WMO.load(entry.filename).then((wmo) => {
      wmo.position.set(
        -(entry.position.z - this.constructor.ZEROPOINT),
        -(entry.position.x - this.constructor.ZEROPOINT),
        entry.position.y
      );

      wmo.doodadSet = entry.doodadSet;

      // Provided as (X, Z, -Y)
      wmo.rotation.set(
        entry.rotation.x * Math.PI / 180,
        -entry.rotation.z * Math.PI / 180,
        entry.rotation.y * Math.PI / 180
      );

      this.add(wmo);

      // TODO: Remove WMO from map on unload
      this.wmos.set(entry.id, wmo);
    }));
  }

  animate(delta, camera, cameraRotated) {
    this.animateDoodads(delta, camera, cameraRotated);
  }

  animateDoodads(delta, camera, cameraRotated) {
    this.doodads.forEach((doodad) => {
      if (!doodad.isAnimated) {
        return;
      }

      if (doodad.animations.length > 0) {
        doodad.animations.update(delta);
      }

      if (cameraRotated && doodad.billboards.length > 0) {
        doodad.applyBillboards(camera);
      }

      if (doodad.skeletonHelper) {
        doodad.skeletonHelper.update();
      }
    });
  }

  static load(id) {
    return DBC.load('Map', id).then((data) => {
      const { internalName: name } = data;
      return WDT.load(`World\\Maps\\${name}\\${name}.wdt`).then((wdt) => {
        return new this(data, wdt);
      });
    });
  }

}

export default WorldMap;
<|endoftext|>


<commit_before>/*
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {_t, _td} from '../../../languageHandler';
import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";

const widgetType = 'm.stickerpicker';

// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
const STICKERPICKER_Z_INDEX = 3500;

// Key to store the widget's AppTile under in PersistedElement
const PERSISTED_ELEMENT_KEY = "stickerPicker";

export default class Stickerpicker extends React.Component {
    static currentWidget;

    constructor(props) {
        super(props);
        this._onShowStickersClick = this._onShowStickersClick.bind(this);
        this._onHideStickersClick = this._onHideStickersClick.bind(this);
        this._launchManageIntegrations = this._launchManageIntegrations.bind(this);
        this._removeStickerpickerWidgets = this._removeStickerpickerWidgets.bind(this);
        this._updateWidget = this._updateWidget.bind(this);
        this._onWidgetAction = this._onWidgetAction.bind(this);
        this._onResize = this._onResize.bind(this);
        this._onFinished = this._onFinished.bind(this);

        this.popoverWidth = 300;
        this.popoverHeight = 300;

        // This is loaded by _acquireScalarClient on an as-needed basis.
        this.scalarClient = null;

        this.state = {
            showStickers: false,
            imError: null,
            stickerpickerX: null,
            stickerpickerY: null,
            stickerpickerWidget: null,
            widgetId: null,
        };
    }

    _acquireScalarClient() {
        if (this.scalarClient) return Promise.resolve(this.scalarClient);
        // TODO: Pick the right manager for the widget
        if (IntegrationManagers.sharedInstance().hasManager()) {
            this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
            return this.scalarClient.connect().then(() => {
                this.forceUpdate();
                return this.scalarClient;
            }).catch((e) => {
                this._imError(_td("Failed to connect to integration manager"), e);
            });
        } else {
            IntegrationManagers.sharedInstance().openNoManagerDialog();
        }
    }

    async _removeStickerpickerWidgets() {
        const scalarClient = await this._acquireScalarClient();
        console.warn('Removing Stickerpicker widgets');
        if (this.state.widgetId) {
            if (scalarClient) {
                scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
                    console.warn('Assets disabled');
                }).catch((err) => {
                    console.error('Failed to disable assets');
                });
            } else {
                console.error("Cannot disable assets: no scalar client");
            }
        } else {
            console.warn('No widget ID specified, not disabling assets');
        }

        this.setState({showStickers: false});
        WidgetUtils.removeStickerpickerWidgets().then(() => {
            this.forceUpdate();
        }).catch((e) => {
            console.error('Failed to remove sticker picker widget', e);
        });
    }

    componentDidMount() {
        // Close the sticker picker when the window resizes
        window.addEventListener('resize', this._onResize);

        this.dispatcherRef = dis.register(this._onWidgetAction);

        // Track updates to widget state in account data
        MatrixClientPeg.get().on('accountData', this._updateWidget);

        // Initialise widget state from current account data
        this._updateWidget();
    }

    componentWillUnmount() {
        const client = MatrixClientPeg.get();
        if (client) client.removeListener('accountData', this._updateWidget);

        window.removeEventListener('resize', this._onResize);
        if (this.dispatcherRef) {
            dis.unregister(this.dispatcherRef);
        }
    }

    componentDidUpdate(prevProps, prevState) {
        this._sendVisibilityToWidget(this.state.showStickers);
    }

    _imError(errorMsg, e) {
        console.error(errorMsg, e);
        this.setState({
            showStickers: false,
            imError: _t(errorMsg),
        });
    }

    _updateWidget() {
        const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
        if (!stickerpickerWidget) {
            Stickerpicker.currentWidget = null;
            this.setState({stickerpickerWidget: null, widgetId: null});
            return;
        }

        const currentWidget = Stickerpicker.currentWidget;
        let currentUrl = null;
        if (currentWidget && currentWidget.content && currentWidget.content.url) {
            currentUrl = currentWidget.content.url;
        }

        let newUrl = null;
        if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
            newUrl = stickerpickerWidget.content.url;
        }

        if (newUrl !== currentUrl) {
            // Destroy the existing frame so a new one can be created
            PersistedElement.destroyElement(PERSISTED_ELEMENT_KEY);
        }

        Stickerpicker.currentWidget = stickerpickerWidget;
        this.setState({
            stickerpickerWidget,
            widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
        });
    }

    _onWidgetAction(payload) {
        switch (payload.action) {
            case "user_widget_updated":
                this.forceUpdate();
                break;
            case "stickerpicker_close":
                this.setState({showStickers: false});
                break;
            case "show_right_panel":
            case "hide_right_panel":
            case "show_left_panel":
            case "hide_left_panel":
                this.setState({showStickers: false});
                break;
        }
    }

    _defaultStickerpickerContent() {
        return (
            <AccessibleButton onClick={this._launchManageIntegrations}
                className='mx_Stickers_contentPlaceholder'>

{ _t("You don't currently have any stickerpacks enabled") }

{ _t("Add some now") }

            </AccessibleButton>
        );
    }

    _errorStickerpickerContent() {
        return (

 { this.state.imError }

        );
    }

    _sendVisibilityToWidget(visible) {
        if (!this.state.stickerpickerWidget) return;
        const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
        if (widgetMessaging && visible !== this._prevSentVisibility) {
            widgetMessaging.sendVisibility(visible);
            this._prevSentVisibility = visible;
        }
    }

    _getStickerpickerContent() {
        // Handle Integration Manager errors
        if (this.state._imError) {
            return this._errorStickerpickerContent();
        }

        // Stickers
        // TODO - Add support for Stickerpickers from multiple app stores.
        // Render content from multiple stickerpack sources, each within their
        // own iframe, within the stickerpicker UI element.
        const stickerpickerWidget = this.state.stickerpickerWidget;
        let stickersContent;

        // Use a separate ReactDOM tree to render the AppTile separately so that it persists and does
        // not unmount when we (a) close the sticker picker (b) switch rooms. It's properties are still
        // updated.
        const PersistedElement = sdk.getComponent("elements.PersistedElement");

        // Load stickerpack content
        if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
            // Set default name
            stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");

            stickersContent = (

                    <PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
                        <AppTile
                            id={stickerpickerWidget.id}
                            url={stickerpickerWidget.content.url}
                            name={stickerpickerWidget.content.name}
                            room={this.props.room}
                            type={stickerpickerWidget.content.type}
                            fullWidth={true}
                            userId={MatrixClientPeg.get().credentials.userId}
                            creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
                            waitForIframeLoad={true}
                            show={true}
                            showMenubar={true}
                            onEditClick={this._launchManageIntegrations}
                            onDeleteClick={this._removeStickerpickerWidgets}
                            showTitle={false}
                            showMinimise={true}
                            showDelete={false}
                            showCancel={false}
                            showPopout={false}
                            onMinimiseClick={this._onHideStickersClick}
                            handleMinimisePointerEvents={true}
                            whitelistCapabilities={['m.sticker', 'visibility']}
                            userWidget={true}
                        />
                    </PersistedElement>

            );
        } else {
            // Default content to show if stickerpicker widget not added
            stickersContent = this._defaultStickerpickerContent();
        }
        return stickersContent;
    }

    /**
     * Show the sticker picker overlay
     * If no stickerpacks have been added, show a link to the integration manager add sticker packs page.
     * @param  {Event} e Event that triggered the function
     * @returns Nothing of use when the thing happens.
     */
    _onShowStickersClick(e) {
        if (!SettingsStore.getValue("integrationProvisioning")) {
            // Intercept this case and spawn a warning.
            return IntegrationManagers.sharedInstance().showDisabledDialog();
        }

        // XXX: Simplify by using a context menu that is positioned relative to the sticker picker button

        const buttonRect = e.target.getBoundingClientRect();

        // The window X and Y offsets are to adjust position when zoomed in to page
        let x = buttonRect.right + window.pageXOffset - 41;

        // Amount of horizontal space between the right of menu and the right of the viewport
        //  (10 = amount needed to make chevron centrally aligned)
        const rightPad = 10;

        // When the sticker picker would be displayed off of the viewport, adjust x
        //  (302 = width of context menu, including borders)
        x = Math.min(x, document.body.clientWidth - (302 + rightPad));

        // Offset the chevron location, which is relative to the left of the context menu
        //  (10 = offset when context menu would not be displayed off viewport)
        //  (8 = value required in practice (possibly 10 - 2 where the 2 = context menu borders)
        const stickerPickerChevronOffset = Math.max(10, 8 + window.pageXOffset + buttonRect.left - x);

        const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;

        this.setState({
            showStickers: true,
            stickerPickerX: x,
            stickerPickerY: y,
            stickerPickerChevronOffset,
        });
    }

    /**
     * Trigger hiding of the sticker picker overlay
     * @param  {Event} ev Event that triggered the function call
     */
    _onHideStickersClick(ev) {
        this.setState({showStickers: false});
    }

    /**
     * Called when the window is resized
     */
    _onResize() {
        this.setState({showStickers: false});
    }

    /**
     * The stickers picker was hidden
     */
    _onFinished() {
        this.setState({showStickers: false});
    }

    /**
     * Launch the integration manager on the stickers integration page
     */
    _launchManageIntegrations() {
        // TODO: Open the right integration manager for the widget
        if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
            IntegrationManagers.sharedInstance().openAll(
                this.props.room,
                `type_${widgetType}`,
                this.state.widgetId,
            );
        } else {
            IntegrationManagers.sharedInstance().getPrimaryManager().open(
                this.props.room,
                `type_${widgetType}`,
                this.state.widgetId,
            );
        }
    }

    render() {
        const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
        const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
        let stickersButton;

        const stickerPicker = <ContextualMenu
            elementClass={GenericElementContextMenu}
            chevronOffset={this.state.stickerPickerChevronOffset}
            chevronFace={'bottom'}
            left={this.state.stickerPickerX}
            top={this.state.stickerPickerY}
            menuWidth={this.popoverWidth}
            menuHeight={this.popoverHeight}
            element={this._getStickerpickerContent()}
            onFinished={this._onFinished}
            menuPaddingTop={0}
            menuPaddingLeft={0}
            menuPaddingRight={0}
            zIndex={STICKERPICKER_Z_INDEX}
        />;

        if (this.state.showStickers) {
            // Show hide-stickers button
            stickersButton =
                <AccessibleButton
                    id='stickersButton'
                    key="controls_hide_stickers"
                    className="mx_MessageComposer_button mx_MessageComposer_stickers mx_Stickers_hideStickers"
                    onClick={this._onHideStickersClick}
                    title={_t("Hide Stickers")}
                >
                </AccessibleButton>;
        } else {
            // Show show-stickers button
            stickersButton =
                <AccessibleButton
                    id='stickersButton'
                    key="controls_show_stickers"
                    className="mx_MessageComposer_button mx_MessageComposer_stickers"
                    onClick={this._onShowStickersClick}
                    title={_t("Show Stickers")}
                >
                </AccessibleButton>;
        }
        return <React.Fragment>
            {stickersButton}
            {this.state.showStickers && stickerPicker}
        </React.Fragment>;
    }
}
<commit_msg>Our linter is seriously picky. <commit_after>/*
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {_t, _td} from '../../../languageHandler';
import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";

const widgetType = 'm.stickerpicker';

// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
const STICKERPICKER_Z_INDEX = 3500;

// Key to store the widget's AppTile under in PersistedElement
const PERSISTED_ELEMENT_KEY = "stickerPicker";

export default class Stickerpicker extends React.Component {
    static currentWidget;

    constructor(props) {
        super(props);
        this._onShowStickersClick = this._onShowStickersClick.bind(this);
        this._onHideStickersClick = this._onHideStickersClick.bind(this);
        this._launchManageIntegrations = this._launchManageIntegrations.bind(this);
        this._removeStickerpickerWidgets = this._removeStickerpickerWidgets.bind(this);
        this._updateWidget = this._updateWidget.bind(this);
        this._onWidgetAction = this._onWidgetAction.bind(this);
        this._onResize = this._onResize.bind(this);
        this._onFinished = this._onFinished.bind(this);

        this.popoverWidth = 300;
        this.popoverHeight = 300;

        // This is loaded by _acquireScalarClient on an as-needed basis.
        this.scalarClient = null;

        this.state = {
            showStickers: false,
            imError: null,
            stickerpickerX: null,
            stickerpickerY: null,
            stickerpickerWidget: null,
            widgetId: null,
        };
    }

    _acquireScalarClient() {
        if (this.scalarClient) return Promise.resolve(this.scalarClient);
        // TODO: Pick the right manager for the widget
        if (IntegrationManagers.sharedInstance().hasManager()) {
            this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
            return this.scalarClient.connect().then(() => {
                this.forceUpdate();
                return this.scalarClient;
            }).catch((e) => {
                this._imError(_td("Failed to connect to integration manager"), e);
            });
        } else {
            IntegrationManagers.sharedInstance().openNoManagerDialog();
        }
    }

    async _removeStickerpickerWidgets() {
        const scalarClient = await this._acquireScalarClient();
        console.warn('Removing Stickerpicker widgets');
        if (this.state.widgetId) {
            if (scalarClient) {
                scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
                    console.warn('Assets disabled');
                }).catch((err) => {
                    console.error('Failed to disable assets');
                });
            } else {
                console.error("Cannot disable assets: no scalar client");
            }
        } else {
            console.warn('No widget ID specified, not disabling assets');
        }

        this.setState({showStickers: false});
        WidgetUtils.removeStickerpickerWidgets().then(() => {
            this.forceUpdate();
        }).catch((e) => {
            console.error('Failed to remove sticker picker widget', e);
        });
    }

    componentDidMount() {
        // Close the sticker picker when the window resizes
        window.addEventListener('resize', this._onResize);

        this.dispatcherRef = dis.register(this._onWidgetAction);

        // Track updates to widget state in account data
        MatrixClientPeg.get().on('accountData', this._updateWidget);

        // Initialise widget state from current account data
        this._updateWidget();
    }

    componentWillUnmount() {
        const client = MatrixClientPeg.get();
        if (client) client.removeListener('accountData', this._updateWidget);

        window.removeEventListener('resize', this._onResize);
        if (this.dispatcherRef) {
            dis.unregister(this.dispatcherRef);
        }
    }

    componentDidUpdate(prevProps, prevState) {
        this._sendVisibilityToWidget(this.state.showStickers);
    }

    _imError(errorMsg, e) {
        console.error(errorMsg, e);
        this.setState({
            showStickers: false,
            imError: _t(errorMsg),
        });
    }

    _updateWidget() {
        const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
        if (!stickerpickerWidget) {
            Stickerpicker.currentWidget = null;
            this.setState({stickerpickerWidget: null, widgetId: null});
            return;
        }

        const currentWidget = Stickerpicker.currentWidget;
        let currentUrl = null;
        if (currentWidget && currentWidget.content && currentWidget.content.url) {
            currentUrl = currentWidget.content.url;
        }

        let newUrl = null;
        if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
            newUrl = stickerpickerWidget.content.url;
        }

        if (newUrl !== currentUrl) {
            // Destroy the existing frame so a new one can be created
            PersistedElement.destroyElement(PERSISTED_ELEMENT_KEY);
        }

        Stickerpicker.currentWidget = stickerpickerWidget;
        this.setState({
            stickerpickerWidget,
            widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
        });
    }

    _onWidgetAction(payload) {
        switch (payload.action) {
            case "user_widget_updated":
                this.forceUpdate();
                break;
            case "stickerpicker_close":
                this.setState({showStickers: false});
                break;
            case "show_right_panel":
            case "hide_right_panel":
            case "show_left_panel":
            case "hide_left_panel":
                this.setState({showStickers: false});
                break;
        }
    }

    _defaultStickerpickerContent() {
        return (
            <AccessibleButton onClick={this._launchManageIntegrations}
                className='mx_Stickers_contentPlaceholder'>

{ _t("You don't currently have any stickerpacks enabled") }

{ _t("Add some now") }

            </AccessibleButton>
        );
    }

    _errorStickerpickerContent() {
        return (

 { this.state.imError }

        );
    }

    _sendVisibilityToWidget(visible) {
        if (!this.state.stickerpickerWidget) return;
        const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
        if (widgetMessaging && visible !== this._prevSentVisibility) {
            widgetMessaging.sendVisibility(visible);
            this._prevSentVisibility = visible;
        }
    }

    _getStickerpickerContent() {
        // Handle Integration Manager errors
        if (this.state._imError) {
            return this._errorStickerpickerContent();
        }

        // Stickers
        // TODO - Add support for Stickerpickers from multiple app stores.
        // Render content from multiple stickerpack sources, each within their
        // own iframe, within the stickerpicker UI element.
        const stickerpickerWidget = this.state.stickerpickerWidget;
        let stickersContent;

        // Use a separate ReactDOM tree to render the AppTile separately so that it persists and does
        // not unmount when we (a) close the sticker picker (b) switch rooms. It's properties are still
        // updated.
        const PersistedElement = sdk.getComponent("elements.PersistedElement");

        // Load stickerpack content
        if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
            // Set default name
            stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");

            stickersContent = (

                    <PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
                        <AppTile
                            id={stickerpickerWidget.id}
                            url={stickerpickerWidget.content.url}
                            name={stickerpickerWidget.content.name}
                            room={this.props.room}
                            type={stickerpickerWidget.content.type}
                            fullWidth={true}
                            userId={MatrixClientPeg.get().credentials.userId}
                            creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
                            waitForIframeLoad={true}
                            show={true}
                            showMenubar={true}
                            onEditClick={this._launchManageIntegrations}
                            onDeleteClick={this._removeStickerpickerWidgets}
                            showTitle={false}
                            showMinimise={true}
                            showDelete={false}
                            showCancel={false}
                            showPopout={false}
                            onMinimiseClick={this._onHideStickersClick}
                            handleMinimisePointerEvents={true}
                            whitelistCapabilities={['m.sticker', 'visibility']}
                            userWidget={true}
                        />
                    </PersistedElement>

            );
        } else {
            // Default content to show if stickerpicker widget not added
            stickersContent = this._defaultStickerpickerContent();
        }
        return stickersContent;
    }

    /**
     * Show the sticker picker overlay
     * If no stickerpacks have been added, show a link to the integration manager add sticker packs page.
     * @param  {Event} e Event that triggered the function
     * @return Nothing of use when the thing happens.
     */
    _onShowStickersClick(e) {
        if (!SettingsStore.getValue("integrationProvisioning")) {
            // Intercept this case and spawn a warning.
            return IntegrationManagers.sharedInstance().showDisabledDialog();
        }

        // XXX: Simplify by using a context menu that is positioned relative to the sticker picker button

        const buttonRect = e.target.getBoundingClientRect();

        // The window X and Y offsets are to adjust position when zoomed in to page
        let x = buttonRect.right + window.pageXOffset - 41;

        // Amount of horizontal space between the right of menu and the right of the viewport
        //  (10 = amount needed to make chevron centrally aligned)
        const rightPad = 10;

        // When the sticker picker would be displayed off of the viewport, adjust x
        //  (302 = width of context menu, including borders)
        x = Math.min(x, document.body.clientWidth - (302 + rightPad));

        // Offset the chevron location, which is relative to the left of the context menu
        //  (10 = offset when context menu would not be displayed off viewport)
        //  (8 = value required in practice (possibly 10 - 2 where the 2 = context menu borders)
        const stickerPickerChevronOffset = Math.max(10, 8 + window.pageXOffset + buttonRect.left - x);

        const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;

        this.setState({
            showStickers: true,
            stickerPickerX: x,
            stickerPickerY: y,
            stickerPickerChevronOffset,
        });
    }

    /**
     * Trigger hiding of the sticker picker overlay
     * @param  {Event} ev Event that triggered the function call
     */
    _onHideStickersClick(ev) {
        this.setState({showStickers: false});
    }

    /**
     * Called when the window is resized
     */
    _onResize() {
        this.setState({showStickers: false});
    }

    /**
     * The stickers picker was hidden
     */
    _onFinished() {
        this.setState({showStickers: false});
    }

    /**
     * Launch the integration manager on the stickers integration page
     */
    _launchManageIntegrations() {
        // TODO: Open the right integration manager for the widget
        if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
            IntegrationManagers.sharedInstance().openAll(
                this.props.room,
                `type_${widgetType}`,
                this.state.widgetId,
            );
        } else {
            IntegrationManagers.sharedInstance().getPrimaryManager().open(
                this.props.room,
                `type_${widgetType}`,
                this.state.widgetId,
            );
        }
    }

    render() {
        const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
        const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
        let stickersButton;

        const stickerPicker = <ContextualMenu
            elementClass={GenericElementContextMenu}
            chevronOffset={this.state.stickerPickerChevronOffset}
            chevronFace={'bottom'}
            left={this.state.stickerPickerX}
            top={this.state.stickerPickerY}
            menuWidth={this.popoverWidth}
            menuHeight={this.popoverHeight}
            element={this._getStickerpickerContent()}
            onFinished={this._onFinished}
            menuPaddingTop={0}
            menuPaddingLeft={0}
            menuPaddingRight={0}
            zIndex={STICKERPICKER_Z_INDEX}
        />;

        if (this.state.showStickers) {
            // Show hide-stickers button
            stickersButton =
                <AccessibleButton
                    id='stickersButton'
                    key="controls_hide_stickers"
                    className="mx_MessageComposer_button mx_MessageComposer_stickers mx_Stickers_hideStickers"
                    onClick={this._onHideStickersClick}
                    title={_t("Hide Stickers")}
                >
                </AccessibleButton>;
        } else {
            // Show show-stickers button
            stickersButton =
                <AccessibleButton
                    id='stickersButton'
                    key="controls_show_stickers"
                    className="mx_MessageComposer_button mx_MessageComposer_stickers"
                    onClick={this._onShowStickersClick}
                    title={_t("Show Stickers")}
                >
                </AccessibleButton>;
        }
        return <React.Fragment>
            {stickersButton}
            {this.state.showStickers && stickerPicker}
        </React.Fragment>;
    }
}
<|endoftext|>


<commit_before>'use strict';

module.exports = function(grunt) {

    // Load grunt tasks automatically
    require('load-grunt-tasks')(grunt);

    // Time how long tasks take. Can help when optimizing build times
    require('time-grunt')(grunt);

    // include browserify alias to config file
    var browserifyAliasConfig = require('./browserify.config.js');

    // Define the configuration for all the tasks
    grunt.initConfig({

        // Project settings
        yeoman: {
            // Configurable paths
            app: 'app',
            dist: 'dist'
        },

        // The actual grunt server settings
        connect: {
            options: {
                port: 9000,
                livereload: 35729,
                // Change this to '0.0.0.0' to access the server from outside
                hostname: 'localhost'
            },
            livereload: {
                options: {
                    open: true,
                    base: [
                        '.tmp',
                        '<%%= yeoman.app %>'
                    ]
                }
            },
            test: {
                options: {
                    hostname: 'localhost',
                    port: 9001,
                    base: [
                        '.tmp',
                        'test',
                        '<%%= yeoman.app %>'
                    ]
                }
            },
            dist: {
                options: {
                    open: true,
                    base: '<%%= yeoman.dist %>',
                    livereload: false
                }
            }
        },

        // Watches files for changes and runs tasks based on the changed files
        watch: {
            //js: {
                //files: ['<%%= yeoman.app %>/src/{,*/}*.js'],
                //tasks: ['jshint'],
                //options: { livereload: true }
            //},
            jstest: {
                files: ['test/spec/{,*/}*.js'],
                tasks: ['test:watch']
            },
            //uncomment this if you want to run testing everytime your scripts changing
            //karma: {
                //files: ['app/src/**/*.js', 'test/**/*.js'],
                //tasks: ['karma:unit'] //NOTE the :run flag
            //},
            gruntfile: {
                files: ['Gruntfile.js']
            },
            compass: {
                files: ['<%%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
                tasks: ['compass:server', 'autoprefixer', 'concat']
            },
            browserifySpec: {
                files: ['<%%= yeoman.app %>/src/**/*.spec.js'],
                tasks: ['browserify:spec']
            },
            browserify: {
                files: [
                    '<%%= yeoman.app %>/src/**/*.js',
                    '!<%%= yeoman.app %>/src/**/*.spec.js',
                    '!<%%= yeoman.app %>/src/main.js',
                    '!<%%= yeoman.app %>/src/spec.js'
                ],
                tasks: ['browserify:app'],
                options: { livereload: true }
            },
            styles: {
                files: ['<%%= yeoman.app %>/styles/{,*/}*.css'],
                tasks: ['newer:copy:styles', 'autoprefixer']
            },
            livereload: {
                options: {
                    livereload: '<%%= connect.options.livereload %>'
                },
                files: [
                    '<%%= yeoman.app %>/{,*/}*.html',
                    '<%%= yeoman.app %>/src/**/*.html',
                    '.tmp/styles/{,*/}*.css',
                    '<%%= yeoman.app %>/images/{,*/}*.{gif,jpeg,jpg,png,svg,webp}'
                ]
            }
        },

        shell: { // Task
            protractor: { // Target
                options: { stdout: true },
                command: 'protractor config/e2e.conf.js'
            }
        },

        // Empties folders to start fresh
        clean: {
            dist: {
                files: [{
                    dot: true,
                    src: [
                        '.tmp',
                        '<%%= yeoman.dist %>/*',
                        '!<%%= yeoman.dist %>/.git*'
                    ]
                }]
            },
            afterBuild: {
                files: [{
                    dot: true,
                    src: ['<%%= yeoman.dist %>/src/index.js']
                }]
            },
            server: '.tmp'
        },

        // Make sure code styles are up to par and there are no obvious mistakes
        jshint: {
            options: {
                jshintrc: '.jshintrc',
                reporter: require('jshint-stylish')
            },
            all: [
                'Gruntfile.js',
                '<%%= yeoman.app %>/src/{,*/}*.js',
                '!<%%= yeoman.app %>/src/vendor/*',
                'test/spec/{,*/}*.js'
            ]
        },<% if (testFramework === 'jasmine') { %>

        // karma testing
        karma: {
            unit: {
                configFile: 'config/karma.conf.js'
            }
        },

        // Jasmine testing framework configuration options
        jasmine: {
            pivotal: {
                src: '<%%= yeoman.app %>/src/**/*.js',
                options: {
                    specs: 'test/spec/*Spec.js',
                    helpers: 'test/spec/*Helper.js'
                }
            }
        },<% } else { %>

        // Mocha tesing framework configuration options
        mocha: {
            all: {
                options: {
                    run: true,
                    urls: ['http://<%%= connect.test.options.hostname %>:<%%= connect.test.options.port %>/index.html']
                }
            }
        },<% } %>

        // Compiles Sass to CSS and generates necessary files if requested
        compass: {
            options: {
                sassDir: '<%%= yeoman.app %>/styles',
                cssDir: '.tmp/styles',
                generatedImagesDir: '.tmp/images/generated',
                imagesDir: '<%%= yeoman.app %>/images',
                javascriptsDir: '<%= yeoman.app %>/src',
                fontsDir: '<%%= yeoman.app %>/styles/fonts',
                importPath: '<%%= yeoman.app %>/bower_components',
                httpImagesPath: '/images',
                httpGeneratedImagesPath: '/images/generated',
                httpFontsPath: '/styles/fonts',
                relativeAssets: false,
                assetCacheBuster: false
            },
            dist: {
                options: {
                    generatedImagesDir: '<%%= yeoman.dist %>/images/generated'
                }
            },
            server: {
                options: {
                    debugInfo: true
                }
            }
        },

        // Add vendor prefixed styles
        autoprefixer: {
            options: {
                browsers: ['last 1 version']
            },
            dist: {
                files: [{
                    expand: true,
                    cwd: '.tmp/styles/',
                    src: '{,*/}*.',
                    app: {
                        html: '<%%= yeoman.app %>/index.html',
                        ignorePath: '<%= yeoman.app %>/'
                    },
                    dest: '.tmp/styles/'
                }]
            }
        },

        //browserify task
        browserify: {
          app: {
            files: { '<%%= yeoman.app %>/src/main.js': ['<%%= yeoman.app %>/src/index.js'] },
            options: { alias: browserifyAliasConfig, debug: true }
          },
          dist: {
            files: { '<%%= yeoman.dist %>/src/main.js': ['<%%= yeoman.app %>/src/index.js'] },
            options: { alias: browserifyAliasConfig }
          },
          spec: {
            files: { '<%%= yeoman.app %>/src/spec.js': ['<%%= yeoman.app %>/src/**/*.spec.js'] },
            options: { alias: browserifyAliasConfig }
          }
        },

        // Automatically inject Bower components into the HTML file
        'bower-install': {
            app: {
                html: '<%%= yeoman.app %>/index.html',
                ignorePath: '<%%= yeoman.app %>/'
            }
        },

        // Renames files for browser caching purposes
        rev: {
            dist: {
                files: {
                    src: [
                        '<%%= yeoman.dist %>/src/{,*/}*.js',
                        '<%%= yeoman.dist %>/styles/{,*/}*.css',
                        '<%%= yeoman.dist %>/images/{,*/}*.{gif,jpeg,jpg,png,webp}',
                        '<%%= yeoman.dist %>/styles/fonts/{,*/}*.*'
                    ]
                }
            }
        },

        // Reads HTML for usemin blocks to enable smart builds that automatically
        // concat, minify and revision files. Creates configurations in memory so
        // additional tasks can operate on them
        useminPrepare: {
            options: { dest: '<%%= yeoman.dist %>' },
            html: '<%%= yeoman.app %>/index.html'
        },

        // Performs rewrites based on rev and the useminPrepare configuration
        usemin: {
            options: { assetsDirs: ['<%%= yeoman.dist %>'] },
            html: ['<%%= yeoman.dist %>/{,*/}*.html'],
            css: ['<%%= yeoman.dist %>/styles/{,*/}*.css']
        },

        // The following *-min tasks produce minified files in the dist folder
        imagemin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.app %>/images',
                    src: '{,*/}*.{gif,jpeg,jpg,png}',
                    dest: '<%%= yeoman.dist %>/images'
                }],
                options: { cache: false }
            }
        },
        svgmin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.app %>/images',
                    src: '{,*/}*.svg',
                    dest: '<%%= yeoman.dist %>/images'
                }]
            }
        },
        htmlmin: {
            dist: {
                options: {
                    collapseBooleanAttributes: true,
                    collapseWhitespace: true,
                    removeAttributeQuotes: true,
                    removeCommentsFromCDATA: true,
                    removeEmptyAttributes: true,
                    removeOptionalTags: true,
                    removeRedundantAttributes: true,
                    useShortDoctype: true
                },
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.dist %>',
                    src: '{,*/}*.html',
                    dest: '<%%= yeoman.dist %>'
                }]
            }
        },

        // By default, your `index.html`'s <!-- Usemin block --> will take care of
        // minification. These next options are pre-configured if you do not wish
        // to use the Usemin blocks.
        cssmin: {
            dist: {
                files: {
                    '<%%= yeoman.dist %>/styles/style.css': [
                        '.tmp/styles/{,*/}*.css'
                    ]
                }
            }
        },
        uglify: {
            dist: {
                files: [{
                    src: '<%%= yeoman.dist %>/src/{,*/}*.js', // source files mask
                    expand: true // allow dynamic building
                }]
            }
        },
        concat: {
            dist: {
                src: ['.tmp/styles/{,*/}*.css'],
                dest: '.tmp/styles/style.css'
            }
        },

        // Copies remaining files to places other tasks can use
        copy: {
            dist: {
                files: [{
                    expand: true,
                    dot: true,
                    cwd: '<%%= yeoman.app %>',
                    dest: '<%%= yeoman.dist %>',
                    src: [
                        '*.{ico,png,txt}',
                        '.htaccess',
                        'images/{,*/}*.webp',
                        '{,*/}*.html',
                        'styles/fonts/{,*/}*.*',
                        'bower_components/sass-bootstrap/fonts/*.*',
                        'bower_components/font-awesome/fonts/*.*'
                    ]
                }]
            },
            styles: {
                expand: true,
                dot: true,
                cwd: '<%%= yeoman.app %>/styles',
                dest: '.tmp/styles/',
                src: '{,*/}*.css'
            }
        },<% if (includeModernizr) { %>

        // Generates a custom Modernizr build that includes only the tests you
        // reference in your app
        modernizr: {
            dist: {
                devFile: '<%%= yeoman.app %>/bower_components/modernizr/modernizr.js',
                outputFile: '<%%= yeoman.dist %>/src/vendor/modernizr.js',
                files: {
                    src: [
                        '<%%= yeoman.dist %>/src/{,*/}*.js',
                        '<%%= yeoman.dist %>/styles/{,*/}*.css',
                        '!<%%= yeoman.dist %>/src/vendor/*'
                    ]
                },
                uglify: true
            }
        },<% } %>

        // Run some tasks in parallel to speed up build process
        concurrent: {
            server: [
                'compass:server',
                'copy:styles'
            ],
            test: [
                'copy:styles'
            ],
            dist: [
                'compass',
                'copy:styles',
                'imagemin',
                'svgmin'
            ]
        }
    });

    grunt.registerTask('serve', function(target) {
        if (target === 'dist') {
            return grunt.task.run(['build', 'connect:dist:keepalive']);
        }

        grunt.task.run([
            'clean:server',
            'concurrent:server',
            'browserify:app',
            'browserify:spec',
            'concat',
            'autoprefixer',
            'connect:livereload',
            'watch'
        ]);
    });

    grunt.registerTask('server', function() {
        grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
        grunt.task.run(['serve']);
    });

    grunt.registerTask('test', function(target) {
        if (target !== 'watch') {
            grunt.task.run([
                'clean:server',
                'concurrent:test',
                'autoprefixer',
            ]);
        }

        grunt.task.run([
            'connect:test',<% if (testFramework === 'mocha') { %>
            'mocha'<% } else if (testFramework === 'jasmine') { %>
            'jasmine'<% } %>
        ]);
    });

    grunt.registerTask('build', [
        'clean:dist',
        'useminPrepare',
        'concurrent:dist',
        'autoprefixer',
        'cssmin',
        'browserify:dist',
        'clean:afterBuild',
        'copy:dist',<% if (includeModernizr) { %>
        'modernizr',<% } %>
        'uglify',
        // 'rev',
        'usemin',
        'htmlmin'
    ]);

    grunt.registerTask('default', [
        'newer:jshint',
        'test',
        'build'
    ]);
};
<commit_msg>update build<commit_after>'use strict';

module.exports = function(grunt) {

    // Load grunt tasks automatically
    require('load-grunt-tasks')(grunt);

    // Time how long tasks take. Can help when optimizing build times
    require('time-grunt')(grunt);

    // include browserify alias to config file
    var browserifyAliasConfig = require('./browserify.config.js');

    // Define the configuration for all the tasks
    grunt.initConfig({

        // Project settings
        yeoman: {
            // Configurable paths
            app: 'app',
            dist: 'dist'
        },

        // The actual grunt server settings
        connect: {
            options: {
                port: 9000,
                livereload: 35729,
                // Change this to '0.0.0.0' to access the server from outside
                hostname: 'localhost'
            },
            livereload: {
                options: {
                    open: true,
                    base: [
                        '.tmp',
                        '<%%= yeoman.app %>'
                    ]
                }
            },
            test: {
                options: {
                    hostname: 'localhost',
                    port: 9001,
                    base: [
                        '.tmp',
                        'test',
                        '<%%= yeoman.app %>'
                    ]
                }
            },
            dist: {
                options: {
                    open: true,
                    base: '<%%= yeoman.dist %>',
                    livereload: false
                }
            }
        },

        // Watches files for changes and runs tasks based on the changed files
        watch: {
            //js: {
                //files: ['<%%= yeoman.app %>/src/{,*/}*.js'],
                //tasks: ['jshint'],
                //options: { livereload: true }
            //},
            jstest: {
                files: ['test/spec/{,*/}*.js'],
                tasks: ['test:watch']
            },
            //uncomment this if you want to run testing everytime your scripts changing
            //karma: {
                //files: ['app/src/**/*.js', 'test/**/*.js'],
                //tasks: ['karma:unit'] //NOTE the :run flag
            //},
            gruntfile: {
                files: ['Gruntfile.js']
            },
            compass: {
                files: ['<%%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
                tasks: ['compass:server', 'autoprefixer', 'concat']
            },
            browserifySpec: {
                files: ['<%%= yeoman.app %>/src/**/*.spec.js'],
                tasks: ['browserify:spec']
            },
            browserify: {
                files: [
                    '<%%= yeoman.app %>/src/**/*.js',
                    '!<%%= yeoman.app %>/src/**/*.spec.js',
                    '!<%%= yeoman.app %>/src/main.js',
                    '!<%%= yeoman.app %>/src/spec.js'
                ],
                tasks: ['browserify:app'],
                options: { livereload: true }
            },
            styles: {
                files: ['<%%= yeoman.app %>/styles/{,*/}*.css'],
                tasks: ['newer:copy:styles', 'autoprefixer']
            },
            livereload: {
                options: {
                    livereload: '<%%= connect.options.livereload %>'
                },
                files: [
                    '<%%= yeoman.app %>/{,*/}*.html',
                    '<%%= yeoman.app %>/src/**/*.html',
                    '.tmp/styles/{,*/}*.css',
                    '<%%= yeoman.app %>/images/{,*/}*.{gif,jpeg,jpg,png,svg,webp}'
                ]
            }
        },

        shell: { // Task
            protractor: { // Target
                options: { stdout: true },
                command: 'protractor config/e2e.conf.js'
            }
        },

        // Empties folders to start fresh
        clean: {
            dist: {
                files: [{
                    dot: true,
                    src: [
                        '.tmp',
                        '<%%= yeoman.dist %>/*',
                        '!<%%= yeoman.dist %>/.git*'
                    ]
                }]
            },
            afterBuild: {
                files: [{
                    dot: true,
                    src: ['<%%= yeoman.dist %>/src/index.js']
                }]
            },
            server: '.tmp'
        },

        // Make sure code styles are up to par and there are no obvious mistakes
        jshint: {
            options: {
                jshintrc: '.jshintrc',
                reporter: require('jshint-stylish')
            },
            all: [
                'Gruntfile.js',
                '<%%= yeoman.app %>/src/{,*/}*.js',
                '!<%%= yeoman.app %>/src/vendor/*',
                'test/spec/{,*/}*.js'
            ]
        },<% if (testFramework === 'jasmine') { %>

        // karma testing
        karma: {
            unit: {
                configFile: 'config/karma.conf.js'
            }
        },

        // Jasmine testing framework configuration options
        jasmine: {
            pivotal: {
                src: '<%%= yeoman.app %>/src/**/*.js',
                options: {
                    specs: 'test/spec/*Spec.js',
                    helpers: 'test/spec/*Helper.js'
                }
            }
        },<% } else { %>

        // Mocha tesing framework configuration options
        mocha: {
            all: {
                options: {
                    run: true,
                    urls: ['http://<%%= connect.test.options.hostname %>:<%%= connect.test.options.port %>/index.html']
                }
            }
        },<% } %>

        // Compiles Sass to CSS and generates necessary files if requested
        compass: {
            options: {
                sassDir: '<%%= yeoman.app %>/styles',
                cssDir: '.tmp/styles',
                generatedImagesDir: '.tmp/images/generated',
                imagesDir: '<%%= yeoman.app %>/images',
                javascriptsDir: '<%= yeoman.app %>/src',
                fontsDir: '<%%= yeoman.app %>/styles/fonts',
                importPath: '<%%= yeoman.app %>/bower_components',
                httpImagesPath: '/images',
                httpGeneratedImagesPath: '/images/generated',
                httpFontsPath: '/styles/fonts',
                relativeAssets: false,
                assetCacheBuster: false
            },
            dist: {
                options: {
                    generatedImagesDir: '<%%= yeoman.dist %>/images/generated'
                }
            },
            server: {
                options: {
                    debugInfo: true
                }
            }
        },

        // Add vendor prefixed styles
        autoprefixer: {
            options: {
                browsers: ['last 1 version']
            },
            dist: {
                files: [{
                    expand: true,
                    cwd: '.tmp/styles/',
                    src: '{,*/}*.',
                    app: {
                        html: '<%%= yeoman.app %>/index.html',
                        ignorePath: '<%= yeoman.app %>/'
                    },
                    dest: '.tmp/styles/'
                }]
            }
        },

        //browserify task
        browserify: {
          app: {
            files: { '<%%= yeoman.app %>/src/main.js': ['<%%= yeoman.app %>/src/index.js'] },
            options: { alias: browserifyAliasConfig, debug: true }
          },
          dist: {
            files: { '<%%= yeoman.dist %>/src/main.js': ['<%%= yeoman.app %>/src/index.js'] },
            options: { alias: browserifyAliasConfig }
          },
          spec: {
            files: { '<%%= yeoman.app %>/src/spec.js': ['<%%= yeoman.app %>/src/**/*.spec.js'] },
            options: { alias: browserifyAliasConfig }
          }
        },

        // Automatically inject Bower components into the HTML file
        'bower-install': {
            app: {
                html: '<%%= yeoman.app %>/index.html',
                ignorePath: '<%%= yeoman.app %>/'
            }
        },

        // Renames files for browser caching purposes
        rev: {
            dist: {
                files: {
                    src: [
                        '<%%= yeoman.dist %>/src/{,*/}*.js',
                        '<%%= yeoman.dist %>/styles/{,*/}*.css',
                        '<%%= yeoman.dist %>/images/{,*/}*.{gif,jpeg,jpg,png,webp}',
                        '<%%= yeoman.dist %>/styles/fonts/{,*/}*.*'
                    ]
                }
            }
        },

        // Reads HTML for usemin blocks to enable smart builds that automatically
        // concat, minify and revision files. Creates configurations in memory so
        // additional tasks can operate on them
        useminPrepare: {
            options: { dest: '<%%= yeoman.dist %>' },
            html: '<%%= yeoman.app %>/index.html'
        },

        // Performs rewrites based on rev and the useminPrepare configuration
        usemin: {
            options: { assetsDirs: ['<%%= yeoman.dist %>'] },
            html: ['<%%= yeoman.dist %>/{,*/}*.html'],
            css: ['<%%= yeoman.dist %>/styles/{,*/}*.css']
        },

        // The following *-min tasks produce minified files in the dist folder
        imagemin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.app %>/images',
                    src: '{,*/}*.{gif,jpeg,jpg,png}',
                    dest: '<%%= yeoman.dist %>/images'
                }],
                options: { cache: false }
            }
        },
        svgmin: {
            dist: {
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.app %>/images',
                    src: '{,*/}*.svg',
                    dest: '<%%= yeoman.dist %>/images'
                }]
            }
        },
        htmlmin: {
            dist: {
                options: {
                    collapseBooleanAttributes: true,
                    collapseWhitespace: true,
                    removeAttributeQuotes: true,
                    removeCommentsFromCDATA: true,
                    removeEmptyAttributes: true,
                    removeOptionalTags: true,
                    removeRedundantAttributes: true,
                    useShortDoctype: true
                },
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.dist %>',
                    src: '**/*.html',
                    dest: '<%%= yeoman.dist %>'
                }]
            }
        },

        // By default, your `index.html`'s <!-- Usemin block --> will take care of
        // minification. These next options are pre-configured if you do not wish
        // to use the Usemin blocks.
        cssmin: {
            dist: {
                files: {
                    '<%%= yeoman.dist %>/styles/style.css': [
                        '.tmp/styles/{,*/}*.css'
                    ]
                }
            }
        },
        uglify: {
            dist: {
                files: [{
                    src: '<%%= yeoman.dist %>/src/{,*/}*.js', // source files mask
                    expand: true // allow dynamic building
                }]
            }
        },
        concat: {
            dist: {
                src: ['.tmp/styles/{,*/}*.css'],
                dest: '.tmp/styles/style.css'
            }
        },

        // Copies remaining files to places other tasks can use
        copy: {
            dist: {
                files: [{
                    expand: true,
                    dot: true,
                    cwd: '<%%= yeoman.app %>',
                    dest: '<%%= yeoman.dist %>',
                    src: [
                        '*.{ico,png,txt}',
                        '.htaccess',
                        'images/{,*/}*.webp',
                        '{,*/}*.html',
                        'styles/fonts/{,*/}*.*',
                        'bower_components/sass-bootstrap/fonts/*.*',
                        'bower_components/font-awesome/fonts/*.*'
                    ]
                }]
            },
            html: {
                files: [{
                    expand: true,
                    cwd: '<%%= yeoman.app %>',
                    dest: '<%%= yeoman.dist %>',
                    src: ['src/**/*.html']
                }]
            },
            styles: {
                expand: true,
                dot: true,
                cwd: '<%%= yeoman.app %>/styles',
                dest: '.tmp/styles/',
                src: '{,*/}*.css'
            }
        },<% if (includeModernizr) { %>

        // Generates a custom Modernizr build that includes only the tests you
        // reference in your app
        modernizr: {
            dist: {
                devFile: '<%%= yeoman.app %>/bower_components/modernizr/modernizr.js',
                outputFile: '<%%= yeoman.dist %>/src/vendor/modernizr.js',
                files: {
                    src: [
                        '<%%= yeoman.dist %>/src/{,*/}*.js',
                        '<%%= yeoman.dist %>/styles/{,*/}*.css',
                        '!<%%= yeoman.dist %>/src/vendor/*'
                    ]
                },
                uglify: true
            }
        },<% } %>

        // Run some tasks in parallel to speed up build process
        concurrent: {
            server: [
                'compass:server',
                'copy:styles'
            ],
            test: [
                'copy:styles'
            ],
            dist: [
                'compass',
                'copy:styles',
                'imagemin',
                'svgmin'
            ]
        }
    });

    grunt.registerTask('serve', function(target) {
        if (target === 'dist') {
            return grunt.task.run(['build', 'connect:dist:keepalive']);
        }

        grunt.task.run([
            'clean:server',
            'concurrent:server',
            'browserify:app',
            'browserify:spec',
            'concat',
            'autoprefixer',
            'connect:livereload',
            'watch'
        ]);
    });

    grunt.registerTask('server', function() {
        grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
        grunt.task.run(['serve']);
    });

    grunt.registerTask('test', function(target) {
        if (target !== 'watch') {
            grunt.task.run([
                'clean:server',
                'concurrent:test',
                'autoprefixer',
            ]);
        }

        grunt.task.run([
            'connect:test',<% if (testFramework === 'mocha') { %>
            'mocha'<% } else if (testFramework === 'jasmine') { %>
            'jasmine'<% } %>
        ]);
    });

    grunt.registerTask('build', [
        'clean:dist',
        'useminPrepare',
        'concurrent:dist',
        'autoprefixer',
        'cssmin',
        'browserify:dist',
        'clean:afterBuild',
        'copy:dist',<% if (includeModernizr) { %>
        'modernizr',<% } %>
        'copy:html',
        'uglify',
        // 'rev',
        'usemin',
        'htmlmin'
    ]);

    grunt.registerTask('default', [
        'newer:jshint',
        'test',
        'build'
    ]);
};
<|endoftext|>


<commit_before>const SECOND = 1000;
const MINUTE = SECOND * 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;

/* eslint-disable id-length, complexity */
const TOKENS = {
	Y: 4,
	Q: 1,
	M: 4,
	D: 4,
	d: 4,
	X: 1,
	x: 1,
	H: 2,
	h: 2,
	a: 1,
	A: 1,
	m: 2,
	s: 2,
	S: 3,
	Z: 2
};
/* eslint-enable id-length */

const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

/**
 * Klasa's Timestamp class, parses the pattern once, displays the desired Date or UNIX timestamp with the selected pattern.
 */
class Timestamp {

	/**
	 * @typedef {Object} TimestampObject
	 * @property {string} type The type of the current variable
	 * @property {string} [content] The content of the type. Only accessible if the type is 'literal'
	 * @memberof Timestamp
	 */

	/**
	 * Starts a new Timestamp and parses the pattern.
	 * @since 0.5.0
	 * @param {string} pattern The pattern to parse
	 */
	constructor(pattern) {
		/**
		 * @since 0.5.0
		 * @type {string}
		 */
		this.pattern = pattern;
		/**
		 * @since 0.5.0
		 * @type {TimestampObject[]}
		 * @private
		 */
		this._template = Timestamp._patch(pattern);
	}

	/**
	 * Display the current date with the current pattern.
	 * @since 0.5.0
	 * @param {(Date|number|string)} [time=new Date()] The time to display
	 * @returns {string}
	 */
	display(time = new Date()) {
		return Timestamp._display(this._template, time);
	}

	/**
	 * Display the current date utc with the current pattern.
	 * @since 0.5.0
	 * @param {(Date|number|string)} [time=new Date()] The time to display in utc
	 * @returns {string}
	 */
	displayUTC(time = new Date()) {
		return Timestamp._display(this._template, Timestamp.utc(time));
	}

	/**
	 * Edits the current pattern.
	 * @since 0.5.0
	 * @param {string} pattern The new pattern for this instance
	 * @returns {Timestamp}
	 */
	edit(pattern) {
		this.pattern = pattern;
		this._template = Timestamp._patch(pattern);
		return this;
	}

	/**
	 * Display the current date with the current pattern.
	 * @since 0.5.0
	 * @param {string} pattern The pattern to parse
	 * @param {(Date|number|string)} [time=new Date()] The time to display
	 * @returns {string}
	 */
	static displayArbitrary(pattern, time = new Date()) {
		return Timestamp._display(Timestamp._patch(pattern), time);
	}

	/**
	 * Shows the user friendly duration of time between a period and now.
	 * @since 0.5.0
	 * @param {(Date|number|string)} earlier The time to compare
	 * @param {boolean} [showIn] Whether the output should be prefixed
	 * @returns {string}
	 */
	static toNow(earlier, showIn) {
		if (!(earlier instanceof Date)) earlier = new Date(earlier);
		const returnString = showIn ? 'in ' : '';
		let duration = (Date.now() - earlier) / 1000;

		// Compare the duration in seconds
		if (duration < 45) return `${returnString}seconds`;
		else if (duration < 90) return `${returnString}a minute`;

		// Compare the duration in minutes
		duration /= 60;
		if (duration < 45) return `${returnString + Math.round(duration)} minutes`;
		else if (duration < 90) return `${returnString}an hour`;

		// Compare the duration in hours
		duration /= 60;
		if (duration < 22) return `${returnString + Math.round(duration)} hours`;
		else if (duration < 36) return `${returnString}a day`;

		// Compare the duration in days
		duration /= 24;
		if (duration < 26) return `${returnString + Math.round(duration)} days`;
		else if (duration < 46) return `${returnString}a month`;
		else if (duration < 320) return `${returnString + Math.round(duration / 30)} months`;
		else if (duration < 548) return `${returnString}a year`;

		return `${returnString + Math.floor(duration / 365)} years`;
	}

	/**
	 * Display the current date with the current pattern.
	 * @since 0.5.0
	 * @param {string} template The pattern to parse
	 * @param {(Date|number|string)} time The time to display
	 * @returns {string}
	 * @private
	 */
	static _display(template, time) {
		let output = '';
		const parsedTime = time instanceof Date ? time : new Date(time);
		for (const entry of template) output += entry.content || Timestamp._parse(entry.type, parsedTime);

		return output;
	}

	/**
	 * Parses the current variable.
	 * @since 0.5.0
	 * @param {string} type The type of variable
	 * @param {Date} time The current time
	 * @returns {string}
	 * @private
	 */
	static _parse(type, time) {
		switch (type) {
			// Dates
			case 'Y':
			case 'YY': return String(time.getFullYear()).slice(0, 2);
			case 'YYY':
			case 'YYYY': return String(time.getFullYear());
			case 'Q': return String((time.getMonth() + 1) / 3);
			case 'M':
			case 'MM': return String(time.getMonth() + 1);
			case 'MMM':
			case 'MMMM': return months[time.getMonth()];
			case 'D': return String(time.getDate());
			case 'DD': return String(time.getDate()).padStart(2, '0');
			case 'DDD':
			case 'DDDD': {
				const start = new Date(time.getFullYear(), 0, 0);
				const diff = ((time.getMilliseconds() - start.getMilliseconds()) + (start.getTimezoneOffset() - time.getTimezoneOffset())) * MINUTE;
				return String(Math.floor(diff / DAY));
			}
			case 'd': {
				const day = String(time.getDate());
				if (day !== '11' && day.endsWith('1')) return `${day}st`;
				if (day !== '12' && day.endsWith('2')) return `${day}nd`;
				if (day !== '13' && day.endsWith('3')) return `${day}rd`;
				return `${day}th`;
			}
			case 'dd': {
				return days[time.getDay()].slice(0, 2);
			}
			case 'ddd': {
				return days[time.getDay()].slice(0, 3);
			}
			case 'dddd': {
				return days[time.getDay()];
			}
			case 'X': return String(time.valueOf() / SECOND);
			case 'x': return String(time.valueOf());

			// Times
			case 'H': return String(time.getHours());
			case 'HH': return String(time.getHours()).padStart(2, '0');
			case 'h': return String(time.getHours() % 12);
			case 'hh': return String(time.getHours() % 12).padStart(2, '0');
			case 'a': return time.getHours() < 12 ? 'am' : 'pm';
			case 'A': return time.getHours() < 12 ? 'AM' : 'PM';
			case 'm': return String(time.getMinutes());
			case 'mm': return String(time.getMinutes()).padStart(2, '0');
			case 's': return String(time.getSeconds());
			case 'ss': return String(time.getSeconds()).padStart(2, '0');
			case 'S': return String(time.getMilliseconds());
			case 'SS': return String(time.getMilliseconds()).padStart(2, '0');
			case 'SSS': return String(time.getMilliseconds()).padStart(3, '0');
			case 'Z':
			case 'ZZ': {
				const offset = time.getTimezoneOffset();
				return `${offset >= 0 ? '+' : '-'}${String(offset / -60).padStart(2, '0')}:${String(offset % 60).padStart(2, '0')}`;
			}
			default: return type;
		}
	}

	/**
	 * Parses the pattern.
	 * @since 0.5.0
	 * @param {string} pattern The pattern to parse
	 * @returns {TimestampObject[]}
	 * @private
	 */
	static _patch(pattern) {
		const template = [];
		for (let i = 0; i < pattern.length; i++) {
			let current = '';
			const currentChar = pattern[i];
			if (currentChar in TOKENS) {
				current += currentChar;
				while (pattern[i + 1] === currentChar && current.length < TOKENS[currentChar]) current += pattern[++i];
				template.push({ type: current });
			} else if (currentChar === '[') {
				while (i + 1 < pattern.length && pattern[i + 1] !== ']') current += pattern[++i];
				i++;
				template.push({ type: 'literal', content: current });
			} else {
				current += currentChar;
				while (i + 1 < pattern.length && !(pattern[i + 1] in TOKENS) && pattern[i + 1] !== '[') current += pattern[++i];
				template.push({ type: 'literal', content: current });
			}
		}

		return template;
	}

	/**
	 * Creates a UTC Date object to work with.
	 * @since 0.5.0
	 * @param {Date} [date=new Date()] The date to convert to utc
	 * @returns {Date}
	 */
	static utc(date = new Date()) {
		return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
	}

}

module.exports = Timestamp;
<commit_msg>reduce duplication in Timestamp<commit_after>const SECOND = 1000;
const MINUTE = SECOND * 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;

/* eslint-disable id-length, complexity */
const TOKENS = {
	Y: 4,
	Q: 1,
	M: 4,
	D: 4,
	d: 4,
	X: 1,
	x: 1,
	H: 2,
	h: 2,
	a: 1,
	A: 1,
	m: 2,
	s: 2,
	S: 3,
	Z: 2
};
/* eslint-enable id-length */

const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

/**
 * Klasa's Timestamp class, parses the pattern once, displays the desired Date or UNIX timestamp with the selected pattern.
 */
class Timestamp {

	/**
	 * @typedef {Object} TimestampObject
	 * @property {string} type The type of the current variable
	 * @property {string} [content] The content of the type. Only accessible if the type is 'literal'
	 * @memberof Timestamp
	 */

	/**
	 * Starts a new Timestamp and parses the pattern.
	 * @since 0.5.0
	 * @param {string} pattern The pattern to parse
	 */
	constructor(pattern) {
		/**
		 * @since 0.5.0
		 * @type {string}
		 */
		this.pattern = pattern;
		/**
		 * @since 0.5.0
		 * @type {TimestampObject[]}
		 * @private
		 */
		this._template = Timestamp._patch(pattern);
	}

	/**
	 * Display the current date with the current pattern.
	 * @since 0.5.0
	 * @param {(Date|number|string)} [time=new Date()] The time to display
	 * @returns {string}
	 */
	display(time = new Date()) {
		return Timestamp._display(this._template, time);
	}

	/**
	 * Display the current date utc with the current pattern.
	 * @since 0.5.0
	 * @param {(Date|number|string)} [time=new Date()] The time to display in utc
	 * @returns {string}
	 */
	displayUTC(time) {
		return Timestamp._display(this._template, Timestamp.utc(time));
	}

	/**
	 * Edits the current pattern.
	 * @since 0.5.0
	 * @param {string} pattern The new pattern for this instance
	 * @returns {Timestamp}
	 */
	edit(pattern) {
		this.pattern = pattern;
		this._template = Timestamp._patch(pattern);
		return this;
	}

	/**
	 * Display the current date with the current pattern.
	 * @since 0.5.0
	 * @param {string} pattern The pattern to parse
	 * @param {(Date|number|string)} [time=new Date()] The time to display
	 * @returns {string}
	 */
	static displayArbitrary(pattern, time = new Date()) {
		return Timestamp._display(Timestamp._patch(pattern), time);
	}

	/**
	 * Shows the user friendly duration of time between a period and now.
	 * @since 0.5.0
	 * @param {(Date|number|string)} earlier The time to compare
	 * @param {boolean} [showIn] Whether the output should be prefixed
	 * @returns {string}
	 */
	static toNow(earlier, showIn) {
		if (!(earlier instanceof Date)) earlier = new Date(earlier);
		const returnString = showIn ? 'in ' : '';
		let duration = (Date.now() - earlier) / 1000;

		// Compare the duration in seconds
		if (duration < 45) return `${returnString}seconds`;
		else if (duration < 90) return `${returnString}a minute`;

		// Compare the duration in minutes
		duration /= 60;
		if (duration < 45) return `${returnString + Math.round(duration)} minutes`;
		else if (duration < 90) return `${returnString}an hour`;

		// Compare the duration in hours
		duration /= 60;
		if (duration < 22) return `${returnString + Math.round(duration)} hours`;
		else if (duration < 36) return `${returnString}a day`;

		// Compare the duration in days
		duration /= 24;
		if (duration < 26) return `${returnString + Math.round(duration)} days`;
		else if (duration < 46) return `${returnString}a month`;
		else if (duration < 320) return `${returnString + Math.round(duration / 30)} months`;
		else if (duration < 548) return `${returnString}a year`;

		return `${returnString + Math.floor(duration / 365)} years`;
	}

	/**
	 * Creates a UTC Date object to work with.
	 * @since 0.5.0
	 * @param {(Date|number|string)} [time=new Date()] The date to convert to utc
	 * @returns {Date}
	 */
	static utc(time = new Date()) {
		time = Timestamp.resolveDate(time);
		return new Date(time.getUTCFullYear(), time.getUTCMonth(), time.getUTCDate(), time.getUTCHours(), time.getUTCMinutes(), time.getUTCSeconds());
	}

	/**
	 * Display the current date with the current pattern.
	 * @since 0.5.0
	 * @param {string} template The pattern to parse
	 * @param {(Date|number|string)} time The time to display
	 * @returns {string}
	 * @private
	 */
	static _display(template, time) {
		let output = '';
		const parsedTime = Timestamp.resolveDate(time);
		for (const entry of template) output += entry.content || Timestamp._parse(entry.type, parsedTime);

		return output;
	}

	/**
	 * Parses the current variable.
	 * @since 0.5.0
	 * @param {string} type The type of variable
	 * @param {Date} time The current time
	 * @returns {string}
	 * @private
	 */
	static _parse(type, time) {
		switch (type) {
			// Dates
			case 'Y':
			case 'YY': return String(time.getFullYear()).slice(0, 2);
			case 'YYY':
			case 'YYYY': return String(time.getFullYear());
			case 'Q': return String((time.getMonth() + 1) / 3);
			case 'M':
			case 'MM': return String(time.getMonth() + 1);
			case 'MMM':
			case 'MMMM': return months[time.getMonth()];
			case 'D': return String(time.getDate());
			case 'DD': return String(time.getDate()).padStart(2, '0');
			case 'DDD':
			case 'DDDD': {
				const start = new Date(time.getFullYear(), 0, 0);
				const diff = ((time.getMilliseconds() - start.getMilliseconds()) + (start.getTimezoneOffset() - time.getTimezoneOffset())) * MINUTE;
				return String(Math.floor(diff / DAY));
			}
			case 'd': {
				const day = String(time.getDate());
				if (day !== '11' && day.endsWith('1')) return `${day}st`;
				if (day !== '12' && day.endsWith('2')) return `${day}nd`;
				if (day !== '13' && day.endsWith('3')) return `${day}rd`;
				return `${day}th`;
			}
			case 'dd': {
				return days[time.getDay()].slice(0, 2);
			}
			case 'ddd': {
				return days[time.getDay()].slice(0, 3);
			}
			case 'dddd': {
				return days[time.getDay()];
			}
			case 'X': return String(time.valueOf() / SECOND);
			case 'x': return String(time.valueOf());

			// Times
			case 'H': return String(time.getHours());
			case 'HH': return String(time.getHours()).padStart(2, '0');
			case 'h': return String(time.getHours() % 12);
			case 'hh': return String(time.getHours() % 12).padStart(2, '0');
			case 'a': return time.getHours() < 12 ? 'am' : 'pm';
			case 'A': return time.getHours() < 12 ? 'AM' : 'PM';
			case 'm': return String(time.getMinutes());
			case 'mm': return String(time.getMinutes()).padStart(2, '0');
			case 's': return String(time.getSeconds());
			case 'ss': return String(time.getSeconds()).padStart(2, '0');
			case 'S': return String(time.getMilliseconds());
			case 'SS': return String(time.getMilliseconds()).padStart(2, '0');
			case 'SSS': return String(time.getMilliseconds()).padStart(3, '0');
			case 'Z':
			case 'ZZ': {
				const offset = time.getTimezoneOffset();
				return `${offset >= 0 ? '+' : '-'}${String(offset / -60).padStart(2, '0')}:${String(offset % 60).padStart(2, '0')}`;
			}
			default: return type;
		}
	}

	/**
	 * Parses the pattern.
	 * @since 0.5.0
	 * @param {string} pattern The pattern to parse
	 * @returns {TimestampObject[]}
	 * @private
	 */
	static _patch(pattern) {
		const template = [];
		for (let i = 0; i < pattern.length; i++) {
			let current = '';
			const currentChar = pattern[i];
			if (currentChar in TOKENS) {
				current += currentChar;
				while (pattern[i + 1] === currentChar && current.length < TOKENS[currentChar]) current += pattern[++i];
				template.push({ type: current });
			} else if (currentChar === '[') {
				while (i + 1 < pattern.length && pattern[i + 1] !== ']') current += pattern[++i];
				i++;
				template.push({ type: 'literal', content: current });
			} else {
				current += currentChar;
				while (i + 1 < pattern.length && !(pattern[i + 1] in TOKENS) && pattern[i + 1] !== '[') current += pattern[++i];
				template.push({ type: 'literal', content: current });
			}
		}

		return template;
	}

	/**
	 * Resolves a date.
	 * @since 0.5.0
	 * @param {(Date|number|string)} time The time to parse
	 * @returns {Date}
	 * @private
	 */
	static resolveDate(time) {
		return time instanceof Date ? time : new Date(time);
	}

}

module.exports = Timestamp;
<|endoftext|>


<commit_before>var fs = require('fs');
var chokidar = require('chokidar');
var request = require('request');
var async = require('async');
var path = require('path');
var cheerio = require('cheerio');

var getFiles = function(dir, done) {
  var results = [];
  fs.readdir(dir, function(err, list) {
    if (err) {
      return done(err);
    }
    var pending = list.length;
    if (!pending) {
      return done(null, results);
    }
    list.forEach(function(file) {
      file = path.resolve(dir, file);
      fs.stat(file, function(err, stat) {
        if (stat && stat.isDirectory()) {
          if (!file.match(/node_modules$/)) {
            getFiles(file, function(err, res) {
              results = results.concat(res);
              if (!--pending) {
                done(null, results);
              }
            });
          } else {
            if (!--pending) {
              done(null, results);
            }
          }
        } else {
          if (file.match(/\.(gif|jpg|jpeg|tiff|png)$/i)) {
            results.push(file);
          }
          if (!--pending) {
            done(null, results);
          }
        }
      });
    });
  });
};

var getGoogleImage = function(searchTerm, index, usedImages, safeSearch, next) {
  var googleSearch = 'http://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=' + searchTerm.split('\s').join('+');
  googleSearch += '&start=' + index;
  var resultSize = '&rsz=8';
  googleSearch += resultSize;
  var safe = '&safe=';
  if (safeSearch) {
    safe += 'active';
  } else {
    safe += 'off';
  }
  googleSearch += safe;
  request(googleSearch, function(err, res, body) {
    var randomImage;
    if (!err) {
      if (JSON.parse(body).responseData) {
        var images = JSON.parse(body).responseData.results;
        randomImage = images[Math.floor(Math.random() * images.length)].url;
        if (usedImages.indexOf(randomImage) === -1) {
          next(null, randomImage);
        } else {
          usedImages.push(randomImage);
          index = Math.floor(Math.random() * index * 2);
          getGoogleImage(searchTerm, index, usedImages, safeSearch, next);
        }
      } else if (index > 1) {
        getGoogleImage(searchTerm, index - 1, usedImages, safeSearch, next);
      } else {
        next(new Error('no Google image search results matching keywords.'));
      }
    } else {
      next(err);
    }
  });
};

module.exports.burritorizeFolder = function(directory, searchTerm, safeSearch, next) {
  var usedImages = [];
  async.waterfall([
    function(callback) {
      getFiles(directory, callback);
    },
    function(files, callback) {
      async.each(files, function(file, cb) {
        async.waterfall([
          function(cb1) {
            var index = Math.floor(Math.random() * files.length * 2);
            if (!searchTerm) {
              var fileName = file.split("/").pop().split('.')[0];
              getGoogleImage(fileName, index, usedImages, safeSearch, cb1);

            } else {
              getGoogleImage(searchTerm, index, usedImages, safeSearch, cb1);
            }
          },
          function(image, cb1) {
            usedImages.push(image);
            request(image).pipe(fs.createWriteStream(file)).on('finish', cb1);
          }
        ], cb);
      }, callback);
    }
  ], next);
};

module.exports.burritoWatch = function(directory, searchTerm, safeSearch) {
  module.exports.burritorizeFolder(directory, searchTerm, safeSearch, function() {
    var ignoreList = [];
    var usedImages = [];
    var watcher = chokidar.watch(directory, {
      ignored: /[\/\\]\./,
      persistent: true
    });
    watcher
      .on('add', function(path, stats) {
        if (path.match(/\.(gif|jpg|jpeg|tiff|png)$/i) && ignoreList.indexOf(path) === -1) {
          ignoreList.push(path);
          if (!searchTerm) {
            var fileName = path.split("/").pop().split('.')[0];
            getGoogleImage(fileName, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          } else {
            getGoogleImage(searchTerm, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          }
        }
      });

    watcher
      .on('change', function(path, stats) {
        if (path.match(/\.(gif|jpg|jpeg|tiff|png)$/i) && ignoreList.indexOf(path) === -1) {
          ignoreList.push(path);
          if (!searchTerm) {
            var fileName = path.split("/").pop().split('.')[0];
            getGoogleImage(fileName, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          } else {
            getGoogleImage(searchTerm, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          }
        }
      });
  });
};
<commit_msg>removes cheerio<commit_after>var fs = require('fs');
var chokidar = require('chokidar');
var request = require('request');
var async = require('async');
var path = require('path');

var getFiles = function(dir, done) {
  var results = [];
  fs.readdir(dir, function(err, list) {
    if (err) {
      return done(err);
    }
    var pending = list.length;
    if (!pending) {
      return done(null, results);
    }
    list.forEach(function(file) {
      file = path.resolve(dir, file);
      fs.stat(file, function(err, stat) {
        if (stat && stat.isDirectory()) {
          if (!file.match(/node_modules$/)) {
            getFiles(file, function(err, res) {
              results = results.concat(res);
              if (!--pending) {
                done(null, results);
              }
            });
          } else {
            if (!--pending) {
              done(null, results);
            }
          }
        } else {
          if (file.match(/\.(gif|jpg|jpeg|tiff|png)$/i)) {
            results.push(file);
          }
          if (!--pending) {
            done(null, results);
          }
        }
      });
    });
  });
};

var getGoogleImage = function(searchTerm, index, usedImages, safeSearch, next) {
  var googleSearch = 'http://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=' + searchTerm.split('\s').join('+');
  googleSearch += '&start=' + index;
  var resultSize = '&rsz=8';
  googleSearch += resultSize;
  var safe = '&safe=';
  if (safeSearch) {
    safe += 'active';
  } else {
    safe += 'off';
  }
  googleSearch += safe;
  request(googleSearch, function(err, res, body) {
    var randomImage;
    if (!err) {
      if (JSON.parse(body).responseData) {
        var images = JSON.parse(body).responseData.results;
        randomImage = images[Math.floor(Math.random() * images.length)].url;
        if (usedImages.indexOf(randomImage) === -1) {
          next(null, randomImage);
        } else {
          usedImages.push(randomImage);
          index = Math.floor(Math.random() * index * 2);
          getGoogleImage(searchTerm, index, usedImages, safeSearch, next);
        }
      } else if (index > 1) {
        getGoogleImage(searchTerm, index - 1, usedImages, safeSearch, next);
      } else {
        next(new Error('no Google image search results matching keywords.'));
      }
    } else {
      next(err);
    }
  });
};

module.exports.burritorizeFolder = function(directory, searchTerm, safeSearch, next) {
  var usedImages = [];
  async.waterfall([
    function(callback) {
      getFiles(directory, callback);
    },
    function(files, callback) {
      async.each(files, function(file, cb) {
        async.waterfall([
          function(cb1) {
            var index = Math.floor(Math.random() * files.length * 2);
            if (!searchTerm) {
              var fileName = file.split("/").pop().split('.')[0];
              getGoogleImage(fileName, index, usedImages, safeSearch, cb1);

            } else {
              getGoogleImage(searchTerm, index, usedImages, safeSearch, cb1);
            }
          },
          function(image, cb1) {
            usedImages.push(image);
            request(image).pipe(fs.createWriteStream(file)).on('finish', cb1);
          }
        ], cb);
      }, callback);
    }
  ], next);
};

module.exports.burritoWatch = function(directory, searchTerm, safeSearch) {
  module.exports.burritorizeFolder(directory, searchTerm, safeSearch, function() {
    var ignoreList = [];
    var usedImages = [];
    var watcher = chokidar.watch(directory, {
      ignored: /[\/\\]\./,
      persistent: true
    });
    watcher
      .on('add', function(path, stats) {
        if (path.match(/\.(gif|jpg|jpeg|tiff|png)$/i) && ignoreList.indexOf(path) === -1) {
          ignoreList.push(path);
          if (!searchTerm) {
            var fileName = path.split("/").pop().split('.')[0];
            getGoogleImage(fileName, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          } else {
            getGoogleImage(searchTerm, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          }
        }
      });

    watcher
      .on('change', function(path, stats) {
        if (path.match(/\.(gif|jpg|jpeg|tiff|png)$/i) && ignoreList.indexOf(path) === -1) {
          ignoreList.push(path);
          if (!searchTerm) {
            var fileName = path.split("/").pop().split('.')[0];
            getGoogleImage(fileName, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          } else {
            getGoogleImage(searchTerm, Math.floor(Math.random() * 10), usedImages, safeSearch, function(err, url) {
              if (!err) {
                usedImages.push(url);
                request(url).pipe(fs.createWriteStream(path));
              }
            });
          }
        }
      });
  });
};
<|endoftext|>