Compare commits

...

155 Commits
2.2.0 ... 2.5.0

Author SHA1 Message Date
Athou
c1dac2e064 2.5.0 release 2020-09-02 21:20:20 +02:00
Jérémie Panzer
f707993188 fix travis build 2020-08-15 08:33:12 +02:00
Athou
ea612d9d53 add missing validCheckSum 2020-05-18 09:42:08 +02:00
Jeremie Panzer
b44e737448 fix liquibase script when running on an empty postgresql database 2020-03-12 13:45:06 +01:00
Jeremie Panzer
bb429afd95 ignore swagger in eclipse 2020-03-12 12:54:20 +01:00
dependabot[bot]
475a8f8a28 Bump bower from 1.4.1 to 1.8.8 (#920)
Bumps [bower](https://github.com/bower/bower) from 1.4.1 to 1.8.8.
- [Release notes](https://github.com/bower/bower/releases)
- [Changelog](https://github.com/bower/bower/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bower/bower/compare/v1.4.1...v1.8.8)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-30 09:01:55 +02:00
Athou
c7ba5ca894 make swagger aware that dates are serialized as longs 2019-05-03 22:01:18 +02:00
Athou
3023f0a7cc fix build 2019-05-03 18:57:02 +02:00
Athou
ddaefbc952 deduplicate method names across all the api (swagger requires unique api operations) 2019-05-03 18:40:50 +02:00
Jeremie Panzer
0b3a0fb3ed add missing required 2019-05-02 13:42:49 +02:00
Athou
7f40a430fd hide securitycheck user from swagger documentation 2019-05-01 23:33:55 +02:00
Athou
05f5d3b25c add missing "required" flags 2019-05-01 20:31:48 +02:00
Athou
c3ca0b18b3 value field of annotation is actually the name of the class 2019-05-01 19:57:52 +02:00
Athou
696e0b1fa7 maven config for swagger plugin changed 2019-05-01 19:56:48 +02:00
Athou
201f7dbd3e restore cookieMaxAge behavior 2019-04-23 01:14:26 +02:00
Athou
0bfd3e906c stop hibernate HHH90000015 spam 2019-04-22 20:55:39 +02:00
Jérémie Panzer
71ac2bfc45 support for Java9+ (#906)
* initial java9+ support

* restore session management, updated for jetty 9.4

* Session actually implements EntityManager

* reusable method for setting the timeout
2019-04-22 20:30:06 +02:00
Athou
5370db7c5e rename for clarity 2019-03-17 07:05:29 +01:00
Athou
bcc30e40ba Merge branch 'ildar-shaimordanov-master' 2019-03-17 06:46:51 +01:00
Athou
2f70f654f7 extensible mechanism for feed url building 2019-03-17 06:44:09 +01:00
ildar-shaimordanov
b64115dcbd improve youtube feed URL getter 2019-03-12 05:52:00 +04:00
ildar-shaimordanov
c9c71d8582 workaround for youtube channels 2019-03-12 02:13:41 +04:00
Jérémie Panzer
689bc19296 Merge pull request #896 from nelsonblaha/patch-1
English correction for configuration comment
2019-02-10 20:33:12 +01:00
Ben Nelson
27498ab649 English correction for configuration comment 2019-02-10 11:35:46 -06:00
Athou
678a11f998 blur event seems to trigger twice for some reason, make sure we don't fetch the feed twice (#825) 2018-07-31 15:51:48 +02:00
Athou
e9ef98716f configurable user agent string (#825) 2018-07-31 15:21:45 +02:00
Athou
b3ce43eaf7 faster replace for large feeds (#881) 2018-07-11 17:13:38 +02:00
Jérémie Panzer
72083b7e87 Merge pull request #880 from Athou/snyk-fix-0oenl9
[Snyk] Fix for 6 vulnerable dependencies
2018-06-23 13:49:14 +02:00
snyk-bot
0cc94c2033 fix: pom.xml to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JAVA-COMH2DATABASE-31685
- https://snyk.io/vuln/SNYK-JAVA-MYSQL-31399
- https://snyk.io/vuln/SNYK-JAVA-MYSQL-31449
- https://snyk.io/vuln/SNYK-JAVA-MYSQL-31580
- https://snyk.io/vuln/SNYK-JAVA-ORGAPACHEHTTPCOMPONENTS-31517
- https://snyk.io/vuln/SNYK-JAVA-ORGJSOUP-31218
2018-06-14 05:55:43 +00:00
Jérémie Panzer
1d6296b400 Merge pull request #870 from asny23/update-japanese-translation
improve japanese translation
2018-04-12 12:18:50 +02:00
Unknown
7ad5da2a9e translate and improve japanese in ja.js 2018-04-11 23:27:56 +09:00
Athou
a7665a9994 add current year to filtering context 2018-02-25 13:38:37 +01:00
Jérémie Panzer
a4cd3f26e8 Update README.md 2018-02-22 18:21:48 +01:00
Athou
fcdb33b64b utility for testing feeds 2018-02-06 15:17:37 +01:00
Athou
7fd6119bcf add author to rss generated feeds (#858) 2017-12-22 18:50:32 +01:00
Jérémie Panzer
b4d4b2473c Merge pull request #854 from Busimus/patch-7
Updated Russian translation
2017-11-08 15:34:32 +01:00
Alexander Bus
91f715c3c3 Updated Russian translation 2017-10-28 01:16:57 +07:00
Athou
ea5fccfe5f fix build 2017-10-12 12:10:04 +02:00
Athou
86835eec73 request may not be a HttpUriRequest when using a proxy (#850) 2017-10-12 10:21:11 +02:00
Jérémie Panzer
2bccee2333 Merge pull request #849 from ema-pe/update-italian-translation
Update italian translation
2017-09-27 15:00:56 +02:00
Emanuele Petriglia
2d01b0d714 Update italian translation 2017-09-27 14:09:37 +02:00
Jérémie Panzer
44bf37b05a Update bower.json
fix alignment
2017-08-18 16:10:51 +02:00
Jérémie Panzer
cf617f0a64 Merge pull request #847 from sometoby/missing-shortcut-help
Add missing shortcut help for 'r'
2017-08-18 14:27:13 +02:00
Jérémie Panzer
eeeaffd883 Merge pull request #846 from sometoby/tinycon-unread-badge
Use tinycon to display unread article count
2017-08-18 14:25:07 +02:00
Tobias Umbach
d178302d34 Add comment so shortcut code is easier to find 2017-08-18 08:40:05 +02:00
Tobias Umbach
83a5364903 Add missing shortcut help for 'r', refresh 2017-08-18 08:39:49 +02:00
Tobias Umbach
aef76db664 Update CHANGELOG 2017-08-18 08:21:36 +02:00
Tobias Umbach
c3b3240191 Use tinycon to display unread article count 2017-08-14 11:15:43 +02:00
Athou
f381974955 prepare for next version 2017-08-01 13:55:11 +02:00
Athou
bd16dd98c4 update readme 2017-08-01 13:55:04 +02:00
Athou
2fca6132a0 2.4.0 release 2017-08-01 13:50:40 +02:00
Jérémie Panzer
137eba33c9 prerequisites is for maven plugins 2017-08-01 13:44:23 +02:00
Jérémie Panzer
143699c0a4 colors! 2017-08-01 13:44:02 +02:00
Jérémie Panzer
0485403fff Merge pull request #844 from TyBrown/issue/842
Set iOS Meta Tags to use Full Screen 'default' status bar
2017-07-11 18:45:56 +02:00
Ty Brown
489fcb9666 Set iOS Meta Tags to use Full Screen 'default' status bar 2017-07-11 11:29:20 -05:00
Jérémie Panzer
7cc3b84ebc Merge pull request #843 from TyBrown/graphite_metrics
Add feature to emit Graphite metrics based on configuration
2017-07-11 16:35:49 +02:00
Ty Brown
cb254f87d4 Add feature to emit Graphite metrics based on configuration 2017-07-05 21:56:00 -05:00
Athou
d4db98fd64 fix api key generation 2017-05-09 08:59:31 +02:00
Athou
d14a6d8311 Merge pull request #839 from glujan/master
Update pl.js
2017-05-08 12:57:45 +02:00
Grzegorz Janik
286c115167 Update pl.js 2017-05-08 12:48:10 +02:00
Athou
6038b9e052 Update README.md 2017-05-08 12:16:37 +02:00
Athou
552082a36a Merge pull request #837 from Drey91/patch-1
Update es.js
2017-03-07 09:39:07 +01:00
Drey91
5cea92d96d Update es.js
Corrección de algunos errores en la traducción en Español. [Correction of some errors in the Spanish translation]
2017-03-07 09:33:24 +01:00
Athou
02b7b89b94 Merge pull request #829 from ebraminio/patch-5
Use Safari transparent tab bar as loading progress indicator
2016-11-24 11:58:54 +01:00
ebraminio
93697cf1f5 Use Safari transparent tab bar as loading progress indicator 2016-11-24 14:21:09 +03:30
Athou
8daaee28c3 fix version number 2016-11-17 20:35:50 +01:00
Athou
c32f608ec5 upgrade postgresql jdbc driver (fix #827) 2016-11-17 20:30:34 +01:00
Athou
7b09029c5b Merge pull request #823 from ebraminio/patch-5
Add rel="noreferrer" to more places
2016-10-07 12:14:28 +02:00
Ebrahim Byagowi
6e1c414c84 Add rel="noreferrer" to more places 2016-10-07 01:13:08 +03:30
Athou
e57976be99 fix findbugs warning 2016-10-05 09:01:34 +02:00
Athou
a37e6a3f4c Merge pull request #821 from canoine/master
Update fr.js
2016-09-30 10:18:06 +02:00
canoine
2dbe4064b2 Update fr.js 2016-09-28 03:00:44 +02:00
Athou
2b0c0d467a formatting 2016-09-23 08:55:09 +02:00
Athou
40fa4516df use icons 2016-09-23 08:54:46 +02:00
Athou
5201c0cd14 Merge branch 'Hubcapp-master' 2016-09-23 08:54:18 +02:00
tyler
61039dcd7e remove some whitespace, put tuples assignment back on one line 2016-09-05 05:09:57 -04:00
tyler
039ff4ee41 decided to do ctrl-f for " order " and found this piece. Don't know what bug this will fix but probably best to update to reflect additional sorting options. Sorry for ternary operator abuse 2016-09-05 04:56:30 -04:00
tyler
b40349805f switch abc & zyx 2016-09-05 00:41:17 -04:00
tyler
d709d119ac change button for Alphabetic sort to have different "icon".
also ABC and ZYX sorting were switched.
2016-09-04 23:57:12 -04:00
tyler
8d2b6bdc12 Add translation placeholders for new hover text on alphabetic sort, make whatever button you sorted by "active" 2016-09-04 22:39:03 -04:00
tyler
ff78af2d56 fix lots of bugs with alphabetic sort by properly assigning FeedEntry.FeedEntryContent.title instead of trying to make FeedEntryStatus know about titles. 2016-09-04 21:37:04 -04:00
tyler
ada53dba3b Add Alphabetical sorting
First "working" version of Alphabetical sorting in Commafeed. Needs better front end interface, translations, and probably the API is buggy surrounding "order" now b/c there is probably some code still that assumes there are only two possible ways to sort (date asc, date desc).
2016-09-02 03:39:16 -04:00
Hubcapp
ba2f6c0f66 Merge branch 'master' of https://github.com/Athou/commafeed 2016-09-02 02:33:20 -05:00
Athou
268869345c reduce npm verbosity (fixes #811) 2016-08-23 12:11:42 +02:00
Athou
4b556bd3a9 fix password change (#805) 2016-08-22 15:05:15 +02:00
Athou
6f10d35a4c no casting of sessionfactory necessary this way 2016-07-28 11:28:45 +02:00
Athou
33167fcdce Merge pull request #803 from clapd10/patch-1
Update ko.js
2016-07-08 17:45:03 +02:00
clapd10
e9c85b0e77 Update ko.js
improved translation
2016-07-09 00:42:29 +09:00
Athou
e521254600 add indonesian 2016-06-17 14:03:57 +02:00
Athou
a773d98400 Merge pull request #802 from antosamalona/indonesian
add a new file for translating language to Indonesian
2016-06-17 14:03:04 +02:00
AnTo 'SamalonA
ae066d3cd9 update it 2016-06-17 01:51:39 +08:00
AnTo 'SamalonA
b5726fc0f3 add a new file for translating language to Indonesian 2016-06-17 01:35:39 +08:00
Athou
4a056a0d27 Merge pull request #788 from jart/patch-1
Upgrade Apache Commons Collections to v4.1
2016-04-11 10:52:35 +02:00
Justine Tunney
7817431bce Upgrade Apache Commons Collections to v4.1
Version 4.0 has a CVSS 10.0 vulnerability. That's the worst kind of
vulnerability that exists. By merely existing on the classpath, this
library causes the Java serialization parser for the entire JVM process
to go from being a state machine to a turing machine. A turing machine
with an exec() function!

https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-8103
https://commons.apache.org/proper/commons-collections/security-reports.html
http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
2016-04-10 20:56:38 -07:00
Athou
c02d2745c3 fix dev environment 2016-04-07 12:59:23 +02:00
Athou
ee610ec800 build time decreased a lot with npm upgrade 2016-04-07 12:59:23 +02:00
Athou
6c0d585fef Merge pull request #787 from ebraminio/master
Add rel="noreferrer" to resolve window.opener issue
2016-03-22 17:02:51 +01:00
Ebrahim Byagowi
29417005b0 Add rel="noreferrer" to resolve window.opener issue
https://mathiasbynens.github.io/rel-noopener
2016-03-22 20:09:12 +04:30
Athou
cf87fd8340 fix facebook sharing 2016-03-12 21:39:30 +01:00
Athou
f1b85b0dde readme update 2016-03-02 11:22:55 +01:00
Athou
abef73d384 version bump 2016-03-02 11:22:27 +01:00
Athou
535f947f88 2.3.0 release 2016-03-02 11:21:10 +01:00
Athou
f27e243cc4 readme update 2016-03-02 11:20:57 +01:00
Athou
6a699ed5f1 Merge pull request #782 from badarg/improve-spanish-translation
Improved spanish translation.
2016-02-15 12:34:02 +01:00
badarg
9c1f5efab5 Improved spanish translation.
Fixed grammar, orthography, sundry mistakes, and added missing strings.
2016-02-14 20:36:28 +01:00
Athou
6b7ce56f6b Merge pull request #779 from canoine/patch-1
Update fr.js
2016-01-20 06:31:20 +01:00
canoine
b76ee4a2d0 Update fr.js
Translation of the sentences that were still in english.
Some stylistic, grammatical and orthographic corrections.
2016-01-20 01:55:09 +01:00
Athou
b444a74a44 change user agent so that self hosted instances don't point to commafeed.com 2016-01-18 10:01:05 +01:00
Athou
d43820cc82 change npm install log level to info 2016-01-08 16:12:15 +01:00
Athou
e74e8fe1c2 Merge pull request #769 from matrixik/patch-1
[add] css ids for toolbar
2015-11-30 10:09:33 +01:00
Dobrosław Żybort
9eb6e8ec27 [add] css ids for toolbar 2015-11-30 09:48:20 +01:00
Athou
fae94d3696 jdbc drivers upgrade 2015-11-25 16:50:42 +01:00
Athou
68e5ed64c9 dropwizard upgrade 2015-11-25 16:49:18 +01:00
Athou
f912d3b8bd swagger upgrade 2015-11-25 16:44:28 +01:00
Athou
fc03d2ee91 Merge pull request #768 from giucal/master
Little contribution to the Italian translation
2015-11-24 06:54:19 +01:00
Giuseppe Calabrese
523b2b8db4 Fixed a couple typos.
I felt free to change the `filtering_expression_help` and make it less ambiguous.
2015-11-23 22:30:11 +01:00
Giuseppe Calabrese
d547e9b6d7 Fixed some translation. 2015-11-23 21:22:49 +01:00
Athou
71efc9f854 fix #766 2015-11-07 23:45:04 +01:00
Athou
4f289f7467 Merge pull request #764 from JmsBnz/patch-2
Update it.js
2015-11-03 11:31:13 +01:00
JmsBnz
02ef8bee71 Update it.js
Some translation fixes
2015-10-31 11:38:21 +01:00
Athou
ff5c1b00d7 Merge pull request #762 from ebraminio/patch-4
Add magnet links support
2015-10-21 15:44:46 +02:00
ebraminio
30264be311 Add magnet links support 2015-10-19 23:45:53 +03:30
Athou
8ea44ab8c7 Merge pull request #761 from ebraminio/patch-3
Use correct characters for ru lang
2015-10-10 06:28:30 +02:00
ebraminio
1b8ff7ca61 Use correct characters for ru lang
It is similar but works better on some font setups
2015-10-10 00:45:50 +03:30
Athou
f00a066c22 languages should start with an uppercase (fix #759) 2015-09-01 16:58:18 +02:00
Athou
859cf468aa add openjdk ppa on ubuntu lts (fix #756) 2015-08-27 12:45:37 +02:00
Athou
5b486a917b dropwizard upgrade 2015-08-26 17:50:20 +02:00
Athou
9ace6b70f0 frontend-maven-plugin upgrade (#747) 2015-08-22 03:01:18 +02:00
Athou
447029ae70 skip jsoup parsing for null strings (#754) 2015-08-19 11:10:02 +02:00
Hubcapp
83f26cde53 Merge pull request #4 from Athou/master
pull latest commits from Athou's main branch to my fork
2015-08-19 01:52:13 -05:00
Athou
8ac52690fd fix wrong parameter name (#752) 2015-08-18 13:14:35 +02:00
Athou
6934b2bd27 remove println 2015-08-17 16:30:27 +02:00
Athou
6647e4fcd4 additional timer metrics 2015-08-14 12:58:55 +02:00
Athou
21710f55f3 proxy image enclosures too (#750) 2015-08-07 10:07:42 +02:00
Athou
27bd9a7489 fix test (#750) 2015-08-06 10:42:04 +02:00
Athou
630d37125c hide enclosure if already in entry content (fix #748) 2015-08-05 09:45:07 +02:00
Athou
9424237534 cleanup 2015-07-27 14:38:52 +02:00
Athou
cba3fbeb5f generate swagger file before running gulp (#746) 2015-07-25 08:48:56 +02:00
Athou
58778ccf43 dropwizard upgrade 2015-07-10 09:05:01 +02:00
Athou
6c61d47d78 swagger.json no longer generated at runtime 2015-07-09 16:08:31 +02:00
Athou
35e02f9d98 querydsl upgrade 2015-07-09 12:34:54 +02:00
Athou
58c1650863 make mvnw executable 2015-07-05 14:10:04 +02:00
Athou
9b14ffa14c update readme to use maven wrapper 2015-07-04 17:10:16 +02:00
Athou
96c09bf4cd ending quote missing 2015-07-04 17:05:03 +02:00
Athou
737cec744a fix mvnw on windows 2015-07-04 17:02:23 +02:00
Athou
13ed92bb94 add maven-wrapper 2015-07-04 09:06:47 +02:00
Athou
076594c78e force filter expression to lowercase 2015-06-29 12:56:17 +02:00
Athou
b6b1b4ebbe fix build, 2.0.1 has been deleted (https://github.com/dlmanning/gulp-sass/issues/305) 2015-06-26 15:05:10 +02:00
Athou
4007f37492 maven 3.3 setup instructions 2015-06-26 14:13:14 +02:00
Athou
532d671feb upgrade frontend-maven-plugin (fix #743) 2015-06-26 14:01:42 +02:00
Athou
fed7a1ac84 rewrite query using subqueries 2015-06-25 11:20:50 +02:00
Athou
ddfd170ea8 make eclipse mars happy 2015-06-24 11:42:01 +02:00
Athou
bae5c67dfa dropwizard upgrade 2015-06-19 08:41:23 +02:00
Athou
84f51603fb bump version 2015-06-19 08:33:43 +02:00
Athou
f73ddc03e9 readme update 2015-06-19 08:33:12 +02:00
130 changed files with 2253 additions and 1225 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ config.yml
# build directory # build directory
target target
target-ide
# log files # log files
log log

View File

@@ -1,3 +1,3 @@
language: java language: java
jdk: jdk:
- oraclejdk8 - openjdk8

View File

@@ -1,3 +1,23 @@
v 2.5.0
- unread count is now displayed in a favicon badge when supported
- the user agent string for the bot fetching feeds is now configurable
- feed parsing performance improvements
- support for java9+ runtime
- can now properly start from an empty postgresql database
v 2.4.0
- users were not able to change password or delete account
- fix api key generation
- feed entries can now be sorted alphabetically
- fix facebook sharing
- fix layout on iOS
- postgresql driver update (fix for postgres 9.6)
- various internationalization fixes
- security fixes
v 2.3.0
- dropwizard upgrade 0.9.1
- feed enclosures are hidden if they already displayed in the content
- fix youtube favicons
- various internationalization fixes
v 2.2.0 v 2.2.0
- fix youtube and instagram favicon fetching - fix youtube and instagram favicon fetching
- mark as read filter was lost when a feed was rearranged with drag&drop - mark as read filter was lost when a feed was rearranged with drag&drop
@@ -37,4 +57,4 @@ v2.0.0
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory consumption and better overall performances. - The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory consumption and better overall performances.
See the README on how to build CommaFeed from now on. See the README on how to build CommaFeed from now on.
- CommaFeed should no longer fetch the same feed multiple times in a row - CommaFeed should no longer fetch the same feed multiple times in a row
- Users can use their username or email to log in - Users can use their username or email to log in

View File

@@ -3,6 +3,7 @@
Sources for [CommaFeed.com](http://www.commafeed.com/). Sources for [CommaFeed.com](http://www.commafeed.com/).
Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS. Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS.
CommaFeed is now considered feature-complete and is in maintenance mode.
## Related open-source projects ## Related open-source projects
@@ -13,32 +14,43 @@ Browser extensions: [Chrome](https://github.com/Athou/commafeed-chrome) - [Firef
## Deployment on your own server ## Deployment on your own server
### The short version ### The very short version (download precompiled package)
mkdir commafeed && cd commafeed
wget https://github.com/Athou/commafeed/releases/download/2.4.0/commafeed.jar
wget https://raw.githubusercontent.com/Athou/commafeed/2.4.0/config.yml.example -O config.yml
vi config.yml
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
### The short version (build from sources)
git clone https://github.com/Athou/commafeed.git git clone https://github.com/Athou/commafeed.git
cd commafeed cd commafeed
mvn clean package ./mvnw clean package
cp config.yml.example config.yml cp config.yml.example config.yml
vi config.yml vi config.yml
java -Djava.net.preferIPv4Stack=true -jar target/commafeed.jar server config.yml java -Djava.net.preferIPv4Stack=true -jar target/commafeed.jar server config.yml
### The long version ### The long version (same as the short version, but more detailed)
CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x). CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x).
For storage, you can either use an embedded H2 database (use it only to test CommaFeed) or an external MySQL, PostgreSQL or SQLServer database. For storage, you can either use an embedded H2 database (use it only to test CommaFeed) or an external MySQL, PostgreSQL or SQLServer database.
You also need Maven 3.x (and a Java 1.8+ JDK) installed in order to build the application. You also need the Java 1.8+ JDK in order to build the application.
To install maven and openjdk on Ubuntu, issue the following commands To install the required packages to build CommaFeed on Ubuntu, issue the following commands
# if openjdk-8-jdk is not available on your ubuntu version (14.04 LTS), add the following repo first
sudo add-apt-repository ppa:openjdk-r/ppa
sudo apt-get update
sudo apt-get install g++ build-essential openjdk-8-jdk
sudo apt-get install g++ build-essential openjdk-8-jdk maven
# Make sure java8 is the selected java version # Make sure java8 is the selected java version
sudo update-alternatives --config java sudo update-alternatives --config java
sudo update-alternatives --config javac sudo update-alternatives --config javac
On Windows and other operating systems, just download maven 3.x from the [official site](http://maven.apache.org/), extract it somewhere and add the `bin` directory to your `PATH` environment variable.
Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip) Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip)
git clone https://github.com/Athou/commafeed.git git clone https://github.com/Athou/commafeed.git
@@ -46,7 +58,7 @@ Clone this repository. If you don't have git you can download the sources as a z
Now build the application Now build the application
mvn clean package ./mvnw clean package
Copy `config.yml.example` to `config.yml` then edit the file to your liking. Copy `config.yml.example` to `config.yml` then edit the file to your liking.
Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`. Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`.
@@ -55,20 +67,6 @@ Issue the following command to run the app, the server will listen by default on
You can use a proxy http server such as nginx or apache. You can use a proxy http server such as nginx or apache.
## Deployment on OpenShift
[OpenShift](https://openshift.redhat.com) is Red Hat's Platform-as-a-Service (PaaS) that allows developers to quickly develop, host, and scale applications in a cloud environment. CommaFeed runs perfectly on OpenShift and can even be used in the free tier. Follow the [Getting Started](https://developers.openshift.com/en/getting-started-overview.html) guide and after you sign up and install the Command Line Tools (RHC), do:
rhc create-app commafeed diy-0.1 mysql-5.5
cd commafeed
git remote add upstream -m master https://github.com/Athou/commafeed.git
git pull -s recursive -X theirs upstream master
git push
# To upgrade an existing openshift installation
git pull upstream master
git push
## Translate CommaFeed into your language ## Translate CommaFeed into your language
Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n). Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n).
@@ -112,7 +110,7 @@ Steps to configuring a development environment for CommaFeed may include, but ma
## Copyright and license ## Copyright and license
Copyright 2013-2014 CommaFeed. Copyright 2013-2016 CommaFeed.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License. you may not use this work except in compliance with the License.

View File

@@ -28,7 +28,8 @@
"devicejs": "0.2.4", "devicejs": "0.2.4",
"readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c", "readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c",
"zocial-less": "1.0.0", "zocial-less": "1.0.0",
"swagger-ui": "2.1.8-M1" "swagger-ui": "2.1.0",
"tinycon": "0.6.5"
}, },
"resolutions": { "resolutions": {
"angular": "1.3.14", "angular": "1.3.14",

View File

@@ -28,6 +28,14 @@ app:
smtpTls: false smtpTls: false
smtpUserName: user smtpUserName: user
smtpPassword: pass smtpPassword: pass
# Graphite Metric settings
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
graphiteEnabled: false
graphitePrefix: "test.commafeed"
graphiteHost: "localhost"
graphitePort: 2003
graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # wether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
@@ -59,6 +67,9 @@ app:
# announcement string displayed on the main page # announcement string displayed on the main page
announcement: announcement:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# Database connection # Database connection
# ------------------- # -------------------
# for MySQL # for MySQL
@@ -75,7 +86,7 @@ app:
database: database:
driverClass: org.h2.Driver driverClass: org.h2.Driver
url: jdbc:h2:./target/example;mv_store=false url: jdbc:h2:./target/example
user: sa user: sa
password: sa password: sa
properties: properties:

View File

@@ -4,7 +4,7 @@ app:
# url used to access commafeed # url used to access commafeed
publicUrl: http://localhost:8082/ publicUrl: http://localhost:8082/
# wether to allow user registrations # whether to allow user registrations
allowRegistrations: false allowRegistrations: false
# create a demo account the first time the app starts # create a demo account the first time the app starts
@@ -29,6 +29,14 @@ app:
smtpUserName: smtpUserName:
smtpPassword: smtpPassword:
smtpFromAddress: smtpFromAddress:
# Graphite Metric settings
# Allows those who use Graphite to have CommaFeed send metrics for graphing (time in seconds)
graphiteEnabled: false
graphitePrefix: "test.commafeed"
graphiteHost: "localhost"
graphitePort: 2003
graphiteInterval: 60
# wether this commafeed instance has a lot of feeds to refresh # wether this commafeed instance has a lot of feeds to refresh
# leave this to false in almost all cases # leave this to false in almost all cases
@@ -60,6 +68,9 @@ app:
# announcement string displayed on the main page # announcement string displayed on the main page
announcement: announcement:
# user-agent string that will be used by the http client, leave empty for the default one
userAgent:
# Database connection # Database connection
# ------------------- # -------------------
# for MySQL # for MySQL
@@ -100,6 +111,7 @@ logging:
com.commafeed: INFO com.commafeed: INFO
liquibase: INFO liquibase: INFO
io.dropwizard.server.ServerFactory: INFO io.dropwizard.server.ServerFactory: INFO
org.hibernate.orm.deprecation: "OFF"
appenders: appenders:
- type: console - type: console
- type: file - type: file
@@ -120,4 +132,4 @@ redis:
timeout: 2000 timeout: 2000
database: 0 database: 0
maxTotal: 500 maxTotal: 500

View File

@@ -49,8 +49,9 @@ gulp.task('select2', function() {
gulp.task('swagger-ui', function() { gulp.task('swagger-ui', function() {
var index_html = SRC_DIR + 'api/index.html'; var index_html = SRC_DIR + 'api/index.html';
var swagger_json = 'target/swagger/swagger.json';
var lib = SRC_DIR + 'lib/swagger-ui/dist/**/*'; var lib = SRC_DIR + 'lib/swagger-ui/dist/**/*';
return gulp.src([lib, index_html]).pipe(gulp.dest(BUILD_DIR + 'api')); return gulp.src([lib, index_html, swagger_json]).pipe(gulp.dest(BUILD_DIR + 'api'));
}); });
gulp.task('template-cache', function() { gulp.task('template-cache', function() {

BIN
maven/maven-wrapper.jar Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
#Maven download properties
#Sat Jul 04 09:06:32 CEST 2015
distributionUrl=https\://repository.apache.org/content/repositories/releases/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip

234
mvnw vendored Executable file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
#
# Look for the Apple JDKs first to preserve the existing behaviour, and then look
# for the new JDKs provided by Oracle.
#
if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
#
# Apple JDKs
#
export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
fi
if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
#
# Apple JDKs
#
export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
fi
if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
#
# Oracle JDKs
#
export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
fi
if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
#
# Apple JDKs
#
export JAVA_HOME=`/usr/libexec/java_home`
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Migwn, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
local basedir=$(pwd)
local wdir=$(pwd)
while [ "$wdir" != '/' ] ; do
wdir=$(cd "$wdir/.."; pwd)
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER="org.apache.maven.wrapper.MavenWrapperMain"
exec "$JAVACMD" \
$MAVEN_OPTS \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
-classpath \
"$MAVEN_PROJECTBASEDIR/maven/maven-wrapper.jar" \
${WRAPPER_LAUNCHER} "$@"

141
mvnw.bat Normal file
View File

@@ -0,0 +1,141 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:init
set MAVEN_CMD_LINE_ARGS=%*
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\maven\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
%MAVEN_JAVA_EXE% -Dmaven.multiModuleProjectDirectory="" %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

View File

@@ -4,7 +4,7 @@
"main": "main.js", "main": "main.js",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"bower": "1.4.1", "bower": "1.8.8",
"gulp": "3.8.11", "gulp": "3.8.11",
"gulp-rev": "4.0.0", "gulp-rev": "4.0.0",
"gulp-rev-replace": "0.4.1", "gulp-rev-replace": "0.4.1",
@@ -13,7 +13,7 @@
"gulp-filter": "2.0.2", "gulp-filter": "2.0.2",
"gulp-connect": "2.2.0", "gulp-connect": "2.2.0",
"connect-modrewrite": "0.8.1", "connect-modrewrite": "0.8.1",
"gulp-sass": "2.0.1", "gulp-sass": "2.0.2",
"gulp-useref": "1.1.2", "gulp-useref": "1.1.2",
"gulp-angular-templatecache": "1.6.0" "gulp-angular-templatecache": "1.6.0"
} }

223
pom.xml
View File

@@ -4,20 +4,16 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.commafeed</groupId> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>2.2.0</version> <version>2.5.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>CommaFeed</name> <name>CommaFeed</name>
<prerequisites>
<maven>3.0.5</maven>
</prerequisites>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<dropwizard.version>0.8.1</dropwizard.version> <dropwizard.version>1.3.20</dropwizard.version>
<guice.version>4.0</guice.version> <guice.version>4.2.2</guice.version>
<querydsl.version>3.6.4</querydsl.version> <querydsl.version>4.2.1</querydsl.version>
<rome.version>1.5.0</rome.version> <rome.version>1.5.0</rome.version>
</properties> </properties>
@@ -36,43 +32,11 @@
</resource> </resource>
</resources> </resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<versionRange>[0.0.22,)</versionRange>
<goals>
<goal>npm</goal>
<goal>gulp</goal>
<goal>bower</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute>
<runOnIncremental>false</runOnIncremental>
</execute>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version> <version>3.8.0</version>
<configuration> <configuration>
<source>${java.version}</source> <source>${java.version}</source>
<target>${java.version}</target> <target>${java.version}</target>
@@ -129,20 +93,53 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>com.github.kongchen</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>3.1.7</version>
<configuration>
<apiSources>
<apiSource>
<locations>
<location>com.commafeed.frontend.resource</location>
<location>com.commafeed.frontend.model</location>
<location>com.commafeed.frontend.model.request</location>
</locations>
<swaggerDirectory>target/swagger</swaggerDirectory>
<basePath>/rest</basePath>
<info>
<title>CommaFeed</title>
<version>${project.version}</version>
</info>
<typesToSkip>
<typeToSkip>com.commafeed.backend.model.User</typeToSkip>
</typesToSkip>
</apiSource>
</apiSources>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>com.github.eirslett</groupId> <groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId> <artifactId>frontend-maven-plugin</artifactId>
<version>0.0.22</version> <version>1.6</version>
<executions> <executions>
<execution> <execution>
<id>install node and npm</id> <id>install node and npm</id>
<goals> <goals>
<goal>install-node-and-npm</goal> <goal>install-node-and-npm</goal>
</goals> </goals>
<phase>generate-resources</phase> <phase>compile</phase>
<configuration> <configuration>
<nodeVersion>v0.12.4</nodeVersion> <nodeVersion>v6.11.4</nodeVersion>
<npmVersion>2.10.1</npmVersion> <npmVersion>3.10.6</npmVersion>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -150,13 +147,17 @@
<goals> <goals>
<goal>npm</goal> <goal>npm</goal>
</goals> </goals>
<phase>generate-resources</phase> <phase>compile</phase>
<configuration>
<arguments>install</arguments>
</configuration>
</execution> </execution>
<execution> <execution>
<id>bower install</id> <id>bower install</id>
<goals> <goals>
<goal>bower</goal> <goal>bower</goal>
</goals> </goals>
<phase>compile</phase>
<configuration> <configuration>
<arguments>install</arguments> <arguments>install</arguments>
</configuration> </configuration>
@@ -166,14 +167,14 @@
<goals> <goals>
<goal>gulp</goal> <goal>gulp</goal>
</goals> </goals>
<phase>generate-resources</phase> <phase>compile</phase>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-jar-plugin</artifactId>
<version>2.6</version> <version>3.1.1</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>
@@ -185,11 +186,70 @@
</plugins> </plugins>
</build> </build>
<profiles>
<profile>
<id>only-eclipse</id>
<activation>
<property>
<name>m2e.version</name>
</property>
</activation>
<build>
<directory>target-ide</directory>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<versionRange>[0.0.22,)</versionRange>
<goals>
<goal>npm</goal>
<goal>gulp</goal>
<goal>bower</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute>
<runOnIncremental>false</runOnIncremental>
</execute>
</action>
</pluginExecution>
<pluginExecution>
<pluginExecutionFilter>
<groupId>com.github.kongchen</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<versionRange>[3.1.7,)</versionRange>
<goals>
<goal>generate</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.16.4</version> <version>1.18.6</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -213,6 +273,16 @@
<groupId>io.dropwizard</groupId> <groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId> <artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version> <version>${dropwizard.version}</version>
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>aopalliance-repackaged</artifactId>
</exclusion>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.dropwizard</groupId> <groupId>io.dropwizard</groupId>
@@ -233,12 +303,28 @@
<groupId>io.dropwizard</groupId> <groupId>io.dropwizard</groupId>
<artifactId>dropwizard-forms</artifactId> <artifactId>dropwizard-forms</artifactId>
<version>${dropwizard.version}</version> <version>${dropwizard.version}</version>
<exclusions>
<exclusion>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>
<version>4.5</version> <version>4.5.2</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<artifactId>commons-logging</artifactId> <artifactId>commons-logging</artifactId>
@@ -248,35 +334,24 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.wordnik</groupId> <groupId>io.swagger</groupId>
<artifactId>swagger-jaxrs</artifactId> <artifactId>swagger-annotations</artifactId>
<version>1.5.3-M1</version> <version>1.5.22</version>
<exclusions>
<exclusion>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.mysema.querydsl</groupId> <groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId> <artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version> <version>${querydsl.version}</version>
<scope>provided</scope> <scope>provided</scope>
<classifier>hibernate</classifier> <classifier>hibernate</classifier>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.mysema.querydsl</groupId> <groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId> <artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version> <version>${querydsl.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
@@ -285,7 +360,7 @@
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
<version>4.0</version> <version>4.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
@@ -342,11 +417,15 @@
<artifactId>jdom2</artifactId> <artifactId>jdom2</artifactId>
<version>2.0.6</version> <version>2.0.6</version>
</dependency> </dependency>
<dependency>
<groupId>org.ahocorasick</groupId>
<artifactId>ahocorasick</artifactId>
<version>0.4.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.jsoup</groupId> <groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId> <artifactId>jsoup</artifactId>
<version>1.8.2</version> <version>1.8.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.ibm.icu</groupId> <groupId>com.ibm.icu</groupId>
@@ -374,17 +453,17 @@
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
<version>1.4.187</version> <version>1.4.197</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version> <version>5.1.42</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
<version>9.4-1201-jdbc41</version> <version>9.4.1212</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.sourceforge.jtds</groupId> <groupId>net.sourceforge.jtds</groupId>

View File

@@ -25,7 +25,7 @@
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function () {
window.swaggerUi = new SwaggerUi({ window.swaggerUi = new SwaggerUi({
url: "../rest/swagger.json", url: "./swagger.json",
dom_id: "swagger-ui-container", dom_id: "swagger-ui-container",
supportedSubmitMethods: ['get', 'post', 'put', 'delete'], supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function(swaggerApi, swaggerUi){ onComplete: function(swaggerApi, swaggerUi){

View File

@@ -41,6 +41,7 @@
"refresh" : "إعادة انعاش", "refresh" : "إعادة انعاش",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "الترتيب حسب التاريخ تصاعدي / تنازلي", "sort_by_asc_desc" : "الترتيب حسب التاريخ تصاعدي / تنازلي",
"sort_by_abc_zyx" : "Sort alphabetically",
"titles_only" : "العناوين فقط", "titles_only" : "العناوين فقط",
"expanded_view" : "عرض موسع", "expanded_view" : "عرض موسع",
"mark_all_as_read" : "اعتبر الكل مقروء", "mark_all_as_read" : "اعتبر الكل مقروء",
@@ -174,7 +175,8 @@
"font_size" : "increase/decrease font size of the current entry ", "font_size" : "increase/decrease font size of the current entry ",
"go_to_all" : "go to the All view ", "go_to_all" : "go to the All view ",
"go_to_starred" : "go to the Starred view ", "go_to_starred" : "go to the Starred view ",
"feed_search" : "navigate to a subscription by entering the subscription name" "feed_search" : "navigate to a subscription by entering the subscription name",
"refresh": "refresh"
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"refresh" : "Actualitzar", "refresh" : "Actualitzar",
"refresh_all" : "Força l'actualització de tots els canals", "refresh_all" : "Força l'actualització de tots els canals",
"sort_by_asc_desc" : "Ordenar per data asc/desc", "sort_by_asc_desc" : "Ordenar per data asc/desc",
"sort_by_abc_zyx" : "Sort alphabetically",
"titles_only" : "Només títols", "titles_only" : "Només títols",
"expanded_view" : "Vista ampliada", "expanded_view" : "Vista ampliada",
"mark_all_as_read" : "Marcar tots llegits", "mark_all_as_read" : "Marcar tots llegits",

View File

@@ -41,6 +41,7 @@
"refresh " : " Obnovit", "refresh " : " Obnovit",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc " : " Seřadit podle nejnovějšího/nejstaršího", "sort_by_asc_desc " : " Seřadit podle nejnovějšího/nejstaršího",
"sort_by_abc_zyx" : "Sort alphabetically",
"titles_only " : " Zobrazit jenom titulky", "titles_only " : " Zobrazit jenom titulky",
"expanded_view " : " Rozšířený náhled", "expanded_view " : " Rozšířený náhled",
"mark_all_as_read " : " Označit vše jako přečtené", "mark_all_as_read " : " Označit vše jako přečtené",

View File

@@ -41,6 +41,7 @@
"refresh" : "Adnewyddu", "refresh" : "Adnewyddu",
"refresh_all" : "Gorfodi ail-lwytho pob ffrwd", "refresh_all" : "Gorfodi ail-lwytho pob ffrwd",
"sort_by_asc_desc" : "Trefnu yn ôl dyddiad", "sort_by_asc_desc" : "Trefnu yn ôl dyddiad",
"sort_by_abc_zyx" : "Sort alphabetically",
"titles_only" : "Teitlau yn unig", "titles_only" : "Teitlau yn unig",
"expanded_view" : "Golwg estynedig", "expanded_view" : "Golwg estynedig",
"mark_all_as_read" : "Nodi'r cyfan fel wedi ei ddarllen", "mark_all_as_read" : "Nodi'r cyfan fel wedi ei ddarllen",

View File

@@ -41,6 +41,7 @@
"refresh" : "Opdater", "refresh" : "Opdater",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Sorter efter dato ny/gammel", "sort_by_asc_desc" : "Sorter efter dato ny/gammel",
"sort_by_abc_zyx" : "Sort alphabetically",
"titles_only" : "Kun titler", "titles_only" : "Kun titler",
"expanded_view" : "Udvidet visning", "expanded_view" : "Udvidet visning",
"mark_all_as_read" : "Marker alle som læst", "mark_all_as_read" : "Marker alle som læst",

View File

@@ -41,6 +41,7 @@
"refresh" : "Aktualisieren", "refresh" : "Aktualisieren",
"refresh_all" : "Erzwinge Aktualisierung aller Feeds", "refresh_all" : "Erzwinge Aktualisierung aller Feeds",
"sort_by_asc_desc" : "Nach Datum sortieren (auf-/absteigend)", "sort_by_asc_desc" : "Nach Datum sortieren (auf-/absteigend)",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Nur Überschriften", "titles_only" : "Nur Überschriften",
"expanded_view" : "Ausgedehnte Ansicht", "expanded_view" : "Ausgedehnte Ansicht",
"mark_all_as_read" : "Alle Artikel als gelesen markieren", "mark_all_as_read" : "Alle Artikel als gelesen markieren",

View File

@@ -41,6 +41,7 @@
"refresh" : "Refresh", "refresh" : "Refresh",
"refresh_all" : "Force refresh all my feeds", "refresh_all" : "Force refresh all my feeds",
"sort_by_asc_desc" : "Sort by date asc/desc", "sort_by_asc_desc" : "Sort by date asc/desc",
"sort_by_abc_zyx" : "Sort alphabetically",
"titles_only" : "Titles only", "titles_only" : "Titles only",
"expanded_view" : "Expanded view", "expanded_view" : "Expanded view",
"mark_all_as_read" : "Mark all as read", "mark_all_as_read" : "Mark all as read",
@@ -176,7 +177,8 @@
"font_size" : "increase/decrease font size of the current entry", "font_size" : "increase/decrease font size of the current entry",
"go_to_all" : "go to the All view", "go_to_all" : "go to the All view",
"go_to_starred" : "go to the Starred view", "go_to_starred" : "go to the Starred view",
"feed_search" : "navigate to a subscription by entering the subscription name" "feed_search" : "navigate to a subscription by entering the subscription name",
"refresh": "refresh"
} }
} }
} }

View File

@@ -7,24 +7,26 @@
"download" : "Descargar", "download" : "Descargar",
"link" : "Enlace", "link" : "Enlace",
"bookmark" : "Marcador", "bookmark" : "Marcador",
"close" : "Close ", "close" : "Cerrar",
"tags" : "Tags " "tags" : "Etiquetas"
}, },
"tree" : { "tree" : {
"subscribe" : "Subscribir", "subscribe" : "Suscribirse",
"import" : "Importar", "import" : "Importar",
"new_category" : "Nueva categoría", "new_category" : "Nueva categoría",
"all" : "Todos", "all" : "Todos",
"starred" : "Destacado" "starred" : "Destacados"
}, },
"subscribe" : { "subscribe" : {
"feed_url" : "URL del Canal", "feed_url" : "URL del canal",
"feed_name" : "Nombre del Canal", "filtering_expression" : "Expresión de filtrado",
"filtering_expression_help" : "Si no está vacía, una expresión se evalúa como 'cierta' o 'falsa'. Si es falsa, las nueva entradas de este canal se marcarán como leídas automáticamente.\nLas variables disponibles son 'title' (título), 'content'(contenido), 'url' (URL), 'author' (autor), y 'categories' (categorías) y sus contenidos son convertidos a minúsculas para facilitar la comparación de strings (cadenas de texto).\nEjemplo: url.contains('youtube') or (author eq 'athou' and title.contains('github').\nLa sintaxis completa está disponible <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>aquí</a>.",
"feed_name" : "Nombre del canal",
"category" : "Categoría" "category" : "Categoría"
}, },
"import" : { "import" : {
"google_reader_prefix" : "Déjame importar tus canales de tu", "google_reader_prefix" : "Déjame importar tus canales de tu cuenta ",
"google_reader_suffix" : " cuenta.", "google_reader_suffix" : ".",
"google_download" : "También puedes subir tu archivo subscriptions.xml.", "google_download" : "También puedes subir tu archivo subscriptions.xml.",
"google_download_link" : "Descárgalo de aquí.", "google_download_link" : "Descárgalo de aquí.",
"xml_file" : "Archivo OPML" "xml_file" : "Archivo OPML"
@@ -34,147 +36,148 @@
"parent" : "Padre" "parent" : "Padre"
}, },
"toolbar" : { "toolbar" : {
"unread" : "Sin Leer", "unread" : "No leídos",
"all" : "Todos", "all" : "Todos",
"previous_entry" : "Entrada Anterior", "previous_entry" : "Entrada anterior",
"next_entry" : "Próxima Entrada", "next_entry" : "Entrada siguiente",
"refresh" : "Atualizar", "refresh" : "Actualizar",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Forzar la actualización de todos mis canales.",
"sort_by_asc_desc" : "Ordenar por fecha asc/desc", "sort_by_asc_desc" : "Ordenar por fecha asc/desc.",
"titles_only" : "Sólo Títulos", "sort_by_abc_zyx" : "Ordenar alfabéticamente",
"expanded_view" : "Vista Expandida", "titles_only" : "Sólo títulos",
"expanded_view" : "Vista expandida",
"mark_all_as_read" : "Marcar todos como leído", "mark_all_as_read" : "Marcar todos como leído",
"mark_all_older_12_hours" : "Items older than 12 hours ", "mark_all_older_12_hours" : "Entradas anteriores a 12 horas.",
"mark_all_older_day" : "Artículos anteriores a un día", "mark_all_older_day" : "Entradas anteriores a un día.",
"mark_all_older_week" : "Artículos más de una semana", "mark_all_older_week" : "Entradas anteriores a una semana.",
"mark_all_older_two_weeks" : "Artículos más de does semanas", "mark_all_older_two_weeks" : "Entradas anteriores a 2 semanas.",
"settings" : "Ajustes", "settings" : "Ajustes",
"profile" : "Perfil", "profile" : "Perfil",
"admin" : "Admin", "admin" : "Admin",
"about" : "Acerca de", "about" : "Acerca de...",
"logout" : "Cerrar sesión", "logout" : "Cerrar sesión",
"donate" : "Donar" "donate" : "Donar"
}, },
"view" : { "view" : {
"entry_source" : "from ", "entry_source" : "de ",
"entry_author" : "by ", "entry_author" : "por ",
"error_while_loading_feed" : "Error mientras se cargaba este canal", "error_while_loading_feed" : "Error mientras se cargaba este canal.",
"keep_unread" : "Guardar no leídos", "keep_unread" : "Mantener como no leído.",
"no_unread_items" : "no tiene items sin leer.", "no_unread_items" : "no tiene entradas sin leer.",
"mark_up_to_here" : "Mark as read up to here ", "mark_up_to_here" : "Marcar como leídos hasta aquí.",
"search_for" : "searching for: ", "search_for" : "buscando: ",
"no_search_results" : "No match found for the requested keywords " "no_search_results" : "No se han encontrado resultados para las palabras clave especificadas."
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Type in a subscription... ", "hint" : "Introduce una suscripción...",
"help" : "Use the return key to select and arrow keys to navigate. ", "help" : "Usa la tecla Intro para seleccionar y las teclas de flecha para navegar.",
"result_prefix" : "Your subscriptions: " "result_prefix" : "Tus suscripciones:"
}, },
"settings" : { "settings" : {
"general" : { "general" : {
"value" : "General", "value" : "General",
"language" : "Lenguaje", "language" : "Idioma",
"language_contribute" : "Contribuye con traducciones", "language_contribute" : "Contribuye con traducciones.",
"show_unread" : "Mostrar canales y categorías sin entradas no leídas.", "show_unread" : "Mostrar canales y categorías sin entradas no leídas.",
"social_buttons" : "Mostrar botones de compartir de redes sociales.", "social_buttons" : "Mostrar botones para compartir de redes sociales.",
"scroll_marks" : "En vista expandida, el desplazamiento por las entradas las marca como leídas" "scroll_marks" : "En vista expandida, el desplazamiento por las entradas las marca como leídas."
}, },
"appearance" : "Appearance ", "appearance" : "Apariencia",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ", "scroll_speed" : "Velocidad de desplazamiento al navegar entre entradas (en milisegundos)",
"scroll_speed_help" : "set to 0 to disable ", "scroll_speed_help" : "ponlo a 0 para desactivarlo",
"theme" : "Theme ", "theme" : "Tema",
"submit_your_theme" : "Submit your theme ", "submit_your_theme" : "Envía tu tema ",
"custom_css" : "CSS Personalizado" "custom_css" : "CSS personalizado"
}, },
"details" : { "details" : {
"feed_details" : "Detalles de Canales", "feed_details" : "Detalles del canal",
"url" : "URL", "url" : "URL",
"website" : "Website ", "website" : "Sitio web",
"name" : "Nombre", "name" : "Nombre",
"category" : "Categoría", "category" : "Categoría",
"position" : "Position ", "position" : "Posición",
"last_refresh" : "Última actualización", "last_refresh" : "Última actualización",
"message" : "Last refresh message ", "message" : "Último mensaje de actualización",
"next_refresh" : "Next refresh ", "next_refresh" : "Próxima actualización",
"queued_for_refresh" : "Queued for refresh ", "queued_for_refresh" : "En cola para actualizar",
"feed_url" : "URL del Canal", "feed_url" : "URL del canal",
"generate_api_key_first" : "Genera una llave API en tu perfil primero.", "generate_api_key_first" : "Genera una clave API en tu perfil primero.",
"unsubscribe" : "Terminar subscripción", "unsubscribe" : "Terminar suscripción",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "¿Estás seguro de querer terminar tu suscripción a este canal?",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "¿Estás seguro de querer eliminar esta categoría?",
"category_details" : "Detalles de la categoría", "category_details" : "Detalles de la categoría",
"tag_details" : "Tag details ", "tag_details" : "Detalles de las etiquetas ",
"parent_category" : "Categoría principal" "parent_category" : "Categoría principal"
}, },
"profile" : { "profile" : {
"user_name" : "Nombre de usuario", "user_name" : "Nombre de usuario",
"email" : "Correo", "email" : "Correo electrónico",
"change_password" : "Cambiar contraseña", "change_password" : "Cambiar contraseña",
"confirm_password" : "Confirmar contraseña", "confirm_password" : "Confirmar contraseña",
"minimum_6_chars" : "Mínimo 6 caracteres", "minimum_6_chars" : "Mínimo 6 caracteres",
"passwords_do_not_match" : "Las contraseñas no coinciden", "passwords_do_not_match" : "Las contraseñas no coinciden",
"api_key" : "Llave API", "api_key" : "Clave API",
"api_key_not_generated" : "No generado todavía", "api_key_not_generated" : "No generado todavía",
"generate_new_api_key" : "Generar nueva llave API", "generate_new_api_key" : "Generar nueva clave API",
"generate_new_api_key_info" : "Al cambiar la contraseña se generará una nueva llave API", "generate_new_api_key_info" : "Al cambiar la contraseña se generará una nueva clave API.",
"opml_export" : "Exportación de OPML", "opml_export" : "Exportación de OPML",
"delete_account" : "Eliminar cuenta", "delete_account" : "Eliminar cuenta",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "¿Eliminar tu cuenta? ¡No habrá vuelta atrás! "
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
"value" : "REST API", "value" : "REST API",
"line1" : "CommaFeed está construido con el uso de JAX-RS y AngularJS. Por eso, un REST API esta disponible.", "line1" : "CommaFeed está construido sobre JAX-RS y AngularJS. Por lo tanto, una REST API está disponible.",
"link_to_documentation" : "Vínculo de la documentación." "link_to_documentation" : "Enlace a la documentación."
}, },
"keyboard_shortcuts" : "Atajos de teclado", "keyboard_shortcuts" : "Atajos de teclado",
"version" : "CommaFeed version ", "version" : "Versión de CommaFeed",
"line1_prefix" : "CommaFeed es un proyecto open-source. El código se encuentra en ", "line1_prefix" : "CommaFeed es un proyecto de código abierto. El código se encuentra en ",
"line1_suffix" : ".", "line1_suffix" : ".",
"line2_prefix" : "Si encuentras un problema, por favor reportalo en la página de problemas de ", "line2_prefix" : "Si encuentras un problema, por favor repórtalo en la página de problemas de ",
"line2_suffix" : " del proyecto.", "line2_suffix" : " del proyecto.",
"line3" : "Si te gusta este proyecto, por favor considera realizar una donacion para apoyar al desarrollador y ayudar a cubrir los costos de mantenimiento.", "line3" : "Si te gusta este proyecto, por favor considera realizar una donación para apoyar al desarrollador y ayudar a cubrir los costes de mantenimiento.",
"line4" : "For those of you who prefer bitcoin, here is the address ", "line4" : "Para aquellos de vosotros que prefieran bitcoin, aquí está la dirección ",
"goodies" : { "goodies" : {
"value" : "Goodies", "value" : "Extras",
"android_app" : "Android app ", "android_app" : "Apps para Android",
"subscribe_url" : "Subscribe URL ", "subscribe_url" : "URL para suscribirse ",
"chrome_extension" : "Extensión de Chrome", "chrome_extension" : "Extensión para Chrome.",
"firefox_extension" : "Extensión de Firefox", "firefox_extension" : "Extensión para Firefox.",
"opera_extension" : "Opera extension ", "opera_extension" : "Extensón para Opera.",
"subscribe_bookmarklet" : "Add subscription bookmarklet (click) ", "subscribe_bookmarklet" : "Añadir marcador de suscripción (clic).",
"subscribe_bookmarklet_asc" : "Oldest first ", "subscribe_bookmarklet_asc" : "Más antiguos primero",
"subscribe_bookmarklet_desc" : "Newest first ", "subscribe_bookmarklet_desc" : "Más recientes primero",
"next_unread_bookmarklet" : "Next unread item bookmarklet (drag to bookmark bar) " "next_unread_bookmarklet" : "Marcador a la siguiente entrada no leída (arástralo a la barra de marcadores) "
}, },
"translation" : { "translation" : {
"value" : "Traducción", "value" : "Traducción",
"message" : "Necesitamos tu ayuda para ayudar a traducir CommaFeed.", "message" : "Necesitamos tu ayuda para ayudar a traducir CommaFeed.",
"link" : "Ver como contribuir con traducciones." "link" : "Ver cómo contribuir con traducciones."
}, },
"announcements" : "Anuncios", "announcements" : "Anuncios",
"shortcuts" : { "shortcuts" : {
"mouse_middleclick" : "ratón botón medio", "mouse_middleclick" : "click medio",
"open_next_entry" : "abrir próxima entrada", "open_next_entry" : "abrir la siguiente entrada",
"open_previous_entry" : "abrir entrada previa", "open_previous_entry" : "abrir la entrada anterior",
"spacebar" : "space/shift+space ", "spacebar" : "espacio/mayúsculas+espacio",
"move_page_down_up" : "moves the page down/up ", "move_page_down_up" : "mueve la página arriba/abajo",
"focus_next_entry" : "Establecer el foco en la próxima entrada sin abrirlo", "focus_next_entry" : "establecer el foco en la siguiente entrada sin abrirla",
"focus_previous_entry" : "Establecer el foco en la entrada anterior sin abrirlo", "focus_previous_entry" : "establecer el foco en la entrada anterior sin abrirla",
"open_next_feed" : "open next feed or category ", "open_next_feed" : "abrir el siguiente canal o categoría",
"open_previous_feed" : "open previous feed or category ", "open_previous_feed" : "abrir el canal o categoría previo",
"open_close_current_entry" : "abrir/cerrar entrada actual", "open_close_current_entry" : "abrir/cerrar la entrada actual",
"open_current_entry_in_new_window" : "abrir entrada actual en una nueva ventana", "open_current_entry_in_new_window" : "abrir la entrada actual en una nueva ventana",
"open_current_entry_in_new_window_background" : "open current entry in a new window in the background ", "open_current_entry_in_new_window_background" : "abrir la entrada actual en una nueva ventana en segundo plano",
"star_unstar" : "marcar/no marcar la entrada actual", "star_unstar" : "destacar la entrada actual",
"mark_current_entry" : "marcar como leída/no la leída entrada actual", "mark_current_entry" : "marcar la entrada actual como leída/no la leída",
"mark_all_as_read" : "marcar todas las entradas como leídas", "mark_all_as_read" : "marcar todas las entradas como leídas",
"open_in_new_tab_mark_as_read" : "abrir entrada en una nueva pestaña y marcar como leída", "open_in_new_tab_mark_as_read" : "abrir entrada en una nueva pestaña y marcar como leída",
"fullscreen" : "toggle full screen mode ", "fullscreen" : "activar/desactivar el modo pantalla completa ",
"font_size" : "increase/decrease font size of the current entry ", "font_size" : "aumentar/reducir el tamaño de la fuente de la entrada actual",
"go_to_all" : "go to the All view ", "go_to_all" : "ver Todos",
"go_to_starred" : "go to the Starred view ", "go_to_starred" : "ver Destacados",
"feed_search" : "navigate to a subscription by entering the subscription name " "feed_search" : "navega a una suscripción al introducir su nombre"
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"refresh" : "تازه‌سازی", "refresh" : "تازه‌سازی",
"refresh_all" : "مجبورکردن تازه‌سازی همهٔ خوراک‌ها", "refresh_all" : "مجبورکردن تازه‌سازی همهٔ خوراک‌ها",
"sort_by_asc_desc" : "مرتب‌کردن بر اساس تاریخ به‌صورت صعودی/نزولی", "sort_by_asc_desc" : "مرتب‌کردن بر اساس تاریخ به‌صورت صعودی/نزولی",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "فقط عنوان‌ها", "titles_only" : "فقط عنوان‌ها",
"expanded_view" : "نمای گسترش‌یافته", "expanded_view" : "نمای گسترش‌یافته",
"mark_all_as_read" : "علامت‌گذاری تمامی مطالب به‌عنوان خوانده‌شده", "mark_all_as_read" : "علامت‌گذاری تمامی مطالب به‌عنوان خوانده‌شده",

View File

@@ -41,6 +41,7 @@
"refresh" : "Päivitä", "refresh" : "Päivitä",
"refresh_all" : "Pakota kaikkien syötteiden päivitys", "refresh_all" : "Pakota kaikkien syötteiden päivitys",
"sort_by_asc_desc" : "Järjestä päivämäärän mukaan nousevasti/laskevasti", "sort_by_asc_desc" : "Järjestä päivämäärän mukaan nousevasti/laskevasti",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Näytä vain otsikot", "titles_only" : "Näytä vain otsikot",
"expanded_view" : "Laajennettu näkymä", "expanded_view" : "Laajennettu näkymä",
"mark_all_as_read" : "Merkitse kaikki luetuiksi", "mark_all_as_read" : "Merkitse kaikki luetuiksi",

View File

@@ -34,24 +34,25 @@
"parent" : "Parent" "parent" : "Parent"
}, },
"toolbar" : { "toolbar" : {
"unread" : "Non-lus", "unread" : "Non lus",
"all" : "Tous", "all" : "Tous",
"previous_entry" : "Article précédent", "previous_entry" : "Article précédent",
"next_entry" : "Article suivant", "next_entry" : "Article suivant",
"refresh" : "Rafraîchir", "refresh" : "Rafraîchir",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Rafraîchir tous les flux",
"sort_by_asc_desc" : "Trier par date croissante/décroissante", "sort_by_asc_desc" : "Trier par date croissante/décroissante",
"sort_by_abc_zyx" : "Trier par ordre alphabétique",
"titles_only" : "Titres uniquement", "titles_only" : "Titres uniquement",
"expanded_view" : "Vue étendue", "expanded_view" : "Vue étendue",
"mark_all_as_read" : "Tout marquer comme lu", "mark_all_as_read" : "Tout marquer comme lu",
"mark_all_older_12_hours" : "Items older than 12 hours ", "mark_all_older_12_hours" : "Articles de plus de 12 heures",
"mark_all_older_day" : "Articles de plus d'un jour", "mark_all_older_day" : "Articles de plus d'une journée",
"mark_all_older_week" : "Articles de plus d'une semaine", "mark_all_older_week" : "Articles de plus d'une semaine",
"mark_all_older_two_weeks" : "Articles de plus d'un mois", "mark_all_older_two_weeks" : "Articles de plus d'un mois",
"settings" : "Préférences", "settings" : "Préférences",
"profile" : "Profil", "profile" : "Profil",
"admin" : "Administration", "admin" : "Administration",
"about" : "A propos", "about" : "À propos",
"logout" : "Déconnexion", "logout" : "Déconnexion",
"donate" : "Faire un don" "donate" : "Faire un don"
}, },
@@ -59,16 +60,16 @@
"entry_source" : "sur", "entry_source" : "sur",
"entry_author" : "par ", "entry_author" : "par ",
"error_while_loading_feed" : "Erreur durant le chargement de ce flux", "error_while_loading_feed" : "Erreur durant le chargement de ce flux",
"keep_unread" : "Garder non-lu", "keep_unread" : "Garder non lu",
"no_unread_items" : "n'a pas d'articles non-lus.", "no_unread_items" : "n'a pas d'articles non lus.",
"mark_up_to_here" : "Marquer comme lu jusqu'ici", "mark_up_to_here" : "Marquer comme lu jusqu'ici",
"search_for" : "searching for: ", "search_for" : "recherche : ",
"no_search_results" : "No match found for the requested keywords " "no_search_results" : "Pas de résultats avec le terme indiqué."
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Tapez un nom de flux", "hint" : "Tapez un nom de flux",
"help" : "Utilisez la touche entrée pour sélectionner et les flèches pour naviguer", "help" : "Utilisez la touche entrée pour sélectionner et les flèches pour naviguer",
"result_prefix" : "Vos flux:" "result_prefix" : "Vos flux :"
}, },
"settings" : { "settings" : {
"general" : { "general" : {
@@ -76,12 +77,12 @@
"language" : "Langue", "language" : "Langue",
"language_contribute" : "Contribuer aux traductions", "language_contribute" : "Contribuer aux traductions",
"show_unread" : "Afficher les flux et les catégories pour lesquels tout est déjà lu", "show_unread" : "Afficher les flux et les catégories pour lesquels tout est déjà lu",
"social_buttons" : "Afficher les boutons de partage sur réseaux sociaux", "social_buttons" : "Afficher les boutons de partage sur les réseaux sociaux",
"scroll_marks" : "En mode de lecture étendu, marquer comme lu les éléments lorsque la fenêtre descend." "scroll_marks" : "En mode de lecture étendu, marquer les éléments comme lus lorsque la fenêtre descend."
}, },
"appearance" : "Apparence", "appearance" : "Apparence",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ", "scroll_speed" : "Vitesse de défilement entre les entrées (en millisecondes) ",
"scroll_speed_help" : "set to 0 to disable ", "scroll_speed_help" : "Mettez 0 pour désactiver",
"theme" : "Thème", "theme" : "Thème",
"submit_your_theme" : "Soumettez votre thème.", "submit_your_theme" : "Soumettez votre thème.",
"custom_css" : "CSS personnelle" "custom_css" : "CSS personnelle"
@@ -94,16 +95,16 @@
"category" : "Catégorie", "category" : "Catégorie",
"position" : "Position", "position" : "Position",
"last_refresh" : "Dernière mise à jour", "last_refresh" : "Dernière mise à jour",
"message" : "Last refresh message ", "message" : "Message de la dernière mise à jour ",
"next_refresh" : "Prochaine mise à jour", "next_refresh" : "Prochaine mise à jour",
"queued_for_refresh" : "En file d'attente", "queued_for_refresh" : "En file d'attente",
"feed_url" : "URL du flux", "feed_url" : "URL du flux",
"generate_api_key_first" : "Générez une clé API dans votre profil d'abord.", "generate_api_key_first" : "Générez d'abord une clé API dans votre profil.",
"unsubscribe" : "Se désabonner", "unsubscribe" : "Se désabonner",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "Êtes-vous sûr de vouloir vous désabonner de de flux ? ",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "Êtes-vous sûr de vouloir supprimer cette catégorie ? ",
"category_details" : "Détails de la catégorie", "category_details" : "Détails de la catégorie",
"tag_details" : "Tag details ", "tag_details" : "Détails du tag",
"parent_category" : "Catégorie parente" "parent_category" : "Catégorie parente"
}, },
"profile" : { "profile" : {
@@ -116,15 +117,15 @@
"api_key" : "Clé API", "api_key" : "Clé API",
"api_key_not_generated" : "Pas encore générée", "api_key_not_generated" : "Pas encore générée",
"generate_new_api_key" : "Générer une nouvelle clé API", "generate_new_api_key" : "Générer une nouvelle clé API",
"generate_new_api_key_info" : "Changer de mot de passe va générer une nouvelle clé API", "generate_new_api_key_info" : "Changer de mot de passe générera une nouvelle clé API",
"opml_export" : "Export du fichier OPML", "opml_export" : "Export du fichier OPML",
"delete_account" : "Effacer le compte", "delete_account" : "Effacer le compte",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?"
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
"value" : "API REST", "value" : "API REST",
"line1" : "CommaFeed utilise JAX-RS et AngularJS, donc une API REST est disponible.", "line1" : "CommaFeed utilise JAX-RS et AngularJS, une API REST est donc disponible.",
"link_to_documentation" : "Lien vers la documentation." "link_to_documentation" : "Lien vers la documentation."
}, },
"keyboard_shortcuts" : "Raccourcis clavier", "keyboard_shortcuts" : "Raccourcis clavier",
@@ -145,7 +146,7 @@
"subscribe_bookmarklet" : "Bookmarklet d'ajout d'abonnement", "subscribe_bookmarklet" : "Bookmarklet d'ajout d'abonnement",
"subscribe_bookmarklet_asc" : "Du plus ancien au plus récent", "subscribe_bookmarklet_asc" : "Du plus ancien au plus récent",
"subscribe_bookmarklet_desc" : "Du plus récent au plus ancien", "subscribe_bookmarklet_desc" : "Du plus récent au plus ancien",
"next_unread_bookmarklet" : "Bookmarklet vers le prochain article non-lu" "next_unread_bookmarklet" : "Bookmarklet vers le prochain article non lu"
}, },
"translation" : { "translation" : {
"value" : "Traduction", "value" : "Traduction",
@@ -165,9 +166,9 @@
"open_previous_feed" : "Sélectionner le flux ou la catégorie précédente", "open_previous_feed" : "Sélectionner le flux ou la catégorie précédente",
"open_close_current_entry" : "Ouvrir/fermer l'article courant", "open_close_current_entry" : "Ouvrir/fermer l'article courant",
"open_current_entry_in_new_window" : "Ouvrir l'article courant dans une nouvelle fenêtre", "open_current_entry_in_new_window" : "Ouvrir l'article courant dans une nouvelle fenêtre",
"open_current_entry_in_new_window_background" : "Ouvrir l'article courant dans une nouvelle fenêtre en arrière plan", "open_current_entry_in_new_window_background" : "Ouvrir l'article courant dans une nouvelle fenêtre en arrière-plan",
"star_unstar" : "Ajouter/enlever l'article courant des favoris", "star_unstar" : "Ajouter/enlever l'article courant des favoris",
"mark_current_entry" : "Marquer comme lue/non-lue l'article courant", "mark_current_entry" : "Marquer comme lu/non lu l'article courant",
"mark_all_as_read" : "Marquer tous les articles comme lus", "mark_all_as_read" : "Marquer tous les articles comme lus",
"open_in_new_tab_mark_as_read" : "Ouvrir l'article courant dans une nouvelle fenêtre et marquer comme lu", "open_in_new_tab_mark_as_read" : "Ouvrir l'article courant dans une nouvelle fenêtre et marquer comme lu",
"fullscreen" : "Activer/désactiver le mode plein-écran", "fullscreen" : "Activer/désactiver le mode plein-écran",
@@ -177,4 +178,4 @@
"feed_search" : "Naviguer vers un flux en entrant son nom" "feed_search" : "Naviguer vers un flux en entrant son nom"
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"refresh" : "Actualizar", "refresh" : "Actualizar",
"refresh_all" : "Forzar a actualización de todas as fontes ", "refresh_all" : "Forzar a actualización de todas as fontes ",
"sort_by_asc_desc" : "Ordenar por data asc/desc", "sort_by_asc_desc" : "Ordenar por data asc/desc",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Só títulos", "titles_only" : "Só títulos",
"expanded_view" : "Vista expandida", "expanded_view" : "Vista expandida",
"mark_all_as_read" : "Marcar todos como lidos", "mark_all_as_read" : "Marcar todos como lidos",

View File

@@ -41,6 +41,7 @@
"refresh" : "واج‌أری", "refresh" : "واج‌أری",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "تاریخˇ سر دچئن", "sort_by_asc_desc" : "تاریخˇ سر دچئن",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "خالی تیتران", "titles_only" : "خالی تیتران",
"expanded_view" : "واشاده نما", "expanded_view" : "واشاده نما",
"mark_all_as_read" : "همه‌ته مطالبه چاکون بخانده", "mark_all_as_read" : "همه‌ته مطالبه چاکون بخانده",

View File

@@ -41,6 +41,7 @@
"refresh" : "Frissítés", "refresh" : "Frissítés",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Rendezés időrend szerint", "sort_by_asc_desc" : "Rendezés időrend szerint",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Csak cím", "titles_only" : "Csak cím",
"expanded_view" : "Részletes nézet", "expanded_view" : "Részletes nézet",
"mark_all_as_read" : "Az összes megjelölése olvasottként", "mark_all_as_read" : "Az összes megjelölése olvasottként",

183
src/main/app/i18n/id.js Normal file
View File

@@ -0,0 +1,183 @@
{
"global" : {
"save" : "Simpan",
"cancel" : "Batal",
"delete" : "Hapus",
"required" : "Diminta",
"download" : "Unduh",
"link" : "Tautan",
"bookmark" : "Penanda halaman buku",
"close" : "Tutup",
"tags" : "Penanda"
},
"tree" : {
"subscribe" : "Berlangganan",
"import" : "Impor",
"new_category" : "Kategori Baru",
"all" : "Semua",
"starred" : "Diutamakan"
},
"subscribe" : {
"feed_url" : "Umpan URL",
"feed_name" : "Nama Umpan",
"category" : "Kategori"
},
"import" : {
"google_reader_prefix" : "Izinkan saya mengimpor umpan Anda dari kepunyaan Anda ",
"google_reader_suffix" : " akun.",
"google_download" : "Atau, unggah berkas subscriptions.xml Anda.",
"google_download_link" : "Unggah dari sini.",
"xml_file" : "Berkas OPML"
},
"new_category" : {
"name" : "Nama",
"parent" : "Induk"
},
"toolbar" : {
"unread" : "Belum dibaca",
"all" : "Semua",
"previous_entry" : "Catatan sebelumnya",
"next_entry" : "Catatan selanjutnya",
"refresh" : "Segarkan",
"refresh_all" : "Memaksa menyegarkan semua umpan saya",
"sort_by_asc_desc" : "Urutkan menurut tanggal asc/desc",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Hanya Judul",
"expanded_view" : "Penglihatan diperluas",
"mark_all_as_read" : "Tandai semua sebagai telah dibaca",
"mark_all_older_12_hours" : "Butir lebih lama 12 jam",
"mark_all_older_day" : "Butir lebih lama sehari",
"mark_all_older_week" : "Butir lebih lama seminggu",
"mark_all_older_two_weeks" : "Butir lebih lama dua minggu",
"settings" : "Pengaturan",
"profile" : "Profil",
"admin" : "Admin",
"about" : "Tentang",
"logout" : "Keluar",
"donate" : "Donasi"
},
"view" : {
"entry_source" : "dari ",
"entry_author" : "oleh ",
"error_while_loading_feed" : "Galat saat memuat umpan ini",
"keep_unread" : "Tetapkan belum dibaca",
"no_unread_items" : "Tidak ada yang butir yang belum dibaca.",
"mark_up_to_here" : "Tandai sebagai dibaca di sini",
"search_for" : "mencari: ",
"no_search_results" : "Tidak ditemukan kata kunci yang sesuai dengan permintaan"
},
"feedsearch" : {
"hint" : "Ketik sebuah langganan...",
"help" : "Gunakan tombol enter untuk memilih dan tombol panah untuk navigasi.",
"result_prefix" : "Langganan Anda:"
},
"settings" : {
"general" : {
"value" : "Umum",
"language" : "Bahasa",
"language_contribute" : "Kontribusi dengan terjemahan",
"show_unread" : "Menampilkan umpan dan kategori tanpa catatan belum dibaca",
"social_buttons" : "Menampilkan tombol sosial berbagi",
"scroll_marks" : "Di penglihatan luas, menggulir melalui catatan menandakan sebagai telah dibaca"
},
"appearance" : "Penampilan",
"scroll_speed" : "Kecepatan menggulir ketika menavigasi antar catatan (dalam milidetik)",
"scroll_speed_help" : "setel ke 0 untuk menonaktifkan",
"theme" : "Tema",
"submit_your_theme" : "Mengajukan tema Anda",
"custom_css" : "Ubah CSS"
},
"details" : {
"feed_details" : "Rincian Umpan",
"url" : "URL",
"website" : "Situs",
"name" : "Nama",
"category" : "Kategori",
"position" : "Posisi",
"last_refresh" : "Penyegaran terakhir",
"message" : "Menyegarkan pesan terakhir",
"next_refresh" : "Penyegaran selanjutnya",
"queued_for_refresh" : "Antri untuk penyegaran",
"feed_url" : "Umpan URL",
"filtering_expression" : "Penyaring ekspresi",
"filtering_expression_help" : "Jika tidak kosong, sebuah ekspresi mengevaluasi ke 'benar' atau 'salah'. Jika salah, catatan baru untuk umpan ini akan ditandai sebagai telah dibaca secara otomatis. \nVariabel yang tersedia adalah 'judul', 'konten', 'url' 'penulis' dan 'kategori' dan konten mereka dikonversi dari huruf kecil ke perbandingan string yang mudah. \nContoh: url.contains('youtube') or (author eq 'athou' and title.contains('github'). \nSintaksis lengkap tersedia di <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>here</a>.",
"generate_api_key_first" : "Menghasilkan sebuah kunci API di profil Anda terlebih dahulu.",
"unsubscribe" : "Berhenti berlangganan",
"unsubscribe_confirmation" : "Apakah Anda yakin ingin berhenti berlangganan dari umpan ini?",
"delete_category_confirmation" : "Apakah Anda yakin ingin menghapus dari kategori ini?",
"category_details" : "rincian Kategori",
"tag_details" : "rincian Penanda",
"parent_category" : "kategori Induk"
},
"profile" : {
"user_name" : "nama Pengguna",
"email" : "Surel",
"change_password" : "Ganti kata sandi",
"confirm_password" : "Konfirmasi kata sandi",
"minimum_6_chars" : "Minimal 6 karakter",
"passwords_do_not_match" : "Kata sandi tidak sesuai",
"api_key" : "kunci API",
"api_key_not_generated" : "Belum menghasilkan",
"generate_new_api_key" : "Hasilkan kunci API baru",
"generate_new_api_key_info" : "Mengganti kata sandi akan menghasilkan sebuah kunci API baru",
"opml_export" : "ekspor OPML",
"delete_account" : "Hapus akun",
"delete_account_confirmation" : "Hapus akun Anda? Hal ini tidak dapat dikembalikan!"
},
"about" : {
"rest_api" : {
"value" : "REST API",
"line1" : "CommaFeed dibangun di atas JAX-RS dan AngularJS. Dengan demikian, tersedia sebuah REST API.",
"link_to_documentation" : "Tautan menuju dokumentasi."
},
"keyboard_shortcuts" : "pintasan Papanketik",
"version" : "versi CommaFeed",
"line1_prefix" : "CommaFeed adalah suatu proyek open-source. Sumber di ",
"line1_suffix" : ".",
"line2_prefix" : "Jika Anda mengalami sebuah isu, silahkan laporkan pada halaman isu ",
"line2_suffix" : " proyek.",
"line3" : "Jika Anda menyukai proyek ini, silahkan mempertimbangkan suatu donasi untuk mendukung pengembang dan membantu menutupi biaya online situs ini.",
"line4" : "Untuk Anda yang lebih suka bitcoin, alamatnya di sini",
"goodies" : {
"value" : "Bingkisan",
"android_app" : "Android app",
"subscribe_url" : "URL Langganan",
"chrome_extension" : "ekstensi Chrome",
"firefox_extension" : "ekstensi Firefox",
"opera_extension" : "ekstensi Opera",
"subscribe_bookmarklet" : "Tambahkan bookmarklet langganan(klik)",
"subscribe_bookmarklet_asc" : "Terlama dahulu",
"subscribe_bookmarklet_desc" : "Terbaru dahulu",
"next_unread_bookmarklet" : "Butir bookmarklet selanjutnya yang belum dibaca (seret ke batang penanda halaman buku)"
},
"translation" : {
"value" : "Terjemahan",
"message" : "Kami membutuhkan bantuan Anda untuk menterjemahkan CommaFeed.",
"link" : "Lihat bagaimana berkontribusi dengan terjemahan."
},
"announcements" : "Pengumuman",
"shortcuts" : {
"mouse_middleclick" : "klik tengah tetikus",
"open_next_entry" : "buka catatan selanjutnya",
"open_previous_entry" : "buka catatan sebelumnya",
"spacebar" : "spasi/shift+spasi",
"move_page_down_up" : "pindah halaman bawah/atas",
"focus_next_entry" : "setel fokus pada catatan selanjutnya tanpa membukanya",
"focus_previous_entry" : "setel fokus pada catatan sebelumnya tanpa membukanya",
"open_next_feed" : "buka umpan atau kategori selanjutnya",
"open_previous_feed" : "buka umpan atau kategori sebelumnya",
"open_close_current_entry" : "buka/tutup catatan saat ini",
"open_current_entry_in_new_window" : "buka catatan saat ini di sebuah jendela baru",
"open_current_entry_in_new_window_background" : "buka catatan saat ini di sebuah jendela baru pada latar",
"star_unstar" : "tanda bintang/tidak catatan saat ini",
"mark_current_entry" : "tandai sebagai telah dibaca/belum catatan saat ini",
"mark_all_as_read" : "tandai semua catatan sebagai telah dibaca",
"open_in_new_tab_mark_as_read" : "buka catatan di tab baru dan tandai sebagai telah dibaca",
"fullscreen" : "beralih modus layar penuh",
"font_size" : "tingkatkan/turunkan ukuran huruf dari catatan saat ini",
"go_to_all" : "menuju ke lihat Semua",
"go_to_starred" : "menuju ke lihat Tanda Bintang",
"feed_search" : "navigasi ke langganan dengan memasukkan nama langganan"
}
}
}

View File

@@ -1,33 +1,33 @@
{ {
"global" : { "global" : {
"save" : "Salva", "save" : "Salva",
"cancel" : "Cancella", "cancel" : "Annulla",
"delete" : "Elimina", "delete" : "Elimina",
"required" : "Richiesto", "required" : "Richiesto",
"download" : "Download", "download" : "Download",
"link" : "Link", "link" : "Link",
"bookmark" : "Segnalibro", "bookmark" : "Segnalibro",
"close" : "Chiudi", "close" : "Chiudi",
"tags" : "Etichette " "tags" : "Tag"
}, },
"tree" : { "tree" : {
"subscribe" : "Abbonati", "subscribe" : "Iscriviti",
"import" : "Importa", "import" : "Importa",
"new_category" : "Nuova categoria", "new_category" : "Nuova categoria",
"all" : "Tutto", "all" : "Tutti",
"starred" : "Preferiti" "starred" : "Preferiti"
}, },
"subscribe" : { "subscribe" : {
"feed_url" : "Feed URL", "feed_url" : "URL feed",
"feed_name" : "Nome feed", "feed_name" : "Nome feed",
"category" : "Categoria" "category" : "Categoria"
}, },
"import" : { "import" : {
"google_reader_prefix" : "Permettimi di importare i tuoi feed dal tuo ", "google_reader_prefix" : "Permettimi di importare i feed dal tuo account ",
"google_reader_suffix" : " account.", "google_reader_suffix" : ".",
"google_download" : "Oppure, carica il tuo file subscriptions.xml.", "google_download" : "Oppure carica il tuo file subscriptions.xml.",
"google_download_link" : "Scaricalo da qui.", "google_download_link" : "Puoi scaricalo da qui.",
"xml_file" : "OPML File" "xml_file" : "File OPML"
}, },
"new_category" : { "new_category" : {
"name" : "Nome", "name" : "Nome",
@@ -38,12 +38,13 @@
"all" : "Tutti", "all" : "Tutti",
"previous_entry" : "Precedente", "previous_entry" : "Precedente",
"next_entry" : "Successivo", "next_entry" : "Successivo",
"refresh" : "Ricarica", "refresh" : "Aggiorna",
"refresh_all" : "Forza l'aggiornamento di tutte i miei feed", "refresh_all" : "Forza l'aggiornamento di tutti i feed",
"sort_by_asc_desc" : "Ordina per data ascendente/decrescente", "sort_by_asc_desc" : "Ordina per data crescente/decrescente",
"sort_by_abc_zyx" : "Ordina alfabeticamente",
"titles_only" : "Solo i titoli", "titles_only" : "Solo i titoli",
"expanded_view" : "Espandi", "expanded_view" : "Espandi",
"mark_all_as_read" : "Segna tutto come già letto", "mark_all_as_read" : "Segna tutti come già letti",
"mark_all_older_12_hours" : "Elementi più vecchi di 12 ore", "mark_all_older_12_hours" : "Elementi più vecchi di 12 ore",
"mark_all_older_day" : "Elementi più vecchi di un giorno", "mark_all_older_day" : "Elementi più vecchi di un giorno",
"mark_all_older_week" : "Elementi più vecchi di una settimana", "mark_all_older_week" : "Elementi più vecchi di una settimana",
@@ -56,56 +57,56 @@
"donate" : "Dona" "donate" : "Dona"
}, },
"view" : { "view" : {
"entry_source" : "da ", "entry_source" : "da ",
"entry_author" : "di ", "entry_author" : "di ",
"error_while_loading_feed" : "Si è verificato un errore durante il caricamento di questo feed", "error_while_loading_feed" : "Si è verificato un errore durante il caricamento del feed",
"keep_unread" : "Mantiene come non leggere", "keep_unread" : "Mantieni come da leggere",
"no_unread_items" : "Non ci sono elementi da leggere.", "no_unread_items" : "non contiene elementi da leggere",
"mark_up_to_here" : "Segna come letto fino qui", "mark_up_to_here" : "Segna come letto fin qui",
"search_for" : "cercando: ", "search_for" : "Cerca: ",
"no_search_results" : "Nessun risultato trovato per le parole chiave cercate" "no_search_results" : "Nessun risultato per le parole chiave cercate"
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Digita in una sottoscrizione... ", "hint" : "Digita il nome di una sottoscrizione... ",
"help" : "Usa il tasto invio per selezionare e le frecce per navigare.", "help" : "Usa il tasto Invio per selezionare e le frecce per navigare.",
"result_prefix" : "Le tue sottoscrizioni:" "result_prefix" : "Le tue sottoscrizioni:"
}, },
"settings" : { "settings" : {
"general" : { "general" : {
"value" : "Generali", "value" : "Generali",
"language" : "Lingua", "language" : "Lingua",
"language_contribute" : "Contribuisci nelle traduzioni", "language_contribute" : "Contribuisci alle traduzioni",
"show_unread" : "Mostra i feed e le categorie con elementi non letti", "show_unread" : "Mostra i feed e le categorie con voci non lette",
"social_buttons" : "Mostra i pulsanti social network di condivisione", "social_buttons" : "Mostra i pulsanti di condivisione social",
"scroll_marks" : "In modalità estesa, segna come letto le voci quando scorri" "scroll_marks" : "In vista estesa, segna come lette le voci che scorri"
}, },
"appearance" : "Aspetto", "appearance" : "Aspetto",
"scroll_speed" : "Velocità dello scorrimento durante la navigazione tra i feed (in millisecondi) ", "scroll_speed" : "Velocità di scorrimento quando navighi tra i feed (in millisecondi)",
"scroll_speed_help" : "Imposta 0 per disabilitare", "scroll_speed_help" : "imposta su 0 per disabilitare",
"theme" : "Tema", "theme" : "Tema",
"submit_your_theme" : "Proponi il tuo tema", "submit_your_theme" : "Inserisci il tuo tema",
"custom_css" : "CSS personalizzato" "custom_css" : "CSS personalizzato"
}, },
"details" : { "details" : {
"feed_details" : "Dettagli feed", "feed_details" : "Dettagli feed",
"url" : "URL ", "url" : "URL",
"website" : "Sito Web", "website" : "Sito web",
"name" : "Nome", "name" : "Nome",
"category" : "Categoria", "category" : "Categoria",
"position" : "Posizione", "position" : "Posizione",
"last_refresh" : "Ultimo aggiornamento", "last_refresh" : "Ultimo aggiornamento",
"message" : "Ultimo messaggio di aggiornamento", "message" : "Ultimo messaggio di aggiornamento",
"next_refresh" : "Prossimo aggiornamento", "next_refresh" : "Prossimo aggiornamento",
"queued_for_refresh" : "In attesa per l'aggiornamento", "queued_for_refresh" : "In coda per l'aggiornamento",
"feed_url" : "URL del feed ", "feed_url" : "URL feed",
"filtering_expression" : "Espressione del filtro", "filtering_expression" : "Espressione filtro",
"filtering_expression_help" : "Se non è vuoto, una espressione viene misurata in 'true' o 'false'. Se falsa, i nuovi elementi di questo feed verranno segnati automaticamente come letti.\nLe variabili accettate sono 'title', 'content', 'url' 'author' e 'categories' e il loro contenuto è convertito in minuscolo per una facile confronto di stringhe.\Esempio: url.contains('youtube') o (author eq 'athou' and title.contains('github').\nLa sintassi completa è disponibile <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>qui</a>.", "filtering_expression_help" : "Quando non è vuota, l'espressione viene applicata a ogni nuovo elemento e valutata come 'vera' o 'falsa'. Se falsa, l'elemento verrà segnato automaticamente come letto.\nLe variabili accettate sono 'title' (titolo), 'content' (contenuto), 'url', 'author' (autore) e 'categories' (categorie); il loro contenuto è convertito in minuscolo per facilitarne il confronto.\nEsempio: url.contains('youtube') or (author eq 'athou' and title.contains('github')).\nLa sintassi completa è disponibile <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>qui</a> (in inglese).",
"generate_api_key_first" : "Genera prima una chiave API nelle impostazioni del tuo profilo.", "generate_api_key_first" : "Genera prima una chiave API nelle impostazioni del tuo profilo.",
"unsubscribe" : "Annulla la sottoscrizione", "unsubscribe" : "Disiscriviti",
"unsubscribe_confirmation" : "Sei sicuro di voler annullare la sottoscrizione da questo feed?", "unsubscribe_confirmation" : "Sei sicuro di voler annullare la sottoscrizione al feed?",
"delete_category_confirmation" : "Sei sicuro di voler eliminare questa categoria?", "delete_category_confirmation" : "Sei sicuro di voler eliminare questa categoria?",
"category_details" : "Dettagli categoria", "category_details" : "Dettagli categoria",
"tag_details" : "Dettagli etichette ", "tag_details" : "Dettagli tag",
"parent_category" : "Categoria principale" "parent_category" : "Categoria principale"
}, },
"profile" : { "profile" : {
@@ -117,66 +118,67 @@
"passwords_do_not_match" : "Le password non corrispondono", "passwords_do_not_match" : "Le password non corrispondono",
"api_key" : "chiave API", "api_key" : "chiave API",
"api_key_not_generated" : "Non ancora generata", "api_key_not_generated" : "Non ancora generata",
"generate_new_api_key" : "Genera una nuova chiave API ", "generate_new_api_key" : "Genera una nuova chiave API",
"generate_new_api_key_info" : "Cambiando la password sarà generata una nuova chiave API ì", "generate_new_api_key_info" : "Cambiando la password sarà generata una nuova chiave API",
"opml_export" : "Esporta OPML", "opml_export" : "Esporta OPML",
"delete_account" : "Elimina il profilo", "delete_account" : "Elimina account",
"delete_account_confirmation" : "Eliminare il tuo profilo? Non si può tornare indietro!" "delete_account_confirmation" : "Vuoi eliminare il tuo account? Non si può tornare indietro!"
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
"value" : "REST API", "value" : "REST API",
"line1" : "CommaFeed è costruito sopra JAX-RS e AngularJS. Ed ovviamente, una REST API è disponibile.", "line1" : "CommaFeed è basato su JAX-RS e AngularJS. Pertanto è disponibile una REST API.",
"link_to_documentation" : "Collegamento alla documentazione." "link_to_documentation" : "Link alla documentazione."
}, },
"keyboard_shortcuts" : "Scorciatoie da tastiera", "keyboard_shortcuts" : "Scorciatoie da tastiera",
"version" : "Versione di CommaFeed", "version" : "Versione di CommaFeed",
"line1_prefix" : "CommaFeed è un progetto open source. I codici sono ospitati su ", "line1_prefix" : "CommaFeed è un progetto open source. Trovi i sorgenti su ",
"line1_suffix" : ".", "line1_suffix" : ".",
"line2_prefix" : "Se hai qualche problema, segnalalo sulla pagina del ", "line2_prefix" : "Se hai qualche problema, segnalalo sulla pagina del progetto ",
"line2_suffix" : " progetto.", "line2_suffix" : ".",
"line3" : "Se ti piace il progetto, considera una donazione per supportare lo sviluppatore ed a aiutare per coprire i costi di mantenenimento di questo sito online.", "line3" : "Se ti piace questo progetto, considera una donazione per supportare lo sviluppatore e aiutare a coprire i costi di manutenzione di questo sito.",
"line4" : "Se preferisci i Bitcoin, questo è l'indirizzo", "line4" : "Se preferisci Bitcoin, questo è l'indirizzo",
"goodies" : { "goodies" : {
"value" : "Goodies", "value" : "Cose che potrebbero interessarti",
"android_app" : "Applicazione Android", "android_app" : "Applicazione Android",
"subscribe_url" : "Sottoscrivi URL", "subscribe_url" : "Sottoscrivi URL",
"chrome_extension" : "Estensione per Chrome", "chrome_extension" : "Estensione per Chrome",
"firefox_extension" : "Estensione per Firefox", "firefox_extension" : "Estensione per Firefox",
"opera_extension" : "Estensione per Opera", "opera_extension" : "Estensione per Opera",
"subscribe_bookmarklet" : "Aggiungi la sottoscrizione ai segnalibri (clicca)", "subscribe_bookmarklet" : "Aggiungi la sottoscrizione ai segnalibri (clicca)",
"subscribe_bookmarklet_asc" : "I più vecchi prima", "subscribe_bookmarklet_asc" : "Prima i vecchi",
"subscribe_bookmarklet_desc" : "I più nuovi prima", "subscribe_bookmarklet_desc" : "Prima i recenti",
"next_unread_bookmarklet" : "Prossimo elemento non letto nei segnalibri (trascinali nella barra dei segnalibri)" "next_unread_bookmarklet" : "Segnalibro al prossimo elemento da leggere (trascinalo nella barra dei segnalibri)"
}, },
"translation" : { "translation" : {
"value" : "Traduzioni", "value" : "Traduzioni",
"message" : "Abbiamo bisogno del tuo aiuto per tradurre CommaFeed.", "message" : "Abbiamo bisogno del tuo aiuto per tradurre CommaFeed.",
"link" : "Vedi come aiutarci nella traduzioni." "link" : "Scopri come aiutarci nelle traduzioni."
}, },
"announcements" : "Annunci", "announcements" : "Annunci",
"shortcuts" : { "shortcuts" : {
"mouse_middleclick" : "click centrale del mouse", "mouse_middleclick" : "click centrale del mouse",
"open_next_entry" : "apri l'elemento successivo", "open_next_entry" : "apri successivo",
"open_previous_entry" : "apri l'elemento precedente", "open_previous_entry" : "apri precedente",
"spacebar" : "spazio/shift+spazio", "spacebar" : "SPAZIO/MAIUSC+SPAZIO",
"move_page_down_up" : "muovi la pagina sopra/sotto", "move_page_down_up" : "muove la pagina in su/giù",
"focus_next_entry" : "imposta il fuoco sull'elemento successivo senza aprirlo", "focus_next_entry" : "metti a fuoco l'elemento successivo senza aprirlo",
"focus_previous_entry" : "imposta il fuoco sull'elemento precedente senza aprirlo", "focus_previous_entry" : "metti a fuoco l'elemento precedente senza aprirlo",
"open_next_feed" : "apri il feed successivo od una categoria", "open_next_feed" : "apri il prossimo feed o categoria",
"open_previous_feed" : "apri il feed precedente od una categoria", "open_previous_feed" : "apri il feed o la categoria precedente",
"open_close_current_entry" : "apri/chiusi la categoria corrente", "open_close_current_entry" : "apri/chiudi la voce corrente",
"open_current_entry_in_new_window" : "apri il corrente elemento in una nuova finestra", "open_current_entry_in_new_window" : "apri la voce corrente in una nuova finestra",
"open_current_entry_in_new_window_background" : "apri il corrente elemento in una nuova finestra in secondo piano", "open_current_entry_in_new_window_background" : "apri la voce corrente in una nuova finestra in secondo piano",
"star_unstar" : "segna/togli il segno all'elemento corrente", "star_unstar" : "metti/togli la tua preferenza alla voce corrente",
"mark_current_entry" : "segna come letto/non letto l'elemento corrente", "mark_current_entry" : "segna la voce corrente come letta/non letta",
"mark_all_as_read" : "segna come letti tutti gli elementi", "mark_all_as_read" : "segna tutte le voci come lette",
"open_in_new_tab_mark_as_read" : "apri l'elemento in una nuova finestra e segnala come letta", "open_in_new_tab_mark_as_read" : "apri voce in un nuovo tab e segna come letta",
"fullscreen" : "alterna la modalità a schermo intero", "fullscreen" : "commuta la modalità a schermo intero",
"font_size" : "aumenta/decrementa la grandezza del font dell'elemento corrente", "font_size" : "aumenta/decrementa la dimensione del font per la voce corrente",
"go_to_all" : "vai nella visione totale", "go_to_all" : "vai alla vista Tutti",
"go_to_starred" : "vai nella visione dei preferiti", "go_to_starred" : "vai alla vista Preferiti",
"feed_search" : "naviga in una sottoscrizione scrivendo il suo nome" "feed_search" : "raggiungi una sottoscrizione scrivendo il suo nome",
"refresh" : "aggiorna"
} }
} }
} }

View File

@@ -3,7 +3,7 @@
"save" : "保存", "save" : "保存",
"cancel" : "取り消し", "cancel" : "取り消し",
"delete" : "削除", "delete" : "削除",
"required" : "Required", "required" : "必須",
"download" : "ダウンロード", "download" : "ダウンロード",
"link" : "リンク", "link" : "リンク",
"bookmark" : "ブックマーク", "bookmark" : "ブックマーク",
@@ -15,7 +15,7 @@
"import" : "インポート", "import" : "インポート",
"new_category" : "新しいカテゴリー", "new_category" : "新しいカテゴリー",
"all" : "全て", "all" : "全て",
"starred" : "スター付" "starred" : "スター付"
}, },
"subscribe" : { "subscribe" : {
"feed_url" : "フィードURL", "feed_url" : "フィードURL",
@@ -40,7 +40,8 @@
"next_entry" : "次のエントリー", "next_entry" : "次のエントリー",
"refresh" : "更新", "refresh" : "更新",
"refresh_all" : "全てのフィードを更新", "refresh_all" : "全てのフィードを更新",
"sort_by_asc_desc" : "昇順/降順にソート", "sort_by_asc_desc" : "日時でソート",
"sort_by_abc_zyx" : "名前でソート",
"titles_only" : "タイトルのみ", "titles_only" : "タイトルのみ",
"expanded_view" : "拡張ビュー", "expanded_view" : "拡張ビュー",
"mark_all_as_read" : "全て既読にする", "mark_all_as_read" : "全て既読にする",
@@ -100,8 +101,8 @@
"feed_url" : "フィードURL", "feed_url" : "フィードURL",
"generate_api_key_first" : "最初にあなたのAPIキーを生成して下さい。", "generate_api_key_first" : "最初にあなたのAPIキーを生成して下さい。",
"unsubscribe" : "購読解除", "unsubscribe" : "購読解除",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "フィードの購読を解除してよろしいですか?",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "カテゴリーを削除してよろしいですか?",
"category_details" : "カテゴリー詳細", "category_details" : "カテゴリー詳細",
"tag_details" : "タグ詳細", "tag_details" : "タグ詳細",
"parent_category" : "親カテゴリー" "parent_category" : "親カテゴリー"
@@ -116,10 +117,10 @@
"api_key" : "APIキー", "api_key" : "APIキー",
"api_key_not_generated" : "APIキーが生成されていません", "api_key_not_generated" : "APIキーが生成されていません",
"generate_new_api_key" : "新しいAPIキーを生成", "generate_new_api_key" : "新しいAPIキーを生成",
"generate_new_api_key_info" : "パスワード変更新しいAPIキーが生成されます", "generate_new_api_key_info" : "パスワード変更すると新しいAPIキーが生成されます",
"opml_export" : "OPMLエクスポート", "opml_export" : "OPMLエクスポート",
"delete_account" : "アカウント削除", "delete_account" : "アカウント削除",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "アカウントを削除してよろしいですか? 削除すると戻すことはできません!"
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
@@ -149,10 +150,10 @@
}, },
"translation" : { "translation" : {
"value" : "翻訳", "value" : "翻訳",
"message" : "CommaFeedの翻訳に助けが必要です", "message" : "CommaFeedの翻訳にご協力ください",
"link" : "どうやって翻訳に貢献できるか見て下さい。" "link" : "翻訳にあたっての案内はこちら"
}, },
"announcements" : "Announcements", "announcements" : "お知らせ",
"shortcuts" : { "shortcuts" : {
"mouse_middleclick" : "中クリック", "mouse_middleclick" : "中クリック",
"open_next_entry" : "次のエントリーを開く", "open_next_entry" : "次のエントリーを開く",
@@ -170,7 +171,7 @@
"mark_current_entry" : "現在のエントリーを既読/未読にする", "mark_current_entry" : "現在のエントリーを既読/未読にする",
"mark_all_as_read" : "全エントリーを既読にする", "mark_all_as_read" : "全エントリーを既読にする",
"open_in_new_tab_mark_as_read" : "エントリーを既読にして新しいタブで開く", "open_in_new_tab_mark_as_read" : "エントリーを既読にして新しいタブで開く",
"fullscreen" : "フルスクリーントグル", "fullscreen" : "フルスクリーン切り替え",
"font_size" : "現在のエントリーのフォントサイズを大きく/小さくする", "font_size" : "現在のエントリーのフォントサイズを大きく/小さくする",
"go_to_all" : "All viewに変更する", "go_to_all" : "All viewに変更する",
"go_to_starred" : "スター付きviewに変更する", "go_to_starred" : "スター付きviewに変更する",

View File

@@ -4,23 +4,23 @@
"cancel" : "취소", "cancel" : "취소",
"delete" : "삭제", "delete" : "삭제",
"required" : "필수", "required" : "필수",
"download" : "Download ", "download" : "다운로드",
"link" : "Link ", "link" : "링크",
"bookmark" : "Bookmark ", "bookmark" : "북마크",
"close" : "Close ", "close" : "닫기 ",
"tags" : "Tags " "tags" : "태그 "
}, },
"tree" : { "tree" : {
"subscribe" : "구독", "subscribe" : "구독",
"import" : "임포트", "import" : "가져오기",
"new_category" : "새로운 카테고리", "new_category" : "새로운 카테고리",
"all" : "전체", "all" : "전체",
"starred" : "스타" "starred" : "중요 표시됨"
}, },
"subscribe" : { "subscribe" : {
"feed_url" : "피드 URL", "feed_url" : "피드 URL",
"feed_name" : "피드 이름", "feed_name" : "피드 이름",
"category" : "카테로기" "category" : "카테고리"
}, },
"import" : { "import" : {
"google_reader_prefix" : "당신의 Google Reader", "google_reader_prefix" : "당신의 Google Reader",
@@ -31,23 +31,24 @@
}, },
"new_category" : { "new_category" : {
"name" : "이름", "name" : "이름",
"parent" : "카테고리 주소로 가기" "parent" : "부모 카테고리"
}, },
"toolbar" : { "toolbar" : {
"unread" : "읽음", "unread" : "읽지 않음",
"all" : "전체", "all" : "전체",
"previous_entry" : "Previous entry ", "previous_entry" : "이전 항목",
"next_entry" : "Next entry ", "next_entry" : "다음 항목",
"refresh" : "리프래쉬", "refresh" : "새로고침",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "모든 피드를 강제로 새로고침",
"sort_by_asc_desc" : "Sort by date asc/desc ", "sort_by_asc_desc" : "날짜별 오름차/내림차순 정렬",
"titles_only" : "Titles only ", "sort_by_abc_zyx" : "Sort Alphabetically",
"expanded_view" : "Expanded view ", "titles_only" : "제목만 표시하기",
"mark_all_as_read" : "읽음표시", "expanded_view" : "Expanded View",
"mark_all_older_12_hours" : "Items older than 12 hours ", "mark_all_as_read" : "읽음으로 표시",
"mark_all_older_day" : "Items older than a day ", "mark_all_older_12_hours" : "12시간보다 오래된 항목",
"mark_all_older_week" : "Items older than a week ", "mark_all_older_day" : "1일보다 오래된 항목",
"mark_all_older_two_weeks" : "Items older than two weeks ", "mark_all_older_week" : "1주일보다 오래된 항목",
"mark_all_older_two_weeks" : "2주일보다 오래된 항목",
"settings" : "설정", "settings" : "설정",
"profile" : "프로필", "profile" : "프로필",
"admin" : "괸리자", "admin" : "괸리자",
@@ -57,124 +58,124 @@
}, },
"view" : { "view" : {
"entry_source" : "from ", "entry_source" : "from ",
"entry_author" : "by ", "entry_author" : "by ",
"error_while_loading_feed" : "피드로딩중 에러", "error_while_loading_feed" : "피드 로딩중 에러",
"keep_unread" : "안읽은것 저장", "keep_unread" : "항상 읽지 않음으로 표시",
"no_unread_items" : " 읽지않은 항목이 없니다.", "no_unread_items" : " 읽지 않은 항목이 없니다.",
"mark_up_to_here" : "Mark as read up to here ", "mark_up_to_here" : "이 위로 읽음으로 표시",
"search_for" : "searching for: ", "search_for" : "검색: ",
"no_search_results" : "No match found for the requested keywords " "no_search_results" : "검색 결과 없음"
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Type in a subscription... ", "hint" : "구독 이름을 입력하세요",
"help" : "Use the return key to select and arrow keys to navigate. ", "help" : "화살표 키로 이동하고 엔터 키로 선택하세요.",
"result_prefix" : "Your subscriptions: " "result_prefix" : "검색 결과:"
}, },
"settings" : { "settings" : {
"general" : { "general" : {
"value" : "일반", "value" : "일반",
"language" : "일반 언어", "language" : "언어",
"language_contribute" : "번역 도움하기", "language_contribute" : "번역에 기여하기",
"show_unread" : "안읽은 항목들이 있는 피드와 카테고리 보여주기", "show_unread" : "안 읽은 항목들이 있는 피드와 카테고리 보여주기",
"social_buttons" : "소셜미디아 버튼들 보여주기", "social_buttons" : "공유 버튼 표시하기",
"scroll_marks" : "Expanded View에서 스크롤하면 항목들을 읽음으로 저장하기" "scroll_marks" : "Expanded View에서 스크롤하면 항목들을 읽음으로 표시하기"
}, },
"appearance" : "Appearance ", "appearance" : "외관",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ", "scroll_speed" : "항목 사이를 이동할 때 스크롤 속도 (밀리초로 설정)",
"scroll_speed_help" : "set to 0 to disable ", "scroll_speed_help" : "비활성화하려면 0으로 설정하세요",
"theme" : "Theme ", "theme" : "테마",
"submit_your_theme" : "Submit your theme ", "submit_your_theme" : "새 테마 업로드",
"custom_css" : "커스 CSS" "custom_css" : "커스 CSS"
}, },
"details" : { "details" : {
"feed_details" : "피드 세", "feed_details" : "피드 세",
"url" : "유알엘", "url" : "URL",
"website" : "Website ", "website" : "웹사이트",
"name" : "이름", "name" : "이름",
"category" : "카테고리", "category" : "카테고리",
"position" : "Position ", "position" : "위치",
"last_refresh" : "마지막 리프래쉬", "last_refresh" : "마지막 새로고침",
"message" : "Last refresh message ", "message" : "마지막 새로고침 메시지",
"next_refresh" : "Next refresh ", "next_refresh" : "다음 새로고침",
"queued_for_refresh" : "Queued for refresh ", "queued_for_refresh" : "새로고침 대기중",
"feed_url" : "피드 유알엘", "feed_url" : "피드 URL",
"generate_api_key_first" : "당신의 프로필을 위해 API Key를 먼저 생성하세요.", "generate_api_key_first" : "당신의 프로필을 위해 API Key를 먼저 생성하세요.",
"unsubscribe" : "주소 삭제", "unsubscribe" : "구독 해제",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "정말 이 피드를 구독 해제하시겠습니까?",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "정말 이 카테고리를 삭제하시겠습니까?",
"category_details" : "카테고리 세", "category_details" : "카테고리 세",
"tag_details" : "Tag details ", "tag_details" : "태그 상세",
"parent_category" : "부모 카테고리" "parent_category" : "부모 카테고리"
}, },
"profile" : { "profile" : {
"user_name" : "사용자 이름", "user_name" : "사용자 이름",
"email" : "이메일", "email" : "이메일",
"change_password" : "비밀번호변경", "change_password" : "비밀번호 변경",
"confirm_password" : "비밀번호확인", "confirm_password" : "비밀번호 확인",
"minimum_6_chars" : "최소 6문자가 필요합니다.", "minimum_6_chars" : "최소 6개의 문자가 필요합니다.",
"passwords_do_not_match" : "비밀번호가 일치하지 않습니다.", "passwords_do_not_match" : "비밀번호가 일치하지 않습니다.",
"api_key" : "API key", "api_key" : "API key",
"api_key_not_generated" : "아직 API Key가 생성되지 않았습니다.", "api_key_not_generated" : "아직 API Key가 생성되지 않았습니다.",
"generate_new_api_key" : "API Key 생성하기", "generate_new_api_key" : "API Key 생성하기",
"generate_new_api_key_info" : "비밀번호를 변경하면 새로운 API Key가 생성됩니다.", "generate_new_api_key_info" : "비밀번호를 변경하면 새로운 API Key가 생성됩니다.",
"opml_export" : "OPML export ", "opml_export" : "OPML 내보내기",
"delete_account" : "프로필삭제", "delete_account" : "계정 삭제하기",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "계정을 삭제하시겠습니까? 되돌릴 수 없어요!"
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
"value" : "REST API", "value" : "REST API",
"line1" : "CommaFeed는 JAX-RS하고 AngularJS를 이용해 만들었습니다. 그렇기 때문에 REST API를 사용할수있습니다.", "line1" : "CommaFeed는 JAX-RS AngularJS를 이용해 만들었습니다. 그렇기 때문에 REST API를 사용할수있습니다.",
"link_to_documentation" : "문서 링크." "link_to_documentation" : "문서 링크."
}, },
"keyboard_shortcuts" : "단축", "keyboard_shortcuts" : "단축",
"version" : "CommaFeed version ", "version" : "CommaFeed 버전",
"line1_prefix" : "CommaFeed는 오픈 소스프로젝트입니다. 소스는", "line1_prefix" : "CommaFeed는 오픈 소스 프로젝트입니다. 소스는",
"line1_suffix" : "에 있습니다.", "line1_suffix" : "에 있습니다.",
"line2_prefix" : "문제가 발생하는 경우", "line2_prefix" : "문제가 발생하는 경우",
"line2_suffix" : " 프로젝트 문제페이지에 보고하십시.", "line2_suffix" : " 프로젝트 문제 페이지에 보고하십시.",
"line3" : "이 프로젝트를 좋아하시면 개발자를 지원하고 웹사이트 유지용비를 충당하는 데 도움이되는 기부금을 고려하시기 바랍니다.", "line3" : "이 프로젝트를 좋아하시면 개발자를 지원하고 웹사이트 유지비용을 충당하는 데 도움이 되는 기부금을 고려하시기 바랍니다.",
"line4" : "For those of you who prefer bitcoin, here is the address ", "line4" : "비트코인으로 기부하기",
"goodies" : { "goodies" : {
"value" : "Goodies", "value" : "Goodies",
"android_app" : "Android app ", "android_app" : "안드로이드 앱",
"subscribe_url" : "Subscribe URL ", "subscribe_url" : "구독 URL",
"chrome_extension" : "Chrome extension ", "chrome_extension" : "Chrome 확장 프로그램",
"firefox_extension" : "Firefox extension ", "firefox_extension" : "Firefox 확장 기능",
"opera_extension" : "Opera extension ", "opera_extension" : "Opera 확장 기능",
"subscribe_bookmarklet" : "Add subscription bookmarklet (click) ", "subscribe_bookmarklet" : "구독 북마크 추가 (클릭)",
"subscribe_bookmarklet_asc" : "Oldest first ", "subscribe_bookmarklet_asc" : "오래된 것 먼저",
"subscribe_bookmarklet_desc" : "Newest first ", "subscribe_bookmarklet_desc" : "새로운 것 먼저",
"next_unread_bookmarklet" : "Next unread item bookmarklet (drag to bookmark bar) " "next_unread_bookmarklet" : "안 읽은 항목 북마크 (북마크바에 끌기) "
}, },
"translation" : { "translation" : {
"value" : "번역", "value" : "번역",
"message" : "CommaFeed를 번역할려면 당신의 도움이 필요합니다.", "message" : "CommaFeed를 번역하는데 당신의 도움이 필요합니다.",
"link" : "번역에 기여하기" "link" : "번역에 기여하기"
}, },
"announcements" : "공지", "announcements" : "공지",
"shortcuts" : { "shortcuts" : {
"mouse_middleclick" : "마우 미들클릭", "mouse_middleclick" : "마우 미들클릭",
"open_next_entry" : "다음 항목 열기", "open_next_entry" : "다음 항목 열기",
"open_previous_entry" : "이전 항목 열기", "open_previous_entry" : "이전 항목 열기",
"spacebar" : "space/shift+space ", "spacebar" : "space/shift+space ",
"move_page_down_up" : "moves the page down/up ", "move_page_down_up" : "페이지 아래/위로 이동 ",
"focus_next_entry" : "set focus on next entry without opening it ", "focus_next_entry" : "열지 않고 다음 항목 보기",
"focus_previous_entry" : "set focus on previous entry without opening it ", "focus_previous_entry" : "열지 않고 이전 항목 보기",
"open_next_feed" : "open next feed or category ", "open_next_feed" : "다음 피드나 카테고리 열기",
"open_previous_feed" : "open previous feed or category ", "open_previous_feed" : "이전 피드나 카테고리 열기",
"open_close_current_entry" : "현재 항목 열기/닫기", "open_close_current_entry" : "현재 항목 열기/닫기",
"open_current_entry_in_new_window" : "새 창에서 현재 항목열기", "open_current_entry_in_new_window" : "새 창으로 현재 항목 열기",
"open_current_entry_in_new_window_background" : "open current entry in a new window in the background ", "open_current_entry_in_new_window_background" : "백그라운드에 새 창으로 현재 항목 열기",
"star_unstar" : "현재 항목 스타/스타제거", "star_unstar" : "현재 항목 중요 표시/중요 표시 제거",
"mark_current_entry" : "현재 항목 읽음/안읽음 표시", "mark_current_entry" : "현재 항목 읽음/안읽음 표시",
"mark_all_as_read" : "모든 항목 읽음으로 표시", "mark_all_as_read" : "모든 항목 읽음으로 표시",
"open_in_new_tab_mark_as_read" : "읽음으로 표시하고 새로운 탭에서 열기", "open_in_new_tab_mark_as_read" : "읽음으로 표시하고 새로운 탭에서 열기",
"fullscreen" : "toggle full screen mode ", "fullscreen" : "전체화면 켜기/끄기",
"font_size" : "increase/decrease font size of the current entry ", "font_size" : "현재 항목의 글꼴 크기를 크게/작게",
"go_to_all" : "go to the All view ", "go_to_all" : "모든 항목 보기",
"go_to_starred" : "go to the Starred view ", "go_to_starred" : "중요 표시한 항목 보기",
"feed_search" : "navigate to a subscription by entering the subscription name " "feed_search" : "구독 이름으로 구독 찾기"
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"refresh" : "Refresh", "refresh" : "Refresh",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Aturkan mengikut tarikh (baru/lama)", "sort_by_asc_desc" : "Aturkan mengikut tarikh (baru/lama)",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Tajuk sahaja", "titles_only" : "Tajuk sahaja",
"expanded_view" : "Wide view", "expanded_view" : "Wide view",
"mark_all_as_read" : "Tanda kesemuanya telah dibaca", "mark_all_as_read" : "Tanda kesemuanya telah dibaca",

View File

@@ -41,6 +41,7 @@
"refresh" : "Oppdater", "refresh" : "Oppdater",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Sorter etter dato ny/gammel", "sort_by_asc_desc" : "Sorter etter dato ny/gammel",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Kun titler", "titles_only" : "Kun titler",
"expanded_view" : "Utvidet visning", "expanded_view" : "Utvidet visning",
"mark_all_as_read" : "Merk alle som lest", "mark_all_as_read" : "Merk alle som lest",

View File

@@ -41,6 +41,7 @@
"refresh" : "Vernieuwen", "refresh" : "Vernieuwen",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Sorteer op datum opl/afl", "sort_by_asc_desc" : "Sorteer op datum opl/afl",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Alleen titels", "titles_only" : "Alleen titels",
"expanded_view" : "Uitgebreide weergave", "expanded_view" : "Uitgebreide weergave",
"mark_all_as_read" : "Markeer alles als gelezen", "mark_all_as_read" : "Markeer alles als gelezen",

View File

@@ -41,6 +41,7 @@
"refresh" : "Oppdater", "refresh" : "Oppdater",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Force refresh all my feeds ",
"sort_by_asc_desc" : "Sorter etter dato ny/gamal", "sort_by_asc_desc" : "Sorter etter dato ny/gamal",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Berre titlar", "titles_only" : "Berre titlar",
"expanded_view" : "Utvida visning", "expanded_view" : "Utvida visning",
"mark_all_as_read" : "Merk alle som lesne", "mark_all_as_read" : "Merk alle som lesne",

View File

@@ -39,12 +39,13 @@
"previous_entry" : "Poprzedni element", "previous_entry" : "Poprzedni element",
"next_entry" : "Następny element", "next_entry" : "Następny element",
"refresh" : "Odswież", "refresh" : "Odswież",
"refresh_all" : "Force refresh all my feeds ", "refresh_all" : "Odśwież teraz wszystkie kanały ",
"sort_by_asc_desc" : "Sortuj od najnowszego/najstarszego", "sort_by_asc_desc" : "Sortuj od najnowszego/najstarszego",
"sort_by_abc_zyx" : "Sortuj alfabetycznie",
"titles_only" : "Widok listy", "titles_only" : "Widok listy",
"expanded_view" : "Widok rozwinięty", "expanded_view" : "Widok rozwinięty",
"mark_all_as_read" : "Oznacz wszystko jako przeczytane", "mark_all_as_read" : "Oznacz wszystko jako przeczytane",
"mark_all_older_12_hours" : "Items older than 12 hours ", "mark_all_older_12_hours" : "Elementy starsze niż 12 godzin ",
"mark_all_older_day" : "Elementy starsze niż dzień", "mark_all_older_day" : "Elementy starsze niż dzień",
"mark_all_older_week" : "Elementy starsze niż tydzień", "mark_all_older_week" : "Elementy starsze niż tydzień",
"mark_all_older_two_weeks" : "Elementy starsze niż dwa tygodnie", "mark_all_older_two_weeks" : "Elementy starsze niż dwa tygodnie",
@@ -61,9 +62,9 @@
"error_while_loading_feed" : "Wystąpił błąd podczas ładowania tego kanału.", "error_while_loading_feed" : "Wystąpił błąd podczas ładowania tego kanału.",
"keep_unread" : "Pozostaw nieprzeczytane", "keep_unread" : "Pozostaw nieprzeczytane",
"no_unread_items" : " nie ma nieprzeczytanych elementów.", "no_unread_items" : " nie ma nieprzeczytanych elementów.",
"mark_up_to_here" : "Mark as read up to here ", "mark_up_to_here" : "Oznacz jako przeczytane do tego elementu ",
"search_for" : "searching for: ", "search_for" : "wyszukiwanie dla: ",
"no_search_results" : "No match found for the requested keywords " "no_search_results" : "Nie znaleziono wyników dla wyszukiwanej frazy "
}, },
"feedsearch" : { "feedsearch" : {
"hint" : "Wpisz subskrybcję...", "hint" : "Wpisz subskrybcję...",
@@ -80,8 +81,8 @@
"scroll_marks" : "W widoku rozwiniętym przewijanie oznacza elementy jako przeczytane" "scroll_marks" : "W widoku rozwiniętym przewijanie oznacza elementy jako przeczytane"
}, },
"appearance" : "Wygląd", "appearance" : "Wygląd",
"scroll_speed" : "Scrolling speed when navigating between entries (in milliseconds) ", "scroll_speed" : "Prędkość przewijania podczas nawigowania pomiędzy wpisami (w milisekundach) ",
"scroll_speed_help" : "set to 0 to disable ", "scroll_speed_help" : "ustaw na 0 by wyłączyć ",
"theme" : "Motyw", "theme" : "Motyw",
"submit_your_theme" : "Wyślij swój motyw", "submit_your_theme" : "Wyślij swój motyw",
"custom_css" : "Własny styl CSS" "custom_css" : "Własny styl CSS"
@@ -89,21 +90,23 @@
"details" : { "details" : {
"feed_details" : "Szczegóły kanału", "feed_details" : "Szczegóły kanału",
"url" : "URL", "url" : "URL",
"website" : "Website ", "website" : "Strona internetowa",
"name" : "Nazwa", "name" : "Nazwa",
"category" : "Kategoria", "category" : "Kategoria",
"position" : "Pozycja", "position" : "Pozycja",
"last_refresh" : "Ostatnio odświeżony", "last_refresh" : "Ostatnio odświeżony",
"message" : "Last refresh message ", "message" : "Ostatnia odpowiedź odświeżenia",
"next_refresh" : "Następne odświeżenie", "next_refresh" : "Następne odświeżenie",
"queued_for_refresh" : "W kolejce do odświeżenia", "queued_for_refresh" : "W kolejce do odświeżenia",
"feed_url" : "URL kanału", "feed_url" : "URL kanału",
"filtering_expression" : "Wyrażenie filtrujące",
"filtering_expression_help" : "Ustaw puste, by wyłączyć. W przeciwnym razie wyrażenie zwracające 'true' lub 'false'. Dla 'false' nowe elementy w kanale będą autmatycznie \noznaczane jako przeczytane. Dostępne zmienne to: 'title', 'content', 'url' 'author' and 'categories'. Ich zawartość jest konwertowana na małe litery \npodczas porówynywania tekstu. Przykład: url.contains('youtube') albo (author eq 'athou' and title.contains('github') \nPełna dostępna składnia jest dostępna pod <a href='http://commons.apache.org/proper/commons-jexl/reference/syntax.html' target='_blank'>tym</a> adresem.",
"generate_api_key_first" : "Najpierw wygeneruj klucz API w swoim profilu.", "generate_api_key_first" : "Najpierw wygeneruj klucz API w swoim profilu.",
"unsubscribe" : "Cofnij subskrypcje", "unsubscribe" : "Cofnij subskrypcje",
"unsubscribe_confirmation" : "Are you sure you want to unsubscribe from this feed? ", "unsubscribe_confirmation" : "Czy na pewno chcesz cofnąć sybskrypcję tego kanału? ",
"delete_category_confirmation" : "Are you sure you want to delete this category? ", "delete_category_confirmation" : "Czy na pewno chcesz usunąć tą kategor? ",
"category_details" : "Szczegóły kategorii", "category_details" : "Szczegóły kategorii",
"tag_details" : "Tag details ", "tag_details" : "Szczegóły tagu ",
"parent_category" : "Kategoria nadrzędna" "parent_category" : "Kategoria nadrzędna"
}, },
"profile" : { "profile" : {
@@ -119,7 +122,7 @@
"generate_new_api_key_info" : "Zmiana hasła spowoduje wygenerowanie nowego klucza API", "generate_new_api_key_info" : "Zmiana hasła spowoduje wygenerowanie nowego klucza API",
"opml_export" : "Eksportuj do pliku OPML", "opml_export" : "Eksportuj do pliku OPML",
"delete_account" : "Usuń konto", "delete_account" : "Usuń konto",
"delete_account_confirmation" : "Delete your acount? There's no turning back! " "delete_account_confirmation" : "Na pewno usunąć to konto? Nie można tego cofnąć! "
}, },
"about" : { "about" : {
"rest_api" : { "rest_api" : {
@@ -143,8 +146,8 @@
"firefox_extension" : "Dodatek do Firefoxa", "firefox_extension" : "Dodatek do Firefoxa",
"opera_extension" : "Dodatek do Opery", "opera_extension" : "Dodatek do Opery",
"subscribe_bookmarklet" : "Dodaj subskrybcje jako skryptozakładkę (kliknij)", "subscribe_bookmarklet" : "Dodaj subskrybcje jako skryptozakładkę (kliknij)",
"subscribe_bookmarklet_asc" : "Oldest first ", "subscribe_bookmarklet_asc" : "Najpierw najstarsze ",
"subscribe_bookmarklet_desc" : "Newest first ", "subscribe_bookmarklet_desc" : "Najpierw najnowsze ",
"next_unread_bookmarklet" : "Następny nieprzeczytany element jako skryptozakładka (przeciągnij na pasek zakładek)" "next_unread_bookmarklet" : "Następny nieprzeczytany element jako skryptozakładka (przeciągnij na pasek zakładek)"
}, },
"translation" : { "translation" : {
@@ -172,9 +175,9 @@
"open_in_new_tab_mark_as_read" : "otwórz w nowej zakładce i oznacz jako przeczytane", "open_in_new_tab_mark_as_read" : "otwórz w nowej zakładce i oznacz jako przeczytane",
"fullscreen" : "przełącz tryb pełnoekranowy", "fullscreen" : "przełącz tryb pełnoekranowy",
"font_size" : "zmień wielkość czcionki", "font_size" : "zmień wielkość czcionki",
"go_to_all" : "go to the All view ", "go_to_all" : "przejdź do widoku Wszystkich elementów ",
"go_to_starred" : "go to the Starred view ", "go_to_starred" : "przejdź do Elementów oznaczonych gwiazdką ",
"feed_search" : "przejdź do subskrybcji wpisując jej nazwę" "feed_search" : "przejdź do subskrybcji wpisując jej nazwę"
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"refresh" : "Atualizar", "refresh" : "Atualizar",
"refresh_all" : "Forçar atualização de todos os meus feeds", "refresh_all" : "Forçar atualização de todos os meus feeds",
"sort_by_asc_desc" : "Ordenar por data cresc/decres", "sort_by_asc_desc" : "Ordenar por data cresc/decres",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Somente títulos", "titles_only" : "Somente títulos",
"expanded_view" : "Modo Expandido", "expanded_view" : "Modo Expandido",
"mark_all_as_read" : "Marcar tudo como lido", "mark_all_as_read" : "Marcar tudo como lido",

View File

@@ -41,6 +41,7 @@
"refresh" : "Обновить", "refresh" : "Обновить",
"refresh_all" : "Обновить все подписки вручную", "refresh_all" : "Обновить все подписки вручную",
"sort_by_asc_desc" : "Сначала новые/старые", "sort_by_asc_desc" : "Сначала новые/старые",
"sort_by_abc_zyx" : "По алфавиту",
"titles_only" : "Только заголовки", "titles_only" : "Только заголовки",
"expanded_view" : "Развёрнутый вид", "expanded_view" : "Развёрнутый вид",
"mark_all_as_read" : "Отметить всё как прочитанное", "mark_all_as_read" : "Отметить всё как прочитанное",
@@ -177,4 +178,4 @@
"feed_search" : "перейти к подписке по названию" "feed_search" : "перейти к подписке по названию"
} }
} }
} }

View File

@@ -41,6 +41,7 @@
"refresh" : "Obnoviť", "refresh" : "Obnoviť",
"refresh_all" : "Vynútené obnovenie všetkých položiek", "refresh_all" : "Vynútené obnovenie všetkých položiek",
"sort_by_asc_desc" : "Zoradiť podľa najnovšieho/najstaršieho", "sort_by_asc_desc" : "Zoradiť podľa najnovšieho/najstaršieho",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Náhľad titulkov", "titles_only" : "Náhľad titulkov",
"expanded_view" : "Rozšírený náhľad", "expanded_view" : "Rozšírený náhľad",
"mark_all_as_read" : "Označiť všetky ako prečítané", "mark_all_as_read" : "Označiť všetky ako prečítané",

View File

@@ -41,6 +41,7 @@
"refresh" : "Uppdatera", "refresh" : "Uppdatera",
"refresh_all" : "Tvinga uppdatering av alla prenumerationer", "refresh_all" : "Tvinga uppdatering av alla prenumerationer",
"sort_by_asc_desc" : "Sortera efter datum stigande/fallande", "sort_by_asc_desc" : "Sortera efter datum stigande/fallande",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Endast titlar", "titles_only" : "Endast titlar",
"expanded_view" : "Expanderad vy", "expanded_view" : "Expanderad vy",
"mark_all_as_read" : "Markera alla som lästa", "mark_all_as_read" : "Markera alla som lästa",

View File

@@ -41,6 +41,7 @@
"refresh" : "Yenile", "refresh" : "Yenile",
"refresh_all" : "Tüm yayınları yenilemek için zorla", "refresh_all" : "Tüm yayınları yenilemek için zorla",
"sort_by_asc_desc" : "Tarihe göre sırala artan/azalan", "sort_by_asc_desc" : "Tarihe göre sırala artan/azalan",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "Sadece başlıklar", "titles_only" : "Sadece başlıklar",
"expanded_view" : "Genişletilmiş görünüm", "expanded_view" : "Genişletilmiş görünüm",
"mark_all_as_read" : "Tümünü okundu işaretle", "mark_all_as_read" : "Tümünü okundu işaretle",

View File

@@ -41,6 +41,7 @@
"refresh" : "刷新", "refresh" : "刷新",
"refresh_all" : "刷新所有订阅", "refresh_all" : "刷新所有订阅",
"sort_by_asc_desc" : "按日期升序/降序排序", "sort_by_asc_desc" : "按日期升序/降序排序",
"sort_by_abc_zyx" : "Sort Alphabetically",
"titles_only" : "仅显示标题", "titles_only" : "仅显示标题",
"expanded_view" : "显示内容", "expanded_view" : "显示内容",
"mark_all_as_read" : "标记所有为已读", "mark_all_as_read" : "标记所有为已读",

View File

@@ -5,7 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
@@ -70,6 +70,7 @@
<script type="text/javascript" src="lib/mousetrap/mousetrap.js"></script> <script type="text/javascript" src="lib/mousetrap/mousetrap.js"></script>
<script type="text/javascript" src="lib/momentjs/min/moment-with-locales.js"></script> <script type="text/javascript" src="lib/momentjs/min/moment-with-locales.js"></script>
<script type="text/javascript" src="lib/devicejs/lib/device.js"></script> <script type="text/javascript" src="lib/devicejs/lib/device.js"></script>
<script type="text/javascript" src="lib/tinycon/tinycon.js"></script>
<script type="text/javascript" src="js/controllers.js"></script> <script type="text/javascript" src="js/controllers.js"></script>
<script type="text/javascript" src="js/directives.js"></script> <script type="text/javascript" src="js/directives.js"></script>

View File

@@ -43,7 +43,7 @@ module.controller('SubscribeCtrl', ['$scope', '$location', 'FeedService', 'Categ
// 'ok', 'loading' or 'failed' // 'ok', 'loading' or 'failed'
$scope.state = 'ok'; $scope.state = 'ok';
$scope.urlChanged = function() { $scope.urlChanged = function() {
if ($scope.sub.url) { if ($scope.sub.url && $scope.state != 'loading') {
$scope.state = 'loading'; $scope.state = 'loading';
$scope.sub.title = 'Loading...'; $scope.sub.title = 'Loading...';
FeedService.fetch({ FeedService.fetch({
@@ -194,11 +194,7 @@ module.controller('CategoryTreeCtrl', [
}; };
$scope.$watch(rootUnreadCount, function(value) { $scope.$watch(rootUnreadCount, function(value) {
var label = 'CommaFeed'; Tinycon.setBubble(value);
if (value > 0) {
label = '(' + value + ') ' + label;
}
$window.document.title = label;
}); });
var mark = function(node, entry) { var mark = function(node, entry) {
@@ -531,7 +527,11 @@ module.controller('ToolbarCtrl', [
$scope.toggleOrder = function() { $scope.toggleOrder = function() {
var settings = $scope.settingsService.settings; var settings = $scope.settingsService.settings;
settings.readingOrder = settings.readingOrder == 'asc' ? 'desc' : 'asc'; settings.readingOrder = settings.readingOrder == 'desc' ? 'asc' : 'desc';
};
$scope.toggleAbcOrder = function() {
var settings = $scope.settingsService.settings;
settings.readingOrder = settings.readingOrder == 'abc' ? 'zyx' : 'abc';
}; };
$scope.toAdmin = function() { $scope.toAdmin = function() {
@@ -1057,6 +1057,7 @@ module.controller('FeedListCtrl', [
} }
}; };
// keyboard shortcuts
Mousetrap.bind('j', function(e) { Mousetrap.bind('j', function(e) {
$scope.$apply(function() { $scope.$apply(function() {
$scope.navigationMode = 'keyboard'; $scope.navigationMode = 'keyboard';

View File

@@ -360,3 +360,14 @@ module.directive('metricGauge', function() {
templateUrl : 'templates/_metrics.gauge.html' templateUrl : 'templates/_metrics.gauge.html'
}; };
}); });
module.directive('metricTimer', function() {
return {
scope : {
metric : '=',
label : '='
},
restrict : 'E',
templateUrl : 'templates/_metrics.timer.html'
};
});

View File

@@ -12,6 +12,7 @@ module.service('LangService', [function() {
'gl': 'Galician', 'gl': 'Galician',
'glk': 'گیلکی', 'glk': 'گیلکی',
'hu': 'Magyar', 'hu': 'Magyar',
'id': 'Indonesian',
'ja': '日本語', 'ja': '日本語',
'ko': '한국어', 'ko': '한국어',
'nl': 'Nederlands', 'nl': 'Nederlands',
@@ -19,7 +20,7 @@ module.service('LangService', [function() {
'nn': 'Norsk (nynorsk)', 'nn': 'Norsk (nynorsk)',
'pt': 'Português', 'pt': 'Português',
'pl': 'Polski', 'pl': 'Polski',
'ru': 'русский', 'ru': 'Русский',
'fi': 'Suomi', 'fi': 'Suomi',
'sv': 'Svenska', 'sv': 'Svenska',
'zh': '简体中文', 'zh': '简体中文',
@@ -31,4 +32,4 @@ module.service('LangService', [function() {
'cs': 'Čeština', 'cs': 'Čeština',
'ms': 'Bahasa Malaysian' 'ms': 'Bahasa Malaysian'
} }
}]); }]);

View File

@@ -1,5 +1,8 @@
#loading-bar .bar { #loading-bar .bar {
background: #2c3e50; background: #2c3e50;
height: 101px;
top: -100px;
position: fixed;
} }
#loading-bar .peg { #loading-bar .peg {
@@ -9,4 +12,4 @@
#loading-bar-spinner .spinner-icon { #loading-bar-spinner .spinner-icon {
border-top-color: #2c3e50; border-top-color: #2c3e50;
border-left-color: #2c3e50; border-left-color: #2c3e50;
} }

View File

@@ -4,14 +4,8 @@
<dt>Mean</dt> <dt>Mean</dt>
<dd>{{metric.meanRate | number:2}}</dd> <dd>{{metric.meanRate | number:2}}</dd>
<dt>1 min</dt> <dt>1/5/15 min</dt>
<dd>{{metric.oneMinuteRate | number:2}}</dd> <dd>{{metric.oneMinuteRate | number:2}} {{metric.fiveMinuteRate | number:2}} {{metric.fifteenMinuteRate | number:2}}</dd>
<dt>5 min</dt>
<dd>{{metric.fiveMinuteRate | number:2}}</dd>
<dt>15 min</dt>
<dd>{{metric.fifteenMinuteRate | number:2}}</dd>
<dt>Total</dt> <dt>Total</dt>
<dd>{{metric.count}}</dd> <dd>{{metric.count}}</dd>

View File

@@ -0,0 +1,17 @@
<div>
<span>{{label}}</span>
<dl class="dl-horizontal">
<dt>Mean</dt>
<dd>{{metric.meanRate | number:2}}</dd>
<dt>1/5/15 min</dt>
<dd>{{metric.oneMinuteRate | number:2}} {{metric.fiveMinuteRate | number:2}} {{metric.fifteenMinuteRate | number:2}}</dd>
<dt>Total</dt>
<dd>{{metric.count}}</dd>
<dt>min/max/mean (ms)</dt>
<dd>{{metric.snapshot.min/1000000 | number:0}} {{metric.snapshot.max/1000000 | number:0}} {{metric.snapshot.mean/1000000 | number:0}}</dd>
</dl>
</div>

View File

@@ -1,4 +1,7 @@
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>r</dt>
<dd>{{ 'about.shortcuts.refresh' | translate }}</dd>
<dt>j</dt> <dt>j</dt>
<dd>{{ 'about.shortcuts.open_next_entry' | translate }}</dd> <dd>{{ 'about.shortcuts.open_next_entry' | translate }}</dd>
@@ -68,4 +71,4 @@
</dt> </dt>
<dd>{{ 'about.shortcuts.feed_search' | translate }}</dd> <dd>{{ 'about.shortcuts.feed_search' | translate }}</dd>
</dl> </dl>

View File

@@ -6,14 +6,14 @@
</button> </button>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-nav">
<a type="button" class="btn btn-default" ng-click="previousEntry()" title="{{ 'toolbar.previous_entry' | translate }}"> <a type="button" class="btn btn-default" ng-click="previousEntry()" title="{{ 'toolbar.previous_entry' | translate }}">
<i class="icon-chevron-up"></i> <i class="icon-chevron-up"></i>
</a> </a>
<a type="button" class="btn btn-default" ng-click="nextEntry()" title="{{ 'toolbar.next_entry' | translate }}"> <a type="button" class="btn btn-default" ng-click="nextEntry()" title="{{ 'toolbar.next_entry' | translate }}">
<i class="icon-chevron-down"></i> <i class="icon-chevron-down"></i>
</a> </a>
<div class="btn-group"> <div class="btn-group" id="toolbar-refresh">
<a type="button" class="btn btn-default" ng-click="refresh()" title="{{ 'toolbar.refresh' | translate }}"> <a type="button" class="btn btn-default" ng-click="refresh()" title="{{ 'toolbar.refresh' | translate }}">
<i class="icon-refresh"></i> <i class="icon-refresh"></i>
</a> </a>
@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-mark-read">
<a type="button" class="btn btn-default" ng-click="markAllAsRead()" title="{{ 'toolbar.mark_all_as_read' | translate }}"> <a type="button" class="btn btn-default" ng-click="markAllAsRead()" title="{{ 'toolbar.mark_all_as_read' | translate }}">
<i class="icon-ok"></i> <i class="icon-ok"></i>
</a> </a>
@@ -57,34 +57,38 @@
</button> </button>
</div> </div>
<div class="actions btn-group"> <div class="actions btn-group" id="toolbar-read-mode">
<div ng-if="!MobileService.mobile || MobileService.rightMenu"> <div ng-if="!MobileService.mobile || MobileService.rightMenu">
<div class="btn-group read-mode"> <div class="btn-group read-mode">
<button type="button" class="btn btn-default" ng-click="settingsService.settings.readingMode = 'unread'" <button type="button" class="btn btn-default" ng-click="settingsService.settings.readingMode = 'unread'" ng-class="{'active': settingsService.settings.readingMode == 'unread'}">{{
ng-class="{'active': settingsService.settings.readingMode == 'unread'}">{{ 'toolbar.unread' | translate }}</button> 'toolbar.unread' | translate }}</button>
<button type="button" class="btn btn-default" ng-click="settingsService.settings.readingMode = 'all'" <button type="button" class="btn btn-default" ng-click="settingsService.settings.readingMode = 'all'" ng-class="{'active': settingsService.settings.readingMode == 'all'}">{{
ng-class="{'active': settingsService.settings.readingMode == 'all'}">{{ 'toolbar.all' | translate }}</button> 'toolbar.all' | translate }}</button>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-read-order">
<a type="button" class="btn btn-default" ng-click="toggleOrder()" title="{{ 'toolbar.sort_by_asc_desc' | translate }}"> <a type="button" class="btn btn-default" ng-click="toggleOrder()" title="{{ 'toolbar.sort_by_asc_desc' | translate }}"
<i ng-class="{'active' : settingsService.settings.readingOrder == 'asc' || settingsService.settings.readingOrder == 'desc'}">
ng-class="{'icon-arrow-up' : settingsService.settings.readingOrder == 'asc', 'icon-arrow-down': settingsService.settings.readingOrder == 'desc'}"></i> <i ng-class="{'icon-arrow-up' : settingsService.settings.readingOrder == 'asc', 'icon-arrow-down': settingsService.settings.readingOrder != 'asc'}"></i>
</a> </a>
<button type="button" class="btn btn-default" ng-click="toggleAbcOrder()" title="{{ 'toolbar.sort_by_abc_zyx' | translate }}"
ng-class="{'active' : settingsService.settings.readingOrder == 'abc' || settingsService.settings.readingOrder == 'zyx'}">
<i ng-class="{'icon-sort-by-alphabet' : settingsService.settings.readingOrder != 'zyx', 'icon-sort-by-alphabet-alt': settingsService.settings.readingOrder == 'zyx'}"></i>
</button>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-read-view-settings">
<a type="button" class="btn btn-default" ng-click="settingsService.settings.viewMode = 'title'" <a type="button" class="btn btn-default" ng-click="settingsService.settings.viewMode = 'title'" ng-class="{'active': settingsService.settings.viewMode == 'title'}"
ng-class="{'active': settingsService.settings.viewMode == 'title'}" title="{{ 'toolbar.titles_only' | translate }}"> title="{{ 'toolbar.titles_only' | translate }}">
<i class="icon-list"></i> <i class="icon-list"></i>
</a> </a>
<a type="button" class="btn btn-default" ng-click="settingsService.settings.viewMode = 'expanded'" <a type="button" class="btn btn-default" ng-click="settingsService.settings.viewMode = 'expanded'" ng-class="{'active': settingsService.settings.viewMode == 'expanded'}"
ng-class="{'active': settingsService.settings.viewMode == 'expanded'}" title="{{ 'toolbar.expanded_view' | translate }}"> title="{{ 'toolbar.expanded_view' | translate }}">
<i class="icon-th-list"></i> <i class="icon-th-list"></i>
</a> </a>
</div> </div>
<div class="btn-group"> <div class="btn-group" id="toolbar-settings">
<a class="btn btn-default" ng-click="toSettings()" title="{{ 'toolbar.settings' | translate }}"> <a class="btn btn-default" ng-click="toSettings()" title="{{ 'toolbar.settings' | translate }}">
<i class="icon-cog"></i> <i class="icon-cog"></i>
</a> </a>
@@ -133,4 +137,4 @@
</div> </div>
<span ng-if="!MobileService.mobile" ng-bind-html="ServerService.announcement | trustHtml"></span> <span ng-if="!MobileService.mobile" ng-bind-html="ServerService.announcement | trustHtml"></span>
</div> </div>
</div> </div>

View File

@@ -1,21 +1,28 @@
<div> <div>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedQueues.refill']" label="'Refresh queue refill rate (/sec)'"></metric-meter> <div class="col-md-6">
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed']" label="'Feed refreshed (/sec)'"></metric-meter> <metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedQueues.refill']" label="'Refresh queue refill rate (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated']" label="'Feed updated (/sec)'"></metric-meter> <metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed']" label="'Feed refreshed (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit']" label="'Entry cache hit (/sec)'"></metric-meter> <metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated']" label="'Feed updated (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss']" label="'Entry cache miss (/sec)'"></metric-meter> <metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit']" label="'Entry cache hit (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss']" label="'Entry cache miss (/sec)'"></metric-meter>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active']"
label="'Feed Updater active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending']"
label="'Feed Updater queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active']" <metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active']"
label="'Feed Updater active'"></metric-gauge> label="'Feed Worker active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending']" <metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending']"
label="'Feed Updater queued'"></metric-gauge> label="'Feed Worker queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active']" <metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.addQueue']" label="'Task Giver Add Queue'"></metric-gauge>
label="'Feed Worker active'"></metric-gauge> <metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.takeQueue']" label="'Task Giver Take Queue'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending']" <metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.giveBackQueue']" label="'Task Giver Give Back Queue'"></metric-gauge>
label="'Feed Worker queued'"></metric-gauge> </div>
<div class="col-md-6">
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.addQueue']" label="'Task Giver Add Queue'"></metric-gauge> <div ng-repeat="(name, timer) in metrics.timers">
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.takeQueue']" label="'Task Giver Take Queue'"></metric-gauge> <metric-timer metric="timer" label="name"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feed.FeedQueues.giveBackQueue']" label="'Task Giver Give Back Queue'"></metric-gauge> </div>
</div>
</div> </div>

View File

@@ -6,7 +6,7 @@
<span ng-switch-when="starred">{{ 'tree.starred' | translate }}</span> <span ng-switch-when="starred">{{ 'tree.starred' | translate }}</span>
<span ng-switch-default> <span ng-switch-default>
<span ng-hide="feedLink">{{name}}</span> <span ng-hide="feedLink">{{name}}</span>
<a ng-show="feedLink" href="{{feedLink}}" target="_blank">{{name}}</a> <a ng-show="feedLink" href="{{feedLink}}" target="_blank" rel="noreferrer">{{name}}</a>
</span> </span>
</span> </span>
<span ng-show="name"> &#187;</span> <span ng-show="name"> &#187;</span>
@@ -20,7 +20,7 @@
<div ng-repeat="entry in entries" class="entry entry-font-size-{{font_size}}" id="entry_{{entry.id}}" <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 }"> ng-class="{unread: entry.read == false, current: current==entry, open: isOpen, closed: !isOpen }">
<div class="entry-heading" ng-swipe-right="mark(entry, !entry.read)"> <div class="entry-heading" ng-swipe-right="mark(entry, !entry.read)">
<a href="{{entry.url}}" target="_blank" class="entry-heading-link" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)"> <a href="{{entry.url}}" target="_blank" rel="noreferrer" class="entry-heading-link" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)">
<span class="feed-name"> <span class="feed-name">
<span class="star" ng-mouseup="star(entry, !entry.starred, $event)"> <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>
@@ -31,7 +31,7 @@
<span class="entry-date">{{entry.date | entryDate}}</span> <span class="entry-date">{{entry.date | entryDate}}</span>
<span class="entry-name" ng-class="{shrink: true, rtl: entry.rtl}" ng-bind-html="entry.title | highlight:keywords | trustHtml"></span> <span class="entry-name" ng-class="{shrink: true, rtl: entry.rtl}" ng-bind-html="entry.title | highlight:keywords | trustHtml"></span>
</a> </a>
<a href="{{entry.url}}" target="_blank" class="entry-external-link" ng-click="mark(entry, true)"> <a href="{{entry.url}}" target="_blank" rel="noreferrer" class="entry-external-link" ng-click="mark(entry, true)">
<i class="icon-external-link"></i> <i class="icon-external-link"></i>
</a> </a>
</div> </div>
@@ -39,7 +39,7 @@
ng-mouseup="bodyClicked(entry, $event)" ng-class="{rtl: entry.rtl}"> ng-mouseup="bodyClicked(entry, $event)" ng-class="{rtl: entry.rtl}">
<div class="entry-header"> <div class="entry-header">
<div class="entry-title"> <div class="entry-title">
<a href="{{entry.url}}" target="_blank" ng-bind-html="entry.title | highlight:keywords | trustHtml"></a> <a href="{{entry.url}}" target="_blank" rel="noreferrer" ng-bind-html="entry.title | highlight:keywords | trustHtml"></a>
<div class="entry-subtitle"> <div class="entry-subtitle">
<span class="entry-source" ng-if="selectedType == 'category'"> <span class="entry-source" ng-if="selectedType == 'category'">
<span class="entry-source-prefix">{{ 'view.entry_source' | translate }}</span> <span class="entry-source-prefix">{{ 'view.entry_source' | translate }}</span>
@@ -61,8 +61,7 @@
<div class="entry-body-content"> <div class="entry-body-content">
<div ng-if="!MobileService.mobile" ng-bind-html="entry.content | iframeHttpsRewrite| highlight:keywords | trustHtml"></div> <div ng-if="!MobileService.mobile" ng-bind-html="entry.content | iframeHttpsRewrite| highlight:keywords | trustHtml"></div>
<div ng-if="MobileService.mobile" ng-bind-html="entry.content | iframeHttpsRewrite| highlight:keywords | appendImageTitles | trustHtml"></div> <div ng-if="MobileService.mobile" ng-bind-html="entry.content | iframeHttpsRewrite| highlight:keywords | appendImageTitles | trustHtml"></div>
<div class="entry-enclosure" ng-if="entry.enclosureType && (entry.enclosureUrl && entry.content && entry.content.indexOf(entry.enclosureUrl) == -1)">
<div class="entry-enclosure" ng-if="entry.enclosureType">
<video controls ng-if="entry.enclosureType && entry.enclosureType.indexOf('video') == 0"> <video controls ng-if="entry.enclosureType && entry.enclosureType.indexOf('video') == 0">
<source ng-src="{{entry.enclosureUrl | trustUrl}}" type="{{entry.enclosureType}}" /> <source ng-src="{{entry.enclosureUrl | trustUrl}}" type="{{entry.enclosureType}}" />
</video> </video>
@@ -94,7 +93,7 @@
title="Gmail" popup ng-if="settingsService.settings.gmail"> title="Gmail" popup ng-if="settingsService.settings.gmail">
<i class="icon-gmail"></i> <i class="icon-gmail"></i>
</a> </a>
<a href="http://www.facebook.com/sharer.php?u=={{entry.url|escape}}" title="Facebook" popup ng-if="settingsService.settings.facebook"> <a href="http://www.facebook.com/sharer.php?u={{entry.url|escape}}" title="Facebook" popup ng-if="settingsService.settings.facebook">
<i class="icon-facebook"></i> <i class="icon-facebook"></i>
</a> </a>
<a href="http://twitter.com/share?text={{entry.title|escape}}&url={{entry.url|escape}}" title="Twitter" popup <a href="http://twitter.com/share?text={{entry.title|escape}}&url={{entry.url|escape}}" title="Twitter" popup

View File

@@ -1,21 +1,8 @@
package com.commafeed; package com.commafeed;
import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.forms.MultiPartBundle;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.server.DefaultServerFactory;
import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -26,8 +13,6 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.server.session.SessionHandler;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import com.commafeed.backend.feed.FeedRefreshTaskGiver; import com.commafeed.backend.feed.FeedRefreshTaskGiver;
@@ -60,13 +45,21 @@ import com.commafeed.frontend.servlet.CustomCssServlet;
import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.LogoutServlet;
import com.commafeed.frontend.servlet.NextUnreadServlet; import com.commafeed.frontend.servlet.NextUnreadServlet;
import com.commafeed.frontend.session.SessionHelperFactoryProvider; import com.commafeed.frontend.session.SessionHelperFactoryProvider;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
import com.google.inject.Key; import com.google.inject.Key;
import com.google.inject.TypeLiteral; import com.google.inject.TypeLiteral;
import com.wordnik.swagger.jaxrs.config.BeanConfig;
import com.wordnik.swagger.jaxrs.listing.ApiListingResource; import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.forms.MultiPartBundle;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.migrations.MigrationsBundle;
import io.dropwizard.server.DefaultServerFactory;
import io.dropwizard.servlets.CacheBustingFilter;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
public class CommaFeedApplication extends Application<CommaFeedConfiguration> { public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
@@ -117,7 +110,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics())); Injector injector = Guice.createInjector(new CommaFeedModule(hibernateBundle.getSessionFactory(), config, environment.metrics()));
// session management // session management
environment.servlets().setSessionHandler(new SessionHandler(config.getSessionManagerFactory().build())); environment.servlets().setSessionHandler(config.getSessionHandlerFactory().build());
// support for "@SecurityCheck User user" injection // support for "@SecurityCheck User user" injection
environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class))); environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class)));
@@ -158,21 +151,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class)); environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class));
environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class)); environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class));
// Swagger
environment.jersey().register(new ApiListingResource());
environment.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
String modelsPackage = "com.commafeed.frontend.model";
String requestsPackage = "com.commafeed.frontend.model.request";
String endpointsPackage = "com.commafeed.frontend.resource";
List<String> packages = Arrays.asList(modelsPackage, requestsPackage, endpointsPackage);
BeanConfig swaggerConfig = new BeanConfig();
swaggerConfig.setTitle("CommaFeed");
swaggerConfig.setVersion("1");
swaggerConfig.setBasePath("/rest");
swaggerConfig.setResourcePackage(StringUtils.join(packages, ","));
swaggerConfig.setScan(true);
// cache configuration // cache configuration
// prevent caching on REST resources, except for favicons // prevent caching on REST resources, except for favicons
environment.servlets().addFilter("cache-filter", new CacheBustingFilter() { environment.servlets().addFilter("cache-filter", new CacheBustingFilter() {

View File

@@ -1,8 +1,5 @@
package com.commafeed; package com.commafeed;
import io.dropwizard.Configuration;
import io.dropwizard.db.DataSourceFactory;
import java.util.Date; import java.util.Date;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@@ -10,15 +7,17 @@ import javax.validation.Valid;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import lombok.Getter;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.NotBlank;
import com.commafeed.backend.cache.RedisPoolFactory; import com.commafeed.backend.cache.RedisPoolFactory;
import com.commafeed.frontend.session.SessionManagerFactory; import com.commafeed.frontend.session.SessionHandlerFactory;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.Configuration;
import io.dropwizard.db.DataSourceFactory;
import lombok.Getter;
@Getter @Getter
public class CommaFeedConfiguration extends Configuration { public class CommaFeedConfiguration extends Configuration {
@@ -45,7 +44,7 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
@NotNull @NotNull
@JsonProperty("session") @JsonProperty("session")
private SessionManagerFactory sessionManagerFactory = new SessionManagerFactory(); private SessionHandlerFactory SessionHandlerFactory = new SessionHandlerFactory();
@Valid @Valid
@NotNull @NotNull
@@ -96,6 +95,12 @@ public class CommaFeedConfiguration extends Configuration {
private String smtpPassword; private String smtpPassword;
private String smtpFromAddress; private String smtpFromAddress;
private boolean graphiteEnabled;
private String graphitePrefix;
private String graphiteHost;
private int graphitePort;
private int graphiteInterval;
@NotNull @NotNull
@Valid @Valid
private Boolean heavyLoad; private Boolean heavyLoad;
@@ -132,10 +137,11 @@ public class CommaFeedConfiguration extends Configuration {
@Valid @Valid
private CacheType cache; private CacheType cache;
@NotNull
@Valid @Valid
private String announcement; private String announcement;
private String userAgent;
public Date getUnreadThreshold() { public Date getUnreadThreshold() {
int keepStatusDays = getKeepStatusDays(); int keepStatusDays = getKeepStatusDays();
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null; return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;

View File

@@ -1,5 +1,7 @@
package com.commafeed; package com.commafeed;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -7,6 +9,10 @@ import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.graphite.Graphite;
import com.codahale.metrics.graphite.GraphiteReporter;
import com.codahale.metrics.MetricFilter;
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
import com.commafeed.CommaFeedConfiguration.CacheType; import com.commafeed.CommaFeedConfiguration.CacheType;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.cache.NoopCacheService; import com.commafeed.backend.cache.NoopCacheService;
@@ -20,6 +26,9 @@ import com.commafeed.backend.task.OldStatusesCleanupTask;
import com.commafeed.backend.task.OrphanedContentsCleanupTask; import com.commafeed.backend.task.OrphanedContentsCleanupTask;
import com.commafeed.backend.task.OrphanedFeedsCleanupTask; import com.commafeed.backend.task.OrphanedFeedsCleanupTask;
import com.commafeed.backend.task.ScheduledTask; import com.commafeed.backend.task.ScheduledTask;
import com.commafeed.backend.urlprovider.FeedURLProvider;
import com.commafeed.backend.urlprovider.InPageReferenceFeedURLProvider;
import com.commafeed.backend.urlprovider.YoutubeFeedURLProvider;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.Multibinder;
@@ -49,10 +58,36 @@ public class CommaFeedModule extends AbstractModule {
faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class); faviconMultibinder.addBinding().to(FacebookFaviconFetcher.class);
faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class); faviconMultibinder.addBinding().to(DefaultFaviconFetcher.class);
Multibinder<FeedURLProvider> urlProviderMultibinder = Multibinder.newSetBinder(binder(), FeedURLProvider.class);
urlProviderMultibinder.addBinding().to(InPageReferenceFeedURLProvider.class);
urlProviderMultibinder.addBinding().to(YoutubeFeedURLProvider.class);
Multibinder<ScheduledTask> taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class); Multibinder<ScheduledTask> taskMultibinder = Multibinder.newSetBinder(binder(), ScheduledTask.class);
taskMultibinder.addBinding().to(OldStatusesCleanupTask.class); taskMultibinder.addBinding().to(OldStatusesCleanupTask.class);
taskMultibinder.addBinding().to(OldEntriesCleanupTask.class); taskMultibinder.addBinding().to(OldEntriesCleanupTask.class);
taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class); taskMultibinder.addBinding().to(OrphanedFeedsCleanupTask.class);
taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class); taskMultibinder.addBinding().to(OrphanedContentsCleanupTask.class);
ApplicationSettings settings = config.getApplicationSettings();
if (settings.isGraphiteEnabled()) {
final String graphitePrefix = settings.getGraphitePrefix();
final String graphiteHost = settings.getGraphiteHost();
final int graphitePort = settings.getGraphitePort();
final int graphiteInterval = settings.getGraphiteInterval();
log.info("Graphite Metrics will be sent to host={}, port={}, prefix={}, interval={}sec", graphiteHost, graphitePort, graphitePrefix, graphiteInterval);
final Graphite graphite = new Graphite(new InetSocketAddress(graphiteHost, graphitePort));
final GraphiteReporter reporter = GraphiteReporter.forRegistry(metrics)
.prefixedWith(graphitePrefix)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.filter(MetricFilter.ALL)
.build(graphite);
reporter.start(graphiteInterval, TimeUnit.SECONDS);
} else {
log.info("Graphite Metrics Disabled. Metrics will not be sent.");
}
} }
} }

View File

@@ -16,7 +16,7 @@ import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
class ContentEncodingInterceptor implements HttpResponseInterceptor { class ContentEncodingInterceptor implements HttpResponseInterceptor {
private static final Set<String> ALLOWED_CONTENT_ENCODINGS = new HashSet<>(Arrays.asList("gzip", "x-gzip", "deflate", "identity")); private static final Set<String> ALLOWED_CONTENT_ENCODINGS = new HashSet<>(Arrays.asList("gzip", "x-gzip", "deflate", "identity"));
@Override @Override
@@ -28,17 +28,17 @@ class ContentEncodingInterceptor implements HttpResponseInterceptor {
} }
} }
} }
private boolean containsUnsupportedEncodings(Header contentEncodingHeader) { private boolean containsUnsupportedEncodings(Header contentEncodingHeader) {
HeaderElement[] codecs = contentEncodingHeader.getElements(); HeaderElement[] codecs = contentEncodingHeader.getElements();
for (final HeaderElement codec : codecs) { for (final HeaderElement codec : codecs) {
String codecName = codec.getName().toLowerCase(Locale.US); String codecName = codec.getName().toLowerCase(Locale.US);
if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) { if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) {
return true; return true;
} }
} }
return false; return false;
} }
@@ -47,9 +47,9 @@ class ContentEncodingInterceptor implements HttpResponseInterceptor {
@Override @Override
public Header getContentEncoding() { public Header getContentEncoding() {
return null; return null;
}; }
}; };
response.setEntity(wrapped); response.setEntity(wrapped);
} }

View File

@@ -70,7 +70,10 @@ public class HttpGetter {
@Inject @Inject
public HttpGetter(CommaFeedConfiguration config) { public HttpGetter(CommaFeedConfiguration config) {
this.userAgent = String.format("CommaFeed/%s (https://www.commafeed.com)", config.getVersion()); this.userAgent = config.getApplicationSettings().getUserAgent();
if (this.userAgent == null) {
this.userAgent = String.format("CommaFeed/%s (https://github.com/Athou/commafeed)", config.getVersion());
}
} }
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException { public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
@@ -151,9 +154,13 @@ public class HttpGetter {
contentType = entity.getContentType().getValue(); contentType = entity.getContentType().getValue();
} }
} }
HttpUriRequest req = (HttpUriRequest) context.getRequest();
HttpHost host = context.getTargetHost(); String urlAfterRedirect = url;
String urlAfterRedirect = req.getURI().isAbsolute() ? req.getURI().toString() : host.toURI() + req.getURI(); if (context.getRequest() instanceof HttpUriRequest) {
HttpUriRequest req = (HttpUriRequest) context.getRequest();
HttpHost host = context.getTargetHost();
urlAfterRedirect = req.getURI().isAbsolute() ? req.getURI().toString() : host.toURI() + req.getURI();
}
long duration = System.currentTimeMillis() - start; long duration = System.currentTimeMillis() - start;
result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect); result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect);
@@ -219,4 +226,11 @@ public class HttpGetter {
return null; return null;
} }
} }
public static void main(String[] args) throws Exception {
CommaFeedConfiguration config = new CommaFeedConfiguration();
HttpGetter getter = new HttpGetter(config);
HttpResult result = getter.getBinary("https://sourceforge.net/projects/mpv-player-windows/rss", 30000);
System.out.println(new String(result.content));
}
} }

