Merge pull request #1703 from AudriusButkevicius/page

Add pagination to Out of sync item list (fixes #1509)
This commit is contained in:
Jakob Borg 2015-04-26 18:42:17 +09:00
commit ff984425a3
7 changed files with 712 additions and 90 deletions

View File

@ -111,7 +111,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
getRestMux.HandleFunc("/rest/db/completion", withModel(m, restGetDBCompletion)) // device folder
getRestMux.HandleFunc("/rest/db/file", withModel(m, restGetDBFile)) // folder file
getRestMux.HandleFunc("/rest/db/ignores", withModel(m, restGetDBIgnores)) // folder
getRestMux.HandleFunc("/rest/db/need", withModel(m, restGetDBNeed)) // folder
getRestMux.HandleFunc("/rest/db/need", withModel(m, restGetDBNeed)) // folder [perpage] [page]
getRestMux.HandleFunc("/rest/db/status", withModel(m, restGetDBStatus)) // folder
getRestMux.HandleFunc("/rest/db/browse", withModel(m, restGetDBBrowse)) // folder [prefix] [dirsonly] [levels]
getRestMux.HandleFunc("/rest/events", restGetEvents) // since [limit]
@ -133,7 +133,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
// The POST handlers
postRestMux := http.NewServeMux()
postRestMux.HandleFunc("/rest/db/prio", withModel(m, restPostDBPrio)) // folder file
postRestMux.HandleFunc("/rest/db/prio", withModel(m, restPostDBPrio)) // folder file [perpage] [page]
postRestMux.HandleFunc("/rest/db/ignores", withModel(m, restPostDBIgnores)) // folder
postRestMux.HandleFunc("/rest/db/override", withModel(m, restPostDBOverride)) // folder
postRestMux.HandleFunc("/rest/db/scan", withModel(m, restPostDBScan)) // folder [sub...]
@ -379,15 +379,29 @@ func restPostDBOverride(m *model.Model, w http.ResponseWriter, r *http.Request)
}
func restGetDBNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var folder = qs.Get("folder")
qs := r.URL.Query()
folder := qs.Get("folder")
page, err := strconv.Atoi(qs.Get("page"))
if err != nil || page < 1 {
page = 1
}
perpage, err := strconv.Atoi(qs.Get("perpage"))
if err != nil || perpage < 1 {
perpage = 1 << 16
}
progress, queued, rest, total := m.NeedFolderFiles(folder, page, perpage)
progress, queued, rest := m.NeedFolderFiles(folder, 100)
// Convert the struct to a more loose structure, and inject the size.
output := map[string][]jsonDBFileInfo{
output := map[string]interface{}{
"progress": toNeedSlice(progress),
"queued": toNeedSlice(queued),
"rest": toNeedSlice(rest),
"total": total,
"page": page,
"perpage": perpage,
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")

View File

@ -967,10 +967,22 @@
<hr/>
<table class="table table-striped table-condensed">
<tr ng-repeat="f in needed.progress" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.name}}">{{f.name | basename}}</td>
<td ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal">
<!-- Icon -->
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
<!-- Name -->
<td ng-if="f.type != 'queued'" title="{{f.name}}">{{f.name | basename}}</td>
<td ng-if="f.type == 'queued'">
<a href="" ng-click="bumpFile(neededFolder, f.name)" title="{{'Move to top of queue' | translate}}">
<span class="glyphicon glyphicon-eject"></span>
</a>
<span title="{{f.name}}">&ensp;{{f.name | basename}}</span>
</td>
<!-- Size/Progress -->
<td ng-if="f.type == 'progress' && f.action == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
@ -982,24 +994,20 @@
</span>
</div>
</td>
<td class="text-right small-data" ng-if="a != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
<td class="text-right small-data" ng-if="f.type != 'progress' || f.action != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
</td>
</tr>
<tr ng-repeat="f in needed.queued" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td><a href="" ng-if="$index != 0" ng-click="bumpFile(neededFolder, f.name)" title="{{'Move to top of queue' | translate}}"><span class="glyphicon glyphicon-eject"></span></a><span ng-if="$index != 0">&ensp;</span><span title="{{f.name}}">{{f.name | basename}}</span></td>
<td class="text-right small-data">
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
</td>
</tr>
<tr ng-repeat="f in needed.rest" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.name}}">{{f.name | basename}}</td>
<td class="text-right small-data"><span ng-if="f.size > 0">{{f.size | binary}}B</span></td>
</tr>
</table>
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 20, 30, 50, 100]" ng-class="{ active: neededPageSize == option }">
<a href="#" ng-click="neededChangePageSize(option)">{{option}}</a>
<li>
</ul>
<div class="clearfix">
</modal>
<!-- About modal -->
@ -1087,6 +1095,7 @@
<script src="vendor/angular/angular.min.js"></script>
<script src="vendor/angular/angular-translate.min.js"></script>
<script src="vendor/angular/angular-translate-loader.min.js"></script>
<script src="vendor/angular/angular-dirPagination.js"></script>
<script src="vendor/jquery/jquery-2.0.3.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
<!-- / vendor scripts -->

