Compare commits

...

255 Commits
1.2.0 ... 1.5.0

Author SHA1 Message Date
Athou
bbcd79e49f Merge pull request #602 from swoga/patch-1
Added translations for german
2014-07-11 12:38:55 +02:00
Peter
4dabf47822 Update de.properties 2014-07-09 15:46:04 +02:00
Athou
db258d4ecc starred items ignore the unreadOnly flag 2014-05-07 23:27:41 +02:00
Athou
8b237db690 ignore loading bar for some requests 2014-04-29 15:55:03 +02:00
Athou
416350c004 reset latency threshold to default 2014-04-29 15:46:44 +02:00
Athou
8d63377e78 patch angular loading bar to allow undefined config 2014-04-29 13:14:54 +02:00
Athou
377176df05 don't minimize already minified files 2014-04-29 12:43:35 +02:00
Athou
95da0078b3 angularjs upgrade 2014-04-29 12:16:02 +02:00
Athou
6392b87afc upgrade to 0.4.0 2014-04-29 12:02:39 +02:00
Athou
ba04d2adfe use angular-loading-bar instead of spin.js for ajax indicator 2014-04-29 10:44:39 +02:00
Athou
517ce1a726 dependencies update 2014-04-22 16:56:31 +02:00
Athou
36492cbff5 fix opml export 2014-04-21 09:53:04 +02:00
Athou
4b46aa08ac display title texts for images on mobile (fix #585) 2014-04-21 09:47:26 +02:00
Athou
1a9a80c0da change title pattern (fix #584) 2014-04-19 06:31:41 +02:00
Athou
32a30019a7 individual settings for share buttons, and added tumblr (#582) 2014-04-16 12:30:25 +02:00
Athou
bb72131354 remove invalid content-encoding headers (fix #580) 2014-04-16 11:42:39 +02:00
Athou
3a8d72cab4 default charset for http should be iso-8859-1 (https://www.w3.org/International/O-HTTP-charset) 2014-04-09 16:21:33 +02:00
Athou
f5f7a8e63b remove invalid content encoding headers (#580) 2014-04-04 11:41:52 +02:00
Athou
570c4f3a1f display cause of invalid feed (#580) 2014-04-04 11:41:15 +02:00
Athou
172164b74b Merge pull request #574 from ekovi/patch-17
Update _svetla.scss
2014-03-22 11:46:49 +01:00
ekovi
49835ae234 Update _svetla.scss 2014-03-22 11:31:31 +01:00
Athou
c4f1e910f8 Merge pull request #573 from ekovi/patch-16
Update _svetla.scss
2014-03-21 11:44:57 +01:00
ekovi
3a621b61c6 Update _svetla.scss
hopefully finished
2014-03-21 11:44:26 +01:00
Athou
c28f0d6788 changed rest endpoint to reflect cleanup task changes 2014-03-21 11:34:29 +01:00
Athou
2db9224ffc first clean entries then clean feeds 2014-03-21 11:28:51 +01:00
Athou
043b1df585 more logging 2014-03-21 00:26:32 +01:00
Athou
0626200787 Merge pull request #572 from ekovi/patch-15
Update _svetla.scss
2014-03-20 23:39:21 +01:00
ekovi
b7ee61a8df Update _svetla.scss 2014-03-20 23:05:18 +01:00
Athou
6e1cdaf50e Merge pull request #571 from ekovi/patch-14
Update _dark.scss
2014-03-19 03:32:22 +01:00
ekovi
e770f802e7 Update _dark.scss 2014-03-18 21:39:33 +01:00
Athou
8e4cf77fcb Merge pull request #569 from ekovi/patch-12
added theme entry
2014-03-18 15:56:45 +01:00
Athou
bc3bd42ce3 Merge pull request #568 from ekovi/patch-11
new theme
2014-03-18 15:56:24 +01:00
Athou
f73e0ba307 Merge pull request #570 from ekovi/patch-13
added reference for new theme
2014-03-18 15:55:33 +01:00
ekovi
5703b5e8d4 added reference for new theme 2014-03-18 15:23:06 +01:00
ekovi
cecbb2cf72 added theme entry 2014-03-18 15:21:20 +01:00
ekovi
8638e4751d new theme 2014-03-18 15:18:19 +01:00
Athou
3b69e3b029 actually remove entries for feeds 2014-03-17 06:18:36 +01:00
Athou
dced21c8e4 revert ng-grid back to 2.0.7, fix admin user list 2014-03-16 15:17:23 +01:00
Athou
dab26af294 allow feeds without entries (fix #565) 2014-03-15 04:24:40 +01:00
Athou
65f118e561 Merge pull request #564 from ekovi/patch-9
Update _svetla.scss
2014-03-14 12:45:31 +01:00
ekovi
67f533b9f6 Update _svetla.scss
'popup-able' feedback button
2014-03-14 12:44:39 +01:00
Athou
93573bcdb7 Merge pull request #563 from ekovi/patch-8
Update _dark.scss
2014-03-14 12:44:12 +01:00
ekovi
2263801c55 Update _dark.scss
{}
2014-03-14 12:43:38 +01:00
Athou
10c34d0440 Merge pull request #562 from ekovi/patch-7
Update _dark.scss
2014-03-14 12:41:14 +01:00
ekovi
4430ef3847 Update _dark.scss
just a few minor changes (to better ofc)
2014-03-14 12:40:27 +01:00
Athou
8e331b908d Merge pull request #561 from ekovi/patch-6
Update _svetla.scss
2014-03-13 14:17:50 +01:00
ekovi
dbc6fb58e0 Update _svetla.scss 2014-03-13 14:03:38 +01:00
Athou
db298ab684 Merge pull request #560 from ekovi/patch-6
Update _dark.scss
2014-03-13 09:42:56 +01:00
ekovi
170a6095e6 Update _dark.scss 2014-03-13 09:30:28 +01:00
Athou
6dd1bf3281 restore pointer mouse icon on hover 2014-03-10 14:51:59 +01:00
Athou
b1500cebfd remove bold from labels 2014-03-10 14:37:45 +01:00
Athou
6202bdbc28 added default bootstrap theme 2014-03-10 14:37:45 +01:00
Athou
39bfb61b95 dependencies update 2014-03-10 14:37:45 +01:00
Athou
fa79524ed4 fix checkbox position 2014-03-10 14:37:44 +01:00
Athou
ab5b70e52b Merge pull request #558 from ekovi/patch-6
Update _dark.scss
2014-03-10 13:13:24 +01:00
ekovi
4f8cd53b83 Update _dark.scss
cleaned style and reworked a bit
2014-03-10 13:07:23 +01:00
Athou
afb6221e5e Merge pull request #557 from ekovi/patch-5
Update _dark.scss
2014-03-10 06:48:47 +01:00
ekovi
f78aedc30d Update _dark.scss
some minor stuff, transition, etc.
2014-03-10 00:25:20 +01:00
ekovi
80ff2c8ff7 Update _dark.scss 2014-03-09 20:40:42 +01:00
Athou
579a77dfc9 remove debug logging 2014-03-06 15:50:57 +01:00
Athou
f902d967a6 wording 2014-03-06 15:48:03 +01:00
Athou
0899e0b0bf server now returns wether the 'unread only' flag was ignored while generating the response (fix scrolling for results in a feed search) 2014-03-06 15:46:19 +01:00
Athou
65d6f8616b fix tag removal 2014-03-03 13:15:37 +01:00
Athou
5c27f0834c wait in the new spawned thread 2014-03-03 12:50:32 +01:00
Athou
a5f7b56bf2 let everything settle a little while longer 2014-03-03 12:42:16 +01:00
Athou
63ec92038c fix tagging 2014-03-03 12:03:42 +01:00
Athou
464ac36ddb added bootstrap webjar 2014-03-03 11:39:09 +01:00
Athou
840bc2ef7a added catalan language 2014-03-02 09:28:24 +01:00
Athou
e248504528 new webjars 2014-03-01 18:34:10 +01:00
Athou
f4f3d9ca48 handle invalid feeds having unescaped html entities 2014-03-01 18:19:49 +01:00
Athou
e727ee414b use webjars if possible 2014-02-28 14:45:12 +01:00
Athou
1e9295b386 wro4j upgrade 2014-02-28 13:43:30 +01:00
Athou
b980cdc2c2 Merge pull request #554 from ekovi/patch-4
Update controllers.js
2014-02-27 16:16:31 +01:00
Athou
fbe722facd Merge pull request #553 from ekovi/patch-3
Update app.scss
2014-02-27 16:16:29 +01:00
Athou
1897d8e0c0 Merge pull request #552 from ekovi/patch-2
_dark.scss
2014-02-27 16:16:27 +01:00
ekovi
3745a152aa Update controllers.js
added theme entry
2014-02-26 21:15:53 +01:00
ekovi
a7731acb08 Update app.scss
added theme entry
2014-02-26 21:11:56 +01:00
ekovi
16dd5deed4 update
forgot to enclose in #theme {}
2014-02-26 21:09:24 +01:00
ekovi
c9f70650a0 Create _dark.scss
new theme
2014-02-26 21:07:31 +01:00
Athou
eaa84253df dependencies update 2014-02-26 16:14:11 +01:00
Athou
45abcd7385 instantiate whitelist only once 2014-02-26 08:37:00 +01:00
Athou
8a633aa648 if link is empty, use guid instead if able (fix #551) 2014-02-26 08:36:40 +01:00
Athou
05e092062d wicket upgrade 2014-02-26 08:36:15 +01:00
Athou
e83602a05c load angular main js file first 2014-02-25 06:29:17 +01:00
Athou
abf8666e24 angularjs update 2014-02-25 06:15:45 +01:00
Athou
af1ccc6669 1.5.0-snapshot 2014-02-24 16:04:41 +01:00
Athou
cdcbfbff68 stable enough, time to tag 2014-02-24 16:03:38 +01:00
Athou
6860940afc fix javax.net.ssl.SSLProtocolException: handshake alert:
unrecognized_name (fix #549)
2014-02-22 13:12:55 +01:00
Athou
bfc2ee3663 readme update 2014-02-20 11:22:42 +01:00
Athou
b104622081 switch to bonecp 2014-02-20 10:32:16 +01:00
Athou
a861387bd7 handle null categories 2014-02-14 15:41:17 +01:00
Athou
b0f2260fad dependencies update 2014-02-12 21:02:00 +01:00
Athou
97f0d98ffd Merge pull request #544 from ebraminio/master
Update Persian translation
2014-02-03 04:23:11 -08:00
Ebrahim Byagowi
1ad58a029c Update Persian translation 2014-02-03 15:49:09 +03:30
Athou
4c27da0433 propagate exception 2014-02-02 12:30:41 +01:00
Athou
faf69b43c3 fix aspect ratio for large images 2014-02-02 12:19:52 +01:00
Athou
7fff561268 switch to dbcp as tomcat-pool seems to leak connections 2014-01-09 09:13:30 +01:00
Athou
5e1360a65b smarter log cleanup script (#533) 2014-01-07 12:02:15 +01:00
Athou
cc92d2f546 Merge pull request #537 from Busimus/patch-3
Fixed typo.
2014-01-07 02:53:14 -08:00
Athou
def75a250f Merge pull request #538 from Busimus/patch-6
Updated ru.properties
2014-01-07 02:52:53 -08:00
Alexander Bus
15cd7caf9b Update ru.properties 2014-01-06 23:05:41 +07:00
Alexander Bus
41a51530ef Fixed typo. 2014-01-06 22:14:13 +07:00
Athou
3a101941b3 mark as read when swiping entry title to the right 2013-12-18 18:36:22 +01:00
Athou
0976fee4df fix left padding on mobile 2013-12-13 17:42:40 +01:00
Athou
f87da777da improved support for fxos 2013-12-13 17:29:48 +01:00
Athou
e1c2bf0890 Merge pull request #534 from JKakku/patch-3
Translated confirmation messages
2013-12-12 22:46:25 -08:00
JKakku
b829defb30 Translated confirmation messages 2013-12-13 01:12:41 +02:00
Athou
fa8770d2a7 restore main content padding 2013-12-12 15:12:43 +01:00
Athou
222c8a65af git plugin update 2013-12-12 13:11:07 +01:00
Athou
76f5b67ac4 openshift changes (fix #532) 2013-12-12 11:51:48 +01:00
Athou
1791d49efe resizeable subscription list 2013-12-12 11:51:48 +01:00
Athou
64e1b5df09 fix sync during development 2013-12-12 10:25:08 +01:00
Athou
e1ff077623 Merge pull request #531 from LpSamuelm/patch-20
Translated new labels to Swedish
2013-12-11 01:24:42 -08:00
LpSamuelm
1361072558 Translated new labels to Swedish
Translatin' labels! Oh yeah!
2013-12-11 10:23:20 +01:00
Athou
5119434d21 more metrics 2013-12-10 14:02:06 +01:00
Athou
b29540b14e first add feeds from the queue, then if needed fetch feeds from the database to fill the batch 2013-12-10 11:08:52 +01:00
Athou
e69785bb89 smaller margins on mobile 2013-12-10 09:28:58 +01:00
Athou
76465fee07 remove horizontal scrolling on mobile 2013-12-06 04:12:19 +01:00
Athou
b52c459ebb calculate offset correctly for tags and starred listing (fix #530) 2013-12-05 12:25:17 +01:00
Athou
1d73982545 apply where clause when predicate list has been populated 2013-12-05 12:23:59 +01:00
Athou
74f6c45f36 Merge pull request #527 from evenorbert/patch-2
Update hu.properties
2013-12-02 07:20:56 -08:00
Norbert Evenich
0490b528e4 Update hu.properties
Updated hungarian translation.
2013-12-02 16:07:26 +01:00
Athou
ffa1e14449 feed url autofocus 2013-11-29 16:13:26 +01:00
Athou
b8fe89b2f4 tomee upgrade 2013-11-29 16:10:43 +01:00
Athou
94b293202c support for meego devices 2013-11-29 12:19:55 +01:00
Athou
7ef143a642 mousetrap upgrade 2013-11-29 11:24:35 +01:00
Athou
057f6916e9 spinjs upgrade 2013-11-29 11:24:29 +01:00
Athou
e24e892cb3 jquery upgrade 2013-11-29 11:22:03 +01:00
Athou
78976b06e2 lodash upgrade 2013-11-29 11:21:42 +01:00
Athou
96cfcd5b2b angularjs upgrade 2013-11-29 10:15:29 +01:00
Athou
12bda0122c make images fit available width 2013-11-28 19:35:31 +01:00
Athou
4ac4e5abf2 commons collection upgrade to 4.0 2013-11-28 16:56:52 +01:00
Athou
268f0f53a8 close tree when button clicked on mobile 2013-11-28 11:58:53 +01:00
Athou
71521f3428 fix mobile layout 2013-11-28 10:45:10 +01:00
Athou
6101fb2bef new translations 2013-11-28 10:12:52 +01:00
Athou
8f6aa0896b fix bootstrap dialogs 2013-11-28 10:12:10 +01:00
Athou
b8f0af5b2e fix radio buttons 2013-11-28 09:15:14 +01:00
Athou
32730f6c41 force full refreshes only when under heavy load 2013-11-27 15:54:23 +01:00
Athou
7caa99f8f2 bootstrap3 2013-11-27 15:54:23 +01:00
Athou
4f8e2ab478 limit transaction size 2013-11-27 08:07:44 +01:00
Athou
5c44f392ca remove warning 2013-11-26 15:11:44 +01:00
Athou
174d21fd4e move logic to user service 2013-11-26 15:09:32 +01:00
Athou
c2ed6d47f1 force a full refresh of the user's feeds when he logs in 2013-11-26 07:05:59 +01:00
Athou
0f6f717d09 tweaking batch size again 2013-11-20 11:33:21 +01:00
Athou
d7fb637f68 same batch size for all operations 2013-11-16 11:27:48 +01:00
Athou
fce9086b27 remove deprecated duplicate feed detection 2013-11-16 07:40:44 +01:00
Athou
97586cd2c8 batch delete entries too 2013-11-16 07:30:01 +01:00
Athou
b74458f0b0 more logging 2013-11-15 21:38:11 +01:00
Athou
7c7a0fceaf reduce batch size for feeds 2013-11-15 15:53:22 +01:00
Athou
425a8880cd smaller preview image 2013-11-14 15:27:31 +01:00
Athou
23fe90ec64 fix log message 2013-11-14 14:41:41 +01:00
Athou
c01ec5d039 fix openshift log cleanup script 2013-11-14 13:02:24 +01:00
Athou
4f284165c2 more database cleanup tasks 2013-11-14 12:56:08 +01:00
Athou
2a62ccff11 jackson upgrade 2013-11-14 12:42:07 +01:00
Athou
d09cf472dd disable services not needed 2013-11-13 16:26:23 +01:00
Athou
5c721ae6f5 prevent scanning classes twice 2013-11-13 16:18:34 +01:00
Athou
2bb8fcdb5f scan our classes only 2013-11-13 16:17:10 +01:00
Athou
6eda93098b trust should be the last filter 2013-11-12 15:52:30 +01:00
Athou
6344f554d6 rewrite iframes to use https if commafeed uses https 2013-11-12 15:44:56 +01:00
Athou
7e4c1f374c use latest wro4j in prod profile, using latest jruby version (fix #315) 2013-11-12 11:49:11 +01:00
Athou
28eaab7f7d trust enclosure urls 2013-11-12 11:35:22 +01:00
Athou
1937944f7e fix search 2013-11-12 09:57:59 +01:00
Athou
3b4b84fdab formatting 2013-11-12 09:45:26 +01:00
Athou
32325bb49c angularjs 1.2.0 upgrade 2013-11-12 09:43:42 +01:00
Athou
c01c1e93f9 lombok upgrade 2013-11-12 08:53:38 +01:00
Athou
eac096019f jsoup upgrade 2013-11-12 08:53:22 +01:00
Athou
9f9389e846 liquibase upgrade 2013-11-12 08:53:05 +01:00
Athou
a71317881f wicket upgrade 2013-11-12 08:52:46 +01:00
Athou
7092824c96 grid dates formatting (fix #421) 2013-11-08 11:37:45 +01:00
Athou
0ff998bbd7 bigger items on mobile 2013-11-08 11:37:45 +01:00
Athou
fc318ad211 smarter mobile detection (fix #255 and fix #487) 2013-11-08 11:37:44 +01:00
Athou
73323335cb cosmetic fix 2013-11-08 11:37:44 +01:00
Athou
ef57c5523d Merge pull request #523 from utimukat55/translate_ja
Add translation ja
2013-11-07 06:10:46 -08:00
utimukat55
846f4a7222 Add translation ja 2013-11-07 21:27:14 +09:00
Athou
05036778d6 httpclient upgrade 2013-11-05 15:27:19 +01:00
Athou
52df661238 mysql jdbc driver update 2013-10-29 13:26:39 +01:00
Athou
7957dc237e for generated feeds, set 'all' as default instead of 'unread' 2013-10-29 07:05:19 +01:00
Athou
3fe419ba2f disable openejb stats 2013-10-24 14:50:51 +02:00
Athou
61944656b8 allow same query parameters for entriesAsFeed (fix #521) 2013-10-24 09:50:35 +02:00
Athou
1cb997b66d moved query 2013-10-24 09:50:35 +02:00
Athou
89463808db return exception stacktrace in the body 2013-10-23 07:55:14 +02:00
Athou
6aca66d8cf prevent NPE 2013-10-20 17:12:53 +02:00
Athou
38f8102fb3 readability support (fix #108) 2013-10-20 15:26:58 +02:00
Athou
e709499240 Merge pull request #520 from LpSamuelm/patch-19
Translated new labels to Swedish
2013-10-15 18:34:45 -07:00
LpSamuelm
0b714d5e52 Translated new labels to Swedish
WOAH TAGS
2013-10-16 03:32:12 +02:00
Athou
98e4f0c6dc Merge pull request #519 from ekovi/patch-1
updated
2013-10-15 11:18:32 -07:00
ekovi
d82d0af565 updated 2013-10-15 19:21:00 +02:00
Athou
d8abb7039d Merge pull request #518 from JKakku/patch-2
Update fi.properties
2013-10-15 09:22:26 -07:00
JKakku
84dc11048d Update fi.properties
Added the missing stuff since the last update couple of months ago.
2013-10-15 18:17:31 +03:00
Athou
bad915bbaa fix warnings 2013-10-14 15:57:35 +02:00
Athou
287dea2d36 use tag for feed name is available 2013-10-14 07:41:19 +02:00
Athou
a0b937769d autofocus the input when it appears 2013-10-14 07:01:14 +02:00
Athou
6acef4a406 fix mark up to link positioning on mobile 2013-10-14 02:44:48 +02:00
Athou
8b77eb9850 add valid checksum (fix #517) 2013-10-14 02:15:03 +02:00
Athou
6f22836dcb remove double slash in image 2013-10-13 21:34:53 +02:00
Athou
a4347c8878 highlight tag if selected 2013-10-13 16:02:59 +02:00
Athou
836f7eff09 better index usage 2013-10-13 12:15:41 +02:00
Athou
c993bd472d cleanup 2013-10-13 12:04:29 +02:00
Athou
431ab92a02 tagging support (#96) 2013-10-13 11:58:22 +02:00
Athou
94f469a6b1 js dependencies update 2013-10-13 10:49:03 +02:00
Athou
3fec1c6890 dependencies update 2013-10-12 17:41:07 +02:00
Athou
f8316911bd another typo 2013-10-12 14:57:01 +02:00
Athou
642d1f6be5 typo 2013-10-12 14:56:24 +02:00
Athou
5a82c3a130 Merge pull request #515 from rthome/master
Translate new strings to German
2013-10-12 00:10:15 -07:00
Raffael Thome
6a8174afac Translate new strings to German 2013-10-12 08:17:49 +02:00
Athou
f4c86634f7 liquibase upgrade, removing dirty fix 2013-10-10 10:07:16 +02:00
Athou
322e588a4e Merge pull request #514 from LpSamuelm/patch-18
Translated new labels to Swedish
2013-10-07 22:06:03 -07:00
Athou
822dee7a13 scale better, don't block when the pool is exhausted 2013-10-08 07:05:31 +02:00
LpSamuelm
101e179788 Translated new labels to Swedish 2013-10-07 21:04:35 +02:00
Athou
57abee6cf0 use the url of the feed as the base url to resolve relative entry links when the declared link in the feed is relative 2013-10-03 12:42:05 +02:00
Athou
b615847b09 make sure entries with same update date are always sorted the same way 2013-10-03 11:05:13 +02:00
Athou
ffef87e249 customizable scrolling speed 2013-10-03 10:40:58 +02:00
Athou
ba3b8df4c9 mark older than half a day 2013-10-03 10:02:16 +02:00
Athou
40175d3e54 dependencies update 2013-09-25 13:51:09 +02:00
Athou
06b047cfe6 1.4.0 snapshot 2013-09-25 13:48:29 +02:00
Athou
1f4d62ab47 1.3.0 release 2013-09-25 13:47:13 +02:00
Athou
a7b826bd4f prevent unintentional entry list reset 2013-09-20 08:11:28 +02:00
Athou
407481faa6 delete operation does not support limit. limit on select and delete afterwards 2013-09-18 09:24:31 +02:00
Athou
305b68546c create a new transaction for each delete chunk 2013-09-18 09:24:05 +02:00
Athou
136c41c6aa delete old read statuses by chunks in order to avoid large transactions 2013-09-17 13:01:27 +02:00
Athou
587b25b18b Merge pull request #510 from Cymrodor/patch-1
Update cy.properties
2013-09-16 19:40:29 -07:00
Cymrodor
beaa40ad65 Update cy.properties
Ychwanegu, cwtogi, cywiro a thacluso.
2013-09-16 23:21:39 +01:00
Athou
1389a5a238 readme update 2013-09-16 20:32:37 +02:00
Athou
2f34ff8a9f prevent NPE if session does not exist 2013-09-16 07:01:47 +02:00
Athou
d3626b0e7c reduce blockquotes font size 2013-09-10 19:07:17 +02:00
Athou
bb4529b6f1 improve scrolling performance by registering events only once instead of once per entry 2013-09-10 16:12:39 +02:00
Athou
dd94125d52 remove unneeded synchronization locks on settings 2013-09-08 19:08:26 +02:00
Athou
a7149e3740 don't start a new reporter every time the registry is injected 2013-09-05 16:30:14 +02:00
Athou
b64d041385 Merge pull request #505 from ekovi/patch-3
Update _svetla.scss
2013-09-01 01:36:17 -07:00
Athou
cc04bdfbc5 Merge pull request #504 from LpSamuelm/patch-17
Translated new labels to Swedish
2013-09-01 01:34:53 -07:00
Athou
d8c772ed5e compact forms 2013-09-01 10:33:36 +02:00
Athou
dfcc4eeebd return an error message when feed/category is not found instead of returning an empty feed/category 2013-09-01 10:33:35 +02:00
ekovi
e491841d4a Update _svetla.scss
some changes and fixes
2013-08-30 20:37:36 +02:00
LpSamuelm
ccb72837b3 Translated new labels to Swedish 2013-08-30 09:24:47 +02:00
Athou
6560fc9d05 display gauges as well 2013-08-23 14:12:13 +02:00
Athou
14d5879735 fix issue where only the first directive was shown 2013-08-23 13:40:23 +02:00
Athou
7fa8bef3de initial metrics page setup 2013-08-23 12:58:24 +02:00
Athou
966caae727 store and use urlAfterRedirect if different than the actual url 2013-08-22 15:55:05 +02:00
Athou
a14484ee03 retrieve the final url after potential http 30x redirect 2013-08-22 15:36:04 +02:00
Athou
fb9b42ab12 added log4j entry for metrics 2013-08-22 15:27:24 +02:00
Athou
6974abdb95 don't compare strings with == 2013-08-22 12:04:00 +02:00
Athou
65efdeb1df wicket update 2013-08-22 09:13:56 +02:00
Athou
54a39ea0a9 fix scrolling issues on some mobile devices (#482) 2013-08-22 09:13:56 +02:00
Athou
641350cbde detect categories in opml files by checking if they have children 2013-08-22 06:20:44 +02:00
Athou
06ece8f5ee Merge pull request #497 from ekovi/patch-1
translation of additional entries
2013-08-21 20:44:32 -07:00
ekovi
ca87f1c47a translation of additional entries 2013-08-21 21:17:06 +02:00
Athou
c38ddb5d00 add a note about hsqldb data location (fix #496) 2013-08-21 13:04:12 +02:00
Athou
1acd7c4a01 set serialid 2013-08-20 09:47:08 +02:00
Athou
d92c2ebdf7 measure refill rate 2013-08-18 17:19:01 +02:00
Athou
8f19e9408e report through jmx 2013-08-18 17:13:45 +02:00
Athou
3ecb47da5a use timers instead of meters 2013-08-18 16:42:01 +02:00
Athou
ae03b42c6d pretty print response if method is annotated with @PrettyPrint or 'pretty' req param is set to true 2013-08-18 16:29:41 +02:00
Athou
ee4eb9bb07 use codahale metrics library instead of our own 2013-08-18 16:29:07 +02:00
Athou
a0be2e0879 added gmail social sharing button 2013-08-17 21:55:29 +02:00
Athou
a3414d7156 let's use snapshots 2013-08-17 13:47:14 +02:00
209 changed files with 6550 additions and 4034 deletions

View File

@@ -261,7 +261,7 @@
<property name="external_port">${env.OPENSHIFT_JBOSSEAP_CLUSTER_PROXY_PORT}
</property>
<property name="bind_port">7600</property>
<property name="bind_addr">${env.OPENSHIFT_INTERNAL_IP}</property>
<property name="bind_addr">${env.OPENSHIFT_JBOSSEAP_IP}</property>
</transport>
<protocol type="TCPPING">
<property name="timeout">3000</property>
@@ -476,15 +476,15 @@
<interfaces>
<interface name="management">
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" />
<loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
</interface>
<interface name="public">
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" />
<loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
</interface>
<interface name="unsecure">
<!-- Used for IIOP sockets in the standarad configuration. To secure JacORB
you need to setup SSL -->
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" />
<loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
</interface>
</interfaces>

View File

@@ -1 +1,7 @@
rm -rf $OPENSHIFT_JBOSSAS_LOG_DIR\*.log.*
if [ $OPENSHIFT_JBOSSAS_LOG_DIR ]; then
rm -rf $OPENSHIFT_JBOSSAS_LOG_DIR/*.log.*
fi
if [ $OPENSHIFT_JBOSSEAP_LOG_DIR ]; then
rm -rf $OPENSHIFT_JBOSSEAP_LOG_DIR/*.log.*
fi

View File

@@ -41,7 +41,7 @@ To install maven and openjdk on Ubuntu, issue the following commands
sudo apt-get update
sudo apt-get install openjdk-7-jdk maven3
Not required but if you don't, use 'mvn3' instead of 'mvn' for the rest of the instructions.
# Not required but if you don't, use 'mvn3' instead of 'mvn' for the rest of the instructions.
sudo ln -s /usr/bin/mvn3 /usr/bin/mvn
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.
@@ -54,16 +54,16 @@ If you don't have git you can download the sources as a zip file from [here](htt
Now build the application
Embedded HSQL database:
# Embedded HSQL database:
mvn clean package tomee:build -Pprod
External MySQL database:
# External MySQL database:
mvn clean package tomee:build -Pprod -Pmysql
External PostgreSQL database:
# External PostgreSQL database:
mvn clean package tomee:build -Pprod -Ppgsql
External Microsoft SQL Server database:
# External Microsoft SQL Server database:
mvn clean package tomee:build -Pprod -Pmssql
It will generate a zip file at `target/commafeed.zip` with everything you need to run the application.
@@ -74,12 +74,15 @@ It will generate a zip file at `target/commafeed.zip` with everything you need t
* If you don't use the embedded database, create a database in your external database instance, then uncomment the `Resource` element corresponding to the database engine you use from `conf/tomee.xml` and edit the default credentials.
* If you'd like to change the default port (8082), edit `conf/server.xml` and look for `<Connector port="8082" protocol="HTTP/1.1"`. Change the port to the value you'd like to use.
* CommaFeed will run on the `/commafeed` context. If you'd like to change the context, go to `webapps` and rename `commafeed.war`. Use the special name `ROOT.war` to deploy to the root context.
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
If you use the embedded database, note that the database file will be created in the current directory, so make sure you always start the app in the same directory. You can optionally set an absolute path instead of a relative one in `tomee.xml`.
* To update the application with a newer version, pull the latest changes and use the same command you used to build the complete TomEE package, but without the `tomee:build` part (keep `-Pprod -P<database>`).
This will generate the file `target/commafeed.war`. Copy this file to your tomee `webapps/` directory.
* The application is online at [http://localhost:8082/commafeed](http://localhost:8082/commafeed). Don't forget to set the public URL in the admin settings.
* The default user is `admin` and the password is `admin`.
You can use nginx or apache as a proxy http server. Note that when using apache, the `ProxyPreserveHost on` option should be set in your config file.
Local development
-----------------

View File

@@ -1 +1 @@
set JAVA_OPTS=-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC
set JAVA_OPTS=-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Djsse.enableSNIExtension=false

View File

@@ -1 +1 @@
export JAVA_OPTS="-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC"
export JAVA_OPTS="-Djava.net.preferIPv4Stack=true -Xmx1024m -XX:MaxPermSize=256m -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Djsse.enableSNIExtension=false"

137
pom.xml
View File

@@ -4,7 +4,7 @@
<groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId>
<version>1.2.0</version>
<version>1.5.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>CommaFeed</name>
@@ -79,7 +79,7 @@
<artifactId>tomee-maven-plugin</artifactId>
<version>1.5.2</version>
<configuration>
<tomeeVersion>1.5.2</tomeeVersion>
<tomeeVersion>1.6.0</tomeeVersion>
<tomeeClassifier>plus</tomeeClassifier>
<tomeeHttpPort>8082</tomeeHttpPort>
<args>-Xmx1024m -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled</args>
@@ -104,13 +104,18 @@
<lib>org.hibernate.common:hibernate-commons-annotations:4.0.1.Final</lib>
<lib>org.hibernate:hibernate-validator:4.3.1.Final</lib>
<lib>org.jboss.logging:jboss-logging:3.1.3.GA</lib>
<lib>org.javassist:javassist:3.15.0-GA</lib>
<lib>org.apache.openejb:openejb-bonecp:4.6.0</lib>
<lib>com.jolbox:bonecp:0.8.0.RELEASE</lib>
<lib>com.google.guava:guava:14.0.1</lib>
<lib>dom4j:dom4j:1.6.1</lib>
<lib>antlr:antlr:2.7.7</lib>
<lib>remove:openjpa-</lib>
<lib>remove:hsqldb</lib>
<lib>org.hsqldb:hsqldb:2.3.0</lib>
<lib>mysql:mysql-connector-java:5.1.24</lib>
<lib>mysql:mysql-connector-java:5.1.26</lib>
<lib>postgresql:postgresql:9.1-901.jdbc4</lib>
<lib>net.sourceforge.jtds:jtds:1.3.1</lib>
@@ -169,7 +174,7 @@
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.1.5</version>
<version>2.1.7</version>
<executions>
<execution>
<goals>
@@ -189,7 +194,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.12.0</version>
<version>1.12.6</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -214,28 +219,28 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.1.0</version>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>3.0.2</version>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>14.0.1</version>
<version>16.0.1</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.8</version>
<version>1.9</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
@@ -260,7 +265,7 @@
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3</version>
<version>1.3.1</version>
</dependency>
<dependency>
@@ -298,34 +303,34 @@
<dependency>
<groupId>com.google.gwt</groupId>
<artifactId>gwt-servlet</artifactId>
<version>2.5.1</version>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cssparser</groupId>
<artifactId>cssparser</artifactId>
<version>0.9.9</version>
<version>0.9.13</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.5</version>
<version>4.3.3</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.7.2</version>
<version>1.7.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.2</version>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
@@ -336,28 +341,28 @@
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-core</artifactId>
<version>6.9.1</version>
<version>6.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-auth-roles</artifactId>
<version>6.9.1</version>
<version>6.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-extensions</artifactId>
<version>6.9.1</version>
<version>6.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-cdi</artifactId>
<version>6.9.1</version>
<version>6.14.0</version>
</dependency>
<dependency>
<groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-extensions</artifactId>
<version>1.6.3</version>
<version>1.7.5</version>
</dependency>
<dependency>
@@ -372,6 +377,88 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>lodash</artifactId>
<version>2.4.1-3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery-mousewheel</artifactId>
<version>3.1.9</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angularjs</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angular-ui-router</artifactId>
<version>0.2.8-2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angular-ui-utils</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>ui-select2</artifactId>
<version>0.0.5</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angular-ui-bootstrap</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>mousetrap</artifactId>
<version>1.4.6</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>momentjs</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>ng-grid</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>device.js</artifactId>
<version>139f208</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>ngInfiniteScroll</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@@ -391,7 +478,7 @@
<plugin>
<groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-maven-plugin</artifactId>
<version>1.6.3</version>
<version>1.7.5</version>
<executions>
<execution>
<id>js</id>
@@ -401,7 +488,7 @@
</goals>
<configuration>
<targetGroups>app</targetGroups>
<options>indent,devel,noarg,quotmark,laxcomma,laxbreak</options>
<options>devel,noarg,quotmark,laxcomma,laxbreak</options>
</configuration>
</execution>
<execution>
@@ -538,7 +625,7 @@
<plugin>
<groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-maven-plugin</artifactId>
<version>1.6.3</version>
<version>1.7.5</version>
<executions>
<execution>
<goals>

View File

@@ -4,42 +4,45 @@ import java.io.IOException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.DecompressingHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.SystemDefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.wicket.util.io.IOUtils;
/**
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers
@@ -52,8 +55,32 @@ public class HttpGetter {
private static final String ACCEPT_LANGUAGE = "en";
private static final String PRAGMA_NO_CACHE = "No-cache";
private static final String CACHE_CONTROL_NO_CACHE = "no-cache";
private static final String UTF8 = "UTF-8";
private static final String HTTPS = "https";
private static final List<String> ALLOWED_CONTENT_ENCODINGS = Arrays.asList("gzip", "x-gzip", "deflate", "identity");
private static final HttpResponseInterceptor REMOVE_INCORRECT_CONTENT_ENCODING = new HttpResponseInterceptor() {
@Override
public void process(HttpResponse response, HttpContext context) throws HttpException, IOException {
HttpEntity entity = response.getEntity();
if (entity != null && entity.getContentLength() != 0) {
Header header = entity.getContentEncoding();
if (header != null) {
HeaderElement[] codecs = header.getElements();
for (final HeaderElement codec : codecs) {
String codecName = codec.getName().toLowerCase(Locale.US);
if (!ALLOWED_CONTENT_ENCODINGS.contains(codecName)) {
response.setEntity(new HttpEntityWrapper(entity) {
@Override
public Header getContentEncoding() {
return null;
};
});
}
}
}
}
}
};
private static SSLContext SSL_CONTEXT = null;
static {
@@ -65,8 +92,6 @@ public class HttpGetter {
}
}
private static final X509HostnameVerifier VERIFIER = new DefaultHostnameVerifier();
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
return getBinary(url, null, null, timeout);
}
@@ -90,9 +115,12 @@ public class HttpGetter {
HttpResult result = null;
long start = System.currentTimeMillis();
HttpClient client = newClient(timeout);
CloseableHttpClient client = newClient(timeout);
CloseableHttpResponse response = null;
try {
HttpGet httpget = new HttpGet(url);
HttpClientContext context = HttpClientContext.create();
httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE);
httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE);
httpget.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
@@ -105,9 +133,8 @@ public class HttpGetter {
httpget.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
}
HttpResponse response = null;
try {
response = client.execute(httpget);
response = client.execute(httpget, context);
int code = response.getStatusLine().getStatusCode();
if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("'304 - not modified' http code received");
@@ -123,15 +150,14 @@ public class HttpGetter {
}
}
Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
String lastModifiedResponse = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
if (lastModified != null && StringUtils.equals(lastModified, lastModifiedResponse)) {
String lastModifiedHeaderValue = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
if (lastModifiedHeaderValue != null && StringUtils.equals(lastModified, lastModifiedHeaderValue)) {
throw new NotModifiedException("lastModifiedHeader is the same");
}
String eTagResponse = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
if (eTag != null && StringUtils.equals(eTag, eTagResponse)) {
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
String eTagHeaderValue = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
if (eTag != null && StringUtils.equals(eTag, eTagHeaderValue)) {
throw new NotModifiedException("eTagHeader is the same");
}
@@ -144,12 +170,15 @@ public class HttpGetter {
contentType = entity.getContentType().getValue();
}
}
HttpUriRequest req = (HttpUriRequest) context.getRequest();
HttpHost host = context.getTargetHost();
String urlAfterRedirect = req.getURI().isAbsolute() ? req.getURI().toString() : host.toURI() + req.getURI();
long duration = System.currentTimeMillis() - start;
result = new HttpResult(content, contentType, lastModifiedHeader == null ? null : lastModifiedHeader.getValue(),
eTagHeader == null ? null : eTagHeader.getValue(), duration);
result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect);
} finally {
client.getConnectionManager().shutdown();
IOUtils.closeQuietly(response);
IOUtils.closeQuietly(client);
}
return result;
}
@@ -161,13 +190,15 @@ public class HttpGetter {
private String lastModifiedSince;
private String eTag;
private long duration;
private String urlAfterRedirect;
public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration) {
public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration, String urlAfterRedirect) {
this.content = content;
this.contentType = contentType;
this.lastModifiedSince = lastModifiedSince;
this.eTag = eTag;
this.duration = duration;
this.urlAfterRedirect = urlAfterRedirect;
}
public byte[] getContent() {
@@ -190,23 +221,30 @@ public class HttpGetter {
return duration;
}
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
}
public static HttpClient newClient(int timeout) {
DefaultHttpClient client = new SystemDefaultHttpClient();
public static CloseableHttpClient newClient(int timeout) {
HttpClientBuilder builder = HttpClients.custom();
builder.useSystemProperties();
builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING);
builder.disableAutomaticRetries();
SSLSocketFactory ssf = new SSLSocketFactory(SSL_CONTEXT, VERIFIER);
ClientConnectionManager ccm = client.getConnectionManager();
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme(HTTPS, 443, ssf));
builder.setSslcontext(SSL_CONTEXT);
builder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpParams params = client.getParams();
HttpClientParams.setCookiePolicy(params, CookiePolicy.IGNORE_COOKIES);
HttpProtocolParams.setContentCharset(params, UTF8);
HttpConnectionParams.setConnectionTimeout(params, timeout);
HttpConnectionParams.setSoTimeout(params, timeout);
client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
return new DecompressingHttpClient(client);
RequestConfig.Builder configBuilder = RequestConfig.custom();
configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);
configBuilder.setSocketTimeout(timeout);
configBuilder.setConnectTimeout(timeout);
configBuilder.setConnectionRequestTimeout(timeout);
builder.setDefaultRequestConfig(configBuilder.build());
builder.setDefaultConnectionConfig(ConnectionConfig.custom().setCharset(Consts.ISO_8859_1).build());
return builder.build();
}
public static class NotModifiedException extends Exception {
@@ -232,24 +270,4 @@ public class HttpGetter {
return null;
}
}
private static class DefaultHostnameVerifier implements X509HostnameVerifier {
@Override
public void verify(String string, SSLSocket ssls) throws IOException {
}
@Override
public void verify(String string, X509Certificate xc) throws SSLException {
}
@Override
public void verify(String string, String[] strings, String[] strings1) throws SSLException {
}
@Override
public boolean verify(String string, SSLSession ssls) {
return true;
}
};
}

View File

@@ -1,179 +0,0 @@
package com.commafeed.backend;
import javax.ejb.Singleton;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
@Singleton
public class MetricsBean {
@PersistenceContext
EntityManager em;
private Metric lastMinute = new Metric();
private Metric thisMinute = new Metric();
private Metric lastHour = new Metric();
private Metric thisHour = new Metric();
private long minuteTimestamp;
private long hourTimestamp;
@AroundInvoke
private Object roll(InvocationContext context) throws Exception {
long now = System.currentTimeMillis();
if (now - minuteTimestamp > 60000) {
lastMinute = thisMinute;
thisMinute = new Metric();
minuteTimestamp = now;
}
if (now - hourTimestamp > 60000 * 60) {
lastHour = thisHour;
thisHour = new Metric();
hourTimestamp = now;
}
return context.proceed();
}
public void feedRefreshed() {
thisMinute.feedsRefreshed++;
thisHour.feedsRefreshed++;
}
public void feedUpdated() {
thisHour.feedsUpdated++;
thisMinute.feedsUpdated++;
}
public void entryInserted() {
thisHour.entriesInserted++;
thisMinute.entriesInserted++;
}
public void entryCacheHit() {
thisHour.entryCacheHit++;
thisMinute.entryCacheHit++;
}
public void entryCacheMiss() {
thisHour.entryCacheMiss++;
thisMinute.entryCacheMiss++;
}
public void pushReceived(int feedCount) {
thisHour.pushNotificationsReceived++;
thisMinute.pushNotificationsReceived++;
thisHour.pushFeedsQueued += feedCount;
thisMinute.pushFeedsQueued += feedCount;
}
public void threadWaited() {
thisHour.threadWaited++;
thisMinute.threadWaited++;
}
public Metric getLastMinute() {
return lastMinute;
}
public Metric getLastHour() {
return lastHour;
}
public String getCacheStats() {
Session session = em.unwrap(Session.class);
SessionFactory sessionFactory = session.getSessionFactory();
Statistics statistics = sessionFactory.getStatistics();
return statistics.toString();
}
public static class Metric {
private int feedsRefreshed;
private int feedsUpdated;
private int entriesInserted;
private int threadWaited;
private int pushNotificationsReceived;
private int pushFeedsQueued;
private int entryCacheHit;
private int entryCacheMiss;
public int getFeedsRefreshed() {
return feedsRefreshed;
}
public void setFeedsRefreshed(int feedsRefreshed) {
this.feedsRefreshed = feedsRefreshed;
}
public int getFeedsUpdated() {
return feedsUpdated;
}
public void setFeedsUpdated(int feedsUpdated) {
this.feedsUpdated = feedsUpdated;
}
public int getEntriesInserted() {
return entriesInserted;
}
public void setEntriesInserted(int entriesInserted) {
this.entriesInserted = entriesInserted;
}
public int getThreadWaited() {
return threadWaited;
}
public void setThreadWaited(int threadWaited) {
this.threadWaited = threadWaited;
}
public int getPushNotificationsReceived() {
return pushNotificationsReceived;
}
public void setPushNotificationsReceived(int pushNotificationsReceived) {
this.pushNotificationsReceived = pushNotificationsReceived;
}
public int getPushFeedsQueued() {
return pushFeedsQueued;
}
public void setPushFeedsQueued(int pushFeedsQueued) {
this.pushFeedsQueued = pushFeedsQueued;
}
public int getEntryCacheHit() {
return entryCacheHit;
}
public void setEntryCacheHit(int entryCacheHit) {
this.entryCacheHit = entryCacheHit;
}
public int getEntryCacheMiss() {
return entryCacheMiss;
}
public void setEntryCacheMiss(int entryCacheMiss) {
this.entryCacheMiss = entryCacheMiss;
}
}
}

View File

@@ -4,36 +4,45 @@ import java.util.Date;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.DatabaseCleaningService;
/**
* Contains all scheduled tasks
*
*/
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ScheduledTasks {
@Inject
ApplicationSettingsService applicationSettingsService;
@Inject
DatabaseCleaner cleaner;
@PersistenceContext
EntityManager em;
DatabaseCleaningService cleaner;
/**
* clean old read statuses, runs every day at midnight
* clean old read statuses
*/
@Schedule(hour = "0", persistent = false)
@Schedule(hour = "*", persistent = false)
private void cleanupOldStatuses() {
Date threshold = applicationSettingsService.getUnreadThreshold();
if (threshold != null) {
cleaner.cleanStatusesOlderThan(threshold);
}
}
/**
* clean feeds without subscriptions, then clean contents without entries
*/
@Schedule(hour = "*", persistent = false)
private void cleanFeedsAndContents() {
cleaner.cleanEntriesWithoutSubscriptions();
cleaner.cleanFeedsWithoutSubscriptions();
cleaner.cleanContentsWithoutEntries();
}
}

View File

@@ -4,9 +4,12 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import org.apache.commons.pool.impl.GenericObjectPool;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@@ -30,7 +33,14 @@ public class RedisCacheService extends CacheService {
private static ObjectMapper mapper = new ObjectMapper();
private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
private JedisPool pool;
@PostConstruct
private void init() {
JedisPoolConfig config = new JedisPoolConfig();
config.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_GROW);
pool = new JedisPool(config, "localhost");
}
@Override
public List<String> getLastEntries(Feed feed) {

View File

@@ -6,15 +6,12 @@ import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import javax.persistence.criteria.Subquery;
import javax.persistence.metamodel.SingularAttribute;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
@@ -26,7 +23,6 @@ import com.commafeed.backend.model.FeedSubscription_;
import com.commafeed.backend.model.Feed_;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.User_;
import com.commafeed.frontend.model.FeedCount;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -95,8 +91,8 @@ public class FeedDAO extends GenericDAO<Feed> {
public List<Feed> findByTopic(String topic) {
return findByField(Feed_.pushTopicHash, DigestUtils.sha1Hex(topic));
}
public int deleteWithoutSubscriptions(int max) {
public List<Feed> findWithoutSubscriptions(int max) {
CriteriaQuery<Feed> query = builder.createQuery(getType());
Root<Feed> root = query.from(getType());
@@ -105,54 +101,6 @@ public class FeedDAO extends GenericDAO<Feed> {
TypedQuery<Feed> q = em.createQuery(query);
q.setMaxResults(max);
List<Feed> list = q.getResultList();
int deleted = list.size();
delete(list);
return deleted;
}
public static enum DuplicateMode {
NORMALIZED_URL(Feed_.normalizedUrlHash), LAST_CONTENT(Feed_.lastContentHash), PUSH_TOPIC(Feed_.pushTopicHash);
private SingularAttribute<Feed, String> path;
private DuplicateMode(SingularAttribute<Feed, String> path) {
this.path = path;
}
public SingularAttribute<Feed, String> getPath() {
return path;
}
}
public List<FeedCount> findDuplicates(DuplicateMode mode, int offset, int limit, long minCount) {
CriteriaQuery<String> query = builder.createQuery(String.class);
Root<Feed> root = query.from(getType());
Path<String> path = root.get(mode.getPath());
Expression<Long> count = builder.count(path);
query.select(path);
query.groupBy(path);
query.having(builder.greaterThan(count, minCount));
TypedQuery<String> q = em.createQuery(query);
limit(q, offset, limit);
List<String> pathValues = q.getResultList();
List<FeedCount> result = Lists.newArrayList();
for (String pathValue : pathValues) {
FeedCount fc = new FeedCount(pathValue);
for (Feed feed : findByField(mode.getPath(), pathValue)) {
Feed f = new Feed();
f.setId(feed.getId());
f.setUrl(feed.getUrl());
fc.getFeeds().add(f);
}
result.add(fc);
}
return result;
return q.getResultList();
}
}

View File

@@ -2,6 +2,7 @@ package com.commafeed.backend.dao;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
@@ -15,6 +16,7 @@ import com.commafeed.backend.model.FeedEntryContent_;
import com.commafeed.backend.model.FeedEntry_;
import com.google.common.collect.Iterables;
@Stateless
public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
public Long findExisting(String contentHash, String titleHash) {
@@ -44,6 +46,7 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
List<FeedEntryContent> list = q.getResultList();
int deleted = list.size();
delete(list);
return deleted;
}

View File

@@ -6,13 +6,19 @@ import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.FeedSubscription_;
import com.commafeed.backend.model.Feed_;
import com.google.common.collect.Iterables;
@@ -36,6 +42,34 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
return Iterables.getFirst(list, null);
}
public List<FeedEntry> findWithoutSubscriptions(int max) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType());
Join<FeedEntry, Feed> feedJoin = root.join(FeedEntry_.feed);
SetJoin<Feed, FeedSubscription> subJoin = feedJoin.join(Feed_.subscriptions, JoinType.LEFT);
query.where(builder.isNull(subJoin.get(FeedSubscription_.id)));
TypedQuery<FeedEntry> q = em.createQuery(query);
q.setMaxResults(max);
return q.getResultList();
}
public int delete(Feed feed, int max) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType());
query.where(builder.equal(root.get(FeedEntry_.feed), feed));
TypedQuery<FeedEntry> q = em.createQuery(query);
q.setMaxResults(max);
List<FeedEntry> list = q.getResultList();
int deleted = list.size();
delete(list);
return deleted;
}
public int delete(Date olderThan, int max) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType());

View File

@@ -15,8 +15,9 @@ import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.hibernate.Criteria;
import org.hibernate.criterion.Conjunction;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
@@ -31,6 +32,8 @@ 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.FeedEntry_;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.Models;
@@ -40,31 +43,34 @@ import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.frontend.model.UnreadCount;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
@Stateless
public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
private static final String ALIAS_STATUS = "status";
private static final String ALIAS_ENTRY = "entry";
private static final String ALIAS_TAG = "tag";
@Inject
FeedEntryTagDAO feedEntryTagDAO;
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_DESC = new Comparator<FeedEntryStatus>() {
@Override
public int compare(FeedEntryStatus o1, FeedEntryStatus o2) {
return ObjectUtils.compare(o2.getEntryUpdated(), o1.getEntryUpdated());
CompareToBuilder builder = new CompareToBuilder();
builder.append(o2.getEntryUpdated(), o1.getEntryUpdated());
builder.append(o2.getId(), o1.getId());
return builder.build();
};
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = new Comparator<FeedEntryStatus>() {
@Override
public int compare(FeedEntryStatus o1, FeedEntryStatus o2) {
return ObjectUtils.compare(o1.getEntryUpdated(), o2.getEntryUpdated());
};
};
private static final Comparator<FeedEntryStatus> STATUS_COMPARATOR_ASC = Ordering.from(STATUS_COMPARATOR_DESC).reverse();
@Inject
ApplicationSettingsService applicationSettingsService;
public FeedEntryStatus getStatus(FeedSubscription sub, FeedEntry entry) {
public FeedEntryStatus getStatus(User user, FeedSubscription sub, FeedEntry entry) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType());
@@ -77,14 +83,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
List<FeedEntryStatus> statuses = em.createQuery(query).getResultList();
FeedEntryStatus status = Iterables.getFirst(statuses, null);
return handleStatus(status, sub, entry);
return handleStatus(user, status, sub, entry);
}
private FeedEntryStatus handleStatus(FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
private FeedEntryStatus handleStatus(User user, FeedEntryStatus status, FeedSubscription sub, FeedEntry entry) {
if (status == null) {
Date unreadThreshold = applicationSettingsService.getUnreadThreshold();
boolean read = unreadThreshold == null ? false : entry.getUpdated().before(unreadThreshold);
status = new FeedEntryStatus(sub.getUser(), sub, entry);
status = new FeedEntryStatus(user, sub, entry);
status.setRead(read);
status.setMarkable(!read);
} else {
@@ -93,6 +99,12 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
return status;
}
private FeedEntryStatus fetchTags(User user, FeedEntryStatus status) {
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, status.getEntry());
status.setTags(tags);
return status;
}
public List<FeedEntryStatus> findStarred(User user, Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
@@ -102,11 +114,12 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
predicates.add(builder.equal(root.get(FeedEntryStatus_.user), user));
predicates.add(builder.equal(root.get(FeedEntryStatus_.starred), true));
query.where(predicates.toArray(new Predicate[0]));
if (newerThan != null) {
predicates.add(builder.greaterThanOrEqualTo(root.get(FeedEntryStatus_.entryInserted), newerThan));
}
query.where(predicates.toArray(new Predicate[0]));
orderStatusesBy(query, root, order);
@@ -115,13 +128,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
setTimeout(q);
List<FeedEntryStatus> statuses = q.getResultList();
for (FeedEntryStatus status : statuses) {
status = handleStatus(status, status.getSubscription(), status.getEntry());
status = handleStatus(user, status, status.getSubscription(), status.getEntry());
status = fetchTags(user, status);
}
return lazyLoadContent(includeContent, statuses);
}
private Criteria buildSearchCriteria(FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, int limit,
ReadingOrder order, Date last) {
private Criteria buildSearchCriteria(User user, FeedSubscription sub, boolean unreadOnly, String keywords, Date newerThan, int offset, int limit,
ReadingOrder order, Date last, String tag) {
Criteria criteria = getSession().createCriteria(FeedEntry.class, ALIAS_ENTRY);
criteria.add(Restrictions.eq(FeedEntry_.feed.getName(), sub.getFeed()));
@@ -139,7 +153,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
Criteria statusJoin = criteria.createCriteria(FeedEntry_.statuses.getName(), ALIAS_STATUS, JoinType.LEFT_OUTER_JOIN,
Restrictions.eq(FeedEntryStatus_.subscription.getName(), sub));
if (unreadOnly) {
if (unreadOnly && tag == null) {
Disjunction or = Restrictions.disjunction();
or.add(Restrictions.isNull(FeedEntryStatus_.read.getName()));
@@ -152,6 +166,13 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
}
if (tag != null) {
Conjunction and = Restrictions.conjunction();
and.add(Restrictions.eq(FeedEntryTag_.user.getName(), user));
and.add(Restrictions.eq(FeedEntryTag_.name.getName(), tag));
criteria.createCriteria(FeedEntry_.tags.getName(), ALIAS_TAG, JoinType.INNER_JOIN, and);
}
if (newerThan != null) {
criteria.add(Restrictions.ge(FeedEntry_.inserted.getName(), newerThan));
}
@@ -165,13 +186,11 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
if (order != null) {
Order o = null;
if (order == ReadingOrder.asc) {
o = Order.asc(FeedEntry_.updated.getName());
criteria.addOrder(Order.asc(FeedEntry_.updated.getName())).addOrder(Order.asc(FeedEntry_.id.getName()));
} else {
o = Order.desc(FeedEntry_.updated.getName());
criteria.addOrder(Order.desc(FeedEntry_.updated.getName())).addOrder(Order.desc(FeedEntry_.id.getName()));
}
criteria.addOrder(o);
}
if (offset > -1) {
criteria.setFirstResult(offset);
@@ -188,14 +207,14 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
@SuppressWarnings("unchecked")
public List<FeedEntryStatus> findBySubscriptions(List<FeedSubscription> subs, boolean unreadOnly, String keywords, Date newerThan,
int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds) {
public List<FeedEntryStatus> findBySubscriptions(User user, List<FeedSubscription> subs, boolean unreadOnly, String keywords,
Date newerThan, int offset, int limit, ReadingOrder order, boolean includeContent, boolean onlyIds, String tag) {
int capacity = offset + limit;
Comparator<FeedEntryStatus> comparator = order == ReadingOrder.desc ? STATUS_COMPARATOR_DESC : STATUS_COMPARATOR_ASC;
FixedSizeSortedSet<FeedEntryStatus> set = new FixedSizeSortedSet<FeedEntryStatus>(capacity, comparator);
for (FeedSubscription sub : subs) {
Date last = (order != null && set.isFull()) ? set.last().getEntryUpdated() : null;
Criteria criteria = buildSearchCriteria(sub, unreadOnly, keywords, newerThan, -1, capacity, order, last);
Criteria criteria = buildSearchCriteria(user, sub, unreadOnly, keywords, newerThan, -1, capacity, order, last, tag);
ProjectionList projection = Projections.projectionList();
projection.add(Projections.property("id"), "id");
projection.add(Projections.property("updated"), "updated");
@@ -237,7 +256,10 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
for (FeedEntryStatus placeholder : placeholders) {
Long statusId = placeholder.getId();
FeedEntry entry = em.find(FeedEntry.class, placeholder.getEntry().getId());
statuses.add(handleStatus(statusId == null ? null : findById(statusId), placeholder.getSubscription(), entry));
FeedEntryStatus status = handleStatus(user, statusId == null ? null : findById(statusId), placeholder.getSubscription(),
entry);
status = fetchTags(user, status);
statuses.add(status);
}
statuses = lazyLoadContent(includeContent, statuses);
}
@@ -245,9 +267,9 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
@SuppressWarnings("unchecked")
public UnreadCount getUnreadCount(FeedSubscription subscription) {
public UnreadCount getUnreadCount(User user, FeedSubscription subscription) {
UnreadCount uc = null;
Criteria criteria = buildSearchCriteria(subscription, true, null, null, -1, -1, null, null);
Criteria criteria = buildSearchCriteria(user, subscription, true, null, null, -1, -1, null, null, null);
ProjectionList projection = Projections.projectionList();
projection.add(Projections.rowCount(), "count");
projection.add(Projections.max(FeedEntry_.updated.getName()), "updated");
@@ -273,15 +295,15 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
}
private void orderStatusesBy(CriteriaQuery<?> query, Path<FeedEntryStatus> statusJoin, ReadingOrder order) {
orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), order);
orderBy(query, statusJoin.get(FeedEntryStatus_.entryUpdated), statusJoin.get(FeedEntryStatus_.id), order);
}
private void orderBy(CriteriaQuery<?> query, Path<Date> date, ReadingOrder order) {
private void orderBy(CriteriaQuery<?> query, Path<Date> date, Path<Long> id, ReadingOrder order) {
if (order != null) {
if (order == ReadingOrder.asc) {
query.orderBy(builder.asc(date));
query.orderBy(builder.asc(date), builder.asc(id));
} else {
query.orderBy(builder.desc(date));
query.orderBy(builder.desc(date), builder.desc(id));
}
}
}
@@ -290,10 +312,17 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
setTimeout(query, applicationSettingsService.get().getQueryTimeout());
}
public int deleteOldStatuses(Date olderThan) {
Query query = em.createNamedQuery("Statuses.deleteOld");
query.setParameter("date", olderThan);
return query.executeUpdate();
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
Root<FeedEntryStatus> root = query.from(getType());
Predicate p1 = builder.lessThan(root.get(FeedEntryStatus_.entryInserted), olderThan);
Predicate p2 = builder.isFalse(root.get(FeedEntryStatus_.starred));
query.where(p1, p2);
TypedQuery<FeedEntryStatus> q = em.createQuery(query);
q.setMaxResults(limit);
return q.getResultList();
}
}

View File

@@ -0,0 +1,43 @@
package com.commafeed.backend.dao;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedEntryTag_;
import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.User_;
@Stateless
public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
public List<String> findByUser(User user) {
CriteriaQuery<String> query = builder.createQuery(String.class);
Root<FeedEntryTag> root = query.from(getType());
query.select(root.get(FeedEntryTag_.name));
query.distinct(true);
Predicate p1 = builder.equal(root.get(FeedEntryTag_.user).get(User_.id), user.getId());
query.where(p1);
return cache(em.createQuery(query)).getResultList();
}
public List<FeedEntryTag> findByEntry(User user, FeedEntry entry) {
CriteriaQuery<FeedEntryTag> query = builder.createQuery(getType());
Root<FeedEntryTag> root = query.from(getType());
Predicate p1 = builder.equal(root.get(FeedEntryTag_.user).get(User_.id), user.getId());
Predicate p2 = builder.equal(root.get(FeedEntryTag_.entry).get(FeedEntry_.id), entry.getId());
query.where(p1, p2);
return cache(em.createQuery(query)).getResultList();
}
}

View File

@@ -63,10 +63,11 @@ public abstract class GenericDAO<T extends AbstractModel> {
}
}
public void delete(Collection<? extends AbstractModel> objects) {
public int delete(Collection<? extends AbstractModel> objects) {
for (AbstractModel object : objects) {
delete(object);
}
return objects.size();
}
public void deleteById(Long id) {

View File

@@ -50,6 +50,8 @@ public class FeedFetcher {
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
content = result.getContent();
fetchedFeed = parser.parse(feedUrl, content);
} else {
throw e;
}
} else {
throw e;
@@ -77,6 +79,7 @@ public class FeedFetcher {
feed.setEtagHeader(FeedUtils.truncate(result.geteTag(), 255));
feed.setLastContentHash(hash);
fetchedFeed.setFetchDuration(result.getDuration());
fetchedFeed.setUrlAfterRedirect(result.getUrlAfterRedirect());
return fetchedFeed;
}

View File

@@ -55,6 +55,7 @@ public class FeedParser {
if (xmlString == null) {
throw new FeedException("Input string is null for url " + feedUrl);
}
xmlString = FeedUtils.replaceHtmlEntitiesWithNumericEntities(xmlString);
InputSource source = new InputSource(new StringReader(xmlString));
SyndFeed rss = new SyndFeedInput().build(source);
handleForeignMarkup(rss);
@@ -66,10 +67,6 @@ public class FeedParser {
feed.setLink(rss.getLink());
List<SyndEntry> items = rss.getEntries();
if (items.isEmpty()) {
throw new FeedException("No items in the feed.");
}
for (SyndEntry item : items) {
FeedEntry entry = new FeedEntry();
@@ -82,8 +79,13 @@ public class FeedParser {
continue;
}
entry.setGuid(FeedUtils.truncate(guid, 2048));
entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink()), 2048));
entry.setUpdated(validateDate(getEntryUpdateDate(item), true));
entry.setUrl(FeedUtils.truncate(FeedUtils.toAbsoluteUrl(item.getLink(), feed.getLink(), feed.getUrlAfterRedirect()), 2048));
// if link is empty but guid is used as url
if (StringUtils.isBlank(entry.getUrl()) && StringUtils.startsWith(entry.getGuid(), "http")) {
entry.setUrl(entry.getGuid());
}
FeedEntryContent content = new FeedEntryContent();
content.setContent(getContent(item));

View File

@@ -8,11 +8,11 @@ import com.commafeed.backend.model.FeedEntry;
public class FeedRefreshContext {
private Feed feed;
private List<FeedEntry> entries;
private boolean isUrgent;
private boolean urgent;
public FeedRefreshContext(Feed feed, boolean isUrgent) {
this.feed = feed;
this.isUrgent = isUrgent;
this.urgent = isUrgent;
}
public Feed getFeed() {
@@ -24,11 +24,11 @@ public class FeedRefreshContext {
}
public boolean isUrgent() {
return isUrgent;
return urgent;
}
public void setUrgent(boolean isUrgent) {
this.isUrgent = isUrgent;
public void setUrgent(boolean urgent) {
this.urgent = urgent;
}
public List<FeedEntry> getEntries() {

View File

@@ -7,6 +7,9 @@ import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
/**
* 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}
@@ -19,7 +22,7 @@ public class FeedRefreshExecutor {
private ThreadPoolExecutor pool;
private LinkedBlockingDeque<Runnable> queue;
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity) {
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity, MetricRegistry metrics) {
log.info("Creating pool {} with {} threads", poolName, threads);
this.poolName = poolName;
pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
@@ -51,20 +54,26 @@ public class FeedRefreshExecutor {
}
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "active"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return pool.getActiveCount();
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "pending"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return queue.size();
}
});
}
public void execute(Task task) {
pool.execute(task);
}
public int getQueueSize() {
return queue.size();
}
public int getActiveCount() {
return pool.getActiveCount();
}
public static interface Task extends Runnable {
boolean isUrgent();
}

View File

@@ -17,7 +17,9 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.time.DateUtils;
import com.commafeed.backend.MetricsBean;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.services.ApplicationSettingsService;
@@ -41,7 +43,7 @@ public class FeedRefreshTaskGiver {
ApplicationSettingsService applicationSettingsService;
@Inject
MetricsBean metricsBean;
MetricRegistry metrics;
@Inject
FeedRefreshWorker worker;
@@ -54,10 +56,35 @@ public class FeedRefreshTaskGiver {
private ExecutorService executor;
private Meter feedRefreshed;
private Meter threadWaited;
private Meter refill;
@PostConstruct
public void init() {
backgroundThreads = applicationSettingsService.get().getBackgroundThreads();
executor = Executors.newFixedThreadPool(1);
feedRefreshed = metrics.meter(MetricRegistry.name(getClass(), "feedRefreshed"));
threadWaited = metrics.meter(MetricRegistry.name(getClass(), "threadWaited"));
refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
metrics.register(MetricRegistry.name(getClass(), "addQueue"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return addQueue.size();
}
});
metrics.register(MetricRegistry.name(getClass(), "takeQueue"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return takeQueue.size();
}
});
metrics.register(MetricRegistry.name(getClass(), "giveBackQueue"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return giveBackQueue.size();
}
});
}
@PreDestroy
@@ -73,26 +100,25 @@ public class FeedRefreshTaskGiver {
}
public void start() {
try {
// sleeping for a little while, let everything settle
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("interrupted while sleeping");
}
log.info("starting feed refresh task giver");
executor.execute(new Runnable() {
@Override
public void run() {
try {
// sleeping for a little while, let everything settle
Thread.sleep(60000);
} catch (InterruptedException e) {
log.error("interrupted while sleeping");
}
while (!executor.isShutdown()) {
try {
FeedRefreshContext context = take();
if (context != null) {
metricsBean.feedRefreshed();
feedRefreshed.mark();
worker.updateFeed(context);
} else {
log.debug("nothing to do, sleeping for 15s");
metricsBean.threadWaited();
threadWaited.mark();
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
@@ -138,22 +164,26 @@ public class FeedRefreshTaskGiver {
* refills the refresh queue and empties the giveBack queue while at it
*/
private void refill() {
int count = Math.min(100, 3 * backgroundThreads);
refill.mark();
// first, get feeds that are up to refresh from the database
List<FeedRefreshContext> contexts = Lists.newArrayList();
if (!applicationSettingsService.get().isCrawlingPaused()) {
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
for (Feed feed : feeds) {
contexts.add(new FeedRefreshContext(feed, false));
}
int batchSize = Math.min(100, 3 * backgroundThreads);
// add feeds we got from the add() method
int addQueueSize = addQueue.size();
for (int i = 0; i < Math.min(batchSize, addQueueSize); i++) {
contexts.add(addQueue.poll());
}
// then, add to those the feeds we got from the add() method. We add them at the beginning of the list as they probably have a
// higher priority
int size = addQueue.size();
for (int i = 0; i < size; i++) {
contexts.add(0, addQueue.poll());
// add feeds that are up to refresh from the database
if (!applicationSettingsService.get().isCrawlingPaused()) {
int count = batchSize - contexts.size();
if (count > 0) {
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
for (Feed feed : feeds) {
contexts.add(new FeedRefreshContext(feed, false));
}
}
}
// set the disabledDate to now as we use the disabledDate in feedDAO to decide what to refresh next. We also use a map to remove
@@ -169,8 +199,8 @@ public class FeedRefreshTaskGiver {
takeQueue.addAll(map.values());
// add feeds from the giveBack queue to the map, overriding duplicates
size = giveBackQueue.size();
for (int i = 0; i < size; i++) {
int giveBackQueueSize = giveBackQueue.size();
for (int i = 0; i < giveBackQueueSize; i++) {
Feed feed = giveBackQueue.poll();
map.put(feed.getId(), new FeedRefreshContext(feed, false));
}

View File

@@ -19,7 +19,8 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import com.commafeed.backend.MetricsBean;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
@@ -57,7 +58,7 @@ public class FeedRefreshUpdater {
ApplicationSettingsService applicationSettingsService;
@Inject
MetricsBean metricsBean;
MetricRegistry metrics;
@Inject
FeedSubscriptionDAO feedSubscriptionDAO;
@@ -71,12 +72,22 @@ public class FeedRefreshUpdater {
private FeedRefreshExecutor pool;
private Striped<Lock> locks;
private Meter entryCacheMiss;
private Meter entryCacheHit;
private Meter feedUpdated;
private Meter entryInserted;
@PostConstruct
public void init() {
ApplicationSettings settings = applicationSettingsService.get();
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1);
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000));
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000), metrics);
locks = Striped.lazyWeakLock(threads * 100000);
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
}
@PreDestroy
@@ -116,10 +127,10 @@ public class FeedRefreshUpdater {
subscriptions = feedSubscriptionDAO.findByFeed(feed);
}
ok &= addEntry(feed, entry, subscriptions);
metricsBean.entryCacheMiss();
entryCacheMiss.mark();
} else {
log.debug("cache hit for {}", entry.getUrl());
metricsBean.entryCacheHit();
entryCacheHit.mark();
}
currentEntries.add(cacheKey);
@@ -147,7 +158,7 @@ public class FeedRefreshUpdater {
// requeue asap
feed.setDisabledUntil(new Date(0));
}
metricsBean.feedUpdated();
feedUpdated.mark();
taskGiver.giveBack(feed);
}
@@ -180,7 +191,7 @@ public class FeedRefreshUpdater {
if (locked1 && locked2) {
boolean inserted = feedUpdateService.addEntry(feed, entry);
if (inserted) {
metricsBean.entryInserted();
entryInserted.mark();
}
success = true;
} else {
@@ -213,13 +224,4 @@ public class FeedRefreshUpdater {
}
}
}
public int getQueueSize() {
return pool.getQueueSize();
}
public int getActiveCount() {
return pool.getActiveCount();
}
}

View File

@@ -12,7 +12,10 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.HttpGetter.NotModifiedException;
import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.ApplicationSettings;
@@ -37,6 +40,9 @@ public class FeedRefreshWorker {
@Inject
FeedRefreshTaskGiver taskGiver;
@Inject
MetricRegistry metrics;
@Inject
ApplicationSettingsService applicationSettingsService;
@@ -46,7 +52,7 @@ public class FeedRefreshWorker {
private void init() {
ApplicationSettings settings = applicationSettingsService.get();
int threads = settings.getBackgroundThreads();
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000));
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000), metrics);
}
@PreDestroy
@@ -58,14 +64,6 @@ public class FeedRefreshWorker {
pool.execute(new FeedTask(context));
}
public int getQueueSize() {
return pool.getQueueSize();
}
public int getActiveCount() {
return pool.getActiveCount();
}
private class FeedTask implements Task {
private FeedRefreshContext context;
@@ -90,17 +88,21 @@ public class FeedRefreshWorker {
int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes();
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
try {
FetchedFeed fetchedFeed = fetcher.fetch(feed.getUrl(), false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
String url = ObjectUtils.firstNonNull(feed.getUrlAfterRedirect(), feed.getUrl());
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
feed.getLastPublishedDate(), feed.getLastContentHash());
// stops here if NotModifiedException or any other exception is
// thrown
// stops here if NotModifiedException or any other exception is thrown
List<FeedEntry> entries = fetchedFeed.getEntries();
if (applicationSettingsService.get().isHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
.getAverageEntryInterval(), disabledUntil);
}
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
if (StringUtils.equals(url, urlAfterRedirect)) {
urlAfterRedirect = null;
}
feed.setUrlAfterRedirect(urlAfterRedirect);
feed.setLink(fetchedFeed.getFeed().getLink());
feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());

View File

@@ -1,6 +1,8 @@
package com.commafeed.backend.feeds;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -15,6 +17,7 @@ import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.math.stat.descriptive.SummaryStatistics;
import org.apache.wicket.request.UrlUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Document.OutputSettings;
@@ -50,6 +53,8 @@ public class FeedUtils {
private static final List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
private static final Whitelist WHITELIST = buildWhiteList();
public static String truncate(String string, int length) {
if (string != null) {
string = string.substring(0, Math.min(length, string.length()));
@@ -57,6 +62,39 @@ public class FeedUtils {
return string;
}
private static synchronized Whitelist buildWhiteList() {
Whitelist whitelist = new Whitelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em", "h1",
"h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub", "sup",
"table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
whitelist.addAttributes("div", "dir");
whitelist.addAttributes("pre", "dir");
whitelist.addAttributes("code", "dir");
whitelist.addAttributes("table", "dir");
whitelist.addAttributes("p", "dir");
whitelist.addAttributes("a", "href", "title");
whitelist.addAttributes("blockquote", "cite");
whitelist.addAttributes("col", "span", "width");
whitelist.addAttributes("colgroup", "span", "width");
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https");
whitelist.addEnforcedAttribute("a", "target", "_blank");
return whitelist;
}
/**
* Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
* feed
@@ -91,6 +129,14 @@ public class FeedUtils {
}
return encoding;
}
public static String replaceHtmlEntitiesWithNumericEntities(String source){
String result = source;
for(String entity : HtmlEntities.NUMERIC_MAPPING.keySet()){
result = result.replace(entity, HtmlEntities.NUMERIC_MAPPING.get(entity));
}
return result;
}
/**
* Normalize the url. The resulting url is not meant to be fetched but rather used as a mean to identify a feed and avoid duplicates
@@ -152,38 +198,9 @@ public class FeedUtils {
public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) {
baseUri = StringUtils.trimToEmpty(baseUri);
Whitelist whitelist = new Whitelist();
whitelist.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl", "dt", "em",
"h1", "h2", "h3", "h4", "h5", "h6", "i", "iframe", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong",
"sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul");
whitelist.addAttributes("div", "dir");
whitelist.addAttributes("pre", "dir");
whitelist.addAttributes("code", "dir");
whitelist.addAttributes("table", "dir");
whitelist.addAttributes("p", "dir");
whitelist.addAttributes("a", "href", "title");
whitelist.addAttributes("blockquote", "cite");
whitelist.addAttributes("col", "span", "width");
whitelist.addAttributes("colgroup", "span", "width");
whitelist.addAttributes("iframe", "src", "height", "width", "allowfullscreen", "frameborder", "style");
whitelist.addAttributes("img", "align", "alt", "height", "src", "title", "width", "style");
whitelist.addAttributes("ol", "start", "type");
whitelist.addAttributes("q", "cite");
whitelist.addAttributes("table", "border", "bordercolor", "summary", "width");
whitelist.addAttributes("td", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "width");
whitelist.addAttributes("th", "border", "bordercolor", "abbr", "axis", "colspan", "rowspan", "scope", "width");
whitelist.addAttributes("ul", "type");
whitelist.addProtocols("a", "href", "ftp", "http", "https", "mailto");
whitelist.addProtocols("blockquote", "cite", "http", "https");
whitelist.addProtocols("img", "src", "http", "https");
whitelist.addProtocols("q", "cite", "http", "https");
whitelist.addEnforcedAttribute("a", "target", "_blank");
Document dirty = Jsoup.parseBodyFragment(content, baseUri);
Cleaner cleaner = new Cleaner(whitelist);
Cleaner cleaner = new Cleaner(WHITELIST);
Document clean = cleaner.clean(dirty);
for (Element e : clean.select("iframe[style]")) {
@@ -389,17 +406,37 @@ public class FeedUtils {
return url;
}
public static String toAbsoluteUrl(String url, String baseUrl) {
/**
*
* @param url
* the url of the entry
* @param feedLink
* the url of the feed as described in the feed
* @param feedUrl
* the url of the feed that we used to fetch the feed
* @return an absolute url pointing to the entry
*/
public static String toAbsoluteUrl(String url, String feedLink, String feedUrl) {
url = StringUtils.trimToNull(StringUtils.normalizeSpace(url));
if (baseUrl == null || url == null || url.startsWith("http")) {
if (url == null || url.startsWith("http")) {
return url;
}
if (url.startsWith("/") == false) {
url = "/" + url;
String baseUrl = (feedLink == null || UrlUtils.isRelative(feedLink)) ? feedUrl : feedLink;
if (baseUrl == null) {
return url;
}
return baseUrl + url;
String result = null;
try {
result = new URL(new URL(baseUrl), url).toString();
} catch (MalformedURLException e) {
log.debug("could not parse url : " + e.getMessage(), e);
result = url;
}
return result;
}
public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {

View File

@@ -12,6 +12,7 @@ public class FetchedFeed {
private List<FeedEntry> entries = Lists.newArrayList();
private String title;
private String urlAfterRedirect;
private long fetchDuration;
public Feed getFeed() {
@@ -45,4 +46,13 @@ public class FetchedFeed {
public void setFetchDuration(long fetchDuration) {
this.fetchDuration = fetchDuration;
}
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
public void setUrlAfterRedirect(String urlAfterRedirect) {
this.urlAfterRedirect = urlAfterRedirect;
}
}

View File

@@ -0,0 +1,266 @@
package com.commafeed.backend.feeds;
import java.util.Collections;
import java.util.Map;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
public class HtmlEntities {
public static final Map<String, String> NUMERIC_MAPPING = Collections.unmodifiableMap(loadMap());
private static synchronized Map<String, String> loadMap() {
Map<String, String> map = Maps.newLinkedHashMap();
map.put("&Aacute;", "&#193;");
map.put("&aacute;", "&#225;");
map.put("&Acirc;", "&#194;");
map.put("&acirc;", "&#226;");
map.put("&acute;", "&#180;");
map.put("&AElig;", "&#198;");
map.put("&aelig;", "&#230;");
map.put("&Agrave;", "&#192;");
map.put("&agrave;", "&#224;");
map.put("&alefsym;", "&#8501;");
map.put("&Alpha;", "&#913;");
map.put("&alpha;", "&#945;");
map.put("&amp;", "&#38;");
map.put("&and;", "&#8743;");
map.put("&ang;", "&#8736;");
map.put("&Aring;", "&#197;");
map.put("&aring;", "&#229;");
map.put("&asymp;", "&#8776;");
map.put("&Atilde;", "&#195;");
map.put("&atilde;", "&#227;");
map.put("&Auml;", "&#196;");
map.put("&auml;", "&#228;");
map.put("&bdquo;", "&#8222;");
map.put("&Beta;", "&#914;");
map.put("&beta;", "&#946;");
map.put("&brvbar;", "&#166;");
map.put("&bull;", "&#8226;");
map.put("&cap;", "&#8745;");
map.put("&Ccedil;", "&#199;");
map.put("&ccedil;", "&#231;");
map.put("&cedil;", "&#184;");
map.put("&cent;", "&#162;");
map.put("&Chi;", "&#935;");
map.put("&chi;", "&#967;");
map.put("&circ;", "&#710;");
map.put("&clubs;", "&#9827;");
map.put("&cong;", "&#8773;");
map.put("&copy;", "&#169;");
map.put("&crarr;", "&#8629;");
map.put("&cup;", "&#8746;");
map.put("&curren;", "&#164;");
map.put("&dagger;", "&#8224;");
map.put("&Dagger;", "&#8225;");
map.put("&darr;", "&#8595;");
map.put("&dArr;", "&#8659;");
map.put("&deg;", "&#176;");
map.put("&Delta;", "&#916;");
map.put("&delta;", "&#948;");
map.put("&diams;", "&#9830;");
map.put("&divide;", "&#247;");
map.put("&Eacute;", "&#201;");
map.put("&eacute;", "&#233;");
map.put("&Ecirc;", "&#202;");
map.put("&ecirc;", "&#234;");
map.put("&Egrave;", "&#200;");
map.put("&egrave;", "&#232;");
map.put("&empty;", "&#8709;");
map.put("&emsp;", "&#8195;");
map.put("&ensp;", "&#8194;");
map.put("&Epsilon;", "&#917;");
map.put("&epsilon;", "&#949;");
map.put("&equiv;", "&#8801;");
map.put("&Eta;", "&#919;");
map.put("&eta;", "&#951;");
map.put("&ETH;", "&#208;");
map.put("&eth;", "&#240;");
map.put("&Euml;", "&#203;");
map.put("&euml;", "&#235;");
map.put("&euro;", "&#8364;");
map.put("&exist;", "&#8707;");
map.put("&fnof;", "&#402;");
map.put("&forall;", "&#8704;");
map.put("&frac12;", "&#189;");
map.put("&frac14;", "&#188;");
map.put("&frac34;", "&#190;");
map.put("&frasl;", "&#8260;");
map.put("&Gamma;", "&#915;");
map.put("&gamma;", "&#947;");
map.put("&ge;", "&#8805;");
map.put("&harr;", "&#8596;");
map.put("&hArr;", "&#8660;");
map.put("&hearts;", "&#9829;");
map.put("&hellip;", "&#8230;");
map.put("&Iacute;", "&#205;");
map.put("&iacute;", "&#237;");
map.put("&Icirc;", "&#206;");
map.put("&icirc;", "&#238;");
map.put("&iexcl;", "&#161;");
map.put("&Igrave;", "&#204;");
map.put("&igrave;", "&#236;");
map.put("&image;", "&#8465;");
map.put("&infin;", "&#8734;");
map.put("&int;", "&#8747;");
map.put("&Iota;", "&#921;");
map.put("&iota;", "&#953;");
map.put("&iquest;", "&#191;");
map.put("&isin;", "&#8712;");
map.put("&Iuml;", "&#207;");
map.put("&iuml;", "&#239;");
map.put("&Kappa;", "&#922;");
map.put("&kappa;", "&#954;");
map.put("&Lambda;", "&#923;");
map.put("&lambda;", "&#955;");
map.put("&lang;", "&#9001;");
map.put("&laquo;", "&#171;");
map.put("&larr;", "&#8592;");
map.put("&lArr;", "&#8656;");
map.put("&lceil;", "&#8968;");
map.put("&ldquo;", "&#8220;");
map.put("&le;", "&#8804;");
map.put("&lfloor;", "&#8970;");
map.put("&lowast;", "&#8727;");
map.put("&loz;", "&#9674;");
map.put("&lrm;", "&#8206;");
map.put("&lsaquo;", "&#8249;");
map.put("&lsquo;", "&#8216;");
map.put("&macr;", "&#175;");
map.put("&mdash;", "&#8212;");
map.put("&micro;", "&#181;");
map.put("&middot;", "&#183;");
map.put("&minus;", "&#8722;");
map.put("&Mu;", "&#924;");
map.put("&mu;", "&#956;");
map.put("&nabla;", "&#8711;");
map.put("&nbsp;", "&#160;");
map.put("&ndash;", "&#8211;");
map.put("&ne;", "&#8800;");
map.put("&ni;", "&#8715;");
map.put("&not;", "&#172;");
map.put("&notin;", "&#8713;");
map.put("&nsub;", "&#8836;");
map.put("&Ntilde;", "&#209;");
map.put("&ntilde;", "&#241;");
map.put("&Nu;", "&#925;");
map.put("&nu;", "&#957;");
map.put("&Oacute;", "&#211;");
map.put("&oacute;", "&#243;");
map.put("&Ocirc;", "&#212;");
map.put("&ocirc;", "&#244;");
map.put("&OElig;", "&#338;");
map.put("&oelig;", "&#339;");
map.put("&Ograve;", "&#210;");
map.put("&ograve;", "&#242;");
map.put("&oline;", "&#8254;");
map.put("&Omega;", "&#937;");
map.put("&omega;", "&#969;");
map.put("&Omicron;", "&#927;");
map.put("&omicron;", "&#959;");
map.put("&oplus;", "&#8853;");
map.put("&or;", "&#8744;");
map.put("&ordf;", "&#170;");
map.put("&ordm;", "&#186;");
map.put("&Oslash;", "&#216;");
map.put("&oslash;", "&#248;");
map.put("&Otilde;", "&#213;");
map.put("&otilde;", "&#245;");
map.put("&otimes;", "&#8855;");
map.put("&Ouml;", "&#214;");
map.put("&ouml;", "&#246;");
map.put("&para;", "&#182;");
map.put("&part;", "&#8706;");
map.put("&permil;", "&#8240;");
map.put("&perp;", "&#8869;");
map.put("&Phi;", "&#934;");
map.put("&phi;", "&#966;");
map.put("&Pi;", "&#928;");
map.put("&pi;", "&#960;");
map.put("&piv;", "&#982;");
map.put("&plusmn;", "&#177;");
map.put("&pound;", "&#163;");
map.put("&prime;", "&#8242;");
map.put("&Prime;", "&#8243;");
map.put("&prod;", "&#8719;");
map.put("&prop;", "&#8733;");
map.put("&Psi;", "&#936;");
map.put("&psi;", "&#968;");
map.put("&quot;", "&#34;");
map.put("&radic;", "&#8730;");
map.put("&rang;", "&#9002;");
map.put("&raquo;", "&#187;");
map.put("&rarr;", "&#8594;");
map.put("&rArr;", "&#8658;");
map.put("&rceil;", "&#8969;");
map.put("&rdquo;", "&#8221;");
map.put("&real;", "&#8476;");
map.put("&reg;", "&#174;");
map.put("&rfloor;", "&#8971;");
map.put("&Rho;", "&#929;");
map.put("&rho;", "&#961;");
map.put("&rlm;", "&#8207;");
map.put("&rsaquo;", "&#8250;");
map.put("&rsquo;", "&#8217;");
map.put("&sbquo;", "&#8218;");
map.put("&Scaron;", "&#352;");
map.put("&scaron;", "&#353;");
map.put("&sdot;", "&#8901;");
map.put("&sect;", "&#167;");
map.put("&shy;", "&#173;");
map.put("&Sigma;", "&#931;");
map.put("&sigma;", "&#963;");
map.put("&sigmaf;", "&#962;");
map.put("&sim;", "&#8764;");
map.put("&spades;", "&#9824;");
map.put("&sub;", "&#8834;");
map.put("&sube;", "&#8838;");
map.put("&sum;", "&#8721;");
map.put("&sup1;", "&#185;");
map.put("&sup2;", "&#178;");
map.put("&sup3;", "&#179;");
map.put("&sup;", "&#8835;");
map.put("&supe;", "&#8839;");
map.put("&szlig;", "&#223;");
map.put("&Tau;", "&#932;");
map.put("&tau;", "&#964;");
map.put("&there4;", "&#8756;");
map.put("&Theta;", "&#920;");
map.put("&theta;", "&#952;");
map.put("&thetasym;", "&#977;");
map.put("&thinsp;", "&#8201;");
map.put("&THORN;", "&#222;");
map.put("&thorn;", "&#254;");
map.put("&tilde;", "&#732;");
map.put("&times;", "&#215;");
map.put("&trade;", "&#8482;");
map.put("&Uacute;", "&#218;");
map.put("&uacute;", "&#250;");
map.put("&uarr;", "&#8593;");
map.put("&uArr;", "&#8657;");
map.put("&Ucirc;", "&#219;");
map.put("&ucirc;", "&#251;");
map.put("&Ugrave;", "&#217;");
map.put("&ugrave;", "&#249;");
map.put("&uml;", "&#168;");
map.put("&upsih;", "&#978;");
map.put("&Upsilon;", "&#933;");
map.put("&upsilon;", "&#965;");
map.put("&Uuml;", "&#220;");
map.put("&uuml;", "&#252;");
map.put("&weierp;", "&#8472;");
map.put("&Xi;", "&#926;");
map.put("&xi;", "&#958;");
map.put("&Yacute;", "&#221;");
map.put("&yacute;", "&#253;");
map.put("&yen;", "&#165;");
map.put("&yuml;", "&#255;");
map.put("&Yuml;", "&#376;");
map.put("&Zeta;", "&#918;");
map.put("&zeta;", "&#950;");
map.put("&zwj;", "&#8205;");
map.put("&zwnj;", "&#8204;");
return map;
}
}

View File

@@ -0,0 +1,30 @@
package com.commafeed.backend.metrics;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import lombok.extern.slf4j.Slf4j;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
@ApplicationScoped
@Slf4j
public class MetricRegistryProducer {
private MetricRegistry registry;
@PostConstruct
private void init() {
log.info("initializing metrics registry");
registry = new MetricRegistry();
JmxReporter.forRegistry(registry).build().start();
log.info("metrics registry initialized");
}
@Produces
public MetricRegistry produceMetricsRegistry() {
return registry;
}
}

View File

@@ -33,6 +33,12 @@ public class Feed extends AbstractModel {
@Column(length = 2048, nullable = false)
private String url;
/**
* cache the url after potential http 30x redirects
*/
@Column(name = "url_after_redirect", length = 2048, nullable = false)
private String urlAfterRedirect;
@Column(length = 2048, nullable = false)
private String normalizedUrl;
@@ -130,11 +136,4 @@ public class Feed extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP)
private Date pushLastPing;
public Feed() {
}
public Feed(String url) {
this.url = url;
}
}

View File

@@ -55,5 +55,8 @@ public class FeedEntry extends AbstractModel {
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
private Set<FeedEntryStatus> statuses;
@OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
private Set<FeedEntryTag> tags;
}

View File

@@ -1,6 +1,7 @@
package com.commafeed.backend.model;
import java.util.Date;
import java.util.List;
import javax.persistence.Cacheable;
import javax.persistence.Column;
@@ -8,6 +9,8 @@ import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@@ -19,6 +22,8 @@ import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import com.google.common.collect.Lists;
@Entity
@Table(name = "FEEDENTRYSTATUSES")
@SuppressWarnings("serial")
@@ -26,6 +31,7 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
@NamedQueries(@NamedQuery(name="Statuses.deleteOld", query="delete from FeedEntryStatus s where s.entryInserted < :date and s.starred = false"))
public class FeedEntryStatus extends AbstractModel {
@ManyToOne(fetch = FetchType.LAZY)
@@ -42,6 +48,9 @@ public class FeedEntryStatus extends AbstractModel {
@Transient
private boolean markable;
@Transient
private List<FeedEntryTag> tags = Lists.newArrayList();
/**
* Denormalization starts here

View File

@@ -0,0 +1,46 @@
package com.commafeed.backend.model;
import javax.persistence.Cacheable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(name = "FEEDENTRYTAGS")
@SuppressWarnings("serial")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter
@Setter
public class FeedEntryTag extends AbstractModel {
@JoinColumn(name = "user_id")
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@JoinColumn(name = "entry_id")
@ManyToOne(fetch = FetchType.LAZY)
private FeedEntry entry;
@Column(name = "name", length = 40)
private String name;
public FeedEntryTag() {
}
public FeedEntryTag(User user, FeedEntry entry, String name) {
this.name = name;
this.entry = entry;
this.user = user;
}
}

View File

@@ -32,7 +32,7 @@ public class User extends AbstractModel {
@Column(length = 32, nullable = false, unique = true)
private String name;
@Column(length = 255, unique = true)
private String email;
@@ -66,4 +66,8 @@ public class User extends AbstractModel {
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private Set<FeedSubscription> subscriptions;
@Column(name = "last_full_refresh")
@Temporal(TemporalType.TIMESTAMP)
private Date lastFullRefresh;
}

View File

@@ -59,7 +59,6 @@ public class UserSettings extends AbstractModel {
private boolean showRead;
private boolean scrollMarks;
private boolean socialButtons;
@Column(length = 32)
private String theme;
@@ -68,4 +67,18 @@ public class UserSettings extends AbstractModel {
@Column(length = Integer.MAX_VALUE)
private String customCss;
@Column(name = "scroll_speed")
private int scrollSpeed;
private boolean email;
private boolean gmail;
private boolean facebook;
private boolean twitter;
private boolean googleplus;
private boolean tumblr;
private boolean pocket;
private boolean instapaper;
private boolean buffer;
private boolean readability;
}

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend.feeds;
package com.commafeed.backend.opml;
import java.util.Date;
import java.util.List;
@@ -34,9 +34,14 @@ public class OPMLExporter {
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
// export root categories
for (FeedCategory cat : categories) {
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
if (cat.getParent() == null) {
opml.getOutlines().add(buildCategoryOutline(cat, subscriptions));
}
}
// export root subscriptions
for (FeedSubscription sub : subscriptions) {
if (sub.getCategory() == null) {
opml.getOutlines().add(buildSubscriptionOutline(sub));

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend.feeds;
package com.commafeed.backend.opml;
import java.io.StringReader;
import java.util.List;
@@ -11,10 +11,12 @@ import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.User;
import com.commafeed.backend.services.FeedSubscriptionService;
@@ -56,8 +58,8 @@ public class OPMLImporter {
@SuppressWarnings("unchecked")
private void handleOutline(User user, Outline outline, FeedCategory parent) {
if (StringUtils.isEmpty(outline.getType())) {
List<Outline> children = outline.getChildren();
if (CollectionUtils.isNotEmpty(children)) {
String name = FeedUtils.truncate(outline.getText(), 128);
if (name == null) {
name = FeedUtils.truncate(outline.getTitle(), 128);
@@ -75,7 +77,6 @@ public class OPMLImporter {
feedCategoryDAO.saveOrUpdate(category);
}
List<Outline> children = outline.getChildren();
for (Outline child : children) {
handleOutline(user, child, category);
}
@@ -87,7 +88,7 @@ public class OPMLImporter {
if (StringUtils.isBlank(name)) {
name = "Unnamed subscription";
}
// make sure we continue with the import process even a feed failed
// make sure we continue with the import process even if a feed failed
try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
} catch (FeedSubscriptionException e) {

View File

@@ -9,13 +9,14 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.wicket.util.io.IOUtils;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
@@ -67,10 +68,11 @@ public class SubscriptionHandler {
post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed");
post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
HttpClient client = HttpGetter.newClient(20000);
CloseableHttpClient client = HttpGetter.newClient(20000);
CloseableHttpResponse response = null;
try {
post.setEntity(new UrlEncodedFormEntity(nvp));
HttpResponse response = client.execute(post);
response = client.execute(post);
int code = response.getStatusLine().getStatusCode();
if (code != 204 && code != 202 && code != 200) {
@@ -90,7 +92,8 @@ public class SubscriptionHandler {
} catch (Exception e) {
log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic);
} finally {
client.getConnectionManager().shutdown();
IOUtils.closeQuietly(response);
IOUtils.closeQuietly(client);
}
}
}

View File

@@ -3,7 +3,8 @@ package com.commafeed.backend.services;
import java.util.Date;
import java.util.Enumeration;
import javax.ejb.Singleton;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.apache.commons.lang.time.DateUtils;
@@ -15,7 +16,7 @@ import com.commafeed.backend.dao.ApplicationSettingsDAO;
import com.commafeed.backend.model.ApplicationSettings;
import com.google.common.collect.Iterables;
@Singleton
@ApplicationScoped
public class ApplicationSettingsService {
@Inject
@@ -23,19 +24,21 @@ public class ApplicationSettingsService {
private ApplicationSettings settings;
public void save(ApplicationSettings settings) {
this.settings = settings;
applicationSettingsDAO.saveOrUpdate(settings);
applyLogLevel();
@PostConstruct
private void init() {
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
}
public ApplicationSettings get() {
if (settings == null) {
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
}
return settings;
}
public void save(ApplicationSettings settings) {
applicationSettingsDAO.saveOrUpdate(settings);
this.settings = settings;
applyLogLevel();
}
public Date getUnreadThreshold() {
int keepStatusDays = get().getKeepStatusDays();
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;

View File

@@ -1,6 +1,7 @@
package com.commafeed.backend;
package com.commafeed.backend.services;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -15,15 +16,18 @@ import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.services.ApplicationSettingsService;
/**
* Contains utility methods for cleaning the database
*
*/
@Slf4j
public class DatabaseCleaner {
public class DatabaseCleaningService {
private static final int BATCH_SIZE = 100;
@Inject
FeedDAO feedDAO;
@@ -42,13 +46,28 @@ public class DatabaseCleaner {
@Inject
ApplicationSettingsService applicationSettingsService;
public long cleanEntriesWithoutSubscriptions() {
log.info("cleaning entries without subscriptions");
long total = 0;
int deleted = 0;
do {
List<FeedEntry> entries = feedEntryDAO.findWithoutSubscriptions(BATCH_SIZE);
deleted = feedEntryDAO.delete(entries);
total += deleted;
log.info("removed {} entries without subscriptions", total);
} while (deleted != 0);
log.info("cleanup done: {} entries without subscriptions deleted", total);
return total;
}
public long cleanFeedsWithoutSubscriptions() {
log.info("cleaning feeds without subscriptions");
long total = 0;
int deleted = -1;
int deleted = 0;
do {
deleted = feedDAO.deleteWithoutSubscriptions(10);
List<Feed> feeds = feedDAO.findWithoutSubscriptions(BATCH_SIZE);
deleted = feedDAO.delete(feeds);
total += deleted;
log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0);
@@ -57,15 +76,15 @@ public class DatabaseCleaner {
}
public long cleanContentsWithoutEntries() {
log.info("cleaning contents without entries");
long total = 0;
int deleted = -1;
int deleted = 0;
do {
deleted = feedEntryContentDAO.deleteWithoutEntries(10);
deleted = feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE);
total += deleted;
log.info("removed {} feeds without subscriptions", total);
log.info("removed {} contents without entries", total);
} while (deleted != 0);
log.info("cleanup done: {} feeds without subscriptions deleted", total);
log.info("cleanup done: {} contents without entries deleted", total);
return total;
}
@@ -74,9 +93,9 @@ public class DatabaseCleaner {
cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value));
long total = 0;
int deleted = -1;
int deleted = 0;
do {
deleted = feedEntryDAO.delete(cal.getTime(), 100);
deleted = feedEntryDAO.delete(cal.getTime(), BATCH_SIZE);
total += deleted;
log.info("removed {} entries", total);
} while (deleted != 0);
@@ -99,9 +118,19 @@ public class DatabaseCleaner {
feedDAO.saveOrUpdate(into);
}
public void cleanStatusesOlderThan(Date olderThan) {
public long cleanStatusesOlderThan(Date olderThan) {
log.info("cleaning old read statuses");
int deleted = feedEntryStatusDAO.deleteOldStatuses(olderThan);
log.info("cleaned {} read statuses", deleted);
long total = 0;
List<FeedEntryStatus> list = Collections.emptyList();
do {
list = feedEntryStatusDAO.getOldStatuses(olderThan, BATCH_SIZE);
if (!list.isEmpty()) {
feedEntryStatusDAO.delete(list);
total += list.size();
log.info("cleaned {} old read statuses", total);
}
} while (!list.isEmpty());
log.info("cleanup done: {} old read statuses deleted", total);
return total;
}
}

View File

@@ -43,7 +43,7 @@ public class FeedEntryService {
return;
}
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
if (status.isMarkable()) {
status.setRead(read);
feedEntryStatusDAO.saveOrUpdate(status);
@@ -64,14 +64,14 @@ public class FeedEntryService {
return;
}
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry);
FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
status.setStarred(starred);
feedEntryStatusDAO.saveOrUpdate(status);
}
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO
.findBySubscriptions(subscriptions, true, null, null, -1, -1, null, false, false);
List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, -1, -1, null, false,
false, null);
markList(statuses, olderThan);
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(user);

View File

@@ -0,0 +1,61 @@
package com.commafeed.backend.services;
import java.util.List;
import java.util.Map;
import javax.ejb.Stateless;
import javax.inject.Inject;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.User;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@Stateless
public class FeedEntryTagService {
@Inject
FeedEntryDAO feedEntryDAO;
@Inject
FeedEntryTagDAO feedEntryTagDAO;
public void updateTags(User user, Long entryId, List<String> tagNames) {
FeedEntry entry = feedEntryDAO.findById(entryId);
if (entry == null) {
return;
}
List<FeedEntryTag> tags = feedEntryTagDAO.findByEntry(user, entry);
Map<String, FeedEntryTag> tagMap = Maps.uniqueIndex(tags, new Function<FeedEntryTag, String>() {
@Override
public String apply(FeedEntryTag input) {
return input.getName();
}
});
List<FeedEntryTag> addList = Lists.newArrayList();
List<FeedEntryTag> removeList = Lists.newArrayList();
for (String tagName : tagNames) {
FeedEntryTag tag = tagMap.get(tagName);
if (tag == null) {
addList.add(new FeedEntryTag(user, entry, tagName));
}
}
for (FeedEntryTag tag : tags) {
if (!tagNames.contains(tag.getName())) {
removeList.add(tag);
}
}
feedEntryTagDAO.saveOrUpdate(addList);
feedEntryTagDAO.delete(removeList);
}
}

View File

@@ -95,11 +95,19 @@ public class FeedSubscriptionService {
}
}
public UnreadCount getUnreadCount(FeedSubscription sub) {
public void refreshAll(User user) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
Feed feed = sub.getFeed();
taskGiver.add(feed, true);
}
}
public UnreadCount getUnreadCount(User user, FeedSubscription sub) {
UnreadCount count = cache.getUnreadCount(sub);
if (count == null) {
log.debug("unread count cache miss for {}", Models.getId(sub));
count = feedEntryStatusDAO.getUnreadCount(sub);
count = feedEntryStatusDAO.getUnreadCount(user, sub);
cache.setUnreadCount(sub, count);
}
return count;
@@ -109,7 +117,7 @@ public class FeedSubscriptionService {
Map<Long, UnreadCount> map = Maps.newHashMap();
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) {
map.put(sub.getId(), getUnreadCount(sub));
map.put(sub.getId(), getUnreadCount(user, sub));
}
return map;
}

View File

@@ -41,6 +41,9 @@ public class UserService {
@Inject
ApplicationSettingsService applicationSettingsService;
@Inject
FeedSubscriptionService feedSubscriptionService;
public User login(String name, String password) {
if (name == null || password == null) {
return null;
@@ -52,10 +55,21 @@ public class UserService {
if (authenticated) {
Date lastLogin = user.getLastLogin();
Date now = new Date();
boolean saveUser = false;
// only update lastLogin field every hour in order to not
// invalidate the cache everytime someone logs in
if (lastLogin == null || lastLogin.before(DateUtils.addHours(now, -1))) {
user.setLastLogin(now);
saveUser = true;
}
if (applicationSettingsService.get().isHeavyLoad()
&& (user.getLastFullRefresh() == null || user.getLastFullRefresh().before(DateUtils.addMinutes(now, -30)))) {
user.setLastFullRefresh(now);
saveUser = true;
feedSubscriptionService.refreshAll(user);
}
if (saveUser) {
userDAO.saveOrUpdate(user);
}
return user;

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend;
package com.commafeed.backend.startup;
import java.sql.Connection;

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend;
package com.commafeed.backend.startup;
import java.io.InputStream;
import java.io.InputStreamReader;

View File

@@ -10,6 +10,8 @@ import com.commafeed.backend.services.UserService;
// extend Component in order to benefit from injection
public class CommaFeedSessionServices extends Component {
private static final long serialVersionUID = 1L;
@Inject
UserService userService;

View File

@@ -41,4 +41,7 @@ public class Entries implements Serializable {
@ApiProperty("list of entries")
private List<Entry> entries = Lists.newArrayList();
@ApiProperty("if true, the unread flag was ignored in the request, all entries are returned regardless of their read status")
private boolean ignoredReadStatus;
}

View File

@@ -3,6 +3,7 @@ package com.commafeed.frontend.model;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import lombok.Data;
@@ -10,7 +11,9 @@ import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedEntryTag;
import com.commafeed.backend.model.FeedSubscription;
import com.google.common.collect.Lists;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
@@ -43,6 +46,12 @@ public class Entry implements Serializable {
entry.setFeedLink(sub.getFeed().getLink());
entry.setIconUrl(FeedUtils.getFaviconUrl(sub, publicUrl));
List<String> tags = Lists.newArrayList();
for (FeedEntryTag tag : status.getTags()) {
tags.add(tag.getName());
}
entry.setTags(tags);
if (content != null) {
entry.setRtl(FeedUtils.isRTL(feedEntry));
entry.setTitle(content.getTitle());
@@ -125,4 +134,7 @@ public class Entry implements Serializable {
@ApiProperty("wether the entry is still markable (old entry statuses are discarded)")
private boolean markable;
@ApiProperty("tags")
private List<String> tags;
}

View File

@@ -27,9 +27,6 @@ public class Settings implements Serializable {
@ApiProperty(value = "user wants category and feeds with no unread entries shown", required = true)
private boolean showRead;
@ApiProperty(value = "user wants social buttons (facebook, twitter, ...) shown", required = true)
private boolean socialButtons;
@ApiProperty(value = "In expanded view, scroll through entries mark them as read", required = true)
private boolean scrollMarks;
@@ -38,5 +35,19 @@ public class Settings implements Serializable {
@ApiProperty(value = "user's custom css for the website")
private String customCss;
@ApiProperty(value = "user's preferred scroll speed when navigating between entries")
private int scrollSpeed;
private boolean email;
private boolean gmail;
private boolean facebook;
private boolean twitter;
private boolean googleplus;
private boolean tumblr;
private boolean pocket;
private boolean instapaper;
private boolean buffer;
private boolean readability;
}

View File

@@ -0,0 +1,22 @@
package com.commafeed.frontend.model.request;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import com.wordnik.swagger.annotations.ApiClass;
import com.wordnik.swagger.annotations.ApiProperty;
@SuppressWarnings("serial")
@ApiClass("Tag Request")
@Data
public class TagRequest implements Serializable {
@ApiProperty(value = "entry id", required = true)
private Long entryId;
@ApiProperty(value = "tags")
private List<String> tags;
}

View File

@@ -1,45 +1,54 @@
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
<head>
<title>CommaFeed</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="app-icon-57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="app-icon-114.png" />
<link rel="apple-touch-icon" sizes="144x144" href="app-icon-144.png" />
<link rel="icon" sizes="32x32" href="app-icon-32.png" />
<link rel="icon" sizes="64x64" href="app-icon-64.png" />
<link rel="icon" sizes="128x128" href="app-icon-128.png" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<meta name="application-name" content="CommaFeed" />
<meta name="msapplication-navbutton-color" content="#F88A14" />
<meta name="msapplication-starturl" content="/" />
<meta name="msapplication-square70x70logo" content="metro-icon-70.png"/>
<meta name="msapplication-square150x150logo" content="metro-icon-150.png"/>
<link rel="fluid-icon" href="app-icon-512.png" title="CommaFeed" />
<link rel="logo" type="image/svg" href="app-icon.svg" />
<title>CommaFeed</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="app-icon-57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="app-icon-114.png" />
<link rel="apple-touch-icon" sizes="144x144" href="app-icon-144.png" />
<link rel="icon" sizes="32x32" href="app-icon-32.png" />
<link rel="icon" sizes="64x64" href="app-icon-64.png" />
<link rel="icon" sizes="128x128" href="app-icon-128.png" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<meta name="application-name" content="CommaFeed" />
<meta name="msapplication-navbutton-color" content="#F88A14" />
<meta name="msapplication-starturl" content="/" />
<meta name="msapplication-square70x70logo" content="metro-icon-70.png" />
<meta name="msapplication-square150x150logo" content="metro-icon-150.png" />
<link rel="fluid-icon" href="app-icon-512.png" title="CommaFeed" />
<link rel="logo" type="image/svg" href="app-icon.svg" />
</head>
<body>
<wicket:child />
<wicket:container wicket:id="footer-container"/>
<wicket:container wicket:id="footer-container" />
<wicket:container wicket:id="uservoice">
<script>(function(){var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/XYpTZZteqS4lHvgrTXeA.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s)})()</script>
<script>
(function() {
var uv = document.createElement('script');
uv.type = 'text/javascript';
uv.async = true;
uv.src = '//widget.uservoice.com/XYpTZZteqS4lHvgrTXeA.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(uv, s);
})();
</script>
<script>
UserVoice = window.UserVoice || [];
UserVoice.push(['showTab', 'classic_widget', {
mode: 'full',
default_mode: 'feedback',
primary_color: '#000',
link_color: '#007dbf',
forum_id: 204509,
support_tab_name: 'Contact',
feedback_tab_name: 'Feedback',
tab_label: 'Feedback',
tab_color: '#7e72db',
tab_position: 'bottom-right',
tab_inverted: false
mode : 'full',
default_mode : 'feedback',
primary_color : '#000',
link_color : '#007dbf',
forum_id : 204509,
support_tab_name : 'Contact',
feedback_tab_name : 'Feedback',
tab_label : 'Feedback',
tab_color : '#7e72db',
tab_position : 'bottom-right',
tab_inverted : false
}]);
</script>
</wicket:container>

View File

@@ -15,7 +15,6 @@ import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO;
@@ -29,6 +28,7 @@ import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.MailService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.CommaFeedSession;
import com.commafeed.frontend.utils.WicketUtils;
import com.google.common.collect.Maps;

View File

@@ -4,8 +4,8 @@ import javax.inject.Inject;
import org.apache.wicket.markup.html.WebPage;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.CommaFeedSession;
public class DemoLoginPage extends WebPage {

View File

@@ -4,6 +4,7 @@ import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.frontend.CommaFeedSession;
@@ -21,7 +22,11 @@ public class HomePage extends BasePage {
response.render(CssHeaderItem.forReference(new UserCustomCssReference() {
@Override
protected String getCss() {
UserSettings settings = userSettingsDAO.findByUser(CommaFeedSession.get().getUser());
User user = CommaFeedSession.get().getUser();
if (user == null) {
return null;
}
UserSettings settings = userSettingsDAO.findByUser(user);
return settings == null ? null : settings.getCustomCss();
}
}, new PageParameters().add("_t", System.currentTimeMillis()), null));

View File

@@ -54,13 +54,13 @@ public class NextUnreadRedirectPage extends WebPage {
List<FeedEntryStatus> statuses = null;
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
statuses = feedEntryStatusDAO.findBySubscriptions(subs, true, null, null, 0, 1, order, true, false);
statuses = feedEntryStatusDAO.findBySubscriptions(user, subs, true, null, null, 0, 1, order, true, false, null);
} else {
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
if (category != null) {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children);
statuses = feedEntryStatusDAO.findBySubscriptions(subscriptions, true, null, null, 0, 1, order, true, false);
statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, 0, 1, order, true, false, null);
}
}

View File

@@ -5,15 +5,19 @@
<div class="text-center">
<img src="images/logo_2.png" />
<div wicket:id="feedback"></div>
<form wicket:id="form">
New Password:
<input type="password" wicket:id="password" />
<br />
Confirm:
<input type="password" wicket:id="confirm" />
<br />
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="button" class="btn" wicket:id="cancel" value="Home page" />
<form wicket:id="form" class="form-horizontal">
<div class="form-group">
<label>New Password</label>
<input type="password" wicket:id="password" />
</div>
<div class="form-group">
<label>Confirm</label>
<input type="password" wicket:id="confirm" />
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="button" class="btn btn-default" wicket:id="cancel" value="Home page" />
</div>
</form>
</div>
</div>

View File

@@ -2,15 +2,18 @@
<body>
<wicket:extend>
<div class="container">
<div class="text-center">
<div class="col-xs-6 col-xs-offset-3 text-center">
<img src="images/logo_2.png" />
<div wicket:id="feedback"></div>
<form wicket:id="form">
Email:
<input type="email" wicket:id="email" />
<br />
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="button" class="btn" wicket:id="cancel" value="Cancel" />
<form wicket:id="form" class="form-horizontal">
<div class="form-group">
<label>Email</label>
<input type="email" wicket:id="email" class="form-control"/>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="button" class="btn btn-default" wicket:id="cancel" value="Cancel" />
</div>
</form>
</div>
</div>

View File

@@ -3,8 +3,8 @@
<body>
<wicket:extend>
<div class="welcome">
<div class="container-fluid">
<div class="row-fluid header">
<div class="container">
<div class="row header">
<div class="pull-left">
<a wicket:id="logo-link">
<img src="images/logo_2.png"></img>
@@ -23,14 +23,14 @@
</a>
</div>
</div>
<div class="row-fluid">
<div class="span6">
<div class="row">
<div class="col-md-6">
<div class="well" id="login-panel">
<h3>Login</h3>
<span wicket:id="login"></span>
</div>
</div>
<div class="span6" wicket:enclosure="register">
<div class="col-md-6" wicket:enclosure="register">
<div class="well" id="register-panel">
<h3>Register</h3>
<span wicket:id="register"></span>
@@ -40,7 +40,7 @@
<hr />
<div class="footer">
<div class="row-fluid">
<div class="row">
<span>
&copy;
<a href="http://www.commafeed.com" target="_blank">CommaFeed</a>

View File

@@ -4,24 +4,20 @@
<wicket:panel>
<span wicket:id="feedback"></span>
<form wicket:id="signInForm">
<div class="control-group">
<label class="control-label" for="username">User Name</label>
<div class="controls">
<input type="text" id="username" wicket:id="username" class="input-block-level"></input>
</div>
<div class="form-group">
<label for="username">User Name</label>
<input type="text" id="username" wicket:id="username" class="form-control"></input>
</div>
<div class="control-group">
<label class="control-label" for="password">Password</label>
<div class="controls">
<input type="password" id="password" wicket:id="password" class="input-block-level"></input>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" wicket:id="password" class="form-control"></input>
</div>
<p class="help-block" wicket:id="rememberMeRow">
<label class="checkbox">
<div wicket:id="rememberMeRow">
<label>
<input wicket:id="rememberMe" type="checkbox" />
Remember me
</label>
</p>
</div>
<div>
<input type="submit" class="btn btn-primary" value="Log in" />
<a wicket:id="recover" class="pull-right">Forgot password?</a>

View File

@@ -4,23 +4,17 @@
<wicket:panel>
<div wicket:id="feedback"></div>
<form wicket:id="form" autocomplete="off">
<div class="control-group">
<label class="control-label">User Name</label>
<div class="controls">
<input type="text" wicket:id="name" class="input-block-level"></input>
</div>
<div class="form-group">
<label>User Name</label>
<input type="text" wicket:id="name" class="form-control"></input>
</div>
<div class="control-group">
<label class="control-label">Password</label>
<div class="controls">
<input type="password" wicket:id="password" class="input-block-level"></input>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" wicket:id="password" class="form-control"></input>
</div>
<div class="control-group">
<label class="control-label">Email address (used for password recovery only)</label>
<div class="controls">
<input type="email" wicket:id="email" class="input-block-level"></input>
</div>
<div class="form-group">
<label>Email address (used for password recovery only)</label>
<input type="email" wicket:id="email" class="form-control"></input>
</div>
<div>
<input type="submit" class="btn btn-primary" value="Register" />

View File

@@ -0,0 +1,18 @@
package com.commafeed.frontend.resources;
import ro.isdc.wro.model.resource.processor.impl.css.CssUrlRewritingProcessor;
public class CustomCssUrlRewritingProcessor extends CssUrlRewritingProcessor {
/**
* ignore webjar image replacements since they won't be available at runtime anyway
*/
@Override
protected String replaceImageUrl(String cssUri, String imageUrl) {
if (cssUri.startsWith("webjar:")) {
return imageUrl;
}
return super.replaceImageUrl(cssUri, imageUrl);
}
}

View File

@@ -20,6 +20,7 @@ public class WroAdditionalProvider implements ProcessorProvider {
map.put("sassOnlyProcessor", new SassOnlyProcessor());
map.put("sassImport", new SassImportProcessor());
map.put("timestamp", new TimestampProcessor());
map.put("cssUrlRewriting", new CustomCssUrlRewritingProcessor());
return map;
}

View File

@@ -16,6 +16,7 @@ public class WroManagerFactory extends ConfigurableWroManagerFactory {
map.put("sassOnlyProcessor", new SassOnlyProcessor());
map.put("sassImport", new SassImportProcessor());
map.put("timestamp", new TimestampProcessor());
map.put("cssUrlRewriting", new CustomCssUrlRewritingProcessor());
}
}

View File

@@ -6,9 +6,11 @@ import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
@@ -19,6 +21,7 @@ import org.apache.http.HttpHeaders;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
@Provider
@Consumes(MediaType.APPLICATION_JSON)
@@ -30,6 +33,9 @@ public class JsonProvider implements MessageBodyReader<Object>, MessageBodyWrite
private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Context
private HttpServletRequest request;
@Override
public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
@@ -38,10 +44,29 @@ public class JsonProvider implements MessageBodyReader<Object>, MessageBodyWrite
httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_VALUE);
httpHeaders.putSingle(HttpHeaders.PRAGMA, CACHE_CONTROL_VALUE);
getMapper().writeValue(entityStream, value);
ObjectWriter writer = getMapper().writer();
if (hasPrettyPrint(annotations)) {
writer = writer.withDefaultPrettyPrinter();
}
writer.writeValue(entityStream, value);
}
private boolean hasPrettyPrint(Annotation[] annotations) {
boolean prettyPrint = false;
for (Annotation annotation : annotations) {
if (PrettyPrint.class.equals(annotation.annotationType())) {
prettyPrint = true;
break;
}
}
if (!prettyPrint && request != null) {
prettyPrint = Boolean.parseBoolean(request.getParameter("pretty"));
}
return prettyPrint;
}
@Override
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {

View File

@@ -0,0 +1,12 @@
package com.commafeed.frontend.rest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface PrettyPrint {
}

View File

@@ -24,6 +24,7 @@ import org.apache.wicket.protocol.http.servlet.ServletWebResponse;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.util.crypt.Base64;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role;
@@ -42,6 +43,9 @@ public abstract class AbstractREST {
@Context
private HttpServletResponse response;
@Inject
MetricRegistry metrics;
@Inject
private UserDAO userDAO;
@@ -93,10 +97,13 @@ public abstract class AbstractREST {
}
@AroundInvoke
public Object checkSecurity(InvocationContext context) throws Exception {
public Object intercept(InvocationContext context) throws Exception {
Method method = context.getMethod();
// check security
boolean allowed = true;
User user = null;
Method method = context.getMethod();
SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method.getAnnotation(SecurityCheck.class) : method
.getDeclaringClass().getAnnotation(SecurityCheck.class);
@@ -118,7 +125,16 @@ public abstract class AbstractREST {
}
return context.proceed();
Object result = null;
com.codahale.metrics.Timer.Context timer = metrics.timer(
MetricRegistry.name(method.getDeclaringClass(), method.getName(), "responseTime")).time();
try {
result = context.proceed();
} finally {
timer.stop();
}
return result;
}
private boolean checkRole(Role requiredRole) {
@@ -130,4 +146,5 @@ public abstract class AbstractREST {
boolean authorized = roles.hasAnyRole(new Roles(requiredRole.name()));
return authorized;
}
}

View File

@@ -1,48 +1,40 @@
package com.commafeed.frontend.rest.resources;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.DatabaseCleaner;
import com.commafeed.backend.MetricsBean;
import com.commafeed.backend.StartupBean;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedDAO.DuplicateMode;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedRefreshUpdater;
import com.commafeed.backend.feeds.FeedRefreshWorker;
import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.DatabaseCleaningService;
import com.commafeed.backend.services.FeedService;
import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.FeedCount;
import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.FeedMergeRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.rest.PrettyPrint;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.wordnik.swagger.annotations.Api;
@@ -70,10 +62,10 @@ public class AdminREST extends AbstractREST {
FeedDAO feedDAO;
@Inject
MetricsBean metricsBean;
MetricRegistry metrics;
@Inject
DatabaseCleaner cleaner;
DatabaseCleaningService cleaner;
@Inject
FeedRefreshWorker feedRefreshWorker;
@@ -226,26 +218,24 @@ public class AdminREST extends AbstractREST {
@Path("/metrics")
@GET
@PrettyPrint
@ApiOperation(value = "Retrieve server metrics")
public Response getMetrics(@QueryParam("backlog") @DefaultValue("false") boolean backlog) {
Map<String, Object> map = Maps.newLinkedHashMap();
map.put("lastMinute", metricsBean.getLastMinute());
map.put("lastHour", metricsBean.getLastHour());
if (backlog) {
map.put("backlog", taskGiver.getUpdatableCount());
}
map.put("http_active", feedRefreshWorker.getActiveCount());
map.put("http_queue", feedRefreshWorker.getQueueSize());
map.put("database_active", feedRefreshUpdater.getActiveCount());
map.put("database_queue", feedRefreshUpdater.getQueueSize());
map.put("cache", metricsBean.getCacheStats());
public Response getMetrics() {
return Response.ok(metrics).build();
}
@Path("/cleanup/entries")
@GET
@ApiOperation(value = "Entries cleanup", notes = "Delete entries without subscriptions")
public Response cleanupEntries() {
Map<String, Long> map = Maps.newHashMap();
map.put("entries_without_subscriptions", cleaner.cleanEntriesWithoutSubscriptions());
return Response.ok(map).build();
}
@Path("/cleanup/feeds")
@GET
@ApiOperation(value = "Feeds cleanup", notes = "Delete feeds without subscriptions and entries without feeds")
@ApiOperation(value = "Feeds cleanup", notes = "Delete feeds without subscriptions")
public Response cleanupFeeds() {
Map<String, Long> map = Maps.newHashMap();
map.put("feeds_without_subscriptions", cleaner.cleanFeedsWithoutSubscriptions());
@@ -261,44 +251,4 @@ public class AdminREST extends AbstractREST {
return Response.ok(map).build();
}
@Path("/cleanup/entries")
@GET
@ApiOperation(value = "Entries cleanup", notes = "Delete entries older than given date")
public Response cleanupEntries(@QueryParam("days") @DefaultValue("30") int days) {
Map<String, Long> map = Maps.newHashMap();
map.put("old_entries", cleaner.cleanEntriesOlderThan(days, TimeUnit.DAYS));
return Response.ok(map).build();
}
@Path("/cleanup/findDuplicateFeeds")
@GET
@ApiOperation(value = "Find duplicate feeds")
public Response findDuplicateFeeds(@QueryParam("mode") DuplicateMode mode, @QueryParam("page") int page,
@QueryParam("limit") int limit, @QueryParam("minCount") long minCount) {
List<FeedCount> list = feedDAO.findDuplicates(mode, limit * page, limit, minCount);
return Response.ok(list).build();
}
@Path("/cleanup/merge")
@POST
@ApiOperation(value = "Merge feeds", notes = "Merge feeds together")
public Response mergeFeeds(@ApiParam(required = true) FeedMergeRequest request) {
Feed into = feedDAO.findById(request.getIntoFeedId());
if (into == null) {
return Response.status(Status.BAD_REQUEST).entity("'into feed' not found").build();
}
List<Feed> feeds = Lists.newArrayList();
for (Long feedId : request.getFeedIds()) {
Feed feed = feedDAO.findById(feedId);
feeds.add(feed);
}
if (feeds.isEmpty()) {
return Response.status(Status.BAD_REQUEST).entity("'from feeds' empty").build();
}
cleaner.mergeFeeds(into, feeds);
return Response.ok().build();
}
}

View File

@@ -22,8 +22,8 @@ import javax.ws.rs.core.Response.Status;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
@@ -106,7 +106,8 @@ public class CategoryREST extends AbstractREST {
@ApiParam(
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,
@ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds) {
@ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
@ApiParam(value = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
Preconditions.checkNotNull(readType);
@@ -135,11 +136,11 @@ public class CategoryREST extends AbstractREST {
}
if (ALL.equals(id)) {
entries.setName("All");
entries.setName(ObjectUtils.defaultIfNull(tag, "All"));
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser());
removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, keywords, newerThanDate, offset,
limit + 1, order, true, onlyIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), subs, unreadOnly, keywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag);
for (FeedEntryStatus status : list) {
entries.getEntries().add(
@@ -161,8 +162,8 @@ public class CategoryREST extends AbstractREST {
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), parent);
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(getUser(), categories);
removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, keywords, newerThanDate, offset,
limit + 1, order, true, onlyIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), subs, unreadOnly, keywords, newerThanDate,
offset, limit + 1, order, true, onlyIds, tag);
for (FeedEntryStatus status : list) {
entries.getEntries().add(
@@ -170,8 +171,9 @@ public class CategoryREST extends AbstractREST {
.isImageProxyEnabled()));
}
entries.setName(parent.getName());
} else {
return Response.status(Status.NOT_FOUND).entity("<message>category not found</message>").build();
}
}
boolean hasMore = entries.getEntries().size() > limit;
@@ -181,6 +183,7 @@ public class CategoryREST extends AbstractREST {
}
entries.setTimestamp(System.currentTimeMillis());
entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null);
FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords);
return Response.ok(entries).build();
}
@@ -191,16 +194,24 @@ public class CategoryREST extends AbstractREST {
@Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true)
public Response getCategoryEntriesAsFeed(
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id) {
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@ApiParam(
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds,
@ApiParam(value = "comma-separated list of excluded subscription ids") @QueryParam("excludedSubscriptionIds") String excludedSubscriptionIds,
@ApiParam(value = "keep only entries tagged with this tag") @QueryParam("tag") String tag) {
Preconditions.checkNotNull(id);
ReadingMode readType = ReadingMode.all;
ReadingOrder order = ReadingOrder.desc;
int offset = 0;
int limit = 20;
Entries entries = (Entries) getCategoryEntries(id, readType, null, offset, limit, order, null, false, null).getEntity();
Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, onlyIds, excludedSubscriptionIds,
tag);
if (response.getStatus() != Status.OK.getStatusCode()) {
return response;
}
Entries entries = (Entries) response.getEntity();
SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0");

View File

@@ -1,17 +1,23 @@
package com.commafeed.frontend.rest.resources;
import java.util.List;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedEntryTagService;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.MultipleMarkRequest;
import com.commafeed.frontend.model.request.StarRequest;
import com.commafeed.frontend.model.request.TagRequest;
import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
@@ -30,6 +36,12 @@ public class EntryREST extends AbstractREST {
@Inject
FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
FeedEntryTagDAO feedEntryTagDAO;
@Inject
FeedEntryTagService feedEntryTagService;
@Inject
ApplicationSettingsService applicationSettingsService;
@@ -71,4 +83,24 @@ public class EntryREST extends AbstractREST {
return Response.ok().build();
}
@Path("/tags")
@GET
@ApiOperation(value = "Get list of tags for the user", notes = "Get list of tags for the user")
public Response getTags() {
List<String> tags = feedEntryTagDAO.findByUser(getUser());
return Response.ok(tags).build();
}
@Path("/tag")
@POST
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
public Response tagFeedEntry(@ApiParam(value = "Tag Request", required = true) TagRequest req) {
Preconditions.checkNotNull(req);
Preconditions.checkNotNull(req.getEntryId());
feedEntryTagService.updateTags(getUser(), req.getEntryId(), req.getTags());
return Response.ok().build();
}
}

View File

@@ -37,7 +37,6 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
@@ -47,8 +46,6 @@ import com.commafeed.backend.feeds.FeedFetcher;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.feeds.FetchedFeed;
import com.commafeed.backend.feeds.OPMLExporter;
import com.commafeed.backend.feeds.OPMLImporter;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
@@ -56,9 +53,12 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.opml.OPMLExporter;
import com.commafeed.backend.opml.OPMLImporter;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedSubscriptionService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
@@ -71,6 +71,7 @@ import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.sun.syndication.feed.opml.Opml;
import com.sun.syndication.feed.synd.SyndEntry;
@@ -167,8 +168,8 @@ public class FeedREST extends AbstractREST {
entries.setErrorCount(subscription.getFeed().getErrorCount());
entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(Arrays.asList(subscription), unreadOnly, keywords,
newerThanDate, offset, limit + 1, order, true, onlyIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), Arrays.asList(subscription), unreadOnly,
keywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null);
for (FeedEntryStatus status : list) {
entries.getEntries().add(
@@ -181,9 +182,12 @@ public class FeedREST extends AbstractREST {
entries.setHasMore(true);
entries.getEntries().remove(entries.getEntries().size() - 1);
}
} else {
return Response.status(Status.NOT_FOUND).entity("<message>feed not found</message>").build();
}
entries.setTimestamp(System.currentTimeMillis());
entries.setIgnoredReadStatus(keywords != null);
FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords);
return Response.ok(entries).build();
}
@@ -193,16 +197,22 @@ public class FeedREST extends AbstractREST {
@ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries")
@Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true)
public Response getFeedEntriesAsFeed(@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id) {
public Response getFeedEntriesAsFeed(
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id,
@ApiParam(value = "all entries or only unread ones", allowableValues = "all,unread", required = true) @DefaultValue("all") @QueryParam("readType") ReadingMode readType,
@ApiParam(value = "only entries newer than this") @QueryParam("newerThan") Long newerThan,
@ApiParam(value = "offset for paging") @DefaultValue("0") @QueryParam("offset") int offset,
@ApiParam(value = "limit for paging, default 20, maximum 1000") @DefaultValue("20") @QueryParam("limit") int limit,
@ApiParam(value = "date ordering", allowableValues = "asc,desc") @QueryParam("order") @DefaultValue("desc") ReadingOrder order,
@ApiParam(
value = "search for keywords in either the title or the content of the entries, separated by spaces, 3 characters minimum") @QueryParam("keywords") String keywords,
@ApiParam(value = "return only entry ids") @DefaultValue("false") @QueryParam("onlyIds") boolean onlyIds) {
Preconditions.checkNotNull(id);
ReadingMode readType = ReadingMode.all;
ReadingOrder order = ReadingOrder.desc;
int offset = 0;
int limit = 20;
Entries entries = (Entries) getFeedEntries(id, readType, null, offset, limit, order, null, false).getEntity();
Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords, onlyIds);
if (response.getStatus() != Status.OK.getStatusCode()) {
return response;
}
Entries entries = (Entries) response.getEntity();
SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0");
@@ -235,7 +245,7 @@ public class FeedREST extends AbstractREST {
try {
FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null);
info = new FeedInfo();
info.setUrl(feed.getFeed().getUrl());
info.setUrl(feed.getUrlAfterRedirect());
info.setTitle(feed.getTitle());
} catch (Exception e) {
@@ -256,7 +266,8 @@ public class FeedREST extends AbstractREST {
try {
info = fetchFeedInternal(req.getUrl());
} catch (Exception e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(Throwables.getStackTraceAsString(Throwables.getRootCause(e)))
.build();
}
return Response.ok(info).build();
}
@@ -265,11 +276,7 @@ public class FeedREST extends AbstractREST {
@GET
@ApiOperation(value = "Queue all feeds of the user for refresh", notes = "Manually add all feeds of the user to the refresh queue")
public Response queueAllForRefresh() {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser());
for (FeedSubscription sub : subs) {
Feed feed = sub.getFeed();
taskGiver.add(feed, true);
}
feedSubscriptionService.refreshAll(getUser());
return Response.ok().build();
}
@@ -368,8 +375,10 @@ public class FeedREST extends AbstractREST {
try {
url = fetchFeedInternal(url).getUrl();
FeedCategory category = CategoryREST.ALL.equals(req.getCategoryId()) ? null : feedCategoryDAO.findById(Long.valueOf(req
.getCategoryId()));
FeedCategory category = null;
if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
}
FeedInfo info = fetchFeedInternal(url);
feedSubscriptionService.subscribe(getUser(), info.getUrl(), req.getTitle(), category);
} catch (Exception e) {

View File

@@ -3,6 +3,7 @@ package com.commafeed.frontend.rest.resources;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
@@ -22,7 +23,8 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import com.commafeed.backend.MetricsBean;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.feeds.FeedParser;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
@@ -52,8 +54,13 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
@Inject
ApplicationSettingsService applicationSettingsService;
@Inject
MetricsBean metricsBean;
private Meter pushReceived;
@PostConstruct
public void initMetrics() {
pushReceived = metrics.meter(MetricRegistry.name(getClass(), "pushReceived"));
}
@Path("/callback")
@GET
@@ -119,7 +126,7 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
log.debug("pushing content to queue for {}", feed.getUrl());
taskGiver.add(feed, false);
}
metricsBean.pushReceived(feeds.size());
pushReceived.mark();
} catch (Exception e) {
log.error("Could not parse pubsub callback: " + e.getMessage());

View File

@@ -10,10 +10,10 @@ import javax.ws.rs.core.Response.Status;
import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.services.ApplicationPropertiesService;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.model.ServerInfo;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;

View File

@@ -11,7 +11,6 @@ import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
@@ -25,6 +24,7 @@ import com.commafeed.backend.model.UserSettings.ViewMode;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel;
@@ -74,20 +74,44 @@ public class UserREST extends AbstractREST {
s.setReadingOrder(settings.getReadingOrder().name());
s.setViewMode(settings.getViewMode().name());
s.setShowRead(settings.isShowRead());
s.setSocialButtons(settings.isSocialButtons());
s.setEmail(settings.isEmail());
s.setGmail(settings.isGmail());
s.setFacebook(settings.isFacebook());
s.setTwitter(settings.isTwitter());
s.setGoogleplus(settings.isGoogleplus());
s.setTumblr(settings.isTumblr());
s.setPocket(settings.isPocket());
s.setInstapaper(settings.isInstapaper());
s.setBuffer(settings.isBuffer());
s.setReadability(settings.isReadability());
s.setScrollMarks(settings.isScrollMarks());
s.setTheme(settings.getTheme());
s.setCustomCss(settings.getCustomCss());
s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed());
} else {
s.setReadingMode(ReadingMode.unread.name());
s.setReadingOrder(ReadingOrder.desc.name());
s.setViewMode(ViewMode.title.name());
s.setShowRead(true);
s.setTheme("default");
s.setSocialButtons(true);
s.setEmail(true);
s.setGmail(true);
s.setFacebook(true);
s.setTwitter(true);
s.setGoogleplus(true);
s.setTumblr(true);
s.setPocket(true);
s.setInstapaper(true);
s.setBuffer(true);
s.setReadability(true);
s.setScrollMarks(true);
s.setLanguage("en");
s.setScrollSpeed(400);
}
return Response.ok(s).build();
}
@@ -114,8 +138,20 @@ public class UserREST extends AbstractREST {
s.setScrollMarks(settings.isScrollMarks());
s.setTheme(settings.getTheme());
s.setCustomCss(settings.getCustomCss());
s.setSocialButtons(settings.isSocialButtons());
s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed());
s.setEmail(settings.isEmail());
s.setGmail(settings.isGmail());
s.setFacebook(settings.isFacebook());
s.setTwitter(settings.isTwitter());
s.setGoogleplus(settings.isGoogleplus());
s.setTumblr(settings.isTumblr());
s.setPocket(settings.isPocket());
s.setInstapaper(settings.isInstapaper());
s.setBuffer(settings.isBuffer());
s.setReadability(settings.isReadability());
userSettingsDAO.saveOrUpdate(s);
return Response.ok().build();

View File

@@ -28,6 +28,8 @@ import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.lang.StringUtils;
/**
* See http://en.wikipedia.org/wiki/URL_normalization for a reference Note: some
* parts of the code are adapted from: http://stackoverflow.com/a/4057470/405418
@@ -46,7 +48,7 @@ public class URLCanonicalizer {
URL canonicalURL = new URL(UrlResolver.resolveUrl(context == null ? "" : context, href));
String host = canonicalURL.getHost().toLowerCase();
if (host == "") {
if (StringUtils.isBlank(host)) {
// This is an invalid Url.
return null;
}

View File

@@ -1,45 +0,0 @@
package liquibase.integration.cdi;
import java.sql.SQLException;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;
import javax.sql.DataSource;
import liquibase.integration.cdi.annotations.LiquibaseType;
import liquibase.resource.ResourceAccessor;
/**
* temporary fix until https://liquibase.jira.com/browse/CORE-1325 is fixed
*/
public class CDIBootstrap implements Extension {
void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
}
void afterDeploymentValidation(@Observes AfterDeploymentValidation event, BeanManager manager) {
}
@Produces
@LiquibaseType
public CDILiquibaseConfig createConfig() {
return null;
}
@Produces
@LiquibaseType
public DataSource createDataSource() throws SQLException {
return null;
}
@Produces
@LiquibaseType
public ResourceAccessor create() {
return null;
}
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd">
<named-query name="Statuses.deleteOld">
<query>delete from FeedEntryStatus s where s.entryInserted &lt; :date and s.starred = false</query>
</named-query>
</entity-mappings>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
@@ -9,6 +8,7 @@
<jta-data-source>${jpa.datasource.name}</jta-data-source>
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<properties>
<property name="openejb.jpa.auto-scan" value="true" />
<property name="format_sql" value="true" />
<property name="use_sql_comments" value="true" />
@@ -22,26 +22,17 @@
<property name="hibernate.generate_statistics" value="true" />
<property name="hibernate.cache.use_second_level_cache"
value="${jpa.cache}" />
<property name="hibernate.cache.use_second_level_cache" value="${jpa.cache}" />
<property name="hibernate.cache.use_query_cache" value="${jpa.cache}" />
<property name="hibernate.cache.region.factory_class"
value="org.hibernate.cache.infinispan.InfinispanRegionFactory" />
<property name="hibernate.cache.infinispan.statistics"
value="true" />
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.infinispan.InfinispanRegionFactory" />
<property name="hibernate.cache.infinispan.statistics" value="true" />
<property name="hibernate.cache.infinispan.entity.eviction.strategy"
value="LRU" />
<property
name="hibernate.cache.infinispan.entity.eviction.wake_up_interval"
value="2000" />
<property name="hibernate.cache.infinispan.entity.eviction.max_entries"
value="10000" />
<property name="hibernate.cache.infinispan.entity.expiration.lifespan"
value="60000" />
<property name="hibernate.cache.infinispan.entity.expiration.max_idle"
value="30000" />
<property name="hibernate.cache.infinispan.entity.eviction.strategy" value="LRU" />
<property name="hibernate.cache.infinispan.entity.eviction.wake_up_interval" value="2000" />
<property name="hibernate.cache.infinispan.entity.eviction.max_entries" value="10000" />
<property name="hibernate.cache.infinispan.entity.expiration.lifespan" value="60000" />
<property name="hibernate.cache.infinispan.entity.expiration.max_idle" value="30000" />
</properties>
</persistence-unit>

View File

@@ -357,6 +357,7 @@
</changeSet>
<changeSet author="athou" id="status-cleanup">
<validCheckSum>7:cf40ae235c2d4086c5fa6ac64102c6a9</validCheckSum>
<delete tableName="FEEDENTRYSTATUSES">
<where>read_status = false and starred = false</where>
</delete>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
<changeSet author="athou" id="add-url-after-redirect">
<addColumn tableName="FEEDS">
<column name="url_after_redirect" type="VARCHAR(2048)" />
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
<changeSet author="athou" id="scroll-speed">
<addColumn tableName="USERSETTINGS">
<column name="scroll_speed" type="BIGINT" />
</addColumn>
</changeSet>
<changeSet author="athou" id="set-default-scroll-speed">
<update tableName="USERSETTINGS">
<column name="scroll_speed" valueNumeric="400" />
</update>
</changeSet>
<changeSet author="athou" id="create-tags-table">
<validCheckSum>7:fdd37bdee09c8fbbcbcd867b05decaae</validCheckSum>
<createTable tableName="FEEDENTRYTAGS">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" />
</column>
<column name="entry_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="user_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="name" type="VARCHAR(40)">
<constraints nullable="false" />
</column>
</createTable>
<addForeignKeyConstraint constraintName="fk_entry_id" baseTableName="FEEDENTRYTAGS" baseColumnNames="entry_id"
referencedTableName="FEEDENTRIES" referencedColumnNames="id" />
<addForeignKeyConstraint constraintName="fk_user_id" baseTableName="FEEDENTRYTAGS" baseColumnNames="user_id"
referencedTableName="USERS" referencedColumnNames="id" />
<createIndex tableName="FEEDENTRYTAGS" indexName="user_entry_name_index">
<column name="user_id" />
<column name="entry_id" />
<column name="name" />
</createIndex>
</changeSet>
<changeSet author="athou" id="add-full-refresh-timestamp">
<addColumn tableName="USERS">
<column name="last_full_refresh" type="DATETIME" />
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="add-detailed-social-options" author="athou">
<addColumn tableName="USERSETTINGS">
<column name="email" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="gmail" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="facebook" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="twitter" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="googleplus" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="tumblr" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="pocket" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="instapaper" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="buffer" type="BIT"></column>
</addColumn>
<addColumn tableName="USERSETTINGS">
<column name="readability" type="BIT"></column>
</addColumn>
<dropColumn tableName="USERSETTINGS" columnName="socialButtons" />
<update tableName="USERSETTINGS">
<column name="email" valueBoolean="true"></column>
<column name="gmail" valueBoolean="true"></column>
<column name="facebook" valueBoolean="true"></column>
<column name="twitter" valueBoolean="true"></column>
<column name="googleplus" valueBoolean="true"></column>
<column name="tumblr" valueBoolean="true"></column>
<column name="pocket" valueBoolean="true"></column>
<column name="instapaper" valueBoolean="true"></column>
<column name="buffer" valueBoolean="true"></column>
<column name="readability" valueBoolean="true"></column>
</update>
</changeSet>
</databaseChangeLog>

View File

@@ -6,5 +6,8 @@
<include file="changelogs/db.changelog-1.0.xml" />
<include file="changelogs/db.changelog-1.1.xml" />
<include file="changelogs/db.changelog-1.2.xml" />
<include file="changelogs/db.changelog-1.3.xml" />
<include file="changelogs/db.changelog-1.4.xml" />
<include file="changelogs/db.changelog-1.5.xml" />
</databaseChangeLog>

View File

@@ -6,6 +6,7 @@ global.download=تحميل
global.link=رابط
global.bookmark=مرجعية
global.close=أغلق
global.tags=Tags ####### Needs translation
tree.subscribe=اشترك
tree.import=استورد
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=الترتيب حسب التاريخ تصاعدي / ت
toolbar.titles_only=العناوين فقط
toolbar.expanded_view=عرض موسع
toolbar.mark_all_as_read=اعتبر الكل مقروء
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=العناصر الأقدم من يوم
toolbar.mark_all_older_week=العناصر الأقدم من أسبوع
toolbar.mark_all_older_two_weeks=العناصر الأقدم من أسبوعين
@@ -66,6 +68,8 @@ settings.general.show_unread=Show feeds and categories with no unread entries
settings.general.social_buttons=Show social sharing buttons
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
settings.appearance=Appearance
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Theme
settings.submit_your_theme=Submit your theme
settings.custom_css=Custom CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh
details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Unsubscribe
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Category details
details.tag_details=Tag details ####### Needs translation
details.parent_category=Parent category
profile.user_name=User name
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generate new API key
profile.generate_new_api_key_info=Changing password will generate a new API key
profile.opml_export=OPML export
profile.delete_account=Delete account
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Keyboard shortcuts

View File

@@ -0,0 +1,156 @@
global.save=Desa
global.cancel=Cancel·la
global.delete=Esborra
global.required=Requerit
global.download=Descarrega
global.link=Enllaç
global.bookmark=Adreça d'interès
global.close=Tancar
global.tags=Etiquetes
tree.subscribe=Subscriure
tree.import=Importa
tree.new_category=Nova categoria
tree.all=Tot
tree.starred=Destacats
subscribe.feed_url=URL del canal
subscribe.feed_name=Nom del canal
subscribe.category=Categoria
import.google_reader_prefix=Importaré els canals del teu
import.google_reader_suffix= compte.
import.google_download=O be, carrega el teu fitxer subscriptions.xml.
import.google_download_link=Descarrega'l d'aquí.
import.xml_file=Fitxer OPML
new_category.name=Nom
new_category.parent=Arrel
toolbar.unread=Per llegir
toolbar.all=Tots
toolbar.previous_entry=Entrada prèvia
toolbar.next_entry=Entrada següent
toolbar.refresh=Actualitzar
toolbar.refresh_all=Força l'actualització de tots els canals
toolbar.sort_by_asc_desc=Ordenar per data asc/desc
toolbar.titles_only=Només títols
toolbar.expanded_view=Vista ampliada
toolbar.mark_all_as_read=Marcar tots llegits
toolbar.mark_all_older_12_hours=Ítems més vells de 12 hores
toolbar.mark_all_older_day=Ítems més vells d'un dia
toolbar.mark_all_older_week=Ítems més vells d'una setmana
toolbar.mark_all_older_two_weeks=Ítems més vells de dues setmanes
toolbar.settings=Configuració
toolbar.profile=Perfil
toolbar.admin=Admin
toolbar.about=Quant a
toolbar.logout=Desconnecta't
toolbar.donate=Donació
view.entry_source=de
view.entry_author=per
view.error_while_loading_feed=Error carregant el canal
view.keep_unread=Conserva com a no llegit
view.no_unread_items=no té ítems sense llegir.
view.mark_up_to_here=Marcar com a llegit fins aquí
view.search_for=cercant:
view.no_search_results=No hi ha coincidències per les paraules clau sol·licitades
feedsearch.hint=Introdueix una subscripció...
feedsearch.help=Utilitza la tecla de retorn per seleccionar i les tecles de cursor per navegar.
feedsearch.result_prefix=Les teves subscripcions:
settings.general=General
settings.general.language=Idioma
settings.general.language.contribute=Contribueix amb traduccions
settings.general.show_unread=Mostrar canals i categories amb entrades sense llegir
settings.general.social_buttons=Mostrar botons per compartir en xarxes socials
settings.general.scroll_marks=A la vista ampliada si et desplaces per les entrades les marques com a llegides
settings.appearance=Aparença
settings.scroll_speed=Velocitat de desplaçament quan navegues entre entrades (en mil·lisegons)
settings.scroll_speed.help=Fixa a 0 per desactivar
settings.theme=Tema
settings.submit_your_theme=Envia un tema
settings.custom_css=CSS personalitzat
details.feed_details=Detalls del canal
details.url=URL
details.website=Lloc web
details.name=Nom
details.category=Categoria
details.position=Posició
details.last_refresh=Darrera actualització
details.message=Darrer missatge d'actualització
details.next_refresh=Propera actualització
details.queued_for_refresh=A la cua d'actualització
details.feed_url=URL del canal
details.generate_api_key_first=Abans cal que generis una clau API en el teu perfil.
details.unsubscribe=Cancel·la la subscripció
details.unsubscribe_confirmation=Segur que vols cancel·lar la subscripció del canal?
details.delete_category_confirmation=Segur que vols esborrar la categoria?
details.category_details=Detalls de la categoria
details.tag_details=Detalls de l'etiqueta
details.parent_category=Categoria arrel
profile.user_name=Nom d'usuari
profile.email=Adreça electrònica
profile.change_password=Canvia la contrasenya
profile.confirm_password=Confirma la contrasenya
profile.minimum_6_chars=Mínim de 6 caracters
profile.passwords_do_not_match=Les contrasenyes no coincideixen
profile.api_key=Clau API
profile.api_key_not_generated=Encara no s'ha generat
profile.generate_new_api_key=Genera una nova clau API
profile.generate_new_api_key_info=El canvi de contrasenya generarà una nova clau API
profile.opml_export=Exporta OPML
profile.delete_account=Esborra el compte
profile.delete_account_confirmation=Vols esborrar el teu compte? No ho podràs desfer!
about.rest_api=REST API
about.keyboard_shortcuts=Dreceres de teclat
about.version=Versió de CommaFeed
about.line1_prefix=CommaFeed és un projecte de codi font obert. El codi font és hostatjat a
about.line1_suffix=.
about.line2_prefix=Si trobes un problema, si us plau informa'n a la pàgina de problemes del
about.line2_suffix=\ projecte.
about.line3=Si t'agrada el projecte, pensa en fer un donatiu per recolzar el desenvolupador i per ajudar amb les despeses de l'hostatge del lloc web.
about.line4=I pels que preferiu bitcoin, aquí teniu l'adreça
about.goodies=Afegitons
about.goodies.android_app=App Android
about.goodies.subscribe_url=URL de subscripció
about.goodies.chrome_extension=Extensió del Chrome
about.goodies.firefox_extension=Extensió del Firefox
about.goodies.opera_extension=Extensió de l'Opera
about.goodies.subscribe_bookmarklet=Afegeix bookmarklet de subscripció (clica)
about.goodies.subscribe_bookmarklet_asc=Primer els vells
about.goodies.subscribe_bookmarklet_desc=Primer els nous
about.goodies.next_unread_bookmarklet=Bookmarklet del proper ítem sense llegir (arrosega a la barra d'adreces d'interès)
about.translation=Traducció
about.translation.message=Necessitem la teva ajuda per traduir CommaFeed.
about.translation.link=Informació per contribuir amb traduccions.
about.announcements=Anuncis
about.rest_api.line1=CommaFeed funciona amb JAX-RS i AngularJS. Per tant, té disponible una API REST.
about.rest_api.link_to_documentation=Enllaç a la documentació.
about.shortcuts.mouse_middleclick=Clic amb el botó del mig
about.shortcuts.open_next_entry=obrir entrada següent
about.shortcuts.open_previous_entry=obrir entrada prèvia
about.shortcuts.spacebar=espai/majúscula+espai
about.shortcuts.move_page_down_up=mou la pàgina avall/amunt
about.shortcuts.focus_next_entry=fixa el focus en l'entrada següent entrada sense obrir-la
about.shortcuts.focus_previous_entry=fixa el focus en l'entrada prèvia sense obrir-la
about.shortcuts.open_next_feed=obrir canal o categoria següent
about.shortcuts.open_previous_feed=obrir canal o categoria prèvia
about.shortcuts.open_close_current_entry=obre/tanca entrada actual
about.shortcuts.open_current_entry_in_new_window=obrir entrada actual en una finestra nova
about.shortcuts.open_current_entry_in_new_window_background=obrir entrada actual en una finestra nova en segon pla
about.shortcuts.star_unstar=destacar/treure destacat a l'entrada actual
about.shortcuts.mark_current_entry=marcar com a llegida/no llegida l'entrada actual
about.shortcuts.mark_all_as_read=marcar totes les entrades com a llegides
about.shortcuts.open_in_new_tab_mark_as_read=obrir entrada en una pestanya nova i marcar com a llegida
about.shortcuts.fullscreen=commutar el mode de pantalla completa
about.shortcuts.font_size=incrementar/reduir la mida de la font de l'entrada actual
about.shortcuts.go_to_all=anar a la vista de Tot
about.shortcuts.go_to_starred=anar a la vista de Destacats
about.shortcuts.feed_search=navegar a una subscripció introduint-ne el nom

View File

@@ -6,6 +6,7 @@ global.download = Stáhnout
global.link = Odkaz
global.bookmark = Záložky
global.close = Zavřít
global.tags=Tags ####### Needs translation
tree.subscribe = Nový odběr
tree.import = Importovat
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc = Seřadit podle nejnovějšího/nejstaršího
toolbar.titles_only = Zobrazit jenom titulky
toolbar.expanded_view = Rozšířený náhled
toolbar.mark_all_as_read = Označit vše jako přečtené
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day = Položky starší než den
toolbar.mark_all_older_week = Položky starší než týden
toolbar.mark_all_older_two_weeks = Položky starší než dva týdny
@@ -66,6 +68,8 @@ settings.general.show_unread = Zobrazit položky a kategorie z přečtenými pol
settings.general.social_buttons = Zobrazit možnosti sdílení
settings.general.scroll_marks = Skrolování v rozšířeném náhledu označí položky jako přečtené
settings.appearance = Vzhled
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme = Motiv
settings.submit_your_theme = Nahrát vlastní motiv
settings.custom_css = Vlastní motiv (CSS)
@@ -83,7 +87,10 @@ details.queued_for_refresh = Ve frontě na obnovu
details.feed_url = URL RSS zdroje
details.generate_api_key_first = Vygenerujte si API klíč na stránce vašeho profilu.
details.unsubscribe = Odhlásit odběr.
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details = Detail kategorie
details.tag_details=Tag details ####### Needs translation
details.parent_category = Hlavní kategorie
profile.user_name = Uživatelské jméno
@@ -98,6 +105,7 @@ profile.generate_new_api_key = Vygenerovat nový API klíč
profile.generate_new_api_key_info = Změnou hesla vygenerujete nový API klíč
profile.opml_export = exportovat do formátu OPML
profile.delete_account = Odstranit účet
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api = REST API
about.keyboard_shortcuts = Klávesové zkratky

View File

@@ -6,6 +6,7 @@ global.download=Lawrlwytho
global.link=Dolen
global.bookmark=Nod tudalen
global.close=Cau
global.tags=Tags ####### Needs translation
tree.subscribe=Tanysgrifio
tree.import=Mewnforio
@@ -17,10 +18,10 @@ subscribe.feed_url=URL Ffrwd
subscribe.feed_name=Enw Ffrwd
subscribe.category=Categori
import.google_reader_prefix=Gadawa i mi fewnforio dy ffrydiau o dy
import.google_reader_prefix=Gad i mi fewnforio dy ffrydiau o dy
import.google_reader_suffix= gyfrif.
import.google_download=Fel arall, lanlwytho dy ffeil tanysgrifiadau.xml
import.google_download_link=Lawrlwytho fe yma.
import.google_download=Fel arall, lanlwytha dy ffeil tanysgrifiadau.xml
import.google_download_link=Lawrlwytha fe yma.
import.xml_file=Ffeil OPML
new_category.name=Enw
@@ -31,14 +32,15 @@ toolbar.all=Popeth
toolbar.previous_entry=Eitem blaenorol
toolbar.next_entry=Eitem nesaf
toolbar.refresh=Adnewyddu
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
toolbar.refresh_all=Gorfodi ail-lwytho pob ffrwd
toolbar.sort_by_asc_desc=Trefnu yn ôl dyddiad
toolbar.titles_only=Teitlau yn unig
toolbar.expanded_view=Golygfa estynedig
toolbar.mark_all_as_read=Marcio popeth fel darllenwyd
toolbar.mark_all_older_day=Eitemau sy'n hyn na diwrnod
toolbar.mark_all_older_week=Eitemau sy'n hyn nag wythnos
toolbar.mark_all_older_two_weeks=Eitemau sy'n hyn na phythefnos
toolbar.expanded_view=Golwg estynedig
toolbar.mark_all_as_read=Nodi'r cyfan fel wedi ei ddarllen
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Eitemau hyn na diwrnod
toolbar.mark_all_older_week=Eitemau hyn nag wythnos
toolbar.mark_all_older_two_weeks=Eitemau hyn na phythefnos
toolbar.settings=Gosodiadau
toolbar.profile=Proffil
toolbar.admin=Gweinyddwr
@@ -46,44 +48,49 @@ toolbar.about=Ynghylch
toolbar.logout=Allgofnodi
toolbar.donate=Rhoddi
view.entry_source=from ####### Needs translation
view.entry_author=by ####### Needs translation
view.error_while_loading_feed=Gwall tra'n llwytho'r ffrwd
view.keep_unread=Cadw fel heb ei darllen
view.no_unread_items=dim eitemau heb eu darllen
view.mark_up_to_here=Mark as read up to here ####### Needs translation
view.search_for=searching for: ####### Needs translation
view.no_search_results=No match found for the requested keywords ####### Needs translation
view.entry_source=o
view.entry_author=gan
view.error_while_loading_feed=Gwall wrth lwytho'r ffrwd
view.keep_unread=Parhau i'w nodi fel heb ei ddarllen
view.no_unread_items=: Dim eitemau heb eu darllen ###### Cynnwys y colon oherwydd gystrawen y cyd-destyn
view.mark_up_to_here=Nodi'r rhai hyd yma fel wedi eu darllen
view.search_for=yn chwilio am:
view.no_search_results=Ni chanfuwyd unrhyw beth gyda'r geiriau hynny
feedsearch.hint=Teipio tanysgrifiad...
feedsearch.hint=Rho'r tanysgrifiad...
feedsearch.help=Defnyddia'r dychwelwr i ddethol a saethau i lywio
feedsearch.result_prefix=Dy danysgrifiadau:
settings.general=Cyffredinol
settings.general.language=Iaith
settings.general.language.contribute=Cyfrannu gyda chyfieithiadau
settings.general.language.contribute=Cyfrannu drwy gyfieithu
settings.general.show_unread=Dangos ffrydiau a chategoriau gyda dim eitemau heb eu darllen
settings.general.social_buttons=Dangos botymau rhannu
settings.general.scroll_marks=Mewn golygfa estynedig, sgrolio trwy eitemau yn marcio fel darllenwyd
settings.appearance=Golygfa
settings.general.scroll_marks=Marcio eitemau fel wedi eu darllen wrth sgrolio drwyddynt yn y golwg estynedig ###### Defnyddio gystrawen debyg i'r ddau uwch.
settings.appearance=Golwg
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Thema
settings.submit_your_theme=Cyflwyno dy thema
settings.submit_your_theme=Cyflwyna dy thema
settings.custom_css=CSS wedi'i addasu
details.feed_details=Manylion ffrwd
details.url=URL
details.website=Website ####### Needs translation
details.website=Gwefan
details.name=Enw
details.category=Categori
details.position=Safle
details.last_refresh=Adnewyddiad diwethaf
details.message=Last refresh message ####### Needs translation
details.message=Neges adnewyddiad diwethaf
details.next_refresh=Adnewyddiad nesaf
details.queued_for_refresh=Ciwiwyd am adnewyddu
details.queued_for_refresh=Ciwiwyd i'w adnewyddu
details.feed_url=URL Ffrwd
details.generate_api_key_first=Cynhyrchu allwedd API yn dy broffil yn gyntaf.
details.generate_api_key_first=Rhaid creu allwedd API yn dy broffil yn gyntaf.
details.unsubscribe=Dad-danysgrifio
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Manylion categori
details.tag_details=Tag details ####### Needs translation
details.parent_category=Categori rhiant
profile.user_name=Enw defnyddiwr
@@ -91,59 +98,60 @@ profile.email=E-bost
profile.change_password=Newid cyfrinair
profile.confirm_password=Cadarnhau cyfrinair
profile.minimum_6_chars=Isafswm 6 nod
profile.passwords_do_not_match=Cyfrineiriau yn wahanol
profile.api_key=allwedd API
profile.api_key_not_generated=Heb gynhyrchu eto
profile.generate_new_api_key=Cynhyrchu allwedd API newydd
profile.generate_new_api_key_info=Newid cyfrinair yn cynhyrchu allwedd API newydd
profile.passwords_do_not_match=Mae'r cyfrineiriau yn wahanol
profile.api_key=Allwedd API
profile.api_key_not_generated=Heb ei gynhyrchu eto
profile.generate_new_api_key=Creu allwedd API newydd
profile.generate_new_api_key_info=Mae newid cyfrinair yn creu allwedd API newydd
profile.opml_export=Allforio OPML
profile.delete_account=Dileu cyfrif
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Llwybr byr bysellfwrdd
about.version=CommaFeed version ####### Needs translation
about.line1_prefix=CommaFeed yn prosiect cod agored. Mae'r cod ar
about.version=Fersiwn CommaFeed: ###### Cynnwys y colon oherwydd gystrawen y cyd-destun
about.line1_prefix=Mae CommaFeed yn prosiect cod agored. Mae'r cod ar
about.line1_suffix=.
about.line2_prefix=Os wyt ti'n ffeindio problem, plis adrodda fe ar dudalen problemau o'r
about.line2_prefix=Os wyt ti'n ffeindio problem, plîs gad wybod amdano ar dudalen problemau o'r
about.line2_suffix=\ prosiect.
about.line3=Os wyt ti'n hoffi'r prosiect, plis ystyried cyfraniad er mwyn cefnogi'r datblygwr a helpu gyda chynnal a chadw o'r wefan hon.
about.line4=I'r rhai sy'n hoff o bitcoin, dyma'r gyfeiriad
about.line3=Os wyt ti'n hoffi'r prosiect, plîs ystyria cyfrannu i gefnogi'r datblygwr a helpu gyda chynnal a chadw'r wefan hon.
about.line4=I'r rhai sy'n hoff o Bitcoin, dyma'r cyfeiriad
about.goodies=Goodies
about.goodies.android_app=Android app ####### Needs translation
about.goodies.android_app=Ap Android
about.goodies.subscribe_url=URL Tanysgrifio
about.goodies.chrome_extension=estyniad Chrome
about.goodies.firefox_extension=estyniad Firefox
about.goodies.opera_extension=estyniad Opera
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio (clicio)
about.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio ###### Dim angen 'Click' - digon amlwg o'r cyd-destyn
about.goodies.subscribe_bookmarklet_asc=Hynaf yn gyntaf
about.goodies.subscribe_bookmarklet_desc=Diweddaraf yn gyntaf
about.goodies.next_unread_bookmarklet=Botwm eitem nesaf heb ei ddarllen (llusgo i far nodau)
about.translation=Translation
about.translation.message=Rydym ni angen dy help i gyfieithu CommaFeed.
about.translation=Cyfieithiad
about.translation.message=Rydym angen dy help i gyfieithu CommaFeed.
about.translation.link=Gweler sut i gyfrannu i gyfieithiadau.
about.announcements=Datganiadau
about.rest_api.line1=Mae CommaFeed wedi cael ei adeiladu ar JAX-RS ac AngularJS. Mae REST API ar gael.
about.rest_api.line1=Adeiladir CommaFeed ar JAX-RS ac AngularJS. Mae REST API ar gael.
about.rest_api.link_to_documentation=Dolen i'r ddogfennaeth.
about.shortcuts.mouse_middleclick=llygoden clic-canol
about.shortcuts.open_next_entry=agor eitem nesaf
about.shortcuts.open_previous_entry=agor eitem flaenorol
about.shortcuts.spacebar=space/shift+space ####### Needs translation
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation
about.shortcuts.focus_next_entry=gosod ffocws ar eitem nesaf heb ei hagor
about.shortcuts.focus_previous_entry=gosod ffocws ar eitem flaenorol heb ei hagor
about.shortcuts.open_next_feed=agor ffrwd neu gategori nesaf
about.shortcuts.open_previous_feed=agor ffrwd neu gategori blaenorol
about.shortcuts.open_close_current_entry=agor/cau eitem gyfredol
about.shortcuts.open_current_entry_in_new_window=agor eitem gyfredol mewn ffenestr newydd
about.shortcuts.open_current_entry_in_new_window_background=agor eitem gyfredol mewn ffenestr newydd yn y cefndir
about.shortcuts.star_unstar=serennu/dadserennu eitem gyfredol
about.shortcuts.mark_current_entry=marcio eitem gyfredol fel darllenwyd/heb ddarllen
about.shortcuts.mark_all_as_read=marcio popeth fel darllenwyd
about.shortcuts.open_in_new_tab_mark_as_read=agor eitem mewn tab newydd a marcio fel darllenwyd
about.shortcuts.fullscreen=toggle full screen mode ####### Needs translation
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation
about.shortcuts.go_to_all=go to the All view ####### Needs translation
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
about.shortcuts.feed_search=llywio i danysgrifiad trwy rhoi ei enw mewn
about.shortcuts.mouse_middleclick=clic botwm canol llygoden
about.shortcuts.open_next_entry=agor yr eitem nesaf
about.shortcuts.open_previous_entry=agor yr eitem flaenorol
about.shortcuts.spacebar=space/shift+space
about.shortcuts.move_page_down_up=symud y tudalen i lawr/fyny
about.shortcuts.focus_next_entry=newid ffocws i'r eitem nesaf heb ei hagor
about.shortcuts.focus_previous_entry=newid ffocws i'r eitem flaenorol heb ei hagor
about.shortcuts.open_next_feed=agor y ffrwd neu gategori nesaf
about.shortcuts.open_previous_feed=agor y ffrwd neu gategori blaenorol
about.shortcuts.open_close_current_entry=agor/cau yr eitem gyfredol
about.shortcuts.open_current_entry_in_new_window=agor yr eitem gyfredol mewn ffenestr newydd
about.shortcuts.open_current_entry_in_new_window_background=agor yr eitem gyfredol mewn ffenestr newydd yn y cefndir
about.shortcuts.star_unstar=serennu/dadserennu'r eitem gyfredol
about.shortcuts.mark_current_entry=marcio'r eitem gyfredol fel wedi/heb ei ddarllen
about.shortcuts.mark_all_as_read=marcio popeth fel wedi ei ddarllen
about.shortcuts.open_in_new_tab_mark_as_read=agor yr eitem mewn tab newydd a'i farcio fel wedi ei ddarllen
about.shortcuts.fullscreen=toglo'r golwg sgrin lawn
about.shortcuts.font_size=cynyddu/lleihau maint ffont yr eitem gyfredol
about.shortcuts.go_to_all=newid i olwg 'Popeth'
about.shortcuts.go_to_starred=newid i olwg 'Serennwyd'
about.shortcuts.feed_search=llywio i danysgrifiad gan roi ei enw mewn

View File

@@ -6,6 +6,7 @@ global.download=Hent
global.link=Link
global.bookmark=Bogmærke
global.close=Luk
global.tags=Tags ####### Needs translation
tree.subscribe=Abonner
tree.import=Importer
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sorter efter dato ny/gammel
toolbar.titles_only=Kun titler
toolbar.expanded_view=Udvidet visning
toolbar.mark_all_as_read=Marker alle som læst
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Artikler ældere end én dag
toolbar.mark_all_older_week=Artikler ældere end én uge
toolbar.mark_all_older_two_weeks=Artikler ældere end to uger
@@ -66,6 +68,8 @@ settings.general.show_unread=Vis abonnomenter og kategorier med læste artikler
settings.general.social_buttons=Vis delingsknapper
settings.general.scroll_marks=I udvidet visning, marker artikler som læste når der rulles forbi dem
settings.appearance=Udseende
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Tema
settings.submit_your_theme=Indsend dit tema
settings.custom_css=Brugerdefineret CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=I kø til opdatering
details.feed_url=URL for abonnement
details.generate_api_key_first=Generer en API nøgle i din profil først.
details.unsubscribe=Afmeld abonnement
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Kategori detaljer
details.tag_details=Tag details ####### Needs translation
details.parent_category=Overordnet kategori
profile.user_name=Brugernavn
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generer ny API nøgle
profile.generate_new_api_key_info=Ændring af adgangskode vil generere en ny API nøgle
profile.opml_export=OPML eksport
profile.delete_account=Slet konto
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Tastaturgenveje

View File

@@ -6,6 +6,7 @@ global.download=Herunterladen
global.link=Link
global.bookmark=Lesezeichen
global.close=Schließen
global.tags=Tags
tree.subscribe=Abonnieren
tree.import=Importieren
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Nach Datum sortieren (auf-/absteigend)
toolbar.titles_only=Nur Überschriften
toolbar.expanded_view=Ausgedehnte Ansicht
toolbar.mark_all_as_read=Alle Artikel als gelesen markieren
toolbar.mark_all_older_12_hours=Artikel älter als 12 Stunden
toolbar.mark_all_older_day=Artikel älter als ein Tag
toolbar.mark_all_older_week=Artikel älter als eine Woche
toolbar.mark_all_older_two_weeks=Artikel älter als zwei Wochen
@@ -66,6 +68,8 @@ settings.general.show_unread=Zeige Feeds und Kategorien mit ungelesenen Einträg
settings.general.social_buttons=Zeige Buttons zum Teilen von Inhalten über soziale Netzwerke
settings.general.scroll_marks=In der ausgedehnten Ansicht werden Artikel beim Scrollen als gelesen markiert
settings.appearance=Aussehen
settings.scroll_speed=Geschwindigkeit beim scrollen zwischen Einträgen (in Millisekunden)
settings.scroll_speed.help=setze auf 0 zum deaktivieren
settings.theme=Theme
settings.submit_your_theme=Füg dein Theme hinzu
settings.custom_css=Eigenes CSS
@@ -77,13 +81,16 @@ details.name=Name
details.category=Kategorie
details.position=Position
details.last_refresh=Letzte Aktualisierung
details.message=Last refresh message ####### Needs translation
details.message=Nachricht der letzten Aktualisierung
details.next_refresh=Nächste Aktualisierung
details.queued_for_refresh=Wartet auf Aktualisierung
details.feed_url=Feed Adresse
details.generate_api_key_first=Generiere zuerst einen API Schlüssel in deinem Profil.
details.unsubscribe=Kündigen
details.unsubscribe_confirmation=Bist du sicher das du diesen Feed kündigen möchtest?
details.delete_category_confirmation=Bist du sicher das du diese Kategorie löschen möchtest?
details.category_details=Kategoriedetails
details.tag_details=Tag Details
details.parent_category=Übergeordnete Kategorie
profile.user_name=Benutzername
@@ -97,7 +104,8 @@ profile.api_key_not_generated=Noch nicht generiert
profile.generate_new_api_key=Generiere einen neuen API key
profile.generate_new_api_key_info=Das Ändern des Passwortes erzeugt einen neuen API Schlüssel
profile.opml_export=OPML exportieren
profile.delete_account=Lösche den Account
profile.delete_account=Account löschen
profile.delete_account_confirmation=Deinen Account löschen? Es gibt kein Zurück!
about.rest_api=REST API
about.keyboard_shortcuts=Tastatur Kurzbefehle

View File

@@ -6,6 +6,7 @@ global.download=Download
global.link=Link
global.bookmark=Bookmark
global.close=Close
global.tags=Tags
tree.subscribe=Subscribe
tree.import=Import
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc
toolbar.titles_only=Titles only
toolbar.expanded_view=Expanded view
toolbar.mark_all_as_read=Mark all as read
toolbar.mark_all_older_12_hours=Items older than 12 hours
toolbar.mark_all_older_day=Items older than a day
toolbar.mark_all_older_week=Items older than a week
toolbar.mark_all_older_two_weeks=Items older than two weeks
@@ -66,6 +68,8 @@ settings.general.show_unread=Show feeds and categories with no unread entries
settings.general.social_buttons=Show social sharing buttons
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
settings.appearance=Appearance
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds)
settings.scroll_speed.help=set to 0 to disable
settings.theme=Theme
settings.submit_your_theme=Submit your theme
settings.custom_css=Custom CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh
details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Unsubscribe
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed?
details.delete_category_confirmation=Are you sure you want to delete this category?
details.category_details=Category details
details.tag_details=Tag details
details.parent_category=Parent category
profile.user_name=User name
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generate new API key
profile.generate_new_api_key_info=Changing password will generate a new API key
profile.opml_export=OPML export
profile.delete_account=Delete account
profile.delete_account_confirmation=Delete your account? There's no turning back!
about.rest_api=REST API
about.keyboard_shortcuts=Keyboard shortcuts
@@ -145,4 +153,4 @@ about.shortcuts.fullscreen=toggle full screen mode
about.shortcuts.font_size=increase/decrease font size of the current entry
about.shortcuts.go_to_all=go to the All view
about.shortcuts.go_to_starred=go to the Starred view
about.shortcuts.feed_search=navigate to a subscription by entering the subscription name
about.shortcuts.feed_search=navigate to a subscription by entering the subscription name

View File

@@ -6,6 +6,7 @@ global.download=Descargar
global.link=Enlace
global.bookmark=Marcador
global.close=Close ####### Needs translation
global.tags=Tags ####### Needs translation
tree.subscribe=Subscribir
tree.import=Importar
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Ordenar por fecha asc/desc
toolbar.titles_only=Sólo Títulos
toolbar.expanded_view=Vista Expandida
toolbar.mark_all_as_read=Marcar todos como leído
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Artículos anteriores a un día
toolbar.mark_all_older_week=Artículos más de una semana
toolbar.mark_all_older_two_weeks=Artículos más de does semanas
@@ -66,6 +68,8 @@ settings.general.show_unread=Mostrar canales y categorías sin entradas no leíd
settings.general.social_buttons=Mostrar botones de compartir de redes sociales.
settings.general.scroll_marks=En vista expandida, el desplazamiento por las entradas las marca como leídas
settings.appearance=Appearance ####### Needs translation
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Theme ####### Needs translation
settings.submit_your_theme=Submit your theme ####### Needs translation
settings.custom_css=CSS Personalizado
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh ####### Needs translation
details.feed_url=URL del Canal
details.generate_api_key_first=Genera una llave API en tu perfil primero.
details.unsubscribe=Terminar subscripción
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Detalles de la categoría
details.tag_details=Tag details ####### Needs translation
details.parent_category=Categoría principal
profile.user_name=Nombre de usuario
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Generar nueva llave API
profile.generate_new_api_key_info=Al cambiar la contraseña se generará una nueva llave API
profile.opml_export=Exportación de OPML
profile.delete_account=Eliminar cuenta
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Atajos de teclado

View File

@@ -6,6 +6,7 @@ global.download=بارگیری
global.link=پیوند
global.bookmark=بوکمارک
global.close=بستن
global.tags=برجسپ‌ها
tree.subscribe=مشترک شوید
tree.import=درون‌ریزی
@@ -31,11 +32,12 @@ toolbar.all=همه
toolbar.previous_entry=مطلب قبلی
toolbar.next_entry=مطلب بعدی
toolbar.refresh=تازه‌سازی
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
toolbar.refresh_all=مجبورکردن تازه‌سازی همهٔ خوراک‌ها
toolbar.sort_by_asc_desc=مرتب‌کردن بر اساس تاریخ به‌صورت صعودی/نزولی
toolbar.titles_only=فقط عنوان‌ها
toolbar.expanded_view=نمای گسترش‌یافته
toolbar.mark_all_as_read=علامت‌گذاری تمامی مطالب به‌عنوان خوانده‌شده
toolbar.mark_all_older_12_hours=مطالب قدیمی‌تر از ۱۲ ساعت
toolbar.mark_all_older_day=مطالب قدیمی‌تر از یک روز
toolbar.mark_all_older_week=مطالب قدیمی‌تر از یک هفته
toolbar.mark_all_older_two_weeks=مطالب قدیمی تر از چند هفته قیل
@@ -52,8 +54,8 @@ view.error_while_loading_feed=متأسفانه، هنگام بارگیری ای
view.keep_unread=خوانده‌نشده نگه‌دار
view.no_unread_items=هیچ مطلب خوانده‌نشده‌ای ندارد.
view.mark_up_to_here=تا اینجا را خوانده‌شده در نظر بگیر
view.search_for=searching for: ####### Needs translation
view.no_search_results=No match found for the requested keywords ####### Needs translation
view.search_for=جستجو برای:
view.no_search_results=هیج نتیجه‌ای برای کلیدواژه‌های درخواستی یافت نشد
feedsearch.hint=نوشتن بر روی یک اشتراک...
feedsearch.help=دکمهٔ بازگشت برای انتخاب و دکمه‌های جهت‌دار را برای ناوبری استفاده کن.
@@ -66,6 +68,8 @@ settings.general.show_unread=تنها خوراک‌ها و دسته‌های ر
settings.general.social_buttons=نشان‌دادن دکمه‌های اشتراک‌گذاری در شبکه‌های اجتماعی
settings.general.scroll_marks=در نمای گسترش‌یافته، لغزیدن بر روی مطالب به‌عنوان نشانه‌گذاری به‌عنوان خوانده‌شده در نظر گرفته‌شوند.
settings.appearance=ظاهر
settings.scroll_speed=سرعت لغزش هنگام گشتن بین مدخل‌ها (به میلی‌ثانیه)
settings.scroll_speed.help=قراردادن به ۰ برای غیرفعال‌کردن
settings.theme=پوسته
settings.submit_your_theme=پوستهٔ خود را ارسال‌کنید
settings.custom_css=سی‌اس‌اس شخصی‌سازی‌شده
@@ -77,13 +81,16 @@ details.name=نام
details.category=دسته
details.position=موقعیت
details.last_refresh=آخرین بروزرسانی
details.message=Last refresh message ####### Needs translation
details.message=پیام آخرین تازه‌سازی
details.next_refresh=بروزرسانی بعدی
details.queued_for_refresh=منتظر برای بروزرسانی
details.feed_url=نشانی خوراک
details.generate_api_key_first=ابتدا یک کلید API در نمایهٔ خود ایجاد کنید.
details.unsubscribe=لغو اشتراک
details.unsubscribe_confirmation=مطمئنید می‌خواهید از این این لغو اشتراک کنید؟
details.delete_category_confirmation=مطمئنید می‌خواهید این رده را حذف کنید؟
details.category_details=جزئیات دسته
details.tag_details=جزئیات برچسپ
details.parent_category=ردهٔ پدر
profile.user_name=نام کاربری
@@ -98,10 +105,11 @@ profile.generate_new_api_key=ایجاد کلید جدید API
profile.generate_new_api_key_info=تغییر گذرواژه کلید API به‌وجود خواهد آورد.
profile.opml_export=خارج‌سازی OPML
profile.delete_account=حذف حساب کاربری
profile.delete_account_confirmation=حذف حسابتان؟ بازگشتی وجود ندارد!
about.rest_api=REST API
about.keyboard_shortcuts=کلیدهای میانبر
about.version=CommaFeed version ####### Needs translation
about.version=نسخهٔ کامافید
about.line1_prefix=کامافید یک پروژه متن‌باز است. مخازن آن در
about.line1_suffix=میزبانی می‌شود.
about.line2_prefix=اگر شما به مسئله‌ای برخورده اید، لطفاً آن را در صفحه مسائل گزارش دهید
@@ -143,7 +151,7 @@ about.shortcuts.mark_all_as_read=علامت‌گذاری تمامی مطالب
about.shortcuts.open_in_new_tab_mark_as_read=باز‌کردن مطلب در سربرگ جدید و علامت‌گذاری آن به‌عنوان خوانده‌شده
about.shortcuts.fullscreen=فعال/غیرفعال‌کردن حالت تمام صفحه
about.shortcuts.font_size=افزایش/کاهش اندازهٔ قلم مدخل فعلی
about.shortcuts.go_to_all=go to the All view ####### Needs translation
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
about.shortcuts.go_to_all=رفتن به نمای همه
about.shortcuts.go_to_starred=رفتن به نمای ستاره داده‌شده‌ها
about.shortcuts.feed_search=ناوبری به یک اشتراک با نوشتن نام اشتراک

View File

@@ -6,6 +6,7 @@ global.download=Lataa
global.link=Linkki
global.bookmark=Kirjanmerkki
global.close=Sulje
global.tags=Tagit
tree.subscribe=Tilaa syöte
tree.import=Tuo
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Järjestä päivämäärän mukaan nousevasti/laskevast
toolbar.titles_only=Näytä vain otsikot
toolbar.expanded_view=Laajennettu näkymä
toolbar.mark_all_as_read=Merkitse kaikki luetuiksi
toolbar.mark_all_older_12_hours=12 tuntia vanhemmat otsikot
toolbar.mark_all_older_day=Päivää vanhemmat otsikot
toolbar.mark_all_older_week=Viikkoa vanhemmat otsikot
toolbar.mark_all_older_two_weeks=Kahta viikkoa vanhemmat otsikot
@@ -52,8 +54,8 @@ view.error_while_loading_feed=Virhe tilausta ladattaessa
view.keep_unread=Pidä lukemattomana
view.no_unread_items=ei sisällä lukemattomia otsikoita.
view.mark_up_to_here=Merkitse luetuksi tähän asti
view.search_for=searching for: ####### Needs translation
view.no_search_results=No match found for the requested keywords ####### Needs translation
view.search_for=Etsi sanoilla:
view.no_search_results=Ei tuloksia annetuilla hakusanoilla.
feedsearch.hint=Kirjoita syötteen nimi...
feedsearch.help=Siirry syötteiden välillä nuolinäppäimillä ja valitse syöte enterillä.
@@ -66,6 +68,8 @@ settings.general.show_unread=Näytä syötteet ja kansiot, joissa ei ole lukemat
settings.general.social_buttons=Näytä jakonapit
settings.general.scroll_marks=Laajennetussa näkymässä otsikoiden selaaminen merkitsee ne luetuiksi
settings.appearance=Ulkonäkö
settings.scroll_speed=Vieritysnopeus otsikoiden välillä navigoidessa (millisekunneissa)
settings.scroll_speed.help=Aseta 0 poistaaksesi vieritys käytöstä.
settings.theme=Teema
settings.submit_your_theme=Lähetä oma teemasi
settings.custom_css=Oma CSS
@@ -77,13 +81,16 @@ details.name=Nimi
details.category=Kansio
details.position=Paikka
details.last_refresh=Viimeisin päivitys
details.message=Last refresh message ####### Needs translation
details.message=Viimeisimmän päivityksen viesti
details.next_refresh=Seuraava päivitys
details.queued_for_refresh=Jonossa päivitettäväksi
details.feed_url=Syötteen osoite
details.generate_api_key_first=Luo API-avain profiilissasi.
details.unsubscribe=Peruuta tilaus
details.unsubscribe_confirmation=Haluatko varmasti lopettaa tämän syötteen tilauksen?
details.delete_category_confirmation=Haluatko varmasti poistaa tämän kansion?
details.category_details=Kansion tiedot
details.tag_details=Tagin tiedot
details.parent_category=Yläkansio
profile.user_name=Käyttäjänimi
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Luo uusi API-avain
profile.generate_new_api_key_info=Salasanan vaihtaminen luo uuden API-avaimen
profile.opml_export=OPML vienti
profile.delete_account=Poista tunnus
profile.delete_account_confirmation=Haluatko varmasti poistaa tunnuksesi? Tätä ei voi perua!
about.rest_api=REST-API
about.keyboard_shortcuts=Näppäinoikotiet

View File

@@ -6,6 +6,7 @@ global.download=Télécharger
global.link=Lien
global.bookmark=Favoris
global.close=Fermer
global.tags=Tags ####### Needs translation
tree.subscribe=S'abonner
tree.import=Importer
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Trier par date croissante/décroissante
toolbar.titles_only=Titres uniquement
toolbar.expanded_view=Vue étendue
toolbar.mark_all_as_read=Tout marquer comme lu
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Articles de plus d'un jour
toolbar.mark_all_older_week=Articles de plus d'une semaine
toolbar.mark_all_older_two_weeks=Articles de plus d'un mois
@@ -66,6 +68,8 @@ settings.general.show_unread=Afficher les flux et les catégories pour lesquels
settings.general.social_buttons=Afficher les boutons de partage sur réseaux sociaux
settings.general.scroll_marks=En mode de lecture étendu, marquer comme lu les éléments lorsque la fenêtre descend.
settings.appearance=Apparence
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Thème
settings.submit_your_theme=Soumettez votre thème.
settings.custom_css=CSS personnelle
@@ -83,7 +87,10 @@ details.queued_for_refresh=En file d'attente
details.feed_url=URL du flux
details.generate_api_key_first=Générez une clé API dans votre profil d'abord.
details.unsubscribe=Se désabonner
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Détails de la catégorie
details.tag_details=Tag details ####### Needs translation
details.parent_category=Catégorie parente
profile.user_name=Nom
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Générer une nouvelle clé API
profile.generate_new_api_key_info=Changer de mot de passe va générer une nouvelle clé API
profile.opml_export=Export du fichier OPML
profile.delete_account=Effacer le compte
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=API REST
about.keyboard_shortcuts=Raccourcis clavier

View File

@@ -6,6 +6,7 @@ global.download=Descargar
global.link=Ligazón
global.bookmark=Marcador
global.close=Pechar
global.tags=Tags ####### Needs translation
tree.subscribe=Subscribir
tree.import=Importar
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Ordenar por data asc/desc
toolbar.titles_only=Só títulos
toolbar.expanded_view=Vista expandida
toolbar.mark_all_as_read=Marcar todos como lidos
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Artigos anteriores a un día
toolbar.mark_all_older_week=Artigos de máis de unha semana
toolbar.mark_all_older_two_weeks=Artigos de máis de dúas semanas
@@ -66,6 +68,8 @@ settings.general.show_unread=Mostrar fontes e categorías sen entradas non lidas
settings.general.social_buttons=Mostrar botóns de compartir en redes sociais.
settings.general.scroll_marks=En vista expandida, o desplazamento polas entradas márcaas como lidas.
settings.appearance=Aspecto
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Decorado
settings.submit_your_theme=Envíe o seu decorado
settings.custom_css=CSS Personalizado
@@ -83,7 +87,10 @@ details.queued_for_refresh=En cola para actualizar
details.feed_url=URL da fonte
details.generate_api_key_first=Antes debes xerar unha chave API no teu perfil.
details.unsubscribe=Rematar suscripción
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Detalles da categoría
details.tag_details=Tag details ####### Needs translation
details.parent_category=Categoría principal
profile.user_name=Nome de usuario
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Xerar nova chave da API
profile.generate_new_api_key_info=Ao cambiar o contrasinal xerarase unha nova chave API
profile.opml_export=Exportación de OPML
profile.delete_account=Eliminar conta
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Atallos de teclado

View File

@@ -6,6 +6,7 @@ global.download=جیرأکش
global.link=خال
global.bookmark=بوکمارک
global.close=دَوَستن
global.tags=Tags ####### Needs translation
tree.subscribe=مشترک ببید
tree.import=درینأدأن
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=تاریخˇ سر دچئن
toolbar.titles_only=خالی تیتران
toolbar.expanded_view=واشاده نما
toolbar.mark_all_as_read=همه‌ته مطالبه چاکون بخانده
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=یک روز پیشترˇ مطالب
toolbar.mark_all_older_week=یک هفته پیشترˇ مطالب
toolbar.mark_all_older_two_weeks=چن هفته پیشترˇ مطالب
@@ -66,6 +68,8 @@ settings.general.show_unread=تنها خوراک‌ها و دسته‌های ر
settings.general.social_buttons=نشان‌دادن دکمه‌های اشتراک‌گذاری در شبکه‌های اجتماعی
settings.general.scroll_marks=در نمای گسترش‌یافته، لغزیدن بر روی مطالب به‌عنوان نشانه‌گذاری به‌عنوان خوانده‌شده در نظر گرفته‌شوند.
settings.appearance=ظاهر
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=پوسته
settings.submit_your_theme=شیمه پوستهٰ اوسه کونید
settings.custom_css=سی‌اس‌اس شخصی‌سازی‌شده
@@ -83,7 +87,10 @@ details.queued_for_refresh=منتظر برای بروزرسانی
details.feed_url=نشانی خوراک
details.generate_api_key_first=ابتدا یک کلید API در نمایهٔ خود ایجاد کنید.
details.unsubscribe=لغو اشتراک
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=جرگه جزئیات
details.tag_details=Tag details ####### Needs translation
details.parent_category=پئرˇ جرگه
profile.user_name=کاربری نام
@@ -98,6 +105,7 @@ profile.generate_new_api_key=تازه کلید چاگودن API
profile.generate_new_api_key_info=رمزه عوضأگودن API کلیده چاکونه.
profile.opml_export=برینأدأن OPML
profile.delete_account=کاربری حسابه پاکودن
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=وئر زئنˇ کلیدان

View File

@@ -1,149 +1,157 @@
global.save=Mentés
global.cancel=Mégsem
global.delete=Törlés
global.required=Szükséges
global.download=Letöltés
global.link=Link
global.bookmark=Könyvjelző
global.close=Bezár
tree.subscribe=Feliratkozás
tree.import=Importálás
tree.new_category=Új kategória
tree.all=Összes
tree.starred=Csillagozott
subscribe.feed_url=Hírcsatorna URL
subscribe.feed_name=Hírcsatorna neve
subscribe.category=Kategória
import.google_reader_prefix=Engedd meg, hogy importáljuk a hírcsatornáidat a
import.google_reader_suffix= fiókjából.
import.google_download=Alternatívaként, feltöltheti a subscriptions.xml fájlt.
import.google_download_link=Letöltheti innen.
import.xml_file=OPML Fájl
new_category.name=Név
new_category.parent=Szülő
toolbar.unread=Olvasatlan
toolbar.all=Összes
toolbar.previous_entry=Előző elem
toolbar.next_entry=Következő elem
toolbar.refresh=Frissítés
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
toolbar.sort_by_asc_desc=Rendezés időrend szerint
toolbar.titles_only=Csak cím
toolbar.expanded_view=Részletes nézet
toolbar.mark_all_as_read=Az összes megjelölése olvasottként
toolbar.mark_all_older_day=Régebbiek, mint egy nap
toolbar.mark_all_older_week=Régebbiek, mint egy hét
toolbar.mark_all_older_two_weeks=Régebbiek, mint két hét
toolbar.settings=Beállítások
toolbar.profile=Profil
toolbar.admin=Admin
toolbar.about=Névjegy
toolbar.logout=Kilépés
toolbar.donate=Anyagi támogatás
view.entry_source=from ####### Needs translation
view.entry_author=by ####### Needs translation
view.error_while_loading_feed=Hiba történt ennek a hírcsatornának a betöltésekor
view.keep_unread=Megtartása olvasatlanként
view.no_unread_items=nincsen olvasatlan eleme.
view.mark_up_to_here=Mark as read up to here ####### Needs translation
view.search_for=searching for: ####### Needs translation
view.no_search_results=No match found for the requested keywords ####### Needs translation
feedsearch.hint=Keressen a hírcsatornák között...
feedsearch.help=Használja a nyíl billentyűket a navigáláshoz, az enter-t a kiválasztáshoz.
feedsearch.result_prefix=Az ön feliratkozásai:
settings.general=Általános
settings.general.language=Nyelv
settings.general.language.contribute=Segítsen a fordításban
settings.general.show_unread=Mutassa azokat a hírcsatornákat és kategóriákat amelyekben nincsen olvasatlan bejegyzés
settings.general.social_buttons=Mutassa a közösségi oldalak megosztás gombjait
settings.general.scroll_marks=Kiterjesztett nézetben, görgetéssel olvasottként jelöli meg a bejegyzést
settings.appearance=Megjelenés
settings.theme=Téma
settings.submit_your_theme=Küldje el a témáját
settings.custom_css=Saját CSS
details.feed_details=Hírcsatorna részletei
details.url=URL
details.website=Website ####### Needs translation
details.name=Név
details.category=Kategória
details.position=Position ####### Needs translation
details.last_refresh=Utolsó frissítés
details.message=Last refresh message ####### Needs translation
details.next_refresh=Következő frissítés
details.queued_for_refresh=Frissítésre vár
details.feed_url=Hírcsatorna URL
details.generate_api_key_first=A profiljában először egy API kulcsot kell generálnia.
details.unsubscribe=Leiratkozás
details.category_details=Kategória részletei
details.parent_category=Szülő kategória
profile.user_name=Felhasználói név
profile.email=E-mail
profile.change_password=Jelszó megváltoztatás
profile.confirm_password=Jelszó megerősítése
profile.minimum_6_chars=Legalább 8 karakter
profile.passwords_do_not_match=A jelszavak nem egyeznek
profile.api_key=API kulcs
profile.api_key_not_generated=Még nincsen generálva
profile.generate_new_api_key=Új API kulcs generálása
profile.generate_new_api_key_info=A jelszó megváltoztatása új API kulcsot generál
profile.opml_export=OPML exportálása
profile.delete_account=Fiók törlése
about.rest_api=REST API
about.keyboard_shortcuts=Gyorsbillentyűk
about.version=CommaFeed version ####### Needs translation
about.line1_prefix=A CommaFeed egy nyílt forrású projekt. A forrás megtalálható a
about.line1_suffix=oldalán.
about.line2_prefix=Ha hibába ütközik, kérjük jelentse azt a
about.line2_suffix=projekt oldalán.
about.line3=Ha tetszik önnek ez a szolgáltatás, akkor kérjük támogassa a fejlesztőket és, hogy fentarthassák a weboldalt.
about.line4=Akik jobban szeretnék az oldalt bitcon-nal támogatni, itt a cím
about.goodies=Hasznos dolgok
about.goodies.android_app=Android app ####### Needs translation
about.goodies.subscribe_url=Feliratkozás az URL-re
about.goodies.chrome_extension=Chrome bővítmény
about.goodies.firefox_extension=Firefox kiterjesztés
about.goodies.opera_extension=Opera kiterjesztés
about.goodies.subscribe_bookmarklet=Feliratkozás bookmarklet hozzáadása (klikkeléssel)
about.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation
about.goodies.next_unread_bookmarklet=Következő olvasatlan elem bookmarklet (húzza fel a könyvjelzősávba)
about.translation=Fordítás
about.translation.message=Segítségét kérjük a CommaFeed fordításához.
about.translation.link=Nézze meg, hogyan tud segíteni ebben.
about.announcements=Bejelentések
about.rest_api.line1=A CommaFeed a JAX-RS-re és az AngularJS-re épül. Ezért a RESTA API elérhető.
about.rest_api.link_to_documentation=Link a dokumentációhoz.
about.shortcuts.mouse_middleclick=középső egérgomb
about.shortcuts.open_next_entry=következő hír megnyitása
about.shortcuts.open_previous_entry=előző hír megnyitása
about.shortcuts.spacebar=space/shift+space ####### Needs translation
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation
about.shortcuts.focus_next_entry=megnyitás nélkül fókuszál a övetkező elemre
about.shortcuts.focus_previous_entry=megnyitás nélkül fókuszál az előző elemre
about.shortcuts.open_next_feed=a következő hírcsatorna vagy kategória megnyitása
about.shortcuts.open_previous_feed=az előző hírcsatorna vagy kategória megnyitása
about.shortcuts.open_close_current_entry=a jelenlegi elem megnyitása/bezárása
about.shortcuts.open_current_entry_in_new_window=a jelenlegi elem megnyitása új ablakban
about.shortcuts.open_current_entry_in_new_window_background=a jelenlegi elem megnyitása a háttérben, új ablakban
about.shortcuts.star_unstar=hírelem csillagozása
about.shortcuts.mark_current_entry=elem megjelölése olvasottként
about.shortcuts.mark_all_as_read=az összes elem megjelölése olvasottként
about.shortcuts.open_in_new_tab_mark_as_read=elem megnyitása új fülön és megjelölése olvasottként
about.shortcuts.fullscreen=toggle full screen mode ####### Needs translation
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation
about.shortcuts.go_to_all=go to the All view ####### Needs translation
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
about.shortcuts.feed_search=név szerinti keresés a hírcsatornák között
global.save=Mentés
global.cancel=Mégsem
global.delete=Törlés
global.required=Szükséges
global.download=Letöltés
global.link=Link
global.bookmark=Könyvjelző
global.close=Bezár
global.tags=Címkék
tree.subscribe=Feliratkozás
tree.import=Importálás
tree.new_category=Új kategória
tree.all=Összes
tree.starred=Csillagozott
subscribe.feed_url=Hírcsatorna URL
subscribe.feed_name=Hírcsatorna neve
subscribe.category=Kategória
import.google_reader_prefix=Engedd meg, hogy importáljuk a hírcsatornáidat a
import.google_reader_suffix= fiókjából.
import.google_download=Alternatívaként, feltöltheti a subscriptions.xml fájlt.
import.google_download_link=Letöltheti innen.
import.xml_file=OPML Fájl
new_category.name=Név
new_category.parent=Szülő
toolbar.unread=Olvasatlan
toolbar.all=Összes
toolbar.previous_entry=Előző elem
toolbar.next_entry=Következő elem
toolbar.refresh=Frissítés
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
toolbar.sort_by_asc_desc=Rendezés időrend szerint
toolbar.titles_only=Csak cím
toolbar.expanded_view=Részletes nézet
toolbar.mark_all_as_read=Az összes megjelölése olvasottként
toolbar.mark_all_older_12_hours=Régebbiek 12 óránál
toolbar.mark_all_older_day=Régebbiek, mint egy nap
toolbar.mark_all_older_week=Régebbiek, mint egy hét
toolbar.mark_all_older_two_weeks=Régebbiek, mint két hét
toolbar.settings=Beállítások
toolbar.profile=Profil
toolbar.admin=Admin
toolbar.about=Névjegy
toolbar.logout=Kilépés
toolbar.donate=Anyagi támogatás
view.entry_source=forrás
view.entry_author=szerző
view.error_while_loading_feed=Hiba történt ennek a hírcsatornának a betöltésekor
view.keep_unread=Megtartása olvasatlanként
view.no_unread_items=nincsen olvasatlan eleme.
view.mark_up_to_here=Megjelölés olvasottnak eddig
view.search_for=keresés erre:
view.no_search_results=Nem található semmi erre a keresett szóra
feedsearch.hint=Keressen a hírcsatornák között...
feedsearch.help=Használja a nyíl billentyűket a navigáláshoz, az enter-t a kiválasztáshoz.
feedsearch.result_prefix=Az ön feliratkozásai:
settings.general=Általános
settings.general.language=Nyelv
settings.general.language.contribute=Segítsen a fordításban
settings.general.show_unread=Mutassa azokat a hírcsatornákat és kategóriákat amelyekben nincsen olvasatlan bejegyzés
settings.general.social_buttons=Mutassa a közösségi oldalak megosztás gombjait
settings.general.scroll_marks=Kiterjesztett nézetben, görgetéssel olvasottként jelöli meg a bejegyzést
settings.appearance=Megjelenés
settings.scroll_speed=A görgetés sebessége, amikor a cikkek között navigál (miliszekundumban)
settings.scroll_speed.help=Írjon be 0-át a letiltáshoz
settings.theme=Téma
settings.submit_your_theme=Küldje el a témáját
settings.custom_css=Saját CSS
details.feed_details=Hírcsatorna részletei
details.url=URL
details.website=Weboldal
details.name=Név
details.category=Kategória
details.position=Pozició
details.last_refresh=Utolsó frissítés
details.message=Utolsó frissítési üzenet
details.next_refresh=Következő frissítés
details.queued_for_refresh=Frissítésre vár
details.feed_url=Hírcsatorna URL
details.generate_api_key_first=A profiljában először egy API kulcsot kell generálnia.
details.unsubscribe=Leiratkozás
details.unsubscribe_confirmation=Biztos, hogy le akar iratkozni errről a csatornáról?
details.delete_category_confirmation=Biztos, hog törölni akarja ezt a kategóriát?
details.category_details=Kategória részletei
details.tag_details=Címke részletei
details.parent_category=Szülő kategória
profile.user_name=Felhasználói név
profile.email=E-mail
profile.change_password=Jelszó megváltoztatás
profile.confirm_password=Jelszó megerősítése
profile.minimum_6_chars=Legalább 8 karakter
profile.passwords_do_not_match=A jelszavak nem egyeznek
profile.api_key=API kulcs
profile.api_key_not_generated=Még nincsen generálva
profile.generate_new_api_key=Új API kulcs generálása
profile.generate_new_api_key_info=A jelszó megváltoztatása új API kulcsot generál
profile.opml_export=OPML exportálása
profile.delete_account=Fiók törlése
profile.delete_account_confirmation=Törli a fiókját? Innen már nincs visszatérés!
about.rest_api=REST API
about.keyboard_shortcuts=Gyorsbillentyűk
about.version=CommaFeed verzió
about.line1_prefix=A CommaFeed egy nyílt forrású projekt. A forrás megtalálható a
about.line1_suffix=oldalán.
about.line2_prefix=Ha hibába ütközik, kérjük jelentse azt a
about.line2_suffix=projekt oldalán.
about.line3=Ha tetszik önnek ez a szolgáltatás, akkor kérjük támogassa a fejlesztőket és, hogy fentarthassák a weboldalt.
about.line4=Akik jobban szeretnék az oldalt bitcon-nal támogatni, itt a cím
about.goodies=Hasznos dolgok
about.goodies.android_app=Android alkalmazás
about.goodies.subscribe_url=Feliratkozás az URL-re
about.goodies.chrome_extension=Chrome bővítmény
about.goodies.firefox_extension=Firefox kiterjesztés
about.goodies.opera_extension=Opera kiterjesztés
about.goodies.subscribe_bookmarklet=Feliratkozás bookmarklet hozzáadása (klikkeléssel)
about.goodies.subscribe_bookmarklet_asc=Régebbiek először
about.goodies.subscribe_bookmarklet_desc=Újak először
about.goodies.next_unread_bookmarklet=Következő olvasatlan elem bookmarklet (húzza fel a könyvjelzősávba)
about.translation=Fordítás
about.translation.message=Segítségét kérjük a CommaFeed fordításához.
about.translation.link=Nézze meg, hogyan tud segíteni ebben.
about.announcements=Bejelentések
about.rest_api.line1=A CommaFeed a JAX-RS-re és az AngularJS-re épül. Ezért a RESTA API elérhető.
about.rest_api.link_to_documentation=Link a dokumentációhoz.
about.shortcuts.mouse_middleclick=középső egérgomb
about.shortcuts.open_next_entry=következő hír megnyitása
about.shortcuts.open_previous_entry=előző hír megnyitása
about.shortcuts.spacebar=szóköz/shift+szóköz
about.shortcuts.move_page_down_up=fel/le lépkedhet az oldalon
about.shortcuts.focus_next_entry=megnyitás nélkül fókuszál a övetkező elemre
about.shortcuts.focus_previous_entry=megnyitás nélkül fókuszál az előző elemre
about.shortcuts.open_next_feed=a következő hírcsatorna vagy kategória megnyitása
about.shortcuts.open_previous_feed=az előző hírcsatorna vagy kategória megnyitása
about.shortcuts.open_close_current_entry=a jelenlegi elem megnyitása/bezárása
about.shortcuts.open_current_entry_in_new_window=a jelenlegi elem megnyitása új ablakban
about.shortcuts.open_current_entry_in_new_window_background=a jelenlegi elem megnyitása a háttérben, új ablakban
about.shortcuts.star_unstar=hírelem csillagozása
about.shortcuts.mark_current_entry=elem megjelölése olvasottként
about.shortcuts.mark_all_as_read=az összes elem megjelölése olvasottként
about.shortcuts.open_in_new_tab_mark_as_read=elem megnyitása új fülön és megjelölése olvasottként
about.shortcuts.fullscreen=teljes képernyős mód bekapcsolása
about.shortcuts.font_size=a jelenlegi elemnél növeli/csökkenti a betűméretet
about.shortcuts.go_to_all=átkapcsol az Összes nézetre
about.shortcuts.go_to_starred=átkapcsol a Csillagozott nézetre
about.shortcuts.feed_search=név szerinti keresés a hírcsatornák között

View File

@@ -6,6 +6,7 @@ global.download=Download
global.link=Link
global.bookmark=Segnalibro
global.close=Chiudi
global.tags=Tags ####### Needs translation
tree.subscribe=Iscriviti
tree.import=Importa
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc
toolbar.titles_only=Solo titoli
toolbar.expanded_view=Espandi
toolbar.mark_all_as_read=Contrassegna tutto come già letto
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Elementi più vecchi di un giorno
toolbar.mark_all_older_week=Elementi più vecchi di una settimana
toolbar.mark_all_older_two_weeks=Elementi più vecchi di due settimane
@@ -66,6 +68,8 @@ settings.general.show_unread=Show feeds and categories with no unread entries
settings.general.social_buttons=Visualizza i social button
settings.general.scroll_marks=Marca come letto quando scorri
settings.appearance=Appearance ####### Needs translation
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Tema
settings.submit_your_theme=Sottoponi il tuo tema
settings.custom_css=Css modificato
@@ -83,7 +87,10 @@ details.queued_for_refresh=In attesa per l'aggiornamento
details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Annulla l"'"iscrizione
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Dettagli categoria
details.tag_details=Tag details ####### Needs translation
details.parent_category=Parent category
profile.user_name=User name
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Genera una nuova chiave API
profile.generate_new_api_key_info=Cambiando la password sarà generata una nuova chiave API
profile.opml_export=Esporta OPML
profile.delete_account=Elimina account
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Scorciatoie da tastiera

View File

@@ -0,0 +1,157 @@
global.save=保存
global.cancel=取り消し
global.delete=削除
global.required=Required
global.download=ダウンロード
global.link=リンク
global.bookmark=ブックマーク
global.close=閉じる
global.tags=タグ
tree.subscribe=購読
tree.import=インポート
tree.new_category=新しいカテゴリー
tree.all=全て
tree.starred=スター付
subscribe.feed_url=フィードURL
subscribe.feed_name=フィード名
subscribe.category=カテゴリー
import.google_reader_prefix=Googleアカウントからフィードを
import.google_reader_suffix=インポートします。
import.google_download=または、お持ちのsubscriptions.xmlファイルをアップロードします。
import.google_download_link=このリンクからダウンロードして下さい。
import.xml_file=OPMLファイル
new_category.name=名前
new_category.parent=親カテゴリー
toolbar.unread=未読
toolbar.all=全て
toolbar.previous_entry=前のエントリー
toolbar.next_entry=次のエントリー
toolbar.refresh=更新
toolbar.refresh_all=全てのフィードを更新
toolbar.sort_by_asc_desc=昇順/降順にソート
toolbar.titles_only=タイトルのみ
toolbar.expanded_view=拡張ビュー
toolbar.mark_all_as_read=全て既読にする
toolbar.mark_all_older_12_hours=12時間以上前のアイテム
toolbar.mark_all_older_day=前日より前のアイテム
toolbar.mark_all_older_week=1週間以上前のアイテム
toolbar.mark_all_older_two_weeks=2週間以上前のアイテム
toolbar.settings=設定
toolbar.profile=Profile
toolbar.admin=管理者
toolbar.about=About
toolbar.logout=ログアウト
toolbar.donate=寄付
view.entry_source= より
view.entry_author= 著者
view.error_while_loading_feed=フィード読み込み中にエラーが発生しました。
view.keep_unread=未読として保持
view.no_unread_items=未読アイテムはありません。
view.mark_up_to_here=ここまで既読
view.search_for=検索:
view.no_search_results=検索結果はありません。
feedsearch.hint=購読フィードを入力...
feedsearch.help=Enterキーで選択、矢印キーで移動します。
feedsearch.result_prefix=見つかった購読フィード:
settings.general=一般
settings.general.language=言語
settings.general.language.contribute=翻訳に貢献する
settings.general.show_unread=未読エントリーのないフィードとカテゴリーを表示
settings.general.social_buttons=共有ボタンを表示
settings.general.scroll_marks=拡張ビューではエントリーのスクロールで既読にする
settings.appearance=外観
settings.scroll_speed=エントリー間のスクロールスピード(ミリ秒)
settings.scroll_speed.help=0に設定すると無効になります
settings.theme=テーマ
settings.submit_your_theme=テーマを登録する
settings.custom_css=カスタムCSS
details.feed_details=フィードの詳細
details.url=URL
details.website=Webサイト
details.name=名前
details.category=カテゴリー
details.position=位置
details.last_refresh=最終更新
details.message=最終更新メッセージ
details.next_refresh=次回更新
details.queued_for_refresh=更新キュー
details.feed_url=フィードURL
details.generate_api_key_first=最初にあなたのAPIキーを生成して下さい。
details.unsubscribe=購読解除
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=カテゴリー詳細
details.tag_details=タグ詳細
details.parent_category=親カテゴリー
profile.user_name=ユーザ名
profile.email=E-mail
profile.change_password=パスワードの変更
profile.confirm_password=変更パスワードの確認
profile.minimum_6_chars=6文字以上
profile.passwords_do_not_match=パスワードが一致しません
profile.api_key=APIキー
profile.api_key_not_generated=APIキーが生成されていません
profile.generate_new_api_key=新しいAPIキーを生成
profile.generate_new_api_key_info=パスワードの変更は新しいAPIキーが生成されます
profile.opml_export=OPMLエクスポート
profile.delete_account=アカウント削除
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=キーボードショートカット
about.version=CommaFeedバージョン
about.line1_prefix=CommaFeedはオープンソースプロジェクトです。ソースは
about.line1_suffix=にホスティングされています。
about.line2_prefix=もし問題を登録したい場合、
about.line2_suffix=プロジェクトのissuesページに報告して下さい。
about.line3=このプロジェクトを気に入った場合、開発者やWebサイトの運営コストをサポートするための寄付を検討して下さいね。
about.line4=Bitcoinなら寄付できる方、アドレスはこちらです。
about.goodies=Goodies
about.goodies.android_app=Androidアプリ
about.goodies.subscribe_url=購読URL
about.goodies.chrome_extension=Chrome拡張
about.goodies.firefox_extension=Firefox拡張
about.goodies.opera_extension=Opera拡張
about.goodies.subscribe_bookmarklet=購読ブックマークレットを追加(クリック)
about.goodies.subscribe_bookmarklet_asc=古い順
about.goodies.subscribe_bookmarklet_desc=新しい順
about.goodies.next_unread_bookmarklet=次の未読アイテムブックマークレット(ブックマークバーへドラッグ)
about.translation=翻訳
about.translation.message=CommaFeedの翻訳に助けが必要です
about.translation.link=どうやって翻訳に貢献できるか見て下さい。
about.announcements=Announcements
about.rest_api.line1=CommaFeedはJAX-RSとAngularJSを使用しているので、REST APIも利用可能です。
about.rest_api.link_to_documentation=ドキュメントへのリンク
about.shortcuts.mouse_middleclick=中クリック
about.shortcuts.open_next_entry=次のエントリーを開く
about.shortcuts.open_previous_entry=前のエントリーを開く
about.shortcuts.spacebar=space/shift+space
about.shortcuts.move_page_down_up=ページ移動
about.shortcuts.focus_next_entry=次のエントリーを開かずにフォーカス移動
about.shortcuts.focus_previous_entry=前のエントリーを開かずにフォーカス移動
about.shortcuts.open_next_feed=次のフィード/カテゴリーを開く
about.shortcuts.open_previous_feed=前のフィード/カテゴリーを開く
about.shortcuts.open_close_current_entry=現在のエントリーを開く/閉じる
about.shortcuts.open_current_entry_in_new_window=現在のエントリーを新しいウィンドウで開く
about.shortcuts.open_current_entry_in_new_window_background=現在のエントリーを新しいバックグラウンドウィンドウで開く
about.shortcuts.star_unstar=現在のエントリーにスターを付ける/解除する
about.shortcuts.mark_current_entry=現在のエントリーを既読/未読にする
about.shortcuts.mark_all_as_read=全エントリーを既読にする
about.shortcuts.open_in_new_tab_mark_as_read=エントリーを既読にして新しいタブで開く
about.shortcuts.fullscreen=フルスクリーントグル
about.shortcuts.font_size=現在のエントリーのフォントサイズを大きく/小さくする
about.shortcuts.go_to_all=All viewに変更する
about.shortcuts.go_to_starred=スター付きviewに変更する
about.shortcuts.feed_search=購読画面(subscription nameの入力)に移動する

View File

@@ -6,6 +6,7 @@ global.download=Download ####### Needs translation
global.link=Link ####### Needs translation
global.bookmark=Bookmark ####### Needs translation
global.close=Close ####### Needs translation
global.tags=Tags ####### Needs translation
tree.subscribe=구독
tree.import=임포트
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc ####### Needs translation
toolbar.titles_only=Titles only ####### Needs translation
toolbar.expanded_view=Expanded view ####### Needs translation
toolbar.mark_all_as_read=읽음표시
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Items older than a day ####### Needs translation
toolbar.mark_all_older_week=Items older than a week ####### Needs translation
toolbar.mark_all_older_two_weeks=Items older than two weeks ####### Needs translation
@@ -66,6 +68,8 @@ settings.general.show_unread=안읽은 항목들이 있는 피드와 카테고
settings.general.social_buttons=소셜미디아 버튼들 보여주기
settings.general.scroll_marks=Expanded View에서 스크롤하면 항목들을 읽음으로 저장하기
settings.appearance=Appearance ####### Needs translation
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Theme ####### Needs translation
settings.submit_your_theme=Submit your theme ####### Needs translation
settings.custom_css=커스톰 CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh ####### Needs translation
details.feed_url=피드 유알엘
details.generate_api_key_first=당신의 프로필을 위해 API Key를 먼저 생성하세요.
details.unsubscribe=주소 삭제
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=카테고리 세부
details.tag_details=Tag details ####### Needs translation
details.parent_category=부모 카테고리
profile.user_name=사용자 이름
@@ -98,6 +105,7 @@ profile.generate_new_api_key=API Key 생성하기
profile.generate_new_api_key_info=비밀번호를 변경하면 새로운 API Key가 생성됩니다.
profile.opml_export=OPML export ####### Needs translation
profile.delete_account=프로필삭제
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=단축기

View File

@@ -1,4 +1,5 @@
ar=العربية
ca=Català
en=English
es=Español
de=Deutsch
@@ -7,6 +8,7 @@ fr=Français
gl=Galician
glk=گیلکی
hu=Magyar
ja=日本語
ko=한국어
nl=Nederlands
nb=Norsk (bokmål)

View File

@@ -6,6 +6,7 @@ global.download=Muat turun
global.link=Pautan
global.bookmark=Bookmark
global.close=Tutup
global.tags=Tags ####### Needs translation
tree.subscribe=Langgan
tree.import=Import
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Aturkan mengikut tarikh (baru/lama)
toolbar.titles_only=Tajuk sahaja
toolbar.expanded_view=Wide view
toolbar.mark_all_as_read=Tanda kesemuanya telah dibaca
toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_day=Lebih lama daripada sehari
toolbar.mark_all_older_week=Lebih lama daripada seminggu
toolbar.mark_all_older_two_weeks=Lebih lama daripada dua minggu
@@ -66,6 +68,8 @@ settings.general.show_unread=Tunjuk semua feed dan kategori yang telah dibaca
settings.general.social_buttons=Tunjuk social sharing
settings.general.scroll_marks=Dalam wide view, tanda item dibaca ketika scrolling
settings.appearance=Rupa
settings.scroll_speed=Scrolling speed when navigating between entries (in milliseconds) ####### Needs translation
settings.scroll_speed.help=set to 0 to disable ####### Needs translation
settings.theme=Tema
settings.submit_your_theme=Muat naik tema anda
settings.custom_css=Custom CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Diaturkan untuk refresh
details.feed_url=URL Feed
details.generate_api_key_first=Janakan API key dalam profil anda dahulu.
details.unsubscribe=Hentikan langganan
details.unsubscribe_confirmation=Are you sure you want to unsubscribe from this feed? ####### Needs translation
details.delete_category_confirmation=Are you sure you want to delete this category? ####### Needs translation
details.category_details=Butir-butir kategori
details.tag_details=Tag details ####### Needs translation
details.parent_category=Kategori induk
profile.user_name=User name
@@ -98,6 +105,7 @@ profile.generate_new_api_key=Jana API key baru
profile.generate_new_api_key_info=Pertukaran kata laluan akan menjanakan API key yang baru
profile.opml_export=Export OPML
profile.delete_account=Padam akaun
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API
about.keyboard_shortcuts=Pintasan papan kekunci

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