tagging support (#96)

This commit is contained in:
Athou
2013-10-13 10:49:44 +02:00
parent 94f469a6b1
commit 431ab92a02
53 changed files with 840 additions and 234 deletions

View File

@@ -125,17 +125,40 @@ module.controller('SubscribeCtrl', ['$scope', 'FeedService', 'CategoryService',
}]);
module.controller('CategoryTreeCtrl', ['$scope', '$timeout', '$stateParams', '$window', '$location', '$state', '$route', 'CategoryService',
'AnalyticsService',
function($scope, $timeout, $stateParams, $window, $location, $state, $route, CategoryService, AnalyticsService) {
'AnalyticsService', 'EntryService',
function($scope, $timeout, $stateParams, $window, $location, $state, $route, CategoryService, AnalyticsService, EntryService) {
$scope.selectedType = $stateParams._type;
$scope.selectedId = $stateParams._id;
$scope.EntryService = EntryService;
$scope.starred = {
id : 'starred',
name : 'Starred'
};
$scope.tags = [];
$scope.$watch('EntryService.tags', function(newValue, oldValue) {
if (newValue) {
$scope.tags = [];
_.each(newValue, function(e) {
$scope.tags.push({
id : e,
name : e,
isTag : true
});
});
}
}, true);
$scope.toTag = function(tag) {
$state.transitionTo('feeds.view', {
_type : 'tag',
_id : tag
});
};
$scope.$on('$stateChangeSuccess', function() {
$scope.selectedType = $stateParams._type;
$scope.selectedId = $stateParams._id;
@@ -201,7 +224,7 @@ module.controller('CategoryTreeCtrl', ['$scope', '$timeout', '$stateParams', '$w
var getCurrentIndex = function(id, type, flat) {
var index = -1;
for ( var i = 0; i < flat.length; i++) {
for (var i = 0; i < flat.length; i++) {
var node = flat[i];
if (node[0] == id && node[1] == type) {
index = i;
@@ -355,7 +378,7 @@ module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'F
};
return;
}
for ( var i = 0; i < CategoryService.flatCategories.length; i++) {
for (var i = 0; i < CategoryService.flatCategories.length; i++) {
var cat = CategoryService.flatCategories[i];
if (cat.id == $stateParams._id) {
$scope.category = {
@@ -423,6 +446,65 @@ module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'F
};
}]);
module.controller('TagDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', 'ProfileService', '$dialog',
function($scope, $state, $stateParams, FeedService, CategoryService, ProfileService, $dialog) {
$scope.CategoryService = CategoryService;
$scope.user = ProfileService.get();
$scope.tag = $stateParams._id;
$scope.back = function() {
$state.transitionTo('feeds.view', {
_id : $scope.tag,
_type : 'tag'
});
};
$scope.deleteTag = function() {
var category = $scope.category;
var title = 'Delete tag';
var msg = 'Delete tag ' + tag + ' ?';
var btns = [{
result : 'cancel',
label : 'Cancel'
}, {
result : 'ok',
label : 'OK',
cssClass : 'btn-primary'
}];
$dialog.messageBox(title, msg, btns).open().then(function(result) {
if (result == 'ok') {
CategoryService.remove({
id : category.id
}, function() {
CategoryService.init();
});
$state.transitionTo('feeds.view', {
_id : 'all',
_type : 'category'
});
}
});
};
$scope.save = function() {
var cat = $scope.category;
CategoryService.modify({
id : cat.id,
name : cat.name,
position : cat.position,
parentId : cat.parentId
}, function() {
CategoryService.init();
$state.transitionTo('feeds.view', {
_id : 'all',
_type : 'category'
});
});
};
}]);
module.controller('ToolbarCtrl', [
'$scope',
'$http',
@@ -501,7 +583,7 @@ module.controller('ToolbarCtrl', [
$scope.markAllAsRead = function() {
markAll();
};
$scope.markAll12Hours = function() {
markAll(new Date().getTime() - 43200000);
};
@@ -574,7 +656,7 @@ module.controller('FeedSearchCtrl', ['$scope', '$state', '$filter', '$timeout',
}
var filtered = $scope.filtered;
for ( var i = 0; i < filtered.length; i++) {
for (var i = 0; i < filtered.length; i++) {
if ($scope.focus.id == filtered[i].id) {
index = i;
break;
@@ -706,6 +788,12 @@ module.controller('FeedListCtrl', [
}
});
$scope.$watch('settingsService.settings.readingOrder', function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
$scope.$emit('emitReload');
}
});
$scope.limit = SettingsService.settings.viewMode == 'title' ? 10 : 5;
$scope.busy = false;
$scope.hasMore = true;
@@ -733,7 +821,7 @@ module.controller('FeedListCtrl', [
}
var callback = function(data) {
for ( var i = 0; i < data.entries.length; i++) {
for (var i = 0; i < data.entries.length; i++) {
var entry = data.entries[i];
if (!_.some($scope.entries, {
id : entry.id
@@ -750,15 +838,23 @@ module.controller('FeedListCtrl', [
$scope.feedLink = data.feedLink;
};
var service = $scope.selectedType == 'feed' ? FeedService : CategoryService;
service.entries({
var data = {
id : $scope.selectedId,
readType : $scope.keywords ? 'all' : $scope.settingsService.settings.readingMode,
order : $scope.settingsService.settings.readingOrder,
offset : offset,
limit : limit,
keywords : $scope.keywords
}, callback);
};
if ($scope.selectedType == 'feed') {
FeedService.entries(data, callback);
} else if ($scope.selectedType == 'category') {
CategoryService.entries(data, callback);
} else if ($scope.selectedType == 'tag') {
data.tag = data.id;
data.id = 'all';
CategoryService.entries(data, callback);
}
};
var watch_scrolling = true;
@@ -808,7 +904,7 @@ module.controller('FeedListCtrl', [
var docTop = w.scrollTop();
var current = null;
for ( var i = 0; i < $scope.entries.length; i++) {
for (var i = 0; i < $scope.entries.length; i++) {
var entry = $scope.entries[i];
var e = $('#entry_' + entry.id);
if (e.offset().top + e.height() > docTop + $('#toolbar').outerHeight()) {
@@ -871,7 +967,7 @@ module.controller('FeedListCtrl', [
$scope.markUpTo = function(entry) {
var entries = [];
for ( var i = 0; i < $scope.entries.length; i++) {
for (var i = 0; i < $scope.entries.length; i++) {
var e = $scope.entries[i];
if (!e.read) {
entries.push({
@@ -910,7 +1006,7 @@ module.controller('FeedListCtrl', [
var getCurrentIndex = function() {
var index = -1;
if ($scope.current) {
for ( var i = 0; i < $scope.entries.length; i++) {
for (var i = 0; i < $scope.entries.length; i++) {
if ($scope.current == $scope.entries[i]) {
index = i;
break;
@@ -1351,7 +1447,7 @@ module.controller('ManageDuplicateFeedsCtrl', ['$scope', 'AdminCleanupService',
var callback = function() {
alert('done!');
};
for ( var i = 0; i < $scope.counts.length; i++) {
for (var i = 0; i < $scope.counts.length; i++) {
var count = $scope.counts[i];
if (count.autoMerge) {
AdminCleanupService.mergeFeeds({

View File

@@ -30,6 +30,38 @@ module.directive('popup', function() {
};
});
/**
* entry tag handling
*/
module.directive('tags', function() {
return {
restrict : 'E',
scope : {
entry : '='
},
replace : true,
templateUrl : 'templates/_tags.html',
controller : ['$scope', 'EntryService', function($scope, EntryService) {
$scope.select2Options = {
'multiple' : true,
'simple_tags' : true,
'maximumInputLength' : 40,
tags : EntryService.tags
};
$scope.$watch('entry.tags', function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
var data = {
entryId : $scope.entry.id,
tags : newValue
};
EntryService.tag(data);
}
}, true);
}]
};
});
/**
* Reusable favicon component
*/
@@ -116,7 +148,8 @@ module.directive('category', [function() {
selectedId : '=',
showLabel : '=',
showChildren : '=',
unreadCount : '&'
unreadCount : '&',
tag : '='
},
restrict : 'E',
replace : true,
@@ -174,13 +207,14 @@ module.directive('category', [function() {
}
};
$scope.categoryClicked = function(id) {
$scope.categoryClicked = function(id, isTag) {
MobileService.toggleLeftMenu();
if ($scope.selectedType == 'category' && id == $scope.selectedId) {
var type = isTag ? 'tag' : 'category';
if ($scope.selectedType == type && id == $scope.selectedId) {
$scope.$emit('emitReload');
} else {
$state.transitionTo('feeds.view', {
_type : 'category',
_type : type,
_id : id
});
}
@@ -192,10 +226,16 @@ module.directive('category', [function() {
});
};
$scope.showCategoryDetails = function(category) {
$state.transitionTo('feeds.category_details', {
_id : category.id
});
$scope.showCategoryDetails = function(id, isTag) {
if (isTag) {
$state.transitionTo('feeds.tag_details', {
_id : id
});
} else {
$state.transitionTo('feeds.category_details', {
_id : id
});
}
};
$scope.toggleCategory = function(category, event) {

View File

@@ -54,6 +54,11 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
templateUrl : 'templates/feeds.category_details.html',
controller : 'CategoryDetailsCtrl'
});
$stateProvider.state('feeds.tag_details', {
url : '/details/tag/:_id',
templateUrl : 'templates/feeds.tag_details.html',
controller : 'TagDetailsCtrl'
});
$stateProvider.state('feeds.help', {
url : '/help',
templateUrl : 'templates/feeds.help.html',
@@ -105,7 +110,7 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
templateUrl : 'templates/admin.metrics.html',
controller : 'MetricsCtrl'
});
$urlRouterProvider.when('/', '/feeds/view/category/all');
$urlRouterProvider.when('/admin', '/admin/settings');
$urlRouterProvider.otherwise('/');

View File

@@ -126,7 +126,7 @@ module.factory('CategoryService', ['$resource', '$http', function($resource, $ht
callback(category, parentName);
var children = category.children;
if (children) {
for ( var c = 0; c < children.length; c++) {
for (var c = 0; c < children.length; c++) {
traverse(callback, children[c], category.name);
}
}
@@ -271,9 +271,29 @@ module.factory('EntryService', ['$resource', '$http', function($resource, $http)
params : {
_method : 'star'
}
},
tag : {
method : 'POST',
params : {
_method : 'tag'
}
}
};
var res = $resource('rest/entry/:_method', {}, actions);
res.tags = [];
var initTags = function() {
$http.get('rest/entry/tags').success(function(data) {
res.tags = [];
res.tags.push.apply(res.tags, data);
});
};
var oldTag = res.tag;
res.tag = function(data) {
oldTag(data, function() {
initTags();
});
};
initTags();
return res;
}]);
@@ -296,7 +316,6 @@ module.factory('AdminMetricsService', ['$resource', function($resource) {
return res;
}]);
module.factory('AdminCleanupService', ['$resource', function($resource) {
var actions = {
findDuplicateFeeds : {

View File

@@ -143,7 +143,7 @@
}
.full-screen #feed-accordion .entry-body-content {
max-width: 100%;
max-width: 100%;
}
#feed-accordion .entry-enclosure {
@@ -172,6 +172,24 @@
text-decoration: none;
}
#feed-accordion .tags-panel {
margin-left: 30px;
}
#feed-accordion .tags-panel .label{
margin-left: 5px;
}
.select2-container-multi .select2-choices .select2-search-field input {
padding: 2px
}
#feed-accordion .tag-input {
margin: 0 0 0 5px;
padding: 0;
width: 200px;
}
#feed-accordion .entry-buttons label {
margin-bottom: 0px;
font-size: 12px;

View File

@@ -30,7 +30,7 @@
}
.sidebar-nav-fixed:hover {
overflow-y: auto;
overflow-y: auto;
}
.full-screen .left-menu {
@@ -39,6 +39,7 @@
.css-treeview {
margin-top: 15px;
margin-bottom: 30px;
font-family: inherit;
font-size: 13px;
white-space: nowrap;

View File

@@ -2,6 +2,15 @@
cursor: pointer;
}
.nolink {
color: inherit;
}
.nolink:hover {
text-decoration: none;
color: inherit;
}
.block {
display: block;
}

View File

@@ -40,6 +40,9 @@
right: 35px;
margin-top: 22px;
}
#feed-accordion .tags-panel {
display: block;
}
body.left-menu-active .left-menu {
display: block !important;
width: 100%;
@@ -61,4 +64,4 @@
#uvTab {
display: none;
}
}
}

View File

@@ -1,14 +1,14 @@
<li>
<div class="pointer tree-item" ng-if="showLabel" ng-class="getClass(level - 1)" droppable="node">
<div class="dropdown pull-right">
<div class="pull-right" ng-click="showCategoryDetails(node)">
<div class="pull-right" ng-click="showCategoryDetails(node.id, node.isTag)">
<i class="icon-wrench config pointer"></i>
</div>
</div>
<div ng-click="categoryClicked(node.id)" ng-dblclick="showCategoryDetails(node)">
<div ng-click="categoryClicked(node.id, node.isTag)" ng-dblclick="showCategoryDetails(node.id, node.isTag)">
<span class="fldr">
<i ng-class="{'icon-caret-right': !node.expanded, 'icon-caret-down': node.expanded}" ng-click="toggleCategory(node, $event)" ng-show="showChildren"></i>
<i ng-class="{'icon-star' : node.id == 'starred', 'icon-inbox': node.id == 'all'}" ng-show="!showChildren"></i>
<i ng-class="{'icon-star' : node.id == 'starred', 'icon-inbox': node.id == 'all', 'icon-tag' : node.isTag}" ng-show="!showChildren"></i>
</span>
<span ng-class="{selected: (node.id == selectedId && selectedType == 'category')}">
<span ng-class="{unread: unreadCount({category:node})}" class="bidi-embed">

View File

@@ -0,0 +1,12 @@
<span>
<a ng-click="edit_mode=!edit_mode" class="nolink pointer">
<i class="icon-tags"></i>
${global.tags}
</a>
<span ng-if="!edit_mode">
<span class="label label-info" ng-repeat="tag in entry.tags">{{tag}}</span>
</span>
<span ng-if="edit_mode">
<input type="text" ui-select2="select2Options" ng-model="entry.tags" class="tag-input" />
</span>
</span>

View File

@@ -1,13 +1,15 @@
<div class="css-treeview" ng-controller="CategoryTreeCtrl">
<ul>
<category node="CategoryService.subscriptions" show-label="'${tree.all}'" show-children="false"
level="0" selected-type="selectedType" selected-id="selectedId"
unread-count="unreadCount(category)"> </category>
<category node="starred" show-label="'${tree.starred}'" show-children="false"
level="0" selected-type="selectedType" selected-id="selectedId"
unread-count="unreadCount(category)"> </category>
<category node="CategoryService.subscriptions" show-label="false" show-children="true"
level="0" selected-type="selectedType" selected-id="selectedId"
unread-count="unreadCount(category)"> </category>
<category node="CategoryService.subscriptions" show-label="'${tree.all}'" show-children="false" level="0" selected-type="selectedType"
selected-id="selectedId" unread-count="unreadCount(category)"> </category>
<category node="starred" show-label="'${tree.starred}'" show-children="false" level="0" selected-type="selectedType"
selected-id="selectedId" unread-count="unreadCount(category)"> </category>
<category node="CategoryService.subscriptions" show-label="false" show-children="true" level="0" selected-type="selectedType"
selected-id="selectedId" unread-count="unreadCount(category)"> </category>
<li ng-repeat="tag in tags | orderBy: 'name'">
<category node="tag" show-label="tag.name" show-children="false" level="0" selected-type="selectedType" selected-id="selectedId"
unread-count="unreadCount(category)"> </category>
</li>
</ul>
</div>

View File

@@ -0,0 +1,19 @@
<div>
<div class="page-header">
<h3>${details.tag_details}</h3>
</div>
<form name="form" class="form-horizontal" ng-submit="save()">
<div class="control-group">
<label class="control-label">${details.feed_url}</label>
<div class="controls horizontal-align">
<a ng-show="user.apiKey" href="{{'rest/category/entriesAsFeed?id=all&tag=' + tag + '&apiKey=' + user.apiKey}}" target="_blank">${global.link}</a>
<span ng-show="!user.apiKey">${details.generate_api_key_first}</span>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn" ng-click="back()">${global.cancel}</button>
</div>
</form>
</div>

View File

@@ -13,9 +13,9 @@
<span ng-show="keywords">${view.search_for} '{{keywords}}'</span>
</h3>
</div>
<div infinite-scroll="loadMoreEntries()" infinite-scroll-disabled="busy || !settingsService.settings.readingMode" infinite-scroll-distance="1" id="feed-accordion"
ng-class="{'expanded' : settingsService.settings.viewMode == 'expanded' }">
<div infinite-scroll="loadMoreEntries()" infinite-scroll-disabled="busy || !settingsService.settings.readingMode"
infinite-scroll-distance="1" id="feed-accordion" ng-class="{'expanded' : settingsService.settings.viewMode == 'expanded' }">
<div ng-show="message && errorCount > 10">${view.error_while_loading_feed} : {{message}}</div>
<div ng-repeat="entry in entries" class="entry entry-font-size-{{font_size}}" id="entry_{{entry.id}}"
ng-class="{unread: entry.read == false, current: current==entry, open: isOpen, closed: !isOpen }">
@@ -23,8 +23,7 @@
<a href="{{entry.url}}" target="_blank" class="entry-heading-link" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)">
<span class="feed-name">
<span class="star" ng-mouseup="star(entry, !entry.starred, $event)">
<i ng-class="{'icon-star icon-star-yellow': entry.starred, 'icon-star-empty': !entry.starred}"
class="pointer"></i>
<i ng-class="{'icon-star icon-star-yellow': entry.starred, 'icon-star-empty': !entry.starred}" class="pointer"></i>
</span>
<favicon url="entry.iconUrl" />
{{entry.feedName}}
@@ -36,15 +35,17 @@
<i class="icon-external-link"></i>
</a>
</div>
<div class="entry-body" ng-if="settingsService.settings.viewMode == 'expanded' || (isOpen && current == entry)" ng-mouseup="bodyClicked(entry, $event)"
ng-class="{rtl: entry.rtl}">
<div class="entry-body" ng-if="settingsService.settings.viewMode == 'expanded' || (isOpen && current == entry)"
ng-mouseup="bodyClicked(entry, $event)" ng-class="{rtl: entry.rtl}">
<div class="entry-header">
<div class="entry-title">
<a href="{{entry.url}}" target="_blank" ng-bind-html-unsafe="entry.title | highlight:keywords"></a>
<div class="entry-subtitle">
<span class="entry-source" ng-if="selectedType == 'category'">
<span class="entry-source-prefix">${view.entry_source}</span>
<a ng-click="goToFeed(entry.feedId)" class="pointer bidi-embed"><span>{{entry.feedName}}</span></a>
<a ng-click="goToFeed(entry.feedId)" class="pointer bidi-embed">
<span>{{entry.feedName}}</span>
</a>
</span>
<span class="entry-author" ng-if="entry.author">
<span class="entry-author-prefix">${view.entry_author}</span>
@@ -66,16 +67,13 @@
<div ng-if="entry.enclosureType && entry.enclosureType.indexOf('image') == 0">
<img ng-src="{{entry.enclosureUrl}}" />
</div>
<a href="{{entry.enclosureUrl}}" target="_blank" ng-if="entry.enclosureType" download>
${global.download}
</a>
<a href="{{entry.enclosureUrl}}" target="_blank" ng-if="entry.enclosureType" download> ${global.download} </a>
</div>
</div>
<div class="entry-buttons form-horizontal">
<span class="star" ng-mouseup="star(entry, !entry.starred, $event)">
<i ng-class="{'icon-star icon-star-yellow': entry.starred, 'icon-star-empty': !entry.starred}"
class="pointer"></i>
<i ng-class="{'icon-star icon-star-yellow': entry.starred, 'icon-star-empty': !entry.starred}" class="pointer"></i>
</span>
<label class="checkbox inline" ng-if="entry.markable">
<input type="checkbox" ng-checked="!entry.read" ng-click="mark(entry, !entry.read)" class="mousetrap"></input>
@@ -86,7 +84,8 @@
<a href="mailto:?subject={{entry.title|escape}}&body={{entry.url|escape}}" title="E-mail" popup>
<i class="icon-envelope"></i>
</a>
<a href="https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su={{entry.title|escape}}&body={{entry.url|escape}}" title="Gmail" popup>
<a href="https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su={{entry.title|escape}}&body={{entry.url|escape}}"
title="Gmail" popup>
<i class="icon-gmail"></i>
</a>
<a href="http://www.facebook.com/sharer.php?u=={{entry.url|escape}}" title="Facebook" popup>
@@ -108,7 +107,10 @@
<i class="icon-buffer"></i>
</a>
</span>
<span class="tags-panel">
<tags entry="entry"></tags>
</span>
<a ng-click="markUpTo(entry)" class="mark-up-to" title="${view.mark_up_to_here}">
<i class="icon-step-forward icon-rotate-90"></i>
</a>