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 name="external_port">${env.OPENSHIFT_JBOSSEAP_CLUSTER_PROXY_PORT}
</property> </property>
<property name="bind_port">7600</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> </transport>
<protocol type="TCPPING"> <protocol type="TCPPING">
<property name="timeout">3000</property> <property name="timeout">3000</property>
@@ -476,15 +476,15 @@
<interfaces> <interfaces>
<interface name="management"> <interface name="management">
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" /> <loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
</interface> </interface>
<interface name="public"> <interface name="public">
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" /> <loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
</interface> </interface>
<interface name="unsecure"> <interface name="unsecure">
<!-- Used for IIOP sockets in the standarad configuration. To secure JacORB <!-- Used for IIOP sockets in the standarad configuration. To secure JacORB
you need to setup SSL --> you need to setup SSL -->
<loopback-address value="${env.OPENSHIFT_INTERNAL_IP}" /> <loopback-address value="${env.OPENSHIFT_JBOSSEAP_IP}" />
</interface> </interface>
</interfaces> </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 update
sudo apt-get install openjdk-7-jdk maven3 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 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. 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 Now build the application
Embedded HSQL database: # Embedded HSQL database:
mvn clean package tomee:build -Pprod mvn clean package tomee:build -Pprod
External MySQL database: # External MySQL database:
mvn clean package tomee:build -Pprod -Pmysql mvn clean package tomee:build -Pprod -Pmysql
External PostgreSQL database: # External PostgreSQL database:
mvn clean package tomee:build -Pprod -Ppgsql mvn clean package tomee:build -Pprod -Ppgsql
External Microsoft SQL Server database: # External Microsoft SQL Server database:
mvn clean package tomee:build -Pprod -Pmssql 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. 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 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. * 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. * 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>`). * 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. 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 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`. * 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 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> <groupId>com.commafeed</groupId>
<artifactId>commafeed</artifactId> <artifactId>commafeed</artifactId>
<version>1.2.0</version> <version>1.5.0-SNAPSHOT</version>
<packaging>war</packaging> <packaging>war</packaging>
<name>CommaFeed</name> <name>CommaFeed</name>
@@ -79,7 +79,7 @@
<artifactId>tomee-maven-plugin</artifactId> <artifactId>tomee-maven-plugin</artifactId>
<version>1.5.2</version> <version>1.5.2</version>
<configuration> <configuration>
<tomeeVersion>1.5.2</tomeeVersion> <tomeeVersion>1.6.0</tomeeVersion>
<tomeeClassifier>plus</tomeeClassifier> <tomeeClassifier>plus</tomeeClassifier>
<tomeeHttpPort>8082</tomeeHttpPort> <tomeeHttpPort>8082</tomeeHttpPort>
<args>-Xmx1024m -XX:MaxPermSize=512m -XX:+CMSClassUnloadingEnabled</args> <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.common:hibernate-commons-annotations:4.0.1.Final</lib>
<lib>org.hibernate:hibernate-validator:4.3.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.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>dom4j:dom4j:1.6.1</lib>
<lib>antlr:antlr:2.7.7</lib> <lib>antlr:antlr:2.7.7</lib>
<lib>remove:openjpa-</lib> <lib>remove:openjpa-</lib>
<lib>remove:hsqldb</lib> <lib>remove:hsqldb</lib>
<lib>org.hsqldb:hsqldb:2.3.0</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>postgresql:postgresql:9.1-901.jdbc4</lib>
<lib>net.sourceforge.jtds:jtds:1.3.1</lib> <lib>net.sourceforge.jtds:jtds:1.3.1</lib>
@@ -169,7 +174,7 @@
<plugin> <plugin>
<groupId>pl.project13.maven</groupId> <groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId> <artifactId>git-commit-id-plugin</artifactId>
<version>2.1.5</version> <version>2.1.7</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>
@@ -189,7 +194,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>0.12.0</version> <version>1.12.6</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -214,28 +219,28 @@
<dependency> <dependency>
<groupId>redis.clients</groupId> <groupId>redis.clients</groupId>
<artifactId>jedis</artifactId> <artifactId>jedis</artifactId>
<version>2.1.0</version> <version>2.2.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.liquibase</groupId> <groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId> <artifactId>liquibase-core</artifactId>
<version>3.0.2</version> <version>3.1.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>14.0.1</version> <version>16.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-beanutils</groupId> <groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId> <artifactId>commons-beanutils</artifactId>
<version>1.8.3</version> <version>1.9.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
<version>1.8</version> <version>1.9</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-collections</groupId> <groupId>commons-collections</groupId>
@@ -260,7 +265,7 @@
<dependency> <dependency>
<groupId>commons-fileupload</groupId> <groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId> <artifactId>commons-fileupload</artifactId>
<version>1.3</version> <version>1.3.1</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -298,34 +303,34 @@
<dependency> <dependency>
<groupId>com.google.gwt</groupId> <groupId>com.google.gwt</groupId>
<artifactId>gwt-servlet</artifactId> <artifactId>gwt-servlet</artifactId>
<version>2.5.1</version> <version>2.6.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.sourceforge.cssparser</groupId> <groupId>net.sourceforge.cssparser</groupId>
<artifactId>cssparser</artifactId> <artifactId>cssparser</artifactId>
<version>0.9.9</version> <version>0.9.13</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>
<version>4.2.5</version> <version>4.3.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jsoup</groupId> <groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId> <artifactId>jsoup</artifactId>
<version>1.7.2</version> <version>1.7.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>2.2.2</version> <version>2.3.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId> <artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version> <version>1.7.7</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>log4j</groupId> <groupId>log4j</groupId>
@@ -336,28 +341,28 @@
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-core</artifactId> <artifactId>wicket-core</artifactId>
<version>6.9.1</version> <version>6.14.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-auth-roles</artifactId> <artifactId>wicket-auth-roles</artifactId>
<version>6.9.1</version> <version>6.14.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-extensions</artifactId> <artifactId>wicket-extensions</artifactId>
<version>6.9.1</version> <version>6.14.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.wicket</groupId> <groupId>org.apache.wicket</groupId>
<artifactId>wicket-cdi</artifactId> <artifactId>wicket-cdi</artifactId>
<version>6.9.1</version> <version>6.14.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ro.isdc.wro4j</groupId> <groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-extensions</artifactId> <artifactId>wro4j-extensions</artifactId>
<version>1.6.3</version> <version>1.7.5</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -372,6 +377,88 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </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> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
@@ -391,7 +478,7 @@
<plugin> <plugin>
<groupId>ro.isdc.wro4j</groupId> <groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-maven-plugin</artifactId> <artifactId>wro4j-maven-plugin</artifactId>
<version>1.6.3</version> <version>1.7.5</version>
<executions> <executions>
<execution> <execution>
<id>js</id> <id>js</id>
@@ -401,7 +488,7 @@
</goals> </goals>
<configuration> <configuration>
<targetGroups>app</targetGroups> <targetGroups>app</targetGroups>
<options>indent,devel,noarg,quotmark,laxcomma,laxbreak</options> <options>devel,noarg,quotmark,laxcomma,laxbreak</options>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -538,7 +625,7 @@
<plugin> <plugin>
<groupId>ro.isdc.wro4j</groupId> <groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-maven-plugin</artifactId> <artifactId>wro4j-maven-plugin</artifactId>
<version>1.6.3</version> <version>1.7.5</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>

View File

