<commit_before>const StringUtil = require('../util/string-util');
const log = require('../util/log');

const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
    return new Promise(resolve => {
        let svgString = costume.asset.decodeText();
        // SVG Renderer load fixes "quirks" associated with Scratch 2 projects
        if (optVersion && optVersion === 2 && !runtime.v2SvgAdapter) {
            log.error('No V2 SVG adapter present; SVGs may not render correctly.');
        } else if (optVersion && optVersion === 2 && runtime.v2SvgAdapter) {
            runtime.v2SvgAdapter.loadString(svgString, true /* fromVersion2 */);
            svgString = runtime.v2SvgAdapter.toString();
            // Put back into storage
            const storage = runtime.storage;
            costume.asset.encodeTextData(svgString, storage.DataFormat.SVG, true);
            costume.assetId = costume.asset.assetId;
            costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
        }
        // createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's
        // undefined here
        costume.skinId = runtime.renderer.createSVGSkin(svgString, rotationCenter);
        costume.size = runtime.renderer.getSkinSize(costume.skinId);
        // Now we should have a rotationCenter even if we didn't before
        if (!rotationCenter) {
            rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
            costume.rotationCenterX = rotationCenter[0];
            costume.rotationCenterY = rotationCenter[1];
            costume.bitmapResolution = 1;
        }

        resolve(costume);
    });
};

const canvasPool = (function () {
    /**
     * A pool of canvas objects that can be reused to reduce memory
     * allocations. And time spent in those allocations and the later garbage
     * collection.
     */
    class CanvasPool {
        constructor () {
            this.pool = [];
            this.clearSoon = null;
        }

        /**
         * After a short wait period clear the pool to let the VM collect
         * garbage.
         */
        clear () {
            if (!this.clearSoon) {
                this.clearSoon = new Promise(resolve => setTimeout(resolve, 1000))
                    .then(() => {
                        this.pool.length = 0;
                        this.clearSoon = null;
                    });
            }
        }

        /**
         * Return a canvas. Create the canvas if the pool is empty.
         * @returns {HTMLCanvasElement} A canvas element.
         */
        create () {
            return this.pool.pop() || document.createElement('canvas');
        }

        /**
         * Release the canvas to be reused.
         * @param {HTMLCanvasElement} canvas A canvas element.
         */
        release (canvas) {
            this.clear();
            this.pool.push(canvas);
        }
    }

    return new CanvasPool();
}());

/**
 * Return a promise to fetch a bitmap from storage and return it as a canvas
 * If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3)
 * If the costume has a text layer asset, which is a text part from Scratch 1.4, then this function
 * will merge the two image assets. See the issue LLK/scratch-vm#672 for more information.
 * @param {!object} costume - the Scratch costume object.
 * @param {!Runtime} runtime - Scratch runtime, used to access the v2BitmapAdapter
 * @param {?object} rotationCenter - optionally passed in coordinates for the center of rotation for the image. If
 *     none is given, the rotation center of the costume will be set to the middle of the costume later on.
 * @property {number} costume.bitmapResolution - the resolution scale for a bitmap costume.
 * @returns {?Promise} - a promise which will resolve to an object {canvas, rotationCenter, assetMatchesBase},
 *     or reject on error.
 *     assetMatchesBase is true if the asset matches the base layer; false if it required adjustment
 */
const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
    if (!costume || !costume.asset) {
        return Promise.reject('Costume load failed. Assets were missing.');
    }
    if (!runtime.v2BitmapAdapter) {
        return Promise.reject('No V2 Bitmap adapter present.');
    }

    return Promise.all([costume.asset, costume.textLayerAsset].map(asset => {
        if (!asset) {
            return null;
        }

        if (typeof createImageBitmap !== 'undefined') {
            return createImageBitmap(
                new Blob([asset.data], {type: asset.assetType.contentType})
            );
        }

        return new Promise((resolve, reject) => {
            const image = new Image();
            image.onload = function () {
                resolve(image);
                image.onload = null;
                image.onerror = null;
            };
            image.onerror = function () {
                reject('Costume load failed. Asset could not be read.');
                image.onload = null;
                image.onerror = null;
            };
            image.src = asset.encodeDataURI();
        });
    }))
        .then(([baseImageElement, textImageElement]) => {
            const mergeCanvas = canvasPool.create();

            const scale = costume.bitmapResolution === 1 ? 2 : 1;
            mergeCanvas.width = baseImageElement.width;
            mergeCanvas.height = baseImageElement.height;

            const ctx = mergeCanvas.getContext('2d');
            ctx.drawImage(baseImageElement, 0, 0);
            if (textImageElement) {
                ctx.drawImage(textImageElement, 0, 0);
            }
            // Track the canvas we merged the bitmaps onto separately from the
            // canvas that we receive from resize if scale is not 1. We know
            // resize treats mergeCanvas as read only data. We don't know when
            // resize may use or modify the canvas. So we'll only release the
            // mergeCanvas back into the canvas pool. Reusing the canvas from
            // resize may cause errors.
            let canvas = mergeCanvas;
            if (scale !== 1) {
                canvas = runtime.v2BitmapAdapter.resize(mergeCanvas, canvas.width * scale, canvas.height * scale);
            }

            // By scaling, we've converted it to bitmap resolution 2
            if (rotationCenter) {
                rotationCenter[0] = rotationCenter[0] * scale;
                rotationCenter[1] = rotationCenter[1] * scale;
                costume.rotationCenterX = rotationCenter[0];
                costume.rotationCenterY = rotationCenter[1];
            }
            costume.bitmapResolution = 2;

            // Clean up the costume object
            delete costume.textLayerMD5;
            delete costume.textLayerAsset;

            return {
                canvas,
                mergeCanvas,
                rotationCenter,
                // True if the asset matches the base layer; false if it required adjustment
                assetMatchesBase: scale === 1 && !textImageElement
            };
        })
        .catch(() => {
            // Clean up the text layer properties if it fails to load
            delete costume.textLayerMD5;
            delete costume.textLayerAsset;
        });
};

const loadBitmap_ = function (costume, runtime, _rotationCenter) {
    return fetchBitmapCanvas_(costume, runtime, _rotationCenter)
        .then(fetched => {
            const updateCostumeAsset = function (dataURI) {
                if (!runtime.v2BitmapAdapter) {
                    // TODO: This might be a bad practice since the returned
                    // promise isn't acted on. If this is something we should be
                    // creating a rejected promise for we should also catch it
                    // somewhere and act on that error (like logging).
                    //
                    // Return a rejection to stop executing updateCostumeAsset.
                    return Promise.reject('No V2 Bitmap adapter present.');
                }

                const storage = runtime.storage;
                costume.asset = storage.createAsset(
                    storage.AssetType.ImageBitmap,
                    storage.DataFormat.PNG,
                    runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI),
                    null,
                    true // generate md5
                );
                costume.dataFormat = storage.DataFormat.PNG;
                costume.assetId = costume.asset.assetId;
                costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
            };

            if (!fetched.assetMatchesBase) {
                updateCostumeAsset(fetched.canvas.toDataURL());
            }

            return fetched;
        })
        .then(({canvas, mergeCanvas, rotationCenter}) => {
            // createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
            costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
            canvasPool.release(mergeCanvas);
            const renderSize = runtime.renderer.getSkinSize(costume.skinId);
            costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2

            if (!rotationCenter) {
                rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
                // Actual rotation center, since all bitmaps are resolution 2
                costume.rotationCenterX = rotationCenter[0] * 2;
                costume.rotationCenterY = rotationCenter[1] * 2;
                costume.bitmapResolution = 2;
            }
            return costume;
        });
};

/**
 * Initialize a costume from an asset asynchronously.
 * Do not call this unless there is a renderer attached.
 * @param {!object} costume - the Scratch costume object.
 * @property {int} skinId - the ID of the costume's render skin, once installed.
 * @property {number} rotationCenterX - the X component of the costume's origin.
 * @property {number} rotationCenterY - the Y component of the costume's origin.
 * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
 * @property {!Asset} costume.asset - the asset of the costume loaded from storage.
 * @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
 * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set
 *     to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0.
 * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
 */
const loadCostumeFromAsset = function (costume, runtime, optVersion) {
    costume.assetId = costume.asset.assetId;
    const renderer = runtime.renderer;
    if (!renderer) {
        log.error('No rendering module present; cannot load costume: ', costume.name);
        return Promise.resolve(costume);
    }
    const AssetType = runtime.storage.AssetType;
    let rotationCenter;
    // Use provided rotation center and resolution if they are defined. Bitmap resolution
    // should only ever be 1 or 2.
    if (typeof costume.rotationCenterX === 'number' && !isNaN(costume.rotationCenterX) &&
            typeof costume.rotationCenterY === 'number' && !isNaN(costume.rotationCenterY)) {
        rotationCenter = [costume.rotationCenterX, costume.rotationCenterY];
    }
    if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) {
        return loadVector_(costume, runtime, rotationCenter, optVersion)
            .catch(error => {
                log.warn(`${error.name}: ${error.message}`);
                // Use default asset if original fails to load
                costume.assetId = runtime.storage.defaultAssetId.ImageVector;
                costume.asset = runtime.storage.get(costume.assetId);
                costume.md5 = `${costume.assetId}.${AssetType.ImageVector.runtimeFormat}`;
                return loadVector_(costume, runtime);
            });
    }
    return loadBitmap_(costume, runtime, rotationCenter, optVersion);
};

/**
 * Load a costume's asset into memory asynchronously.
 * Do not call this unless there is a renderer attached.
 * @param {!string} md5ext - the MD5 and extension of the costume to be loaded.
 * @param {!object} costume - the Scratch costume object.
 * @property {int} skinId - the ID of the costume's render skin, once installed.
 * @property {number} rotationCenterX - the X component of the costume's origin.
 * @property {number} rotationCenterY - the Y component of the costume's origin.
 * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
 * @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
 * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set
 *     to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0.
 * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
 */