View File

@ -9,6 +9,7 @@
/*global $: false, angular: false, console: false, validLangs: false */
var syncthing = angular.module('syncthing', [
'angularUtils.directives.dirPagination',
'pascalprecht.translate',
'syncthing.core'

View File

@ -40,6 +40,10 @@ angular.module('syncthing.core')
$scope.folderStats = {};
$scope.progress = {};
$scope.version = {};
$scope.needed = [];
$scope.neededTotal = 0;
$scope.neededCurrentPage = 1;
$scope.neededPageSize = 10;
$(window).bind('beforeunload', function () {
navigatingAway = true;
@ -415,14 +419,63 @@ angular.module('syncthing.core')
}
function refreshNeed(folder) {
$http.get(urlbase + "/db/need?folder=" + encodeURIComponent(folder)).success(function (data) {
var url = urlbase + "/db/need?folder=" + encodeURIComponent(folder);
url += "&page=" + $scope.neededCurrentPage;
url += "&perpage=" + $scope.neededPageSize;
$http.get(url).success(function (data) {
if ($scope.neededFolder == folder) {
console.log("refreshNeed", folder, data);
$scope.needed = data;
parseNeeded(data);
}
}).error($scope.emitHTTPError);
}
function needAction(file) {
var fDelete = 4096;
var fDirectory = 16384;
if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
return 'rmdir';
} else if ((file.flags & fDelete) === fDelete) {
return 'rm';
} else if ((file.flags & fDirectory) === fDirectory) {
return 'touch';
} else {
return 'sync';
}
};
function parseNeeded(data) {
var merged = [];
data.progress.forEach(function (item) {
item.type = "progress";
item.action = needAction(item);
merged.push(item);
});
data.queued.forEach(function (item) {
item.type = "queued";
item.action = needAction(item);
merged.push(item);
});
data.rest.forEach(function (item) {
item.type = "rest";
item.action = needAction(item);
merged.push(item);
});
$scope.needed = merged;
$scope.neededTotal = data.total;
}
$scope.neededPageChanged = function (page) {
$scope.neededCurrentPage = page;
refreshNeed($scope.neededFolder);
};
$scope.neededChangePageSize = function (perpage) {
$scope.neededPageSize = perpage;
refreshNeed($scope.neededFolder);
}
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
@ -1181,24 +1234,11 @@ angular.module('syncthing.core')
$('#needed').modal().on('hidden.bs.modal', function () {
$scope.neededFolder = undefined;
$scope.needed = undefined;
$scope.neededTotal = 0;
$scope.neededCurrentPage = 1;
});
};
$scope.needAction = function (file) {
var fDelete = 4096;
var fDirectory = 16384;
if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
return 'rmdir';
} else if ((file.flags & fDelete) === fDelete) {
return 'rm';
} else if ((file.flags & fDirectory) === fDirectory) {
return 'touch';
} else {
return 'sync';
}
};
$scope.override = function (folder) {
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
};
@ -1220,10 +1260,14 @@ angular.module('syncthing.core')
};
$scope.bumpFile = function (folder, file) {
$http.post(urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file)).success(function (data) {
var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
// In order to get the right view of data in the response.
url += "&page=" + $scope.neededCurrentPage;
url += "&perpage=" + $scope.neededPageSize;
$http.post(url).success(function (data) {
if ($scope.neededFolder == folder) {
console.log("bumpFile", folder, data);
$scope.needed = data;
parseNeeded(data);
}
}).error($scope.emitHTTPError);
};

View File

@ -0,0 +1,520 @@
/**
* dirPagination - AngularJS module for paginating (almost) anything.
*
*
* Credits
* =======
*
* Daniel Tabuenca: https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ
* for the idea on how to dynamically invoke the ng-repeat directive.
*
* I borrowed a couple of lines and a few attribute names from the AngularUI Bootstrap project:
* https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js
*
* Copyright 2014 Michael Bromley <michael@michaelbromley.co.uk>
*/
(function() {
/**
* Config
*/
var moduleName = 'angularUtils.directives.dirPagination';
var DEFAULT_ID = '__default';
/**
* Module
*/
var module;
try {
module = angular.module(moduleName);
} catch(err) {
// named module does not exist, so create one
module = angular.module(moduleName, []);
}
module
.directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
.directive('dirPaginateNoCompile', noCompileDirective)
.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
.filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
.service('paginationService', paginationService)
.provider('paginationTemplate', paginationTemplateProvider)
.run(['$templateCache',dirPaginationControlsTemplateInstaller]);
function dirPaginateDirective($compile, $parse, paginationService) {
return {
terminal: true,
multiElement: true,
compile: dirPaginationCompileFn
};
function dirPaginationCompileFn(tElement, tAttrs){
var expression = tAttrs.dirPaginate;
// regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/;
if (match[2].match(filterPattern) === null) {
throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
}
var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
var collectionGetter = $parse(itemsPerPageFilterRemoved);
addNoCompileAttributes(tElement);
// If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any
// dir-pagination-controls directives that may be looking for this ID.
var rawId = tAttrs.paginationId || DEFAULT_ID;
paginationService.registerInstance(rawId);
return function dirPaginationLinkFn(scope, element, attrs){
// Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and
// potentially register a new ID if it evaluates to a different value than the rawId.
var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID;
paginationService.registerInstance(paginationId);
var repeatExpression = getRepeatExpression(expression, paginationId);
addNgRepeatToElement(element, attrs, repeatExpression);
removeTemporaryAttributes(element);
var compiled = $compile(element);
var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId);
paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);
if (typeof attrs.totalItems !== 'undefined') {
paginationService.setAsyncModeTrue(paginationId);
scope.$watch(function() {
return $parse(attrs.totalItems)(scope);
}, function (result) {
if (0 <= result) {
paginationService.setCollectionLength(paginationId, result);
}
});
} else {
scope.$watchCollection(function() {
return collectionGetter(scope);
}, function(collection) {
if (collection) {
paginationService.setCollectionLength(paginationId, collection.length);
}
});
}
// Delegate to the link function returned by the new compilation of the ng-repeat
compiled(scope);
};
}
/**
* If a pagination id has been specified, we need to check that it is present as the second argument passed to
* the itemsPerPage filter. If it is not there, we add it and return the modified expression.
*
* @param expression
* @param paginationId
* @returns {*}
*/
function getRepeatExpression(expression, paginationId) {
var repeatExpression,
idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);
if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'");
} else {
repeatExpression = expression;
}
return repeatExpression;
}
/**
* Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the
* appropriate multi-element ng-repeat to the first and last element in the range.
* @param element
* @param attrs
* @param repeatExpression
*/
function addNgRepeatToElement(element, attrs, repeatExpression) {
if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) {
// using multiElement mode (dir-paginate-start, dir-paginate-end)
attrs.$set('ngRepeatStart', repeatExpression);
element.eq(element.length - 1).attr('ng-repeat-end', true);
} else {
attrs.$set('ngRepeat', repeatExpression);
}
}
/**
* Adds the dir-paginate-no-compile directive to each element in the tElement range.
* @param tElement
*/
function addNoCompileAttributes(tElement) {
angular.forEach(tElement, function(el) {
if (el.nodeType === Node.ELEMENT_NODE) {
angular.element(el).attr('dir-paginate-no-compile', true);
}
});
}
/**
* Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives.
* @param element
*/
function removeTemporaryAttributes(element) {
angular.forEach(element, function(el) {
if (el.nodeType === Node.ELEMENT_NODE) {
angular.element(el).removeAttr('dir-paginate-no-compile');
}
});
element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate');
element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end');
}
/**
* Creates a getter function for the current-page attribute, using the expression provided or a default value if
* no current-page expression was specified.
*
* @param scope
* @param attrs
* @param paginationId
* @returns {*}
*/
function makeCurrentPageGetterFn(scope, attrs, paginationId) {
var currentPageGetter;
if (attrs.currentPage) {
currentPageGetter = $parse(attrs.currentPage);
} else {
// if the current-page attribute was not set, we'll make our own
var defaultCurrentPage = paginationId + '__currentPage';
scope[defaultCurrentPage] = 1;
currentPageGetter = $parse(defaultCurrentPage);
}
return currentPageGetter;
}
}
/**
* This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end).
* It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of
* any inner directives. It is then removed in the link function, and all inner directives are then manually compiled.
*/
function noCompileDirective() {
return {
priority: 5000,
terminal: true
};
}
function dirPaginationControlsTemplateInstaller($templateCache) {
$templateCache.put('angularUtils.directives.dirPagination.template', '<ul class="pagination" ng-if="1 < pages.length"><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(1)">&laquo;</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(pagination.current - 1)">&lsaquo;</a></li><li ng-repeat="pageNumber in pages track by $index" ng-class="{ active : pagination.current == pageNumber, disabled : pageNumber == \'...\' }"><a href="" ng-click="setCurrent(pageNumber)">{{ pageNumber }}</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.current + 1)">&rsaquo;</a></li><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.last)">&raquo;</a></li></ul>');
}
function dirPaginationControlsDirective(paginationService, paginationTemplate) {
var numberRegex = /^\d+$/;
return {
restrict: 'AE',
templateUrl: function(elem, attrs) {
return attrs.templateUrl || paginationTemplate.getPath();
},
scope: {
maxSize: '=?',
onPageChange: '&?',
paginationId: '=?'
},
link: dirPaginationControlsLinkFn
};
function dirPaginationControlsLinkFn(scope, element, attrs) {
// rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has
// not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is
// no corresponding dir-paginate directive and wrongly throwing an exception.
var rawId = attrs.paginationId || DEFAULT_ID;
var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID;
if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) {
var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' ';
throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.';
}
if (!scope.maxSize) { scope.maxSize = 9; }
scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;
var paginationRange = Math.max(scope.maxSize, 5);
scope.pages = [];
scope.pagination = {
last: 1,
current: 1
};
scope.range = {
lower: 1,
upper: 1,
total: 1
};
scope.$watch(function() {
return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
}, function(length) {
if (0 < length) {
generatePagination();
}
});
scope.$watch(function() {
return (paginationService.getItemsPerPage(paginationId));
}, function(current, previous) {
if (current != previous && typeof previous !== 'undefined') {
goToPage(scope.pagination.current);
}
});
scope.$watch(function() {
return paginationService.getCurrentPage(paginationId);
}, function(currentPage, previousPage) {
if (currentPage != previousPage) {
goToPage(currentPage);
}
});
scope.setCurrent = function(num) {
if (isValidPageNumber(num)) {
num = parseInt(num, 10);
paginationService.setCurrentPage(paginationId, num);
}
};
function goToPage(num) {
if (isValidPageNumber(num)) {
scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
scope.pagination.current = num;
updateRangeValues();
// if a callback has been set, then call it with the page number as an argument
if (scope.onPageChange) {
scope.onPageChange({ newPageNumber : num });
}
}
}
function generatePagination() {
var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1;
scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
scope.pagination.current = page;
scope.pagination.last = scope.pages[scope.pages.length - 1];
if (scope.pagination.last < scope.pagination.current) {
scope.setCurrent(scope.pagination.last);
} else {
updateRangeValues();
}
}
/**
* This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination
* template to display the current page range, e.g. "showing 21 - 40 of 144 results";
*/
function updateRangeValues() {
var currentPage = paginationService.getCurrentPage(paginationId),
itemsPerPage = paginationService.getItemsPerPage(paginationId),
totalItems = paginationService.getCollectionLength(paginationId);
scope.range.lower = (currentPage - 1) * itemsPerPage + 1;
scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems);
scope.range.total = totalItems;
}
function isValidPageNumber(num) {
return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
}
}
/**
* Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
* links used in pagination
*
* @param currentPage
* @param rowsPerPage
* @param paginationRange
* @param collectionLength
* @returns {Array}
*/
function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) {
var pages = [];
var totalPages = Math.ceil(collectionLength / rowsPerPage);
var halfWay = Math.ceil(paginationRange / 2);
var position;
if (currentPage <= halfWay) {
position = 'start';
} else if (totalPages - halfWay < currentPage) {
position = 'end';
} else {
position = 'middle';
}
var ellipsesNeeded = paginationRange < totalPages;
var i = 1;
while (i <= totalPages && i <= paginationRange) {
var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end'));
var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start'));
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
pages.push('...');
} else {
pages.push(pageNumber);
}
i ++;
}
return pages;
}
/**
* Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position.
*
* @param i
* @param currentPage
* @param paginationRange
* @param totalPages
* @returns {*}
*/
function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
var halfWay = Math.ceil(paginationRange/2);
if (i === paginationRange) {
return totalPages;
} else if (i === 1) {
return i;
} else if (paginationRange < totalPages) {
if (totalPages - halfWay < currentPage) {
return totalPages - paginationRange + i;
} else if (halfWay < currentPage) {
return currentPage - halfWay + i;
} else {
return i;
}
} else {
return i;
}
}
}
/**
* This filter slices the collection into pages based on the current page number and number of items per page.
* @param paginationService
* @returns {Function}
*/
function itemsPerPageFilter(paginationService) {
return function(collection, itemsPerPage, paginationId) {
if (typeof (paginationId) === 'undefined') {
paginationId = DEFAULT_ID;
}
if (!paginationService.isRegistered(paginationId)) {
throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.';
}
var end;
var start;
if (collection instanceof Array) {
itemsPerPage = parseInt(itemsPerPage) || 9999999999;
if (paginationService.isAsyncMode(paginationId)) {
start = 0;
} else {
start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage;
}
end = start + itemsPerPage;
paginationService.setItemsPerPage(paginationId, itemsPerPage);
return collection.slice(start, end);
} else {
return collection;
}
};
}
/**
* This service allows the various parts of the module to communicate and stay in sync.
*/
function paginationService() {
var instances = {};
var lastRegisteredInstance;
this.registerInstance = function(instanceId) {
if (typeof instances[instanceId] === 'undefined') {
instances[instanceId] = {
asyncMode: false
};
lastRegisteredInstance = instanceId;
}
};
this.isRegistered = function(instanceId) {
return (typeof instances[instanceId] !== 'undefined');
};
this.getLastInstanceId = function() {
return lastRegisteredInstance;
};
this.setCurrentPageParser = function(instanceId, val, scope) {
instances[instanceId].currentPageParser = val;
instances[instanceId].context = scope;
};
this.setCurrentPage = function(instanceId, val) {
instances[instanceId].currentPageParser.assign(instances[instanceId].context, val);
};
this.getCurrentPage = function(instanceId) {
var parser = instances[instanceId].currentPageParser;
return parser ? parser(instances[instanceId].context) : 1;
};
this.setItemsPerPage = function(instanceId, val) {
instances[instanceId].itemsPerPage = val;
};
this.getItemsPerPage = function(instanceId) {
return instances[instanceId].itemsPerPage;
};
this.setCollectionLength = function(instanceId, val) {
instances[instanceId].collectionLength = val;
};
this.getCollectionLength = function(instanceId) {
return instances[instanceId].collectionLength;
};
this.setAsyncModeTrue = function(instanceId) {
instances[instanceId].asyncMode = true;
};
this.isAsyncMode = function(instanceId) {
return instances[instanceId].asyncMode;
};
}
/**
* This provider allows global configuration of the template path used by the dir-pagination-controls directive.
*/
function paginationTemplateProvider() {
var templatePath = 'angularUtils.directives.dirPagination.template';
this.setPath = function(path) {
templatePath = path;
};
this.$get = function() {
return {
getPath: function() {
return templatePath;
}
};
};
}
})();

