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; }; self.resetPage = function() { if (self.currentPage() > self.lastPage()) { self.currentPage(self.lastPage()); } }; //~~ 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 = _.without(filters, 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 < 1) 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 || seconds < 1) return "-"; var m; if (base != undefined) { m = moment(base); } else { m = moment(); } m.add(seconds, "s"); return m.fromNow(true); } function formatFuzzyPrintTime(totalSeconds) { /** * Formats a print time estimate in a very fuzzy way. * * Accuracy decreases the higher the estimation is: * * * less than 30s: "a few seconds" * * 30s to a minute: "less than a minute" * * 1 to 30min: rounded to full minutes, above 30s is minute + 1 ("27 minutes", "2 minutes") * * 30min to 40min: "40 minutes" * * 40min to 50min: "50 minutes" * * 50min to 1h: "1 hour" * * 1 to 12h: rounded to half hours, 15min to 45min is ".5", above that hour + 1 ("4 hours", "2.5 hours") * * 12 to 24h: rounded to full hours, above 30min is hour + 1, over 23.5h is "1 day" * * Over a day: rounded to half days, 8h to 16h is ".5", above that days + 1 ("1 day", "4 days", "2.5 days") */ if (!totalSeconds || totalSeconds < 1) return "-"; var d = moment.duration(totalSeconds, "seconds"); var seconds = d.seconds(); var minutes = d.minutes(); var hours = d.hours(); var days = d.asDays(); var replacements = { days: days, hours: hours, minutes: minutes, seconds: seconds, totalSeconds: totalSeconds }; var text = "-"; if (days >= 1) { // days if (hours >= 16) { replacements.days += 1; text = gettext("%(days)d days"); } else if (hours >= 8 && hours < 16) { text = gettext("%(days)d.5 days"); } else { if (days == 1) { text = gettext("%(days)d day"); } else { text = gettext("%(days)d days"); } } } else if (hours >= 1) { // only hours if (hours < 12) { if (minutes < 15) { // less than .15 => .0 if (hours == 1) { text = gettext("%(hours)d hour"); } else { text = gettext("%(hours)d hours"); } } else if (minutes >= 15 && minutes < 45) { // between .25 and .75 => .5 text = gettext("%(hours)d.5 hours"); } else { // over .75 => hours + 1 replacements.hours += 1; text = gettext("%(hours)d hours"); } } else { if (hours == 23 && minutes > 30) { // over 23.5 hours => 1 day text = gettext("1 day"); } else { if (minutes > 30) { // over .5 => hours + 1 replacements.hours += 1; } text = gettext("%(hours)d hours"); } } } else if (minutes >= 1) { // only minutes if (minutes < 2) { if (seconds < 30) { text = gettext("a minute"); } else { text = gettext("2 minutes"); } } else if (minutes < 30) { if (seconds > 30) { replacements.minutes += 1; } text = gettext("%(minutes)d minutes"); } else if (minutes <= 40) { text = gettext("40 minutes"); } else if (minutes <= 50) { text = gettext("50 minutes"); } else { text = gettext("1 hour"); } } else { // only seconds if (seconds < 30) { text = gettext("a few seconds"); } else { text = gettext("less than a minute"); } } return _.sprintf(text, replacements); } 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; } /** * Shows a progress modal depending on a supplied promise. * * Will listen to the supplied promise, update the progress on .progress events and * enabling the close button and (optionally) closing the dialog on promise resolve. * * The calling code should call "notify" on the deferred backing the promise and supply * two parameters: the text to display on the progress bar and the optional output field and * a boolean value indicating whether the operation behind that update was successful or not. * Non-successful progress updates will remove the barClassSuccess class from the progress bar and * apply the barClassFailure class and also apply the outputClassFailure to the produced line * in the output. * * To determine the progress, calling code should supply the prognosed maximum number of * progress events. An internal counter will increment on each progress event and used together * with the max value to calculate the percentage to display on the progress bar. * * If no max value is set, the progress bar will show a striped animation at 100% fill status * to visualize "unknown but ongoing" status. * * Available options: * * * title: the title of the modal, defaults to "Progress" * * message: the message of the modal, defaults to "" * * buttonText: the text on the close button, defaults to "Close" * * max: maximum number of expected progress events (when 100% will be reached), defaults * to undefined * * close: whether to close the dialog on completion, defaults to false * * output: whether to display the progress texts in an output field, defaults to false * * dialogClass: additional class to apply to the dialog div * * barClassSuccess: additional class for the progress bar while all progress events are * successful * * barClassFailure: additional class for the progress bar when a progress event was * unsuccessful * * outputClassSuccess: additional class for successful output lines * * outputClassFailure: additional class for unsuccessful output lines * * @param options modal options * @param promise promise to monitor * @returns {*|jQuery} the modal object */ function showProgressModal(options, promise) { var title = options.title || gettext("Progress"); var message = options.message || ""; var buttonText = options.button || gettext("Close"); var max = options.max || undefined; var close = options.close || false; var output = options.output || false; var dialogClass = options.dialogClass || ""; var barClassSuccess = options.barClassSuccess || ""; var barClassFailure = options.barClassFailure || "bar-danger"; var outputClassSuccess = options.outputClassSuccess || ""; var outputClassFailure = options.outputClassFailure || "text-error"; var modalHeader = $('' + message + '
'); var progress = $(''); var progressBar = $('') .addClass(barClassSuccess); var progressTextBack = $(''); var progressTextFront = $('') .width(progress.width()); if (max == undefined) { progress.addClass("progress-striped active"); progressBar.width("100%"); } progressBar .append(progressTextFront); progress .append(progressTextBack) .append(progressBar); var button = $('') .prop("disabled", true) .attr("data-dismiss", "modal") .attr("aria-hidden", "true"); var modalBody = $('') .addClass('modal-body') .append(paragraph) .append(progress); var pre; if (output) { pre = $(""); modalBody.append(pre); } var modal = $('') .addClass('modal hide fade') .addClass(dialogClass) .append($('').addClass('modal-header').append(modalHeader)) .append(modalBody) .append($('').addClass('modal-footer').append(button)); modal.modal({keyboard: false, backdrop: "static", show: true}); var counter = 0; promise .progress(function(text, success) { var value; if (max === undefined || max <= 0) { value = 100; } else { counter++; value = Math.max(Math.min(counter * 100 / max, 100), 0); } // update progress bar progressBar.width(String(value) + "%"); progressTextFront.text(text); progressTextBack.text(text); progressTextFront.width(progress.width()); // if not successful, apply failure class if (!success && !progressBar.hasClass(barClassFailure)) { progressBar .removeClass(barClassSuccess) .addClass(barClassFailure); } if (output && pre) { if (success) { pre.append($("" + text + "