const loadCostume = function (md5ext, costume, runtime, optVersion) {
    const idParts = StringUtil.splitFirst(md5ext, '.');
    const md5 = idParts[0];
    const ext = idParts[1].toLowerCase();
    costume.dataFormat = ext;

    if (costume.asset) {
        // Costume comes with asset. It could be coming from camera, image upload, drag and drop, or file
        return loadCostumeFromAsset(costume, runtime, optVersion);
    }

    // Need to load the costume from storage. The server should have a reference to this md5.
    if (!runtime.storage) {
        log.error('No storage module present; cannot load costume asset: ', md5ext);
        return Promise.resolve(costume);
    }

    if (!runtime.storage.defaultAssetId) {
        log.error(`No default assets found`);
        return Promise.resolve(costume);
    }

    const AssetType = runtime.storage.AssetType;
    const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;

    const costumePromise = runtime.storage.load(assetType, md5, ext);
    if (!costumePromise) {
        log.error(`Couldn't fetch costume asset: ${md5ext}`);
        return;
    }

    let textLayerPromise;
    if (costume.textLayerMD5) {
        textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png');
    } else {
        textLayerPromise = Promise.resolve(null);
    }

    return Promise.all([costumePromise, textLayerPromise]).then(assetArray => {
        costume.asset = assetArray[0];
        if (assetArray[1]) {
            costume.textLayerAsset = assetArray[1];
        }
        return loadCostumeFromAsset(costume, runtime, optVersion);
    });
};

module.exports = {
    loadCostume,
    loadCostumeFromAsset
};
<commit_msg>Update src/import/load-costume.js<commit_after>const StringUtil = require('../util/string-util');
const log = require('../util/log');

const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
    return new Promise(resolve => {
        let svgString = costume.asset.decodeText();
        // SVG Renderer load fixes "quirks" associated with Scratch 2 projects
        if (optVersion && optVersion === 2 && !runtime.v2SvgAdapter) {
            log.error('No V2 SVG adapter present; SVGs may not render correctly.');
        } else if (optVersion && optVersion === 2 && runtime.v2SvgAdapter) {
            runtime.v2SvgAdapter.loadString(svgString, true /* fromVersion2 */);
            svgString = runtime.v2SvgAdapter.toString();
            // Put back into storage
            const storage = runtime.storage;
            costume.asset.encodeTextData(svgString, storage.DataFormat.SVG, true);
            costume.assetId = costume.asset.assetId;
            costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
        }
        // createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's
        // undefined here
        costume.skinId = runtime.renderer.createSVGSkin(svgString, rotationCenter);
        costume.size = runtime.renderer.getSkinSize(costume.skinId);
        // Now we should have a rotationCenter even if we didn't before
        if (!rotationCenter) {
            rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
            costume.rotationCenterX = rotationCenter[0];
            costume.rotationCenterY = rotationCenter[1];
            costume.bitmapResolution = 1;
        }

        resolve(costume);
    });
};

const canvasPool = (function () {
    /**
     * A pool of canvas objects that can be reused to reduce memory
     * allocations. And time spent in those allocations and the later garbage
     * collection.
     */
    class CanvasPool {
        constructor () {
            this.pool = [];
            this.clearSoon = null;
        }

        /**
         * After a short wait period clear the pool to let the VM collect
         * garbage.
         */
        clear () {
            if (!this.clearSoon) {
                this.clearSoon = new Promise(resolve => setTimeout(resolve, 1000))
                    .then(() => {
                        this.pool.length = 0;
                        this.clearSoon = null;
                    });
            }
        }

        /**
         * Return a canvas. Create the canvas if the pool is empty.
         * @returns {HTMLCanvasElement} A canvas element.
         */
        create () {
            return this.pool.pop() || document.createElement('canvas');
        }

        /**
         * Release the canvas to be reused.
         * @param {HTMLCanvasElement} canvas A canvas element.
         */
        release (canvas) {
            this.clear();
            this.pool.push(canvas);
        }
    }

    return new CanvasPool();
}());

/**
 * Return a promise to fetch a bitmap from storage and return it as a canvas
 * If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3)
 * If the costume has a text layer asset, which is a text part from Scratch 1.4, then this function
 * will merge the two image assets. See the issue LLK/scratch-vm#672 for more information.
 * @param {!object} costume - the Scratch costume object.
 * @param {!Runtime} runtime - Scratch runtime, used to access the v2BitmapAdapter
 * @param {?object} rotationCenter - optionally passed in coordinates for the center of rotation for the image. If
 *     none is given, the rotation center of the costume will be set to the middle of the costume later on.
 * @property {number} costume.bitmapResolution - the resolution scale for a bitmap costume.
 * @returns {?Promise} - a promise which will resolve to an object {canvas, rotationCenter, assetMatchesBase},
 *     or reject on error.
 *     assetMatchesBase is true if the asset matches the base layer; false if it required adjustment
 */
const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
    if (!costume || !costume.asset) {
        return Promise.reject('Costume load failed. Assets were missing.');
    }
    if (!runtime.v2BitmapAdapter) {
        return Promise.reject('No V2 Bitmap adapter present.');
    }

    return Promise.all([costume.asset, costume.textLayerAsset].map(asset => {
        if (!asset) {
            return null;
        }

        if (typeof createImageBitmap !== 'undefined') {
            return createImageBitmap(
                new Blob([asset.data], {type: asset.assetType.contentType})
            );
        }

        return new Promise((resolve, reject) => {
            const image = new Image();
            image.onload = function () {
                resolve(image);
                image.onload = null;
                image.onerror = null;
            };
            image.onerror = function () {
                reject('Costume load failed. Asset could not be read.');
                image.onload = null;
                image.onerror = null;
            };
            image.src = asset.encodeDataURI();
        });
    }))
        .then(([baseImageElement, textImageElement]) => {
            const mergeCanvas = canvasPool.create();

            const scale = costume.bitmapResolution === 1 ? 2 : 1;
            mergeCanvas.width = baseImageElement.width;
            mergeCanvas.height = baseImageElement.height;

            const ctx = mergeCanvas.getContext('2d');
            ctx.drawImage(baseImageElement, 0, 0);
            if (textImageElement) {
                ctx.drawImage(textImageElement, 0, 0);
            }
            // Track the canvas we merged the bitmaps onto separately from the
            // canvas that we receive from resize if scale is not 1. We know
            // resize treats mergeCanvas as read only data. We don't know when
            // resize may use or modify the canvas. So we'll only release the
            // mergeCanvas back into the canvas pool. Reusing the canvas from
            // resize may cause errors.
            let canvas = mergeCanvas;
            if (scale !== 1) {
                canvas = runtime.v2BitmapAdapter.resize(mergeCanvas, canvas.width * scale, canvas.height * scale);
            }

            // By scaling, we've converted it to bitmap resolution 2
            if (rotationCenter) {
                rotationCenter[0] = rotationCenter[0] * scale;
                rotationCenter[1] = rotationCenter[1] * scale;
                costume.rotationCenterX = rotationCenter[0];
                costume.rotationCenterY = rotationCenter[1];
            }
            costume.bitmapResolution = 2;

            // Clean up the costume object
            delete costume.textLayerMD5;
            delete costume.textLayerAsset;

            return {
                canvas,
                mergeCanvas,
                rotationCenter,
                // True if the asset matches the base layer; false if it required adjustment
                assetMatchesBase: scale === 1 && !textImageElement
            };
        })
        .catch(() => {
            // Clean up the text layer properties if it fails to load
            delete costume.textLayerMD5;
            delete costume.textLayerAsset;
        });
};

const loadBitmap_ = function (costume, runtime, _rotationCenter) {
    return fetchBitmapCanvas_(costume, runtime, _rotationCenter)
        .then(fetched => {
            const updateCostumeAsset = function (dataURI) {
                if (!runtime.v2BitmapAdapter) {
                    // TODO: This might be a bad practice since the returned
                    // promise isn't acted on. If this is something we should be
                    // creating a rejected promise for we should also catch it
                    // somewhere and act on that error (like logging).
                    //
                    // Return a rejection to stop executing updateCostumeAsset.
                    return Promise.reject('No V2 Bitmap adapter present.');
                }

                const storage = runtime.storage;
                costume.asset = storage.createAsset(
                    storage.AssetType.ImageBitmap,
                    storage.DataFormat.PNG,
                    runtime.v2BitmapAdapter.convertDataURIToBinary(dataURI),
                    null,
                    true // generate md5
                );
                costume.dataFormat = storage.DataFormat.PNG;
                costume.assetId = costume.asset.assetId;
                costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
            };

            if (!fetched.assetMatchesBase) {
                updateCostumeAsset(fetched.canvas.toDataURL());
            }

            return fetched;
        })
        .then(({canvas, mergeCanvas, rotationCenter}) => {
            // createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
            costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
            canvasPool.release(mergeCanvas);
            const renderSize = runtime.renderer.getSkinSize(costume.skinId);
            costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2

            if (!rotationCenter) {
                rotationCenter = runtime.renderer.getSkinRotationCenter(costume.skinId);
                // Actual rotation center, since all bitmaps are resolution 2
                costume.rotationCenterX = rotationCenter[0] * 2;
                costume.rotationCenterY = rotationCenter[1] * 2;
                costume.bitmapResolution = 2;
            }
            return costume;
        });
};

/**
 * Initialize a costume from an asset asynchronously.
 * Do not call this unless there is a renderer attached.
 * @param {!object} costume - the Scratch costume object.
 * @property {int} skinId - the ID of the costume's render skin, once installed.
 * @property {number} rotationCenterX - the X component of the costume's origin.
 * @property {number} rotationCenterY - the Y component of the costume's origin.
 * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
 * @property {!Asset} costume.asset - the asset of the costume loaded from storage.
 * @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
 * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set
 *     to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0.
 * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
 */
