<commit_before>if(typeof widget_famultibutton == 'undefined') {
    loadplugin('widget_famultibutton');
}

var widget_push = $.extend({}, widget_famultibutton, {
   widgetname : 'push',
   startTimer: function ($elem,secondes){
        var count = secondes;
        var $elm = $elem.data('famultibutton');
        if ($elm){
          $elm.setProgressValue(1);
          $elm.data('countdown',setInterval(function(){
            if (count-- <= 0) {
              clearInterval($elm.data('countdown'));
            }
            $elm.setProgressValue(count/secondes);
          }, 1000));
        }
    },
    init: function () {
        var base = this;
        this.elements = $('div[data-type="'+this.widgetname+'"]');
        this.elements.each(function(index) {
            $(this).data('device',          $(this).data('device')          || ' ');
            $(this).data('off-color',               $(this).data('off-color')                                     || getStyle('.push.off','color')              || '#505050');
            $(this).data('off-background-color',    $(this).data('off-background-color')                          || getStyle('.push.off','background-color')   || '#505050');
            $(this).data('on-color',                $(this).data('on-color')            || getClassColor($(this)) || getStyle('.push.on','color')               || '#aa6900');
            $(this).data('on-background-color',     $(this).data('on-background-color') || getClassColor($(this)) || getStyle('.push.on','background-color')    || '#aa6900');
            $(this).data('background-icon', $(this).data('background-icon') || 'fa-circle-thin');
            $(this).data('set-on',          $(this).data('set-on')          || '');
            $(this).data('set-off',         $(this).data('set-off')         || '');
            $(this).data('mode', 'push');
            base.init_attr($(this));
            base.init_ui($(this));
            var elem = $(this); var secondes;
            $(this).bind("toggleOn", function( event ){
                var seton=elem.data("set-on");
                if (seton && !$.isNumeric(seton)
                        && getPart(seton,1)=="on-for-timer")
                    secondes = getPart(elem.data("set-on"),2);
                if (elem.data("countdown"))
                    secondes = elem.data("countdown");
                if (secondes && $.isNumeric(secondes)){
                     widget_push.startTimer(elem,parseInt(secondes));
                }
            });
        });
    },
    update: function (dev,par) {},
});
<commit_msg>Fixed bug with status arrays and countdown for push widget<commit_after>if(typeof widget_famultibutton == 'undefined') {
    loadplugin('widget_famultibutton');
}

var widget_push = $.extend({}, widget_famultibutton, {
   widgetname : 'push',
   startTimer: function ($elem,secondes){
        var count = secondes;
        var $elm = $elem.data('famultibutton');
        if ($elm){
          $elm.setProgressValue(1);
          $elm.data('countdown',setInterval(function(){
            if (count-- <= 0) {
              clearInterval($elm.data('countdown'));
            }
            $elm.setProgressValue(count/secondes);
          }, 1000));
        }
    },
    init: function () {
        var base = this;
        this.elements = $('div[data-type="'+this.widgetname+'"]');
        this.elements.each(function(index) {
            $(this).data('device',          $(this).data('device')          || ' ');
            $(this).data('off-color',               $(this).data('off-color')                                     || getStyle('.push.off','color')              || '#505050');
            $(this).data('off-background-color',    $(this).data('off-background-color')                          || getStyle('.push.off','background-color')   || '#505050');
            $(this).data('on-color',                $(this).data('on-color')            || getClassColor($(this)) || getStyle('.push.on','color')               || '#aa6900');
            $(this).data('on-background-color',     $(this).data('on-background-color') || getClassColor($(this)) || getStyle('.push.on','background-color')    || '#aa6900');
            $(this).data('background-icon', $(this).data('background-icon') || 'fa-circle-thin');
            $(this).data('set-on',          $(this).data('set-on')          || '');
            $(this).data('set-off',         $(this).data('set-off')         || '');
            $(this).data('mode', 'push');
            base.init_attr($(this));
            base.init_ui($(this));
            var elem = $(this); var secondes;
            $(this).bind("toggleOn", function( event ){
                var seton=elem.data("set-on");
                if (seton && !$.isNumeric(seton) &&!$.isArray(seton)
                        && getPart(seton,1)=="on-for-timer")
                    secondes = getPart(elem.data("set-on"),2);
                if (elem.data("countdown"))
                    secondes = elem.data("countdown");
                if (secondes && $.isNumeric(secondes)){
                     widget_push.startTimer(elem,parseInt(secondes));
                }
            });
        });
    },
    update: function (dev,par) {},
});
<|endoftext|>


