Refactored pagination/sorting/filtering of gcode files into own configurable listHelper now used to provide pagination etc to both gcode and timelapse file list.

master
Gina Häußge 2013-03-06 23:27:16 +01:00
parent dae1c0367a
commit 1fbb73e448
5 changed files with 341 additions and 202 deletions

View File

@ -286,7 +286,6 @@ def getTimelapseData():
files = timelapse.getFinishedTimelapses()
for file in files:
file["size"] = util.getFormattedSize(file["size"])
file["url"] = url_for("downloadTimelapse", filename=file["name"])
return jsonify({

9
octoprint/static/js/less-1.3.3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -679,64 +679,38 @@ function TerminalViewModel() {
function GcodeFilesViewModel() {
var self = this;
self.allFiles = [];
self.files = ko.observableArray([]);
self.pageSize = ko.observable(CONFIG_FILESPERPAGE);
self.currentPage = ko.observable(0);
self.currentSorting = ko.observable("name");
self.currentFilters = ko.observableArray([]);
if( localStorage["currentSorting"] )
self.currentSorting( localStorage["currentSorting"] );
if( localStorage["filterPrinted"] == 1 ) {
var filters = self.currentFilters();
filters.push("printed");
self.currentFilters(_.uniq(filters));
}
self.paginatedFiles = ko.dependentObservable(function() {
if (self.files() == undefined) {
return [];
} else {
var from = Math.max(self.currentPage() * self.pageSize(), 0);
var to = Math.min(from + self.pageSize(), self.files().length);
return self.files().slice(from, to);
}
})
self.lastPage = ko.dependentObservable(function() {
return Math.ceil(self.files().length / self.pageSize()) - 1;
})
self.pages = ko.dependentObservable(function() {
var pages = [];
if (self.lastPage() < 7) {
for (var i = 0; i < self.lastPage() + 1; i++) {
pages.push({ number: i, text: i+1 });
// initialize list helper
self.listHelper = new ItemListHelper(
"gcodeFiles",
{
"name": function(a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
},
"upload": function(a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
"size": function(a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
}
} 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: "…"});
},
{
"printed": function(file) {
return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0);
}
pages.push({ number: self.lastPage(), text: self.lastPage() + 1})
}
return pages;
})
},
"name",
[],
CONFIG_GCODEFILESPERPAGE
)
self.requestData = function() {
$.ajax({
@ -750,12 +724,11 @@ function GcodeFilesViewModel() {
}
self.fromResponse = function(response) {
self.allFiles = response.files;
self._updateFiles();
self.listHelper.updateItems(response.files);
if (response.filename) {
// got a file to scroll to
self.switchToFile(response.filename);
self.listHelper.switchToItem(function(item) {return item.name == response.filename});
}
}
@ -778,131 +751,6 @@ function GcodeFilesViewModel() {
})
}
self.switchToFile = function(filename) {
var pos = -1;
var filelist = self.files();
for (var i = 0; i < filelist.length; i++) {
if (filelist[i].name == filename) {
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.changeSorting = function(sorting) {
if (sorting != "name" && sorting != "upload" && sorting != "size")
return;
self.currentSorting(sorting);
localStorage["currentSorting"] = self.currentSorting(); //store setting in local storage
self.changePage(0);
self._updateFiles();
}
self.toggleFilter = function(filter) {
if (_.contains(self.currentFilters(), filter)) {
self.removeFilter(filter);
} else {
self.addFilter(filter);
}
}
self.addFilter = function(filter) {
if (filter != "printed")
return;
var filters = self.currentFilters();
filters.push(filter);
self.currentFilters(_.uniq(filters));
localStorage["filterPrinted"] = 1;
self._updateFiles();
}
self.removeFilter = function(filter) {
if (filter != "printed")
return;
self.currentFilters(_.without(self.currentFilters(), filter));
localStorage["filterPrinted"] = 0;
self._updateFiles();
}
self._updateFiles = function() {
// determine comparator
var comparator = undefined;
if (self.currentSorting() == "name") {
comparator = function(a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
}
} else if (self.currentSorting() == "upload") {
comparator = function(a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
}
} else if (self.currentSorting() == "size") {
comparator = function(a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
}
}
// work on all files
var result = self.allFiles;
// filter if necessary
var filters = self.currentFilters();
for (var i = 0; i < filters.length; i++) {
var filterFunction = undefined;
var filter = filters[i];
switch (filter) {
case "printed": {
filterFunction = function(file) {
return !(file["prints"] && file["prints"]["success"] && file["prints"]["success"] > 0);
}
break;
}
}
if (typeof filterFunction !== undefined)
result = _.filter(result, filterFunction);
}
// sort if necessary
if (typeof comparator !== undefined)
result.sort(comparator);
// set result list
self.files(result);
}
self.getPopoverContent = function(data) {
var output = "<p><strong>Uploaded:</strong> " + data["date"] + "</p>";
if (data["gcodeAnalysis"]) {
@ -933,7 +781,6 @@ function WebcamViewModel() {
self.timelapseType = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(undefined);
self.files = ko.observableArray([]);
self.isErrorOrClosed = ko.observable(undefined);
self.isOperational = ko.observable(undefined);
@ -951,6 +798,36 @@ function WebcamViewModel() {
self.requestData();
})
// initialize list helper
self.listHelper = new ItemListHelper(
"timelapseFiles",
{
"name": function(a, b) {
// sorts ascending
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
return 0;
},
"creation": function(a, b) {
// sorts descending
if (a["date"] > b["date"]) return -1;
if (a["date"] < b["date"]) return 1;
return 0;
},
"size": function(a, b) {
// sorts descending
if (a["bytes"] > b["bytes"]) return -1;
if (a["bytes"] < b["bytes"]) return 1;
return 0;
}
},
{
},
"name",
[],
CONFIG_TIMELAPSEFILESPERPAGE
)
self.requestData = function() {
$.ajax({
url: AJAX_BASEURL + "timelapse",
@ -962,8 +839,8 @@ function WebcamViewModel() {
}
self.fromResponse = function(response) {
self.timelapseType(response.type)
self.files(response.files)
self.timelapseType(response.type);
self.listHelper.updateItems(response.files);
if (response.type == "timed" && response.config && response.config.interval) {
self.timelapseTimedInterval(response.config.interval)
@ -1089,7 +966,7 @@ function SettingsViewModel() {
self.appearance_color = ko.observable(undefined);
/* I did attempt to allow arbitrary gradients but cross browser support via knockout or jquery was going to be horrible */
self.appearance_available_colors = ko.observable(["white", "red", "orange", "yellow", "green", "blue", "violet"]);
self.appearance_available_colors = ko.observable(["default", "red", "orange", "yellow", "green", "blue", "violet"]);
self.printer_movementSpeedX = ko.observable(undefined);
self.printer_movementSpeedY = ko.observable(undefined);
@ -1265,6 +1142,231 @@ function DataUpdater(connectionViewModel, printerStateViewModel, temperatureView
}
}
function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, filesPerPage) {
var self = this;
self.listType = listType;
self.supportedSorting = supportedSorting;
self.supportedFilters = supportedFilters;
self.defaultSorting = defaultSorting;
self.defaultFilters = defaultFilters;
self.allItems = [];
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);
//~~ item handling
self.updateItems = function(items) {
self.allItems = items;
self._updateItems();
}
//~~ pagination
self.paginatedItems = ko.dependentObservable(function() {
if (self.items() == undefined) {
return [];
} 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 Math.ceil(self.items().length / self.pageSize()) - 1;
})
self.pages = ko.dependentObservable(function() {
var pages = [];
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);
}
}
//~~ 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;
var filters = self.currentFilters();
filters.push(filter);
self.currentFilters(filters);
self._saveCurrentFiltersToLocalStorage();
self._updateItems();
}
self.removeFilter = function(filter) {
if (filter != "printed")
return;
var filters = self.currentFilters();
filters.pop(filter);
self.currentFilters(filters);
self._saveCurrentFiltersToLocalStorage();
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]);
});
// sort if necessary
if (typeof comparator !== undefined)
result.sort(comparator);
// set result list
self.items(result);
}
//~~ local storage
self._saveCurrentSortingToLocalStorage = function() {
self._initializeLocalStorage();
var currentSorting = self.currentSorting();
if (currentSorting !== undefined)
localStorage[self.listType]["currentSorting"] = currentSorting;
else
localStorage[self.listType]["currentSorting"] = undefined;
}
self._loadCurrentSortingFromLocalStorage = function() {
self._initializeLocalStorage();
if (_.contains(_.keys(supportedSorting), localStorage[self.listType]["currentSorting"]))
self.currentSorting(localStorage[self.listType]["currentSorting"]);
else
self.currentSorting(defaultSorting);
}
self._saveCurrentFiltersToLocalStorage = function() {
self._initializeLocalStorage();
var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters());
localStorage[self.listType]["currentFilters"] = filters;
}
self._loadCurrentFiltersFromLocalStorage = function() {
self._initializeLocalStorage();
self.currentFilters(_.intersection(_.keys(self.supportedFilters), localStorage[self.listType, "currentFilters"]));
}
self._initializeLocalStorage = function() {
if (localStorage[self.listType] !== undefined)
return;
localStorage[self.listType] = {
"currentSorting": self.defaultSorting,
"currentFilters": self.defaultFilters
}
}
self._loadCurrentFiltersFromLocalStorage();
self._loadCurrentSortingFromLocalStorage();
}
$(function() {
//~~ View models
@ -1398,10 +1500,17 @@ $(function() {
done: function (e, data) {
gcodeFilesViewModel.fromResponse(data.result);
$("#gcode_upload_progress .bar").css("width", "0%");
$("#gcode_upload_progress").removeClass("progress-striped").removeClass("active");
$("#gcode_upload_progress .bar").text("");
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$("#gcode_upload_progress .bar").css("width", progress + "%");
$("#gcode_upload_progress .bar").text("Uploading ...");
if (progress >= 100) {
$("#gcode_upload_progress").addClass("progress-striped").addClass("active");
$("#gcode_upload_progress .bar").text("Saving ...");
}
}
});

View File

@ -10,17 +10,20 @@
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/ui.css') }}" rel="stylesheet" media="screen">
<!--link href="{{ url_for('static', filename='css/octoprint.css') }}" rel="stylesheet" type="stylesheet/less" media="screen"-->
<link href="{{ url_for('static', filename='gcodeviewer/css/cupertino/jquery-ui-1.9.0.custom.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='gcodeviewer/css/style.css') }}" rel="stylesheet" media="screen">
<script lang="javascript">
var AJAX_BASEURL = "/ajax/";
var CONFIG_FILESPERPAGE = 5;
var CONFIG_GCODEFILESPERPAGE = 5;
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
var WEB_SOCKET_SWF_LOCATION = "{{ url_for('static', filename='js/WebSocketMain.swf') }}";
var WEB_SOCKET_DEBUG = true;
</script>
<script src="{{ url_for('static', filename='js/less-1.3.3.min.js') }}" type="text/javascript"></script>
</head>
<body>
<div id="navbar" class="navbar navbar-fixed-top">
@ -91,11 +94,11 @@
<i class="icon-wrench"></i>
</a>
<ul class="dropdown-menu">
<li><a href="#" data-bind="click: function() { $root.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
<li><a href="#" data-bind="click: function() { $root.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li>
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li>
</ul>
</div>
</div>
@ -109,7 +112,7 @@
<th class="gcode_files_action">Action</th>
</tr>
</thead>
<tbody data-bind="foreach: paginatedFiles">
<tbody data-bind="foreach: listHelper.paginatedItems">
<tr data-bind="css: $root.getSuccessClass($data), popover: { title: name, animation: true, html: true, placement: 'right', trigger: 'hover', delay: 0, content: $root.getPopoverContent($data), html: true }">
<td class="gcode_files_name" data-bind="text: name"></td>
<td class="gcode_files_size" data-bind="text: size"></td>
@ -119,13 +122,13 @@
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: currentPage() === 0}"><a href="#" data-bind="click: prevPage">«</a></li>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: pages">
<li data-bind="css: { active: $data.number === $root.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.changePage($data.number); }"></a></li>
<ul data-bind="foreach: listHelper.pages">
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: currentPage() === lastPage()}"><a href="#" data-bind="click: nextPage">»</a></li>
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
<span class="btn btn-primary btn-block fileinput-button" style="margin-bottom: 10px">
@ -134,7 +137,7 @@
<input id="gcode_upload" type="file" name="gcode_file" class="fileinput-button" data-url="/ajax/gcodefiles/upload">
</span>
<div id="gcode_upload_progress" class="progress" style="width: 100%;">
<div class="bar" style="width: 0%"></div>
<div class="bar" style="width: 0%">Loading ...</div>
</div>
<div>
<small>Hint: You can also drag and drop files on this page to upload them.</small>
@ -469,6 +472,9 @@
<h1>Finished Timelapses</h1>
<div class="pull-right">
<small>Sort by: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">Name (ascending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('creation'); }">Creation date (descending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">Size (descending)</a></small>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
<thead>
<tr>
@ -477,7 +483,7 @@
<th class="timelapse_files_action">Action</th>
</tr>
</thead>
<tbody data-bind="foreach: files">
<tbody data-bind="foreach: listHelper.items">
<tr data-bind="attr: {title: name}">
<td class="timelapse_files_name" data-bind="text: name"></td>
<td class="timelapse_files_size" data-bind="text: size"></td>
@ -485,6 +491,17 @@
</tr>
</tbody>
</table>
<div class="pagination pagination-mini pagination-centered">
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
</ul>
<ul data-bind="foreach: listHelper.pages">
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
</ul>
<ul>
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
</ul>
</div>
{% endif %}
</div>
{% endif %}

View File

@ -3,6 +3,7 @@ __author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
from octoprint.settings import settings
import octoprint.util as util
import os
import threading
@ -10,6 +11,7 @@ import urllib
import time
import subprocess
import fnmatch
import datetime
def getFinishedTimelapses():
files = []
@ -17,9 +19,12 @@ def getFinishedTimelapses():
for osFile in os.listdir(basedir):
if not fnmatch.fnmatch(osFile, "*.mpg"):
continue
statResult = os.stat(os.path.join(basedir, osFile))
files.append({
"name": osFile,
"size": os.stat(os.path.join(basedir, osFile)).st_size
"size": util.getFormattedSize(statResult.st_size),
"bytes": statResult.st_size,
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
})
return files