const loadCostumeFromAsset = function (costume, runtime, optVersion) {
    costume.assetId = costume.asset.assetId;
    const renderer = runtime.renderer;
    if (!renderer) {
        log.error('No rendering module present; cannot load costume: ', costume.name);
        return Promise.resolve(costume);
    }
    const AssetType = runtime.storage.AssetType;
    let rotationCenter;
    // Use provided rotation center and resolution if they are defined. Bitmap resolution
    // should only ever be 1 or 2.
    if (typeof costume.rotationCenterX === 'number' && !isNaN(costume.rotationCenterX) &&
            typeof costume.rotationCenterY === 'number' && !isNaN(costume.rotationCenterY)) {
        rotationCenter = [costume.rotationCenterX, costume.rotationCenterY];
    }
    if (costume.asset.assetType.runtimeFormat === AssetType.ImageVector.runtimeFormat) {
        return loadVector_(costume, runtime, rotationCenter, optVersion)
            .catch(error => {
                log.warn(`Error loading vector image: ${error.name}: ${error.message}`);
                // Use default asset if original fails to load
                costume.assetId = runtime.storage.defaultAssetId.ImageVector;
                costume.asset = runtime.storage.get(costume.assetId);
                costume.md5 = `${costume.assetId}.${AssetType.ImageVector.runtimeFormat}`;
                return loadVector_(costume, runtime);
            });
    }
    return loadBitmap_(costume, runtime, rotationCenter, optVersion);
};

/**
 * Load a costume's asset into memory asynchronously.
 * Do not call this unless there is a renderer attached.
 * @param {!string} md5ext - the MD5 and extension of the costume to be loaded.
 * @param {!object} costume - the Scratch costume object.
 * @property {int} skinId - the ID of the costume's render skin, once installed.
 * @property {number} rotationCenterX - the X component of the costume's origin.
 * @property {number} rotationCenterY - the Y component of the costume's origin.
 * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
 * @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
 * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set
 *     to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0.
 * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error.
 */
const loadCostume = function (md5ext, costume, runtime, optVersion) {
    const idParts = StringUtil.splitFirst(md5ext, '.');
    const md5 = idParts[0];
    const ext = idParts[1].toLowerCase();
    costume.dataFormat = ext;

    if (costume.asset) {
        // Costume comes with asset. It could be coming from camera, image upload, drag and drop, or file
        return loadCostumeFromAsset(costume, runtime, optVersion);
    }

    // Need to load the costume from storage. The server should have a reference to this md5.
    if (!runtime.storage) {
        log.error('No storage module present; cannot load costume asset: ', md5ext);
        return Promise.resolve(costume);
    }

    if (!runtime.storage.defaultAssetId) {
        log.error(`No default assets found`);
        return Promise.resolve(costume);
    }

    const AssetType = runtime.storage.AssetType;
    const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;

    const costumePromise = runtime.storage.load(assetType, md5, ext);
    if (!costumePromise) {
        log.error(`Couldn't fetch costume asset: ${md5ext}`);
        return;
    }

    let textLayerPromise;
    if (costume.textLayerMD5) {
        textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png');
    } else {
        textLayerPromise = Promise.resolve(null);
    }

    return Promise.all([costumePromise, textLayerPromise]).then(assetArray => {
        costume.asset = assetArray[0];
        if (assetArray[1]) {
            costume.textLayerAsset = assetArray[1];
        }
        return loadCostumeFromAsset(costume, runtime, optVersion);
    });
};

module.exports = {
    loadCostume,
    loadCostumeFromAsset
};
<|endoftext|>


<commit_before>var eventPanel;
var eventGrid;
var eventStore;
var eventRecord;
var eventColumnModel;
var eventSelectionModel;

function initEventView(){

	eventGrid = new Ext.grid.GridPanel({
		store:eventStore,
        width:'auto',
        height:300,
        renderTo:'event-view',
		colModel:eventColumnModel,
		loadMask:true,
		bbar:eventPagingBar,

		// customize view config
        viewConfig: {
            forceFit:true,
            enableRowBody:true,
            showPreview:true,
            getRowClass : function(record, rowIndex, p, store){
                if(this.showPreview){
                    p.body = '

Event Description: '+record.data.eventDescr+'
';
                    return getSeverityStyle(record.data.severityLevel);
                }
                return 'x-grid3-row-collapsed';
            }
        },

	});

	eventGrid.on("rowdblclick", function(grid) {
		alert("Yo you clicked an event grid");
		/*var view=new Ext.View(Ext.get('squote'),tpl,{
               store:eventStore
        });
        var sel = grid.getSelectionModel().getSelected();
        var selIndex = ds.indexOf(sel);
        var seldata=sel.data;

        seldata.change=change(seldata.change);
        seldata.pctChange=pctChange(seldata.pctChange);
        seldata.lastChange=Ext.util.Format.date(seldata.lastChange,'m/d/Y');
        tpl.overwrite(view.getEl(), seldata);

        dialog.show(grid.getView().getRow(selIndex));*/
    });

	eventPanel = new Ext.Panel({
		applyTo:'event-view',
		width:'auto',
		title:'EVENTS',
		autoHeight:true,
		bodyBorder:false,
		border:false,
		items:[
			eventGrid
		]
	});

	loadNodeEvents(urlParams.node);
}

eventRecord = new Ext.data.Record.create([
	{name:"eventId", mapping:"id"},
	{name:'severityLevel', mapping:'eventSeverity'},
	{name:"eventTime", mapping:"eventTime"},
	{name:"eventDescr", mapping:"eventDescr"},
	{name:"createTime", mapping:"eventCreateTime"},
	{name:"eventUei", mapping:"eventUei"},
])

eventStore = new Ext.data.Store({
	baseParams:{limit:20},
	proxy:new Ext.data.HttpProxy({
		url:'rest/events',
		method:"GET",
		extraParams:{
			limit:20
		}
	}),
	reader:new Ext.data.XmlReader({
		record:"onmsEvent",
		totalRecords:"@totalCount"
	}, eventRecord)
})

eventColumnModel = new Ext.grid.ColumnModel([
	{
		id:'eventIDCol',
		header: "Event ID",
		name:'eventId',
		sortable: true,
		align:'left',
		renderer:renderTopic
	},{
		header:"Severity",
		name:'severityLevel',
		renderer:renderEventSeverity,
		align:'center'
	},{
		header: "Event Time",
		name:'eventTime',
		width: 150,
		sortable: true,
		renderer:eventTime,
		align:'center'
	}
])

var eventPagingBar = new Ext.PagingToolbar({
	pageSize: 20,
    store: eventStore,
    displayInfo: true,
    displayMsg: 'Displaying topics {0} - {1} of {2}',
    emptyMsg: "No topics to display",
})

// pluggable renders
function renderTopic(value, p, record){
    return String.format('{0}',value);
}