<commit_before>define(['apiclientcore', 'localassetmanager', 'appStorage'], function (ApiClient, localassetmanager, appStorage) {
    'use strict';

    var localPrefix = 'local:';
    var localViewPrefix = 'localview:';

    function isLocalId(str) {
        return startsWith(str, localPrefix);
    }

    function isLocalViewId(str) {
        return startsWith(str, localViewPrefix);
    }

    function isTopLevelLocalViewId(str) {
        return str === 'localview';
    }

    function stripLocalPrefix(str) {
        var res = stripStart(str, localPrefix);
        res = stripStart(res, localViewPrefix);

        return res;
    }

    function startsWith(str, find) {

        if (str && find && str.length > find.length) {
            if (str.indexOf(find) === 0) {
                return true;
            }
        }

        return false;
    }

    function stripStart(str, find) {
        if (startsWith(str, find)) {
            return str.substr(find.length);
        }

        return str;
    }

    function createEmptyList() {
        var result = {
            Items: [],
            TotalRecordCount: 0
        };

        return result;
    }

    function convertGuidToLocal(guid) {

        if (!guid) {
            return null;
        }

        if (isLocalId(guid)) {
            return guid;
        }

        return 'local:' + guid;
    }

    function adjustGuidProperties(downloadedItem) {

        downloadedItem.Id = convertGuidToLocal(downloadedItem.Id);
        downloadedItem.SeriesId = convertGuidToLocal(downloadedItem.SeriesId);
        downloadedItem.SeasonId = convertGuidToLocal(downloadedItem.SeasonId);

        downloadedItem.AlbumId = convertGuidToLocal(downloadedItem.AlbumId);
        downloadedItem.ParentId = convertGuidToLocal(downloadedItem.ParentId);
        downloadedItem.ParentThumbItemId = convertGuidToLocal(downloadedItem.ParentThumbItemId);
        downloadedItem.ParentPrimaryImageItemId = convertGuidToLocal(downloadedItem.ParentPrimaryImageItemId);
        downloadedItem.PrimaryImageItemId = convertGuidToLocal(downloadedItem.PrimaryImageItemId);
        downloadedItem.ParentLogoItemId = convertGuidToLocal(downloadedItem.ParentLogoItemId);
        downloadedItem.ParentBackdropItemId = convertGuidToLocal(downloadedItem.ParentBackdropItemId);

        downloadedItem.ParentBackdropImageTags = null;
    }

    function getLocalView(instance, serverId, userId) {

        return instance.getLocalFolders(serverId, userId).then(function (views) {

            var localView = null;

            if (views.length > 0) {

                localView = {
                    Name: instance.downloadsTitleText || 'Downloads',
                    ServerId: serverId,
                    Id: 'localview',
                    Type: 'localview',
                    IsFolder: true
                };
            }

            return Promise.resolve(localView);
        });
    }

    /**
     * Creates a new api client instance
     * @param {String} serverAddress
     * @param {String} clientName s
     * @param {String} applicationVersion
     */
    function ApiClientEx(serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio) {

        ApiClient.call(this, serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio);
    }

    Object.assign(ApiClientEx.prototype, ApiClient.prototype);

    ApiClientEx.prototype.getPlaybackInfo = function (itemId, options, deviceProfile) {

        if (isLocalId(itemId)) {
            return localassetmanager.getLocalItem(this.serverId(), stripLocalPrefix(itemId)).then(function (item) {

                // TODO: This was already done during the sync process, right? If so, remove it
                var mediaSources = item.Item.MediaSources.map(function (m) {
                    m.SupportsDirectPlay = true;
                    m.SupportsDirectStream = false;
                    m.SupportsTranscoding = false;
                    m.IsLocal = true;
                    return m;
                });

                return {
                    MediaSources: mediaSources
                };
            });
        }

        var instance = this;
        return localassetmanager.getLocalItem(this.serverId(), itemId).then(function (item) {

            if (item) {

                var mediaSources = item.Item.MediaSources.map(function (m) {
                    m.SupportsDirectPlay = true;
                    m.SupportsDirectStream = false;
                    m.SupportsTranscoding = false;
                    m.IsLocal = true;
                    return m;
                });

                return localassetmanager.fileExists(item.LocalPath).then(function (exists) {

                    if (exists) {

                        var res = {
                            MediaSources: mediaSources
                        };

                        return Promise.resolve(res);
                    }

                    return ApiClient.prototype.getPlaybackInfo.call(instance, itemId, options, deviceProfile);
                });
            }

            return ApiClient.prototype.getPlaybackInfo.call(instance, itemId, options, deviceProfile);
        });
    };

    ApiClientEx.prototype.getItems = function (userId, options) {

        var serverInfo = this.serverInfo();
        var i;

        if (serverInfo && options.ParentId === 'localview') {

            return this.getLocalFolders(serverInfo.Id, userId).then(function (items) {
                var result = {
                    Items: items,
                    TotalRecordCount: items.length
                };

                return Promise.resolve(result);
            });

        } else if (serverInfo && options && (isLocalId(options.ParentId) || isLocalViewId(options.ParentId))) {

            return localassetmanager.getViewItems(serverInfo.Id, userId, options).then(function (items) {

                items.forEach(function (item) {
                    adjustGuidProperties(item);
                });

                items.sort(function (a, b) { return a.SortName.toLowerCase().localeCompare(b.SortName.toLowerCase()); });

                var result = {
                    Items: items,
                    TotalRecordCount: items.length
                };

                return Promise.resolve(result);
            });
        } else if (options && options.ExcludeItemIds && options.ExcludeItemIds.length) {

            var exItems = options.ExcludeItemIds.split(',');

            for (i = 0; i < exItems.length; i++) {
                if (isLocalId(exItems[i])) {
                    return Promise.resolve(createEmptyList());
                }
            }
        } else if (options && options.Ids && options.Ids.length) {

            var ids = options.Ids.split(',');
            var hasLocal = false;

            for (i = 0; i < ids.length; i++) {
                if (isLocalId(ids[i])) {
                    hasLocal = true;
                }
            }

            if (hasLocal) {
                return localassetmanager.getItemsFromIds(serverInfo.Id, ids).then(function (items) {

                    items.forEach(function (item) {
                        adjustGuidProperties(item);
                    });

                    var result = {
                        Items: items,
                        TotalRecordCount: items.length
                    };

                    return Promise.resolve(result);
                });
            }
        }

        return ApiClient.prototype.getItems.call(this, userId, options);
    };

    ApiClientEx.prototype.getUserViews = function (options, userId) {

        var instance = this;

        return ApiClient.prototype.getUserViews.call(instance, options, userId).then(function (result) {

            var serverInfo = instance.serverInfo();
            if (serverInfo) {

                return getLocalView(instance, serverInfo.Id, userId).then(function (localView) {

                    if (localView) {

                        result.Items.push(localView);
                        result.TotalRecordCount++;
                    }

                    return Promise.resolve(result);
                });
            }

            return Promise.resolve(result);
        });
    };

    ApiClientEx.prototype.getItem = function (userId, itemId) {

        if (!itemId) {
            throw new Error("null itemId");
        }

        if (itemId) {
            itemId = itemId.toString();
        }

        var serverInfo;

        if (isTopLevelLocalViewId(itemId)) {

            serverInfo = this.serverInfo();

            if (serverInfo) {
                return getLocalView(this, serverInfo.Id, userId);
            }
        }

        if (isLocalViewId(itemId)) {

            serverInfo = this.serverInfo();

            if (serverInfo) {
                return this.getLocalFolders(serverInfo.Id, userId).then(function (items) {

                    var views = items.filter(function (item) {
                        return item.Id === itemId;
                    });

                    if (views.length > 0) {
                        return Promise.resolve(views[0]);
                    }

                    // TODO: Test consequence of this
                    return Promise.reject();
                });
            }
        }

        if (isLocalId(itemId)) {

            serverInfo = this.serverInfo();

            if (serverInfo) {
                return localassetmanager.getLocalItem(serverInfo.Id, stripLocalPrefix(itemId)).then(function (item) {

                    adjustGuidProperties(item.Item);

                    return Promise.resolve(item.Item);
                });
            }
        }

        return ApiClient.prototype.getItem.call(this, userId, itemId);
    };

    ApiClientEx.prototype.getLocalFolders = function (userId) {

        var serverInfo = this.serverInfo();
        userId = userId || serverInfo.UserId;

        return localassetmanager.getViews(serverInfo.Id, userId);
    };

    ApiClientEx.prototype.getCurrentUser = function () {

        var instance = this;

        return ApiClient.prototype.getCurrentUser.call(this).then(function (user) {

            appStorage.setItem('user-' + user.Id, JSON.stringify(user));
            return user;

        }, function (error) {

            var userId = instance.getCurrentUserId();

            if (userId && instance.accessToken()) {
                var json = appStorage.getItem('user-' + userId);

                if (json) {
                    return Promise.resolve(JSON.parse(json));
                }
            }

            return Promise.reject(error);
        });
    };

    ApiClientEx.prototype.getNextUpEpisodes = function (options) {

        if (options.SeriesId) {
            if (isLocalId(options.SeriesId)) {
                return Promise.resolve(createEmptyList());
            }
        }

        return ApiClient.prototype.getNextUpEpisodes.call(this, options);
    };

    ApiClientEx.prototype.getSeasons = function (itemId, options) {

        if (isLocalId(itemId)) {
            options.ParentId = itemId;
            return this.getItems(this.getCurrentUserId(), options);
        }

        return ApiClient.prototype.getSeasons.call(this, itemId, options);
    };

    ApiClientEx.prototype.getEpisodes = function (itemId, options) {

        if (isLocalId(options.SeasonId)) {
            options.ParentId = options.SeasonId;
            return this.getItems(this.getCurrentUserId(), options);
        }

        if (isLocalId(options.seasonId)) {
            options.ParentId = options.seasonId;
            return this.getItems(this.getCurrentUserId(), options);
        }

        // get episodes by recursion
        if (isLocalId(itemId)) {
            options.ParentId = itemId;
            options.Recursive = true;
            return this.getItems(this.getCurrentUserId(), options).then(function (items) {
                var items2 = items.Items.filter(function (item) {

                    return item.Type.toLowerCase() === 'episode';
                });

                var result = {
                    Items: items2,
                    TotalRecordCount: items2.length
                };

                return Promise.resolve(result);
            });
        }

        return ApiClient.prototype.getEpisodes.call(this, itemId, options);
    };

    ApiClientEx.prototype.getLatestOfflineItems = function (options) {

        // Supported options
        // MediaType - Audio/Video/Photo/Book/Game
        // Limit
        // Filters: 'IsNotFolder' or 'IsFolder'

        options.SortBy = 'DateCreated';
        options.SortOrder = 'Descending';

        var serverInfo = this.serverInfo();

        if (serverInfo) {

            return localassetmanager.getViewItems(serverInfo.Id, null, options).then(function (items) {

                items.forEach(function (item) {
                    adjustGuidProperties(item);
                });

                return Promise.resolve(items);
            });
        }

        return Promise.resolve([]);
    };

    ApiClientEx.prototype.getThemeMedia = function (userId, itemId, inherit) {

        if (isLocalViewId(itemId) || isLocalId(itemId) || isTopLevelLocalViewId(itemId)) {
            return Promise.reject();
        }

        return ApiClient.prototype.getThemeMedia.call(this, userId, itemId, inherit);
    };

    ApiClientEx.prototype.getSpecialFeatures = function (userId, itemId) {

        if (isLocalId(itemId)) {
            return Promise.resolve([]);
        }

        return ApiClient.prototype.getSpecialFeatures.call(this, userId, itemId);
    };

    ApiClientEx.prototype.getSimilarItems = function (itemId, options) {

        if (isLocalId(itemId)) {
            return Promise.resolve(createEmptyList());
        }

        return ApiClient.prototype.getSimilarItems.call(this, itemId, options);
    };

    ApiClientEx.prototype.updateFavoriteStatus = function (userId, itemId, isFavorite) {

        if (isLocalId(itemId)) {
            return Promise.resolve();
        }

        return ApiClient.prototype.updateFavoriteStatus.call(this, userId, itemId, isFavorite);
    };

    ApiClientEx.prototype.getScaledImageUrl = function (itemId, options) {

        if (isLocalId(itemId) || (options && options.itemid && isLocalId(options.itemid))) {

            var serverInfo = this.serverInfo();
            var id = stripLocalPrefix(itemId);

            return localassetmanager.getImageUrl(serverInfo.Id, id, options.type, 0);
        }

        return ApiClient.prototype.getScaledImageUrl.call(this, itemId, options);
    };

    ApiClientEx.prototype.reportPlaybackStart = function (options) {

        if (!options) {
            throw new Error("null options");
        }

        if (isLocalId(options.ItemId)) {
            return Promise.resolve();
        }

        return ApiClient.prototype.reportPlaybackStart.call(this, options);
    };

    ApiClientEx.prototype.reportPlaybackProgress = function (options) {

        if (!options) {
            throw new Error("null options");
        }

        if (isLocalId(options.ItemId)) {
            return Promise.resolve();
        }

        return ApiClient.prototype.reportPlaybackProgress.call(this, options);
    };

    ApiClientEx.prototype.reportPlaybackStopped = function (options) {

        if (!options) {
            throw new Error("null options");
        }

        if (isLocalId(options.ItemId)) {

            var serverInfo = this.serverInfo();

            var action =
            {
                Date: new Date().getTime(),
                ItemId: stripLocalPrefix(options.ItemId),
                PositionTicks: options.PositionTicks,
                ServerId: serverInfo.Id,
                Type: 0, // UserActionType.PlayedItem
                UserId: this.getCurrentUserId()
            };

            return localassetmanager.recordUserAction(action);
        }

        return ApiClient.prototype.reportPlaybackStopped.call(this, options);
    };

    ApiClientEx.prototype.getIntros = function (itemId) {

        if (isLocalId(itemId)) {
            return Promise.resolve({
                Items: [],
                TotalRecordCount: 0
            });
        }

        return ApiClient.prototype.getIntros.call(this, itemId);
    };

    ApiClientEx.prototype.getInstantMixFromItem = function (itemId, options) {

        if (isLocalId(itemId)) {
            return Promise.resolve({
                Items: [],
                TotalRecordCount: 0
            });
        }

        return ApiClient.prototype.getInstantMixFromItem.call(this, itemId, options);
    };

    ApiClientEx.prototype.getItemDownloadUrl = function (itemId) {

        if (isLocalId(itemId)) {

            var serverInfo = this.serverInfo();

            if (serverInfo) {

                return localassetmanager.getLocalItem(serverInfo.Id, stripLocalPrefix(itemId)).then(function (item) {

                    return Promise.resolve(item.LocalPath);
                });
            }
        }

        return ApiClient.prototype.getItemDownloadUrl.call(this, itemId);
    };

    return ApiClientEx;

});<commit_msg>update getPlaybackInfo<commit_after>define(['apiclientcore', 'localassetmanager', 'appStorage'], function (ApiClient, localassetmanager, appStorage) {
    'use strict';

    var localPrefix = 'local:';
    var localViewPrefix = 'localview:';

    function isLocalId(str) {
        return startsWith(str, localPrefix);
    }

    function isLocalViewId(str) {
        return startsWith(str, localViewPrefix);
    }

    function isTopLevelLocalViewId(str) {
        return str === 'localview';
    }

    function stripLocalPrefix(str) {
        var res = stripStart(str, localPrefix);
        res = stripStart(res, localViewPrefix);

        return res;
    }

    function startsWith(str, find) {

        if (str && find && str.length > find.length) {
            if (str.indexOf(find) === 0) {
                return true;
            }
        }

        return false;
    }

    function stripStart(str, find) {
        if (startsWith(str, find)) {
            return str.substr(find.length);
        }

        return str;
    }

    function createEmptyList() {
        var result = {
            Items: [],
            TotalRecordCount: 0
        };

        return result;
    }

    function convertGuidToLocal(guid) {

        if (!guid) {
            return null;
        }

        if (isLocalId(guid)) {
            return guid;
        }

        return 'local:' + guid;
    }

    function adjustGuidProperties(downloadedItem) {

        downloadedItem.Id = convertGuidToLocal(downloadedItem.Id);
        downloadedItem.SeriesId = convertGuidToLocal(downloadedItem.SeriesId);
        downloadedItem.SeasonId = convertGuidToLocal(downloadedItem.SeasonId);

        downloadedItem.AlbumId = convertGuidToLocal(downloadedItem.AlbumId);
        downloadedItem.ParentId = convertGuidToLocal(downloadedItem.ParentId);
        downloadedItem.ParentThumbItemId = convertGuidToLocal(downloadedItem.ParentThumbItemId);
        downloadedItem.ParentPrimaryImageItemId = convertGuidToLocal(downloadedItem.ParentPrimaryImageItemId);
        downloadedItem.PrimaryImageItemId = convertGuidToLocal(downloadedItem.PrimaryImageItemId);
        downloadedItem.ParentLogoItemId = convertGuidToLocal(downloadedItem.ParentLogoItemId);
        downloadedItem.ParentBackdropItemId = convertGuidToLocal(downloadedItem.ParentBackdropItemId);

        downloadedItem.ParentBackdropImageTags = null;
    }

    function getLocalView(instance, serverId, userId) {

        return instance.getLocalFolders(serverId, userId).then(function (views) {

            var localView = null;

            if (views.length > 0) {

                localView = {
                    Name: instance.downloadsTitleText || 'Downloads',
                    ServerId: serverId,
                    Id: 'localview',
                    Type: 'localview',
                    IsFolder: true
                };
            }

            return Promise.resolve(localView);
        });
    }

    /**
     * Creates a new api client instance
     * @param {String} serverAddress
     * @param {String} clientName s
     * @param {String} applicationVersion
     */
    function ApiClientEx(serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio) {

        ApiClient.call(this, serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio);
    }

    Object.assign(ApiClientEx.prototype, ApiClient.prototype);

    ApiClientEx.prototype.getPlaybackInfo = function (itemId, options, deviceProfile) {

        var onFailure = function () {
            return ApiClient.prototype.getPlaybackInfo.call(instance, itemId, options, deviceProfile);
        };

        if (isLocalId(itemId)) {
            return localassetmanager.getLocalItem(this.serverId(), stripLocalPrefix(itemId)).then(function (item) {

                // TODO: This was already done during the sync process, right? If so, remove it
                var mediaSources = item.Item.MediaSources.map(function (m) {
                    m.SupportsDirectPlay = true;
                    m.SupportsDirectStream = false;
                    m.SupportsTranscoding = false;
                    m.IsLocal = true;
                    return m;
                });

                return {
                    MediaSources: mediaSources
                };

            }, onFailure);
        }

        var instance = this;
        return localassetmanager.getLocalItem(this.serverId(), itemId).then(function (item) {

            if (item) {

                var mediaSources = item.Item.MediaSources.map(function (m) {
                    m.SupportsDirectPlay = true;
                    m.SupportsDirectStream = false;
                    m.SupportsTranscoding = false;
                    m.IsLocal = true;
                    return m;
                });

                return localassetmanager.fileExists(item.LocalPath).then(function (exists) {

                    if (exists) {

                        var res = {
                            MediaSources: mediaSources
                        };

                        return Promise.resolve(res);
                    }

                    return ApiClient.prototype.getPlaybackInfo.call(instance, itemId, options, deviceProfile);

                }, onFailure);
            }

            return ApiClient.prototype.getPlaybackInfo.call(instance, itemId, options, deviceProfile);

        }, onFailure);
    };

    ApiClientEx.prototype.getItems = function (userId, options) {

        var serverInfo = this.serverInfo();
        var i;

        if (serverInfo && options.ParentId === 'localview') {

            return this.getLocalFolders(serverInfo.Id, userId).then(function (items) {
                var result = {
                    Items: items,
                    TotalRecordCount: items.length
                };

                return Promise.resolve(result);
            });

        } else if (serverInfo && options && (isLocalId(options.ParentId) || isLocalViewId(options.ParentId))) {

            return localassetmanager.getViewItems(serverInfo.Id, userId, options).then(function (items) {

                items.forEach(function (item) {
                    adjustGuidProperties(item);
                });

                items.sort(function (a, b) { return a.SortName.toLowerCase().localeCompare(b.SortName.toLowerCase()); });

                var result = {
                    Items: items,
                    TotalRecordCount: items.length
                };

                return Promise.resolve(result);
            });
        } else if (options && options.ExcludeItemIds && options.ExcludeItemIds.length) {

            var exItems = options.ExcludeItemIds.split(',');

            for (i = 0; i < exItems.length; i++) {
                if (isLocalId(exItems[i])) {
                    return Promise.resolve(createEmptyList());
                }
            }
        } else if (options && options.Ids && options.Ids.length) {

            var ids = options.Ids.split(',');
            var hasLocal = false;

            for (i = 0; i < ids.length; i++) {
                if (isLocalId(ids[i])) {
                    hasLocal = true;
                }
            }

            if (hasLocal) {
                return localassetmanager.getItemsFromIds(serverInfo.Id, ids).then(function (items) {

                    items.forEach(function (item) {
                        adjustGuidProperties(item);
                    });

                    var result = {
                        Items: items,
                        TotalRecordCount: items.length
                    };

                    return Promise.resolve(result);
                });
            }
        }

        return ApiClient.prototype.getItems.call(this, userId, options);
    };

    ApiClientEx.prototype.getUserViews = function (options, userId) {

        var instance = this;

        return ApiClient.prototype.getUserViews.call(instance, options, userId).then(function (result) {

            var serverInfo = instance.serverInfo();
            if (serverInfo) {

                return getLocalView(instance, serverInfo.Id, userId).then(function (localView) {

                    if (localView) {

                        result.Items.push(localView);
                        result.TotalRecordCount++;
                    }

                    return Promise.resolve(result);
                });
            }

            return Promise.resolve(result);
        });
    };

    ApiClientEx.prototype.getItem = function (userId, itemId) {

        if (!itemId) {
            throw new Error("null itemId");
        }

        if (itemId) {
            itemId = itemId.toString();
        }

        var serverInfo;

        if (isTopLevelLocalViewId(itemId)) {

            serverInfo = this.serverInfo();

            if (serverInfo) {
                return getLocalView(this, serverInfo.Id, userId);
            }
        }

        if (isLocalViewId(itemId)) {

            serverInfo = this.serverInfo();

            if (serverInfo) {
                return this.getLocalFolders(serverInfo.Id, userId).then(function (items) {

                    var views = items.filter(function (item) {
                        return item.Id === itemId;
                    });

                    if (views.length > 0) {
                        return Promise.resolve(views[0]);
                    }

                    // TODO: Test consequence of this
                    return Promise.reject();
                });
            }
        }

        if (isLocalId(itemId)) {

            serverInfo = this.serverInfo();

            if (serverInfo) {
                return localassetmanager.getLocalItem(serverInfo.Id, stripLocalPrefix(itemId)).then(function (item) {

                    adjustGuidProperties(item.Item);

                    return Promise.resolve(item.Item);
                });
            }
        }

        return ApiClient.prototype.getItem.call(this, userId, itemId);
    };

    ApiClientEx.prototype.getLocalFolders = function (userId) {

        var serverInfo = this.serverInfo();
        userId = userId || serverInfo.UserId;

        return localassetmanager.getViews(serverInfo.Id, userId);
    };

    ApiClientEx.prototype.getCurrentUser = function () {

        var instance = this;

        return ApiClient.prototype.getCurrentUser.call(this).then(function (user) {

            appStorage.setItem('user-' + user.Id, JSON.stringify(user));
            return user;

        }, function (error) {

            var userId = instance.getCurrentUserId();

            if (userId && instance.accessToken()) {
                var json = appStorage.getItem('user-' + userId);

                if (json) {
                    return Promise.resolve(JSON.parse(json));
                }
            }

            return Promise.reject(error);
        });
    };

    ApiClientEx.prototype.getNextUpEpisodes = function (options) {

        if (options.SeriesId) {
            if (isLocalId(options.SeriesId)) {
                return Promise.resolve(createEmptyList());
            }
        }

        return ApiClient.prototype.getNextUpEpisodes.call(this, options);
    };

    ApiClientEx.prototype.getSeasons = function (itemId, options) {

        if (isLocalId(itemId)) {
            options.ParentId = itemId;
            return this.getItems(this.getCurrentUserId(), options);
        }

        return ApiClient.prototype.getSeasons.call(this, itemId, options);
    };

    ApiClientEx.prototype.getEpisodes = function (itemId, options) {

        if (isLocalId(options.SeasonId)) {
            options.ParentId = options.SeasonId;
            return this.getItems(this.getCurrentUserId(), options);
        }

        if (isLocalId(options.seasonId)) {
            options.ParentId = options.seasonId;
            return this.getItems(this.getCurrentUserId(), options);
        }

        // get episodes by recursion
        if (isLocalId(itemId)) {
            options.ParentId = itemId;
            options.Recursive = true;
            return this.getItems(this.getCurrentUserId(), options).then(function (items) {
                var items2 = items.Items.filter(function (item) {

                    return item.Type.toLowerCase() === 'episode';
                });

                var result = {
                    Items: items2,
                    TotalRecordCount: items2.length
                };

                return Promise.resolve(result);
            });
        }

        return ApiClient.prototype.getEpisodes.call(this, itemId, options);
    };

    ApiClientEx.prototype.getLatestOfflineItems = function (options) {

        // Supported options
        // MediaType - Audio/Video/Photo/Book/Game
        // Limit
        // Filters: 'IsNotFolder' or 'IsFolder'

        options.SortBy = 'DateCreated';
        options.SortOrder = 'Descending';

        var serverInfo = this.serverInfo();

        if (serverInfo) {

            return localassetmanager.getViewItems(serverInfo.Id, null, options).then(function (items) {

                items.forEach(function (item) {
                    adjustGuidProperties(item);
                });

                return Promise.resolve(items);
            });
        }

        return Promise.resolve([]);
    };

    ApiClientEx.prototype.getThemeMedia = function (userId, itemId, inherit) {

        if (isLocalViewId(itemId) || isLocalId(itemId) || isTopLevelLocalViewId(itemId)) {
            return Promise.reject();
        }

        return ApiClient.prototype.getThemeMedia.call(this, userId, itemId, inherit);
    };

    ApiClientEx.prototype.getSpecialFeatures = function (userId, itemId) {

        if (isLocalId(itemId)) {
            return Promise.resolve([]);
        }

        return ApiClient.prototype.getSpecialFeatures.call(this, userId, itemId);
    };

    ApiClientEx.prototype.getSimilarItems = function (itemId, options) {

        if (isLocalId(itemId)) {
            return Promise.resolve(createEmptyList());
        }

        return ApiClient.prototype.getSimilarItems.call(this, itemId, options);
    };

    ApiClientEx.prototype.updateFavoriteStatus = function (userId, itemId, isFavorite) {

        if (isLocalId(itemId)) {
            return Promise.resolve();
        }

        return ApiClient.prototype.updateFavoriteStatus.call(this, userId, itemId, isFavorite);
    };

    ApiClientEx.prototype.getScaledImageUrl = function (itemId, options) {

        if (isLocalId(itemId) || (options && options.itemid && isLocalId(options.itemid))) {

            var serverInfo = this.serverInfo();
            var id = stripLocalPrefix(itemId);

            return localassetmanager.getImageUrl(serverInfo.Id, id, options.type, 0);
        }

        return ApiClient.prototype.getScaledImageUrl.call(this, itemId, options);
    };

    ApiClientEx.prototype.reportPlaybackStart = function (options) {

        if (!options) {
            throw new Error("null options");
        }

        if (isLocalId(options.ItemId)) {
            return Promise.resolve();
        }

        return ApiClient.prototype.reportPlaybackStart.call(this, options);
    };

    ApiClientEx.prototype.reportPlaybackProgress = function (options) {

        if (!options) {
            throw new Error("null options");
        }

        if (isLocalId(options.ItemId)) {
            return Promise.resolve();
        }

        return ApiClient.prototype.reportPlaybackProgress.call(this, options);
    };

    ApiClientEx.prototype.reportPlaybackStopped = function (options) {

        if (!options) {
            throw new Error("null options");
        }

        if (isLocalId(options.ItemId)) {

            var serverInfo = this.serverInfo();

            var action =
            {
                Date: new Date().getTime(),
                ItemId: stripLocalPrefix(options.ItemId),
                PositionTicks: options.PositionTicks,
                ServerId: serverInfo.Id,
                Type: 0, // UserActionType.PlayedItem
                UserId: this.getCurrentUserId()
            };

            return localassetmanager.recordUserAction(action);
        }

        return ApiClient.prototype.reportPlaybackStopped.call(this, options);
    };

    ApiClientEx.prototype.getIntros = function (itemId) {

        if (isLocalId(itemId)) {
            return Promise.resolve({
                Items: [],
                TotalRecordCount: 0
            });
        }

        return ApiClient.prototype.getIntros.call(this, itemId);
    };

    ApiClientEx.prototype.getInstantMixFromItem = function (itemId, options) {

        if (isLocalId(itemId)) {
            return Promise.resolve({
                Items: [],
                TotalRecordCount: 0
            });
        }

        return ApiClient.prototype.getInstantMixFromItem.call(this, itemId, options);
    };

    ApiClientEx.prototype.getItemDownloadUrl = function (itemId) {

        if (isLocalId(itemId)) {

            var serverInfo = this.serverInfo();

            if (serverInfo) {

                return localassetmanager.getLocalItem(serverInfo.Id, stripLocalPrefix(itemId)).then(function (item) {

                    return Promise.resolve(item.LocalPath);
                });
            }
        }

        return ApiClient.prototype.getItemDownloadUrl.call(this, itemId);
    };

    return ApiClientEx;

});<|endoftext|>