File diff suppressed because one or more lines are too long

View File

@ -375,53 +375,71 @@ func (m *Model) NeedSize(folder string) (nfiles int, bytes int64) {
return
}
// NeedFiles returns the list of currently needed files in progress, queued,
// and to be queued on next puller iteration. Also takes a soft cap which is
// only respected when adding files from the model rather than the runner queue.
func (m *Model) NeedFolderFiles(folder string, max int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
// NeedFiles returns paginated list of currently needed files in progress, queued,
// and to be queued on next puller iteration, as well as the total number of
// files currently needed.
func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int) {
m.fmut.RLock()
defer m.fmut.RUnlock()
if rf, ok := m.folderFiles[folder]; ok {
var progress, queued, rest []db.FileInfoTruncated
var seen map[string]bool
total := 0
runner, ok := m.folderRunners[folder]
if ok {
progressNames, queuedNames := runner.Jobs()
progress = make([]db.FileInfoTruncated, len(progressNames))
queued = make([]db.FileInfoTruncated, len(queuedNames))
seen = make(map[string]bool, len(progressNames)+len(queuedNames))
for i, name := range progressNames {
if f, ok := rf.GetGlobalTruncated(name); ok {
progress[i] = f
seen[name] = true
}
}
for i, name := range queuedNames {
if f, ok := rf.GetGlobalTruncated(name); ok {
queued[i] = f
seen[name] = true
}
}
}
left := max - len(progress) - len(queued)
if max < 1 || left > 0 {
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
left--
ft := f.(db.FileInfoTruncated)
if !seen[ft.Name] {
rest = append(rest, ft)
}
return max < 1 || left > 0
})
}
return progress, queued, rest
rf, ok := m.folderFiles[folder]
if !ok {
return nil, nil, nil, 0
}
return nil, nil, nil
var progress, queued, rest []db.FileInfoTruncated
var seen map[string]struct{}
skip := (page - 1) * perpage
get := perpage
runner, ok := m.folderRunners[folder]
if ok {
allProgressNames, allQueuedNames := runner.Jobs()
var progressNames, queuedNames []string
progressNames, skip, get = getChunk(allProgressNames, skip, get)
queuedNames, skip, get = getChunk(allQueuedNames, skip, get)
progress = make([]db.FileInfoTruncated, len(progressNames))
queued = make([]db.FileInfoTruncated, len(queuedNames))
seen = make(map[string]struct{}, len(progressNames)+len(queuedNames))
for i, name := range progressNames {
if f, ok := rf.GetGlobalTruncated(name); ok {
progress[i] = f
seen[name] = struct{}{}
}
}
for i, name := range queuedNames {
if f, ok := rf.GetGlobalTruncated(name); ok {
queued[i] = f
seen[name] = struct{}{}
}
}
}
rest = make([]db.FileInfoTruncated, 0, perpage)
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
total++
if skip > 0 {
skip--
return true
}
if get > 0 {
ft := f.(db.FileInfoTruncated)
if _, ok := seen[ft.Name]; !ok {
rest = append(rest, ft)
get--
}
}
return true
})
return progress, queued, rest, total
}
// Index is called when a new device is connected and we receive their full index.
@ -1616,3 +1634,17 @@ func symlinkInvalid(isLink bool) bool {
}
return false
}
// Skips `skip` elements and retrieves up to `get` elements from a given slice.
// Returns the resulting slice, plus how much elements are left to skip or
// copy to satisfy the values which were provided, given the slice is not
// big enough.
func getChunk(data []string, skip, get int) ([]string, int, int) {
l := len(data)
if l <= skip {
return []string{}, skip - l, get
} else if l < skip+get {
return data[skip:l], 0, get - (l - skip)
}
return data[skip : skip+get], 0, 0
}