function renderEventSeverity(value, p, record){
	var severityLevel;

	if(value == 0){
		severityLevel = 'CLEARED';
	}else if(value == 1){
		severityLevel = 'NORMAL';
	}else if(value == 2){
		severityLevel = 'INDITERMINATE';
	}else if(value == 3){
		severityLevel = 'WARNING';
	}else if(value == 4){
		severityLevel = 'MINOR';
	}else if(value == 5){
		severityLevel = 'MAJOR';
	}else if(value == 6){
		severityLevel = 'CRITICAL';
	}

	return String.format('
{0}
', severityLevel);
}

function eventTime(value, p, r){
	var date = onmsEventDateFormatter(value);
    return String.format('{0}', date);
}

function onmsEventDateFormatter(value){
	var dateArray = value.split("T");
	var date = dateArray[0].split("-");
	var time = dateArray[1].split('-')[0];
	return String.format("{0}/{1}/{2} {3}",date[1], date[2], date[0], time);
}

function getSeverityStyle(severityLevel){
	var bgStyle;
	if(severityLevel == 0){
		bgStyle = 'event-severity-cleared';
	}else if(severityLevel == 1){
		bgStyle = 'event-severity-normal';
	}else if(severityLevel == 2){
		bgStyle = 'event-severity-indeterminate';
	}else if(severityLevel == 3){
		bgStyle = 'event-severity-warning';
	}else if(severityLevel == 4){
		bgStyle = 'event-severity-minor';
	}else if(severityLevel == 5){
		bgStyle = 'event-severity-major';
	}else if(severityLevel == 6){
		bgStyle = 'event-severity-critical';
	}
	return String.format('x-grid3-row-expanded event-severity-border {0}', bgStyle);
}

function acknowledgeEvent(button, eventObj){
	var totalSelected =  eventGrid.getSelectionModel().getCount();
	var selected;

	selected = eventGrid.getSelectionModel().getSelections();

	if(selected.length < 1){
		alert('Please check the events that you would like to acknowledge.');
		return;
	}

	var selectedIds = "";
	for (var i=0; i<selected.length; i++) {
		selectedIds += selected[i].data.eventId + ",";
	};

	alert('selectioned events: ' + selectedIds);
}

function loadNodeEvents(nodeId){
	eventStore.load({params:{'node.id':nodeId}});
}

<commit_msg>EventGrid on nodepage selectionModel update, you can now double click on a event and it will take you to that event's page<commit_after>var eventPanel;
var eventGrid;
var eventStore;
var eventRecord;
var eventColumnModel;
var eventSelectionModel;

function initEventView(){

	eventGrid = new Ext.grid.GridPanel({
		store:eventStore,
        width:'auto',
        height:300,
        renderTo:'event-view',
		colModel:eventColumnModel,
		loadMask:true,
		bbar:eventPagingBar,

		// customize view config
        viewConfig: {
            forceFit:true,
            enableRowBody:true,
            showPreview:true,
            getRowClass : function(record, rowIndex, p, store){
                if(this.showPreview){
                    p.body = '

Event Description: '+record.data.eventDescr+'

';
                    return getSeverityStyle(record.data.severityLevel);
                }
                return 'x-grid3-row-collapsed';
            }
        },

	});

	eventGrid.on("rowdblclick", function(grid) {
		//eventGrid
		var selId = grid.getSelectionModel().getSelected().data.eventId;
		window.location = 'event/detail.jsp?id=' + selId;
    });

	eventPanel = new Ext.Panel({
		applyTo:'event-view',
		width:'auto',
		title:'EVENTS',
		autoHeight:true,
		bodyBorder:false,
		border:false,
		items:[
			eventGrid
		]
	});

	loadNodeEvents(urlParams.node);
}

eventRecord = new Ext.data.Record.create([
	{name:"eventId", mapping:"id"},
	{name:'severityLevel', mapping:'eventSeverity'},
	{name:"eventTime", mapping:"eventTime"},
	{name:"eventDescr", mapping:"eventDescr"},
	{name:"createTime", mapping:"eventCreateTime"},
	{name:"eventUei", mapping:"eventUei"},
])

eventStore = new Ext.data.Store({
	baseParams:{limit:20},
	proxy:new Ext.data.HttpProxy({
		url:'rest/events',
		method:"GET",
		extraParams:{
			limit:20
		}
	}),
	reader:new Ext.data.XmlReader({
		record:"onmsEvent",
		totalRecords:"@totalCount"
	}, eventRecord)
})

eventColumnModel = new Ext.grid.ColumnModel([
	{
		id:'eventIDCol',
		header: "Event ID",
		name:'eventId',
		sortable: true,
		align:'left',
		renderer:renderTopic
	},{
		header:"Severity",
		name:'severityLevel',
		renderer:renderEventSeverity,
		align:'center'
	},{
		header: "Event Time",
		name:'eventTime',
		width: 150,
		sortable: true,
		renderer:eventTime,
		align:'center'
	}
])

var eventPagingBar = new Ext.PagingToolbar({
	pageSize: 20,
    store: eventStore,
    displayInfo: true,
    displayMsg: 'Displaying topics {0} - {1} of {2}',
    emptyMsg: "No topics to display",
})

// pluggable renders
function renderTopic(value, p, record){
    return String.format('{0}',value);
}

function renderEventSeverity(value, p, record){
	var severityLevel;

	if(value == 0){
		severityLevel = 'CLEARED';
	}else if(value == 1){
		severityLevel = 'NORMAL';
	}else if(value == 2){
		severityLevel = 'INDITERMINATE';
	}else if(value == 3){
		severityLevel = 'WARNING';
	}else if(value == 4){
		severityLevel = 'MINOR';
	}else if(value == 5){
		severityLevel = 'MAJOR';
	}else if(value == 6){
		severityLevel = 'CRITICAL';
	}

	return String.format('
{0}
', severityLevel);
}

function eventTime(value, p, r){
	var date = onmsEventDateFormatter(value);
    return String.format('{0}', date);
}

function onmsEventDateFormatter(value){
	var dateArray = value.split("T");
	var date = dateArray[0].split("-");
	var time = dateArray[1].split('-')[0];
	return String.format("{0}/{1}/{2} {3}",date[1], date[2], date[0], time);
}

function getSeverityStyle(severityLevel){
	var bgStyle;
	if(severityLevel == 0){
		bgStyle = 'event-severity-cleared';
	}else if(severityLevel == 1){
		bgStyle = 'event-severity-normal';
	}else if(severityLevel == 2){
		bgStyle = 'event-severity-indeterminate';
	}else if(severityLevel == 3){
		bgStyle = 'event-severity-warning';
	}else if(severityLevel == 4){
		bgStyle = 'event-severity-minor';
	}else if(severityLevel == 5){
		bgStyle = 'event-severity-major';
	}else if(severityLevel == 6){
		bgStyle = 'event-severity-critical';
	}
	return String.format('x-grid3-row-expanded event-severity-border {0}', bgStyle);
}

function acknowledgeEvent(button, eventObj){
	var totalSelected =  eventGrid.getSelectionModel().getCount();
	var selected;

	selected = eventGrid.getSelectionModel().getSelections();

	if(selected.length < 1){
		alert('Please check the events that you would like to acknowledge.');
		return;
	}

	var selectedIds = "";
	for (var i=0; i<selected.length; i++) {
		selectedIds += selected[i].data.eventId + ",";
	};

	alert('selectioned events: ' + selectedIds);
}

function loadNodeEvents(nodeId){
	eventStore.load({params:{'node.id':nodeId}});
}

<|endoftext|>


<commit_before>// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})
<commit_msg>refactor: change module name to grocery<commit_after>// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'

/* global cordova */
angular.module('grocery', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
});
<|endoftext|>


<commit_before>// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    if(window.cordova && window.cordova.plugins.Keyboard) {
      // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
      // for form inputs)
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);

      // Don't remove this line unless you know what you are doing. It stops the viewport
      // from snapping when text inputs are focused. Ionic handles this internally for
      // a much nicer keyboard experience.
      cordova.plugins.Keyboard.disableScroll(true);
    }
    if(window.StatusBar) {
		StatusBar.hide();
    }

  });
})

.config(function($stateProvider, $urlRouterProvider, $ionicConfigProvider) {

	// --------------------------- konfiguration der states inkl. html-templates & zuweisung der controller ---------------------------
	// --------------------------- einigen states werden variablen mitgegeben ---------------------------
	$stateProvider

	.state('start', {
	url: '/start',
	templateUrl: 'templates/start.html',
	controller: 'startCtrl',
	})

	.state('config', {
	url: '/config',
	templateUrl: 'templates/config.html',
	controller: 'configCtrl',
	})

	.state('game', {
	url: '/game',
	templateUrl: 'templates/game.html',
	controller: 'gameCtrl',
	})

	//$urlRouterProvider.otherwise('/start');
	//$urlRouterProvider.otherwise('/config');
	$urlRouterProvider.otherwise('/game');

});

<commit_msg>Starting with config<commit_after>// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    if(window.cordova && window.cordova.plugins.Keyboard) {
      // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
      // for form inputs)
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);

      // Don't remove this line unless you know what you are doing. It stops the viewport
      // from snapping when text inputs are focused. Ionic handles this internally for
      // a much nicer keyboard experience.
      cordova.plugins.Keyboard.disableScroll(true);
    }
    if(window.StatusBar) {
		StatusBar.hide();
    }

  });
})

.config(function($stateProvider, $urlRouterProvider, $ionicConfigProvider) {

	// --------------------------- konfiguration der states inkl. html-templates & zuweisung der controller ---------------------------
	// --------------------------- einigen states werden variablen mitgegeben ---------------------------
	$stateProvider

	.state('start', {
	url: '/start',
	templateUrl: 'templates/start.html',
	controller: 'startCtrl',
	})

	.state('config', {
	url: '/config',
	templateUrl: 'templates/config.html',
	controller: 'configCtrl',
	})

	.state('game', {
	url: '/game',
	templateUrl: 'templates/game.html',
	controller: 'gameCtrl',
	})

	//$urlRouterProvider.otherwise('/start');
	//$urlRouterProvider.otherwise('/config');
	$urlRouterProvider.otherwise('/config');

});

<|endoftext|>


<commit_before>// @flow
import _ from 'lodash';
import { reportError } from 'infra/service/reporter';

const get = (url: string): Promise<any> => new Promise((resolve, reject) => {
  window.FB.api(url, fbRes => {
    if (fbRes.error) {
      const code = fbRes.error.code;
      const type = fbRes.error.type;
      const id = fbRes.error.fbtrace_id;
      const message = fbRes.error.message;

      switch (code) {
        default:
          return reject(new Error(`${type}:${code}:${message}:${id}`));
      }
    }
    return resolve(fbRes);
  });
});

class GraphList {

  data: {}[] = [];
  next: string = '';
  previous: string = '';

  fetch(url: string) {
    return get(url)
      .then(res => {
        this.data = [...this.data, ...res.data];
        if (res.paging) {
          this.previous = res.paging.previous;
          this.next = res.paging.next;
        } else {
          this.previous = '';
          this.next = '';
        }
      });
  }

  fetchForward(url: string, pages: number = 10, currentPage: number = 1) {
    return this.fetch(currentPage === 1 ? url : this.next)
      .then(() => {
        if (this.next && currentPage < pages) {
          return this.fetchForward(url, pages, currentPage + 1);
        } else {
          return this.data;
        }
      });
  }

}

const login = () => new Promise((resolve, reject) => {
  window.FB.login(response => {
    if (response.authResponse) {
      resolve(response);
    } else {
      reject(new Error('User cancelled login or did not fully authorize.'));
    }
  }, { scope: 'public_profile, email, user_managed_groups' });
});

const logout = () => new Promise((resolve) => {
  window.FB.logout();
  resolve(true);
});

const getLoginStatus = () => new Promise((resolve, reject) => {
  window.FB.getLoginStatus((response) => {
    if (response.status === 'connected') {
      resolve(response);
    } else {
      reject(new Error('User not connected.'))
    }
  });
});

const getUser = () => get('/me?fields=id,name,email,picture')
  .then(res => {
    const user: UserProfile = {
      facebookId: res.id,
      name: res.name,
      email: res.email,
      picture: res.picture.data.url,
    };
    return user;
  })
  .catch(err => {
    const errors = err.message.split(':');
    switch (errors[1]) {
      case '2500':
      case '190':
      case '104':
        throw new Error('Access token required. Try to refresh your token with re-login.');
      default:
        reportError(new Error(err.message));
        throw err;
    }
  });

const getUserManagedGroups = (): Promise<Group[]> => get('/me/groups?fields=id,name,privacy,cover,description,owner&limit=100')
  .then(res => {
    const rawGroups = res.data;
    if (rawGroups.length < 1) return [];

    const groups: Group[] = rawGroups.map(raw => ({
      id: raw.id,
      name: raw.name,
      privacy: raw.privacy,
      cover: (raw.cover || {}).source,
      description: raw.description,
      owner: raw.owner,
    }));

    return groups;
  })
  .catch(err => {
    const errors = err.message.split(':');
    switch (errors[1]) {
      case '2500':
      case '190':
      case '104':
        throw new Error('Access token required. Try to refresh your token with re-login.');
      default:
        reportError(new Error(err.message));
        throw err;
    }
  });