@@ -4,42 +4,45 @@ import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; 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.KeyManager;
import javax.net.ssl.SSLContext; 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.TrustManager;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException; 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.methods.HttpGet;
import org.apache.http.client.params.CookiePolicy; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.HttpClientParams; import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ClientConnectionManager; import org.apache.http.config.ConnectionConfig;
import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.DecompressingHttpClient; import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.HttpContext;
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.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.apache.wicket.util.io.IOUtils;
/** /**
* Smart HTTP getter: handles gzip, ssl, last modified and etag headers * 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 ACCEPT_LANGUAGE = "en";
private static final String PRAGMA_NO_CACHE = "No-cache"; private static final String PRAGMA_NO_CACHE = "No-cache";
private static final String CACHE_CONTROL_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; private static SSLContext SSL_CONTEXT = null;
static { 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 { public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
return getBinary(url, null, null, timeout); return getBinary(url, null, null, timeout);
} }
@@ -90,9 +115,12 @@ public class HttpGetter {
HttpResult result = null; HttpResult result = null;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
HttpClient client = newClient(timeout); CloseableHttpClient client = newClient(timeout);
CloseableHttpResponse response = null;
try { try {
HttpGet httpget = new HttpGet(url); HttpGet httpget = new HttpGet(url);
HttpClientContext context = HttpClientContext.create();
httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE); httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE);
httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE); httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE);
httpget.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_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); httpget.addHeader(HttpHeaders.IF_NONE_MATCH, eTag);
} }
HttpResponse response = null;
try { try {
response = client.execute(httpget); response = client.execute(httpget, context);
int code = response.getStatusLine().getStatusCode(); int code = response.getStatusLine().getStatusCode();
if (code == HttpStatus.SC_NOT_MODIFIED) { if (code == HttpStatus.SC_NOT_MODIFIED) {
throw new NotModifiedException("'304 - not modified' http code received"); throw new NotModifiedException("'304 - not modified' http code received");
@@ -123,15 +150,14 @@ public class HttpGetter {
} }
} }
Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED); Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG); String lastModifiedHeaderValue = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
if (lastModifiedHeaderValue != null && StringUtils.equals(lastModified, lastModifiedHeaderValue)) {
String lastModifiedResponse = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
if (lastModified != null && StringUtils.equals(lastModified, lastModifiedResponse)) {
throw new NotModifiedException("lastModifiedHeader is the same"); throw new NotModifiedException("lastModifiedHeader is the same");
} }
String eTagResponse = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue()); Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
if (eTag != null && StringUtils.equals(eTag, eTagResponse)) { String eTagHeaderValue = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
if (eTag != null && StringUtils.equals(eTag, eTagHeaderValue)) {
throw new NotModifiedException("eTagHeader is the same"); throw new NotModifiedException("eTagHeader is the same");
} }
@@ -144,12 +170,15 @@ public class HttpGetter {
contentType = entity.getContentType().getValue(); 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; long duration = System.currentTimeMillis() - start;
result = new HttpResult(content, contentType, lastModifiedHeader == null ? null : lastModifiedHeader.getValue(), result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect);
eTagHeader == null ? null : eTagHeader.getValue(), duration);
} finally { } finally {
client.getConnectionManager().shutdown(); IOUtils.closeQuietly(response);
IOUtils.closeQuietly(client);
} }
return result; return result;
} }
@@ -161,13 +190,15 @@ public class HttpGetter {
private String lastModifiedSince; private String lastModifiedSince;
private String eTag; private String eTag;
private long duration; 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.content = content;
this.contentType = contentType; this.contentType = contentType;
this.lastModifiedSince = lastModifiedSince; this.lastModifiedSince = lastModifiedSince;
this.eTag = eTag; this.eTag = eTag;
this.duration = duration; this.duration = duration;
this.urlAfterRedirect = urlAfterRedirect;
} }
public byte[] getContent() { public byte[] getContent() {
@@ -190,23 +221,30 @@ public class HttpGetter {
return duration; return duration;
} }
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
} }
public static HttpClient newClient(int timeout) { public static CloseableHttpClient newClient(int timeout) {
DefaultHttpClient client = new SystemDefaultHttpClient(); HttpClientBuilder builder = HttpClients.custom();
builder.useSystemProperties();
builder.addInterceptorFirst(REMOVE_INCORRECT_CONTENT_ENCODING);
builder.disableAutomaticRetries();
SSLSocketFactory ssf = new SSLSocketFactory(SSL_CONTEXT, VERIFIER); builder.setSslcontext(SSL_CONTEXT);
ClientConnectionManager ccm = client.getConnectionManager(); builder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme(HTTPS, 443, ssf));
HttpParams params = client.getParams(); RequestConfig.Builder configBuilder = RequestConfig.custom();
HttpClientParams.setCookiePolicy(params, CookiePolicy.IGNORE_COOKIES); configBuilder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);
HttpProtocolParams.setContentCharset(params, UTF8); configBuilder.setSocketTimeout(timeout);
HttpConnectionParams.setConnectionTimeout(params, timeout); configBuilder.setConnectTimeout(timeout);
HttpConnectionParams.setSoTimeout(params, timeout); configBuilder.setConnectionRequestTimeout(timeout);
client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false)); builder.setDefaultRequestConfig(configBuilder.build());
return new DecompressingHttpClient(client);
builder.setDefaultConnectionConfig(ConnectionConfig.custom().setCharset(Consts.ISO_8859_1).build());
return builder.build();
} }
public static class NotModifiedException extends Exception { public static class NotModifiedException extends Exception {
@@ -232,24 +270,4 @@ public class HttpGetter {
return null; 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.Schedule;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.DatabaseCleaningService;
/** /**
* Contains all scheduled tasks * Contains all scheduled tasks
* *
*/ */
@Stateless @Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ScheduledTasks { public class ScheduledTasks {
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject @Inject
DatabaseCleaner cleaner; DatabaseCleaningService cleaner;
@PersistenceContext
EntityManager em;
/** /**
* 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() { private void cleanupOldStatuses() {
Date threshold = applicationSettingsService.getUnreadThreshold(); Date threshold = applicationSettingsService.getUnreadThreshold();
if (threshold != null) { if (threshold != null) {
cleaner.cleanStatusesOlderThan(threshold); 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.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative; import javax.enterprise.inject.Alternative;
import org.apache.commons.pool.impl.GenericObjectPool;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis; import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPool;
@@ -30,7 +33,14 @@ public class RedisCacheService extends CacheService {
private static ObjectMapper mapper = new ObjectMapper(); 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 @Override
public List<String> getLastEntries(Feed feed) { public List<String> getLastEntries(Feed feed) {

View File

@@ -6,15 +6,12 @@ import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join; import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType; import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin; import javax.persistence.criteria.SetJoin;
import javax.persistence.criteria.Subquery; import javax.persistence.criteria.Subquery;
import javax.persistence.metamodel.SingularAttribute;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils; 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.Feed_;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
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.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@@ -95,8 +91,8 @@ public class FeedDAO extends GenericDAO<Feed> {
public List<Feed> findByTopic(String topic) { public List<Feed> findByTopic(String topic) {
return findByField(Feed_.pushTopicHash, DigestUtils.sha1Hex(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()); CriteriaQuery<Feed> query = builder.createQuery(getType());
Root<Feed> root = query.from(getType()); Root<Feed> root = query.from(getType());
@@ -105,54 +101,6 @@ public class FeedDAO extends GenericDAO<Feed> {
TypedQuery<Feed> q = em.createQuery(query); TypedQuery<Feed> q = em.createQuery(query);
q.setMaxResults(max); q.setMaxResults(max);
List<Feed> list = q.getResultList(); return 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;
} }
} }

View File

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

View File

@@ -6,13 +6,19 @@ import java.util.List;
import javax.ejb.Stateless; import javax.ejb.Stateless;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import org.apache.commons.codec.digest.DigestUtils; 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.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.commafeed.backend.model.Feed_;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@@ -36,6 +42,34 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
return Iterables.getFirst(list, null); 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) { public int delete(Date olderThan, int max) {
CriteriaQuery<FeedEntry> query = builder.createQuery(getType()); CriteriaQuery<FeedEntry> query = builder.createQuery(getType());
Root<FeedEntry> root = query.from(getType()); Root<FeedEntry> root = query.from(getType());

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,9 @@ import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; 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 * Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
* {@link Task} instead of {@link Runnable} * {@link Task} instead of {@link Runnable}
@@ -19,7 +22,7 @@ public class FeedRefreshExecutor {
private ThreadPoolExecutor pool; private ThreadPoolExecutor pool;
private LinkedBlockingDeque<Runnable> queue; 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); log.info("Creating pool {} with {} threads", poolName, threads);
this.poolName = poolName; this.poolName = poolName;
pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) { 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) { public void execute(Task task) {
pool.execute(task); pool.execute(task);
} }
public int getQueueSize() {
return queue.size();
}
public int getActiveCount() {
return pool.getActiveCount();
}
public static interface Task extends Runnable { public static interface Task extends Runnable {
boolean isUrgent(); boolean isUrgent();
} }

View File

@@ -17,7 +17,9 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.time.DateUtils; 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.dao.FeedDAO;
import com.commafeed.backend.model.Feed; import com.commafeed.backend.model.Feed;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
@@ -41,7 +43,7 @@ public class FeedRefreshTaskGiver {
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject @Inject
MetricsBean metricsBean; MetricRegistry metrics;
@Inject @Inject
FeedRefreshWorker worker; FeedRefreshWorker worker;
@@ -54,10 +56,35 @@ public class FeedRefreshTaskGiver {
private ExecutorService executor; private ExecutorService executor;
private Meter feedRefreshed;
private Meter threadWaited;
private Meter refill;
@PostConstruct @PostConstruct
public void init() { public void init() {
backgroundThreads = applicationSettingsService.get().getBackgroundThreads(); backgroundThreads = applicationSettingsService.get().getBackgroundThreads();
executor = Executors.newFixedThreadPool(1); 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 @PreDestroy
@@ -73,26 +100,25 @@ public class FeedRefreshTaskGiver {
} }
public void start() { 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"); log.info("starting feed refresh task giver");
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { 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()) { while (!executor.isShutdown()) {
try { try {
FeedRefreshContext context = take(); FeedRefreshContext context = take();
if (context != null) { if (context != null) {
metricsBean.feedRefreshed(); feedRefreshed.mark();
worker.updateFeed(context); worker.updateFeed(context);
} else { } else {
log.debug("nothing to do, sleeping for 15s"); log.debug("nothing to do, sleeping for 15s");
metricsBean.threadWaited(); threadWaited.mark();
try { try {
Thread.sleep(15000); Thread.sleep(15000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -138,22 +164,26 @@ public class FeedRefreshTaskGiver {
* refills the refresh queue and empties the giveBack queue while at it * refills the refresh queue and empties the giveBack queue while at it
*/ */
private void refill() { 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(); List<FeedRefreshContext> contexts = Lists.newArrayList();
if (!applicationSettingsService.get().isCrawlingPaused()) { int batchSize = Math.min(100, 3 * backgroundThreads);
List<Feed> feeds = feedDAO.findNextUpdatable(count, getLastLoginThreshold());
for (Feed feed : feeds) { // add feeds we got from the add() method
contexts.add(new FeedRefreshContext(feed, false)); 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 // add feeds that are up to refresh from the database
// higher priority if (!applicationSettingsService.get().isCrawlingPaused()) {
int size = addQueue.size(); int count = batchSize - contexts.size();
for (int i = 0; i < size; i++) { if (count > 0) {
contexts.add(0, addQueue.poll()); 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 // 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()); takeQueue.addAll(map.values());
// add feeds from the giveBack queue to the map, overriding duplicates // add feeds from the giveBack queue to the map, overriding duplicates
size = giveBackQueue.size(); int giveBackQueueSize = giveBackQueue.size();
for (int i = 0; i < size; i++) { for (int i = 0; i < giveBackQueueSize; i++) {
Feed feed = giveBackQueue.poll(); Feed feed = giveBackQueue.poll();
map.put(feed.getId(), new FeedRefreshContext(feed, false)); 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.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils; 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.cache.CacheService;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO; import com.commafeed.backend.dao.FeedEntryDAO;
@@ -57,7 +58,7 @@ public class FeedRefreshUpdater {
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject @Inject
MetricsBean metricsBean; MetricRegistry metrics;
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@@ -71,12 +72,22 @@ public class FeedRefreshUpdater {
private FeedRefreshExecutor pool; private FeedRefreshExecutor pool;
private Striped<Lock> locks; private Striped<Lock> locks;
private Meter entryCacheMiss;
private Meter entryCacheHit;
private Meter feedUpdated;
private Meter entryInserted;
@PostConstruct @PostConstruct
public void init() { public void init() {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1); 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); 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 @PreDestroy
@@ -116,10 +127,10 @@ public class FeedRefreshUpdater {
subscriptions = feedSubscriptionDAO.findByFeed(feed); subscriptions = feedSubscriptionDAO.findByFeed(feed);
} }
ok &= addEntry(feed, entry, subscriptions); ok &= addEntry(feed, entry, subscriptions);
metricsBean.entryCacheMiss(); entryCacheMiss.mark();
} else { } else {
log.debug("cache hit for {}", entry.getUrl()); log.debug("cache hit for {}", entry.getUrl());
metricsBean.entryCacheHit(); entryCacheHit.mark();
} }
currentEntries.add(cacheKey); currentEntries.add(cacheKey);
@@ -147,7 +158,7 @@ public class FeedRefreshUpdater {
// requeue asap // requeue asap
feed.setDisabledUntil(new Date(0)); feed.setDisabledUntil(new Date(0));
} }
metricsBean.feedUpdated(); feedUpdated.mark();
taskGiver.giveBack(feed); taskGiver.giveBack(feed);
} }
@@ -180,7 +191,7 @@ public class FeedRefreshUpdater {
if (locked1 && locked2) { if (locked1 && locked2) {
boolean inserted = feedUpdateService.addEntry(feed, entry); boolean inserted = feedUpdateService.addEntry(feed, entry);
if (inserted) { if (inserted) {
metricsBean.entryInserted(); entryInserted.mark();
} }
success = true; success = true;
} else { } 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.codec.digest.DigestUtils;
import org.apache.commons.lang.time.DateUtils; 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.HttpGetter.NotModifiedException;
import com.commafeed.backend.feeds.FeedRefreshExecutor.Task; import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
@@ -37,6 +40,9 @@ public class FeedRefreshWorker {
@Inject @Inject
FeedRefreshTaskGiver taskGiver; FeedRefreshTaskGiver taskGiver;
@Inject
MetricRegistry metrics;
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@@ -46,7 +52,7 @@ public class FeedRefreshWorker {
private void init() { private void init() {
ApplicationSettings settings = applicationSettingsService.get(); ApplicationSettings settings = applicationSettingsService.get();
int threads = settings.getBackgroundThreads(); 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 @PreDestroy
@@ -58,14 +64,6 @@ public class FeedRefreshWorker {
pool.execute(new FeedTask(context)); pool.execute(new FeedTask(context));
} }
public int getQueueSize() {
return pool.getQueueSize();
}
public int getActiveCount() {
return pool.getActiveCount();
}
private class FeedTask implements Task { private class FeedTask implements Task {
private FeedRefreshContext context; private FeedRefreshContext context;
@@ -90,17 +88,21 @@ public class FeedRefreshWorker {
int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes(); int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes();
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval); Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
try { 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()); feed.getLastPublishedDate(), feed.getLastContentHash());
// stops here if NotModifiedException or any other exception is // stops here if NotModifiedException or any other exception is thrown
// thrown
List<FeedEntry> entries = fetchedFeed.getEntries(); List<FeedEntry> entries = fetchedFeed.getEntries();
if (applicationSettingsService.get().isHeavyLoad()) { if (applicationSettingsService.get().isHeavyLoad()) {
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed() disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
.getAverageEntryInterval(), disabledUntil); .getAverageEntryInterval(), disabledUntil);
} }
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
if (StringUtils.equals(url, urlAfterRedirect)) {
urlAfterRedirect = null;
}
feed.setUrlAfterRedirect(urlAfterRedirect);
feed.setLink(fetchedFeed.getFeed().getLink()); feed.setLink(fetchedFeed.getFeed().getLink());
feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader()); feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader()); feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());

View File

@@ -1,6 +1,8 @@
package com.commafeed.backend.feeds; package com.commafeed.backend.feeds;
import java.io.StringReader; import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@@ -15,6 +17,7 @@ import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.math.stat.descriptive.SummaryStatistics; import org.apache.commons.math.stat.descriptive.SummaryStatistics;
import org.apache.wicket.request.UrlUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Document.OutputSettings; 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 List<String> ALLOWED_IMG_CSS_RULES = Arrays.asList("display", "width", "height");
private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' }; private static final char[] FORBIDDEN_CSS_RULE_CHARACTERS = new char[] { '(', ')' };
private static final Whitelist WHITELIST = buildWhiteList();
public static String truncate(String string, int length) { public static String truncate(String string, int length) {
if (string != null) { if (string != null) {
string = string.substring(0, Math.min(length, string.length())); string = string.substring(0, Math.min(length, string.length()));
@@ -57,6 +62,39 @@ public class FeedUtils {
return string; 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 * Detect feed encoding by using the declared encoding in the xml processing instruction and by detecting the characters used in the
* feed * feed
@@ -91,6 +129,14 @@ public class FeedUtils {
} }
return encoding; 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 * 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) { public static String handleContent(String content, String baseUri, boolean keepTextOnly) {
if (StringUtils.isNotBlank(content)) { if (StringUtils.isNotBlank(content)) {
baseUri = StringUtils.trimToEmpty(baseUri); 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); Document dirty = Jsoup.parseBodyFragment(content, baseUri);
Cleaner cleaner = new Cleaner(whitelist); Cleaner cleaner = new Cleaner(WHITELIST);
Document clean = cleaner.clean(dirty); Document clean = cleaner.clean(dirty);
for (Element e : clean.select("iframe[style]")) { for (Element e : clean.select("iframe[style]")) {
@@ -389,17 +406,37 @@ public class FeedUtils {
return url; 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)); url = StringUtils.trimToNull(StringUtils.normalizeSpace(url));
if (baseUrl == null || url == null || url.startsWith("http")) { if (url == null || url.startsWith("http")) {
return url; return url;
} }
if (url.startsWith("/") == false) { String baseUrl = (feedLink == null || UrlUtils.isRelative(feedLink)) ? feedUrl : feedLink;
url = "/" + url;
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) { public static String getFaviconUrl(FeedSubscription subscription, String publicUrl) {

View File

@@ -12,6 +12,7 @@ public class FetchedFeed {
private List<FeedEntry> entries = Lists.newArrayList(); private List<FeedEntry> entries = Lists.newArrayList();
private String title; private String title;
private String urlAfterRedirect;
private long fetchDuration; private long fetchDuration;
public Feed getFeed() { public Feed getFeed() {
@@ -45,4 +46,13 @@ public class FetchedFeed {
public void setFetchDuration(long fetchDuration) { public void setFetchDuration(long fetchDuration) {
this.fetchDuration = 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) @Column(length = 2048, nullable = false)
private String url; 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) @Column(length = 2048, nullable = false)
private String normalizedUrl; private String normalizedUrl;
@@ -130,11 +136,4 @@ public class Feed extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
private Date pushLastPing; 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) @OneToMany(mappedBy = "entry", cascade = CascadeType.REMOVE)
private Set<FeedEntryStatus> statuses; 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; package com.commafeed.backend.model;
import java.util.Date; import java.util.Date;
import java.util.List;
import javax.persistence.Cacheable; import javax.persistence.Cacheable;
import javax.persistence.Column; import javax.persistence.Column;
@@ -8,6 +9,8 @@ import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
@@ -19,6 +22,8 @@ import lombok.Setter;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
import com.google.common.collect.Lists;
@Entity @Entity
@Table(name = "FEEDENTRYSTATUSES") @Table(name = "FEEDENTRYSTATUSES")
@SuppressWarnings("serial") @SuppressWarnings("serial")
@@ -26,6 +31,7 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Getter @Getter
@Setter @Setter
@NamedQueries(@NamedQuery(name="Statuses.deleteOld", query="delete from FeedEntryStatus s where s.entryInserted < :date and s.starred = false"))
public class FeedEntryStatus extends AbstractModel { public class FeedEntryStatus extends AbstractModel {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@@ -42,6 +48,9 @@ public class FeedEntryStatus extends AbstractModel {
@Transient @Transient
private boolean markable; private boolean markable;
@Transient
private List<FeedEntryTag> tags = Lists.newArrayList();
/** /**
* Denormalization starts here * 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) @Column(length = 32, nullable = false, unique = true)
private String name; private String name;
@Column(length = 255, unique = true) @Column(length = 255, unique = true)
private String email; private String email;
@@ -66,4 +66,8 @@ public class User extends AbstractModel {
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private Set<FeedSubscription> subscriptions; 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 showRead;
private boolean scrollMarks; private boolean scrollMarks;
private boolean socialButtons;
@Column(length = 32) @Column(length = 32)
private String theme; private String theme;
@@ -68,4 +67,18 @@ public class UserSettings extends AbstractModel {
@Column(length = Integer.MAX_VALUE) @Column(length = Integer.MAX_VALUE)
private String customCss; 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.Date;
import java.util.List; import java.util.List;
@@ -34,9 +34,14 @@ public class OPMLExporter {
List<FeedCategory> categories = feedCategoryDAO.findAll(user); List<FeedCategory> categories = feedCategoryDAO.findAll(user);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subscriptions = feedSubscriptionDAO.findAll(user);
// export root categories
for (FeedCategory cat : 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) { for (FeedSubscription sub : subscriptions) {
if (sub.getCategory() == null) { if (sub.getCategory() == null) {
opml.getOutlines().add(buildSubscriptionOutline(sub)); 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.io.StringReader;
import java.util.List; import java.util.List;
@@ -11,10 +11,12 @@ import javax.inject.Inject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.services.FeedSubscriptionService; import com.commafeed.backend.services.FeedSubscriptionService;
@@ -56,8 +58,8 @@ public class OPMLImporter {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void handleOutline(User user, Outline outline, FeedCategory parent) { private void handleOutline(User user, Outline outline, FeedCategory parent) {
List<Outline> children = outline.getChildren();
if (StringUtils.isEmpty(outline.getType())) { if (CollectionUtils.isNotEmpty(children)) {
String name = FeedUtils.truncate(outline.getText(), 128); String name = FeedUtils.truncate(outline.getText(), 128);
if (name == null) { if (name == null) {
name = FeedUtils.truncate(outline.getTitle(), 128); name = FeedUtils.truncate(outline.getTitle(), 128);
@@ -75,7 +77,6 @@ public class OPMLImporter {
feedCategoryDAO.saveOrUpdate(category); feedCategoryDAO.saveOrUpdate(category);
} }
List<Outline> children = outline.getChildren();
for (Outline child : children) { for (Outline child : children) {
handleOutline(user, child, category); handleOutline(user, child, category);
} }
@@ -87,7 +88,7 @@ public class OPMLImporter {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
name = "Unnamed subscription"; 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 { try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent); feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
} catch (FeedSubscriptionException e) { } catch (FeedSubscriptionException e) {

View File

@@ -9,13 +9,14 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity; 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.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
import org.apache.wicket.util.io.IOUtils;
import com.commafeed.backend.HttpGetter; import com.commafeed.backend.HttpGetter;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver; import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
@@ -67,10 +68,11 @@ public class SubscriptionHandler {
post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed"); post.setHeader(HttpHeaders.USER_AGENT, "CommaFeed");
post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED); post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
HttpClient client = HttpGetter.newClient(20000); CloseableHttpClient client = HttpGetter.newClient(20000);
CloseableHttpResponse response = null;
try { try {
post.setEntity(new UrlEncodedFormEntity(nvp)); post.setEntity(new UrlEncodedFormEntity(nvp));
HttpResponse response = client.execute(post); response = client.execute(post);
int code = response.getStatusLine().getStatusCode(); int code = response.getStatusLine().getStatusCode();
if (code != 204 && code != 202 && code != 200) { if (code != 204 && code != 202 && code != 200) {
@@ -90,7 +92,8 @@ public class SubscriptionHandler {
} catch (Exception e) { } catch (Exception e) {
log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic); log.error("Could not subscribe to {} for {} : " + e.getMessage(), hub, topic);
} finally { } 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.Date;
import java.util.Enumeration; import java.util.Enumeration;
import javax.ejb.Singleton; import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.lang.time.DateUtils; 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.commafeed.backend.model.ApplicationSettings;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@Singleton @ApplicationScoped
public class ApplicationSettingsService { public class ApplicationSettingsService {
@Inject @Inject
@@ -23,19 +24,21 @@ public class ApplicationSettingsService {
private ApplicationSettings settings; private ApplicationSettings settings;
public void save(ApplicationSettings settings) { @PostConstruct
this.settings = settings; private void init() {
applicationSettingsDAO.saveOrUpdate(settings); settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
applyLogLevel();
} }
public ApplicationSettings get() { public ApplicationSettings get() {
if (settings == null) {
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
}
return settings; return settings;
} }
public void save(ApplicationSettings settings) {
applicationSettingsDAO.saveOrUpdate(settings);
this.settings = settings;
applyLogLevel();
}
public Date getUnreadThreshold() { public Date getUnreadThreshold() {
int keepStatusDays = get().getKeepStatusDays(); int keepStatusDays = get().getKeepStatusDays();
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null; 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.Calendar;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; 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.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.Feed; 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.model.FeedSubscription;
import com.commafeed.backend.services.ApplicationSettingsService;
/** /**
* Contains utility methods for cleaning the database * Contains utility methods for cleaning the database
* *
*/ */
@Slf4j @Slf4j
public class DatabaseCleaner { public class DatabaseCleaningService {
private static final int BATCH_SIZE = 100;
@Inject @Inject
FeedDAO feedDAO; FeedDAO feedDAO;
@@ -42,13 +46,28 @@ public class DatabaseCleaner {
@Inject @Inject
ApplicationSettingsService applicationSettingsService; 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() { public long cleanFeedsWithoutSubscriptions() {
log.info("cleaning feeds without subscriptions");
long total = 0; long total = 0;
int deleted = -1; int deleted = 0;
do { do {
deleted = feedDAO.deleteWithoutSubscriptions(10); List<Feed> feeds = feedDAO.findWithoutSubscriptions(BATCH_SIZE);
deleted = feedDAO.delete(feeds);
total += deleted; total += deleted;
log.info("removed {} feeds without subscriptions", total); log.info("removed {} feeds without subscriptions", total);
} while (deleted != 0); } while (deleted != 0);
@@ -57,15 +76,15 @@ public class DatabaseCleaner {
} }
public long cleanContentsWithoutEntries() { public long cleanContentsWithoutEntries() {
log.info("cleaning contents without entries");
long total = 0; long total = 0;
int deleted = -1; int deleted = 0;
do { do {
deleted = feedEntryContentDAO.deleteWithoutEntries(10); deleted = feedEntryContentDAO.deleteWithoutEntries(BATCH_SIZE);
total += deleted; total += deleted;
log.info("removed {} feeds without subscriptions", total); log.info("removed {} contents without entries", total);
} while (deleted != 0); } while (deleted != 0);
log.info("cleanup done: {} feeds without subscriptions deleted", total); log.info("cleanup done: {} contents without entries deleted", total);
return total; return total;
} }
@@ -74,9 +93,9 @@ public class DatabaseCleaner {
cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value)); cal.add(Calendar.MINUTE, -1 * (int) unit.toMinutes(value));
long total = 0; long total = 0;
int deleted = -1; int deleted = 0;
do { do {
deleted = feedEntryDAO.delete(cal.getTime(), 100); deleted = feedEntryDAO.delete(cal.getTime(), BATCH_SIZE);
total += deleted; total += deleted;
log.info("removed {} entries", total); log.info("removed {} entries", total);
} while (deleted != 0); } while (deleted != 0);
@@ -99,9 +118,19 @@ public class DatabaseCleaner {
feedDAO.saveOrUpdate(into); feedDAO.saveOrUpdate(into);
} }
public void cleanStatusesOlderThan(Date olderThan) { public long cleanStatusesOlderThan(Date olderThan) {
log.info("cleaning old read statuses"); log.info("cleaning old read statuses");
int deleted = feedEntryStatusDAO.deleteOldStatuses(olderThan); long total = 0;
log.info("cleaned {} read statuses", deleted); 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; return;
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry); FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
if (status.isMarkable()) { if (status.isMarkable()) {
status.setRead(read); status.setRead(read);
feedEntryStatusDAO.saveOrUpdate(status); feedEntryStatusDAO.saveOrUpdate(status);
@@ -64,14 +64,14 @@ public class FeedEntryService {
return; return;
} }
FeedEntryStatus status = feedEntryStatusDAO.getStatus(sub, entry); FeedEntryStatus status = feedEntryStatusDAO.getStatus(user, sub, entry);
status.setStarred(starred); status.setStarred(starred);
feedEntryStatusDAO.saveOrUpdate(status); feedEntryStatusDAO.saveOrUpdate(status);
} }
public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) { public void markSubscriptionEntries(User user, List<FeedSubscription> subscriptions, Date olderThan) {
List<FeedEntryStatus> statuses = feedEntryStatusDAO List<FeedEntryStatus> statuses = feedEntryStatusDAO.findBySubscriptions(user, subscriptions, true, null, null, -1, -1, null, false,
.findBySubscriptions(subscriptions, true, null, null, -1, -1, null, false, false); false, null);
markList(statuses, olderThan); markList(statuses, olderThan);
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0])); cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
cache.invalidateUserRootCategory(user); 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); UnreadCount count = cache.getUnreadCount(sub);
if (count == null) { if (count == null) {
log.debug("unread count cache miss for {}", Models.getId(sub)); log.debug("unread count cache miss for {}", Models.getId(sub));
count = feedEntryStatusDAO.getUnreadCount(sub); count = feedEntryStatusDAO.getUnreadCount(user, sub);
cache.setUnreadCount(sub, count); cache.setUnreadCount(sub, count);
} }
return count; return count;
@@ -109,7 +117,7 @@ public class FeedSubscriptionService {
Map<Long, UnreadCount> map = Maps.newHashMap(); Map<Long, UnreadCount> map = Maps.newHashMap();
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
for (FeedSubscription sub : subs) { for (FeedSubscription sub : subs) {
map.put(sub.getId(), getUnreadCount(sub)); map.put(sub.getId(), getUnreadCount(user, sub));
} }
return map; return map;
} }

View File

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

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend; package com.commafeed.backend.startup;
import java.sql.Connection; 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.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;

View File

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

View File

@@ -41,4 +41,7 @@ public class Entries implements Serializable {
@ApiProperty("list of entries") @ApiProperty("list of entries")
private List<Entry> entries = Lists.newArrayList(); 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.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List;
import lombok.Data; import lombok.Data;
@@ -10,7 +11,9 @@ import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.model.FeedEntry; import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent; 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.FeedSubscription; 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.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry; import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl; import com.sun.syndication.feed.synd.SyndEntryImpl;
@@ -43,6 +46,12 @@ public class Entry implements Serializable {
entry.setFeedLink(sub.getFeed().getLink()); entry.setFeedLink(sub.getFeed().getLink());
entry.setIconUrl(FeedUtils.getFaviconUrl(sub, publicUrl)); 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) { if (content != null) {
entry.setRtl(FeedUtils.isRTL(feedEntry)); entry.setRtl(FeedUtils.isRTL(feedEntry));
entry.setTitle(content.getTitle()); 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)") @ApiProperty("wether the entry is still markable (old entry statuses are discarded)")
private boolean markable; 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) @ApiProperty(value = "user wants category and feeds with no unread entries shown", required = true)
private boolean showRead; 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) @ApiProperty(value = "In expanded view, scroll through entries mark them as read", required = true)
private boolean scrollMarks; private boolean scrollMarks;
@@ -38,5 +35,19 @@ public class Settings implements Serializable {
@ApiProperty(value = "user's custom css for the website") @ApiProperty(value = "user's custom css for the website")
private String customCss; 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> <!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org" wicket:id="html"> <html xmlns:wicket="http://wicket.apache.org" wicket:id="html">
<head> <head>
<title>CommaFeed</title> <title>CommaFeed</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" /> <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" href="app-icon-57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="app-icon-72.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="114x114" href="app-icon-114.png" />
<link rel="apple-touch-icon" sizes="144x144" href="app-icon-144.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="32x32" href="app-icon-32.png" />
<link rel="icon" sizes="64x64" href="app-icon-64.png" /> <link rel="icon" sizes="64x64" href="app-icon-64.png" />
<link rel="icon" sizes="128x128" href="app-icon-128.png" /> <link rel="icon" sizes="128x128" href="app-icon-128.png" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<meta name="application-name" content="CommaFeed" /> <meta name="application-name" content="CommaFeed" />
<meta name="msapplication-navbutton-color" content="#F88A14" /> <meta name="msapplication-navbutton-color" content="#F88A14" />
<meta name="msapplication-starturl" content="/" /> <meta name="msapplication-starturl" content="/" />
<meta name="msapplication-square70x70logo" content="metro-icon-70.png"/> <meta name="msapplication-square70x70logo" content="metro-icon-70.png" />
<meta name="msapplication-square150x150logo" content="metro-icon-150.png"/> <meta name="msapplication-square150x150logo" content="metro-icon-150.png" />
<link rel="fluid-icon" href="app-icon-512.png" title="CommaFeed" /> <link rel="fluid-icon" href="app-icon-512.png" title="CommaFeed" />
<link rel="logo" type="image/svg" href="app-icon.svg" /> <link rel="logo" type="image/svg" href="app-icon.svg" />
</head> </head>
<body> <body>
<wicket:child /> <wicket:child />
<wicket:container wicket:id="footer-container"/> <wicket:container wicket:id="footer-container" />
<wicket:container wicket:id="uservoice"> <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> <script>
UserVoice = window.UserVoice || []; UserVoice = window.UserVoice || [];
UserVoice.push(['showTab', 'classic_widget', { UserVoice.push(['showTab', 'classic_widget', {
mode: 'full', mode : 'full',
default_mode: 'feedback', default_mode : 'feedback',
primary_color: '#000', primary_color : '#000',
link_color: '#007dbf', link_color : '#007dbf',
forum_id: 204509, forum_id : 204509,
support_tab_name: 'Contact', support_tab_name : 'Contact',
feedback_tab_name: 'Feedback', feedback_tab_name : 'Feedback',
tab_label: 'Feedback', tab_label : 'Feedback',
tab_color: '#7e72db', tab_color : '#7e72db',
tab_position: 'bottom-right', tab_position : 'bottom-right',
tab_inverted: false tab_inverted : false
}]); }]);
</script> </script>
</wicket:container> </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.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.WebPage;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedEntryDAO; 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.model.UserSettings;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.MailService; import com.commafeed.backend.services.MailService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
import com.commafeed.frontend.utils.WicketUtils; import com.commafeed.frontend.utils.WicketUtils;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;

View File

@@ -4,8 +4,8 @@ import javax.inject.Inject;
import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.WebPage;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.services.UserService; import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
public class DemoLoginPage extends WebPage { 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.markup.head.IHeaderResponse;
import org.apache.wicket.request.mapper.parameter.PageParameters; 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.UserRole.Role;
import com.commafeed.backend.model.UserSettings; import com.commafeed.backend.model.UserSettings;
import com.commafeed.frontend.CommaFeedSession; import com.commafeed.frontend.CommaFeedSession;
@@ -21,7 +22,11 @@ public class HomePage extends BasePage {
response.render(CssHeaderItem.forReference(new UserCustomCssReference() { response.render(CssHeaderItem.forReference(new UserCustomCssReference() {
@Override @Override
protected String getCss() { 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(); return settings == null ? null : settings.getCustomCss();
} }
}, new PageParameters().add("_t", System.currentTimeMillis()), null)); }, new PageParameters().add("_t", System.currentTimeMillis()), null));

View File

@@ -54,13 +54,13 @@ public class NextUnreadRedirectPage extends WebPage {
List<FeedEntryStatus> statuses = null; List<FeedEntryStatus> statuses = null;
if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) { if (StringUtils.isBlank(categoryId) || CategoryREST.ALL.equals(categoryId)) {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user); 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 { } else {
FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId)); FeedCategory category = feedCategoryDAO.findById(user, Long.valueOf(categoryId));
if (category != null) { if (category != null) {
List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category); List<FeedCategory> children = feedCategoryDAO.findAllChildrenCategories(user, category);
List<FeedSubscription> subscriptions = feedSubscriptionDAO.findByCategories(user, children); 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"> <div class="text-center">
<img src="images/logo_2.png" /> <img src="images/logo_2.png" />
<div wicket:id="feedback"></div> <div wicket:id="feedback"></div>
<form wicket:id="form"> <form wicket:id="form" class="form-horizontal">
New Password: <div class="form-group">
<input type="password" wicket:id="password" /> <label>New Password</label>
<br /> <input type="password" wicket:id="password" />
Confirm: </div>
<input type="password" wicket:id="confirm" /> <div class="form-group">
<br /> <label>Confirm</label>
<input type="submit" class="btn btn-primary" value="Submit" /> <input type="password" wicket:id="confirm" />
<input type="button" class="btn" wicket:id="cancel" value="Home page" /> </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> </form>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -4,23 +4,17 @@
<wicket:panel> <wicket:panel>
<div wicket:id="feedback"></div> <div wicket:id="feedback"></div>
<form wicket:id="form" autocomplete="off"> <form wicket:id="form" autocomplete="off">
<div class="control-group"> <div class="form-group">
<label class="control-label">User Name</label> <label>User Name</label>
<div class="controls"> <input type="text" wicket:id="name" class="form-control"></input>
<input type="text" wicket:id="name" class="input-block-level"></input>
</div>
</div> </div>
<div class="control-group"> <div class="form-group">
<label class="control-label">Password</label> <label>Password</label>
<div class="controls"> <input type="password" wicket:id="password" class="form-control"></input>
<input type="password" wicket:id="password" class="input-block-level"></input>
</div>
</div> </div>
<div class="control-group"> <div class="form-group">
<label class="control-label">Email address (used for password recovery only)</label> <label>Email address (used for password recovery only)</label>
<div class="controls"> <input type="email" wicket:id="email" class="form-control"></input>
<input type="email" wicket:id="email" class="input-block-level"></input>
</div>
</div> </div>
<div> <div>
<input type="submit" class="btn btn-primary" value="Register" /> <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("sassOnlyProcessor", new SassOnlyProcessor());
map.put("sassImport", new SassImportProcessor()); map.put("sassImport", new SassImportProcessor());
map.put("timestamp", new TimestampProcessor()); map.put("timestamp", new TimestampProcessor());
map.put("cssUrlRewriting", new CustomCssUrlRewritingProcessor());
return map; return map;
} }

View File

@@ -16,6 +16,7 @@ public class WroManagerFactory extends ConfigurableWroManagerFactory {
map.put("sassOnlyProcessor", new SassOnlyProcessor()); map.put("sassOnlyProcessor", new SassOnlyProcessor());
map.put("sassImport", new SassImportProcessor()); map.put("sassImport", new SassImportProcessor());
map.put("timestamp", new TimestampProcessor()); 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.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader; 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.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
@Provider @Provider
@Consumes(MediaType.APPLICATION_JSON) @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); private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Context
private HttpServletRequest request;
@Override @Override
public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException { 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.CACHE_CONTROL, CACHE_CONTROL_VALUE);
httpHeaders.putSingle(HttpHeaders.PRAGMA, 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 @Override
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { 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.request.cycle.RequestCycle;
import org.apache.wicket.util.crypt.Base64; import org.apache.wicket.util.crypt.Base64;
import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
@@ -42,6 +43,9 @@ public abstract class AbstractREST {
@Context @Context
private HttpServletResponse response; private HttpServletResponse response;
@Inject
MetricRegistry metrics;
@Inject @Inject
private UserDAO userDAO; private UserDAO userDAO;
@@ -93,10 +97,13 @@ public abstract class AbstractREST {
} }
@AroundInvoke @AroundInvoke
public Object checkSecurity(InvocationContext context) throws Exception { public Object intercept(InvocationContext context) throws Exception {
Method method = context.getMethod();
// check security
boolean allowed = true; boolean allowed = true;
User user = null; User user = null;
Method method = context.getMethod();
SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method.getAnnotation(SecurityCheck.class) : method SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method.getAnnotation(SecurityCheck.class) : method
.getDeclaringClass().getAnnotation(SecurityCheck.class); .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) { private boolean checkRole(Role requiredRole) {
@@ -130,4 +146,5 @@ public abstract class AbstractREST {
boolean authorized = roles.hasAnyRole(new Roles(requiredRole.name())); boolean authorized = roles.hasAnyRole(new Roles(requiredRole.name()));
return authorized; return authorized;
} }
} }

View File

@@ -1,48 +1,40 @@
package com.commafeed.frontend.rest.resources; package com.commafeed.frontend.rest.resources;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.DatabaseCleaner; import com.codahale.metrics.MetricRegistry;
import com.commafeed.backend.MetricsBean;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.FeedDAO; import com.commafeed.backend.dao.FeedDAO;
import com.commafeed.backend.dao.FeedDAO.DuplicateMode;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver; import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedRefreshUpdater; import com.commafeed.backend.feeds.FeedRefreshUpdater;
import com.commafeed.backend.feeds.FeedRefreshWorker; import com.commafeed.backend.feeds.FeedRefreshWorker;
import com.commafeed.backend.model.ApplicationSettings; import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.User; import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserRole;
import com.commafeed.backend.model.UserRole.Role; import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.DatabaseCleaningService;
import com.commafeed.backend.services.FeedService; import com.commafeed.backend.services.FeedService;
import com.commafeed.backend.services.PasswordEncryptionService; import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService; import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.FeedCount;
import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.UserModel;
import com.commafeed.frontend.model.request.FeedMergeRequest;
import com.commafeed.frontend.model.request.IDRequest; import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.rest.PrettyPrint;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.Api;
@@ -70,10 +62,10 @@ public class AdminREST extends AbstractREST {
FeedDAO feedDAO; FeedDAO feedDAO;
@Inject @Inject
MetricsBean metricsBean; MetricRegistry metrics;
@Inject @Inject
DatabaseCleaner cleaner; DatabaseCleaningService cleaner;
@Inject @Inject
FeedRefreshWorker feedRefreshWorker; FeedRefreshWorker feedRefreshWorker;
@@ -226,26 +218,24 @@ public class AdminREST extends AbstractREST {
@Path("/metrics") @Path("/metrics")
@GET @GET
@PrettyPrint
@ApiOperation(value = "Retrieve server metrics") @ApiOperation(value = "Retrieve server metrics")
public Response getMetrics(@QueryParam("backlog") @DefaultValue("false") boolean backlog) { public Response getMetrics() {
Map<String, Object> map = Maps.newLinkedHashMap(); return Response.ok(metrics).build();
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());
@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(); return Response.ok(map).build();
} }
@Path("/cleanup/feeds") @Path("/cleanup/feeds")
@GET @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() { public Response cleanupFeeds() {
Map<String, Long> map = Maps.newHashMap(); Map<String, Long> map = Maps.newHashMap();
map.put("feeds_without_subscriptions", cleaner.cleanFeedsWithoutSubscriptions()); map.put("feeds_without_subscriptions", cleaner.cleanFeedsWithoutSubscriptions());
@@ -261,44 +251,4 @@ public class AdminREST extends AbstractREST {
return Response.ok(map).build(); 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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
@@ -106,7 +106,8 @@ public class CategoryREST extends AbstractREST {
@ApiParam( @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, 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 = "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); Preconditions.checkNotNull(readType);
@@ -135,11 +136,11 @@ public class CategoryREST extends AbstractREST {
} }
if (ALL.equals(id)) { if (ALL.equals(id)) {
entries.setName("All"); entries.setName(ObjectUtils.defaultIfNull(tag, "All"));
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser()); List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser());
removeExcludedSubscriptions(subs, excludedIds); removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, keywords, newerThanDate, offset, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), subs, unreadOnly, keywords, newerThanDate,
limit + 1, order, true, onlyIds); offset, limit + 1, order, true, onlyIds, tag);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add( entries.getEntries().add(
@@ -161,8 +162,8 @@ public class CategoryREST extends AbstractREST {
List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), parent); List<FeedCategory> categories = feedCategoryDAO.findAllChildrenCategories(getUser(), parent);
List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(getUser(), categories); List<FeedSubscription> subs = feedSubscriptionDAO.findByCategories(getUser(), categories);
removeExcludedSubscriptions(subs, excludedIds); removeExcludedSubscriptions(subs, excludedIds);
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(subs, unreadOnly, keywords, newerThanDate, offset, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), subs, unreadOnly, keywords, newerThanDate,
limit + 1, order, true, onlyIds); offset, limit + 1, order, true, onlyIds, tag);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add( entries.getEntries().add(
@@ -170,8 +171,9 @@ public class CategoryREST extends AbstractREST {
.isImageProxyEnabled())); .isImageProxyEnabled()));
} }
entries.setName(parent.getName()); entries.setName(parent.getName());
} else {
return Response.status(Status.NOT_FOUND).entity("<message>category not found</message>").build();
} }
} }
boolean hasMore = entries.getEntries().size() > limit; boolean hasMore = entries.getEntries().size() > limit;
@@ -181,6 +183,7 @@ public class CategoryREST extends AbstractREST {
} }
entries.setTimestamp(System.currentTimeMillis()); entries.setTimestamp(System.currentTimeMillis());
entries.setIgnoredReadStatus(STARRED.equals(id) || keywords != null || tag != null);
FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords); FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords);
return Response.ok(entries).build(); return Response.ok(entries).build();
} }
@@ -191,16 +194,24 @@ public class CategoryREST extends AbstractREST {
@Produces(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true) @SecurityCheck(value = Role.USER, apiKeyAllowed = true)
public Response getCategoryEntriesAsFeed( 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); Response response = getCategoryEntries(id, readType, newerThan, offset, limit, order, keywords, onlyIds, excludedSubscriptionIds,
tag);
ReadingMode readType = ReadingMode.all; if (response.getStatus() != Status.OK.getStatusCode()) {
ReadingOrder order = ReadingOrder.desc; return response;
int offset = 0; }
int limit = 20; Entries entries = (Entries) response.getEntity();
Entries entries = (Entries) getCategoryEntries(id, readType, null, offset, limit, order, null, false, null).getEntity();
SyndFeed feed = new SyndFeedImpl(); SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0"); feed.setFeedType("rss_2.0");

View File

@@ -1,17 +1,23 @@
package com.commafeed.frontend.rest.resources; package com.commafeed.frontend.rest.resources;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import com.commafeed.backend.dao.FeedEntryStatusDAO; import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedEntryTagDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO; import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.FeedEntryService; 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.MarkRequest;
import com.commafeed.frontend.model.request.MultipleMarkRequest; import com.commafeed.frontend.model.request.MultipleMarkRequest;
import com.commafeed.frontend.model.request.StarRequest; import com.commafeed.frontend.model.request.StarRequest;
import com.commafeed.frontend.model.request.TagRequest;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiOperation;
@@ -30,6 +36,12 @@ public class EntryREST extends AbstractREST {
@Inject @Inject
FeedSubscriptionDAO feedSubscriptionDAO; FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
FeedEntryTagDAO feedEntryTagDAO;
@Inject
FeedEntryTagService feedEntryTagService;
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@@ -71,4 +83,24 @@ public class EntryREST extends AbstractREST {
return Response.ok().build(); 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.ObjectUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.cache.CacheService; import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO; import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO; 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.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.feeds.FetchedFeed; 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.Feed;
import com.commafeed.backend.model.FeedCategory; import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus; 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.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingMode; import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder; 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.ApplicationSettingsService;
import com.commafeed.backend.services.FeedEntryService; import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedSubscriptionService; import com.commafeed.backend.services.FeedSubscriptionService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Entries; import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry; 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.MarkRequest;
import com.commafeed.frontend.model.request.SubscribeRequest; import com.commafeed.frontend.model.request.SubscribeRequest;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.sun.syndication.feed.opml.Opml; import com.sun.syndication.feed.opml.Opml;
import com.sun.syndication.feed.synd.SyndEntry; import com.sun.syndication.feed.synd.SyndEntry;
@@ -167,8 +168,8 @@ public class FeedREST extends AbstractREST {
entries.setErrorCount(subscription.getFeed().getErrorCount()); entries.setErrorCount(subscription.getFeed().getErrorCount());
entries.setFeedLink(subscription.getFeed().getLink()); entries.setFeedLink(subscription.getFeed().getLink());
List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(Arrays.asList(subscription), unreadOnly, keywords, List<FeedEntryStatus> list = feedEntryStatusDAO.findBySubscriptions(getUser(), Arrays.asList(subscription), unreadOnly,
newerThanDate, offset, limit + 1, order, true, onlyIds); keywords, newerThanDate, offset, limit + 1, order, true, onlyIds, null);
for (FeedEntryStatus status : list) { for (FeedEntryStatus status : list) {
entries.getEntries().add( entries.getEntries().add(
@@ -181,9 +182,12 @@ public class FeedREST extends AbstractREST {
entries.setHasMore(true); entries.setHasMore(true);
entries.getEntries().remove(entries.getEntries().size() - 1); 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.setTimestamp(System.currentTimeMillis());
entries.setIgnoredReadStatus(keywords != null);
FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords); FeedUtils.removeUnwantedFromSearch(entries.getEntries(), keywords);
return Response.ok(entries).build(); 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") @ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries")
@Produces(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true) @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); Response response = getFeedEntries(id, readType, newerThan, offset, limit, order, keywords, onlyIds);
if (response.getStatus() != Status.OK.getStatusCode()) {
ReadingMode readType = ReadingMode.all; return response;
ReadingOrder order = ReadingOrder.desc; }
int offset = 0; Entries entries = (Entries) response.getEntity();
int limit = 20;
Entries entries = (Entries) getFeedEntries(id, readType, null, offset, limit, order, null, false).getEntity();
SyndFeed feed = new SyndFeedImpl(); SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0"); feed.setFeedType("rss_2.0");
@@ -235,7 +245,7 @@ public class FeedREST extends AbstractREST {
try { try {
FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null); FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null);
info = new FeedInfo(); info = new FeedInfo();
info.setUrl(feed.getFeed().getUrl()); info.setUrl(feed.getUrlAfterRedirect());
info.setTitle(feed.getTitle()); info.setTitle(feed.getTitle());
} catch (Exception e) { } catch (Exception e) {
@@ -256,7 +266,8 @@ public class FeedREST extends AbstractREST {
try { try {
info = fetchFeedInternal(req.getUrl()); info = fetchFeedInternal(req.getUrl());
} catch (Exception e) { } 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(); return Response.ok(info).build();
} }
@@ -265,11 +276,7 @@ public class FeedREST extends AbstractREST {
@GET @GET
@ApiOperation(value = "Queue all feeds of the user for refresh", notes = "Manually add all feeds of the user to the refresh queue") @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() { public Response queueAllForRefresh() {
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(getUser()); feedSubscriptionService.refreshAll(getUser());
for (FeedSubscription sub : subs) {
Feed feed = sub.getFeed();
taskGiver.add(feed, true);
}
return Response.ok().build(); return Response.ok().build();
} }
@@ -368,8 +375,10 @@ public class FeedREST extends AbstractREST {
try { try {
url = fetchFeedInternal(url).getUrl(); url = fetchFeedInternal(url).getUrl();
FeedCategory category = CategoryREST.ALL.equals(req.getCategoryId()) ? null : feedCategoryDAO.findById(Long.valueOf(req FeedCategory category = null;
.getCategoryId())); if (req.getCategoryId() != null && !CategoryREST.ALL.equals(req.getCategoryId())) {
category = feedCategoryDAO.findById(Long.valueOf(req.getCategoryId()));
}
FeedInfo info = fetchFeedInternal(url); FeedInfo info = fetchFeedInternal(url);
feedSubscriptionService.subscribe(getUser(), info.getUrl(), req.getTitle(), category); feedSubscriptionService.subscribe(getUser(), info.getUrl(), req.getTitle(), category);
} catch (Exception e) { } catch (Exception e) {

View File

@@ -3,6 +3,7 @@ package com.commafeed.frontend.rest.resources;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import javax.annotation.PostConstruct;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes; 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.ArrayUtils;
import org.apache.commons.lang3.StringUtils; 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.dao.FeedDAO;
import com.commafeed.backend.feeds.FeedParser; import com.commafeed.backend.feeds.FeedParser;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver; import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
@@ -52,8 +54,13 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
@Inject @Inject
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject private Meter pushReceived;
MetricsBean metricsBean;
@PostConstruct
public void initMetrics() {
pushReceived = metrics.meter(MetricRegistry.name(getClass(), "pushReceived"));
}
@Path("/callback") @Path("/callback")
@GET @GET
@@ -119,7 +126,7 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
log.debug("pushing content to queue for {}", feed.getUrl()); log.debug("pushing content to queue for {}", feed.getUrl());
taskGiver.add(feed, false); taskGiver.add(feed, false);
} }
metricsBean.pushReceived(feeds.size()); pushReceived.mark();
} catch (Exception e) { } catch (Exception e) {
log.error("Could not parse pubsub callback: " + e.getMessage()); 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;
import com.commafeed.backend.HttpGetter.HttpResult; import com.commafeed.backend.HttpGetter.HttpResult;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.feeds.FeedUtils; import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.services.ApplicationPropertiesService; import com.commafeed.backend.services.ApplicationPropertiesService;
import com.commafeed.backend.services.ApplicationSettingsService; import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.model.ServerInfo; import com.commafeed.frontend.model.ServerInfo;
import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation; 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 org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO; import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO; 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.ApplicationSettingsService;
import com.commafeed.backend.services.PasswordEncryptionService; import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService; import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck; import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Settings; import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel; import com.commafeed.frontend.model.UserModel;
@@ -74,20 +74,44 @@ public class UserREST extends AbstractREST {
s.setReadingOrder(settings.getReadingOrder().name()); s.setReadingOrder(settings.getReadingOrder().name());
s.setViewMode(settings.getViewMode().name()); s.setViewMode(settings.getViewMode().name());
s.setShowRead(settings.isShowRead()); 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.setScrollMarks(settings.isScrollMarks());
s.setTheme(settings.getTheme()); s.setTheme(settings.getTheme());
s.setCustomCss(settings.getCustomCss()); s.setCustomCss(settings.getCustomCss());
s.setLanguage(settings.getLanguage()); s.setLanguage(settings.getLanguage());
s.setScrollSpeed(settings.getScrollSpeed());
} else { } else {
s.setReadingMode(ReadingMode.unread.name()); s.setReadingMode(ReadingMode.unread.name());
s.setReadingOrder(ReadingOrder.desc.name()); s.setReadingOrder(ReadingOrder.desc.name());
s.setViewMode(ViewMode.title.name()); s.setViewMode(ViewMode.title.name());
s.setShowRead(true); s.setShowRead(true);
s.setTheme("default"); 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.setScrollMarks(true);
s.setLanguage("en"); s.setLanguage("en");
s.setScrollSpeed(400);
} }
return Response.ok(s).build(); return Response.ok(s).build();
} }
@@ -114,8 +138,20 @@ public class UserREST extends AbstractREST {
s.setScrollMarks(settings.isScrollMarks()); s.setScrollMarks(settings.isScrollMarks());
s.setTheme(settings.getTheme()); s.setTheme(settings.getTheme());
s.setCustomCss(settings.getCustomCss()); s.setCustomCss(settings.getCustomCss());
s.setSocialButtons(settings.isSocialButtons());
s.setLanguage(settings.getLanguage()); 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); userSettingsDAO.saveOrUpdate(s);
return Response.ok().build(); return Response.ok().build();

View File

@@ -28,6 +28,8 @@ import java.util.Map;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; import java.util.TreeMap;
import org.apache.commons.lang.StringUtils;
/** /**
* See http://en.wikipedia.org/wiki/URL_normalization for a reference Note: some * 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 * 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)); URL canonicalURL = new URL(UrlResolver.resolveUrl(context == null ? "" : context, href));
String host = canonicalURL.getHost().toLowerCase(); String host = canonicalURL.getHost().toLowerCase();
if (host == "") { if (StringUtils.isBlank(host)) {
// This is an invalid Url. // This is an invalid Url.
return null; 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"?> <?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
@@ -9,6 +8,7 @@
<jta-data-source>${jpa.datasource.name}</jta-data-source> <jta-data-source>${jpa.datasource.name}</jta-data-source>
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode> <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<properties> <properties>
<property name="openejb.jpa.auto-scan" value="true" />
<property name="format_sql" value="true" /> <property name="format_sql" value="true" />
<property name="use_sql_comments" value="true" /> <property name="use_sql_comments" value="true" />
@@ -22,26 +22,17 @@
<property name="hibernate.generate_statistics" value="true" /> <property name="hibernate.generate_statistics" value="true" />
<property name="hibernate.cache.use_second_level_cache" <property name="hibernate.cache.use_second_level_cache" value="${jpa.cache}" />
value="${jpa.cache}" />
<property name="hibernate.cache.use_query_cache" value="${jpa.cache}" /> <property name="hibernate.cache.use_query_cache" value="${jpa.cache}" />
<property name="hibernate.cache.region.factory_class" <property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.infinispan.InfinispanRegionFactory" />
value="org.hibernate.cache.infinispan.InfinispanRegionFactory" /> <property name="hibernate.cache.infinispan.statistics" value="true" />
<property name="hibernate.cache.infinispan.statistics"
value="true" />
<property name="hibernate.cache.infinispan.entity.eviction.strategy" <property name="hibernate.cache.infinispan.entity.eviction.strategy" value="LRU" />
value="LRU" /> <property name="hibernate.cache.infinispan.entity.eviction.wake_up_interval" value="2000" />
<property <property name="hibernate.cache.infinispan.entity.eviction.max_entries" value="10000" />
name="hibernate.cache.infinispan.entity.eviction.wake_up_interval" <property name="hibernate.cache.infinispan.entity.expiration.lifespan" value="60000" />
value="2000" /> <property name="hibernate.cache.infinispan.entity.expiration.max_idle" value="30000" />
<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> </properties>
</persistence-unit> </persistence-unit>

View File

@@ -357,6 +357,7 @@
</changeSet> </changeSet>
<changeSet author="athou" id="status-cleanup"> <changeSet author="athou" id="status-cleanup">
<validCheckSum>7:cf40ae235c2d4086c5fa6ac64102c6a9</validCheckSum>
<delete tableName="FEEDENTRYSTATUSES"> <delete tableName="FEEDENTRYSTATUSES">
<where>read_status = false and starred = false</where> <where>read_status = false and starred = false</where>
</delete> </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.0.xml" />
<include file="changelogs/db.changelog-1.1.xml" /> <include file="changelogs/db.changelog-1.1.xml" />
<include file="changelogs/db.changelog-1.2.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> </databaseChangeLog>

View File

@@ -6,6 +6,7 @@ global.download=تحميل
global.link=رابط global.link=رابط
global.bookmark=مرجعية global.bookmark=مرجعية
global.close=أغلق global.close=أغلق
global.tags=Tags ####### Needs translation
tree.subscribe=اشترك tree.subscribe=اشترك
tree.import=استورد tree.import=استورد
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=الترتيب حسب التاريخ تصاعدي / ت
toolbar.titles_only=العناوين فقط toolbar.titles_only=العناوين فقط
toolbar.expanded_view=عرض موسع toolbar.expanded_view=عرض موسع
toolbar.mark_all_as_read=اعتبر الكل مقروء 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_day=العناصر الأقدم من يوم
toolbar.mark_all_older_week=العناصر الأقدم من أسبوع toolbar.mark_all_older_week=العناصر الأقدم من أسبوع
toolbar.mark_all_older_two_weeks=العناصر الأقدم من أسبوعين 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.social_buttons=Show social sharing buttons
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
settings.appearance=Appearance 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.theme=Theme
settings.submit_your_theme=Submit your theme settings.submit_your_theme=Submit your theme
settings.custom_css=Custom CSS settings.custom_css=Custom CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh
details.feed_url=Feed URL details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first. details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Unsubscribe 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.category_details=Category details
details.tag_details=Tag details ####### Needs translation
details.parent_category=Parent category details.parent_category=Parent category
profile.user_name=User name 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.generate_new_api_key_info=Changing password will generate a new API key
profile.opml_export=OPML export profile.opml_export=OPML export
profile.delete_account=Delete account 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.rest_api=REST API
about.keyboard_shortcuts=Keyboard shortcuts 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.link = Odkaz
global.bookmark = Záložky global.bookmark = Záložky
global.close = Zavřít global.close = Zavřít
global.tags=Tags ####### Needs translation
tree.subscribe = Nový odběr tree.subscribe = Nový odběr
tree.import = Importovat 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.titles_only = Zobrazit jenom titulky
toolbar.expanded_view = Rozšířený náhled toolbar.expanded_view = Rozšířený náhled
toolbar.mark_all_as_read = Označit vše jako přečtené 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_day = Položky starší než den
toolbar.mark_all_older_week = Položky starší než tý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 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.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.general.scroll_marks = Skrolování v rozšířeném náhledu označí položky jako přečtené
settings.appearance = Vzhled 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.theme = Motiv
settings.submit_your_theme = Nahrát vlastní motiv settings.submit_your_theme = Nahrát vlastní motiv
settings.custom_css = Vlastní motiv (CSS) 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.feed_url = URL RSS zdroje
details.generate_api_key_first = Vygenerujte si API klíč na stránce vašeho profilu. details.generate_api_key_first = Vygenerujte si API klíč na stránce vašeho profilu.
details.unsubscribe = Odhlásit odběr. 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.category_details = Detail kategorie
details.tag_details=Tag details ####### Needs translation
details.parent_category = Hlavní kategorie details.parent_category = Hlavní kategorie
profile.user_name = Uživatelské jméno 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.generate_new_api_key_info = Změnou hesla vygenerujete nový API klíč
profile.opml_export = exportovat do formátu OPML profile.opml_export = exportovat do formátu OPML
profile.delete_account = Odstranit účet 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.rest_api = REST API
about.keyboard_shortcuts = Klávesové zkratky about.keyboard_shortcuts = Klávesové zkratky

View File

@@ -6,6 +6,7 @@ global.download=Lawrlwytho
global.link=Dolen global.link=Dolen
global.bookmark=Nod tudalen global.bookmark=Nod tudalen
global.close=Cau global.close=Cau
global.tags=Tags ####### Needs translation
tree.subscribe=Tanysgrifio tree.subscribe=Tanysgrifio
tree.import=Mewnforio tree.import=Mewnforio
@@ -17,10 +18,10 @@ subscribe.feed_url=URL Ffrwd
subscribe.feed_name=Enw Ffrwd subscribe.feed_name=Enw Ffrwd
subscribe.category=Categori 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_reader_suffix= gyfrif.
import.google_download=Fel arall, lanlwytho dy ffeil tanysgrifiadau.xml import.google_download=Fel arall, lanlwytha dy ffeil tanysgrifiadau.xml
import.google_download_link=Lawrlwytho fe yma. import.google_download_link=Lawrlwytha fe yma.
import.xml_file=Ffeil OPML import.xml_file=Ffeil OPML
new_category.name=Enw new_category.name=Enw
@@ -31,14 +32,15 @@ toolbar.all=Popeth
toolbar.previous_entry=Eitem blaenorol toolbar.previous_entry=Eitem blaenorol
toolbar.next_entry=Eitem nesaf toolbar.next_entry=Eitem nesaf
toolbar.refresh=Adnewyddu 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.sort_by_asc_desc=Trefnu yn ôl dyddiad
toolbar.titles_only=Teitlau yn unig toolbar.titles_only=Teitlau yn unig
toolbar.expanded_view=Golygfa estynedig toolbar.expanded_view=Golwg estynedig
toolbar.mark_all_as_read=Marcio popeth fel darllenwyd toolbar.mark_all_as_read=Nodi'r cyfan fel wedi ei ddarllen
toolbar.mark_all_older_day=Eitemau sy'n hyn na diwrnod toolbar.mark_all_older_12_hours=Items older than 12 hours ####### Needs translation
toolbar.mark_all_older_week=Eitemau sy'n hyn nag wythnos toolbar.mark_all_older_day=Eitemau hyn na diwrnod
toolbar.mark_all_older_two_weeks=Eitemau sy'n hyn na phythefnos toolbar.mark_all_older_week=Eitemau hyn nag wythnos
toolbar.mark_all_older_two_weeks=Eitemau hyn na phythefnos
toolbar.settings=Gosodiadau toolbar.settings=Gosodiadau
toolbar.profile=Proffil toolbar.profile=Proffil
toolbar.admin=Gweinyddwr toolbar.admin=Gweinyddwr
@@ -46,44 +48,49 @@ toolbar.about=Ynghylch
toolbar.logout=Allgofnodi toolbar.logout=Allgofnodi
toolbar.donate=Rhoddi toolbar.donate=Rhoddi
view.entry_source=from ####### Needs translation view.entry_source=o
view.entry_author=by ####### Needs translation view.entry_author=gan
view.error_while_loading_feed=Gwall tra'n llwytho'r ffrwd view.error_while_loading_feed=Gwall wrth lwytho'r ffrwd
view.keep_unread=Cadw fel heb ei darllen view.keep_unread=Parhau i'w nodi fel heb ei ddarllen
view.no_unread_items=dim eitemau heb eu darllen view.no_unread_items=: Dim eitemau heb eu darllen ###### Cynnwys y colon oherwydd gystrawen y cyd-destyn
view.mark_up_to_here=Mark as read up to here ####### Needs translation view.mark_up_to_here=Nodi'r rhai hyd yma fel wedi eu darllen
view.search_for=searching for: ####### Needs translation view.search_for=yn chwilio am:
view.no_search_results=No match found for the requested keywords ####### Needs translation 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.help=Defnyddia'r dychwelwr i ddethol a saethau i lywio
feedsearch.result_prefix=Dy danysgrifiadau: feedsearch.result_prefix=Dy danysgrifiadau:
settings.general=Cyffredinol settings.general=Cyffredinol
settings.general.language=Iaith 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.show_unread=Dangos ffrydiau a chategoriau gyda dim eitemau heb eu darllen
settings.general.social_buttons=Dangos botymau rhannu settings.general.social_buttons=Dangos botymau rhannu
settings.general.scroll_marks=Mewn golygfa estynedig, sgrolio trwy eitemau yn marcio fel darllenwyd 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=Golygfa 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.theme=Thema
settings.submit_your_theme=Cyflwyno dy thema settings.submit_your_theme=Cyflwyna dy thema
settings.custom_css=CSS wedi'i addasu settings.custom_css=CSS wedi'i addasu
details.feed_details=Manylion ffrwd details.feed_details=Manylion ffrwd
details.url=URL details.url=URL
details.website=Website ####### Needs translation details.website=Gwefan
details.name=Enw details.name=Enw
details.category=Categori details.category=Categori
details.position=Safle details.position=Safle
details.last_refresh=Adnewyddiad diwethaf details.last_refresh=Adnewyddiad diwethaf
details.message=Last refresh message ####### Needs translation details.message=Neges adnewyddiad diwethaf
details.next_refresh=Adnewyddiad nesaf 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.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=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.category_details=Manylion categori
details.tag_details=Tag details ####### Needs translation
details.parent_category=Categori rhiant details.parent_category=Categori rhiant
profile.user_name=Enw defnyddiwr profile.user_name=Enw defnyddiwr
@@ -91,59 +98,60 @@ profile.email=E-bost
profile.change_password=Newid cyfrinair profile.change_password=Newid cyfrinair
profile.confirm_password=Cadarnhau cyfrinair profile.confirm_password=Cadarnhau cyfrinair
profile.minimum_6_chars=Isafswm 6 nod profile.minimum_6_chars=Isafswm 6 nod
profile.passwords_do_not_match=Cyfrineiriau yn wahanol profile.passwords_do_not_match=Mae'r cyfrineiriau yn wahanol
profile.api_key=allwedd API profile.api_key=Allwedd API
profile.api_key_not_generated=Heb gynhyrchu eto profile.api_key_not_generated=Heb ei gynhyrchu eto
profile.generate_new_api_key=Cynhyrchu allwedd API newydd profile.generate_new_api_key=Creu allwedd API newydd
profile.generate_new_api_key_info=Newid cyfrinair yn cynhyrchu allwedd API newydd profile.generate_new_api_key_info=Mae newid cyfrinair yn creu allwedd API newydd
profile.opml_export=Allforio OPML profile.opml_export=Allforio OPML
profile.delete_account=Dileu cyfrif 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.rest_api=REST API
about.keyboard_shortcuts=Llwybr byr bysellfwrdd about.keyboard_shortcuts=Llwybr byr bysellfwrdd
about.version=CommaFeed version ####### Needs translation about.version=Fersiwn CommaFeed: ###### Cynnwys y colon oherwydd gystrawen y cyd-destun
about.line1_prefix=CommaFeed yn prosiect cod agored. Mae'r cod ar about.line1_prefix=Mae CommaFeed yn prosiect cod agored. Mae'r cod ar
about.line1_suffix=. 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.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.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 gyfeiriad about.line4=I'r rhai sy'n hoff o Bitcoin, dyma'r cyfeiriad
about.goodies=Goodies 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.subscribe_url=URL Tanysgrifio
about.goodies.chrome_extension=estyniad Chrome about.goodies.chrome_extension=estyniad Chrome
about.goodies.firefox_extension=estyniad Firefox about.goodies.firefox_extension=estyniad Firefox
about.goodies.opera_extension=estyniad Opera about.goodies.opera_extension=estyniad Opera
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio (clicio) about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio ###### Dim angen 'Click' - digon amlwg o'r cyd-destyn
about.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation about.goodies.subscribe_bookmarklet_asc=Hynaf yn gyntaf
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation about.goodies.subscribe_bookmarklet_desc=Diweddaraf yn gyntaf
about.goodies.next_unread_bookmarklet=Botwm eitem nesaf heb ei ddarllen (llusgo i far nodau) about.goodies.next_unread_bookmarklet=Botwm eitem nesaf heb ei ddarllen (llusgo i far nodau)
about.translation=Translation about.translation=Cyfieithiad
about.translation.message=Rydym ni angen dy help i gyfieithu CommaFeed. about.translation.message=Rydym angen dy help i gyfieithu CommaFeed.
about.translation.link=Gweler sut i gyfrannu i gyfieithiadau. about.translation.link=Gweler sut i gyfrannu i gyfieithiadau.
about.announcements=Datganiadau 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.rest_api.link_to_documentation=Dolen i'r ddogfennaeth.
about.shortcuts.mouse_middleclick=llygoden clic-canol about.shortcuts.mouse_middleclick=clic botwm canol llygoden
about.shortcuts.open_next_entry=agor eitem nesaf about.shortcuts.open_next_entry=agor yr eitem nesaf
about.shortcuts.open_previous_entry=agor eitem flaenorol about.shortcuts.open_previous_entry=agor yr eitem flaenorol
about.shortcuts.spacebar=space/shift+space ####### Needs translation about.shortcuts.spacebar=space/shift+space
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation about.shortcuts.move_page_down_up=symud y tudalen i lawr/fyny
about.shortcuts.focus_next_entry=gosod ffocws ar eitem nesaf heb ei hagor about.shortcuts.focus_next_entry=newid ffocws i'r eitem nesaf heb ei hagor
about.shortcuts.focus_previous_entry=gosod ffocws ar eitem flaenorol heb ei hagor about.shortcuts.focus_previous_entry=newid ffocws i'r eitem flaenorol heb ei hagor
about.shortcuts.open_next_feed=agor ffrwd neu gategori nesaf about.shortcuts.open_next_feed=agor y ffrwd neu gategori nesaf
about.shortcuts.open_previous_feed=agor ffrwd neu gategori blaenorol about.shortcuts.open_previous_feed=agor y ffrwd neu gategori blaenorol
about.shortcuts.open_close_current_entry=agor/cau eitem gyfredol about.shortcuts.open_close_current_entry=agor/cau yr eitem gyfredol
about.shortcuts.open_current_entry_in_new_window=agor eitem gyfredol mewn ffenestr newydd 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 eitem gyfredol mewn ffenestr newydd yn y cefndir 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 eitem gyfredol about.shortcuts.star_unstar=serennu/dadserennu'r eitem gyfredol
about.shortcuts.mark_current_entry=marcio eitem gyfredol fel darllenwyd/heb ddarllen about.shortcuts.mark_current_entry=marcio'r eitem gyfredol fel wedi/heb ei ddarllen
about.shortcuts.mark_all_as_read=marcio popeth fel darllenwyd about.shortcuts.mark_all_as_read=marcio popeth fel wedi ei ddarllen
about.shortcuts.open_in_new_tab_mark_as_read=agor eitem mewn tab newydd a marcio fel darllenwyd 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=toggle full screen mode ####### Needs translation about.shortcuts.fullscreen=toglo'r golwg sgrin lawn
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation about.shortcuts.font_size=cynyddu/lleihau maint ffont yr eitem gyfredol
about.shortcuts.go_to_all=go to the All view ####### Needs translation about.shortcuts.go_to_all=newid i olwg 'Popeth'
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation about.shortcuts.go_to_starred=newid i olwg 'Serennwyd'
about.shortcuts.feed_search=llywio i danysgrifiad trwy rhoi ei enw mewn 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.link=Link
global.bookmark=Bogmærke global.bookmark=Bogmærke
global.close=Luk global.close=Luk
global.tags=Tags ####### Needs translation
tree.subscribe=Abonner tree.subscribe=Abonner
tree.import=Importer tree.import=Importer
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sorter efter dato ny/gammel
toolbar.titles_only=Kun titler toolbar.titles_only=Kun titler
toolbar.expanded_view=Udvidet visning toolbar.expanded_view=Udvidet visning
toolbar.mark_all_as_read=Marker alle som læst 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_day=Artikler ældere end én dag
toolbar.mark_all_older_week=Artikler ældere end én uge toolbar.mark_all_older_week=Artikler ældere end én uge
toolbar.mark_all_older_two_weeks=Artikler ældere end to uger 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.social_buttons=Vis delingsknapper
settings.general.scroll_marks=I udvidet visning, marker artikler som læste når der rulles forbi dem settings.general.scroll_marks=I udvidet visning, marker artikler som læste når der rulles forbi dem
settings.appearance=Udseende 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.theme=Tema
settings.submit_your_theme=Indsend dit tema settings.submit_your_theme=Indsend dit tema
settings.custom_css=Brugerdefineret CSS settings.custom_css=Brugerdefineret CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=I kø til opdatering
details.feed_url=URL for abonnement details.feed_url=URL for abonnement
details.generate_api_key_first=Generer en API nøgle i din profil først. details.generate_api_key_first=Generer en API nøgle i din profil først.
details.unsubscribe=Afmeld abonnement 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.category_details=Kategori detaljer
details.tag_details=Tag details ####### Needs translation
details.parent_category=Overordnet kategori details.parent_category=Overordnet kategori
profile.user_name=Brugernavn 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.generate_new_api_key_info=Ændring af adgangskode vil generere en ny API nøgle
profile.opml_export=OPML eksport profile.opml_export=OPML eksport
profile.delete_account=Slet konto 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.rest_api=REST API
about.keyboard_shortcuts=Tastaturgenveje about.keyboard_shortcuts=Tastaturgenveje

View File

@@ -6,6 +6,7 @@ global.download=Herunterladen
global.link=Link global.link=Link
global.bookmark=Lesezeichen global.bookmark=Lesezeichen
global.close=Schließen global.close=Schließen
global.tags=Tags
tree.subscribe=Abonnieren tree.subscribe=Abonnieren
tree.import=Importieren tree.import=Importieren
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Nach Datum sortieren (auf-/absteigend)
toolbar.titles_only=Nur Überschriften toolbar.titles_only=Nur Überschriften
toolbar.expanded_view=Ausgedehnte Ansicht toolbar.expanded_view=Ausgedehnte Ansicht
toolbar.mark_all_as_read=Alle Artikel als gelesen markieren 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_day=Artikel älter als ein Tag
toolbar.mark_all_older_week=Artikel älter als eine Woche toolbar.mark_all_older_week=Artikel älter als eine Woche
toolbar.mark_all_older_two_weeks=Artikel älter als zwei Wochen 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.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.general.scroll_marks=In der ausgedehnten Ansicht werden Artikel beim Scrollen als gelesen markiert
settings.appearance=Aussehen 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.theme=Theme
settings.submit_your_theme=Füg dein Theme hinzu settings.submit_your_theme=Füg dein Theme hinzu
settings.custom_css=Eigenes CSS settings.custom_css=Eigenes CSS
@@ -77,13 +81,16 @@ details.name=Name
details.category=Kategorie details.category=Kategorie
details.position=Position details.position=Position
details.last_refresh=Letzte Aktualisierung 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.next_refresh=Nächste Aktualisierung
details.queued_for_refresh=Wartet auf Aktualisierung details.queued_for_refresh=Wartet auf Aktualisierung
details.feed_url=Feed Adresse details.feed_url=Feed Adresse
details.generate_api_key_first=Generiere zuerst einen API Schlüssel in deinem Profil. details.generate_api_key_first=Generiere zuerst einen API Schlüssel in deinem Profil.
details.unsubscribe=Kündigen 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.category_details=Kategoriedetails
details.tag_details=Tag Details
details.parent_category=Übergeordnete Kategorie details.parent_category=Übergeordnete Kategorie
profile.user_name=Benutzername 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=Generiere einen neuen API key
profile.generate_new_api_key_info=Das Ändern des Passwortes erzeugt einen neuen API Schlüssel profile.generate_new_api_key_info=Das Ändern des Passwortes erzeugt einen neuen API Schlüssel
profile.opml_export=OPML exportieren 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.rest_api=REST API
about.keyboard_shortcuts=Tastatur Kurzbefehle about.keyboard_shortcuts=Tastatur Kurzbefehle

View File

@@ -6,6 +6,7 @@ global.download=Download
global.link=Link global.link=Link
global.bookmark=Bookmark global.bookmark=Bookmark
global.close=Close global.close=Close
global.tags=Tags
tree.subscribe=Subscribe tree.subscribe=Subscribe
tree.import=Import tree.import=Import
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc
toolbar.titles_only=Titles only toolbar.titles_only=Titles only
toolbar.expanded_view=Expanded view toolbar.expanded_view=Expanded view
toolbar.mark_all_as_read=Mark all as read 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_day=Items older than a day
toolbar.mark_all_older_week=Items older than a week toolbar.mark_all_older_week=Items older than a week
toolbar.mark_all_older_two_weeks=Items older than two weeks 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.social_buttons=Show social sharing buttons
settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read settings.general.scroll_marks=In expanded view, scrolling through entries mark them as read
settings.appearance=Appearance 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.theme=Theme
settings.submit_your_theme=Submit your theme settings.submit_your_theme=Submit your theme
settings.custom_css=Custom CSS settings.custom_css=Custom CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh
details.feed_url=Feed URL details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first. details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Unsubscribe 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.category_details=Category details
details.tag_details=Tag details
details.parent_category=Parent category details.parent_category=Parent category
profile.user_name=User name 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.generate_new_api_key_info=Changing password will generate a new API key
profile.opml_export=OPML export profile.opml_export=OPML export
profile.delete_account=Delete account profile.delete_account=Delete account
profile.delete_account_confirmation=Delete your account? There's no turning back!
about.rest_api=REST API about.rest_api=REST API
about.keyboard_shortcuts=Keyboard shortcuts 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.font_size=increase/decrease font size of the current entry
about.shortcuts.go_to_all=go to the All view about.shortcuts.go_to_all=go to the All view
about.shortcuts.go_to_starred=go to the Starred 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.link=Enlace
global.bookmark=Marcador global.bookmark=Marcador
global.close=Close ####### Needs translation global.close=Close ####### Needs translation
global.tags=Tags ####### Needs translation
tree.subscribe=Subscribir tree.subscribe=Subscribir
tree.import=Importar 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.titles_only=Sólo Títulos
toolbar.expanded_view=Vista Expandida toolbar.expanded_view=Vista Expandida
toolbar.mark_all_as_read=Marcar todos como leído 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_day=Artículos anteriores a un día
toolbar.mark_all_older_week=Artículos más de una semana 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 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.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.general.scroll_marks=En vista expandida, el desplazamiento por las entradas las marca como leídas
settings.appearance=Appearance ####### Needs translation 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.theme=Theme ####### Needs translation
settings.submit_your_theme=Submit your theme ####### Needs translation settings.submit_your_theme=Submit your theme ####### Needs translation
settings.custom_css=CSS Personalizado 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.feed_url=URL del Canal
details.generate_api_key_first=Genera una llave API en tu perfil primero. details.generate_api_key_first=Genera una llave API en tu perfil primero.
details.unsubscribe=Terminar subscripción 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.category_details=Detalles de la categoría
details.tag_details=Tag details ####### Needs translation
details.parent_category=Categoría principal details.parent_category=Categoría principal
profile.user_name=Nombre de usuario 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.generate_new_api_key_info=Al cambiar la contraseña se generará una nueva llave API
profile.opml_export=Exportación de OPML profile.opml_export=Exportación de OPML
profile.delete_account=Eliminar cuenta 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.rest_api=REST API
about.keyboard_shortcuts=Atajos de teclado about.keyboard_shortcuts=Atajos de teclado

View File

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

View File

@@ -6,6 +6,7 @@ global.download=Lataa
global.link=Linkki global.link=Linkki
global.bookmark=Kirjanmerkki global.bookmark=Kirjanmerkki
global.close=Sulje global.close=Sulje
global.tags=Tagit
tree.subscribe=Tilaa syöte tree.subscribe=Tilaa syöte
tree.import=Tuo 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.titles_only=Näytä vain otsikot
toolbar.expanded_view=Laajennettu näkymä toolbar.expanded_view=Laajennettu näkymä
toolbar.mark_all_as_read=Merkitse kaikki luetuiksi 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_day=Päivää vanhemmat otsikot
toolbar.mark_all_older_week=Viikkoa vanhemmat otsikot toolbar.mark_all_older_week=Viikkoa vanhemmat otsikot
toolbar.mark_all_older_two_weeks=Kahta 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.keep_unread=Pidä lukemattomana
view.no_unread_items=ei sisällä lukemattomia otsikoita. view.no_unread_items=ei sisällä lukemattomia otsikoita.
view.mark_up_to_here=Merkitse luetuksi tähän asti view.mark_up_to_here=Merkitse luetuksi tähän asti
view.search_for=searching for: ####### Needs translation view.search_for=Etsi sanoilla:
view.no_search_results=No match found for the requested keywords ####### Needs translation view.no_search_results=Ei tuloksia annetuilla hakusanoilla.
feedsearch.hint=Kirjoita syötteen nimi... feedsearch.hint=Kirjoita syötteen nimi...
feedsearch.help=Siirry syötteiden välillä nuolinäppäimillä ja valitse syöte enterillä. 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.social_buttons=Näytä jakonapit
settings.general.scroll_marks=Laajennetussa näkymässä otsikoiden selaaminen merkitsee ne luetuiksi settings.general.scroll_marks=Laajennetussa näkymässä otsikoiden selaaminen merkitsee ne luetuiksi
settings.appearance=Ulkonäkö 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.theme=Teema
settings.submit_your_theme=Lähetä oma teemasi settings.submit_your_theme=Lähetä oma teemasi
settings.custom_css=Oma CSS settings.custom_css=Oma CSS
@@ -77,13 +81,16 @@ details.name=Nimi
details.category=Kansio details.category=Kansio
details.position=Paikka details.position=Paikka
details.last_refresh=Viimeisin päivitys 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.next_refresh=Seuraava päivitys
details.queued_for_refresh=Jonossa päivitettäväksi details.queued_for_refresh=Jonossa päivitettäväksi
details.feed_url=Syötteen osoite details.feed_url=Syötteen osoite
details.generate_api_key_first=Luo API-avain profiilissasi. details.generate_api_key_first=Luo API-avain profiilissasi.
details.unsubscribe=Peruuta tilaus 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.category_details=Kansion tiedot
details.tag_details=Tagin tiedot
details.parent_category=Yläkansio details.parent_category=Yläkansio
profile.user_name=Käyttäjänimi 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.generate_new_api_key_info=Salasanan vaihtaminen luo uuden API-avaimen
profile.opml_export=OPML vienti profile.opml_export=OPML vienti
profile.delete_account=Poista tunnus profile.delete_account=Poista tunnus
profile.delete_account_confirmation=Haluatko varmasti poistaa tunnuksesi? Tätä ei voi perua!
about.rest_api=REST-API about.rest_api=REST-API
about.keyboard_shortcuts=Näppäinoikotiet about.keyboard_shortcuts=Näppäinoikotiet

View File

@@ -6,6 +6,7 @@ global.download=Télécharger
global.link=Lien global.link=Lien
global.bookmark=Favoris global.bookmark=Favoris
global.close=Fermer global.close=Fermer
global.tags=Tags ####### Needs translation
tree.subscribe=S'abonner tree.subscribe=S'abonner
tree.import=Importer tree.import=Importer
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Trier par date croissante/décroissante
toolbar.titles_only=Titres uniquement toolbar.titles_only=Titres uniquement
toolbar.expanded_view=Vue étendue toolbar.expanded_view=Vue étendue
toolbar.mark_all_as_read=Tout marquer comme lu 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_day=Articles de plus d'un jour
toolbar.mark_all_older_week=Articles de plus d'une semaine toolbar.mark_all_older_week=Articles de plus d'une semaine
toolbar.mark_all_older_two_weeks=Articles de plus d'un mois 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.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.general.scroll_marks=En mode de lecture étendu, marquer comme lu les éléments lorsque la fenêtre descend.
settings.appearance=Apparence 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.theme=Thème
settings.submit_your_theme=Soumettez votre thème. settings.submit_your_theme=Soumettez votre thème.
settings.custom_css=CSS personnelle settings.custom_css=CSS personnelle
@@ -83,7 +87,10 @@ details.queued_for_refresh=En file d'attente
details.feed_url=URL du flux details.feed_url=URL du flux
details.generate_api_key_first=Générez une clé API dans votre profil d'abord. details.generate_api_key_first=Générez une clé API dans votre profil d'abord.
details.unsubscribe=Se désabonner 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.category_details=Détails de la catégorie
details.tag_details=Tag details ####### Needs translation
details.parent_category=Catégorie parente details.parent_category=Catégorie parente
profile.user_name=Nom 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.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.opml_export=Export du fichier OPML
profile.delete_account=Effacer le compte 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.rest_api=API REST
about.keyboard_shortcuts=Raccourcis clavier about.keyboard_shortcuts=Raccourcis clavier

View File

@@ -6,6 +6,7 @@ global.download=Descargar
global.link=Ligazón global.link=Ligazón
global.bookmark=Marcador global.bookmark=Marcador
global.close=Pechar global.close=Pechar
global.tags=Tags ####### Needs translation
tree.subscribe=Subscribir tree.subscribe=Subscribir
tree.import=Importar tree.import=Importar
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Ordenar por data asc/desc
toolbar.titles_only=Só títulos toolbar.titles_only=Só títulos
toolbar.expanded_view=Vista expandida toolbar.expanded_view=Vista expandida
toolbar.mark_all_as_read=Marcar todos como lidos 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_day=Artigos anteriores a un día
toolbar.mark_all_older_week=Artigos de máis de unha semana 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 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.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.general.scroll_marks=En vista expandida, o desplazamento polas entradas márcaas como lidas.
settings.appearance=Aspecto 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.theme=Decorado
settings.submit_your_theme=Envíe o seu decorado settings.submit_your_theme=Envíe o seu decorado
settings.custom_css=CSS Personalizado settings.custom_css=CSS Personalizado
@@ -83,7 +87,10 @@ details.queued_for_refresh=En cola para actualizar
details.feed_url=URL da fonte details.feed_url=URL da fonte
details.generate_api_key_first=Antes debes xerar unha chave API no teu perfil. details.generate_api_key_first=Antes debes xerar unha chave API no teu perfil.
details.unsubscribe=Rematar suscripción 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.category_details=Detalles da categoría
details.tag_details=Tag details ####### Needs translation
details.parent_category=Categoría principal details.parent_category=Categoría principal
profile.user_name=Nome de usuario 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.generate_new_api_key_info=Ao cambiar o contrasinal xerarase unha nova chave API
profile.opml_export=Exportación de OPML profile.opml_export=Exportación de OPML
profile.delete_account=Eliminar conta 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.rest_api=REST API
about.keyboard_shortcuts=Atallos de teclado about.keyboard_shortcuts=Atallos de teclado

View File

@@ -6,6 +6,7 @@ global.download=جیرأکش
global.link=خال global.link=خال
global.bookmark=بوکمارک global.bookmark=بوکمارک
global.close=دَوَستن global.close=دَوَستن
global.tags=Tags ####### Needs translation
tree.subscribe=مشترک ببید tree.subscribe=مشترک ببید
tree.import=درینأدأن tree.import=درینأدأن
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=تاریخˇ سر دچئن
toolbar.titles_only=خالی تیتران toolbar.titles_only=خالی تیتران
toolbar.expanded_view=واشاده نما toolbar.expanded_view=واشاده نما
toolbar.mark_all_as_read=همه‌ته مطالبه چاکون بخانده 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_day=یک روز پیشترˇ مطالب
toolbar.mark_all_older_week=یک هفته پیشترˇ مطالب toolbar.mark_all_older_week=یک هفته پیشترˇ مطالب
toolbar.mark_all_older_two_weeks=چن هفته پیشترˇ مطالب toolbar.mark_all_older_two_weeks=چن هفته پیشترˇ مطالب
@@ -66,6 +68,8 @@ settings.general.show_unread=تنها خوراک‌ها و دسته‌های ر
settings.general.social_buttons=نشان‌دادن دکمه‌های اشتراک‌گذاری در شبکه‌های اجتماعی settings.general.social_buttons=نشان‌دادن دکمه‌های اشتراک‌گذاری در شبکه‌های اجتماعی
settings.general.scroll_marks=در نمای گسترش‌یافته، لغزیدن بر روی مطالب به‌عنوان نشانه‌گذاری به‌عنوان خوانده‌شده در نظر گرفته‌شوند. settings.general.scroll_marks=در نمای گسترش‌یافته، لغزیدن بر روی مطالب به‌عنوان نشانه‌گذاری به‌عنوان خوانده‌شده در نظر گرفته‌شوند.
settings.appearance=ظاهر 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.theme=پوسته
settings.submit_your_theme=شیمه پوستهٰ اوسه کونید settings.submit_your_theme=شیمه پوستهٰ اوسه کونید
settings.custom_css=سی‌اس‌اس شخصی‌سازی‌شده settings.custom_css=سی‌اس‌اس شخصی‌سازی‌شده
@@ -83,7 +87,10 @@ details.queued_for_refresh=منتظر برای بروزرسانی
details.feed_url=نشانی خوراک details.feed_url=نشانی خوراک
details.generate_api_key_first=ابتدا یک کلید API در نمایهٔ خود ایجاد کنید. details.generate_api_key_first=ابتدا یک کلید API در نمایهٔ خود ایجاد کنید.
details.unsubscribe=لغو اشتراک 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.category_details=جرگه جزئیات
details.tag_details=Tag details ####### Needs translation
details.parent_category=پئرˇ جرگه details.parent_category=پئرˇ جرگه
profile.user_name=کاربری نام profile.user_name=کاربری نام
@@ -98,6 +105,7 @@ profile.generate_new_api_key=تازه کلید چاگودن API
profile.generate_new_api_key_info=رمزه عوضأگودن API کلیده چاکونه. profile.generate_new_api_key_info=رمزه عوضأگودن API کلیده چاکونه.
profile.opml_export=برینأدأن OPML profile.opml_export=برینأدأن OPML
profile.delete_account=کاربری حسابه پاکودن profile.delete_account=کاربری حسابه پاکودن
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API about.rest_api=REST API
about.keyboard_shortcuts=وئر زئنˇ کلیدان about.keyboard_shortcuts=وئر زئنˇ کلیدان

View File

@@ -1,149 +1,157 @@
global.save=Mentés global.save=Mentés
global.cancel=Mégsem global.cancel=Mégsem
global.delete=Törlés global.delete=Törlés
global.required=Szükséges global.required=Szükséges
global.download=Letöltés global.download=Letöltés
global.link=Link global.link=Link
global.bookmark=Könyvjelző global.bookmark=Könyvjelző
global.close=Bezár global.close=Bezár
global.tags=Címkék
tree.subscribe=Feliratkozás
tree.import=Importálás tree.subscribe=Feliratkozás
tree.new_category=Új kategória tree.import=Importálás
tree.all=Összes tree.new_category=Új kategória
tree.starred=Csillagozott tree.all=Összes
tree.starred=Csillagozott
subscribe.feed_url=Hírcsatorna URL
subscribe.feed_name=Hírcsatorna neve subscribe.feed_url=Hírcsatorna URL
subscribe.category=Kategória 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_reader_prefix=Engedd meg, hogy importáljuk a hírcsatornáidat a
import.google_download=Alternatívaként, feltöltheti a subscriptions.xml fájlt. import.google_reader_suffix= fiókjából.
import.google_download_link=Letöltheti innen. import.google_download=Alternatívaként, feltöltheti a subscriptions.xml fájlt.
import.xml_file=OPML Fájl import.google_download_link=Letöltheti innen.
import.xml_file=OPML Fájl
new_category.name=Név
new_category.parent=Szülő new_category.name=Név
new_category.parent=Szülő
toolbar.unread=Olvasatlan
toolbar.all=Összes toolbar.unread=Olvasatlan
toolbar.previous_entry=Előző elem toolbar.all=Összes
toolbar.next_entry=Következő elem toolbar.previous_entry=Előző elem
toolbar.refresh=Frissítés toolbar.next_entry=Következő elem
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation toolbar.refresh=Frissítés
toolbar.sort_by_asc_desc=Rendezés időrend szerint toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
toolbar.titles_only=Csak cím toolbar.sort_by_asc_desc=Rendezés időrend szerint
toolbar.expanded_view=Részletes nézet toolbar.titles_only=Csak cím
toolbar.mark_all_as_read=Az összes megjelölése olvasottként toolbar.expanded_view=Részletes nézet
toolbar.mark_all_older_day=Régebbiek, mint egy nap toolbar.mark_all_as_read=Az összes megjelölése olvasottként
toolbar.mark_all_older_week=Régebbiek, mint egy hét toolbar.mark_all_older_12_hours=Régebbiek 12 óránál
toolbar.mark_all_older_two_weeks=Régebbiek, mint két hét toolbar.mark_all_older_day=Régebbiek, mint egy nap
toolbar.settings=Beállítások toolbar.mark_all_older_week=Régebbiek, mint egy hét
toolbar.profile=Profil toolbar.mark_all_older_two_weeks=Régebbiek, mint két hét
toolbar.admin=Admin toolbar.settings=Beállítások
toolbar.about=Névjegy toolbar.profile=Profil
toolbar.logout=Kilépés toolbar.admin=Admin
toolbar.donate=Anyagi támogatás toolbar.about=Névjegy
toolbar.logout=Kilépés
view.entry_source=from ####### Needs translation toolbar.donate=Anyagi támogatás
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.entry_source=forrás
view.keep_unread=Megtartása olvasatlanként view.entry_author=szerző
view.no_unread_items=nincsen olvasatlan eleme. view.error_while_loading_feed=Hiba történt ennek a hírcsatornának a betöltésekor
view.mark_up_to_here=Mark as read up to here ####### Needs translation view.keep_unread=Megtartása olvasatlanként
view.search_for=searching for: ####### Needs translation view.no_unread_items=nincsen olvasatlan eleme.
view.no_search_results=No match found for the requested keywords ####### Needs translation view.mark_up_to_here=Megjelölés olvasottnak eddig
view.search_for=keresés erre:
feedsearch.hint=Keressen a hírcsatornák között... view.no_search_results=Nem található semmi erre a keresett szóra
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: 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.
settings.general=Általános feedsearch.result_prefix=Az ön feliratkozásai:
settings.general.language=Nyelv
settings.general.language.contribute=Segítsen a fordításban settings.general=Általános
settings.general.show_unread=Mutassa azokat a hírcsatornákat és kategóriákat amelyekben nincsen olvasatlan bejegyzés settings.general.language=Nyelv
settings.general.social_buttons=Mutassa a közösségi oldalak megosztás gombjait settings.general.language.contribute=Segítsen a fordításban
settings.general.scroll_marks=Kiterjesztett nézetben, görgetéssel olvasottként jelöli meg a bejegyzést settings.general.show_unread=Mutassa azokat a hírcsatornákat és kategóriákat amelyekben nincsen olvasatlan bejegyzés
settings.appearance=Megjelenés settings.general.social_buttons=Mutassa a közösségi oldalak megosztás gombjait
settings.theme=Téma settings.general.scroll_marks=Kiterjesztett nézetben, görgetéssel olvasottként jelöli meg a bejegyzést
settings.submit_your_theme=Küldje el a témáját settings.appearance=Megjelenés
settings.custom_css=Saját CSS 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
details.feed_details=Hírcsatorna részletei settings.theme=Téma
details.url=URL settings.submit_your_theme=Küldje el a témáját
details.website=Website ####### Needs translation settings.custom_css=Saját CSS
details.name=Név
details.category=Kategória details.feed_details=Hírcsatorna részletei
details.position=Position ####### Needs translation details.url=URL
details.last_refresh=Utolsó frissítés details.website=Weboldal
details.message=Last refresh message ####### Needs translation details.name=Név
details.next_refresh=Következő frissítés details.category=Kategória
details.queued_for_refresh=Frissítésre vár details.position=Pozició
details.feed_url=Hírcsatorna URL details.last_refresh=Utolsó frissítés
details.generate_api_key_first=A profiljában először egy API kulcsot kell generálnia. details.message=Utolsó frissítési üzenet
details.unsubscribe=Leiratkozás details.next_refresh=Következő frissítés
details.category_details=Kategória részletei details.queued_for_refresh=Frissítésre vár
details.parent_category=Szülő kategória details.feed_url=Hírcsatorna URL
details.generate_api_key_first=A profiljában először egy API kulcsot kell generálnia.
profile.user_name=Felhasználói név details.unsubscribe=Leiratkozás
profile.email=E-mail details.unsubscribe_confirmation=Biztos, hogy le akar iratkozni errről a csatornáról?
profile.change_password=Jelszó megváltoztatás details.delete_category_confirmation=Biztos, hog törölni akarja ezt a kategóriát?
profile.confirm_password=Jelszó megerősítése details.category_details=Kategória részletei
profile.minimum_6_chars=Legalább 8 karakter details.tag_details=Címke részletei
profile.passwords_do_not_match=A jelszavak nem egyeznek details.parent_category=Szülő kategória
profile.api_key=API kulcs
profile.api_key_not_generated=Még nincsen generálva profile.user_name=Felhasználói név
profile.generate_new_api_key=Új API kulcs generálása profile.email=E-mail
profile.generate_new_api_key_info=A jelszó megváltoztatása új API kulcsot generál profile.change_password=Jelszó megváltoztatás
profile.opml_export=OPML exportálása profile.confirm_password=Jelszó megerősítése
profile.delete_account=Fiók törlése profile.minimum_6_chars=Legalább 8 karakter
profile.passwords_do_not_match=A jelszavak nem egyeznek
about.rest_api=REST API profile.api_key=API kulcs
about.keyboard_shortcuts=Gyorsbillentyűk profile.api_key_not_generated=Még nincsen generálva
about.version=CommaFeed version ####### Needs translation profile.generate_new_api_key=Új API kulcs generálása
about.line1_prefix=A CommaFeed egy nyílt forrású projekt. A forrás megtalálható a profile.generate_new_api_key_info=A jelszó megváltoztatása új API kulcsot generál
about.line1_suffix=oldalán. profile.opml_export=OPML exportálása
about.line2_prefix=Ha hibába ütközik, kérjük jelentse azt a profile.delete_account=Fiók törlése
about.line2_suffix=projekt oldalán. profile.delete_account_confirmation=Törli a fiókját? Innen már nincs visszatérés!
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.rest_api=REST API
about.goodies=Hasznos dolgok about.keyboard_shortcuts=Gyorsbillentyűk
about.goodies.android_app=Android app ####### Needs translation about.version=CommaFeed verzió
about.goodies.subscribe_url=Feliratkozás az URL-re about.line1_prefix=A CommaFeed egy nyílt forrású projekt. A forrás megtalálható a
about.goodies.chrome_extension=Chrome bővítmény about.line1_suffix=oldalán.
about.goodies.firefox_extension=Firefox kiterjesztés about.line2_prefix=Ha hibába ütközik, kérjük jelentse azt a
about.goodies.opera_extension=Opera kiterjesztés about.line2_suffix=projekt oldalán.
about.goodies.subscribe_bookmarklet=Feliratkozás bookmarklet hozzáadása (klikkeléssel) 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.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation about.line4=Akik jobban szeretnék az oldalt bitcon-nal támogatni, itt a cím
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation about.goodies=Hasznos dolgok
about.goodies.next_unread_bookmarklet=Következő olvasatlan elem bookmarklet (húzza fel a könyvjelzősávba) about.goodies.android_app=Android alkalmazás
about.translation=Fordítás about.goodies.subscribe_url=Feliratkozás az URL-re
about.translation.message=Segítségét kérjük a CommaFeed fordításához. about.goodies.chrome_extension=Chrome bővítmény
about.translation.link=Nézze meg, hogyan tud segíteni ebben. about.goodies.firefox_extension=Firefox kiterjesztés
about.announcements=Bejelentések about.goodies.opera_extension=Opera kiterjesztés
about.rest_api.line1=A CommaFeed a JAX-RS-re és az AngularJS-re épül. Ezért a RESTA API elérhető. about.goodies.subscribe_bookmarklet=Feliratkozás bookmarklet hozzáadása (klikkeléssel)
about.rest_api.link_to_documentation=Link a dokumentációhoz. about.goodies.subscribe_bookmarklet_asc=Régebbiek először
about.goodies.subscribe_bookmarklet_desc=Újak először
about.shortcuts.mouse_middleclick=középső egérgomb about.goodies.next_unread_bookmarklet=Következő olvasatlan elem bookmarklet (húzza fel a könyvjelzősávba)
about.shortcuts.open_next_entry=következő hír megnyitása about.translation=Fordítás
about.shortcuts.open_previous_entry=előző hír megnyitása about.translation.message=Segítségét kérjük a CommaFeed fordításához.
about.shortcuts.spacebar=space/shift+space ####### Needs translation about.translation.link=Nézze meg, hogyan tud segíteni ebben.
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation about.announcements=Bejelentések
about.shortcuts.focus_next_entry=megnyitás nélkül fókuszál a övetkező elemre about.rest_api.line1=A CommaFeed a JAX-RS-re és az AngularJS-re épül. Ezért a RESTA API elérhető.
about.shortcuts.focus_previous_entry=megnyitás nélkül fókuszál az előző elemre about.rest_api.link_to_documentation=Link a dokumentációhoz.
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.mouse_middleclick=középső egérgomb
about.shortcuts.open_close_current_entry=a jelenlegi elem megnyitása/bezárása about.shortcuts.open_next_entry=következő hír megnyitása
about.shortcuts.open_current_entry_in_new_window=a jelenlegi elem megnyitása új ablakban about.shortcuts.open_previous_entry=előző hír megnyitása
about.shortcuts.open_current_entry_in_new_window_background=a jelenlegi elem megnyitása a háttérben, új ablakban about.shortcuts.spacebar=szóköz/shift+szóköz
about.shortcuts.star_unstar=hírelem csillagozása about.shortcuts.move_page_down_up=fel/le lépkedhet az oldalon
about.shortcuts.mark_current_entry=elem megjelölése olvasottként about.shortcuts.focus_next_entry=megnyitás nélkül fókuszál a övetkező elemre
about.shortcuts.mark_all_as_read=az összes elem megjelölése olvasottként about.shortcuts.focus_previous_entry=megnyitás nélkül fókuszál az előző elemre
about.shortcuts.open_in_new_tab_mark_as_read=elem megnyitása új fülön és megjelölése olvasottként about.shortcuts.open_next_feed=a következő hírcsatorna vagy kategória megnyitása
about.shortcuts.fullscreen=toggle full screen mode ####### Needs translation about.shortcuts.open_previous_feed=az előző hírcsatorna vagy kategória megnyitása
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation about.shortcuts.open_close_current_entry=a jelenlegi elem megnyitása/bezárása
about.shortcuts.go_to_all=go to the All view ####### Needs translation about.shortcuts.open_current_entry_in_new_window=a jelenlegi elem megnyitása új ablakban
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation about.shortcuts.open_current_entry_in_new_window_background=a jelenlegi elem megnyitása a háttérben, új ablakban
about.shortcuts.feed_search=név szerinti keresés a hírcsatornák között 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.link=Link
global.bookmark=Segnalibro global.bookmark=Segnalibro
global.close=Chiudi global.close=Chiudi
global.tags=Tags ####### Needs translation
tree.subscribe=Iscriviti tree.subscribe=Iscriviti
tree.import=Importa tree.import=Importa
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Sort by date asc/desc
toolbar.titles_only=Solo titoli toolbar.titles_only=Solo titoli
toolbar.expanded_view=Espandi toolbar.expanded_view=Espandi
toolbar.mark_all_as_read=Contrassegna tutto come già letto 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_day=Elementi più vecchi di un giorno
toolbar.mark_all_older_week=Elementi più vecchi di una settimana toolbar.mark_all_older_week=Elementi più vecchi di una settimana
toolbar.mark_all_older_two_weeks=Elementi più vecchi di due settimane 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.social_buttons=Visualizza i social button
settings.general.scroll_marks=Marca come letto quando scorri settings.general.scroll_marks=Marca come letto quando scorri
settings.appearance=Appearance ####### Needs translation 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.theme=Tema
settings.submit_your_theme=Sottoponi il tuo tema settings.submit_your_theme=Sottoponi il tuo tema
settings.custom_css=Css modificato settings.custom_css=Css modificato
@@ -83,7 +87,10 @@ details.queued_for_refresh=In attesa per l'aggiornamento
details.feed_url=Feed URL details.feed_url=Feed URL
details.generate_api_key_first=Generate an API key in your profile first. details.generate_api_key_first=Generate an API key in your profile first.
details.unsubscribe=Annulla l"'"iscrizione 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.category_details=Dettagli categoria
details.tag_details=Tag details ####### Needs translation
details.parent_category=Parent category details.parent_category=Parent category
profile.user_name=User name 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.generate_new_api_key_info=Cambiando la password sarà generata una nuova chiave API
profile.opml_export=Esporta OPML profile.opml_export=Esporta OPML
profile.delete_account=Elimina account 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.rest_api=REST API
about.keyboard_shortcuts=Scorciatoie da tastiera 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.link=Link ####### Needs translation
global.bookmark=Bookmark ####### Needs translation global.bookmark=Bookmark ####### Needs translation
global.close=Close ####### Needs translation global.close=Close ####### Needs translation
global.tags=Tags ####### Needs translation
tree.subscribe=구독 tree.subscribe=구독
tree.import=임포트 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.titles_only=Titles only ####### Needs translation
toolbar.expanded_view=Expanded view ####### Needs translation toolbar.expanded_view=Expanded view ####### Needs translation
toolbar.mark_all_as_read=읽음표시 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_day=Items older than a day ####### Needs translation
toolbar.mark_all_older_week=Items older than a week ####### 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 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.social_buttons=소셜미디아 버튼들 보여주기
settings.general.scroll_marks=Expanded View에서 스크롤하면 항목들을 읽음으로 저장하기 settings.general.scroll_marks=Expanded View에서 스크롤하면 항목들을 읽음으로 저장하기
settings.appearance=Appearance ####### Needs translation 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.theme=Theme ####### Needs translation
settings.submit_your_theme=Submit your theme ####### Needs translation settings.submit_your_theme=Submit your theme ####### Needs translation
settings.custom_css=커스톰 CSS settings.custom_css=커스톰 CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Queued for refresh ####### Needs translation
details.feed_url=피드 유알엘 details.feed_url=피드 유알엘
details.generate_api_key_first=당신의 프로필을 위해 API Key를 먼저 생성하세요. details.generate_api_key_first=당신의 프로필을 위해 API Key를 먼저 생성하세요.
details.unsubscribe=주소 삭제 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.category_details=카테고리 세부
details.tag_details=Tag details ####### Needs translation
details.parent_category=부모 카테고리 details.parent_category=부모 카테고리
profile.user_name=사용자 이름 profile.user_name=사용자 이름
@@ -98,6 +105,7 @@ profile.generate_new_api_key=API Key 생성하기
profile.generate_new_api_key_info=비밀번호를 변경하면 새로운 API Key가 생성됩니다. profile.generate_new_api_key_info=비밀번호를 변경하면 새로운 API Key가 생성됩니다.
profile.opml_export=OPML export ####### Needs translation profile.opml_export=OPML export ####### Needs translation
profile.delete_account=프로필삭제 profile.delete_account=프로필삭제
profile.delete_account_confirmation=Delete your acount? There's no turning back! ####### Needs translation
about.rest_api=REST API about.rest_api=REST API
about.keyboard_shortcuts=단축기 about.keyboard_shortcuts=단축기

View File

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

View File

@@ -6,6 +6,7 @@ global.download=Muat turun
global.link=Pautan global.link=Pautan
global.bookmark=Bookmark global.bookmark=Bookmark
global.close=Tutup global.close=Tutup
global.tags=Tags ####### Needs translation
tree.subscribe=Langgan tree.subscribe=Langgan
tree.import=Import tree.import=Import
@@ -36,6 +37,7 @@ toolbar.sort_by_asc_desc=Aturkan mengikut tarikh (baru/lama)
toolbar.titles_only=Tajuk sahaja toolbar.titles_only=Tajuk sahaja
toolbar.expanded_view=Wide view toolbar.expanded_view=Wide view
toolbar.mark_all_as_read=Tanda kesemuanya telah dibaca 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_day=Lebih lama daripada sehari
toolbar.mark_all_older_week=Lebih lama daripada seminggu toolbar.mark_all_older_week=Lebih lama daripada seminggu
toolbar.mark_all_older_two_weeks=Lebih lama daripada dua minggu 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.social_buttons=Tunjuk social sharing
settings.general.scroll_marks=Dalam wide view, tanda item dibaca ketika scrolling settings.general.scroll_marks=Dalam wide view, tanda item dibaca ketika scrolling
settings.appearance=Rupa 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.theme=Tema
settings.submit_your_theme=Muat naik tema anda settings.submit_your_theme=Muat naik tema anda
settings.custom_css=Custom CSS settings.custom_css=Custom CSS
@@ -83,7 +87,10 @@ details.queued_for_refresh=Diaturkan untuk refresh
details.feed_url=URL Feed details.feed_url=URL Feed
details.generate_api_key_first=Janakan API key dalam profil anda dahulu. details.generate_api_key_first=Janakan API key dalam profil anda dahulu.
details.unsubscribe=Hentikan langganan 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.category_details=Butir-butir kategori
details.tag_details=Tag details ####### Needs translation
details.parent_category=Kategori induk details.parent_category=Kategori induk
profile.user_name=User name 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.generate_new_api_key_info=Pertukaran kata laluan akan menjanakan API key yang baru
profile.opml_export=Export OPML profile.opml_export=Export OPML
profile.delete_account=Padam akaun 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.rest_api=REST API
about.keyboard_shortcuts=Pintasan papan kekunci about.keyboard_shortcuts=Pintasan papan kekunci

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