View File

@@ -13,7 +13,7 @@ import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.QFeedCategory; import com.commafeed.backend.model.QFeedCategory;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.mysema.query.types.Predicate; import com.querydsl.core.types.Predicate;
@Singleton @Singleton
public class FeedCategoryDAO extends GenericDAO<FeedCategory> { public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
@@ -26,11 +26,11 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} }
public List<FeedCategory> findAll(User user) { public List<FeedCategory> findAll(User user) {
return newQuery().from(category).where(category.user.eq(user)).join(category.user, QUser.user).fetch().list(category); return query().selectFrom(category).where(category.user.eq(user)).join(category.user, QUser.user).fetchJoin().fetch();
} }
public FeedCategory findById(User user, Long id) { public FeedCategory findById(User user, Long id) {
return newQuery().from(category).where(category.user.eq(user), category.id.eq(id)).uniqueResult(category); return query().selectFrom(category).where(category.user.eq(user), category.id.eq(id)).fetchOne();
} }
public FeedCategory findByName(User user, String name, FeedCategory parent) { public FeedCategory findByName(User user, String name, FeedCategory parent) {
@@ -40,7 +40,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} else { } else {
parentPredicate = category.parent.eq(parent); parentPredicate = category.parent.eq(parent);
} }
return newQuery().from(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).uniqueResult(category); return query().selectFrom(category).where(category.user.eq(user), category.name.eq(name), parentPredicate).fetchOne();
} }
public List<FeedCategory> findByParent(User user, FeedCategory parent) { public List<FeedCategory> findByParent(User user, FeedCategory parent) {
@@ -50,7 +50,7 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
} else { } else {
parentPredicate = category.parent.eq(parent); parentPredicate = category.parent.eq(parent);
} }
return newQuery().from(category).where(category.user.eq(user), parentPredicate).list(category); return query().selectFrom(category).where(category.user.eq(user), parentPredicate).fetch();
} }
public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) { public List<FeedCategory> findAllChildrenCategories(User user, FeedCategory parent) {

View File

@@ -15,9 +15,8 @@ import com.commafeed.backend.model.QFeed;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.QUser; import com.commafeed.backend.model.QUser;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.mysema.query.BooleanBuilder; import com.querydsl.jpa.JPAExpressions;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.JPQLQuery;
import com.mysema.query.jpa.hibernate.HibernateSubQuery;
@Singleton @Singleton
public class FeedDAO extends GenericDAO<Feed> { public class FeedDAO extends GenericDAO<Feed> {
@@ -30,26 +29,23 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) { public List<Feed> findNextUpdatable(int count, Date lastLoginThreshold) {
BooleanBuilder disabledDatePredicate = new BooleanBuilder(); JPQLQuery<Feed> query = query().selectFrom(feed);
disabledDatePredicate.or(feed.disabledUntil.isNull()); query.where(feed.disabledUntil.isNull().or(feed.disabledUntil.lt(new Date())));
disabledDatePredicate.or(feed.disabledUntil.lt(new Date()));
HibernateQuery query = null;
if (lastLoginThreshold != null) { if (lastLoginThreshold != null) {
QFeedSubscription subs = QFeedSubscription.feedSubscription; QFeedSubscription subs = QFeedSubscription.feedSubscription;
QUser user = QUser.user; QUser user = QUser.user;
query = newQuery().from(subs);
query.join(subs.feed, feed).join(subs.user, user).where(disabledDatePredicate, user.lastLogin.gt(lastLoginThreshold)); JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(subs);
} else { subQuery.join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
query = newQuery().from(feed); query.where(subQuery.exists());
query.where(disabledDatePredicate);
} }
return query.orderBy(feed.disabledUntil.asc()).limit(count).distinct().list(feed); return query.orderBy(feed.disabledUntil.asc()).limit(count).distinct().fetch();
} }
public Feed findByUrl(String normalizedUrl) { public Feed findByUrl(String normalizedUrl) {
List<Feed> feeds = newQuery().from(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).list(feed); List<Feed> feeds = query().selectFrom(feed).where(feed.normalizedUrlHash.eq(DigestUtils.sha1Hex(normalizedUrl))).fetch();
Feed feed = Iterables.getFirst(feeds, null); Feed feed = Iterables.getFirst(feeds, null);
if (feed != null && StringUtils.equals(normalizedUrl, feed.getNormalizedUrl())) { if (feed != null && StringUtils.equals(normalizedUrl, feed.getNormalizedUrl())) {
return feed; return feed;
@@ -58,12 +54,11 @@ public class FeedDAO extends GenericDAO<Feed> {
} }
public List<Feed> findByTopic(String topic) { public List<Feed> findByTopic(String topic) {
return newQuery().from(feed).where(feed.pushTopicHash.eq(DigestUtils.sha1Hex(topic))).list(feed); return query().selectFrom(feed).where(feed.pushTopicHash.eq(DigestUtils.sha1Hex(topic))).fetch();
} }
public List<Feed> findWithoutSubscriptions(int max) { public List<Feed> findWithoutSubscriptions(int max) {
QFeedSubscription sub = QFeedSubscription.feedSubscription; QFeedSubscription sub = QFeedSubscription.feedSubscription;
return newQuery().from(feed).where(new HibernateSubQuery().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max).list(feed); return query().selectFrom(feed).where(JPAExpressions.selectOne().from(sub).where(sub.feed.eq(feed)).notExists()).limit(max).fetch();
// return newQuery().from(feed).leftJoin(feed.subscriptions, sub).where(sub.id.isNull()).limit(max).list(feed);
} }
} }

View File

@@ -10,13 +10,14 @@ import org.hibernate.SessionFactory;
import com.commafeed.backend.model.FeedEntryContent; import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
import com.commafeed.backend.model.QFeedEntryContent; import com.commafeed.backend.model.QFeedEntryContent;
import com.google.common.collect.Iterables; import com.querydsl.jpa.JPAExpressions;
import com.mysema.query.jpa.hibernate.HibernateSubQuery; import com.querydsl.jpa.JPQLQuery;
@Singleton @Singleton
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> { public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
private QFeedEntryContent content = QFeedEntryContent.feedEntryContent; private QFeedEntryContent content = QFeedEntryContent.feedEntryContent;
private QFeedEntry entry = QFeedEntry.feedEntry;
@Inject @Inject
public FeedEntryContentDAO(SessionFactory sessionFactory) { public FeedEntryContentDAO(SessionFactory sessionFactory) {
@@ -24,16 +25,14 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
} }
public Long findExisting(String contentHash, String titleHash) { public Long findExisting(String contentHash, String titleHash) {
List<Long> list = newQuery().from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash)).limit(1) return query().select(content.id).from(content).where(content.contentHash.eq(contentHash), content.titleHash.eq(titleHash))
.list(content.id); .fetchFirst();
return Iterables.getFirst(list, null);
} }
public int deleteWithoutEntries(int max) { public int deleteWithoutEntries(int max) {
QFeedEntry entry = QFeedEntry.feedEntry;
HibernateSubQuery subQuery = new HibernateSubQuery().from(entry).where(entry.content.id.eq(content.id)); JPQLQuery<Integer> subQuery = JPAExpressions.selectOne().from(entry).where(entry.content.id.eq(content.id));
List<FeedEntryContent> list = newQuery().from(content).where(subQuery.notExists()).limit(max).list(content); List<FeedEntryContent> list = query().selectFrom(content).where(subQuery.notExists()).limit(max).fetch();
int deleted = list.size(); int deleted = list.size();
delete(list); delete(list);

View File

@@ -6,18 +6,17 @@ import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.QFeedEntry; import com.commafeed.backend.model.QFeedEntry;
import com.google.common.collect.Iterables; import com.querydsl.core.Tuple;
import com.mysema.query.Tuple; import com.querydsl.core.types.dsl.NumberExpression;
import com.mysema.query.types.expr.NumberExpression;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Singleton @Singleton
public class FeedEntryDAO extends GenericDAO<FeedEntry> { public class FeedEntryDAO extends GenericDAO<FeedEntry> {
@@ -30,24 +29,25 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
} }
public Long findExisting(String guid, Feed feed) { public Long findExisting(String guid, Feed feed) {
List<Long> list = newQuery().from(entry).where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed)).limit(1) return query().select(entry.id).from(entry).where(entry.guidHash.eq(DigestUtils.sha1Hex(guid)), entry.feed.eq(feed)).limit(1)
.list(entry.id); .fetchOne();
return Iterables.getFirst(list, null);
} }
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) { public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
NumberExpression<Long> count = entry.id.count(); NumberExpression<Long> count = entry.id.count();
List<Tuple> tuples = newQuery().from(entry).groupBy(entry.feed).having(count.gt(maxCapacity)).limit(max).list(entry.feed.id, count); List<Tuple> tuples = query().select(entry.feed.id, count).from(entry).groupBy(entry.feed).having(count.gt(maxCapacity)).limit(max)
.fetch();
return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).collect(Collectors.toList()); return tuples.stream().map(t -> new FeedCapacity(t.get(entry.feed.id), t.get(count))).collect(Collectors.toList());
} }
public int delete(Long feedId, long max) { public int delete(Long feedId, long max) {
List<FeedEntry> list = newQuery().from(entry).where(entry.feed.id.eq(feedId)).limit(max).list(entry);
List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).limit(max).fetch();
return delete(list); return delete(list);
} }
public int deleteOldEntries(Long feedId, long max) { public int deleteOldEntries(Long feedId, long max) {
List<FeedEntry> list = newQuery().from(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).list(entry); List<FeedEntry> list = query().selectFrom(entry).where(entry.feed.id.eq(feedId)).orderBy(entry.updated.asc()).limit(max).fetch();
return delete(list); return delete(list);
} }