const getGroup = (groupId: string): Promise<Group> => get(`/${groupId}?fields=id,name,privacy,cover,description,owner`)
  .then(res => {
    const group: Group = {
      id: res.id,
      name: res.name,
      privacy: res.privacy,
      cover: (res.cover || {}).source,
      description: res.description,
      owner: res.owner,
    }
    return group;
  })
  .catch(err => {
    const errors = err.message.split(':');
    switch (errors[1]) {
      case '190':
      case '104':
        throw new Error('Access token required. Try to refresh your token with re-login.');
      case '100':
      case '803':
        throw new Error('Group ID is not valid.');
      default:
        reportError(new Error(err.message));
        throw err;
    }
  });

const getGroupFeed = (groupId: string, pages: number): Promise<{}[]> => {
  const url = `/${groupId}/feed?fields=created_time,id,message,updated_time,caption,story,description,from,link,name,picture,status_type,type,shares,permalink_url,likes.limit(100),comments.limit(100){id,from,message,created_time,likes.limit(100),comments.limit(100){id,from,message,created_time,likes.limit(100)}}&limit=100`;
  const list = new GraphList();
  return list.fetchForward(url, pages)
    .catch(err => {
      const errors = err.message.split(':');
      switch (errors[1]) {
        case '1':
          throw new Error('oAuth exception, try to refresh your browser manually.');
        default:
          reportError(new Error(err.message));
          throw err;
      }
    });
};

const getGroupComments = (posts: Object[]): Promise<any> => {
  // currently just put existing comments without fetching next page
  const firstComments = [];
  const secondComments = [];
  // get post with comments
  posts.forEach(post => {
    const comments = ((post.comments || {}).data || []).map(comment => ({
      parent: post.id,
      hierarchy: 0,
      ...comment,
    }));
    if (comments) firstComments.push(...comments);
  });
  // get second comments
  firstComments.forEach(com => {
    const comments = ((com.comments || {}).data || []).map(comment => ({
      parent: com.id,
      hierarchy: 1,
      ...comment,
    }));
    if (comments) secondComments.push(...comments);
  });

  const comments = [...firstComments.map(com => _.omit(com, 'comments')), ...secondComments];

  return Promise.resolve(comments)
    .catch(err => {
      const errors = err.message.split(':');
      switch (errors[1]) {
        default:
          reportError(new Error(err.message));
          throw err;
      }
    });
};

const getGroupMembers = (groupId: string, pages: number): Promise<{}[]> => {
  const url = `/${groupId}/members?fields=id,name,administrator,picture,link&limit=100`;
  const list = new GraphList();
  return list.fetchForward(url, pages)
    .catch(err => {
      const errors = err.message.split(':');
      switch (errors[1]) {
        default:
          reportError(new Error(err.message));
          throw err;
      }
    });
};

export default {
  login,
  logout,
  getLoginStatus,
  getUser,
  getGroup,
  getUserManagedGroups,
  getGroupFeed,
  getGroupComments,
  getGroupMembers,
};
<commit_msg>Update graph.js (#199)<commit_after>// @flow
import _ from 'lodash';
import { reportError } from 'infra/service/reporter';

const get = (url: string): Promise<any> => new Promise((resolve, reject) => {
  window.FB.api(url, fbRes => {
    if (fbRes.error) {
      const code = fbRes.error.code;
      const type = fbRes.error.type;
      const id = fbRes.error.fbtrace_id;
      const message = fbRes.error.message;

      switch (code) {
        default:
          return reject(new Error(`${type}:${code}:${message}:${id}`));
      }
    }
    return resolve(fbRes);
  });
});

class GraphList {

  data: {}[] = [];
  next: string = '';
  previous: string = '';

  fetch(url: string) {
    return get(url)
      .then(res => {
        this.data = [...this.data, ...res.data];
        if (res.paging) {
          this.previous = res.paging.previous;
          this.next = res.paging.next;
        } else {
          this.previous = '';
          this.next = '';
        }
      });
  }

  fetchForward(url: string, pages: number = 10, currentPage: number = 1) {
    return this.fetch(currentPage === 1 ? url : this.next)
      .then(() => {
        if (this.next && currentPage < pages) {
          return this.fetchForward(url, pages, currentPage + 1);
        } else {
          return this.data;
        }
      });
  }

}

const login = () => new Promise((resolve, reject) => {
  window.FB.login(response => {
    if (response.authResponse) {
      resolve(response);
    } else {
      reject(new Error('User cancelled login or did not fully authorize.'));
    }
  }, { scope: 'public_profile, email, user_managed_groups' });
});

const logout = () => new Promise((resolve) => {
  window.FB.logout();
  resolve(true);
});

const getLoginStatus = () => new Promise((resolve, reject) => {
  window.FB.getLoginStatus((response) => {
    if (response.status === 'connected') {
      resolve(response);
    } else {
      reject(new Error('User not connected.'))
    }
  });
});

const getUser = () => get('/me?fields=id,name,email,picture')
  .then(res => {
    const user: UserProfile = {
      facebookId: res.id,
      name: res.name,
      email: res.email,
      picture: res.picture.data.url,
    };
    return user;
  })
  .catch(err => {
    const errors = err.message.split(':');
    switch (errors[1]) {
      case '2500':
      case '190':
      case '104':
        throw new Error('Access token required. Try to refresh your token with re-login.');
      default:
        reportError(new Error(err.message));
        throw err;
    }
  });

const getUserManagedGroups = (): Promise<Group[]> => get('/me/groups?fields=id,name,privacy,cover,description,owner&limit=100')
  .then(res => {
    const rawGroups = res.data;
    if (rawGroups.length < 1) return [];

    const groups: Group[] = rawGroups.map(raw => ({
      id: raw.id,
      name: raw.name,
      privacy: raw.privacy,
      cover: (raw.cover || {}).source,
      description: raw.description,
      owner: raw.owner,
    }));

    return groups;
  })
  .catch(err => {
    const errors = err.message.split(':');
    switch (errors[1]) {
      case '2500':
      case '190':
      case '104':
        throw new Error('Access token required. Try to refresh your token with re-login.');
      default:
        reportError(new Error(err.message));
        throw err;
    }
  });

const getGroup = (groupId: string): Promise<Group> => get(`/${groupId}?fields=id,name,privacy,cover,description,owner`)
  .then(res => {
    const group: Group = {
      id: res.id,
      name: res.name,
      privacy: res.privacy,
      cover: (res.cover || {}).source,
      description: res.description,
      owner: res.owner,
    }
    return group;
  })
  .catch(err => {
    const errors = err.message.split(':');
    switch (errors[1]) {
      case '190':
      case '104':
        throw new Error('Access token required. Try to refresh your token with re-login.');
      case '100':
      case '803':
        throw new Error('Group ID is not valid.');
      default:
        reportError(new Error(err.message));
        throw err;
    }
  });

const getGroupFeed = (groupId: string, pages: number): Promise<{}[]> => {
  const url = `/${groupId}/feed?fields=created_time,id,message,updated_time,caption,story,description,from,link,name,picture,status_type,type,shares,permalink_url,likes.limit(50),comments.limit(50){id,from,message,created_time,likes.limit(50),comments.limit(50){id,from,message,created_time,likes.limit(50)}}&limit=50`;
  const list = new GraphList();
  return list.fetchForward(url, pages)
    .catch(err => {
      const errors = err.message.split(':');
      switch (errors[1]) {
        case '1':
          throw new Error('oAuth exception, try to refresh your browser manually.');
        default:
          reportError(new Error(err.message));
          throw err;
      }
    });
};

const getGroupComments = (posts: Object[]): Promise<any> => {
  // currently just put existing comments without fetching next page
  const firstComments = [];
  const secondComments = [];
  // get post with comments
  posts.forEach(post => {
    const comments = ((post.comments || {}).data || []).map(comment => ({
      parent: post.id,
      hierarchy: 0,
      ...comment,
    }));
    if (comments) firstComments.push(...comments);
  });
  // get second comments
  firstComments.forEach(com => {
    const comments = ((com.comments || {}).data || []).map(comment => ({
      parent: com.id,
      hierarchy: 1,
      ...comment,
    }));
    if (comments) secondComments.push(...comments);
  });

  const comments = [...firstComments.map(com => _.omit(com, 'comments')), ...secondComments];

  return Promise.resolve(comments)
    .catch(err => {
      const errors = err.message.split(':');
      switch (errors[1]) {
        default:
          reportError(new Error(err.message));
          throw err;
      }
    });
};

const getGroupMembers = (groupId: string, pages: number): Promise<{}[]> => {
  const url = `/${groupId}/members?fields=id,name,administrator,picture,link&limit=100`;
  const list = new GraphList();
  return list.fetchForward(url, pages)
    .catch(err => {
      const errors = err.message.split(':');
      switch (errors[1]) {
        default:
          reportError(new Error(err.message));
          throw err;
      }
    });
};

export default {
  login,
  logout,
  getLoginStatus,
  getUser,
  getGroup,
  getUserManagedGroups,
  getGroupFeed,
  getGroupComments,
  getGroupMembers,
};
<|endoftext|>


<commit_before>'use strict';

angular.module('Trendicity', [
  'ionic',
  'ionic.contrib.ui.tinderCards',
  'ionic.contrib.icon',
  'config',
  'LocalStorageModule',
  'ngCordova',
  'http-auth-interceptor',
  'uiGmapgoogle-maps'
])

.run(function($rootScope, $ionicPlatform, ENV) {
  console.log('Environment:', ENV.name);

  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }
  });
})

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('app', {
      url: '/app',
      abstract: true,
      templateUrl: 'templates/menu.html',
      controller: 'AppCtrl'
    })

    .state('app.intro', {
      url: '/intro',
      views: {
        'menuContent': {
          templateUrl: 'templates/intro.html',
          controller: 'IntroCtrl'
        }
      }
    })

    .state('app.home', {
      url: '/home',
      abstract: true,
      views: {
        'menuContent' :{
          templateUrl: 'templates/home.html',
          controller: 'HomeCtrl'
        }
      }
    })

    .state('app.home.map', {
      url: '/map',
      views: {
        'tab-map' :{
          templateUrl: 'templates/tab-map.html',
          controller: 'MapViewCtrl as mapCtrl'
        }
      }
    })

    .state('app.home.card', {
      url: '/card',
      views: {
        'tab-card' :{
          templateUrl: 'templates/tab-card.html',
          controller: 'CardViewCtrl'
        }
      }
    })

    .state('app.home.list', {
      url: '/list',
      views: {
        'tab-list' :{
          templateUrl: 'templates/tab-list.html',
          controller: 'ListViewCtrl'
        }
      }
    })

    .state('app.favorites', {
      url: '/favorites',
      views: {
        'menuContent' :{
          templateUrl: 'templates/favorites.html',
          controller: 'FavoritesCtrl'
        }
      }
    });

  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/app/home/map');
})

