function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, exclusiveFilters, filesPerPage) { var self = this; self.listType = listType; self.supportedSorting = supportedSorting; self.supportedFilters = supportedFilters; self.defaultSorting = defaultSorting; self.defaultFilters = defaultFilters; self.exclusiveFilters = exclusiveFilters; self.searchFunction = undefined; self.allItems = []; self.allSize = ko.observable(0); self.items = ko.observableArray([]); self.pageSize = ko.observable(filesPerPage); self.currentPage = ko.observable(0); self.currentSorting = ko.observable(self.defaultSorting); self.currentFilters = ko.observableArray(self.defaultFilters); self.selectedItem = ko.observable(undefined); //~~ item handling self.refresh = function() { self._updateItems(); }; self.updateItems = function(items) { self.allItems = items; self.allSize(items.length); self._updateItems(); }; self.selectItem = function(matcher) { var itemList = self.items(); for (var i = 0; i < itemList.length; i++) { if (matcher(itemList[i])) { self.selectedItem(itemList[i]); break; } } }; self.selectNone = function() { self.selectedItem(undefined); }; self.isSelected = function(data) { return self.selectedItem() == data; }; self.isSelectedByMatcher = function(matcher) { return matcher(self.selectedItem()); }; self.removeItem = function(matcher) { var item = self.getItem(matcher, true); if (item === undefined) { return; } var index = self.allItems.indexOf(item); if (index > -1) { self.allItems.splice(index, 1); self._updateItems(); } }; //~~ pagination self.paginatedItems = ko.dependentObservable(function() { if (self.items() == undefined) { return []; } else if (self.pageSize() == 0) { return self.items(); } else { var from = Math.max(self.currentPage() * self.pageSize(), 0); var to = Math.min(from + self.pageSize(), self.items().length); return self.items().slice(from, to); } }); self.lastPage = ko.dependentObservable(function() { return (self.pageSize() == 0 ? 1 : Math.ceil(self.items().length / self.pageSize()) - 1); }); self.pages = ko.dependentObservable(function() { var pages = []; if (self.pageSize() == 0) { pages.push({ number: 0, text: 1 }); } else if (self.lastPage() < 7) { for (var i = 0; i < self.lastPage() + 1; i++) { pages.push({ number: i, text: i+1 }); } } else { pages.push({ number: 0, text: 1 }); if (self.currentPage() < 5) { for (var i = 1; i < 5; i++) { pages.push({ number: i, text: i+1 }); } pages.push({ number: -1, text: "…"}); } else if (self.currentPage() > self.lastPage() - 5) { pages.push({ number: -1, text: "…"}); for (var i = self.lastPage() - 4; i < self.lastPage(); i++) { pages.push({ number: i, text: i+1 }); } } else { pages.push({ number: -1, text: "…"}); for (var i = self.currentPage() - 1; i <= self.currentPage() + 1; i++) { pages.push({ number: i, text: i+1 }); } pages.push({ number: -1, text: "…"}); } pages.push({ number: self.lastPage(), text: self.lastPage() + 1}) } return pages; }); self.switchToItem = function(matcher) { var pos = -1; var itemList = self.items(); for (var i = 0; i < itemList.length; i++) { if (matcher(itemList[i])) { pos = i; break; } } if (pos > -1) { var page = Math.floor(pos / self.pageSize()); self.changePage(page); } }; self.changePage = function(newPage) { if (newPage < 0 || newPage > self.lastPage()) return; self.currentPage(newPage); }; self.prevPage = function() { if (self.currentPage() > 0) { self.currentPage(self.currentPage() - 1); } }; self.nextPage = function() { if (self.currentPage() < self.lastPage()) { self.currentPage(self.currentPage() + 1); } }; self.getItem = function(matcher, all) { var itemList; if (all !== undefined && all === true) { itemList = self.allItems; } else { itemList = self.items(); } for (var i = 0; i < itemList.length; i++) { if (matcher(itemList[i])) { return itemList[i]; } } return undefined; }; //~~ searching self.changeSearchFunction = function(searchFunction) { self.searchFunction = searchFunction; self.changePage(0); self._updateItems(); }; self.resetSearch = function() { self.changeSearchFunction(undefined); }; //~~ sorting self.changeSorting = function(sorting) { if (!_.contains(_.keys(self.supportedSorting), sorting)) return; self.currentSorting(sorting); self._saveCurrentSortingToLocalStorage(); self.changePage(0); self._updateItems(); }; //~~ filtering self.toggleFilter = function(filter) { if (!_.contains(_.keys(self.supportedFilters), filter)) return; if (_.contains(self.currentFilters(), filter)) { self.removeFilter(filter); } else { self.addFilter(filter); } }; self.addFilter = function(filter) { if (!_.contains(_.keys(self.supportedFilters), filter)) return; for (var i = 0; i < self.exclusiveFilters.length; i++) { if (_.contains(self.exclusiveFilters[i], filter)) { for (var j = 0; j < self.exclusiveFilters[i].length; j++) { if (self.exclusiveFilters[i][j] == filter) continue; self.removeFilter(self.exclusiveFilters[i][j]); } } } var filters = self.currentFilters(); filters.push(filter); self.currentFilters(filters); self._saveCurrentFiltersToLocalStorage(); self.changePage(0); self._updateItems(); }; self.removeFilter = function(filter) { if (!_.contains(_.keys(self.supportedFilters), filter)) return; var filters = self.currentFilters(); filters.pop(filter); self.currentFilters(filters); self._saveCurrentFiltersToLocalStorage(); self.changePage(0); self._updateItems(); }; //~~ update for sorted and filtered view self._updateItems = function() { // determine comparator var comparator = undefined; var currentSorting = self.currentSorting(); if (typeof currentSorting !== undefined && typeof self.supportedSorting[currentSorting] !== undefined) { comparator = self.supportedSorting[currentSorting]; } // work on all items var result = self.allItems; // filter if necessary var filters = self.currentFilters(); _.each(filters, function(filter) { if (typeof filter !== undefined && typeof supportedFilters[filter] !== undefined) result = _.filter(result, supportedFilters[filter]); }); // search if necessary if (typeof self.searchFunction !== undefined && self.searchFunction) { result = _.filter(result, self.searchFunction); } // sort if necessary if (typeof comparator !== undefined) result.sort(comparator); // set result list self.items(result); }; //~~ local storage self._saveCurrentSortingToLocalStorage = function() { if ( self._initializeLocalStorage() ) { var currentSorting = self.currentSorting(); if (currentSorting !== undefined) localStorage[self.listType + "." + "currentSorting"] = currentSorting; else localStorage[self.listType + "." + "currentSorting"] = undefined; } }; self._loadCurrentSortingFromLocalStorage = function() { if ( self._initializeLocalStorage() ) { if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"])) self.currentSorting(localStorage[self.listType + "." + "currentSorting"]); else self.currentSorting(defaultSorting); } }; self._saveCurrentFiltersToLocalStorage = function() { if ( self._initializeLocalStorage() ) { var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters()); localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters); } }; self._loadCurrentFiltersFromLocalStorage = function() { if ( self._initializeLocalStorage() ) { self.currentFilters(_.intersection(_.keys(self.supportedFilters), JSON.parse(localStorage[self.listType + "." + "currentFilters"]))); } }; self._initializeLocalStorage = function() { if (!Modernizr.localstorage) return false; if (localStorage[self.listType + "." + "currentSorting"] !== undefined && localStorage[self.listType + "." + "currentFilters"] !== undefined && JSON.parse(localStorage[self.listType + "." + "currentFilters"]) instanceof Array) return true; localStorage[self.listType + "." + "currentSorting"] = self.defaultSorting; localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(self.defaultFilters); return true; }; self._loadCurrentFiltersFromLocalStorage(); self._loadCurrentSortingFromLocalStorage(); } function formatSize(bytes) { if (!bytes) return "-"; var units = ["bytes", "KB", "MB", "GB"]; for (var i = 0; i < units.length; i++) { if (bytes < 1024) { return _.sprintf("%3.1f%s", bytes, units[i]); } bytes /= 1024; } return _.sprintf("%.1f%s", bytes, "TB"); } function bytesFromSize(size) { if (size == undefined || size.trim() == "") return undefined; var parsed = size.match(/^([+]?[0-9]*\.?[0-9]+)(?:\s*)?(.*)$/); var number = parsed[1]; var unit = parsed[2].trim(); if (unit == "") return parseFloat(number); var units = { b: 1, byte: 1, bytes: 1, kb: 1024, mb: Math.pow(1024, 2), gb: Math.pow(1024, 3), tb: Math.pow(1024, 4) }; unit = unit.toLowerCase(); if (!units.hasOwnProperty(unit)) { return undefined; } var factor = units[unit]; return number * factor; } function formatDuration(seconds) { if (!seconds) return "-"; if (seconds < 0) return "00:00:00"; var s = seconds % 60; var m = (seconds % 3600) / 60; var h = seconds / 3600; return _.sprintf(gettext(/* L10N: duration format */ "%(hour)02d:%(minute)02d:%(second)02d"), {hour: h, minute: m, second: s}); } function formatFuzzyEstimation(seconds, base) { if (!seconds) return "-"; if (seconds < 0) return "-"; var m; if (base != undefined) { m = moment(base); } else { m = moment(); } m.add(seconds, "s"); return m.fromNow(true); } function formatDate(unixTimestamp) { if (!unixTimestamp) return "-"; return moment.unix(unixTimestamp).format(gettext(/* L10N: Date format */ "YYYY-MM-DD HH:mm")); } function formatTimeAgo(unixTimestamp) { if (!unixTimestamp) return "-"; return moment.unix(unixTimestamp).fromNow(); } function formatFilament(filament) { if (!filament || !filament["length"]) return "-"; var result = "%(length).02fm"; if (filament.hasOwnProperty("volume") && filament.volume) { result += " / " + "%(volume).02fcm³"; } return _.sprintf(result, {length: filament["length"] / 1000, volume: filament["volume"]}); } function cleanTemperature(temp) { if (!temp || temp < 10) return gettext("off"); return temp; } function formatTemperature(temp, showF) { if (!temp || temp < 10) return gettext("off"); if (showF) { return _.sprintf("%.1f°C (%.1f°F)", temp, temp * 9 / 5 + 32); } else { return _.sprintf("%.1f°C", temp); } } function pnotifyAdditionalInfo(inner) { return '
" + message + "
"); } var modalHeader = $('×' + message + '
' + question + '
'); var cancelButton = $('' + cancel + '') .attr("data-dismiss", "modal") .attr("aria-hidden", "true"); var proceedButton = $('' + proceed + '') .addClass("btn-" + proceedClass); var modal = $('') .addClass('modal hide fade') .addClass(dialogClass) .append($('').addClass('modal-header').append(modalHeader)) .append($('').addClass('modal-body').append(modalBody)) .append($('').addClass('modal-footer').append(cancelButton).append(proceedButton)); modal.modal("show"); proceedButton.click(function(e) { e.preventDefault(); modal.modal("hide"); if (onproceed && _.isFunction(onproceed)) { onproceed(e); } }); return modal; } function showReloadOverlay() { $("#reloadui_overlay").show(); } function commentableLinesToArray(lines) { return splitTextToArray(lines, "\n", true, function(item) {return !_.startsWith(item, "#")}); } function splitTextToArray(text, sep, stripEmpty, filter) { return _.filter( _.map( text.split(sep), function(item) { return (item) ? item.trim() : ""; } ), function(item) { return (stripEmpty ? item : true) && (filter ? filter(item) : true); } ); } /** * Returns true if comparing data and oldData yields changes, false otherwise. * * E.g. * * hasDataChanged( * {foo: "bar", fnord: {one: "1", two: "2", three: "three", key: "value"}}, * {foo: "bar", fnord: {one: "1", two: "2", three: "3", four: "4"}} * ) * * will return * * true * * and * * hasDataChanged( * {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}, * {foo: "bar", fnord: {one: "1", two: "2", three: "3"}} * ) * * will return * * false * * Note that this will assume data and oldData to be structurally identical (same keys) * and is optimized to check for value changes, not key updates. */ function hasDataChanged(data, oldData) { if (data == undefined) { return false; } if (oldData == undefined) { return true; } if (_.isPlainObject(data)) { return _.any(_.keys(data), function(key) {return hasDataChanged(data[key], oldData[key]);}); } else { return !_.isEqual(data, oldData); } } /** * Compare provided data and oldData plain objects and only return those * substructures of data that actually changed. * * E.g. * * getOnlyChangedData( * {foo: "bar", fnord: {one: "1", two: "2", three: "three"}}, * {foo: "bar", fnord: {one: "1", two: "2", three: "3"}} * ) * * will return * * {fnord: {three: "three"}} * * and * * getOnlyChangedData( * {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}, * {foo: "bar", fnord: {one: "1", two: "2", three: "3"}} * ) * * will return * * {} * * Note that this will assume data and oldData to be structurally identical (same keys) * and is optimized to check for value changes, not key updates. */ function getOnlyChangedData(data, oldData) { if (data == undefined) { return {}; } if (oldData == undefined) { return data; } var f = function(root, oldRoot) { if (!_.isPlainObject(root)) { return root; } var retval = {}; _.forOwn(root, function(value, key) { var oldValue = oldRoot[key]; if (_.isPlainObject(value)) { if (hasDataChanged(value, oldValue)) { retval[key] = f(value, oldValue); } } else { if (!_.isEqual(value, oldValue)) { retval[key] = value; } } }); return retval; }; return f(data, oldData); } function callViewModels(allViewModels, method, callback) { callViewModelsIf(allViewModels, method, undefined, callback); } function callViewModelsIf(allViewModels, method, condition, callback) { if (condition == undefined || !_.isFunction(condition)) { condition = function() { return true; }; } var parameters = undefined; if (!_.isFunction(callback)) { // if callback is not a function that means we are supposed to directly // call the view model method instead of providing it to the callback // - let's figure out how if (callback == undefined) { // directly call view model method with no parameters parameters = undefined; log.trace("Calling method", method, "on view models"); } else if (_.isArray(callback)) { // directly call view model method with these parameters parameters = callback; log.trace("Calling method", method, "on view models with specified parameters", parameters); } else { // ok, this doesn't make sense, callback is neither undefined nor // an array, we'll return without doing anything return; } // we reset this here so we now further down that we want to call // the method directly callback = undefined; } else { log.trace("Providing method", method, "on view models to specified callback", callback); } _.each(allViewModels, function(viewModel) { if (viewModel.hasOwnProperty(method) && condition(viewModel, method)) { if (callback == undefined) { if (parameters != undefined) { // call the method with the provided parameters viewModel[method].apply(viewModel, parameters); } else { // call the method without parameters viewModel[method](); } } else { // provide the method to the callback callback(viewModel[method]); } } }); } var sizeObservable = function(observable) { return ko.computed({ read: function() { return formatSize(observable()); }, write: function(value) { var result = bytesFromSize(value); if (result != undefined) { observable(result); } } }) };