<commit_before>import R from 'ramda';
import * as ActionType from './actionTypes';
import * as STATUS from './constants/statuses';
import * as EDITOR_MODE from './constants/editorModes';
import Selectors from './selectors';
import { uploadToEspruino } from './utils/espruino';

const getTimestamp = () => new Date().getTime();

export const addError = (error) => ({
  type: ActionType.ERROR_ADD,
  payload: error,
  meta: {
    timestamp: getTimestamp(),
    status: STATUS.STARTED,
  },
});

export const deleteError = (id) => ({
  type: ActionType.ERROR_DELETE,
  payload: {
    id,
  },
  meta: {
    timestamp: getTimestamp(),
    status: STATUS.DELETED,
  },
});

export const moveNode = (id, position) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.moveNode(projectState, id, position);

  dispatch({
    type: ActionType.NODE_MOVE,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const dragNode = (id, position) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.dragNode(projectState, id, position);

  dispatch({
    type: ActionType.NODE_MOVE,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const addNode = (typeId, position, patchId) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addNode(projectState, typeId, position, patchId);

  dispatch({
    type: ActionType.NODE_ADD,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const deleteNode = (id) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.deleteNode(projectState, id);

  if (preparedData.payload.nodeType.error) {
    dispatch(addError({ message: preparedData.payload.nodeType.error }));
    return;
  }

  dispatch({
    type: ActionType.NODE_DELETE,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const addLink = (data1, data2) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addLink(projectState, data1, data2);

  dispatch({
    type: ActionType.LINK_ADD,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const deleteLink = (id) => ({
  type: ActionType.LINK_DELETE,
  payload: {
    id,
  },
});

export const updateMeta = (data) => ({
  type: ActionType.META_UPDATE,
  payload: data,
});

export const setNodeSelection = (id) => ({
  type: ActionType.EDITOR_SELECT_NODE,
  payload: {
    id,
  },
});

export const setPinSelection = (nodeId, pinKey) => ({
  type: ActionType.EDITOR_SELECT_PIN,
  payload: {
    nodeId,
    pinKey,
  },
});

export const setLinkSelection = (id) => ({
  type: ActionType.EDITOR_SELECT_LINK,
  payload: {
    id,
  },
});

export const setMode = (mode) => (dispatch, getState) => {
  if (Selectors.Editor.getMode(getState()) === mode) {
    return;
  }

  dispatch({
    type: ActionType.EDITOR_SET_MODE,
    payload: {
      mode,
    },
  });
};

export const deselectAll = () => (dispatch, getState) => {
  const state = getState();
  if (!Selectors.Editor.hasSelection(state)) { return; }

  dispatch({
    type: ActionType.EDITOR_DESELECT_ALL,
    payload: {},
  });
  if (!Selectors.Editor.getModeChecks(state).isDefault) {
    dispatch(setMode(EDITOR_MODE.DEFAULT));
  }
};

export const selectNode = (id) => (dispatch, getState) => {
  const state = getState();
  const selection = Selectors.Editor.getSelection(state);
  const isSelected = Selectors.Editor.isNodeSelected(selection, id);
  const deselect = dispatch(deselectAll());
  const result = [];
  if (deselect) {
    result.push(deselect);
  }

  if (!isSelected) {
    result.push(dispatch(setNodeSelection(id)));
  }

  return result;
};

export const addAndSelectNode = (typeId, position, curPatchId) => (dispatch, getState) => {
  dispatch(addNode(typeId, position, curPatchId));
  dispatch(setMode(EDITOR_MODE.DEFAULT));

  const newId = Selectors.Project.getLastNodeId(getState());
  dispatch(selectNode(newId));
};

export const linkPin = (nodeId, pinKey) => (dispatch, getState) => {
  const data = {
    nodeId,
    pinKey,
  };
  const state = getState();
  const selected = state.editor.linkingPin;
  const deselect = dispatch(deselectAll());
  const result = [];
  if (deselect) {
    result.push(deselect);
  }

  const pins = [selected, data];

  const notEquals = R.not(R.equals(selected, data));

  if (notEquals && selected !== null) {
    const validation = Selectors.Project.validateLink(state, pins);
    if (validation.isValid) {
      result.push(dispatch(addLink(pins[0], pins[1])));
    } else {
      result.push(dispatch(addError({ message: validation.message })));
    }
    dispatch(setMode(EDITOR_MODE.DEFAULT));
  } else if (notEquals) {
    dispatch(setMode(EDITOR_MODE.LINKING));
    result.push(dispatch(setPinSelection(nodeId, pinKey)));
  }

  return result;
};

export const selectLink = (id) => (dispatch, getState) => {
  const state = getState();
  const selection = Selectors.Editor.getSelection(state);
  const isSelected = Selectors.Editor.isLinkSelected(selection, id);
  const deselect = dispatch(deselectAll());
  const result = [];
  if (deselect) {
    result.push(deselect);
  }

  if (!isSelected) {
    result.push(dispatch(setLinkSelection(id)));
  }

  return result;
};

export const setSelectedNodeType = (id) => ({
  type: ActionType.EDITOR_SET_SELECTED_NODETYPE,
  payload: {
    id,
  },
});

export const updateNodeProperty = (nodeId, propKey, propValue) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.updateNodeProperty(
    projectState,
    nodeId,
    propKey,
    propValue
  );

  dispatch({
    type: ActionType.NODE_UPDATE_PROPERTY,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const loadProjectFromJSON = (json) => ({
  type: ActionType.PROJECT_LOAD_DATA,
  payload: json,
});

export const upload = () => (dispatch, getState) => {
  const project = Selectors.Project.getProjectPojo(getState());
  const processes = Selectors.Processes.getProccesses(getState());
  const newId = Selectors.Processes.getNewId(processes);

  dispatch({
    type: ActionType.UPLOAD,
    meta: { status: STATUS.STARTED },
  });

  const progress = (message, percentage) => dispatch({
    type: ActionType.UPLOAD,
    meta: { status: STATUS.PROGRESSED },
    payload: {
      id: newId,
      message,
      percentage,
    },
  });

  const succeed = () => dispatch({
    type: ActionType.UPLOAD,
    payload: {
      id: newId,
    },
    meta: { status: STATUS.SUCCEEDED },
  });

  const fail = (err) => dispatch({
    type: ActionType.UPLOAD,
    meta: { status: STATUS.FAILED },
    payload: {
      id: newId,
      message: err.message,
    },
  });

  uploadToEspruino(project, progress)
    .then(succeed)
    .catch(err => {
      if (err.constructor !== Error) {
        throw err;
      }

      fail(err);
    });
};

export const deleteProcess = (id, type) => ({
  type,
  payload: {
    id,
  },
  meta: {
    status: STATUS.DELETED,
  },
});

export const deleteSelection = () => (dispatch, getState) => {
  const selection = Selectors.Editor.getSelection(getState());
  const DELETE_ACTIONS = {
    Node: deleteNode,
    Link: deleteLink,
  };

  selection.forEach((select) => {
    dispatch(
      DELETE_ACTIONS[select.entity](select.id)
    );
  });
};

export const undoPatch = (id) => ({
  type: ActionType.getPatchUndoType(id),
  payload: {},
});

export const redoPatch = (id) => ({
  type: ActionType.getPatchRedoType(id),
  payload: {},
});

export const clearHistoryPatch = (id) => ({
  type: ActionType.getPatchClearHistoryType(id),
  payload: {},
});

export const switchPatch = (id) => (dispatch, getState) => {
  if (Selectors.Editor.getCurrentPatchId(getState()) === id) { return; }

  dispatch(deselectAll());
  dispatch({
    type: ActionType.EDITOR_SWITCH_PATCH,
    payload: {
      id,
    },
  });
};

export const addPatch = (name, folderId) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addPatch(projectState, name, folderId);

  dispatch({
    type: ActionType.PATCH_ADD,
    payload: preparedData,
  });
};

export const renamePatch = (id, name) => ({
  type: ActionType.PATCH_RENAME,
  payload: {
    name,
  },
  meta: {
    patchId: id,
  },
});

export const deletePatch = (id) => ({
  type: ActionType.PATCH_DELETE,
  payload: {
    id,
  },
});

export const movePatch = (changes) => ({
  type: ActionType.PATCH_MOVE,
  payload: {
    id: changes.id,
    folderId: changes.folderId,
  },
  meta: {
    patchId: changes.id,
  },
});

export const addFolder = (name, parentId) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addFolder(projectState, name, parentId);
  dispatch({
    type: ActionType.FOLDER_ADD,
    payload: preparedData,
  });
};

export const renameFolder = (id, name) => ({
  type: ActionType.FOLDER_RENAME,
  payload: {
    id,
    name,
  },
});

export const deleteFolder = (id) => (dispatch, getState) => {
  const folders = Selectors.Project.getFoldersByFolderId(getState(), id);
  const patches = Selectors.Project.getPatchesByFolderId(getState(), id);

  folders.forEach(folder => dispatch(deleteFolder(folder.id)));
  patches.forEach(patch => dispatch(deletePatch(patch.id)));
  dispatch({
    type: ActionType.FOLDER_DELETE,
    payload: {
      id,
    },
  });
};

export const moveFolder = (changes) => ({
  type: ActionType.FOLDER_MOVE,
  payload: {
    id: changes.id,
    parentId: changes.parentId,
  },
});

export const closeTab = (id) => ({
  type: ActionType.TAB_CLOSE,
  payload: {
    id,
  },
});

export const sortTabs = (newOrderObject) => ({
  type: ActionType.TAB_SORT,
  payload: newOrderObject,
});
<commit_msg>fix(actions): fix argument names and simplified linkPin method (thanks for comments in PR #159)<commit_after>import R from 'ramda';
import * as ActionType from './actionTypes';
import * as STATUS from './constants/statuses';
import * as EDITOR_MODE from './constants/editorModes';
import Selectors from './selectors';
import { uploadToEspruino } from './utils/espruino';

const getTimestamp = () => new Date().getTime();

export const addError = (error) => ({
  type: ActionType.ERROR_ADD,
  payload: error,
  meta: {
    timestamp: getTimestamp(),
    status: STATUS.STARTED,
  },
});

export const deleteError = (id) => ({
  type: ActionType.ERROR_DELETE,
  payload: {
    id,
  },
  meta: {
    timestamp: getTimestamp(),
    status: STATUS.DELETED,
  },
});

export const moveNode = (id, position) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.moveNode(projectState, id, position);

  dispatch({
    type: ActionType.NODE_MOVE,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const dragNode = (id, position) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.dragNode(projectState, id, position);

  dispatch({
    type: ActionType.NODE_MOVE,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const addNode = (typeId, position, patchId) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addNode(projectState, typeId, position, patchId);

  dispatch({
    type: ActionType.NODE_ADD,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const deleteNode = (id) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.deleteNode(projectState, id);

  if (preparedData.payload.nodeType.error) {
    dispatch(addError({ message: preparedData.payload.nodeType.error }));
    return;
  }

  dispatch({
    type: ActionType.NODE_DELETE,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const addLink = (pin1, pin2) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addLink(projectState, pin1, pin2);

  dispatch({
    type: ActionType.LINK_ADD,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const deleteLink = (id) => ({
  type: ActionType.LINK_DELETE,
  payload: {
    id,
  },
});

export const updateMeta = (data) => ({
  type: ActionType.META_UPDATE,
  payload: data,
});

export const setNodeSelection = (id) => ({
  type: ActionType.EDITOR_SELECT_NODE,
  payload: {
    id,
  },
});

export const setPinSelection = (nodeId, pinKey) => ({
  type: ActionType.EDITOR_SELECT_PIN,
  payload: {
    nodeId,
    pinKey,
  },
});

export const setLinkSelection = (id) => ({
  type: ActionType.EDITOR_SELECT_LINK,
  payload: {
    id,
  },
});

export const setMode = (mode) => (dispatch, getState) => {
  if (Selectors.Editor.getMode(getState()) === mode) {
    return;
  }

  dispatch({
    type: ActionType.EDITOR_SET_MODE,
    payload: {
      mode,
    },
  });
};

export const deselectAll = () => (dispatch, getState) => {
  const state = getState();
  if (!Selectors.Editor.hasSelection(state)) { return; }

  dispatch({
    type: ActionType.EDITOR_DESELECT_ALL,
    payload: {},
  });
  if (!Selectors.Editor.getModeChecks(state).isDefault) {
    dispatch(setMode(EDITOR_MODE.DEFAULT));
  }
};

export const selectNode = (id) => (dispatch, getState) => {
  const state = getState();
  const selection = Selectors.Editor.getSelection(state);
  const isSelected = Selectors.Editor.isNodeSelected(selection, id);
  const deselect = dispatch(deselectAll());
  const result = [];
  if (deselect) {
    result.push(deselect);
  }

  if (!isSelected) {
    result.push(dispatch(setNodeSelection(id)));
  }

  return result;
};

export const addAndSelectNode = (typeId, position, curPatchId) => (dispatch, getState) => {
  dispatch(addNode(typeId, position, curPatchId));
  dispatch(setMode(EDITOR_MODE.DEFAULT));

  const newId = Selectors.Project.getLastNodeId(getState());
  dispatch(selectNode(newId));
};

export const linkPin = (nodeId, pinKey) => (dispatch, getState) => {
  const data = {
    nodeId,
    pinKey,
  };
  const state = getState();
  const selected = state.editor.linkingPin;
  const deselect = dispatch(deselectAll());
  const result = [];
  if (deselect) {
    result.push(deselect);
  }

  const pins = [selected, data];

  if (R.equals(selected, data)) {
    // linking a pin to itself
    return result;
  }

  let action;

  if (selected) {
    const validation = Selectors.Project.validateLink(state, pins);
    action = validation.isValid ?
      addLink(pins[0], pins[1]) :
      addError({ message: validation.message });
    dispatch(setMode(EDITOR_MODE.DEFAULT));
  } else {
    dispatch(setMode(EDITOR_MODE.LINKING));
    action = setPinSelection(nodeId, pinKey);
  }

  return R.append(dispatch(action), result);
};

export const selectLink = (id) => (dispatch, getState) => {
  const state = getState();
  const selection = Selectors.Editor.getSelection(state);
  const isSelected = Selectors.Editor.isLinkSelected(selection, id);
  const deselect = dispatch(deselectAll());
  const result = [];
  if (deselect) {
    result.push(deselect);
  }

  if (!isSelected) {
    result.push(dispatch(setLinkSelection(id)));
  }

  return result;
};

export const setSelectedNodeType = (id) => ({
  type: ActionType.EDITOR_SET_SELECTED_NODETYPE,
  payload: {
    id,
  },
});

export const updateNodeProperty = (nodeId, propKey, propValue) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.updateNodeProperty(
    projectState,
    nodeId,
    propKey,
    propValue
  );

  dispatch({
    type: ActionType.NODE_UPDATE_PROPERTY,
    payload: preparedData.payload,
    meta: preparedData.meta,
  });
};

export const loadProjectFromJSON = (json) => ({
  type: ActionType.PROJECT_LOAD_DATA,
  payload: json,
});

export const upload = () => (dispatch, getState) => {
  const project = Selectors.Project.getProjectPojo(getState());
  const processes = Selectors.Processes.getProccesses(getState());
  const newId = Selectors.Processes.getNewId(processes);

  dispatch({
    type: ActionType.UPLOAD,
    meta: { status: STATUS.STARTED },
  });

  const progress = (message, percentage) => dispatch({
    type: ActionType.UPLOAD,
    meta: { status: STATUS.PROGRESSED },
    payload: {
      id: newId,
      message,
      percentage,
    },
  });

  const succeed = () => dispatch({
    type: ActionType.UPLOAD,
    payload: {
      id: newId,
    },
    meta: { status: STATUS.SUCCEEDED },
  });

  const fail = (err) => dispatch({
    type: ActionType.UPLOAD,
    meta: { status: STATUS.FAILED },
    payload: {
      id: newId,
      message: err.message,
    },
  });

  uploadToEspruino(project, progress)
    .then(succeed)
    .catch(err => {
      if (err.constructor !== Error) {
        throw err;
      }

      fail(err);
    });
};

export const deleteProcess = (id, type) => ({
  type,
  payload: {
    id,
  },
  meta: {
    status: STATUS.DELETED,
  },
});

export const deleteSelection = () => (dispatch, getState) => {
  const selection = Selectors.Editor.getSelection(getState());
  const DELETE_ACTIONS = {
    Node: deleteNode,
    Link: deleteLink,
  };

  selection.forEach((select) => {
    dispatch(
      DELETE_ACTIONS[select.entity](select.id)
    );
  });
};

export const undoPatch = (id) => ({
  type: ActionType.getPatchUndoType(id),
  payload: {},
});

export const redoPatch = (id) => ({
  type: ActionType.getPatchRedoType(id),
  payload: {},
});

export const clearHistoryPatch = (id) => ({
  type: ActionType.getPatchClearHistoryType(id),
  payload: {},
});

export const switchPatch = (id) => (dispatch, getState) => {
  if (Selectors.Editor.getCurrentPatchId(getState()) === id) { return; }

  dispatch(deselectAll());
  dispatch({
    type: ActionType.EDITOR_SWITCH_PATCH,
    payload: {
      id,
    },
  });
};

export const addPatch = (name, folderId) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addPatch(projectState, name, folderId);

  dispatch({
    type: ActionType.PATCH_ADD,
    payload: preparedData,
  });
};

export const renamePatch = (id, name) => ({
  type: ActionType.PATCH_RENAME,
  payload: {
    name,
  },
  meta: {
    patchId: id,
  },
});

export const deletePatch = (id) => ({
  type: ActionType.PATCH_DELETE,
  payload: {
    id,
  },
});

export const movePatch = (changes) => ({
  type: ActionType.PATCH_MOVE,
  payload: {
    id: changes.id,
    folderId: changes.folderId,
  },
  meta: {
    patchId: changes.id,
  },
});

export const addFolder = (name, parentId) => (dispatch, getState) => {
  const projectState = Selectors.Project.getProject(getState());
  const preparedData = Selectors.Prepare.addFolder(projectState, name, parentId);
  dispatch({
    type: ActionType.FOLDER_ADD,
    payload: preparedData,
  });
};

export const renameFolder = (id, name) => ({
  type: ActionType.FOLDER_RENAME,
  payload: {
    id,
    name,
  },
});

export const deleteFolder = (id) => (dispatch, getState) => {
  const folders = Selectors.Project.getFoldersByFolderId(getState(), id);
  const patches = Selectors.Project.getPatchesByFolderId(getState(), id);

  folders.forEach(folder => dispatch(deleteFolder(folder.id)));
  patches.forEach(patch => dispatch(deletePatch(patch.id)));
  dispatch({
    type: ActionType.FOLDER_DELETE,
    payload: {
      id,
    },
  });
};

export const moveFolder = (changes) => ({
  type: ActionType.FOLDER_MOVE,
  payload: {
    id: changes.id,
    parentId: changes.parentId,
  },
});

export const closeTab = (id) => ({
  type: ActionType.TAB_CLOSE,
  payload: {
    id,
  },
});

export const sortTabs = (newOrderObject) => ({
  type: ActionType.TAB_SORT,
  payload: newOrderObject,
});
<|endoftext|>


<commit_before>export const identified = {}
const baseUrl = 'https://drdyne.github.io/eve-daytripper'

export const get = id => {
  return fetch(baseUrl + `/static/mapSolarSystems.json`)
  .then(response => response.json())
  .then(systems => systems.find(s => id === s.solarSystemID))
  .then(system => ({
    id: system.solarSystemID,
    name: system.solarSystemName,
    sec: system.security
  }))
}

export const identify = name => {
  const Name = name.toUpperCase()
  if ( identified[Name] ) return Promise.resolve(identified[Name])

  //static content served from gh-pages... check /docs/static/typeids/***.json
  //https://drdyne.github.io/eve-daytripper/static/typeids/A.json

  //return fetch(`/static/typeids/${cat}.json`)
  return fetch(baseUrl + `/static/mapSolarSystems.json`)
  .then(response => response.json())
  .then(systems => systems.find(s => Name.toUpperCase() === s.solarSystemName.toUpperCase()))
  .then(system => {
    if ( !system ) return Promise.reject(`Unknown system: ${Name}`)

    const { solarSystemID, solarSystemName, security } = system
    console.log(`identified ${solarSystemID}:${solarSystemName}`)

    identified[Name] = {
      id: solarSystemID,
      Name: solarSystemName,
      sec: security
    }
    return identified[Name]
  })
}

const Jita = '30000142'
const Hek = '30002053'
export const route = (origin={}, destination={}) => {
  const url = `https://esi.tech.ccp.is/latest/route/${origin.id||Hek}/${destination.id||Jita}/?datasource=tranquility&flag=shortest`
  return fetch(url)
  .then(response => response.json())
  .then(route => {
    console.log(route)
    return Promise.all(route.map(get))
  })
}

export default {
  identify,
  get,
  route
}
<commit_msg>added shortest(o,d) and safest(o,d) to gps api<commit_after>export const identified = {}
const baseUrl = 'https://drdyne.github.io/eve-daytripper'

export const get = id => {
  return fetch(baseUrl + `/static/mapSolarSystems.json`)
  .then(response => response.json())
  .then(systems => systems.find(s => id === s.solarSystemID))
  .then(system => ({
    id: system.solarSystemID,
    name: system.solarSystemName,
    sec: system.security
  }))
}

export const identify = name => {
  const Name = name.toUpperCase()
  if ( identified[Name] ) return Promise.resolve(identified[Name])

  //static content served from gh-pages... check /docs/static/typeids/***.json
  //https://drdyne.github.io/eve-daytripper/static/typeids/A.json

  //return fetch(`/static/typeids/${cat}.json`)
  return fetch(baseUrl + `/static/mapSolarSystems.json`)
  .then(response => response.json())
  .then(systems => systems.find(s => Name === s.solarSystemName.toUpperCase()))
  .then(system => {
    if ( !system ) return Promise.reject(`Unknown system: ${Name}`)

    const { solarSystemID, solarSystemName, security } = system
    console.log(`identified ${solarSystemID}:${solarSystemName}`)

    identified[Name] = {
      id: solarSystemID,
      Name: solarSystemName,
      sec: security
    }
    return identified[Name]
  })
}

const Jita = '30000142'
const Hek = '30002053'
export const route = (origin, destination, options={shortest, safest}) => {
  const flag = options.shortest
  ? 'shortest'
  : options.safest
  ? 'secure'
  : 'shortest'

  const url = `https://esi.tech.ccp.is/latest/route/${origin.id}/${destination.id}/?datasource=tranquility&flag=${flag}`
  return fetch(url)
  .then(response => response.json())
  .then(route => Promise.all(route.map(get)))
  .then(route => route[0].id === origin.id
  ? route
  : route.reverse()) // sometimes ccp server reverses order /shrug
}

export const shortest = (origin, destination) => (
  route(origin, destination, {shortest: true})
)

export const safest = (origin, destination) => {
  route(origin, destination, {safest :true})
}

export default {
  identify,
  get,
  route,
  shortest,
  safest,
}
<|endoftext|>


<commit_before>'use strict';

const fs         = require('fs')
const os         = require('os')
const path       = require('path')

const license    = require('./helpers/license')
const autofind   = require('./helpers/autofind')
const patch      = require('./helpers/patch')

const setup      = require('./tasks/setup')
const download   = require('./tasks/download')
const prerender  = require('./tasks/actions')('prerender')
const script     = require('./tasks/script')
const render     = require('./tasks/render')
const postrender = require('./tasks/actions')('postrender')
const cleanup    = require('./tasks/cleanup')

//
// https://video.stackexchange.com/questions/16706/rendered-file-with-after-effects-is-very-huge
//

module.exports = (job, settings) => {
    settings = Object.assign({}, settings);
    settings.logger = settings.logger || { log: function() {} };

    const binaryAuto = autofind(settings);
    const binaryUser = settings.binary && fs.existsSync(settings.binary) ? settings.binary : null;

    if (!binaryUser && !binaryAuto) {
        return Promise.reject(new Error('you should provide a proper path to After Effects\' \"aerender\" binary'))
    }

    if (binaryAuto && !binaryUser) {
        settings.logger.log('using automatically determined directory of After Effects installation:')
        settings.logger.log(' - ' + binaryAuto)
    }

    settings = Object.assign({
        workpath: path.join(os.tmpdir(), 'nexrender'),

        addLicense: true,
        forceCommandLinePatch: false,

        renderLogs: false,
        multiFrames: false,
        maxMemoryPercent: undefined,
        imageCachePercent: undefined,
    }, settings, {
        binary: binaryUser || binaryAuto,
    })

    // make sure we will have absolute path
    if (!path.isAbsolute(settings.workpath)) {
        settings.workpath = path.join(process.cwd(), settings.workpath);
    }

    // add license helper
    if (settings.addLicense) {
        license(settings);
    }

    // attempt to patch the default
    // Scripts/commandLineRenderer.jsx
    patch(settings);

    return Promise.resolve(job)
        .then(job => setup(job, settings))
        .then(job => download(job, settings))
        .then(job => prerender(job, settings))
        .then(job => script(job, settings))
        .then(job => render(job, settings))
        .then(job => postrender(job, settings))
        .then(job => cleanup(job, settings))
}
<commit_msg>splitted core to init and render<commit_after>'use strict';

const fs         = require('fs')
const os         = require('os')
const path       = require('path')

const license    = require('./helpers/license')
const autofind   = require('./helpers/autofind')
const patch      = require('./helpers/patch')

const setup      = require('./tasks/setup')
const download   = require('./tasks/download')
const prerender  = require('./tasks/actions')('prerender')
const script     = require('./tasks/script')
const render     = require('./tasks/render')
const postrender = require('./tasks/actions')('postrender')
const cleanup    = require('./tasks/cleanup')

//
// https://video.stackexchange.com/questions/16706/rendered-file-with-after-effects-is-very-huge
//

const init = (settings) => {
    settings = Object.assign({}, settings);
    settings.logger = settings.logger || { log: function() {} };

    const binaryAuto = autofind(settings);
    const binaryUser = settings.binary && fs.existsSync(settings.binary) ? settings.binary : null;

    if (!binaryUser && !binaryAuto) {
        return Promise.reject(new Error('you should provide a proper path to After Effects\' \"aerender\" binary'))
    }

    if (binaryAuto && !binaryUser) {
        settings.logger.log('using automatically determined directory of After Effects installation:')
        settings.logger.log(' - ' + binaryAuto)
    }

    settings = Object.assign({
        workpath: path.join(os.tmpdir(), 'nexrender'),

        addLicense: true,
        forceCommandLinePatch: false,

        renderLogs: false,
        multiFrames: false,
        maxMemoryPercent: undefined,
        imageCachePercent: undefined,
    }, settings, {
        binary: binaryUser || binaryAuto,
    })

    // make sure we will have absolute path
    if (!path.isAbsolute(settings.workpath)) {
        settings.workpath = path.join(process.cwd(), settings.workpath);
    }

    // add license helper
    if (settings.addLicense) {
        license(settings);
    }

    // attempt to patch the default
    // Scripts/commandLineRenderer.jsx
    patch(settings);

    return settings;
}

const render = (job, settings) => {
    return Promise.resolve(job)
        .then(job => setup(job, settings))
        .then(job => download(job, settings))
        .then(job => prerender(job, settings))
        .then(job => script(job, settings))
        .then(job => render(job, settings))
        .then(job => postrender(job, settings))
        .then(job => cleanup(job, settings))
}

module.exports = { init, render }
<|endoftext|>


<commit_before>import './Globals/Filters'
import './Globals/Functions'

import './ViewEnvironment'

import Nunjucks from 'nunjucks'
import fs from 'fs'
import path from 'path'

export class ViewFactory {
	app = null
	nunjucks = null
	viewPath = null

	constructor(app) {
		this.app = app

		this.viewPath = app.paths.base(app.config.get('view.path', 'resources/views'))

		const loader = new Nunjucks.FileSystemLoader(this.viewPath, {
			watch: app.config.get('view.watch', app.env() === 'local'),
			noCache: app.config.get('view.disable_cache', false)
		})

		this.nunjucks = new ViewEnvironment(loader, {
			autoescape: app.config.get('view.autoescape', true),
			trimBlocks: app.config.get('view.trim_blocks', false),
			lstripBlocks: app.config.get('view.lstrip_blocks', false),
			throwOnUndefined: app.config.get('view.throw_on_undefined', false)
		})
		this.nunjucks.express(app.express)
		app.express.set('view engine', 'njk')

		Filters(this)
		Functions(this)
	}

	exists(view) {
		return new Promise((resolve, reject) => {
			fs.stat(path.join(this.viewPath, view), (err, stats) => {
				if(!err.isNil) {
					if(err.code === 'ENOENT') {
						return resolve(false)
					}

					return reject(err)
				}

				resolve(stats.isFile())
			})
		})
	}

	share(name, value) {
		this.nunjucks.addGlobal(name, value)
	}

	addFilter(name, callback) {
		this.nunjucks.addFilter(callback)
	}

	addFunction(name, callback) {
		this.nunjucks.addGlobal(name, callback)
	}

	addExtension(name, extension) {
		this.nunjucks.addExtension(name, extension)
	}

}
<commit_msg>Fixed bug in addFilter where it wasn’t passing through name<commit_after>import './Globals/Filters'
import './Globals/Functions'

import './ViewEnvironment'

import Nunjucks from 'nunjucks'
import fs from 'fs'
import path from 'path'

export class ViewFactory {
	app = null
	nunjucks = null
	viewPath = null

	constructor(app) {
		this.app = app

		this.viewPath = app.paths.base(app.config.get('view.path', 'resources/views'))

		const loader = new Nunjucks.FileSystemLoader(this.viewPath, {
			watch: app.config.get('view.watch', app.env() === 'local'),
			noCache: app.config.get('view.disable_cache', false)
		})

		this.nunjucks = new ViewEnvironment(loader, {
			autoescape: app.config.get('view.autoescape', true),
			trimBlocks: app.config.get('view.trim_blocks', false),
			lstripBlocks: app.config.get('view.lstrip_blocks', false),
			throwOnUndefined: app.config.get('view.throw_on_undefined', false)
		})
		this.nunjucks.express(app.express)
		app.express.set('view engine', 'njk')

		Filters(this)
		Functions(this)
	}

	exists(view) {
		return new Promise((resolve, reject) => {
			fs.stat(path.join(this.viewPath, view), (err, stats) => {
				if(!err.isNil) {
					if(err.code === 'ENOENT') {
						return resolve(false)
					}

					return reject(err)
				}

				resolve(stats.isFile())
			})
		})
	}

	share(name, value) {
		this.nunjucks.addGlobal(name, value)
	}

	addFilter(name, callback) {
		this.nunjucks.addFilter(name, callback)
	}

	addFunction(name, callback) {
		this.nunjucks.addGlobal(name, callback)
	}

	addExtension(name, extension) {
		this.nunjucks.addExtension(name, extension)
	}

}
<|endoftext|>


<commit_before>/**
 * @flow
 */
import Immutable from 'immutable';

type OnServerFunc<S> = (arg: any) => S | Promise<S> | { isFinishingOnServer: bool, getServerState: () => Promise<S> };
type StoreUpdater<S> = (state: S, action: Action, onServer: OnServerFunc<S>) => MaybePromise<S>;
type DispatchSettings<S> = {
	replaceState?: S,
	skip?: number,
	finishOnServer?: (state: S, index: number) => Promise<S>,
	finishedUpdaters?: (didFinishOnClient: boolean) => void,
	arg?: any
};

/**
 * A store that uses updaters to dispatch changes to its state.
 */
export default class Store<S> {
	_state: S;
	_updaters: Immutable.List<StoreUpdater<S>>;

	/**
	 * Store constuctor (use Store.createStore).
	 *
	 * @param state				{any}					The current state of the object
	 * @param updaters			{List<StoreUpdater>}	The updaters that can mutate the Store
	 */
	constructor(state: S, updaters: Immutable.List<StoreUpdater<S>>) {
		this._state = state;
		this._updaters = updaters;
	}

	/**
	 * Create a new Store.
	 *
	 * @param initialState	{any}	The initial state of the object
	 *
	 * @return				{Store}	The new Store
	 */
	static createStore(initialState: S): Store<S> {
		return new Store(initialState, Immutable.List());
	}

	/**
	 * Registers a new upator in the store.
	 *
	 * @param updater	{StoreUpdater}	The new function that is able to update the store's state
	 *
	 * @throws							An error when the given updater is not a function
	 *
	 * @return			{Store}			A new Store with the new updater
	 */
	register(updater: StoreUpdater<S>): Store<S> {
		if(typeof updater !== 'function') throw new Error('updaters must be functions');

		const newUpdaters = this._updaters.push(updater);

		return new Store(this._state, newUpdaters);
	}

	/**
	 * Update the Store by calling the actions in all the updaters.
	 *
	 * @param action	{Object}				The action to pass to each of the updaters
	 * @param settings	{[Object]}				The settings for this dispatch
	 *
	 * @return			{Promise<Store>}		A Promise with the new Store with the state after
	 *											calling the updaters
	 */
	dispatch(action: Action, settings: DispatchSettings<S> = {}): Promise<Store<S>> {
		// Check args
		const { replaceState, skip, finishOnServer, arg } = settings;

		if(!action || typeof action !== 'object') throw new Error('actions must be objects');
		if(settings.skip && typeof settings.skip !== 'number') throw new Error('settings.skip must be a number');
		if(settings.finishOnServer && settings.arg) {
			throw new Error("'finishOnServer' and 'arg' cannot both be set for a dispatch");
		}
		if(settings.finishOnServer && typeof settings.finishOnServer !== 'function') {
			throw new Error("'finishOnServer' must be a function");
		}
		if(settings.finishedUpdaters && typeof settings.finishedUpdaters !== 'function') {
			throw new Error("'finishedUpdaters' must be a function");
		}

		// Do nothing if there are no updaters to perform
		if(this._updaters.count() === 0)	return Promise.resolve(this);

		// Dispatch to each updater
		const startingState = typeof settings.replaceState !== 'undefined'? settings.replaceState: this._state;
		const updatedStatePromise = this._updaters.reduce((currStatePromise, updater, index) => {
			// Check if this updater should be skipped
			if(settings.skip && index < settings.skip)	return currStatePromise;

			return currStatePromise.then((state) => {	//TODO, try wrapping this in Promise to get ride of $FlowIssue
				// Check if finishing on server
				if(state.isFinishingOnServer) return state;

				// Check for valid state (pass through to be checked below)
				if(!this._isValidState(state)) return state;

				// Call updater
				const onServer = this._makeOnServer(state, index, settings);
				return updater(state, action, onServer);
			});
		}, Promise.resolve(startingState));

		// $FlowIssue: not recognizing newStatePromise is a Promise
		const newStatePromise = updatedStatePromise.then((updatedState) => {
			const finishedOnClient = !updatedState.isFinishingOnServer;

			if(settings.finishedUpdaters) settings.finishedUpdaters(finishedOnClient);

			if(finishedOnClient) return updatedState;

			// Get state from server
			return updatedState.getServerState();
		});

		// Create new store from the new state
		return newStatePromise.then((newState) => {
			// Check for valid state
			if(!this._isValidState(newState)) throw new Error('a state must be returned from each updater');

			return new Store(newState, this._updaters);
		});
	}

	/**
	 * Replace the state of the current store.
	 *
	 * @param {newState}	The the new store should have
	 *
	 * @return				A store with the same updaters, but the given state
	 */
	replaceState(newState: S): Store<S> {
		return new Store(newState, this._updaters);
	}

	/**
	 * Gets the current state of the Store.
	 *
	 * @return {any}	The state of the Store
	 */
	getState(): S {
		return this._state;
	}

	_makeOnServer(state: S, index: number, settings: DispatchSettings): OnServerFunc<S> {
		// Create on server
		return (onServerFunc) => {
			if(settings.finishOnServer) {
				const resultFromServer = settings.finishOnServer(state, index);
				return {
					isFinishingOnServer: true,
					getServerState() {
						return resultFromServer;
					}
				};
			}

			return Promise.resolve(onServerFunc(settings.arg));
		};
	}

	_isValidState(testState: S): boolean {
		return typeof testState !== 'undefined';
	}
}

export function createStore<S>(initialState: S): Store<S> {
	return Store.createStore(initialState);
}<commit_msg>Remove Unused Code<commit_after>/**
 * @flow
 */
import Immutable from 'immutable';

type OnServerFunc<S> = (arg: any) => S | Promise<S> | { isFinishingOnServer: bool, getServerState: () => Promise<S> };
type StoreUpdater<S> = (state: S, action: Action, onServer: OnServerFunc<S>) => MaybePromise<S>;
type DispatchSettings<S> = {
	replaceState?: S,
	skip?: number,
	finishOnServer?: (state: S, index: number) => Promise<S>,
	finishedUpdaters?: (didFinishOnClient: boolean) => void,
	arg?: any
};

/**
 * A store that uses updaters to dispatch changes to its state.
 */
export default class Store<S> {
	_state: S;
	_updaters: Immutable.List<StoreUpdater<S>>;

	/**
	 * Store constuctor (use Store.createStore).
	 *
	 * @param state				{any}					The current state of the object
	 * @param updaters			{List<StoreUpdater>}	The updaters that can mutate the Store
	 */
	constructor(state: S, updaters: Immutable.List<StoreUpdater<S>>) {
		this._state = state;
		this._updaters = updaters;
	}

	/**
	 * Create a new Store.
	 *
	 * @param initialState	{any}	The initial state of the object
	 *
	 * @return				{Store}	The new Store
	 */
	static createStore(initialState: S): Store<S> {
		return new Store(initialState, Immutable.List());
	}

	/**
	 * Registers a new upator in the store.
	 *
	 * @param updater	{StoreUpdater}	The new function that is able to update the store's state
	 *
	 * @throws							An error when the given updater is not a function
	 *
	 * @return			{Store}			A new Store with the new updater
	 */
	register(updater: StoreUpdater<S>): Store<S> {
		if(typeof updater !== 'function') throw new Error('updaters must be functions');

		const newUpdaters = this._updaters.push(updater);

		return new Store(this._state, newUpdaters);
	}

	/**
	 * Update the Store by calling the actions in all the updaters.
	 *
	 * @param action	{Object}				The action to pass to each of the updaters
	 * @param settings	{[Object]}				The settings for this dispatch
	 *
	 * @return			{Promise<Store>}		A Promise with the new Store with the state after
	 *											calling the updaters
	 */
	dispatch(action: Action, settings: DispatchSettings<S> = {}): Promise<Store<S>> {
		// Check args
		if(!action || typeof action !== 'object') throw new Error('actions must be objects');
		if(settings.skip && typeof settings.skip !== 'number') throw new Error('settings.skip must be a number');
		if(settings.finishOnServer && settings.arg) {
			throw new Error("'finishOnServer' and 'arg' cannot both be set for a dispatch");
		}
		if(settings.finishOnServer && typeof settings.finishOnServer !== 'function') {
			throw new Error("'finishOnServer' must be a function");
		}
		if(settings.finishedUpdaters && typeof settings.finishedUpdaters !== 'function') {
			throw new Error("'finishedUpdaters' must be a function");
		}

		// Do nothing if there are no updaters to perform
		if(this._updaters.count() === 0)	return Promise.resolve(this);

		// Dispatch to each updater
		const startingState = typeof settings.replaceState !== 'undefined'? settings.replaceState: this._state;
		const updatedStatePromise = this._updaters.reduce((currStatePromise, updater, index) => {
			// Check if this updater should be skipped
			if(settings.skip && index < settings.skip)	return currStatePromise;

			return currStatePromise.then((state) => {	//TODO, try wrapping this in Promise to get ride of $FlowIssue
				// Check if finishing on server
				if(state.isFinishingOnServer) return state;

				// Check for valid state (pass through to be checked below)
				if(!this._isValidState(state)) return state;

				// Call updater
				const onServer = this._makeOnServer(state, index, settings);
				return updater(state, action, onServer);
			});
		}, Promise.resolve(startingState));

		// $FlowIssue: not recognizing newStatePromise is a Promise
		const newStatePromise = updatedStatePromise.then((updatedState) => {
			const finishedOnClient = !updatedState.isFinishingOnServer;

			if(settings.finishedUpdaters) settings.finishedUpdaters(finishedOnClient);

			if(finishedOnClient) return updatedState;

			// Get state from server
			return updatedState.getServerState();
		});

		// Create new store from the new state
		return newStatePromise.then((newState) => {
			// Check for valid state
			if(!this._isValidState(newState)) throw new Error('a state must be returned from each updater');

			return new Store(newState, this._updaters);
		});
	}

	/**
	 * Replace the state of the current store.
	 *
	 * @param {newState}	The the new store should have
	 *
	 * @return				A store with the same updaters, but the given state
	 */
	replaceState(newState: S): Store<S> {
		return new Store(newState, this._updaters);
	}

	/**
	 * Gets the current state of the Store.
	 *
	 * @return {any}	The state of the Store
	 */
	getState(): S {
		return this._state;
	}

	_makeOnServer(state: S, index: number, settings: DispatchSettings): OnServerFunc<S> {
		// Create on server
		return (onServerFunc) => {
			if(settings.finishOnServer) {
				const resultFromServer = settings.finishOnServer(state, index);
				return {
					isFinishingOnServer: true,
					getServerState() {
						return resultFromServer;
					}
				};
			}

			return Promise.resolve(onServerFunc(settings.arg));
		};
	}

	_isValidState(testState: S): boolean {
		return typeof testState !== 'undefined';
	}
}

export function createStore<S>(initialState: S): Store<S> {
	return Store.createStore(initialState);
}<|endoftext|>