.constant('$ionicLoadingConfig', {
  template: '
<icon ios="ion-ios7-reloading" android="ion-loading-c" default="ion-refreshing"></icon>
Loading...'
})

.config(function($httpProvider) {
  $httpProvider.interceptors.push('TrendicityInterceptor');
});
<commit_msg>Updated /map route to accept latitude and longitude paramaters<commit_after>'use strict';

angular.module('Trendicity', [
  'ionic',
  'ionic.contrib.ui.tinderCards',
  'ionic.contrib.icon',
  'config',
  'LocalStorageModule',
  'ngCordova',
  'http-auth-interceptor',
  'uiGmapgoogle-maps'
])

.run(function($rootScope, $ionicPlatform, ENV) {
  console.log('Environment:', ENV.name);

  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }
  });
})

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('app', {
      url: '/app',
      abstract: true,
      templateUrl: 'templates/menu.html',
      controller: 'AppCtrl'
    })

    .state('app.intro', {
      url: '/intro',
      views: {
        'menuContent': {
          templateUrl: 'templates/intro.html',
          controller: 'IntroCtrl'
        }
      }
    })

    .state('app.home', {
      url: '/home',
      abstract: true,
      views: {
        'menuContent' :{
          templateUrl: 'templates/home.html',
          controller: 'HomeCtrl'
        }
      }
    })

    .state('app.home.map', {
      url: '/map/?latitude&longitude',
      views: {
        'tab-map' :{
          templateUrl: 'templates/tab-map.html',
          controller: 'MapViewCtrl as mapCtrl'
        }
      }
    })

    .state('app.home.card', {
      url: '/card',
      views: {
        'tab-card' :{
          templateUrl: 'templates/tab-card.html',
          controller: 'CardViewCtrl'
        }
      }
    })

    .state('app.home.list', {
      url: '/list',
      views: {
        'tab-list' :{
          templateUrl: 'templates/tab-list.html',
          controller: 'ListViewCtrl'
        }
      }
    })

    .state('app.favorites', {
      url: '/favorites',
      views: {
        'menuContent' :{
          templateUrl: 'templates/favorites.html',
          controller: 'FavoritesCtrl'
        }
      }
    });

  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/app/home/map');
})

.constant('$ionicLoadingConfig', {
  template: '
<icon ios="ion-ios7-reloading" android="ion-loading-c" default="ion-refreshing"></icon>
Loading...'
})

.config(function($httpProvider) {
  $httpProvider.interceptors.push('TrendicityInterceptor');
});
<|endoftext|>