View File

@@ -17,6 +17,7 @@ import com.commafeed.backend.FixedSizeSortedSet;
import com.commafeed.backend.feed.FeedEntryKeyword; import com.commafeed.backend.feed.FeedEntryKeyword;
import com.commafeed.backend.feed.FeedEntryKeyword.Mode; import com.commafeed.backend.feed.FeedEntryKeyword.Mode;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus; import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryTag; import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
@@ -30,9 +31,9 @@ import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.model.UnreadCount; import com.commafeed.frontend.model.UnreadCount;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.mysema.query.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.mysema.query.Tuple; import com.querydsl.core.Tuple;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.impl.JPAQuery;
@Singleton @Singleton
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> { public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
@@ -62,13 +63,25 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated()); builder.append(o2.getEntryUpdated(), o1.getEntryUpdated());
builder.append(o2.getId(), o1.getId()); builder.append(o2.getId(), o1.getId());
return builder.toComparison(); return builder.toComparison();
}; }
}; };
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse(); private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ABC = new Comparator<FeedEntryStatus>() {
@Override
public int compare(FeedEntryStatus o1, FeedEntryStatus o2) {
CompareToBuilder builder = new CompareToBuilder();
builder.append(o1.getEntry().getContent().getTitle(), o2.getEntry().getContent().getTitle());
builder.append(o1.getId(), o2.getId());
return builder.toComparison();
}
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ZYX = Ordering.from(STATUS_COMPARATOR_ABC).reverse();
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) { public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
List<FeedEntryStatus> statuses = newQuery().from(status).where(status.entry.eq(entry), status.subscription.eq(sub)).list(status); List<FeedEntryStatus> statuses = query().selectFrom(status).where(status.entry.eq(entry), status.subscription.eq(sub)).fetch();
FeedEntryStatus status = Iterables.getFirst(statuses, null); FeedEntryStatus status = Iterables.getFirst(statuses, null);
return handleStatus(user, status, sub, entry); return handleStatus(user, status, sub, entry);
} }
@@ -93,24 +106,25 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) { public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
HibernateQuery query = newQuery().from(status).where(status.user.eq(user), status.starred.isTrue()); JPAQuery<FeedEntryStatus> query = query().selectFrom(status).where(status.user.eq(user), status.starred.isTrue());
if (newerThan != null) { if (newerThan != null) {
query.where(status.entryInserted.gt(newerThan)); query.where(status.entryInserted.gt(newerThan));
} }
if (order == ReadingOrder.asc) { if (order == ReadingOrder.asc) {
query.orderBy(status.entryUpdated.asc(), status.id.asc()); query.orderBy(status.entryUpdated.asc(), status.id.asc());
} else { } else if (order == ReadingOrder.desc) {
query.orderBy(status.entryUpdated.desc(), status.id.desc()); query.orderBy(status.entryUpdated.desc(), status.id.desc());
} else if (order == ReadingOrder.abc) {
query.orderBy(status.entry.content.title.asc(), status.id.desc());
} else { // order == ReadingOrder.xyz
query.orderBy(status.entry.content.title.desc(), status.id.desc());
} }
query.offset(offset).limit(limit); query.offset(offset).limit(limit);
int timeout = config.getApplicationSettings().getQueryTimeout(); setTimeout(query, config.getApplicationSettings().getQueryTimeout());
if (timeout > 0) {
query.setTimeout(timeout / 1000);
}
List<FeedEntryStatus> statuses = query.list(status); List<FeedEntryStatus> statuses = query.fetch();
for (FeedEntryStatus status : statuses) { for (FeedEntryStatus status : statuses) {
status = handleStatus(user, status, status.getSubscription(), status.getEntry()); status = handleStatus(user, status, status.getSubscription(), status.getEntry());
fetchTags(user, status); fetchTags(user, status);
@@ -118,10 +132,10 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return lazyLoadContent(includeContent, statuses); return lazyLoadContent(includeContent, statuses);
} }
private HibernateQuery buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords, Date newerThan, private JPAQuery<FeedEntry> buildQuery(User user, FeedSubscription sub, boolean unreadOnly, List<FeedEntryKeyword> keywords,
int offset, int limit, ReadingOrder order, Date last, String tag) { Date newerThan, int offset, int limit, ReadingOrder order, FeedEntryStatus last, String tag) {
HibernateQuery query = newQuery().from(entry).where(entry.feed.eq(sub.getFeed())); JPAQuery<FeedEntry> query = query().selectFrom(entry).where(entry.feed.eq(sub.getFeed()));
if (CollectionUtils.isNotEmpty(keywords)) { if (CollectionUtils.isNotEmpty(keywords)) {
query.join(entry.content, content); query.join(entry.content, content);
@@ -163,17 +177,29 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
if (last != null) { if (last != null) {
if (order == ReadingOrder.desc) { if (order == ReadingOrder.desc) {
query.where(entry.updated.gt(last)); query.where(entry.updated.gt(last.getEntryUpdated()));
} else { } else if (order == ReadingOrder.asc) {
query.where(entry.updated.lt(last)); query.where(entry.updated.lt(last.getEntryUpdated()));
} else if (order == ReadingOrder.abc) {
query.join(entry.content, content);
query.where(content.title.lt(last.getEntry().getContent().getTitle()));
} else { // order == ReadingOrder.zyx
query.join(entry.content, content);
query.where(content.title.gt(last.getEntry().getContent().getTitle()));
} }
} else if (order != null && (order == ReadingOrder.abc || order == ReadingOrder.zyx)) {
query.join(entry.content, content);
} }
if (order != null) { if (order != null) {
if (order == ReadingOrder.asc) { if (order == ReadingOrder.asc) {
query.orderBy(entry.updated.asc(), entry.id.asc()); query.orderBy(entry.updated.asc(), entry.id.asc());
} else { } else if (order == ReadingOrder.desc) {
query.orderBy(entry.updated.desc(), entry.id.desc()); query.orderBy(entry.updated.desc(), entry.id.desc());
} else if (order == ReadingOrder.abc) {
query.orderBy(content.title.asc(), entry.id.asc());
} else { // order == ReadingOrder.zyx
query.orderBy(content.title.desc(), entry.id.desc());
} }
} }
if (offset > -1) { if (offset > -1) {
@@ -182,10 +208,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
if (limit > -1) { if (limit > -1) {
query.limit(limit); query.limit(limit);
} }
int timeout = config.getApplicationSettings().getQueryTimeout(); setTimeout(query, config.getApplicationSettings().getQueryTimeout());
if (timeout > 0) {
query.setTimeout(timeout / 1000);
}
return query; return query;
} }
@@ -193,20 +216,36 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, List<FeedEntryKeyword> keywords, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent,
boolean onlyIds, String tag) { boolean onlyIds, String tag) {
int capacity = offset + limit; int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
Comparator<FeedEntryStatus> comparator;
if (order == ReadingOrder.desc) {
comparator = STATUS_COMPARATOR_DESC;
} else if (order == ReadingOrder.abc) {
comparator = STATUS_COMPARATOR_ABC;
} else if (order == ReadingOrder.zyx) {
comparator = STATUS_COMPARATOR_ZYX;
} else {
comparator = STATUS_COMPARATOR_ASC;
}
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator); FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
for (FeedSubscription sub : subs) { for (FeedSubscription sub : subs) {
Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null; FeedEntryStatus last = (order != null && set.isFull()) ? set.last() : null;
HibernateQuery query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag); JPAQuery<FeedEntry> query = buildQuery(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
List<Tuple> tuples = query.list(entry.id, entry.updated, status.id); List<Tuple> tuples = query.select(entry.id, entry.updated, status.id, entry.content.title).fetch();
for (Tuple tuple : tuples) { for (Tuple tuple : tuples) {
Long id = tuple.get(entry.id); Long id = tuple.get(entry.id);
Date updated = tuple.get(entry.updated); Date updated = tuple.get(entry.updated);
Long statusId = tuple.get(status.id); Long statusId = tuple.get(status.id);
FeedEntryContent content = new FeedEntryContent();
content.setTitle(tuple.get(entry.content.title));
FeedEntry entry = new FeedEntry(); FeedEntry entry = new FeedEntry();
entry.setId(id); entry.setId(id);
entry.setUpdated(updated); entry.setUpdated(updated);
entry.setContent(content);
FeedEntryStatus status = new FeedEntryStatus(); FeedEntryStatus status = new FeedEntryStatus();
status.setId(statusId); status.setId(statusId);
@@ -245,8 +284,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) { public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
UnreadCount uc = null; UnreadCount uc = null;
HibernateQuery query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null); JPAQuery<FeedEntry> query = buildQuery(user, subscription, true, null, null, -1, -1, null, null, null);
List<Tuple> tuples = query.list(entry.count(), entry.updated.max()); List<Tuple> tuples = query.select(entry.count(), entry.updated.max()).fetch();
for (Tuple tuple : tuples) { for (Tuple tuple : tuples) {
Long count = tuple.get(entry.count()); Long count = tuple.get(entry.count());
Date updated = tuple.get(entry.updated.max()); Date updated = tuple.get(entry.updated.max());
@@ -266,7 +305,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
} }
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) { public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
return newQuery().from(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).list(status); return query().selectFrom(status).where(status.entryInserted.lt(olderThan), status.starred.isFalse()).limit(limit).fetch();
} }
} }

View File

@@ -23,10 +23,10 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
} }
public List<String> findByUser(User user) { public List<String> findByUser(User user) {
return newQuery().from(tag).where(tag.user.eq(user)).distinct().list(tag.name); return query().selectDistinct(tag.name).from(tag).where(tag.user.eq(user)).fetch();
} }
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) { public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
return newQuery().from(tag).where(tag.user.eq(user), tag.entry.eq(entry)).list(tag); return query().selectFrom(tag).where(tag.user.eq(user), tag.entry.eq(entry)).fetch();
} }
} }

