summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBernhard Posselt <dev@bernhard-posselt.com>2014-05-23 00:44:27 +0200
committerBernhard Posselt <dev@bernhard-posselt.com>2014-05-23 00:44:27 +0200
commita0b6fad1ada52457c2c5022fcb7685c1fff16abb (patch)
treef3d89fcbb203dfa9b5ea2f92213567abf13bb813
parent7bcdc3d062996918c8cb59bf827a3db3229b8544 (diff)
add most of the settings except import
-rw-r--r--js/.jshintrc12
-rw-r--r--js/build/app.js205
-rw-r--r--js/controller/SettingsController.js41
-rw-r--r--js/directive/NewsForwardClick.js19
-rw-r--r--js/directive/NewsReadFile.js28
-rw-r--r--js/directive/NewsScroll.js2
-rw-r--r--js/karma.conf.js2
-rw-r--r--js/service/Publisher.js2
-rw-r--r--js/service/Settings.js13
-rw-r--r--js/tests/unit/controller/SettingsControllerSpec.js67
-rw-r--r--js/tests/unit/service/SettingsSpec.js22
-rw-r--r--templates/part.settings.php182
12 files changed, 429 insertions, 166 deletions
diff --git a/js/.jshintrc b/js/.jshintrc
index cd85aa807..1138c1c93 100644
--- a/js/.jshintrc
+++ b/js/.jshintrc
@@ -1,6 +1,6 @@
{
"esnext": true,
- "bitwise": false,
+ "bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
@@ -14,7 +14,7 @@
"nonew": true,
"plusplus": true,
"quotmark": "single",
- "undef": false,
+ "undef": true,
"unused": true,
"strict": true,
"maxparams": false,
@@ -33,11 +33,15 @@
"protractor": true,
"browser": true,
"By": true,
- "jasmine": true,
"it": true,
+ "afterEach": true,
+ "jasmine": true,
"describe": true,
"beforeEach": true,
"expect": true,
- "exports": true
+ "exports": true,
+ "reverse": true,
+ "items": true,
+ "enumerate": true
}
}
diff --git a/js/build/app.js b/js/build/app.js
index 95e376798..743d10fba 100644
--- a/js/build/app.js
+++ b/js/build/app.js
@@ -196,10 +196,42 @@ var $__build_47_app__ = function () {
'use strict';
console.log('here');
});
- app.controller('SettingsController', function () {
- 'use strict';
- console.log('here');
- });
+ app.controller('SettingsController', [
+ 'Settings',
+ '$route',
+ 'FeedResource',
+ function (Settings, $route, FeedResource) {
+ 'use strict';
+ var $__0 = this;
+ this.importing = false;
+ this.opmlImportError = false;
+ this.articleImportError = false;
+ var set = function (key, value) {
+ Settings.set(key, value);
+ if ([
+ 'showAll',
+ 'oldestFirst'
+ ].indexOf(key) >= 0) {
+ $route.reload();
+ }
+ };
+ this.toggleSetting = function (key) {
+ set(key, !$__0.getSetting(key));
+ };
+ this.getSetting = function (key) {
+ return Settings.get(key);
+ };
+ this.importOpml = function (content) {
+ console.log(content);
+ };
+ this.importArticles = function (content) {
+ console.log(content);
+ };
+ this.feedSize = function () {
+ return FeedResource.size();
+ };
+ }
+ ]);
app.factory('FeedResource', [
'Resource',
'$http',
@@ -514,41 +546,52 @@ var $__build_47_app__ = function () {
}, {});
return Resource;
});
- app.service('Settings', function () {
- 'use strict';
- var $__0 = this;
- this.settings = {};
- this.receive = function (data) {
- for (var $__3 = items(data)[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
- try {
- throw undefined;
- } catch (value) {
+ app.service('Settings', [
+ '$http',
+ 'BASE_URL',
+ function ($http, BASE_URL) {
+ 'use strict';
+ var $__0 = this;
+ this.settings = {};
+ this.receive = function (data) {
+ for (var $__3 = items(data)[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
try {
throw undefined;
- } catch (key) {
+ } catch (value) {
try {
throw undefined;
- } catch ($__8) {
- {
- $__8 = $traceurRuntime.assertObject($__4.value);
- key = $__8[0];
- value = $__8[1];
- }
- {
- $traceurRuntime.setProperty($__0.settings, key, value);
+ } catch (key) {
+ try {
+ throw undefined;
+ } catch ($__8) {
+ {
+ $__8 = $traceurRuntime.assertObject($__4.value);
+ key = $__8[0];
+ value = $__8[1];
+ }
+ {
+ $traceurRuntime.setProperty($__0.settings, key, value);
+ }
}
}
}
}
- }
- };
- this.get = function (key) {
- return $__0.settings[$traceurRuntime.toProperty(key)];
- };
- this.set = function (key, value) {
- $traceurRuntime.setProperty($__0.settings, key, value);
- };
- });
+ };
+ this.get = function (key) {
+ return $__0.settings[$traceurRuntime.toProperty(key)];
+ };
+ this.set = function (key, value) {
+ $traceurRuntime.setProperty($__0.settings, key, value);
+ var data = {};
+ $traceurRuntime.setProperty(data, key, value);
+ return $http({
+ url: BASE_URL + '/settings',
+ method: 'POST',
+ data: data
+ });
+ };
+ }
+ ]);
(function (window, document, $) {
'use strict';
var scrollArea = $('#app-content');
@@ -943,17 +986,38 @@ var $__build_47_app__ = function () {
writable: true
}), $__2;
};
+ app.directive('newsTriggerClick', function () {
+ 'use strict';
+ return function (scope, elm, attr) {
+ elm.click(function () {
+ $(attr.newsTriggerClick).trigger('click');
+ });
+ };
+ });
+ app.directive('newsReadFile', function () {
+ 'use strict';
+ return function (scope, elm, attr) {
+ var file = elm[0].files[0];
+ var reader = new FileReader();
+ reader.onload = function (event) {
+ elm[0].value = 0;
+ scope.$fileContent = event.target.result;
+ scope.$apply(attr.newsReadFile);
+ };
+ reader.reasAsText(file);
+ };
+ });
app.directive('newsScroll', [
'$timeout',
function ($timeout) {
'use strict';
- var autoPage = function (enabled, limit, items, callback) {
+ var autoPage = function (enabled, limit, callback, elem) {
if (enabled) {
try {
throw undefined;
} catch (counter) {
counter = 0;
- for (var $__3 = reverse(items.find('.feed_item'))[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
+ for (var $__3 = reverse(elem.find('.feed_item'))[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
try {
throw undefined;
} catch (item) {
@@ -974,33 +1038,28 @@ var $__build_47_app__ = function () {
}
}
};
- var markRead = function (enabled, items, callback) {
+ var markRead = function (enabled, callback, elem) {
if (enabled) {
try {
throw undefined;
- } catch (unreadItems) {
- try {
- throw undefined;
- } catch (ids) {
- ids = [];
- unreadItems = items.find('.feed_item:not(.read)');
- for (var $__3 = unreadItems[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
- try {
- throw undefined;
- } catch (item) {
- item = $__4.value;
- {
- item = $(item);
- if (item.position().top <= -50) {
- ids.push(parseInt($(item).data('id'), 10));
- } else {
- break;
- }
+ } catch (ids) {
+ ids = [];
+ for (var $__3 = elem.find('.feed_item:not(.read)')[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
+ try {
+ throw undefined;
+ } catch (item) {
+ item = $__4.value;
+ {
+ item = $(item);
+ if (item.position().top <= -50) {
+ ids.push(parseInt(item.data('id'), 10));
+ } else {
+ break;
}
}
}
- callback(ids);
}
+ callback(ids);
}
}
};
@@ -1009,38 +1068,32 @@ var $__build_47_app__ = function () {
scope: {
'newsScrollAutoPage': '&',
'newsScrollMarkRead': '&',
- 'newsScrollDisabledMarkRead': '=',
- 'newsScrollDisabledAutoPage': '=',
+ 'newsScrollEnabledMarkRead': '=',
+ 'newsScrollEnableAutoPage': '=',
'newsScrollMarkReadTimeout': '@',
'newsScrollTimeout': '@',
- 'newsScrollAutoPageWhenLeft': '@',
- 'newsScrollItemsSelector': '@'
+ 'newsScrollAutoPageWhenLeft': '@'
},
link: function (scope, elem) {
- var scrolling = false;
+ var allowScroll = true;
scope.newsScrollTimeout = scope.newsScrollTimeout || 1;
scope.newsScrollMarkReadTimeout = scope.newsScrollMarkReadTimeout || 1;
scope.newsScrollAutoPageWhenLeft = scope.newsScrollAutoPageWhenLeft || 50;
- scope.newsScrollItemsSelector = scope.newsScrollItemsSelector || '.items';
- elem.on('scroll', function () {
- if (!scrolling) {
- try {
- throw undefined;
- } catch (items) {
- scrolling = true;
- $timeout(function () {
- scrolling = false;
- }, scope.newsScrollTimeout * 1000);
- items = $(scope.newsScrollItemsSelector);
- autoPage(!scope.newsScrollDisabledAutoPage, scope.newsScrollAutoPageWhenLeft, items, scope.newsScrollAutoPage);
- $timeout(function () {
- markRead(!scope.newsScrollDisabledMarkRead, items, scope.newsScrollMarkRead);
- }, scope.newsScrollMarkReadTimeout * 1000);
- }
+ var scrollHandler = function () {
+ if (allowScroll) {
+ allowScroll = false;
+ $timeout(function () {
+ allowScroll = true;
+ }, scope.newsScrollTimeout * 1000);
+ autoPage(scope.newsScrollEnableAutoPage, scope.newsScrollAutoPageWhenLeft, scope.newsScrollAutoPage, elem);
+ $timeout(function () {
+ markRead(scope.newsScrollEnabledMarkRead, scope.newsScrollMarkRead, elem);
+ }, scope.newsScrollMarkReadTimeout * 1000);
}
- });
+ };
+ elem.on('scroll', scrollHandler);
scope.$on('$destroy', function () {
- elem.off('scroll');
+ elem.off('scroll', scrollHandler);
});
}
};
diff --git a/js/controller/SettingsController.js b/js/controller/SettingsController.js
index 321b6dff9..ae0e1ee52 100644
--- a/js/controller/SettingsController.js
+++ b/js/controller/SettingsController.js
@@ -7,8 +7,45 @@
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright Bernhard Posselt 2014
*/
-app.controller('SettingsController', function () {
+app.controller('SettingsController', function (Settings, $route, FeedResource) {
'use strict';
- console.log('here');
+ this.importing = false;
+ this.opmlImportError = false;
+ this.articleImportError = false;
+
+ let set = (key, value) => {
+ Settings.set(key, value);
+
+ if (['showAll', 'oldestFirst'].indexOf(key) >= 0) {
+ $route.reload();
+ }
+ };
+
+
+ this.toggleSetting = (key) => {
+ set(key, !this.getSetting(key));
+ };
+
+
+ this.getSetting = (key) => {
+ return Settings.get(key);
+ };
+
+
+ this.importOpml = (content) => {
+ console.log(content);
+ };
+
+
+ this.importArticles = (content) => {
+ console.log(content);
+ };
+
+
+ this.feedSize = () => {
+ return FeedResource.size();
+ };
+
+
}); \ No newline at end of file
diff --git a/js/directive/NewsForwardClick.js b/js/directive/NewsForwardClick.js
new file mode 100644
index 000000000..d5c43e442
--- /dev/null
+++ b/js/directive/NewsForwardClick.js
@@ -0,0 +1,19 @@
+/**
+ * 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 2014
+ */
+app.directive('newsTriggerClick', () => {
+ 'use strict';
+
+ return (scope, elm, attr) => {
+ elm.click(() => {
+ $(attr.newsTriggerClick).trigger('click');
+ });
+ };
+
+}); \ No newline at end of file
diff --git a/js/directive/NewsReadFile.js b/js/directive/NewsReadFile.js
new file mode 100644
index 000000000..d3ed5b80e
--- /dev/null
+++ b/js/directive/NewsReadFile.js
@@ -0,0 +1,28 @@
+/**
+ * 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 2014
+ */
+app.directive('newsReadFile', () => {
+ 'use strict';
+
+ return (scope, elm, attr) => {
+
+ let file = elm[0].files[0];
+ let reader = new FileReader();
+
+ reader.onload = (event) => {
+ elm[0].value = 0;
+ scope.$fileContent = event.target.result;
+ scope.$apply(attr.newsReadFile); // FIXME: is there a more flexible
+ // solution where we dont have to
+ // bind the file to scope?
+ };
+
+ reader.reasAsText(file);
+ };
+}); \ No newline at end of file
diff --git a/js/directive/NewsScroll.js b/js/directive/NewsScroll.js
index 043235bbf..126c79b11 100644
--- a/js/directive/NewsScroll.js
+++ b/js/directive/NewsScroll.js
@@ -99,7 +99,7 @@ app.directive('newsScroll', ($timeout) => {
}, scope.newsScrollMarkReadTimeout*1000);
}
- });
+ };
elem.on('scroll', scrollHandler);
diff --git a/js/karma.conf.js b/js/karma.conf.js
index bc00fa12a..02ca523f1 100644
--- a/js/karma.conf.js
+++ b/js/karma.conf.js
@@ -29,7 +29,7 @@ module.exports = function (config) {
options: {
blockBinding: true,
experimental: true,
- sourceMap: false,
+ sourceMap: true,
modules: 'inline'
}
},
diff --git a/js/service/Publisher.js b/js/service/Publisher.js
index d4efa4a80..dd420f629 100644
--- a/js/service/Publisher.js
+++ b/js/service/Publisher.js
@@ -7,6 +7,8 @@
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright Bernhard Posselt 2014
*/
+
+/*jshint undef:false*/
app.service('Publisher', function () {
'use strict';
diff --git a/js/service/Settings.js b/js/service/Settings.js
index 1bc716b07..e8189fcd5 100644
--- a/js/service/Settings.js
+++ b/js/service/Settings.js
@@ -7,7 +7,9 @@
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright Bernhard Posselt 2014
*/
-app.service('Settings', function () {
+
+ /*jshint unused:false*/
+app.service('Settings', function ($http, BASE_URL) {
'use strict';
this.settings = {};
@@ -24,6 +26,15 @@ app.service('Settings', function () {
this.set = (key, value) => {
this.settings[key] = value;
+
+ let data = {};
+ data[key] = value;
+
+ return $http({
+ url: `${BASE_URL}/settings`,
+ method: 'POST',
+ data: data
+ });
};
}); \ No newline at end of file
diff --git a/js/tests/unit/controller/SettingsControllerSpec.js b/js/tests/unit/controller/SettingsControllerSpec.js
new file mode 100644
index 000000000..c438fbcd0
--- /dev/null
+++ b/js/tests/unit/controller/SettingsControllerSpec.js
@@ -0,0 +1,67 @@
+/**
+ * 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 2014
+ */
+describe('SettingsController', () => {
+ 'use strict';
+
+ beforeEach(module('News', ($provide) => {
+ $provide.value('BASE_URL', 'base');
+ }));
+
+
+ it('should set values', inject(($controller) => {
+ let Settings = {
+ set: jasmine.createSpy('Settings'),
+ get: key => key
+ };
+
+ let ctrl = $controller('SettingsController', {
+ Settings: Settings
+ });
+
+ ctrl.toggleSetting(3);
+
+ expect(Settings.set).toHaveBeenCalledWith(3, false);
+ }));
+
+
+ it('should reload page if set needed', inject(($controller) => {
+ let settings = {
+ set: jasmine.createSpy('Settings'),
+ get: key => key
+ };
+
+ let route = {
+ reload: jasmine.createSpy('Route')
+ };
+
+ let ctrl = $controller('SettingsController', {
+ Settings: settings,
+ $route: route
+ });
+
+ ctrl.toggleSetting('showAll');
+ ctrl.toggleSetting('oldestFirst');
+
+ expect(settings.set).toHaveBeenCalledWith('showAll', false);
+ expect(route.reload).toHaveBeenCalled();
+ expect(route.reload.callCount).toBe(2);
+ }));
+
+
+ it('should return feed size', inject(($controller, FeedResource) => {
+ FeedResource.add({url: 'hi'});
+
+ let ctrl = $controller('SettingsController', {
+ FeedResource: FeedResource
+ });
+
+ expect(ctrl.feedSize()).toBe(1);
+ }));
+}); \ No newline at end of file
diff --git a/js/tests/unit/service/SettingsSpec.js b/js/tests/unit/service/SettingsSpec.js
index b76251e3a..06bb365b6 100644
--- a/js/tests/unit/service/SettingsSpec.js
+++ b/js/tests/unit/service/SettingsSpec.js
@@ -10,7 +10,16 @@
describe('Settings', () => {
'use strict';
- beforeEach(module('News'));
+ let http;
+
+ beforeEach(module('News', ($provide) => {
+ $provide.value('BASE_URL', 'base');
+ }));
+
+ beforeEach(inject(($httpBackend) => {
+ http = $httpBackend;
+ }));
+
it('should receive default settings', inject((Settings) => {
Settings.receive({
@@ -22,9 +31,20 @@ describe('Settings', () => {
it('should set values', inject((Settings) => {
+ http.expectPOST('base/settings', {showAll: true}).respond(200, {});
+
Settings.set('showAll', true);
+ http.flush();
+
expect(Settings.get('showAll')).toBe(true);
}));
+
+ afterEach(() => {
+ http.verifyNoOutstandingExpectation();
+ http.verifyNoOutstandingRequest();
+ });
+
+
}); \ No newline at end of file
diff --git a/templates/part.settings.php b/templates/part.settings.php
index b1d5383ea..b8f22cda2 100644
--- a/templates/part.settings.php
+++ b/templates/part.settings.php
@@ -1,84 +1,106 @@
<div id="app-settings-header">
-<button name="app settings"
- class="settings-button"
- oc-click-slide-toggle="{
- selector: '#app-settings-content',
- hideOnFocusLost: true,
- cssClass: 'opened'
- }"></button>
+<button name="app settings"
+ class="settings-button"
+ oc-click-slide-toggle="{
+ selector: '#app-settings-content',
+ hideOnFocusLost: true,
+ cssClass: 'opened'
+ }"></button>
</div>
-<div id="app-settings-content">
- <fieldset class="personalblock">
- <legend><strong><?php p($l->t('Settings')); ?></strong></legend>
- <p ng-click="getSetting('compact')">
- <input type="checkbox" ng-checked="getSetting('compact')"> <?php p($l->t('Use compact view')); ?>
- </p>
- <p ng-click="getSetting('oldestFirst')">
- <input type="checkbox" ng-checked="getSetting('oldestFirst')"> <?php p($l->t('Order by oldest first')); ?>
- </p>
- <p ng-click="getSetting('preventReadOnScroll')">
- <input type="checkbox" ng-checked="getSetting('preventReadOnScroll')"> <?php p($l->t('Do not as mark read when scrolling')); ?>
- </p>
- <legend><strong><?php p($l->t('Subscriptions (OPML)')); ?></strong></legend>
-
- <input type="file" id="opml-upload" name="import" accept="text/x-opml, text/xml"
- oc-read-file="import($fileContent)"/>
- <button title="<?php p($l->t('Import')); ?>"
- class="upload-icon svg"
- oc-forward-click="{selector:'#opml-upload'}">
- <?php p($l->t('Import')); ?>
- </button>
-
-
- <a title="<?php p($l->t('Export')); ?>" class="button download-icon svg"
- href="<?php p(\OCP\Util::linkToRoute('news.export.opml')); ?>"
- target="_blank"
- ng-show="feedBusinessLayer.getNumberOfFeeds() > 0">
- <?php p($l->t('Export')); ?>
- </a>
- <button
- class="download-icon svg"
- title="<?php p($l->t('Export')); ?>"
- ng-hide="feedBusinessLayer.getNumberOfFeeds() > 0" disabled>
- <?php p($l->t('Export')); ?>
- </button>
-
- <p class="error" ng-show="error">
- <?php p($l->t('Error when importing: file does not contain valid OPML')); ?>
- </p>
-
- </fieldset>
-
- <fieldset class="personalblock">
- <legend><strong><?php p($l->t('Unread/Starred Articles')); ?></strong></legend>
- <input type="file" id="google-upload" name="importgoogle"
- accept="application/json"
- oc-read-file="importArticles($fileContent)"/>
- <button title="<?php p($l->t('Import')); ?>"
- class="upload-icon svg"
- ng-class="{loading: importing}"
- ng-disabled="importing"
- oc-forward-click="{selector:'#google-upload'}">
- <?php p($l->t('Import')); ?>
- </button>
-
- <a title="<?php p($l->t('Export')); ?>" class="button download-icon svg"
- href="<?php p(\OCP\Util::linkToRoute('news.export.articles')); ?>"
- target="_blank"
- ng-show="feedBusinessLayer.getNumberOfFeeds() > 0">
- <?php p($l->t('Export')); ?>
- </a>
- <button
- class="download-icon svg"
- title="<?php p($l->t('Export')); ?>"
- ng-hide="feedBusinessLayer.getNumberOfFeeds() > 0" disabled>
- <?php p($l->t('Export')); ?>
- </button>
-
- <p class="error" ng-show="jsonError">
- <?php p($l->t('Error when importing: file does not contain valid JSON')); ?>
- </p>
-
- </fieldset>
+<div id="app-settings-content" ng-controller="SettingsController as Settings">
+ <h3><?php p($l->t('Settings')); ?></h3>
+
+ <p ng-click="Settings.toggleSetting('compact')">
+ <input type="checkbox" ng-checked="Settings.getSetting('compact')">
+ <?php p($l->t('Use compact view')); ?>
+ </p>
+
+ <p ng-click="Settings.toggleSetting('showAll')">
+ <input type="checkbox" ng-checked="Settings.getSetting('showAll')">
+ <?php p($l->t('Show unread articles')); ?>
+ </p>
+
+ <p ng-click="Settings.toggleSetting('oldestFirst')">
+ <input type="checkbox" ng-checked="Settings.getSetting('oldestFirst')">
+ <?php p($l->t('Order by oldest first')); ?>
+ </p>
+
+ <p ng-click="Settings.toggleSetting('preventReadOnScroll')">
+ <input type="checkbox" ng-checked="Settings.getSetting('preventReadOnScroll')">
+ <?php p($l->t('Do not as mark read when scrolling')); ?>
+ </p>
+
+
+ <h3><?php p($l->t('Subscriptions (OPML)')); ?></h3>
+
+ <input type="file"
+ id="opml-upload"
+ name="import"
+ accept="text/x-opml, text/xml"
+ news-read-file="Settings.importOpml($fileContent)"/>
+
+ <button title="<?php p($l->t('Import')); ?>"
+ class="upload-icon svg"
+ news-trigger-click="#opml-upload">
+ <?php p($l->t('Import')); ?>
+ </button>
+
+
+ <a title="<?php p($l->t('Export')); ?>"
+ class="button download-icon svg"
+ href="<?php p(\OCP\Util::linkToRoute('news.export.opml')); ?>"
+ target="_blank"
+ ng-show="feedSize() > 0">
+ <?php p($l->t('Export')); ?>
+ </a>
+
+ <button
+ class="download-icon svg"
+ title="<?php p($l->t('Export')); ?>"
+ ng-hide="feedSize() > 0"
+ disabled>
+ <?php p($l->t('Export')); ?>
+ </button>
+
+ <p class="error" ng-show="Settings.opmlImportError">
+ <?php p($l->t('Error when importing: file does not contain valid OPML')); ?>
+ &l