<commit_before>describe('PlayersController', function() {
  var scope = null;
  var ctrl = null;
  var location = null;
  var stateParams = null;
  var resource = null;
  var httpBackend = null;
  var fakePlayerId = 42;
  var fakeClubId = 77;
  var state;

  var fakePlayer = {
      "id": fakePlayerId,
      "name":"Tia",
      "surname":"Pollich",
      "birthday":"1994-07-26",
      "rank":"dan_4",
      "sex":"female",
      "club_id": fakeClubId
  }

  var fakeClub = {
    id: fakeClubId,
    name: "FakinClub",
    city: "Fakeville",
    description: "They are far from being real"
  };

  var clubs = [
    {
      id: 2,
      name: 'Ryushinkai',
      city: 'Wrocław',
      description: 'Najlepszy klub'
    },
    {
      id: 7,
      name: 'Nowoklub',
      city: 'Daleków',
      description: 'Świeżaki'
    }
  ];

  var setupController = function(playerExists, playerId, results, stateName) {
    return inject(function($location, $stateParams, $rootScope, $resource, $httpBackend, $controller, $state, $templateCache) {
      scope = $rootScope.$new();
      location = $location;
      resource = $resource;
      stateParams = $stateParams;
      httpBackend = $httpBackend;

      state = $state;

      $templateCache.put('Players/_index.html','');
      $templateCache.put('Players/_show.html','');
      //$templateCache.put('Clubs/_form.html','');

      state.go(stateName);
      $rootScope.$apply();
      var request = null;

      if(results) {
        request = new RegExp("players");
        httpBackend.expectGET(request).respond(results);
      } else if (playerId) {
        stateParams.playerId = playerId;
        request = new RegExp("players/" + playerId);
        results = (playerExists)?[200, fakePlayer]:[404];
        httpBackend.expectGET(request).respond(results[0], results[1]);
      }

      ctrl = $controller('PlayersController', {
        $scope: scope,
        $location: location,
        $state: state
      });
    });
  };

  beforeEach(module('ippon'));

  afterEach(function(){
    httpBackend.verifyNoOutstandingExpectation();
    httpBackend.verifyNoOutstandingRequest();
  });

  var setupClub = function(clubId, club) {
    request = new RegExp("clubs/" + clubId);
    results = [200, club];
    httpBackend.expectGET(request).respond(results[0], results[1]);
  }

  var setupClubs = function(clubs) {
    request = new RegExp("clubs");
    results = [200, clubs];
    httpBackend.expectGET(request).respond(results[0], results[1]);
  }

  describe('index', function(){
    var players = [
      {
        "id":71,
        "name":"Tessie",
        "surname":"Ryan",
        "birthday":"1979-04-28",
        "rank":"kyu_3",
        "sex":"male",
        "club_id":71
      },
      {
        "id":72,
        "name":"Elza",
        "surname":"Spinka",
        "birthday":"1982-01-22",
        "rank":"dan_1",
        "sex":"male",
        "club_id":72
      },{
        "id":73,
        "name":"Ines",
        "surname":"Hirthe",
        "birthday":"1970-04-18",
        "rank":"kyu_3",
        "sex":"male",
        "club_id":73
      },
      {
        "id":74,
        "name":"Isac",
        "surname":"Fritsch",
        "birthday":"1957-09-06",
        "rank":"dan_1",
        "sex":"female",
        "club_id":74
      },
      {
        "id":75,
        "name":"Trenton",
        "surname":"Denesik",
        "birthday":"1990-05-04",
        "rank":"dan_2",
        "sex":"female",
        "club_id":75
      },
      {
        "id":76,
        "name":"Karlee",
        "surname":"Kuhic",
        "birthday":"1965-12-10",
        "rank":"kyu_5",
        "sex":"female",
        "club_id":76
      }];

    beforeEach(function(){
      setupController(false,false,players,'players');
      httpBackend.flush();
    });
    it('calls the back-end', function() {
      expect(scope.players).toEqualData(players);
    });
  });

  describe('show',function(){
    describe('player is found', function() {
      beforeEach(function() {
        setupController(true,fakePlayerId,false,'players_show');
        setupClub(fakeClubId,fakeClub);
      });
      it('loads the given player', function() {
        httpBackend.flush();
        var composedPlayer = fakePlayer;
        composedPlayer.club = fakeClub;
        expect(scope.player).toEqualData(composedPlayer);
      });
    });

    describe('player is not found', function() {
      beforeEach(setupController(false, fakePlayerId,false,'players_show'));
      it("doesn't load a player", function() {
        httpBackend.flush();
        expect(scope.player).toBe(null);
        //flash about error
      });
    });
  });

  describe('create', function() {
    var newPlayer = {
      id: 104,
      name: "Steve",
      surname: "Balistreri",
      birthday: "1960-10-06",
      rank: "dan_7",
      sex: "male",
      club_id: 120
    };

    beforeEach(function() {
      setupController(false, false, false, 'players_new');
      setupClubs(clubs);
      var request = new RegExp("\/players");
      httpBackend.expectPOST(request).respond(201, newPlayer);
    });

    it('post to the backend', function() {
      scope.player.name = newPlayer.name;
      scope.player.surname = newPlayer.surname;
      scope.player.birthday = newPlayer.birthday;
      scope.player.rank = newPlayer.rank;
      scope.player.sex = newPlayer.sex;
      scope.player.club_id = newPlayer.club_id;

      scope.save();
      scope.$apply();
      httpBackend.flush();
      expect('#' + location.path()).toBe(state.href('players_show'));
      expect(state.is('players_show')).toBe(true);
    });
  });

  describe('update', function() {
    var updatedPlayer = {
      id: 97,
      name: "Misael",
      surname: "Durgan",
      birthday: "1978-02-28",
      rank: "kyu_2",
      sex: "male",
      club_id: 113
    };

    beforeEach(function() {
      setupController(true, fakePlayerId,false,'players_edit');
      setupClubs(clubs);
      setupClub(fakeClubId, fakeClub);
      httpBackend.flush();
      var request = new RegExp("players/");
      httpBackend.expectPUT(request).respond(204);
    });

    it('posts to the backend', function() {
      scope.player.name = updatedPlayer.name;
      scope.player.surname = updatedPlayer.surname;
      scope.player.birthday = updatedPlayer.birthday;
      scope.player.rank = updatedPlayer.rank;
      scope.player.sex = updatedPlayer.sex;
      scope.player.club_id = updatedPlayer.club_id;
      scope.save();
      httpBackend.flush();
      expect('#'+location.path()).toBe(state.href('players_show',{playerId: scope.player.id}));
      expect(state.is('players_show')).toBe(true);
    });
  });

  describe('delete', function() {
    beforeEach(function() {
      setupController(true,fakePlayerId,false,'players_show');
      setupClub(fakeClubId,fakeClub);
      httpBackend.flush();
      var request = new RegExp("players/" + scope.player.id);
      httpBackend.expectDELETE(request).respond(204);
    });

    it('posts to the backend', function() {
      scope["delete"]();
      scope.$apply();
      httpBackend.flush();
      expect('#'+ location.path()).toBe(state.href('players'));
    });
  });
});
<commit_msg>Add frorm template to template cache in PlayersController js spec<commit_after>describe('PlayersController', function() {
  var scope = null;
  var ctrl = null;
  var location = null;
  var stateParams = null;
  var resource = null;
  var httpBackend = null;
  var fakePlayerId = 42;
  var fakeClubId = 77;
  var state;

  var fakePlayer = {
      "id": fakePlayerId,
      "name":"Tia",
      "surname":"Pollich",
      "birthday":"1994-07-26",
      "rank":"dan_4",
      "sex":"female",
      "club_id": fakeClubId
  }

  var fakeClub = {
    id: fakeClubId,
    name: "FakinClub",
    city: "Fakeville",
    description: "They are far from being real"
  };

  var clubs = [
    {
      id: 2,
      name: 'Ryushinkai',
      city: 'Wrocław',
      description: 'Najlepszy klub'
    },
    {
      id: 7,
      name: 'Nowoklub',
      city: 'Daleków',
      description: 'Świeżaki'
    }
  ];

  var setupController = function(playerExists, playerId, results, stateName) {
    return inject(function($location, $stateParams, $rootScope, $resource, $httpBackend, $controller, $state, $templateCache) {
      scope = $rootScope.$new();
      location = $location;
      resource = $resource;
      stateParams = $stateParams;
      httpBackend = $httpBackend;

      state = $state;

      $templateCache.put('Players/_index.html','');
      $templateCache.put('Players/_show.html','');
      $templateCache.put('Players/_form.html','');

      state.go(stateName);
      $rootScope.$apply();
      var request = null;

      if(results) {
        request = new RegExp("players");
        httpBackend.expectGET(request).respond(results);
      } else if (playerId) {
        stateParams.playerId = playerId;
        request = new RegExp("players/" + playerId);
        results = (playerExists)?[200, fakePlayer]:[404];
        httpBackend.expectGET(request).respond(results[0], results[1]);
      }

      ctrl = $controller('PlayersController', {
        $scope: scope,
        $location: location,
        $state: state
      });
    });
  };

  beforeEach(module('ippon'));

  afterEach(function(){
    httpBackend.verifyNoOutstandingExpectation();
    httpBackend.verifyNoOutstandingRequest();
  });

  var setupClub = function(clubId, club) {
    request = new RegExp("clubs/" + clubId);
    results = [200, club];
    httpBackend.expectGET(request).respond(results[0], results[1]);
  }

  var setupClubs = function(clubs) {
    request = new RegExp("clubs");
    results = [200, clubs];
    httpBackend.expectGET(request).respond(results[0], results[1]);
  }

  describe('index', function(){
    var players = [
      {
        "id":71,
        "name":"Tessie",
        "surname":"Ryan",
        "birthday":"1979-04-28",
        "rank":"kyu_3",
        "sex":"male",
        "club_id":71
      },
      {
        "id":72,
        "name":"Elza",
        "surname":"Spinka",
        "birthday":"1982-01-22",
        "rank":"dan_1",
        "sex":"male",
        "club_id":72
      },{
        "id":73,
        "name":"Ines",
        "surname":"Hirthe",
        "birthday":"1970-04-18",
        "rank":"kyu_3",
        "sex":"male",
        "club_id":73
      },
      {
        "id":74,
        "name":"Isac",
        "surname":"Fritsch",
        "birthday":"1957-09-06",
        "rank":"dan_1",
        "sex":"female",
        "club_id":74
      },
      {
        "id":75,
        "name":"Trenton",
        "surname":"Denesik",
        "birthday":"1990-05-04",
        "rank":"dan_2",
        "sex":"female",
        "club_id":75
      },
      {
        "id":76,
        "name":"Karlee",
        "surname":"Kuhic",
        "birthday":"1965-12-10",
        "rank":"kyu_5",
        "sex":"female",
        "club_id":76
      }];

    beforeEach(function(){
      setupController(false,false,players,'players');
      httpBackend.flush();
    });
    it('calls the back-end', function() {
      expect(scope.players).toEqualData(players);
    });
  });

  describe('show',function(){
    describe('player is found', function() {
      beforeEach(function() {
        setupController(true,fakePlayerId,false,'players_show');
        setupClub(fakeClubId,fakeClub);
      });
      it('loads the given player', function() {
        httpBackend.flush();
        var composedPlayer = fakePlayer;
        composedPlayer.club = fakeClub;
        expect(scope.player).toEqualData(composedPlayer);
      });
    });

    describe('player is not found', function() {
      beforeEach(setupController(false, fakePlayerId,false,'players_show'));
      it("doesn't load a player", function() {
        httpBackend.flush();
        expect(scope.player).toBe(null);
        //flash about error
      });
    });
  });

  describe('create', function() {
    var newPlayer = {
      id: 104,
      name: "Steve",
      surname: "Balistreri",
      birthday: "1960-10-06",
      rank: "dan_7",
      sex: "male",
      club_id: 120
    };

    beforeEach(function() {
      setupController(false, false, false, 'players_new');
      setupClubs(clubs);
      var request = new RegExp("\/players");
      httpBackend.expectPOST(request).respond(201, newPlayer);
    });

    it('post to the backend', function() {
      scope.player.name = newPlayer.name;
      scope.player.surname = newPlayer.surname;
      scope.player.birthday = newPlayer.birthday;
      scope.player.rank = newPlayer.rank;
      scope.player.sex = newPlayer.sex;
      scope.player.club_id = newPlayer.club_id;

      scope.save();
      scope.$apply();
      httpBackend.flush();
      expect('#' + location.path()).toBe(state.href('players_show'));
      expect(state.is('players_show')).toBe(true);
    });
  });

  describe('update', function() {
    var updatedPlayer = {
      id: 97,
      name: "Misael",
      surname: "Durgan",
      birthday: "1978-02-28",
      rank: "kyu_2",
      sex: "male",
      club_id: 113
    };

    beforeEach(function() {
      setupController(true, fakePlayerId,false,'players_edit');
      setupClubs(clubs);
      setupClub(fakeClubId, fakeClub);
      httpBackend.flush();
      var request = new RegExp("players/");
      httpBackend.expectPUT(request).respond(204);
    });

    it('posts to the backend', function() {
      scope.player.name = updatedPlayer.name;
      scope.player.surname = updatedPlayer.surname;
      scope.player.birthday = updatedPlayer.birthday;
      scope.player.rank = updatedPlayer.rank;
      scope.player.sex = updatedPlayer.sex;
      scope.player.club_id = updatedPlayer.club_id;
      scope.save();
      httpBackend.flush();
      expect('#'+location.path()).toBe(state.href('players_show',{playerId: scope.player.id}));
      expect(state.is('players_show')).toBe(true);
    });
  });

  describe('delete', function() {
    beforeEach(function() {
      setupController(true,fakePlayerId,false,'players_show');
      setupClub(fakeClubId,fakeClub);
      httpBackend.flush();
      var request = new RegExp("players/" + scope.player.id);
      httpBackend.expectDELETE(request).respond(204);
    });

    it('posts to the backend', function() {
      scope["delete"]();
      scope.$apply();
      httpBackend.flush();
      expect('#'+ location.path()).toBe(state.href('players'));
    });
  });
});
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2011, Jason Mulligan <jason.mulligan@avoidwork.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of abaaso.flickr nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JASON MULLIGAN BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * abaaso.flickr
 *
 * @author Jason Mulligan <jason.mulligan@avoidwork.com>
 * @link http://avoidwork.com
 * @requires abaaso 1.9
 * @version 1.3
 */