View File

@@ -16,7 +16,7 @@ import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.QFeedSubscription; import com.commafeed.backend.model.QFeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.JPQLQuery;
@Singleton @Singleton
public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> { public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
@@ -29,34 +29,34 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
} }
public FeedSubscription findById(User user, Long id) { public FeedSubscription findById(User user, Long id) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetch() List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.id.eq(id)).leftJoin(sub.feed).fetchJoin()
.leftJoin(sub.category).fetch().list(sub); .leftJoin(sub.category).fetchJoin().fetch();
return initRelations(Iterables.getFirst(subs, null)); return initRelations(Iterables.getFirst(subs, null));
} }
public List<FeedSubscription> findByFeed(Feed feed) { public List<FeedSubscription> findByFeed(Feed feed) {
return newQuery().from(sub).where(sub.feed.eq(feed)).list(sub); return query().selectFrom(sub).where(sub.feed.eq(feed)).fetch();
} }
public FeedSubscription findByFeed(User user, Feed feed) { public FeedSubscription findByFeed(User user, Feed feed) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user), sub.feed.eq(feed)).list(sub); List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user), sub.feed.eq(feed)).fetch();
return initRelations(Iterables.getFirst(subs, null)); return initRelations(Iterables.getFirst(subs, null));
} }
public List<FeedSubscription> findAll(User user) { public List<FeedSubscription> findAll(User user) {
List<FeedSubscription> subs = newQuery().from(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetch().leftJoin(sub.category) List<FeedSubscription> subs = query().selectFrom(sub).where(sub.user.eq(user)).leftJoin(sub.feed).fetchJoin().leftJoin(sub.category)
.fetch().list(sub); .fetchJoin().fetch();
return initRelations(subs); return initRelations(subs);
} }
public List<FeedSubscription> findByCategory(User user, FeedCategory category) { public List<FeedSubscription> findByCategory(User user, FeedCategory category) {
HibernateQuery query = newQuery().from(sub).where(sub.user.eq(user)); JPQLQuery<FeedSubscription> query = query().selectFrom(sub).where(sub.user.eq(user));
if (category == null) { if (category == null) {
query.where(sub.category.isNull()); query.where(sub.category.isNull());
} else { } else {
query.where(sub.category.eq(category)); query.where(sub.category.eq(category));
} }
return initRelations(query.list(sub)); return initRelations(query.fetch());
} }
public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) { public List<FeedSubscription> findByCategories(User user, List<FeedCategory> categories) {

View File

@@ -1,22 +1,27 @@
package com.commafeed.backend.dao; package com.commafeed.backend.dao;
import io.dropwizard.hibernate.AbstractDAO;
import java.util.Collection; import java.util.Collection;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import org.hibernate.annotations.QueryHints;
import com.commafeed.backend.model.AbstractModel; import com.commafeed.backend.model.AbstractModel;
import com.mysema.query.jpa.hibernate.HibernateQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import io.dropwizard.hibernate.AbstractDAO;
public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> { public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T> {
private JPAQueryFactory factory;
protected GenericDAO(SessionFactory sessionFactory) { protected GenericDAO(SessionFactory sessionFactory) {
super(sessionFactory); super(sessionFactory);
this.factory = new JPAQueryFactory(() -> currentSession());
} }
protected HibernateQuery newQuery() { protected JPAQueryFactory query() {
return new HibernateQuery(currentSession()); return factory;
} }
public void saveOrUpdate(T model) { public void saveOrUpdate(T model) {
@@ -27,6 +32,10 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
models.forEach(m -> persist(m)); models.forEach(m -> persist(m));
} }
public void update(T model) {
currentSession().merge(model);
}
public T findById(Long id) { public T findById(Long id) {
return get(id); return get(id);
} }
@@ -42,4 +51,10 @@ public abstract class GenericDAO<T extends AbstractModel> extends AbstractDAO<T>
return objects.size(); return objects.size();
} }
protected void setTimeout(JPAQuery<?> query, int timeoutMs) {
if (timeoutMs > 0) {
query.setHint(QueryHints.TIMEOUT_JPA, timeoutMs);
}
}
} }

