зеркало из https://github.com/nextcloud/news.git
This commit is contained in:
Родитель
7867824595
Коммит
d538613b60
|
@ -1,3 +1,8 @@
|
|||
owncloud-news (8.7.0)
|
||||
* **Enhancement**: Mark current article as active while scrolling
|
||||
* **Enhancement**: Clicking on an article sets it as active, #791
|
||||
* **Enhancement**: Keyboard shortcuts will target the currently active element, #791
|
||||
|
||||
owncloud-news (8.6.0)
|
||||
* **Enhancement**: Also publish error count and last error message through API, #977
|
||||
|
||||
|
|
|
@ -193,6 +193,10 @@
|
|||
background-image: linear-gradient(top, rgb(248,248,248) 0, rgb(255,255,255) 84px);
|
||||
}
|
||||
|
||||
#app-content .item.active {
|
||||
background-image: linear-gradient(to right, orange 0, orange 2px, #ffffff 2px);
|
||||
}
|
||||
|
||||
#app-content :not(.compact) .item {
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"t": true,
|
||||
"url": true,
|
||||
"navigator": true,
|
||||
"oc_requesttoken": true
|
||||
"oc_requesttoken": true,
|
||||
"_": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,197 +8,219 @@
|
|||
* @copyright Bernhard Posselt 2014
|
||||
*/
|
||||
app.controller('ContentController',
|
||||
function (Publisher, FeedResource, ItemResource, SettingsResource, data,
|
||||
$route, $routeParams, $location, FEED_TYPE, ITEM_AUTO_PAGE_SIZE, Loading) {
|
||||
'use strict';
|
||||
function (Publisher, FeedResource, ItemResource, SettingsResource, data,
|
||||
$route, $routeParams, $location, FEED_TYPE, ITEM_AUTO_PAGE_SIZE,
|
||||
Loading, $filter) {
|
||||
'use strict';
|
||||
|
||||
ItemResource.clear();
|
||||
|
||||
// distribute data to models based on key
|
||||
Publisher.publishAll(data);
|
||||
|
||||
|
||||
this.isAutoPagingEnabled = true;
|
||||
|
||||
// the interface should show a hint if there are not enough items sent so
|
||||
// it's assumed that theres nothing to autpage
|
||||
if (ItemResource.size() >= ITEM_AUTO_PAGE_SIZE) {
|
||||
this.isNothingMoreToAutoPage = false;
|
||||
} else {
|
||||
this.isNothingMoreToAutoPage = true;
|
||||
}
|
||||
|
||||
this.getItems = function () {
|
||||
return ItemResource.getAll();
|
||||
};
|
||||
|
||||
this.toggleStar = function (itemId) {
|
||||
ItemResource.toggleStar(itemId);
|
||||
};
|
||||
|
||||
this.toggleItem = function (item) {
|
||||
// TODO: unittest
|
||||
if (this.isCompactView()) {
|
||||
item.show = !item.show;
|
||||
}
|
||||
};
|
||||
|
||||
this.isShowAll = function () {
|
||||
return SettingsResource.get('showAll');
|
||||
};
|
||||
|
||||
this.markRead = function (itemId) {
|
||||
var item = ItemResource.get(itemId);
|
||||
|
||||
if (!item.keepUnread && item.unread === true) {
|
||||
ItemResource.markItemRead(itemId);
|
||||
FeedResource.markItemOfFeedRead(item.feedId);
|
||||
}
|
||||
};
|
||||
|
||||
this.getFeed = function (feedId) {
|
||||
return FeedResource.getById(feedId);
|
||||
};
|
||||
|
||||
this.toggleKeepUnread = function (itemId) {
|
||||
var item = ItemResource.get(itemId);
|
||||
if (!item.unread) {
|
||||
FeedResource.markItemOfFeedUnread(item.feedId);
|
||||
ItemResource.markItemRead(itemId, false);
|
||||
}
|
||||
|
||||
item.keepUnread = !item.keepUnread;
|
||||
};
|
||||
|
||||
var self = this;
|
||||
var getOrdering = function () {
|
||||
var ordering = SettingsResource.get('oldestFirst');
|
||||
|
||||
if (self.isFeed()) {
|
||||
var feed = FeedResource.getById($routeParams.id);
|
||||
if (feed && feed.ordering === 1) {
|
||||
ordering = true;
|
||||
} else if (feed && feed.ordering === 2) {
|
||||
ordering = false;
|
||||
}
|
||||
}
|
||||
|
||||
return ordering;
|
||||
};
|
||||
|
||||
this.orderBy = function () {
|
||||
if (getOrdering()) {
|
||||
return 'id';
|
||||
} else {
|
||||
return '-id';
|
||||
}
|
||||
};
|
||||
|
||||
this.isCompactView = function () {
|
||||
return SettingsResource.get('compact');
|
||||
};
|
||||
|
||||
this.isCompactExpand = function () {
|
||||
return SettingsResource.get('compactExpand');
|
||||
};
|
||||
|
||||
this.autoPagingEnabled = function () {
|
||||
return this.isAutoPagingEnabled;
|
||||
};
|
||||
|
||||
this.markReadEnabled = function () {
|
||||
return !SettingsResource.get('preventReadOnScroll');
|
||||
};
|
||||
|
||||
this.scrollRead = function (itemIds) {
|
||||
var ids = [];
|
||||
var feedIds = [];
|
||||
|
||||
itemIds.forEach(function (itemId) {
|
||||
var item = ItemResource.get(itemId);
|
||||
if (!item.keepUnread) {
|
||||
ids.push(itemId);
|
||||
feedIds.push(item.feedId);
|
||||
}
|
||||
});
|
||||
|
||||
if (ids.length > 0) {
|
||||
FeedResource.markItemsOfFeedsRead(feedIds);
|
||||
ItemResource.markItemsRead(ids);
|
||||
}
|
||||
};
|
||||
|
||||
this.isFeed = function () {
|
||||
return $route.current.$$route.type === FEED_TYPE.FEED;
|
||||
};
|
||||
|
||||
this.autoPage = function () {
|
||||
if (this.isNothingMoreToAutoPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in case a subsequent autopage request comes in wait until
|
||||
// the current one finished and execute a request immediately afterwards
|
||||
if (!this.isAutoPagingEnabled) {
|
||||
this.autoPageAgain = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isAutoPagingEnabled = false;
|
||||
this.autoPageAgain = false;
|
||||
|
||||
var type = $route.current.$$route.type;
|
||||
var id = $routeParams.id;
|
||||
var oldestFirst = getOrdering();
|
||||
var showAll = SettingsResource.get('showAll');
|
||||
var self = this;
|
||||
var search = $location.search().search;
|
||||
ItemResource.clear();
|
||||
|
||||
Loading.setLoading('autopaging', true);
|
||||
// distribute data to models based on key
|
||||
Publisher.publishAll(data);
|
||||
|
||||
ItemResource.autoPage(type, id, oldestFirst, showAll, search)
|
||||
.success(function (data) {
|
||||
Publisher.publishAll(data);
|
||||
|
||||
if (data.items.length >= ITEM_AUTO_PAGE_SIZE) {
|
||||
self.isAutoPagingEnabled = true;
|
||||
this.getFirstItem = function () {
|
||||
var orderFilter = $filter('orderBy');
|
||||
var orderedItems = orderFilter(this.getItems(), this.orderBy());
|
||||
var firstItem = orderedItems[0];
|
||||
if (firstItem === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
self.isNothingMoreToAutoPage = true;
|
||||
return firstItem.id;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.isAutoPagingEnabled = true;
|
||||
// the interface should show a hint if there are not enough items sent
|
||||
// it's assumed that theres nothing to autpage
|
||||
|
||||
if (ItemResource.size() >= ITEM_AUTO_PAGE_SIZE) {
|
||||
this.isNothingMoreToAutoPage = false;
|
||||
} else {
|
||||
this.isNothingMoreToAutoPage = true;
|
||||
}
|
||||
|
||||
this.getItems = function () {
|
||||
return ItemResource.getAll();
|
||||
};
|
||||
|
||||
this.isItemActive = function (id) {
|
||||
return this.activeItem === id;
|
||||
};
|
||||
|
||||
this.setItemActive = function (id) {
|
||||
this.activeItem = id;
|
||||
};
|
||||
|
||||
this.toggleStar = function (itemId) {
|
||||
ItemResource.toggleStar(itemId);
|
||||
};
|
||||
|
||||
this.toggleItem = function (item) {
|
||||
// TODO: unittest
|
||||
if (this.isCompactView()) {
|
||||
item.show = !item.show;
|
||||
}
|
||||
};
|
||||
|
||||
this.isShowAll = function () {
|
||||
return SettingsResource.get('showAll');
|
||||
};
|
||||
|
||||
this.markRead = function (itemId) {
|
||||
var item = ItemResource.get(itemId);
|
||||
|
||||
if (!item.keepUnread && item.unread === true) {
|
||||
ItemResource.markItemRead(itemId);
|
||||
FeedResource.markItemOfFeedRead(item.feedId);
|
||||
}
|
||||
};
|
||||
|
||||
this.getFeed = function (feedId) {
|
||||
return FeedResource.getById(feedId);
|
||||
};
|
||||
|
||||
this.toggleKeepUnread = function (itemId) {
|
||||
var item = ItemResource.get(itemId);
|
||||
if (!item.unread) {
|
||||
FeedResource.markItemOfFeedUnread(item.feedId);
|
||||
ItemResource.markItemRead(itemId, false);
|
||||
}
|
||||
|
||||
if (self.isAutoPagingEnabled && self.autoPageAgain) {
|
||||
self.autoPage();
|
||||
item.keepUnread = !item.keepUnread;
|
||||
};
|
||||
|
||||
var getOrdering = function () {
|
||||
var ordering = SettingsResource.get('oldestFirst');
|
||||
|
||||
if (self.isFeed()) {
|
||||
var feed = FeedResource.getById($routeParams.id);
|
||||
if (feed && feed.ordering === 1) {
|
||||
ordering = true;
|
||||
} else if (feed && feed.ordering === 2) {
|
||||
ordering = false;
|
||||
}
|
||||
}
|
||||
}).error(function () {
|
||||
self.isAutoPagingEnabled = true;
|
||||
}).finally(function () {
|
||||
Loading.setLoading('autopaging', false);
|
||||
});
|
||||
};
|
||||
|
||||
this.getRelativeDate = function (timestamp) {
|
||||
if (timestamp !== undefined && timestamp !== '') {
|
||||
var languageCode = SettingsResource.get('language');
|
||||
var date =
|
||||
moment.unix(timestamp).locale(languageCode).fromNow() + '';
|
||||
return date;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
return ordering;
|
||||
};
|
||||
|
||||
this.refresh = function () {
|
||||
$route.reload();
|
||||
};
|
||||
this.orderBy = function () {
|
||||
if (getOrdering()) {
|
||||
return 'id';
|
||||
} else {
|
||||
return '-id';
|
||||
}
|
||||
};
|
||||
|
||||
this.getMediaType = function (type) {
|
||||
if (type && type.indexOf('audio') === 0) {
|
||||
return 'audio';
|
||||
} else if (type && type.indexOf('video') === 0) {
|
||||
return 'video';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
this.isCompactView = function () {
|
||||
return SettingsResource.get('compact');
|
||||
};
|
||||
|
||||
});
|
||||
this.isCompactExpand = function () {
|
||||
return SettingsResource.get('compactExpand');
|
||||
};
|
||||
|
||||
this.autoPagingEnabled = function () {
|
||||
return this.isAutoPagingEnabled;
|
||||
};
|
||||
|
||||
this.markReadEnabled = function () {
|
||||
return !SettingsResource.get('preventReadOnScroll');
|
||||
};
|
||||
|
||||
this.scrollRead = function (itemIds) {
|
||||
var ids = [];
|
||||
var feedIds = [];
|
||||
|
||||
itemIds.forEach(function (itemId) {
|
||||
var item = ItemResource.get(itemId);
|
||||
if (!item.keepUnread) {
|
||||
ids.push(itemId);
|
||||
feedIds.push(item.feedId);
|
||||
}
|
||||
});
|
||||
|
||||
if (ids.length > 0) {
|
||||
FeedResource.markItemsOfFeedsRead(feedIds);
|
||||
ItemResource.markItemsRead(ids);
|
||||
}
|
||||
};
|
||||
|
||||
this.isFeed = function () {
|
||||
return $route.current.$$route.type === FEED_TYPE.FEED;
|
||||
};
|
||||
|
||||
this.autoPage = function () {
|
||||
if (this.isNothingMoreToAutoPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in case a subsequent autopage request comes in wait until
|
||||
// the current one finished and execute a request immediately
|
||||
// afterwards
|
||||
if (!this.isAutoPagingEnabled) {
|
||||
this.autoPageAgain = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isAutoPagingEnabled = false;
|
||||
this.autoPageAgain = false;
|
||||
|
||||
var type = $route.current.$$route.type;
|
||||
var id = $routeParams.id;
|
||||
var oldestFirst = getOrdering();
|
||||
var showAll = SettingsResource.get('showAll');
|
||||
var self = this;
|
||||
var search = $location.search().search;
|
||||
|
||||
Loading.setLoading('autopaging', true);
|
||||
|
||||
ItemResource.autoPage(type, id, oldestFirst, showAll, search)
|
||||
.success(function (data) {
|
||||
Publisher.publishAll(data);
|
||||
|
||||
if (data.items.length >= ITEM_AUTO_PAGE_SIZE) {
|
||||
self.isAutoPagingEnabled = true;
|
||||
} else {
|
||||
self.isNothingMoreToAutoPage = true;
|
||||
}
|
||||
|
||||
if (self.isAutoPagingEnabled && self.autoPageAgain) {
|
||||
self.autoPage();
|
||||
}
|
||||
}).error(function () {
|
||||
self.isAutoPagingEnabled = true;
|
||||
}).finally(function () {
|
||||
Loading.setLoading('autopaging', false);
|
||||
});
|
||||
};
|
||||
|
||||
this.getRelativeDate = function (timestamp) {
|
||||
if (timestamp !== undefined && timestamp !== '') {
|
||||
var languageCode = SettingsResource.get('language');
|
||||
var date =
|
||||
moment.unix(timestamp).locale(languageCode).fromNow() + '';
|
||||
return date;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
this.refresh = function () {
|
||||
$route.reload();
|
||||
};
|
||||
|
||||
this.getMediaType = function (type) {
|
||||
if (type && type.indexOf('audio') === 0) {
|
||||
return 'audio';
|
||||
} else if (type && type.indexOf('video') === 0) {
|
||||
return 'video';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
this.activeItem = this.getFirstItem();
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* ownCloud - News
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3 or
|
||||
* later. See the COPYING file.
|
||||
*
|
||||
* @author Bernhard Posselt <dev@bernhard-posselt.com>
|
||||
* @copyright Bernhard Posselt 2016
|
||||
*/
|
||||
|
||||
app.directive('newsOnActive', function ($parse) {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, elem, attrs) {
|
||||
elem.on('set-active', function () {
|
||||
var callback = $parse(attrs.newsOnActive);
|
||||
scope.$apply(callback);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
|
@ -234,20 +234,12 @@
|
|||
}
|
||||
};
|
||||
|
||||
var getActiveElement = function (scrollArea) {
|
||||
return scrollArea.find('.item.active:first');
|
||||
};
|
||||
|
||||
var onActiveItem = function (scrollArea, callback) {
|
||||
var items = scrollArea.find('.item');
|
||||
|
||||
items.each(function (index, item) {
|
||||
item = $(item);
|
||||
|
||||
// 130px of the item should be visible
|
||||
if ((item.height() + item.position().top) > 30) {
|
||||
callback(item);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
callback(getActiveElement(scrollArea));
|
||||
};
|
||||
|
||||
var toggleUnread = function (scrollArea) {
|
||||
|
@ -275,6 +267,10 @@
|
|||
});
|
||||
};
|
||||
|
||||
var setItemActive = function (element) {
|
||||
element.dispatchEvent(new CustomEvent('set-active'));
|
||||
};
|
||||
|
||||
var scrollToItem = function (scrollArea, item, expandItemInCompact) {
|
||||
// if you go to the next article in compact view, it should
|
||||
// expand the current one
|
||||
|
@ -282,6 +278,8 @@
|
|||
item.offset().top - scrollArea.offset().top + scrollArea.scrollTop()
|
||||
);
|
||||
|
||||
setItemActive(item[0]);
|
||||
|
||||
if (expandItemInCompact) {
|
||||
onActiveItem(scrollArea, function (item) {
|
||||
if (!item.hasClass('open')) {
|
||||
|
@ -292,64 +290,49 @@
|
|||
};
|
||||
|
||||
var scrollToNextItem = function (scrollArea, expandItemInCompact) {
|
||||
var items = scrollArea.find('.item');
|
||||
var jumped = false;
|
||||
|
||||
items.each(function (index, item) {
|
||||
item = $(item);
|
||||
|
||||
// special treatment for items that have expand enabled:
|
||||
// if you click next and the first item has not been expaned and
|
||||
// is on the top, it should be expanded instead of the next one
|
||||
if ((item.position().top === 0 && expandItemInCompact &&
|
||||
!item.hasClass('open')) ||
|
||||
item.position().top > 10) {
|
||||
scrollToItem(scrollArea, item, expandItemInCompact);
|
||||
|
||||
jumped = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// in case this is the last item it should still scroll below the top
|
||||
if (!jumped) {
|
||||
var activeElement = getActiveElement(scrollArea);
|
||||
var nextElement = activeElement.next();
|
||||
if (nextElement.length > 0) {
|
||||
scrollToItem(scrollArea, nextElement, expandItemInCompact);
|
||||
} else {
|
||||
// in case this is the last item it should still scroll below the
|
||||
scrollArea.scrollTop(scrollArea.prop('scrollHeight'));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var scrollToPreviousItem = function (navigationArea, scrollArea,
|
||||
expandItemInCompact) {
|
||||
var items = scrollArea.find('.item');
|
||||
var jumped = false;
|
||||
var activeElement = getActiveElement(scrollArea);
|
||||
var previousElement = activeElement.prev();
|
||||
|
||||
items.each(function (index, item) {
|
||||
item = $(item);
|
||||
|
||||
if ((item.position().top + 10) >= 0) {
|
||||
var previous = item.prev();
|
||||
|
||||
// if there are no items before the current one
|
||||
if (previous.length > 0) {
|
||||
scrollToItem(scrollArea, previous, expandItemInCompact);
|
||||
} else {
|
||||
tryReload(navigationArea, scrollArea);
|
||||
scrollArea.scrollTop(0);
|
||||
}
|
||||
|
||||
jumped = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// if there was no jump jump to the last element
|
||||
if (!jumped && items.length > 0) {
|
||||
scrollToItem(scrollArea, items.last());
|
||||
// if the active element has been scrolled, the previous element
|
||||
// should be the active one
|
||||
if (activeElement.position().top + 20 <= 0) {
|
||||
scrollToItem(scrollArea, activeElement, expandItemInCompact);
|
||||
} else if (previousElement.length > 0) {
|
||||
scrollToItem(scrollArea, previousElement, expandItemInCompact);
|
||||
} else {
|
||||
tryReload(navigationArea, scrollArea);
|
||||
scrollArea.scrollTop(0);
|
||||
}
|
||||
};
|
||||
|
||||
// mark current item as active when scrolling
|
||||
$(document).ready(function () {
|
||||
var detectAndSetActiveItem = function () {
|
||||
var items = $('#app-content').find('.item');
|
||||
items.each(function (index, item) {
|
||||
var $item = $(item);
|
||||
var bottom = $item.position().top + $item.outerHeight(true);
|
||||
console.log(bottom);
|
||||
if ((bottom - 20) >= 0){
|
||||
setItemActive(item);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
$('#app-content').scroll(_.debounce(detectAndSetActiveItem, 250));
|
||||
});
|
||||
|
||||
$(document).keyup(function (event) {
|
||||
var keyCode = event.keyCode;
|
||||
|
|
|
@ -22,6 +22,13 @@ describe('ContentController', function () {
|
|||
SUBSCRIPTIONS: 3,
|
||||
SHARED: 4
|
||||
});
|
||||
$provide.constant('$route', {
|
||||
current: {
|
||||
$$route: {
|
||||
type: 3
|
||||
}
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
|
@ -50,7 +57,7 @@ describe('ContentController', function () {
|
|||
ItemResource.clear = jasmine.createSpy('clear');
|
||||
|
||||
$controller('ContentController', {
|
||||
data: {},
|
||||
data: {}
|
||||
});
|
||||
|
||||
expect(ItemResource.clear).toHaveBeenCalled();
|
||||
|
@ -395,6 +402,14 @@ describe('ContentController', function () {
|
|||
}));
|
||||
|
||||
|
||||
it ('should toggle active item', function ($controller) {
|
||||
var ctrl = $controller('ContentController');
|
||||
expect(ctrl.isItemActive(3)).toBe(undefined);
|
||||
ctrl.setItemActive(3);
|
||||
expect(ctrl.isItemActive(4)).toBe(false);
|
||||
expect(ctrl.isItemActive(3)).toBe(true);
|
||||
});
|
||||
|
||||
it('should autopage if more than 0 elements',
|
||||
inject(function ($controller, ItemResource, Publisher) {
|
||||
|
||||
|
@ -538,6 +553,11 @@ describe('ContentController', function () {
|
|||
|
||||
it('should refresh the page', inject(function ($controller) {
|
||||
var route = {
|
||||
current: {
|
||||
$$route: {
|
||||
type: 3
|
||||
}
|
||||
},
|
||||
reload: jasmine.createSpy('reload')
|
||||
};
|
||||
var ctrl = $controller('ContentController', {
|
||||
|
|
|
@ -23,8 +23,9 @@
|
|||
ng-repeat="item in Content.getItems() |
|
||||
orderBy:[Content.orderBy()] track by item.id"
|
||||
ng-mouseup="Content.markRead(item.id)"
|
||||
ng-click="Content.markRead(item.id)"
|
||||
ng-class="{read: !item.unread, open: item.show}"
|
||||
ng-click="Content.markRead(item.id); Content.setItemActive(item.id)"
|
||||
news-on-active="Content.setItemActive(item.id)"
|
||||
ng-class="{read: !item.unread, open: item.show, active: Content.isItemActive(item.id)}"
|
||||
data-id="{{ ::item.id }}">
|
||||
|
||||
<div class="utils" ng-click="Content.toggleItem(item)">
|
||||
|
|
Загрузка…
Ссылка в новой задаче