(function (window) {
	"use strict";

	var flickr = (function () {
		var $ = window[abaaso.aliased],
		    config, display, init, key, load, next, prev;

		config = {
			data    : {},
			id      : null,
			key     : null,
			photo   : null,
			loaded  : null,
			timer   : null,
			timeout : 30000,
			sets    : [""],
			slide   : false,
			version : "1.3"
		};

		/**
		 * Displays a photo from the set
		 *
		 * @param {Integer} index  Index of photo
		 */
		display = function (index) {
			if (index != config.photo) return;

			clearTimeout(config.timer);

			var  self  = $.flickr,
			     r     = self.data.get(index).data,
			     obj   = $("#photo"),
			     img   = $.create("img", {style:"display:none;", src: r.sizes.last().source}, obj);

			obj.css("opacity", 0);

			img.on("load", function(){
				this.un("load");
				obj.css("background-image", "url(" + this.src + ")");
				this.destroy();
				obj.css("opacity", 1);
				if (self.config.slide === true) $.flickr.config.timer = setTimeout(function(){ self.next.call(self); }, $.flickr.config.timeout);
			});
		};

		/**
		 * Retrieves a photoset from Flickr
		 */
		init = function () {
			if (config.id === null || config.key === null)
					throw Error($.label.error.invalidArguments);

			var self = $.flickr,
			    i    = (config.sets.length > 1) ? Math.floor(Math.random() * config.sets.length + 1) : 0,
			    uri  = "http://api.flickr.com/services/rest/?&method=flickr.photosets.getPhotos&api_key=" + config.key + "&photoset_id=" + config.sets[i] + "&format=json&jsoncallback=?",
			    fn, index;

			config.loaded = config.sets[i];

			if (typeof $("#year") !== "undefined") $("#year").text(new Date().getFullYear());

			// UI listeners
			$.on(document, "keydown", function (e) { key(e); });
			$.on(document, "keyup", function (e) { $(".click").removeClass("click"); });
			$("#next").on("click", function (e) { next(e); });
			$("#prev").on("click", function (e) { prev(e); });
			$("#play").on("click", function (e) {
				var fn = function () {
					config.timer = setTimeout(fn, config.timeout);
					next();
				};

				switch (true) {
					case config.slide:
						clearTimeout(config.timer);
						config.timer = null;
						break;
					default:
						fn();
				}
				config.slide = !config.slide;
			});

			// Setting up a data store
			$.store(self);
			self.on("afterDataSet", function(r) { load(r, false); }, "photo")
			    .on("afterDataSync", function(data) {
			    	config.data[config.loaded] = data.photo;
			    	index = next();
			    	delete abaaso.flickr.init;
			    }, "photoset");

			self.data.source = "photoset";
			self.data.key    = "id";
			self.data.uri    = uri;
		};

		/**
		 * Keypress handler
		 *
		 * @param {Object} e Keyboard event
		 * @return {Object} Keyboard event
		 */
		key = function (e) {
			var code = (e.keyCode) ? e.keyCode : e.charCode;
			switch (code) {
				case 37:
				case 40:
					prev();
					$("#prev").addClass("click");
					break;
				case 38:
				case 39:
					next();
					$("#next").addClass("click");
					break;
			}
			return e;
		};

		/**
		 * Retrieves sizes for a photo from the set
		 *
		 * @param  {Object}  photo Photo record to display
		 * @param  {Boolean} show  [Optional] Defaults to true, will display the photo
		 * @return {Object} Photo record to display
		 */
		load = function (photo, show) {
			if (typeof photo === "undefined" || typeof photo.key === "undefined") return next();

			show   = (show !== false);
			var self  = $.flickr,
			    uri   = "http://api.flickr.com/services/rest/?&method=flickr.photos.getSizes&api_key=" + config.key + "&photo_id=" + photo.key + "&format=json&jsoncallback=?",
			    index = self.data.keys[photo.key].index,
			    fn, r;

			switch (true) {
				case typeof photo.data.sizes === "undefined":
					fn = function(arg) {
						r = self.data.get(index);
						r.data.sizes = arg.sizes.size.clone();
						self.data.set(r.key, r.data, true);
						if (show === true) display(index);
					};
					uri.jsonp(fn);
					break;
				case show:
					display(index);
					break;
			}

			return photo;
		};

		/**
		 * Displays the next image in the set
		 *
		 * @param  {Object} e Window event
		 * @return {Undefined} undefined
		 */
		next = function (e) {
			var self = $.flickr,
			    i    = (config.photo === null) ? Math.floor(Math.random() * self.data.records.length + 1) : parseInt(config.photo) + 1;

			if (i > self.data.total) i = 0;
			config.photo = i;
			load(self.data.get(i));
			return i;
		};

		/**
		 * Displays the previous image in the set
		 *
		 * @param  {Object} e Window event
		 * @return {Undefined} undefined
		 */
		prev = function (e) {
			var self = $.flickr,
			    i = (config.photo === null) ? Math.floor(Math.random() * self.data.records.length + 1) : parseInt(config.photo) - 1;

			if (i < 0 ) i = self.data.total - 1;
			config.photo  = i;
			load(self.data.get(i));
			return i;
		};

		// @constructor
		return {
			config  : config,
			display : display,
			init    : init,
			key     : key,
			load    : load,
			next    : next,
			prev    : prev
		};
	}),
	fn = function () { abaaso.module("flickr", flickr()); };

	// AMD support
	typeof define === "function" ? define("abaaso.flickr", ["abaaso"], fn) : abaaso.on("render", fn, "abaaso.flickr");
})(window);
<commit_msg>Debugging<commit_after>/**
 * Copyright (c) 2011, Jason Mulligan <jason.mulligan@avoidwork.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of abaaso.flickr nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL JASON MULLIGAN BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * abaaso.flickr
 *
 * @author Jason Mulligan <jason.mulligan@avoidwork.com>
 * @link http://avoidwork.com
 * @requires abaaso 1.9
 * @version 1.3
 */
(function (window) {
	"use strict";

	var flickr = (function () {
		var $ = window[abaaso.aliased],
		    config, display, init, key, load, next, prev;

		config = {
			data    : {},
			id      : null,
			key     : null,
			photo   : null,
			loaded  : null,
			timer   : null,
			timeout : 30000,
			sets    : [""],
			slide   : false,
			version : "1.3"
		};

		/**
		 * Displays a photo from the set
		 *
		 * @param {Integer} index  Index of photo
		 */
		display = function (index) {
			if (index != config.photo) return;

			clearTimeout(config.timer);

			var  self  = $.flickr,
			     r     = self.data.get(index).data,
			     obj   = $("#photo"),
			     img   = $.create("img", {style:"display:none;", src: r.sizes.last().source}, obj);

			obj.css("opacity", 0);

			img.on("load", function(){
				this.un("load");
				obj.css("background-image", "url(" + this.src + ")");
				this.destroy();
				obj.css("opacity", 1);
				if (self.config.slide === true) $.flickr.config.timer = setTimeout(function(){ self.next.call(self); }, $.flickr.config.timeout);
			});
		};

		/**
		 * Retrieves a photoset from Flickr
		 */
		init = function () {
			if (config.id === null || config.key === null)
					throw Error($.label.error.invalidArguments);

			var self = $.flickr,
			    i    = (config.sets.length > 1) ? Math.floor(Math.random() * config.sets.length + 1) : 0,
			    uri  = "http://api.flickr.com/services/rest/?&method=flickr.photosets.getPhotos&api_key=" + config.key + "&photoset_id=" + config.sets[i] + "&format=json&jsoncallback=?",
			    fn, index;

			config.loaded = config.sets[i];

			if (typeof $("#year") !== "undefined") $("#year").text(new Date().getFullYear());

			// UI listeners
			$.on(document, "keydown", function (e) { key(e); });
			$.on(document, "keyup", function (e) { $(".click").removeClass("click"); });
			$("#next").on("click", function (e) { next(e); });
			$("#prev").on("click", function (e) { prev(e); });
			$("#play").on("mouseup", function (e) {
				var fn = function () {
					config.timer = setTimeout(fn, config.timeout);
					next();
				};

				switch (true) {
					case config.slide:
						clearTimeout(config.timer);
						config.timer = null;
						break;
					default:
						fn();
				}
				config.slide = !config.slide;
			});

			// Setting up a data store
			$.store(self);
			self.on("afterDataSet", function(r) { load(r, false); }, "photo")
			    .on("afterDataSync", function(data) {
			    	config.data[config.loaded] = data.photo;
			    	index = next();
			    	delete abaaso.flickr.init;
			    }, "photoset");

			self.data.source = "photoset";
			self.data.key    = "id";
			self.data.uri    = uri;
		};

		/**
		 * Keypress handler
		 *
		 * @param {Object} e Keyboard event
		 * @return {Object} Keyboard event
		 */
		key = function (e) {
			var code = (e.keyCode) ? e.keyCode : e.charCode;
			switch (code) {
				case 37:
				case 40:
					prev();
					$("#prev").addClass("click");
					break;
				case 38:
				case 39:
					next();
					$("#next").addClass("click");
					break;
			}
			return e;
		};

		/**
		 * Retrieves sizes for a photo from the set
		 *
		 * @param  {Object}  photo Photo record to display
		 * @param  {Boolean} show  [Optional] Defaults to true, will display the photo
		 * @return {Object} Photo record to display
		 */
		load = function (photo, show) {
			if (typeof photo === "undefined" || typeof photo.key === "undefined") return next();

			show   = (show !== false);
			var self  = $.flickr,
			    uri   = "http://api.flickr.com/services/rest/?&method=flickr.photos.getSizes&api_key=" + config.key + "&photo_id=" + photo.key + "&format=json&jsoncallback=?",
			    index = self.data.keys[photo.key].index,
			    fn, r;

			switch (true) {
				case typeof photo.data.sizes === "undefined":
					fn = function(arg) {
						r = self.data.get(index);
						r.data.sizes = arg.sizes.size.clone();
						self.data.set(r.key, r.data, true);
						if (show === true) display(index);
					};
					uri.jsonp(fn);
					break;
				case show:
					display(index);
					break;
			}

			return photo;
		};

		/**
		 * Displays the next image in the set
		 *
		 * @param  {Object} e Window event
		 * @return {Undefined} undefined
		 */
		next = function (e) {
			var self = $.flickr,
			    i    = (config.photo === null) ? Math.floor(Math.random() * self.data.records.length + 1) : parseInt(config.photo) + 1;

			if (i > self.data.total) i = 0;
			config.photo = i;
			load(self.data.get(i));
			return i;
		};

		/**
		 * Displays the previous image in the set
		 *
		 * @param  {Object} e Window event
		 * @return {Undefined} undefined
		 */
		prev = function (e) {
			var self = $.flickr,
			    i = (config.photo === null) ? Math.floor(Math.random() * self.data.records.length + 1) : parseInt(config.photo) - 1;

			if (i < 0 ) i = self.data.total - 1;
			config.photo  = i;
			load(self.data.get(i));
			return i;
		};

		// @constructor
		return {
			config  : config,
			display : display,
			init    : init,
			key     : key,
			load    : load,
			next    : next,
			prev    : prev
		};
	}),
	fn = function () { abaaso.module("flickr", flickr()); };

	// AMD support
	typeof define === "function" ? define("abaaso.flickr", ["abaaso"], fn) : abaaso.on("render", fn, "abaaso.flickr");
})(window);
<|endoftext|>