View File

@@ -18,13 +18,13 @@ public class UnitOfWork {
} }
public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) { public static void run(SessionFactory sessionFactory, SessionRunner sessionRunner) {
run(sessionFactory, () -> { call(sessionFactory, () -> {
sessionRunner.runInSession(); sessionRunner.runInSession();
return null; return null;
}); });
} }
public static <T> T run(SessionFactory sessionFactory, SessionRunnerReturningValue<T> sessionRunner) { public static <T> T call(SessionFactory sessionFactory, SessionRunnerReturningValue<T> sessionRunner) {
final Session session = sessionFactory.openSession(); final Session session = sessionFactory.openSession();
if (ManagedSessionContext.hasBind(sessionFactory)) { if (ManagedSessionContext.hasBind(sessionFactory)) {
throw new IllegalStateException("Already in a unit of work!"); throw new IllegalStateException("Already in a unit of work!");

View File

@@ -19,18 +19,18 @@ public class UserDAO extends GenericDAO<User> {
} }
public User findByName(String name) { public User findByName(String name) {
return newQuery().from(user).where(user.name.equalsIgnoreCase(name)).uniqueResult(user); return query().selectFrom(user).where(user.name.equalsIgnoreCase(name)).fetchOne();
} }
public User findByApiKey(String key) { public User findByApiKey(String key) {
return newQuery().from(user).where(user.apiKey.equalsIgnoreCase(key)).uniqueResult(user); return query().selectFrom(user).where(user.apiKey.equalsIgnoreCase(key)).fetchOne();
} }
public User findByEmail(String email) { public User findByEmail(String email) {
return newQuery().from(user).where(user.email.equalsIgnoreCase(email)).uniqueResult(user); return query().selectFrom(user).where(user.email.equalsIgnoreCase(email)).fetchOne();
} }
public long count() { public long count() {
return newQuery().from(user).count(); return query().selectFrom(user).fetchCount();
} }
} }

View File

@@ -25,11 +25,11 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
} }
public List<UserRole> findAll() { public List<UserRole> findAll() {
return newQuery().from(role).leftJoin(role.user).fetch().distinct().list(role); return query().selectFrom(role).leftJoin(role.user).fetchJoin().distinct().fetch();
} }
public List<UserRole> findAll(User user) { public List<UserRole> findAll(User user) {
return newQuery().from(role).where(role.user.eq(user)).distinct().list(role); return query().selectFrom(role).where(role.user.eq(user)).distinct().fetch();
} }
public Set<Role> findRoles(User user) { public Set<Role> findRoles(User user) {

View File

@@ -20,6 +20,6 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
} }
public UserSettings findByUser(User user) { public UserSettings findByUser(User user) {
return newQuery().from(settings).where(settings.user.eq(user)).uniqueResult(settings); return query().selectFrom(settings).where(settings.user.eq(user)).fetchFirst();
} }
} }

View File

@@ -8,9 +8,6 @@ import java.util.Optional;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.client.utils.URLEncodedUtils;
@@ -27,8 +24,11 @@ import com.google.api.services.youtube.model.Channel;
import com.google.api.services.youtube.model.ChannelListResponse; import com.google.api.services.youtube.model.ChannelListResponse;
import com.google.api.services.youtube.model.Thumbnail; import com.google.api.services.youtube.model.Thumbnail;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }) )
@Singleton @Singleton
public class YoutubeFaviconFetcher extends AbstractFaviconFetcher { public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
@@ -54,8 +54,7 @@ public class YoutubeFaviconFetcher extends AbstractFaviconFetcher {
try { try {
List<NameValuePair> params = URLEncodedUtils.parse(url.substring(url.indexOf("?") + 1), StandardCharsets.UTF_8); List<NameValuePair> params = URLEncodedUtils.parse(url.substring(url.indexOf("?") + 1), StandardCharsets.UTF_8);
Optional<NameValuePair> userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst(); Optional<NameValuePair> userId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("user")).findFirst();
Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel")).findFirst(); Optional<NameValuePair> channelId = params.stream().filter(nvp -> nvp.getName().equalsIgnoreCase("channel_id")).findFirst();
System.out.println(userId.isPresent());
if (!userId.isPresent() && !channelId.isPresent()) { if (!userId.isPresent() && !channelId.isPresent()) {
return null; return null;
} }

View File

@@ -2,26 +2,24 @@ package com.commafeed.backend.feed;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.client.ClientProtocolException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.HttpGetter.NotModifiedException; import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.urlprovider.FeedURLProvider;
import com.rometools.rome.io.FeedException; import com.rometools.rome.io.FeedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
@@ -29,9 +27,10 @@ public class FeedFetcher {
private final FeedParser parser; private final FeedParser parser;
private final HttpGetter getter; private final HttpGetter getter;
private final Set<FeedURLProvider> urlProviders;
public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate, public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
String lastContentHash) throws FeedException, ClientProtocolException, IOException, NotModifiedException { String lastContentHash) throws FeedException, IOException, NotModifiedException {
log.debug("Fetching feed {}", feedUrl); log.debug("Fetching feed {}", feedUrl);
FetchedFeed fetchedFeed = null; FetchedFeed fetchedFeed = null;
@@ -44,7 +43,7 @@ public class FeedFetcher {
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content); fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
} catch (FeedException e) { } catch (FeedException e) {
if (extractFeedUrlFromHtml) { if (extractFeedUrlFromHtml) {
String extractedUrl = extractFeedUrl(StringUtils.newStringUtf8(result.getContent()), feedUrl); String extractedUrl = extractFeedUrl(urlProviders, feedUrl, StringUtils.newStringUtf8(result.getContent()));
if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) { if (org.apache.commons.lang3.StringUtils.isNotBlank(extractedUrl)) {
feedUrl = extractedUrl; feedUrl = extractedUrl;
@@ -84,20 +83,13 @@ public class FeedFetcher {
return fetchedFeed; return fetchedFeed;
} }
private String extractFeedUrl(String html, String baseUri) { private static String extractFeedUrl(Set<FeedURLProvider> urlProviders, String url, String urlContent) {
String foundUrl = null; for (FeedURLProvider urlProvider : urlProviders) {
String feedUrl = urlProvider.get(url, urlContent);
Document doc = Jsoup.parse(html, baseUri); if (feedUrl != null)
String root = doc.children().get(0).tagName(); return feedUrl;
if ("html".equals(root)) {
Elements atom = doc.select("link[type=application/atom+xml]");
Elements rss = doc.select("link[type=application/rss+xml]");
if (!atom.isEmpty()) {
foundUrl = atom.get(0).attr("abs:href");
} else if (!rss.isEmpty()) {
foundUrl = rss.get(0).attr("abs:href");
}
} }
return foundUrl;
return null;
} }
} }

View File

@@ -108,7 +108,7 @@ public class FeedQueues {
// add feeds that are up to refresh from the database // add feeds that are up to refresh from the database
int count = batchSize - contexts.size(); int count = batchSize - contexts.size();
if (count > 0) { if (count > 0) {
List<Feed> feeds = UnitOfWork.run(sessionFactory, () -> feedDAO.findNextUpdatable(count, getLastLoginThreshold())); List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findNextUpdatable(count, getLastLoginThreshold()));
for (Feed feed : feeds) { for (Feed feed : feeds) {
contexts.add(new FeedRefreshContext(feed, false)); contexts.add(new FeedRefreshContext(feed, false));
} }

View File

@@ -5,11 +5,11 @@ import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import com.codahale.metrics.Gauge; import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistry;
import lombok.extern.slf4j.Slf4j;
/** /**
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using * Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
* {@link Task} instead of {@link Runnable} * {@link Task} instead of {@link Runnable}
@@ -43,7 +43,7 @@ public class FeedRefreshExecutor {
if (t != null) { if (t != null) {
log.error("thread from pool {} threw a runtime exception", poolName, t); log.error("thread from pool {} threw a runtime exception", poolName, t);
} }
}; }
}; };
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() { pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override @Override

View File

@@ -1,7 +1,5 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import io.dropwizard.lifecycle.Managed;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
@@ -14,10 +12,7 @@ import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
@@ -39,6 +34,9 @@ import com.commafeed.backend.service.FeedUpdateService;
import com.commafeed.backend.service.PubSubService; import com.commafeed.backend.service.PubSubService;
import com.google.common.util.concurrent.Striped; import com.google.common.util.concurrent.Striped;
import io.dropwizard.lifecycle.Managed;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@Singleton @Singleton
public class FeedRefreshUpdater implements Managed { public class FeedRefreshUpdater implements Managed {
@@ -121,7 +119,7 @@ public class FeedRefreshUpdater implements Managed {
if (!lastEntries.contains(cacheKey)) { if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl()); log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) { if (subscriptions == null) {
subscriptions = UnitOfWork.run(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed)); subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
} }
ok &= addEntry(feed, entry, subscriptions); ok &= addEntry(feed, entry, subscriptions);
entryCacheMiss.mark(); entryCacheMiss.mark();
@@ -136,9 +134,7 @@ public class FeedRefreshUpdater implements Managed {
if (subscriptions == null) { if (subscriptions == null) {
feed.setMessage("No new entries found"); feed.setMessage("No new entries found");
} } else if (!subscriptions.isEmpty()) {
if (CollectionUtils.isNotEmpty(subscriptions)) {
List<User> users = subscriptions.stream().map(s -> s.getUser()).collect(Collectors.toList()); List<User> users = subscriptions.stream().map(s -> s.getUser()).collect(Collectors.toList());
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(users.toArray(new User[0])); cache.invalidateUserRootCategory(users.toArray(new User[0]));
@@ -183,7 +179,7 @@ public class FeedRefreshUpdater implements Managed {
locked1 = lock1.tryLock(1, TimeUnit.MINUTES); locked1 = lock1.tryLock(1, TimeUnit.MINUTES);
locked2 = lock2.tryLock(1, TimeUnit.MINUTES); locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
if (locked1 && locked2) { if (locked1 && locked2) {
boolean inserted = UnitOfWork.run(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions)); boolean inserted = UnitOfWork.call(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions));
if (inserted) { if (inserted) {
entryInserted.mark(); entryInserted.mark();
} }

View File

@@ -6,6 +6,7 @@ import java.net.URL;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
@@ -13,8 +14,9 @@ import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import org.ahocorasick.trie.Emit;
import org.ahocorasick.trie.Trie;
import org.ahocorasick.trie.Trie.TrieBuilder;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -40,6 +42,7 @@ import com.ibm.icu.text.CharsetMatch;
import com.steadystate.css.parser.CSSOMParser; import com.steadystate.css.parser.CSSOMParser;
import edu.uci.ics.crawler4j.url.URLCanonicalizer; import edu.uci.ics.crawler4j.url.URLCanonicalizer;
import lombok.extern.slf4j.Slf4j;
/** /**
* Utility methods related to feed handling * Utility methods related to feed handling
@@ -87,12 +90,13 @@ public class FeedUtils {
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width"); whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
whitelist.addAttributes("ul", "type"); whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto"); whitelist.addProtocols("a", "href", "ftp", "http", "https", "magnet", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https"); whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https"); whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https"); whitelist.addProtocols("q", "cite", "http", "https");
whitelist.addEnforcedAttribute("a", "target", "_blank"); whitelist.addEnforcedAttribute("a", "target", "_blank");
whitelist.addEnforcedAttribute("a", "rel", "noreferrer");
return whitelist; return whitelist;
} }
@@ -132,7 +136,32 @@ public class FeedUtils {
} }
public static String replaceHtmlEntitiesWithNumericEntities(String source) { public static String replaceHtmlEntitiesWithNumericEntities(String source) {
return StringUtils.replaceEach(source, HtmlEntities.HTML_ENTITIES, HtmlEntities.NUMERIC_ENTITIES); // Create a buffer sufficiently large that re-allocations are minimized.
StringBuilder sb = new StringBuilder(source.length() << 1);
TrieBuilder builder = Trie.builder();
builder.ignoreOverlaps();
for (String key : HtmlEntities.HTML_ENTITIES) {
builder.addKeyword(key);
}
Trie trie = builder.build();
Collection<Emit> emits = trie.parseText(source);
int prevIndex = 0;
for (Emit emit : emits) {
int matchIndex = emit.getStart();
sb.append(source.substring(prevIndex, matchIndex));
sb.append(HtmlEntities.HTML_TO_NUMERIC_MAP.get(emit.getKeyword()));
prevIndex = emit.getEnd() + 1;
}
// Add the remainder of the string (contains no more matches).
sb.append(source.substring(prevIndex));
return sb.toString();
} }
/** /**
@@ -438,10 +467,7 @@ public class FeedUtils {
return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/" + subscription.getId(); return removeTrailingSlash(publicUrl) + "/rest/feed/favicon/" + subscription.getId();
} }
public static String proxyImages(String content, String publicUrl, boolean proxyImages) { public static String proxyImages(String content, String publicUrl) {
if (!proxyImages) {
return content;
}
if (StringUtils.isBlank(content)) { if (StringUtils.isBlank(content)) {
return content; return content;
} }
@@ -451,7 +477,7 @@ public class FeedUtils {
for (Element element : elements) { for (Element element : elements) {
String href = element.attr("src"); String href = element.attr("src");
if (href != null) { if (href != null) {
String proxy = removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(href); String proxy = proxyImage(href, publicUrl);
element.attr("src", proxy); element.attr("src", proxy);
} }
} }
@@ -459,6 +485,13 @@ public class FeedUtils {
return doc.body().html(); return doc.body().html();
} }
public static String proxyImage(String url, String publicUrl) {
if (StringUtils.isBlank(url)) {
return url;
}
return removeTrailingSlash(publicUrl) + "/rest/server/proxy?u=" + imageProxyEncoder(url);
}
public static String rot13(String msg) { public static String rot13(String msg) {
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
@@ -491,8 +524,8 @@ public class FeedUtils {
Entry entry = it.next(); Entry entry = it.next();
boolean keep = true; boolean keep = true;
for (FeedEntryKeyword keyword : keywords) { for (FeedEntryKeyword keyword : keywords) {
String title = Jsoup.parse(entry.getTitle()).text(); String title = entry.getTitle() == null ? null : Jsoup.parse(entry.getTitle()).text();
String content = Jsoup.parse(entry.getContent()).text(); String content = entry.getContent() == null ? null : Jsoup.parse(entry.getContent()).text();
boolean condition = !StringUtils.containsIgnoreCase(content, keyword.getKeyword()) boolean condition = !StringUtils.containsIgnoreCase(content, keyword.getKeyword())
&& !StringUtils.containsIgnoreCase(title, keyword.getKeyword()); && !StringUtils.containsIgnoreCase(title, keyword.getKeyword());
if (keyword.getMode() == Mode.EXCLUDE) { if (keyword.getMode() == Mode.EXCLUDE) {

View File

@@ -1,9 +1,11 @@
package com.commafeed.backend.feed; package com.commafeed.backend.feed;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
public class HtmlEntities { public class HtmlEntities {
public static final Map<String, String> HTML_TO_NUMERIC_MAP;
public static final String[] HTML_ENTITIES; public static final String[] HTML_ENTITIES;
public static final String[] NUMERIC_ENTITIES; public static final String[] NUMERIC_ENTITIES;
@@ -260,6 +262,7 @@ public class HtmlEntities {
map.put("&zwj;", "&#8205;"); map.put("&zwj;", "&#8205;");
map.put("&zwnj;", "&#8204;"); map.put("&zwnj;", "&#8204;");
HTML_TO_NUMERIC_MAP = Collections.unmodifiableMap(map);
HTML_ENTITIES = map.keySet().toArray(new String[map.size()]); HTML_ENTITIES = map.keySet().toArray(new String[map.size()]);
NUMERIC_ENTITIES = map.values().toArray(new String[map.size()]); NUMERIC_ENTITIES = map.values().toArray(new String[map.size()]);
} }

View File

@@ -8,11 +8,11 @@ import javax.persistence.Lob;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import org.hibernate.annotations.Type;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.Type;
@Entity @Entity
@Table(name = "FEEDENTRYCONTENTS") @Table(name = "FEEDENTRYCONTENTS")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -28,7 +28,7 @@ public class FeedEntryContent extends AbstractModel {
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
@Type(type = "org.hibernate.type.StringClobType") @Type(type = "org.hibernate.type.TextType")
private String content; private String content;
@Column(length = 40) @Column(length = 40)

View File

@@ -10,11 +10,11 @@ import javax.persistence.Lob;
import javax.persistence.OneToOne; import javax.persistence.OneToOne;
import javax.persistence.Table; import javax.persistence.Table;
import org.hibernate.annotations.Type;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.Type;
@Entity @Entity
@Table(name = "USERSETTINGS") @Table(name = "USERSETTINGS")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -27,7 +27,7 @@ public class UserSettings extends AbstractModel {
} }
public enum ReadingOrder { public enum ReadingOrder {
asc, desc asc, desc, abc, zyx
} }
public enum ViewMode { public enum ViewMode {
@@ -61,7 +61,7 @@ public class UserSettings extends AbstractModel {
@Lob @Lob
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
@Type(type = "org.hibernate.type.StringClobType") @Type(type = "org.hibernate.type.TextType")
private String customCss; private String customCss;
@Column(name = "scroll_speed") @Column(name = "scroll_speed")

View File

@@ -8,18 +8,19 @@ import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.ObjectUtils;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.google.common.base.MoreObjects;
import com.rometools.opml.feed.opml.Attribute; import com.rometools.opml.feed.opml.Attribute;
import com.rometools.opml.feed.opml.Opml; import com.rometools.opml.feed.opml.Opml;
import com.rometools.opml.feed.opml.Outline; import com.rometools.opml.feed.opml.Outline;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
public class OPMLExporter { public class OPMLExporter {
@@ -35,11 +36,11 @@ public class OPMLExporter {
List<FeedCategory> categories = feedCategoryDAO.findAll(user); List<FeedCategory> categories = feedCategoryDAO.findAll(user);
Collections.sort(categories, Collections.sort(categories,
(e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0)); (e1, e2) -> ObjectUtils.firstNonNull(e1.getPosition(), 0) - ObjectUtils.firstNonNull(e2.getPosition(), 0));
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
Collections.sort(subscriptions, Collections.sort(subscriptions,
(e1, e2) -> MoreObjects.firstNonNull(e1.getPosition(), 0) - MoreObjects.firstNonNull(e2.getPosition(), 0)); (e1, e2) -> ObjectUtils.firstNonNull(e1.getPosition(), 0) - ObjectUtils.firstNonNull(e2.getPosition(), 0));
// export root categories // export root categories
for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).collect(Collectors.toList())) { for (FeedCategory cat : categories.stream().filter(c -> c.getParent() == null).collect(Collectors.toList())) {

View File

@@ -29,7 +29,7 @@ public class OPML11Parser extends OPML10Parser {
return false; return false;
}; }
@Override @Override
public WireFeed parse(Document document, boolean validate, Locale locale) throws IllegalArgumentException, FeedException { public WireFeed parse(Document document, boolean validate, Locale locale) throws IllegalArgumentException, FeedException {

View File

@@ -6,9 +6,6 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
@@ -19,12 +16,15 @@ import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/** /**
* Contains utility methods for cleaning the database * Contains utility methods for cleaning the database
* *
*/ */
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }) )
@Singleton @Singleton
public class DatabaseCleaningService { public class DatabaseCleaningService {
@@ -42,16 +42,16 @@ public class DatabaseCleaningService {
int deleted = 0; int deleted = 0;
long entriesTotal = 0; long entriesTotal = 0;
do { do {
List<Feed> feeds = UnitOfWork.run(sessionFactory, () -> feedDAO.findWithoutSubscriptions(1)); List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> feedDAO.findWithoutSubscriptions(1));
for (Feed feed : feeds) { for (Feed feed : feeds) {
int entriesDeleted = 0; int entriesDeleted = 0;
do { do {
entriesDeleted = UnitOfWork.run(sessionFactory, () -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE)); entriesDeleted = UnitOfWork.call(sessionFactory, () -> feedEntryDAO.delete(feed.getId(), BATCH_SIZE));
entriesTotal += entriesDeleted; entriesTotal += entriesDeleted;
log.info("removed {} entries for feeds without subscriptions", entriesTotal); log.info("removed {} entries for feeds without subscriptions", entriesTotal);
} while (entriesDeleted > 0); } while (entriesDeleted > 0);
} }
deleted = UnitOfWork.run(sessionFactory, () -> feedDAO.delete(feeds)); deleted = UnitOfWork.call(sessionFactory, () -> feedDAO.delete(feeds));
total += deleted; total += deleted;
log.info("removed {} feeds without subscriptions", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
@@ -64,7 +64,7 @@ public class DatabaseCleaningService {
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
do { do {
deleted = UnitOfWork.run(sessionFactory, () -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE)); deleted = UnitOfWork.call(sessionFactory, () -> feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE));
total += deleted; total += deleted;
log.info("removed {} contents without entries", total); log.info("removed {} contents without entries", total);
} while (deleted != 0); } while (deleted != 0);
@@ -75,7 +75,7 @@ public class DatabaseCleaningService {
public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) { public long cleanEntriesForFeedsExceedingCapacity(final int maxFeedCapacity) {
long total = 0; long total = 0;
while (true) { while (true) {
List<FeedCapacity> feeds = UnitOfWork.run(sessionFactory, List<FeedCapacity> feeds = UnitOfWork.call(sessionFactory,
() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE)); () -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, BATCH_SIZE));
if (feeds.isEmpty()) { if (feeds.isEmpty()) {
break; break;
@@ -85,7 +85,7 @@ public class DatabaseCleaningService {
long remaining = feed.getCapacity() - maxFeedCapacity; long remaining = feed.getCapacity() - maxFeedCapacity;
do { do {
final long rem = remaining; final long rem = remaining;
int deleted = UnitOfWork.run(sessionFactory, int deleted = UnitOfWork.call(sessionFactory,
() -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem))); () -> feedEntryDAO.deleteOldEntries(feed.getId(), Math.min(BATCH_SIZE, rem)));
total += deleted; total += deleted;
remaining -= deleted; remaining -= deleted;
@@ -102,7 +102,7 @@ public class DatabaseCleaningService {
long total = 0; long total = 0;
int deleted = 0; int deleted = 0;
do { do {
deleted = UnitOfWork.run(sessionFactory, deleted = UnitOfWork.call(sessionFactory,
() -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE))); () -> feedEntryStatusDAO.delete(feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE)));
total += deleted; total += deleted;
log.info("removed {} old read statuses", total); log.info("removed {} old read statuses", total);

View File

@@ -1,5 +1,6 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import java.time.Year;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -91,6 +92,8 @@ public class FeedEntryFilteringService {
context.set("url", entry.getUrl() == null ? "" : entry.getUrl().toLowerCase()); context.set("url", entry.getUrl() == null ? "" : entry.getUrl().toLowerCase());
context.set("categories", entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories().toLowerCase()); context.set("categories", entry.getContent().getCategories() == null ? "" : entry.getContent().getCategories().toLowerCase());
context.set("year", Year.now().getValue());
Callable<Object> callable = script.callable(context); Callable<Object> callable = script.callable(context);
Future<Object> future = executor.submit(callable); Future<Object> future = executor.submit(callable);
Object result = null; Object result = null;

View File

@@ -1,14 +1,20 @@
package com.commafeed.backend.service; package com.commafeed.backend.service;
import io.dropwizard.lifecycle.Managed;
import java.sql.Connection;
import java.util.Arrays; import java.util.Arrays;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import javax.sql.DataSource;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.UserRole.Role;
import io.dropwizard.lifecycle.Managed;
import liquibase.Liquibase; import liquibase.Liquibase;
import liquibase.database.Database; import liquibase.database.Database;
import liquibase.database.DatabaseFactory; import liquibase.database.DatabaseFactory;
@@ -20,17 +26,6 @@ import liquibase.structure.DatabaseObject;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.SessionFactory;
import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.internal.SessionFactoryImpl;
import com.commafeed.CommaFeedApplication;
import com.commafeed.CommaFeedConfiguration;
import com.commafeed.backend.dao.UnitOfWork;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.UserRole.Role;
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__({ @Inject })) @RequiredArgsConstructor(onConstructor = @__({ @Inject }))
@Singleton @Singleton
@@ -44,24 +39,17 @@ public class StartupService implements Managed {
@Override @Override
public void start() throws Exception { public void start() throws Exception {
updateSchema(); updateSchema();
long count = UnitOfWork.run(sessionFactory, () -> userDAO.count()); long count = UnitOfWork.call(sessionFactory, () -> userDAO.count());
if (count == 0) { if (count == 0) {
UnitOfWork.run(sessionFactory, () -> initialData()); UnitOfWork.run(sessionFactory, () -> initialData());
} }
} }
private void updateSchema() { private void updateSchema() {
try { Session session = sessionFactory.openSession();
Connection connection = null; session.doWork(connection -> {
try { try {
Thread currentThread = Thread.currentThread();
ClassLoader classLoader = currentThread.getContextClassLoader();
ResourceAccessor accessor = new ClassLoaderResourceAccessor(classLoader);
DataSource dataSource = getDataSource(sessionFactory);
connection = dataSource.getConnection();
JdbcConnection jdbcConnection = new JdbcConnection(connection); JdbcConnection jdbcConnection = new JdbcConnection(connection);
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection); Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(jdbcConnection);
if (database instanceof PostgresDatabase) { if (database instanceof PostgresDatabase) {
@@ -74,17 +62,14 @@ public class StartupService implements Managed {
database.setConnection(jdbcConnection); database.setConnection(jdbcConnection);
} }
ResourceAccessor accessor = new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader());
Liquibase liq = new Liquibase("migrations.xml", accessor, database); Liquibase liq = new Liquibase("migrations.xml", accessor, database);
liq.update("prod"); liq.update("prod");
} finally { } catch (Exception e) {
if (connection != null) { throw new RuntimeException(e);
connection.close();
}
} }
});
} catch (Exception e) { session.close();
throw new RuntimeException(e);
}
} }
private void initialData() { private void initialData() {
@@ -104,15 +89,4 @@ public class StartupService implements Managed {
public void stop() throws Exception { public void stop() throws Exception {
} }
private static DataSource getDataSource(SessionFactory sessionFactory) {
if (sessionFactory instanceof SessionFactoryImpl) {
ConnectionProvider cp = ((SessionFactoryImpl) sessionFactory).getConnectionProvider();
if (cp instanceof DatasourceConnectionProviderImpl) {
return ((DatasourceConnectionProviderImpl) cp).getDataSource();
}
}
return null;
}
} }

View File

@@ -0,0 +1,10 @@
package com.commafeed.backend.urlprovider;
/**
* Tries to find a feed url given the url and page content
*/
public interface FeedURLProvider {
String get(String url, String urlContent);
}

View File

@@ -0,0 +1,28 @@
package com.commafeed.backend.urlprovider;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
public class InPageReferenceFeedURLProvider implements FeedURLProvider {
@Override
public String get(String url, String urlContent) {
String foundUrl = null;
Document doc = Jsoup.parse(urlContent, url);
String root = doc.children().get(0).tagName();
if ("html".equals(root)) {
Elements atom = doc.select("link[type=application/atom+xml]");
Elements rss = doc.select("link[type=application/rss+xml]");
if (!atom.isEmpty()) {
foundUrl = atom.get(0).attr("abs:href");
} else if (!rss.isEmpty()) {
foundUrl = rss.get(0).attr("abs:href");
}
}
return foundUrl;
}
}

View File

@@ -0,0 +1,22 @@
package com.commafeed.backend.urlprovider;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Workaround for Youtube channels
*
* converts the channel URL https://www.youtube.com/channel/CHANNEL_ID to the valid feed URL
* https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
*/
public class YoutubeFeedURLProvider implements FeedURLProvider {
private static final Pattern REGEXP = Pattern.compile("(.*\\byoutube\\.com)\\/channel\\/([^\\/]+)", Pattern.CASE_INSENSITIVE);
@Override
public String get(String url, String urlContent) {
Matcher matcher = REGEXP.matcher(url);
return matcher.find() ? matcher.group(1) + "/feeds/videos.xml?channel_id=" + matcher.group(2) : null;
}
}

View File

@@ -4,34 +4,33 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Entry details") @ApiModel(description = "Entry details")
@Data @Data
public class Category implements Serializable { public class Category implements Serializable {
@ApiModelProperty("category id") @ApiModelProperty(value = "category id", required = true)
private String id; private String id;
@ApiModelProperty("parent category id") @ApiModelProperty(value = "parent category id")
private String parentId; private String parentId;
@ApiModelProperty("category id") @ApiModelProperty(value = "category id", required = true)
private String name; private String name;
@ApiModelProperty("category children categories") @ApiModelProperty(value = "category children categories", required = true)
private List<Category> children = new ArrayList<>(); private List<Category> children = new ArrayList<>();
@ApiModelProperty("category feeds") @ApiModelProperty(value = "category feeds", required = true)
private List<Subscription> feeds = new ArrayList<>(); private List<Subscription> feeds = new ArrayList<>();
@ApiModelProperty("wether the category is expanded or collapsed") @ApiModelProperty(value = "wether the category is expanded or collapsed", required = true)
private boolean expanded; private boolean expanded;
@ApiModelProperty("position of the category in the list") @ApiModelProperty(value = "position of the category in the list", required = true)
private Integer position; private Integer position;
} }

View File

@@ -4,44 +4,45 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("List of entries with some metadata") @ApiModel(description = "List of entries with some metadata")
@Data @Data
public class Entries implements Serializable { public class Entries implements Serializable {
@ApiModelProperty("name of the feed or the category requested") @ApiModelProperty(value = "name of the feed or the category requested", required = true)
private String name; private String name;
@ApiModelProperty("error or warning message") @ApiModelProperty(value = "error or warning message")
private String message; private String message;
@ApiModelProperty("times the server tried to refresh the feed and failed") @ApiModelProperty(value = "times the server tried to refresh the feed and failed", required = true)
private int errorCount; private int errorCount;
@ApiModelProperty("URL of the website, extracted from the feed") @ApiModelProperty(value = "URL of the website, extracted from the feed", required = true)
private String feedLink; private String feedLink;
@ApiModelProperty("list generation timestamp") @ApiModelProperty(value = "list generation timestamp", required = true)
private long timestamp; private long timestamp;
@ApiModelProperty("if the query has more elements") @ApiModelProperty(value = "if the query has more elements", required = true)
private boolean hasMore; private boolean hasMore;
@ApiModelProperty("the requested offset") @ApiModelProperty(value = "the requested offset")
private int offset; private int offset;
@ApiModelProperty("the requested limit") @ApiModelProperty(value = "the requested limit")
private int limit; private int limit;
@ApiModelProperty("list of entries") @ApiModelProperty(value = "list of entries", required = true)
private List<Entry> entries = new ArrayList<>(); private List<Entry> entries = new ArrayList<>();
@ApiModelProperty("if true, the unread flag was ignored in the request, all entries are returned regardless of their read status") @ApiModelProperty(
value = "if true, the unread flag was ignored in the request, all entries are returned regardless of their read status",
required = true)
private boolean ignoredReadStatus; private boolean ignoredReadStatus;
} }

View File

@@ -6,7 +6,7 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.Data; import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
@@ -19,11 +19,13 @@ import com.rometools.rome.feed.synd.SyndEnclosure;
import com.rometools.rome.feed.synd.SyndEnclosureImpl; import com.rometools.rome.feed.synd.SyndEnclosureImpl;
import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndEntry;
import com.rometools.rome.feed.synd.SyndEntryImpl; import com.rometools.rome.feed.synd.SyndEntryImpl;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Entry details") @ApiModel(description = "Entry details")
@Data @Data
public class Entry implements Serializable { public class Entry implements Serializable {
@@ -52,10 +54,12 @@ public class Entry implements Serializable {
if (content != null) { if (content != null) {
entry.setRtl(FeedUtils.isRTL(feedEntry)); entry.setRtl(FeedUtils.isRTL(feedEntry));
entry.setTitle(content.getTitle()); entry.setTitle(content.getTitle());
entry.setContent(FeedUtils.proxyImages(content.getContent(), publicUrl, proxyImages)); entry.setContent(proxyImages ? FeedUtils.proxyImages(content.getContent(), publicUrl) : content.getContent());
entry.setAuthor(content.getAuthor()); entry.setAuthor(content.getAuthor());
entry.setEnclosureUrl(content.getEnclosureUrl());
entry.setEnclosureType(content.getEnclosureType()); entry.setEnclosureType(content.getEnclosureType());
entry.setEnclosureUrl(proxyImages && StringUtils.contains(content.getEnclosureType(), "image")
? FeedUtils.proxyImage(content.getEnclosureUrl(), publicUrl)
: content.getEnclosureUrl());
entry.setCategories(content.getCategories()); entry.setCategories(content.getCategories());
} }
@@ -67,6 +71,7 @@ public class Entry implements Serializable {
entry.setUri(getGuid()); entry.setUri(getGuid());
entry.setTitle(getTitle()); entry.setTitle(getTitle());
entry.setAuthor(getAuthor());
SyndContentImpl content = new SyndContentImpl(); SyndContentImpl content = new SyndContentImpl();
content.setValue(getContent()); content.setValue(getContent());
@@ -84,66 +89,66 @@ public class Entry implements Serializable {
return entry; return entry;
} }
@ApiModelProperty("entry id") @ApiModelProperty(value = "entry id", required = true)
private String id; private String id;
@ApiModelProperty("entry guid") @ApiModelProperty(value = "entry guid", required = true)
private String guid; private String guid;
@ApiModelProperty("entry title") @ApiModelProperty(value = "entry title", required = true)
private String title; private String title;
@ApiModelProperty("entry content") @ApiModelProperty(value = "entry content", required = true)
private String content; private String content;
@ApiModelProperty("comma-separated list of categories") @ApiModelProperty(value = "comma-separated list of categories")
private String categories; private String categories;
@ApiModelProperty("wether entry content and title are rtl") @ApiModelProperty(value = "wether entry content and title are rtl", required = true)
private boolean rtl; private boolean rtl;
@ApiModelProperty("entry author") @ApiModelProperty(value = "entry author")
private String author; private String author;
@ApiModelProperty("entry enclosure url, if any") @ApiModelProperty(value = "entry enclosure url, if any")
private String enclosureUrl; private String enclosureUrl;
@ApiModelProperty("entry enclosure mime type, if any") @ApiModelProperty(value = "entry enclosure mime type, if any")
private String enclosureType; private String enclosureType;
@ApiModelProperty("entry publication date") @ApiModelProperty(value = "entry publication date", dataType = "number", required = true)
private Date date; private Date date;
@ApiModelProperty("entry insertion date in the database") @ApiModelProperty(value = "entry insertion date in the database", dataType = "number", required = true)
private Date insertedDate; private Date insertedDate;
@ApiModelProperty("feed id") @ApiModelProperty(value = "feed id", required = true)
private String feedId; private String feedId;
@ApiModelProperty("feed name") @ApiModelProperty(value = "feed name", required = true)
private String feedName; private String feedName;
@ApiModelProperty("this entry's feed url") @ApiModelProperty(value = "this entry's feed url", required = true)
private String feedUrl; private String feedUrl;
@ApiModelProperty("this entry's website url") @ApiModelProperty(value = "this entry's website url", required = true)
private String feedLink; private String feedLink;
@ApiModelProperty(value = "The favicon url to use for this feed") @ApiModelProperty(value = "The favicon url to use for this feed", required = true)
private String iconUrl; private String iconUrl;
@ApiModelProperty("entry url") @ApiModelProperty(value = "entry url", required = true)
private String url; private String url;
@ApiModelProperty("read sttaus") @ApiModelProperty(value = "read status", required = true)
private boolean read; private boolean read;
@ApiModelProperty("starred status") @ApiModelProperty(value = "starred status", required = true)
private boolean starred; private boolean starred;
@ApiModelProperty("wether the entry is still markable (old entry statuses are discarded)") @ApiModelProperty(value = "wether the entry is still markable (old entry statuses are discarded)", required = true)
private boolean markable; private boolean markable;
@ApiModelProperty("tags") @ApiModelProperty(value = "tags", required = true)
private List<String> tags; private List<String> tags;
} }

View File

@@ -2,16 +2,19 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Feed details") @ApiModel(description = "Feed details")
@Data @Data
public class FeedInfo implements Serializable { public class FeedInfo implements Serializable {
@ApiModelProperty(value = "url", required = true)
private String url; private String url;
@ApiModelProperty(value = "title", required = true)
private String title; private String title;
} }

View File

@@ -2,20 +2,31 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Server infos") @ApiModel(description = "Server infos")
@Data @Data
public class ServerInfo implements Serializable { public class ServerInfo implements Serializable {
@ApiModelProperty
private String announcement; private String announcement;
@ApiModelProperty(required = true)
private String version; private String version;
@ApiModelProperty(required = true)
private String gitCommit; private String gitCommit;
@ApiModelProperty(required = true)
private boolean allowRegistrations; private boolean allowRegistrations;
@ApiModelProperty
private String googleAnalyticsCode; private String googleAnalyticsCode;
@ApiModelProperty(required = true)
private boolean smtpEnabled; private boolean smtpEnabled;
} }

View File

@@ -2,17 +2,16 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("User settings") @ApiModel(description = "User settings")
@Data @Data
public class Settings implements Serializable { public class Settings implements Serializable {
@ApiModelProperty(value = "user's preferred language, english if none") @ApiModelProperty(value = "user's preferred language, english if none", required = true)
private String language; private String language;
@ApiModelProperty(value = "user reads all entries or unread entries only", allowableValues = "all,unread", required = true) @ApiModelProperty(value = "user reads all entries or unread entries only", allowableValues = "all,unread", required = true)
@@ -36,18 +35,37 @@ public class Settings implements Serializable {
@ApiModelProperty(value = "user's custom css for the website") @ApiModelProperty(value = "user's custom css for the website")
private String customCss; private String customCss;
@ApiModelProperty(value = "user's preferred scroll speed when navigating between entries") @ApiModelProperty(value = "user's preferred scroll speed when navigating between entries", required = true)
private int scrollSpeed; private int scrollSpeed;
@ApiModelProperty(required = true)
private boolean email; private boolean email;
@ApiModelProperty(required = true)
private boolean gmail; private boolean gmail;
@ApiModelProperty(required = true)
private boolean facebook; private boolean facebook;
@ApiModelProperty(required = true)
private boolean twitter; private boolean twitter;
@ApiModelProperty(required = true)
private boolean googleplus; private boolean googleplus;
@ApiModelProperty(required = true)
private boolean tumblr; private boolean tumblr;
@ApiModelProperty(required = true)
private boolean pocket; private boolean pocket;
@ApiModelProperty(required = true)
private boolean instapaper; private boolean instapaper;
@ApiModelProperty(required = true)
private boolean buffer; private boolean buffer;
@ApiModelProperty(required = true)
private boolean readability; private boolean readability;
} }

View File

@@ -3,17 +3,17 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import lombok.Data;
import com.commafeed.backend.feed.FeedUtils; import com.commafeed.backend.feed.FeedUtils;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.FeedSubscription;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("User information") @ApiModel(description = "User information")
@Data @Data
public class Subscription implements Serializable { public class Subscription implements Serializable {
@@ -51,10 +51,13 @@ public class Subscription implements Serializable {
@ApiModelProperty(value = "error count", required = true) @ApiModelProperty(value = "error count", required = true)
private int errorCount; private int errorCount;
@ApiModelProperty(value = "last time the feed was refreshed", required = true) @ApiModelProperty(value = "last time the feed was refreshed", dataType = "number", required = true)
private Date lastRefresh; private Date lastRefresh;
@ApiModelProperty(value = "next time the feed refresh is planned, null if refresh is already queued", required = true) @ApiModelProperty(
value = "next time the feed refresh is planned, null if refresh is already queued",
dataType = "number",
required = true)
private Date nextRefresh; private Date nextRefresh;
@ApiModelProperty(value = "this subscription's feed url", required = true) @ApiModelProperty(value = "this subscription's feed url", required = true)
@@ -63,7 +66,7 @@ public class Subscription implements Serializable {
@ApiModelProperty(value = "this subscription's website url", required = true) @ApiModelProperty(value = "this subscription's website url", required = true)
private String feedLink; private String feedLink;
@ApiModelProperty(value = "The favicon url to use for this feed") @ApiModelProperty(value = "The favicon url to use for this feed", required = true)
private String iconUrl; private String iconUrl;
@ApiModelProperty(value = "unread count", required = true) @ApiModelProperty(value = "unread count", required = true)
@@ -75,7 +78,7 @@ public class Subscription implements Serializable {
@ApiModelProperty("position of the subscription's in the list") @ApiModelProperty("position of the subscription's in the list")
private Integer position; private Integer position;
@ApiModelProperty("date of the newest item") @ApiModelProperty(value = "date of the newest item", dataType = "number")
private Date newestItemTime; private Date newestItemTime;
@ApiModelProperty(value = "JEXL string evaluated on new entries to mark them as read if they do not match") @ApiModelProperty(value = "JEXL string evaluated on new entries to mark them as read if they do not match")

View File

@@ -3,17 +3,22 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Unread count") @ApiModel(description = "Unread count")
@Data @Data
public class UnreadCount implements Serializable { public class UnreadCount implements Serializable {
@ApiModelProperty
private long feedId; private long feedId;
@ApiModelProperty
private long unreadCount; private long unreadCount;
@ApiModelProperty(dataType = "number")
private Date newestItemTime; private Date newestItemTime;
public UnreadCount() { public UnreadCount() {

View File

@@ -3,13 +3,12 @@ package com.commafeed.frontend.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("User information") @ApiModel(description = "User information")
@Data @Data
public class UserModel implements Serializable { public class UserModel implements Serializable {
@@ -28,16 +27,16 @@ public class UserModel implements Serializable {
@ApiModelProperty(value = "user password, never returned by the api") @ApiModelProperty(value = "user password, never returned by the api")
private String password; private String password;
@ApiModelProperty(value = "account status") @ApiModelProperty(value = "account status", required = true)
private boolean enabled; private boolean enabled;
@ApiModelProperty(value = "account creation date") @ApiModelProperty(value = "account creation date", dataType = "number", required = true)
private Date created; private Date created;
@ApiModelProperty(value = "last login date") @ApiModelProperty(value = "last login date", dataType = "number")
private Date lastLogin; private Date lastLogin;
@ApiModelProperty(value = "user is admin") @ApiModelProperty(value = "user is admin", required = true)
private boolean admin; private boolean admin;
} }

View File

@@ -2,13 +2,12 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Add Category Request") @ApiModel(description = "Add Category Request")
@Data @Data
public class AddCategoryRequest implements Serializable { public class AddCategoryRequest implements Serializable {

View File

@@ -2,13 +2,12 @@ package com.commafeed.frontend.model.request;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import com.wordnik.swagger.annotations.ApiModel;
import com.wordnik.swagger.annotations.ApiModelProperty;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ApiModel("Category modification request") @ApiModel(description = "Category modification request")
@Data @Data
public class CategoryModificationRequest implements Serializable { public class CategoryModificationRequest implements Serializable {

Some files were not shown because too many files have changed in this diff Show More