Compare commits

...

40 Commits
1.2.0 ... 1.3.0

Author SHA1 Message Date
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
54 changed files with 860 additions and 742 deletions

View File

@@ -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 nginix 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
----------------- -----------------

21
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.3.0</version>
<packaging>war</packaging> <packaging>war</packaging>
<name>CommaFeed</name> <name>CommaFeed</name>
@@ -336,23 +336,23 @@
<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.10.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.10.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.10.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.10.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ro.isdc.wro4j</groupId> <groupId>ro.isdc.wro4j</groupId>
@@ -372,6 +372,17 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-json</artifactId>
<version>3.0.1</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>

View File

@@ -19,12 +19,14 @@ import org.apache.commons.lang.StringUtils;
import org.apache.http.Header; import org.apache.http.Header;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
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.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.HttpClient;
import org.apache.http.client.HttpResponseException; import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.CookiePolicy; import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.params.HttpClientParams; import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.ClientConnectionManager;
@@ -39,6 +41,9 @@ import org.apache.http.impl.client.SystemDefaultHttpClient;
import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams; import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams; import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils; import org.apache.http.util.EntityUtils;
/** /**
@@ -93,6 +98,8 @@ public class HttpGetter {
HttpClient client = newClient(timeout); HttpClient client = newClient(timeout);
try { try {
HttpGet httpget = new HttpGet(url); HttpGet httpget = new HttpGet(url);
HttpContext context = new BasicHttpContext();
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);
@@ -107,7 +114,7 @@ public class HttpGetter {
HttpResponse response = null; 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 +130,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,10 +150,12 @@ public class HttpGetter {
contentType = entity.getContentType().getValue(); contentType = entity.getContentType().getValue();
} }
} }
HttpUriRequest req = (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
HttpHost host = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
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(); client.getConnectionManager().shutdown();
} }
@@ -161,13 +169,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,6 +200,9 @@ public class HttpGetter {
return duration; return duration;
} }
public String getUrlAfterRedirect() {
return urlAfterRedirect;
}
} }
public static HttpClient newClient(int timeout) { public static HttpClient newClient(int timeout) {

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,27 +4,26 @@ 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, runs every day at midnight

View File

@@ -290,10 +290,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

@@ -77,6 +77,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

@@ -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,8 @@ 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.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 +42,7 @@ public class FeedRefreshTaskGiver {
ApplicationSettingsService applicationSettingsService; ApplicationSettingsService applicationSettingsService;
@Inject @Inject
MetricsBean metricsBean; MetricRegistry metrics;
@Inject @Inject
FeedRefreshWorker worker; FeedRefreshWorker worker;
@@ -54,10 +55,17 @@ 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"));
} }
@PreDestroy @PreDestroy
@@ -88,11 +96,11 @@ public class FeedRefreshTaskGiver {
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,6 +146,7 @@ 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() {
refill.mark();
int count = Math.min(100, 3 * backgroundThreads); int count = Math.min(100, 3 * backgroundThreads);
// first, get feeds that are up to refresh from the database // first, get feeds that are up to refresh from the database

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

@@ -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,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

@@ -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;

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

@@ -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,15 @@ 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.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 {
@Inject @Inject
FeedDAO feedDAO; FeedDAO feedDAO;
@@ -99,9 +100,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, 100);
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

@@ -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

@@ -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

@@ -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) {

View File

@@ -17,9 +17,7 @@ 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.FeedDAO.DuplicateMode;
import com.commafeed.backend.dao.UserDAO; import com.commafeed.backend.dao.UserDAO;
@@ -33,14 +31,17 @@ 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.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.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.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@@ -70,10 +71,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,21 +227,10 @@ 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());
return Response.ok(map).build();
} }
@Path("/cleanup/feeds") @Path("/cleanup/feeds")

View File

@@ -170,8 +170,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;
@@ -200,7 +201,11 @@ public class CategoryREST extends AbstractREST {
int offset = 0; int offset = 0;
int limit = 20; int limit = 20;
Entries entries = (Entries) getCategoryEntries(id, readType, null, offset, limit, order, null, false, null).getEntity(); Response response = getCategoryEntries(id, readType, null, offset, limit, order, null, false, null);
if (response.getStatus() != Status.OK.getStatusCode()) {
return response;
}
Entries entries = (Entries) response.getEntity();
SyndFeed feed = new SyndFeedImpl(); SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0"); feed.setFeedType("rss_2.0");

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;
@@ -181,6 +181,8 @@ 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());
@@ -202,7 +204,11 @@ public class FeedREST extends AbstractREST {
int offset = 0; int offset = 0;
int limit = 20; int limit = 20;
Entries entries = (Entries) getFeedEntries(id, readType, null, offset, limit, order, null, false).getEntity(); Response response = getFeedEntries(id, readType, null, offset, limit, order, null, false);
if (response.getStatus() != Status.OK.getStatusCode()) {
return response;
}
Entries entries = (Entries) response.getEntity();
SyndFeed feed = new SyndFeedImpl(); SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0"); feed.setFeedType("rss_2.0");
@@ -235,7 +241,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) {

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;

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

@@ -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

@@ -6,5 +6,6 @@
<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" />
</databaseChangeLog> </databaseChangeLog>

View File

@@ -17,10 +17,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 +31,14 @@ 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_day=Eitemau hyn na diwrnod
toolbar.mark_all_older_week=Eitemau sy'n hyn nag wythnos toolbar.mark_all_older_week=Eitemau hyn nag wythnos
toolbar.mark_all_older_two_weeks=Eitemau sy'n hyn na phythefnos 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,42 +46,42 @@ 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.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.category_details=Manylion categori details.category_details=Manylion categori
details.parent_category=Categori rhiant details.parent_category=Categori rhiant
@@ -91,59 +91,59 @@ 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
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

@@ -20,7 +20,7 @@ subscribe.category=Kategória
import.google_reader_prefix=Importujte si RSS zdroje s vášho import.google_reader_prefix=Importujte si RSS zdroje s vášho
import.google_reader_suffix= účtu. import.google_reader_suffix= účtu.
import.google_download=Alternatívne, môžte nahrať váš subscriptions.xml súbor import.google_download=Alternatívne, môžte nahrať váš subscriptions.xml súbor
import.google_download_link=Download it from here. import.google_download_link=Stiahnuť to môžete s lokácie.
import.xml_file=OPML súbor import.xml_file=OPML súbor
new_category.name=Názov new_category.name=Názov
@@ -31,7 +31,7 @@ toolbar.all=Všetky
toolbar.previous_entry=Predchádzajúca položka toolbar.previous_entry=Predchádzajúca položka
toolbar.next_entry=Nasledujúca položka toolbar.next_entry=Nasledujúca položka
toolbar.refresh=Obnoviť toolbar.refresh=Obnoviť
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation toolbar.refresh_all=Vynútené obnovenie všetkých položiek
toolbar.sort_by_asc_desc=Zoradiť podľa najnovšieho/najstaršieho toolbar.sort_by_asc_desc=Zoradiť podľa najnovšieho/najstaršieho
toolbar.titles_only=Náhľad titulkov toolbar.titles_only=Náhľad titulkov
toolbar.expanded_view=Rozšírený náhľad toolbar.expanded_view=Rozšírený náhľad
@@ -52,8 +52,8 @@ view.error_while_loading_feed=Počas načítavania sa vyskytla chyba
view.keep_unread=Ponechať ako neprečítané view.keep_unread=Ponechať ako neprečítané
view.no_unread_items=nemá žiadne neprečítané položky. view.no_unread_items=nemá žiadne neprečítané položky.
view.mark_up_to_here=Až potiaľto označiť položky ako prečítané view.mark_up_to_here=Až potiaľto označiť položky ako prečítané
view.search_for=searching for: ####### Needs translation view.search_for=Hľadaný výraz:
view.no_search_results=No match found for the requested keywords ####### Needs translation view.no_search_results=Nenašla sa žiadna zhoda pre hľadaný výraz.
feedsearch.hint=Zadajte názov odoberania... feedsearch.hint=Zadajte názov odoberania...
feedsearch.help=Použite klávesu enter pre výber a smerové klávesy pre navigáciu. feedsearch.help=Použite klávesu enter pre výber a smerové klávesy pre navigáciu.
@@ -61,14 +61,14 @@ feedsearch.result_prefix=Vaše odoberania:
settings.general=Všeobecné settings.general=Všeobecné
settings.general.language=Jazyk settings.general.language=Jazyk
settings.general.language.contribute=Pomôžte s prekladom settings.general.language.contribute=Zapojte sa do prekladu
settings.general.show_unread=Zobraziť príspevky a kategórie bez neprečítaných položiek settings.general.show_unread=Zobraziť príspevky a kategórie bez neprečítaných položiek
settings.general.social_buttons=Zobraziť možnosti zdieľania settings.general.social_buttons=Zobraziť možnosti zdieľania
settings.general.scroll_marks=Scrollovanie v rozšírenom náhľade označí položky ako prečítané settings.general.scroll_marks=Scrollovanie v rozšírenom náhľade označí položky ako prečítané
settings.appearance=Vzhľad settings.appearance=Vzhľad
settings.theme=Motív settings.theme=Motív
settings.submit_your_theme=Nahrať vlastný motív settings.submit_your_theme=Nahrať vlastný motív vzhľadu
settings.custom_css=Vlastný motív (CSS) settings.custom_css=Vlastný motív vzhľadu (CSS)
details.feed_details=Detaily odoberania details.feed_details=Detaily odoberania
details.url=URL odkaz details.url=URL odkaz
@@ -76,10 +76,10 @@ details.website=Web stránka
details.name=Názov details.name=Názov
details.category=Kategória details.category=Kategória
details.position=Pozícia details.position=Pozícia
details.last_refresh=Posledné obnovenie details.last_refresh=Predchádzajúce obnovenie
details.message=Last refresh message ####### Needs translation details.message=Predchádzajúca správa obnovenia
details.next_refresh=Nasledujúce obnovenie details.next_refresh=Nasledujúce obnovenie
details.queued_for_refresh=Vo fronte na obnovu details.queued_for_refresh=Vo fronte
details.feed_url=URL RSS zdroja details.feed_url=URL RSS zdroja
details.generate_api_key_first=Vygenerujte si API kľúč vo vašom profile. details.generate_api_key_first=Vygenerujte si API kľúč vo vašom profile.
details.unsubscribe=Zrušiť odoberanie. details.unsubscribe=Zrušiť odoberanie.
@@ -95,13 +95,13 @@ profile.passwords_do_not_match=Heslá sa nezhodujú
profile.api_key=API kľúč profile.api_key=API kľúč
profile.api_key_not_generated=Nie je vygenerovaný profile.api_key_not_generated=Nie je vygenerovaný
profile.generate_new_api_key=Vygenerovať nový API kľúč profile.generate_new_api_key=Vygenerovať nový API kľúč
profile.generate_new_api_key_info=Zmena hesla vygeneruje nový API kľúč profile.generate_new_api_key_info=Zmenou hesla vygenerujete nový API kľúč
profile.opml_export=exportovať do formátu OPML profile.opml_export=exportovať do formátu OPML
profile.delete_account=Odstrániť účet profile.delete_account=Odstrániť účet
about.rest_api=REST API about.rest_api=REST API
about.keyboard_shortcuts=Klávesové skratky about.keyboard_shortcuts=Klávesové skratky
about.version=CommaFeed version ####### Needs translation about.version=CommaFeed verzia
about.line1_prefix=CommaFeed je open source projekt. Zdrojový kód je dostupný na about.line1_prefix=CommaFeed je open source projekt. Zdrojový kód je dostupný na
about.line1_suffix=. about.line1_suffix=.
about.line2_prefix=V prípade, že narazíte na problém, ohláste ho prosím na stránkach about.line2_prefix=V prípade, že narazíte na problém, ohláste ho prosím na stránkach
@@ -114,13 +114,13 @@ about.goodies.subscribe_url=URL
about.goodies.chrome_extension=Rozšírenie pre prehliadač Chrome about.goodies.chrome_extension=Rozšírenie pre prehliadač Chrome
about.goodies.firefox_extension=Rozšírenie pre prehliadač Firefox about.goodies.firefox_extension=Rozšírenie pre prehliadač Firefox
about.goodies.opera_extension=Rozšírenie pre prehliadač Opera about.goodies.opera_extension=Rozšírenie pre prehliadač Opera
about.goodies.subscribe_bookmarklet=Bookmarklet(kliknite) about.goodies.subscribe_bookmarklet=Bookmarklet
about.goodies.subscribe_bookmarklet_asc=Zoradiť podľa najstaršieho about.goodies.subscribe_bookmarklet_asc=Zoradiť podľa najstaršieho
about.goodies.subscribe_bookmarklet_desc=Zoradiť podľa najnovšieho about.goodies.subscribe_bookmarklet_desc=Zoradiť podľa najnovšieho
about.goodies.next_unread_bookmarklet=Záložka nasledujúcej neprečítanej položky(pretiahuť k záložkám) about.goodies.next_unread_bookmarklet=Záložka nasledujúcej neprečítanej položky(pretiahuť k záložkám)
about.translation=Preklad about.translation=Preklad
about.translation.message=Pomôžte s prekladom CommaFeed. about.translation.message=Pomôžte s prekladom CommaFeed.
about.translation.link=Zistite ako môžte pomocť s prekladom. about.translation.link=Zistite, ako sa možete zapojiť do prekladu CommaFeed.
about.announcements=Oznámenia about.announcements=Oznámenia
about.rest_api.line1=CommaFeed je postavený na JAX-RS a AngularJS. Dostupná je REST API. about.rest_api.line1=CommaFeed je postavený na JAX-RS a AngularJS. Dostupná je REST API.
about.rest_api.link_to_documentation=Prejsť na dokumentáciu. about.rest_api.link_to_documentation=Prejsť na dokumentáciu.
@@ -128,22 +128,21 @@ about.rest_api.link_to_documentation=Prejsť na dokumentáciu.
about.shortcuts.mouse_middleclick=klik prostredným tlačítkom about.shortcuts.mouse_middleclick=klik prostredným tlačítkom
about.shortcuts.open_next_entry=zobraziť nasledujúcu položku about.shortcuts.open_next_entry=zobraziť nasledujúcu položku
about.shortcuts.open_previous_entry=zobraziť predchádzajúcu položku about.shortcuts.open_previous_entry=zobraziť predchádzajúcu položku
about.shortcuts.spacebar=space/shift+medzerník about.shortcuts.spacebar=medzerník/shift+medzerník
about.shortcuts.move_page_down_up=pohyb smerom dole/hore about.shortcuts.move_page_down_up=pohyb smerom dole/hore
about.shortcuts.focus_next_entry=presun na nasledujúcu položku bez jej zobrazenia about.shortcuts.focus_next_entry=presun na nasledujúcu položku bez jej zobrazenia
about.shortcuts.focus_previous_entry=presun na predchádzajúcu položku bez jej zobrazenia about.shortcuts.focus_previous_entry=presun na predchádzajúcu položku bez jej zobrazenia
about.shortcuts.open_next_feed=presun na nasledujúci RSS zdroj alebo kategóriu about.shortcuts.open_next_feed=presun na nasledujúci RSS zdroj alebo kategóriu
about.shortcuts.open_previous_feed=presun na predchádzajúci RSS zdroj alebo kategóriu about.shortcuts.open_previous_feed=presun na predchádzajúci RSS zdroj alebo kategóriu
about.shortcuts.open_close_current_entry=zobraziť/zavrieť vybranú položku about.shortcuts.open_close_current_entry=zobraziť vybranú položku
about.shortcuts.open_current_entry_in_new_window=zobraziť vybranú položku v novom okne about.shortcuts.open_current_entry_in_new_window=zobraziť vybranú položku v novom okne
about.shortcuts.open_current_entry_in_new_window_background=otvoriť vybranú položku na pozadí about.shortcuts.open_current_entry_in_new_window_background=otvoriť vybranú položku na pozadí
about.shortcuts.star_unstar=označiť vybranú položku ako obľúbenú/neobľúbenú about.shortcuts.star_unstar=označiť vybranú položku ako obľúbená
about.shortcuts.mark_current_entry=označiť vybranú položku ako prečítanú/neprečítanú about.shortcuts.mark_current_entry=označiť vybranú položku ako prečítanú/neprečítanú
about.shortcuts.mark_all_as_read=označiť všetky položky ako prečítané! about.shortcuts.mark_all_as_read=označiť všetky položky ako prečítané!
about.shortcuts.open_in_new_tab_mark_as_read=zobraziť položku na novej karte a označí ju ako prečítanú about.shortcuts.open_in_new_tab_mark_as_read=zobraziť položku na novej karte a označí ju ako prečítanú
about.shortcuts.fullscreen=zapnúť/vypnúť zobrazenie na celú obrazovku about.shortcuts.fullscreen=prepnutie zobrazenia na celú obrazovku
about.shortcuts.font_size=zväčšiť/zmenšiť veľkost písma pre vybranú položku about.shortcuts.font_size=zmeniť veľkosť písma pre vybranú položku
about.shortcuts.go_to_all=go to the All view ####### Needs translation about.shortcuts.go_to_all=zobraziť všetky položky
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation about.shortcuts.go_to_starred=zobraziť obľúbené položiek
about.shortcuts.feed_search=prejsť k odoberanému RSS zdroju vložením jeho názvu about.shortcuts.feed_search=presun na odoberaný RSS zdroj vložením jeho názvu

View File

@@ -1,149 +1,149 @@
global.save=Spara global.save=Spara
global.cancel=Avbryt global.cancel=Avbryt
global.delete=Radera global.delete=Radera
global.required=Obligatorisk global.required=Obligatorisk
global.download=Ladda ned global.download=Ladda ned
global.link=Länka global.link=Länka
global.bookmark=Bokmärk global.bookmark=Bokmärk
global.close=Stäng global.close=Stäng
tree.subscribe=Prenumerera tree.subscribe=Prenumerera
tree.import=Importera tree.import=Importera
tree.new_category=Ny kategori tree.new_category=Ny kategori
tree.all=Alla tree.all=Alla
tree.starred=Stjärnmärkt tree.starred=Stjärnmärkt
subscribe.feed_url=Prenumerationens URL subscribe.feed_url=Prenumerationens URL
subscribe.feed_name=Prenumerationens namn subscribe.feed_name=Prenumerationens namn
subscribe.category=Kategori subscribe.category=Kategori
import.google_reader_prefix=Låt mig importera dina prenumerationer från ditt import.google_reader_prefix=Låt mig importera dina prenumerationer från ditt
import.google_reader_suffix=-konto. import.google_reader_suffix=-konto.
import.google_download=Alternativt, ladda upp din subscriptions.xml-fil. import.google_download=Alternativt, ladda upp din subscriptions.xml-fil.
import.google_download_link=Ladda ned den här. import.google_download_link=Ladda ned den här.
import.xml_file=OPML-fil import.xml_file=OPML-fil
new_category.name=Namn new_category.name=Namn
new_category.parent=Överordnad new_category.parent=Överordnad
toolbar.unread=Oläst toolbar.unread=Oläst
toolbar.all=Alla toolbar.all=Alla
toolbar.previous_entry=Föregående post toolbar.previous_entry=Föregående post
toolbar.next_entry=Nästa post toolbar.next_entry=Nästa post
toolbar.refresh=Uppdatera toolbar.refresh=Uppdatera
toolbar.refresh_all=Tvinga uppdatering av alla prenumerationer toolbar.refresh_all=Tvinga uppdatering av alla prenumerationer
toolbar.sort_by_asc_desc=Sortera efter datum stigande/fallande toolbar.sort_by_asc_desc=Sortera efter datum stigande/fallande
toolbar.titles_only=Endast titlar toolbar.titles_only=Endast titlar
toolbar.expanded_view=Expanderad vy toolbar.expanded_view=Expanderad vy
toolbar.mark_all_as_read=Markera alla som lästa toolbar.mark_all_as_read=Markera alla som lästa
toolbar.mark_all_older_day=Poster äldre än en dag toolbar.mark_all_older_day=Poster äldre än en dag
toolbar.mark_all_older_week=Poster äldre än en vecka toolbar.mark_all_older_week=Poster äldre än en vecka
toolbar.mark_all_older_two_weeks=Poster äldre än två veckor toolbar.mark_all_older_two_weeks=Poster äldre än två veckor
toolbar.settings=Inställningar toolbar.settings=Inställningar
toolbar.profile=Profil toolbar.profile=Profil
toolbar.admin=Administratör toolbar.admin=Administratör
toolbar.about=Om toolbar.about=Om
toolbar.logout=Logga ut toolbar.logout=Logga ut
toolbar.donate=Donera toolbar.donate=Donera
view.entry_source=från view.entry_source=från
view.entry_author=av view.entry_author=av
view.error_while_loading_feed=Fel under laddning av denna prenumeration view.error_while_loading_feed=Fel under laddning av denna prenumeration
view.keep_unread=Håll oläst view.keep_unread=Håll oläst
view.no_unread_items=har inga olästa poster. view.no_unread_items=har inga olästa poster.
view.mark_up_to_here=Markera som läst upp till denna post view.mark_up_to_here=Markera som läst upp till denna post
view.search_for=searching for: ####### Needs translation view.search_for=söker efter:
view.no_search_results=No match found for the requested keywords ####### Needs translation view.no_search_results=Inga resultat för valda nyckelord
feedsearch.hint=Skriv in en prenumeration... feedsearch.hint=Skriv in en prenumeration...
feedsearch.help=Använd retur-tangenten för att välja och piltangenterna för att navigera. feedsearch.help=Använd retur-tangenten för att välja och piltangenterna för att navigera.
feedsearch.result_prefix=Dina prenumerationer: feedsearch.result_prefix=Dina prenumerationer:
settings.general=Allmänt settings.general=Allmänt
settings.general.language=Språk settings.general.language=Språk
settings.general.language.contribute=Bidra med översättningar settings.general.language.contribute=Bidra med översättningar
settings.general.show_unread=Visa prenumerationer och kategorier utan olästa poster settings.general.show_unread=Visa prenumerationer och kategorier utan olästa poster
settings.general.social_buttons=Visa delningsknappar settings.general.social_buttons=Visa delningsknappar
settings.general.scroll_marks=I expanderad vy, markera poster som lästa genom att scrolla förbi dem settings.general.scroll_marks=I expanderad vy, markera poster som lästa genom att scrolla förbi dem
settings.appearance=Utseende settings.appearance=Utseende
settings.theme=Tema settings.theme=Tema
settings.submit_your_theme=Skicka in ditt tema settings.submit_your_theme=Skicka in ditt tema
settings.custom_css=Anpassad CSS settings.custom_css=Anpassad CSS
details.feed_details=Prenumerationsdetaljer details.feed_details=Prenumerationsdetaljer
details.url=URL details.url=URL
details.website=Webbsida details.website=Webbsida
details.name=Namn details.name=Namn
details.category=Kategori details.category=Kategori
details.position=Position details.position=Position
details.last_refresh=Senaste uppdatering details.last_refresh=Senaste uppdatering
details.message=Last refresh message ####### Needs translation details.message=Senaste uppdateringsmeddelande
details.next_refresh=Nästa uppdatering details.next_refresh=Nästa uppdatering
details.queued_for_refresh=I kö för uppdatering details.queued_for_refresh=I kö för uppdatering
details.feed_url=Prenumerationens URL details.feed_url=Prenumerationens URL
details.generate_api_key_first=Skapa en API-nyckel på din profil först. details.generate_api_key_first=Skapa en API-nyckel på din profil först.
details.unsubscribe=Avprenumerera details.unsubscribe=Avprenumerera
details.category_details=Kategoridetaljer details.category_details=Kategoridetaljer
details.parent_category=Överordnad kategori details.parent_category=Överordnad kategori
profile.user_name=Användarnamn profile.user_name=Användarnamn
profile.email=E-mail profile.email=E-mail
profile.change_password=Ändra lösenord profile.change_password=Ändra lösenord
profile.confirm_password=Bekräfta lösenord profile.confirm_password=Bekräfta lösenord
profile.minimum_6_chars=Minst 6 bokstäver profile.minimum_6_chars=Minst 6 bokstäver
profile.passwords_do_not_match=Lösenorden matchar inte profile.passwords_do_not_match=Lösenorden matchar inte
profile.api_key=API-nyckel profile.api_key=API-nyckel
profile.api_key_not_generated=Inte skapad än profile.api_key_not_generated=Inte skapad än
profile.generate_new_api_key=Skapa ny API-nyckel profile.generate_new_api_key=Skapa ny API-nyckel
profile.generate_new_api_key_info=Lösenordsbyte skapar ny API-nyckel profile.generate_new_api_key_info=Lösenordsbyte skapar ny API-nyckel
profile.opml_export=OPML-export profile.opml_export=OPML-export
profile.delete_account=Radera konto profile.delete_account=Radera konto
about.rest_api=REST-API about.rest_api=REST-API
about.keyboard_shortcuts=Tangentbordsgenvägar about.keyboard_shortcuts=Tangentbordsgenvägar
about.version=CommaFeed-version about.version=CommaFeed-version
about.line1_prefix=CommaFeed är ett open-source-projekt. Källan är tillgänglig på about.line1_prefix=CommaFeed är ett open-source-projekt. Källan är tillgänglig på
about.line1_suffix=. about.line1_suffix=.
about.line2_prefix=Om du träffar på ett problem, meddela det på "Issues"-sidan för about.line2_prefix=Om du träffar på ett problem, meddela det på "Issues"-sidan för
about.line2_suffix=-projektet. about.line2_suffix=-projektet.
about.line3=Om du gillar detta projekt, avväg gärna en donation för att stötta utvecklaren och bidra till kostnaderna för att hålla denna site online. about.line3=Om du gillar detta projekt, avväg gärna en donation för att stötta utvecklaren och bidra till kostnaderna för att hålla denna site online.
about.line4=För er som föredrar Bitcoin, här är adressen about.line4=För er som föredrar Bitcoin, här är adressen
about.goodies=Godsaker about.goodies=Godsaker
about.goodies.android_app=Android-app about.goodies.android_app=Android-app
about.goodies.subscribe_url=Prenumerations-URL about.goodies.subscribe_url=Prenumerations-URL
about.goodies.chrome_extension=Chrome-tillägg about.goodies.chrome_extension=Chrome-tillägg
about.goodies.firefox_extension=Firefox-tillägg about.goodies.firefox_extension=Firefox-tillägg
about.goodies.opera_extension=Opera-tillägg about.goodies.opera_extension=Opera-tillägg
about.goodies.subscribe_bookmarklet=Bokmärke för tillägg av prenumeration (klicka) about.goodies.subscribe_bookmarklet=Bokmärke för tillägg av prenumeration (klicka)
about.goodies.subscribe_bookmarklet_asc=äldst först about.goodies.subscribe_bookmarklet_asc=äldst först
about.goodies.subscribe_bookmarklet_desc=nyast först about.goodies.subscribe_bookmarklet_desc=nyast först
about.goodies.next_unread_bookmarklet=Bokmärke för nästa olästa post (dra till bokmärkesfält) about.goodies.next_unread_bookmarklet=Bokmärke för nästa olästa post (dra till bokmärkesfält)
about.translation=Översättning about.translation=Översättning
about.translation.message=Vi behöver din hjälp med att översätta CommaFeed. about.translation.message=Vi behöver din hjälp med att översätta CommaFeed.
about.translation.link=Se hur du kan bidra med översättningar. about.translation.link=Se hur du kan bidra med översättningar.
about.announcements=Notiser about.announcements=Notiser
about.rest_api.line1=CommaFeed är byggt på JAX-RS och AngularJS. Tack vare detta är en REST-API tillgänglig. about.rest_api.line1=CommaFeed är byggt på JAX-RS och AngularJS. Tack vare detta är en REST-API tillgänglig.
about.rest_api.link_to_documentation=Länk till dokumentation. about.rest_api.link_to_documentation=Länk till dokumentation.
about.shortcuts.mouse_middleclick=mitten-musknapp about.shortcuts.mouse_middleclick=mitten-musknapp
about.shortcuts.open_next_entry=öppna nästa post about.shortcuts.open_next_entry=öppna nästa post
about.shortcuts.open_previous_entry=öppna föregående post about.shortcuts.open_previous_entry=öppna föregående post
about.shortcuts.spacebar=mellanslag/shift+mellanslag about.shortcuts.spacebar=mellanslag/shift+mellanslag
about.shortcuts.move_page_down_up=flyttar sidan ned/upp about.shortcuts.move_page_down_up=flyttar sidan ned/upp
about.shortcuts.focus_next_entry=sätt fokus på nästa post utan att öppna about.shortcuts.focus_next_entry=sätt fokus på nästa post utan att öppna
about.shortcuts.focus_previous_entry=sätt fokus på föregående post utan att öppna about.shortcuts.focus_previous_entry=sätt fokus på föregående post utan att öppna
about.shortcuts.open_next_feed=öppna nästa prenumeration eller kategori about.shortcuts.open_next_feed=öppna nästa prenumeration eller kategori
about.shortcuts.open_previous_feed=öppna föregående prenumeration eller kategori about.shortcuts.open_previous_feed=öppna föregående prenumeration eller kategori
about.shortcuts.open_close_current_entry=öppna/stäng nuvarande post about.shortcuts.open_close_current_entry=öppna/stäng nuvarande post
about.shortcuts.open_current_entry_in_new_window=öppna nuvarande post i nytt fönster about.shortcuts.open_current_entry_in_new_window=öppna nuvarande post i nytt fönster
about.shortcuts.open_current_entry_in_new_window_background=öppna nuvarande post i nytt bakgrundsfönster about.shortcuts.open_current_entry_in_new_window_background=öppna nuvarande post i nytt bakgrundsfönster
about.shortcuts.star_unstar=stjärnmärk/ostjärnmärk nuvarande post about.shortcuts.star_unstar=stjärnmärk/ostjärnmärk nuvarande post
about.shortcuts.mark_current_entry=markera nuvarande post läst/oläst about.shortcuts.mark_current_entry=markera nuvarande post läst/oläst
about.shortcuts.mark_all_as_read=markera alla som lästa about.shortcuts.mark_all_as_read=markera alla som lästa
about.shortcuts.open_in_new_tab_mark_as_read=öppna nuvarande post i ny flik och markera som läst about.shortcuts.open_in_new_tab_mark_as_read=öppna nuvarande post i ny flik och markera som läst
about.shortcuts.fullscreen=växla till/från fullskärmsläge about.shortcuts.fullscreen=växla till/från fullskärmsläge
about.shortcuts.font_size=öka/minska teckenstorlek av nuvarande post about.shortcuts.font_size=öka/minska teckenstorlek av nuvarande post
about.shortcuts.go_to_all=se alla poster about.shortcuts.go_to_all=se alla poster
about.shortcuts.go_to_starred=se stjärnmärkta poster about.shortcuts.go_to_starred=se stjärnmärkta poster
about.shortcuts.feed_search=navigera till en prenumeration via prenumerationsnamn about.shortcuts.feed_search=navigera till en prenumeration via prenumerationsnamn

View File

@@ -2,6 +2,7 @@ log4j.logger.com.commafeed=INFO, CONSOLE
log4j.logger.org=WARN, CONSOLE log4j.logger.org=WARN, CONSOLE
log4j.logger.ro=WARN, CONSOLE log4j.logger.ro=WARN, CONSOLE
log4j.logger.com.wordnik=FATAL, CONSOLE log4j.logger.com.wordnik=FATAL, CONSOLE
log4j.logger.com.codahale=WARN, CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout

View File

@@ -665,15 +665,17 @@ module.controller('FeedListCtrl', [
'$route', '$route',
'$state', '$state',
'$window', '$window',
'$timeout',
'$location', '$location',
'EntryService', 'EntryService',
'SettingsService', 'SettingsService',
'FeedService', 'FeedService',
'CategoryService', 'CategoryService',
'AnalyticsService', 'AnalyticsService',
function($scope, $stateParams, $http, $route, $state, $window, $location, EntryService, SettingsService, FeedService, function($scope, $stateParams, $http, $route, $state, $window, $timeout, $location, EntryService, SettingsService, FeedService,
CategoryService, AnalyticsService) { CategoryService, AnalyticsService) {
$window = angular.element($window);
AnalyticsService.track(); AnalyticsService.track();
$scope.keywords = $location.search().q; $scope.keywords = $location.search().q;
@@ -727,9 +729,6 @@ module.controller('FeedListCtrl', [
} }
var callback = function(data) { var callback = function(data) {
if (data.offset === 0) {
$scope.entries = [];
}
for ( var i = 0; i < data.entries.length; i++) { for ( var i = 0; i < data.entries.length; i++) {
var entry = data.entries[i]; var entry = data.entries[i];
if (!_.some($scope.entries, { if (!_.some($scope.entries, {
@@ -758,6 +757,79 @@ module.controller('FeedListCtrl', [
}, callback); }, callback);
}; };
var watch_scrolling = true;
var watch_current = true;
$scope.$watch('current', function(newValue, oldValue) {
if (!watch_current) {
return;
}
if (newValue && newValue !== oldValue) {
var force = $scope.navigationMode == 'keyboard';
// timeout here to execute after dom update
$timeout(function() {
var docTop = $(window).scrollTop();
var docBottom = docTop + $(window).height();
var elem = $('#entry_' + newValue.id);
var elemTop = elem.offset().top;
var elemBottom = elemTop + elem.height();
if (!force && (elemTop > docTop) && (elemBottom < docBottom)) {
// element is entirely visible
return;
} else {
var scrollTop = elemTop - $('#toolbar').outerHeight();
watch_scrolling = false;
$('html, body').animate({
scrollTop : scrollTop
}, 400, 'swing', function() {
watch_scrolling = true;
});
}
});
}
});
var scrollHandler = function() {
if (!watch_scrolling || _.size($scope.entries) === 0) {
return;
}
$scope.navigationMode = 'scroll';
if (SettingsService.settings.viewMode == 'expanded') {
var w = $(window);
var docTop = w.scrollTop();
var current = null;
for ( var i = 0; i < $scope.entries.length; i++) {
var entry = $scope.entries[i];
var e = $('#entry_' + entry.id);
if (e.offset().top + e.height() > docTop + $('#toolbar').outerHeight()) {
current = entry;
break;
}
}
var previous = $scope.current;
$scope.current = current;
if (previous != current) {
if (SettingsService.settings.scrollMarks) {
$scope.mark($scope.current, true);
}
watch_current = false;
$scope.$apply();
watch_current = true;
}
}
};
var scrollListener = _.throttle(scrollHandler, 200);
$window.on('scroll', scrollListener);
$scope.$on('$destroy', function() {
return $window.off('scroll', scrollListener);
});
$scope.goToFeed = function(id) { $scope.goToFeed = function(id) {
$state.transitionTo('feeds.view', { $state.transitionTo('feeds.view', {
_type : 'feed', _type : 'feed',
@@ -955,16 +1027,6 @@ module.controller('FeedListCtrl', [
} }
}; };
$scope.onScroll = function(entry) {
$scope.navigationMode = 'scroll';
if (SettingsService.settings.viewMode == 'expanded') {
$scope.current = entry;
if (SettingsService.settings.scrollMarks) {
$scope.mark(entry, true);
}
}
};
Mousetrap.bind('j', function(e) { Mousetrap.bind('j', function(e) {
$scope.$apply(function() { $scope.$apply(function() {
$scope.navigationMode = 'keyboard'; $scope.navigationMode = 'keyboard';
@@ -1416,10 +1478,13 @@ module.controller('HelpController', ['$scope', 'CategoryService', 'AnalyticsServ
}]); }]);
module.controller('FooterController', ['$scope', function($scope) { module.controller('FooterController', ['$scope', function($scope) {
var baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf('#')); var baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf('#'));
var hostname = window.location.hostname; var hostname = window.location.hostname;
$scope.subToMeUrl = baseUrl + 'rest/feed/subscribe?url={feed}'; $scope.subToMeUrl = baseUrl + 'rest/feed/subscribe?url={feed}';
$scope.subToMeName = hostname.indexOf('www.commafeed.com') !== -1 ? 'CommaFeed' : 'CommaFeed (' + hostname + ')'; $scope.subToMeName = hostname.indexOf('www.commafeed.com') !== -1 ? 'CommaFeed' : 'CommaFeed (' + hostname + ')';
}]); }]);
module.controller('MetricsCtrl', ['$scope', 'AdminMetricsService', function($scope, AdminMetricsService) {
$scope.metrics = AdminMetricsService.get();
}]);

View File

@@ -59,96 +59,6 @@ module.directive('ngBlur', function() {
}; };
}); });
/**
* Fired when the top of the element is not visible anymore
*/
module.directive('onScrollMiddle', function() {
return {
restrict : 'A',
link : function(scope, element, attrs) {
var w = $(window);
var e = $(element);
var d = $(document);
var offset = parseInt(attrs.onScrollMiddleOffset, 10);
var down = function() {
var docTop = w.scrollTop();
var elemTop = e.offset().top;
var threshold = docTop === 0 ? elemTop - 1 : docTop + offset;
return (elemTop > threshold) ? 'below' : 'above';
};
var up = function() {
var docTop = w.scrollTop();
var elemTop = e.offset().top;
var elemBottom = elemTop + e.height();
var threshold = docTop === 0 ? elemBottom - 1 : docTop + offset;
return (elemBottom > threshold) ? 'below' : 'above';
};
if (!w.data.scrollInit) {
w.data.scrollPosition = d.scrollTop();
w.data.scrollDirection = 'down';
var onScroll = function(e) {
var scroll = d.scrollTop();
w.data.scrollDirection = (scroll - w.data.scrollPosition > 0) ? 'down' : 'up';
w.data.scrollPosition = scroll;
scope.$apply();
};
w.bind('scroll', _.throttle(onScroll, 500));
w.data.scrollInit = true;
}
scope.$watch(down, function downCallback(value, oldValue) {
if (value != oldValue && value == 'above')
scope.$eval(attrs.onScrollMiddle);
});
scope.$watch(up, function upCallback(value, oldValue) {
if (value != oldValue && value == 'below')
scope.$eval(attrs.onScrollMiddle);
});
}
};
});
/**
* Scrolls to the element if the value is true and the attribute is not fully
* visible, unless the attribute scroll-to-force is true
*/
module.directive('scrollTo', ['$timeout', function($timeout) {
return {
restrict : 'A',
link : function(scope, element, attrs) {
scope.$watch(attrs.scrollTo, function(value) {
if (!value)
return;
var force = scope.$eval(attrs.scrollToForce);
// timeout here to execute after dom update
$timeout(function() {
var docTop = $(window).scrollTop();
var docBottom = docTop + $(window).height();
var elemTop = $(element).offset().top;
var elemBottom = elemTop + $(element).height();
if (!force && (elemTop > docTop) && (elemBottom < docBottom)) {
// element is entirely visible
return;
} else {
var offset = parseInt(attrs.scrollToOffset, 10);
var scrollTop = $(element).offset().top + offset;
$('html, body').animate({
scrollTop : scrollTop
}, 0);
}
});
});
}
};
}]);
/** /**
* Prevent mousewheel scrolling from propagating to the parent when scrollbar * Prevent mousewheel scrolling from propagating to the parent when scrollbar
* reaches top or bottom * reaches top or bottom
@@ -409,27 +319,24 @@ module.directive('droppable', ['CategoryService', 'FeedService', function(Catego
}; };
}]); }]);
module.filter('highlight', function() { module.directive('metricMeter', function() {
return function(html, keywords) { return {
if (keywords) { scope : {
var handleKeyword = function(token, html) { metric : '=',
var expr = new RegExp(token, 'gi'); label : '='
var container = $('<span>').html(html); },
var elements = container.find('*').addBack(); restrict : 'E',
var textNodes = elements.not('iframe').contents().not(elements); templateUrl : 'templates/_metrics.meter.html'
textNodes.each(function() { };
var replaced = this.nodeValue.replace(expr, '<span class="highlight-search">$&</span>'); });
$('<span>').html(replaced).insertBefore(this);
$(this).remove();
});
return container.html();
};
var tokens = keywords.split(' '); module.directive('metricGauge', function() {
for ( var i = 0; i < tokens.length; i++) { return {
html = handleKeyword(tokens[i], html); scope : {
} metric : '=',
} label : '='
return html; },
restrict : 'E',
templateUrl : 'templates/_metrics.gauge.html'
}; };
}); });

View File

@@ -20,4 +20,29 @@ module.filter('entryDate', function() {
module.filter('escape', function() { module.filter('escape', function() {
return encodeURIComponent; return encodeURIComponent;
});
module.filter('highlight', function() {
return function(html, keywords) {
if (keywords) {
var handleKeyword = function(token, html) {
var expr = new RegExp(token, 'gi');
var container = $('<span>').html(html);
var elements = container.find('*').addBack();
var textNodes = elements.not('iframe').contents().not(elements);
textNodes.each(function() {
var replaced = this.nodeValue.replace(expr, '<span class="highlight-search">$&</span>');
$('<span>').html(replaced).insertBefore(this);
$(this).remove();
});
return container.html();
};
var tokens = keywords.split(' ');
for ( var i = 0; i < tokens.length; i++) {
html = handleKeyword(tokens[i], html);
}
}
return html;
};
}); });

View File

@@ -100,7 +100,12 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
templateUrl : 'templates/admin.settings.html', templateUrl : 'templates/admin.settings.html',
controller : 'ManageSettingsCtrl' controller : 'ManageSettingsCtrl'
}); });
$stateProvider.state('admin.metrics', {
url : '/metrics',
templateUrl : 'templates/admin.metrics.html',
controller : 'MetricsCtrl'
});
$urlRouterProvider.when('/', '/feeds/view/category/all'); $urlRouterProvider.when('/', '/feeds/view/category/all');
$urlRouterProvider.when('/admin', '/admin/settings'); $urlRouterProvider.when('/admin', '/admin/settings');
$urlRouterProvider.otherwise('/'); $urlRouterProvider.otherwise('/');

View File

@@ -291,6 +291,12 @@ module.factory('AdminSettingsService', ['$resource', function($resource) {
return res; return res;
}]); }]);
module.factory('AdminMetricsService', ['$resource', function($resource) {
var res = $resource('rest/admin/metrics/');
return res;
}]);
module.factory('AdminCleanupService', ['$resource', function($resource) { module.factory('AdminCleanupService', ['$resource', function($resource) {
var actions = { var actions = {
findDuplicateFeeds : { findDuplicateFeeds : {

View File

@@ -6,6 +6,10 @@
display: block; display: block;
} }
.form-horizontal .control-group {
margin-bottom: 10px;
}
.bidi-embed { .bidi-embed {
unicode-bidi: embed; unicode-bidi: embed;
} }
@@ -65,6 +69,10 @@
height: 16px; height: 16px;
} }
blockquote p {
font-size: 14px;
}
.btn,.btn-large,.btn-small,.btn-mini { .btn,.btn-large,.btn-small,.btn-mini {
border-top-left-radius: 2px; border-top-left-radius: 2px;
border-top-right-radius: 2px; border-top-right-radius: 2px;

View File

@@ -46,6 +46,7 @@
} }
body.left-menu-active .sidebar-nav-fixed { body.left-menu-active .sidebar-nav-fixed {
width: 100%; width: 100%;
overflow: auto;
} }
body.left-menu-active .main-content { body.left-menu-active .main-content {
display: none !important; display: none !important;

View File

@@ -1,66 +1,110 @@
#theme-svetla { #theme-svetla {
/*background color*/ /*bg color*/
body, div.form-actions, .toolbar { body, div.form-actions, .toolbar, .entrylist-header ng-scope {
background: #E3D4D1; background: #AFB8BE;
} }
div.form-actions, div.page-header { .form-actions, div.page-header {
border: none; border: none;
} }
pre, #feed-accordion .unread .entry-heading { pre, #feed-accordion .unread .entry-heading {
background: transparent; background: transparent;
} }
/*feeds tree*/ /*feeds tree*/
.css-treeview li .tree-item:hover { .css-treeview li .tree-item:hover {
background: grey; }
}
/*feeds list*/ /*feeds list*/
#feed-accordion .entry-buttons { /*share panel*/ #feed-accordion .entry-buttons { /*share panel*/
background: transparent; background: transparent;
/* uncomment if ---> border: 1px solid black; */ border:none;
} /* ---> border: 1px solid black; */
}
#feed-accordion .entry { #feed-accordion .entry {
border: none; border: none;
} }
#feed-accordion .unread .entry-heading:hover { /* read feed bg */
background: grey; #feed-accordion .entry-heading {
} }
/* readed feed background */ .dropdown-menu.pull-right li.divider {
#feed-accordion .entry-heading { height: 0px;
background: #987B77; background: transparent;
} border-bottom: 0px;
}
ul.dropdown-menu.pull-right li.divider { .dropdown-menu {
height: 0px; background: #E6E6E6;
background: transparent; border-radius: 0px 0px 4px 4px;
border-bottom: 0px; box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.1);
} }
ul.dropdown-menu { /**/
background: #E6E6E6; .btn, .btn.dropdown-toggle {
} background: #CFC7BE;
border: 1px solid #A7B5BE;
border-radius: 0px;
box-shadow: none;
}
/**/ .btn.dropdown-toggle {
.btn { /*background: #eae7e3;*/
background: #B8ACA4; }
border: 1px solid grey;/* ! */
}
button.btn.dropdown-toggle { .btn.active {
background: #7B7672; box-shadow: none;/*!*/
} }
button.btn.dropdown-toggle:hover { button.btn.dropdown-toggle:hover {
background: #e6e6e6; background: #e6e6e6;
} }
span.hidden-phone.hidden-tablet.ng-binding { span.hidden-phone.hidden-tablet.ng-binding {
display: none; display: none;
} }
} .entrylist-header ng-scope {
border: none;
}
.entrylist-header {
border: none;
}
.entry-buttons.form-horizontal {
border-color: black;
}
/****************/
li.pointer a {
border-radius: 0px;
color: black;
}
li.pointer a:hover {
background: transparent;
}
.btn-primary:hover {
background: #E6E6E6;
color: black;
}
.btn-primary:focus {
color: black;
}
.btn-primary {
color: #323639;
text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.75);
box-shadow: none;
}
#feed-accordion.expanded .entry {
box-shadow: none;
border-color: #CFC7BE;
}
}

View File

@@ -0,0 +1,4 @@
<div>
<span>{{label}}</span>
<span>{{metric.value}}</span>
</div>

View File

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

View File

@@ -0,0 +1,13 @@
<div>
<metric-meter metric="metrics.meters['com.commafeed.backend.feeds.FeedRefreshTaskGiver.refill']" label="'Refresh queue refill rate (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feeds.FeedRefreshTaskGiver.feedRefreshed']" label="'Feed refreshed (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feeds.FeedRefreshUpdater.feedUpdated']" label="'Feed updated (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feeds.FeedRefreshUpdater.entryCacheHit']" label="'Entry cache hit (/sec)'"></metric-meter>
<metric-meter metric="metrics.meters['com.commafeed.backend.feeds.FeedRefreshUpdater.entryCacheMiss']" label="'Entry cache miss (/sec)'"></metric-meter>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feeds.FeedRefreshExecutor.feed-refresh-updater.active']" label="'Feed Updater active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feeds.FeedRefreshExecutor.feed-refresh-updater.pending']" label="'Feed Updater queued'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feeds.FeedRefreshExecutor.feed-refresh-worker.active']" label="'Feed Worker active'"></metric-gauge>
<metric-gauge metric="metrics.gauges['com.commafeed.backend.feeds.FeedRefreshExecutor.feed-refresh-worker.pending']" label="'Feed Worker queued'"></metric-gauge>
</div>

View File

@@ -8,7 +8,7 @@
</div> </div>
</div> </div>
<div class="span10 main-content"> <div class="span10 main-content">
<div class="toolbar" ng-include="'templates/_toolbar.html'"></div> <div id="toolbar" class="toolbar" ng-include="'templates/_toolbar.html'"></div>
<div class="entryList"> <div class="entryList">
<div ui-view></div> <div ui-view></div>
</div> </div>

View File

@@ -18,8 +18,6 @@
ng-class="{'expanded' : settingsService.settings.viewMode == 'expanded' }"> ng-class="{'expanded' : settingsService.settings.viewMode == 'expanded' }">
<div ng-show="message && errorCount > 10">${view.error_while_loading_feed} : {{message}}</div> <div ng-show="message && errorCount > 10">${view.error_while_loading_feed} : {{message}}</div>
<div ng-repeat="entry in entries" class="entry entry-font-size-{{font_size}}" id="entry_{{entry.id}}" <div ng-repeat="entry in entries" class="entry entry-font-size-{{font_size}}" id="entry_{{entry.id}}"
scroll-to="navigationMode != 'scroll' && current == entry" scroll-to-force="navigationMode == 'keyboard'" scroll-to-offset="-50"
on-scroll-middle="onScroll(entry)" on-scroll-middle-offset="50"
ng-class="{unread: entry.read == false, current: current==entry, open: isOpen, closed: !isOpen }"> ng-class="{unread: entry.read == false, current: current==entry, open: isOpen, closed: !isOpen }">
<div class="entry-heading"> <div class="entry-heading">
<a href="{{entry.url}}" target="_blank" class="entry-heading-link" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)"> <a href="{{entry.url}}" target="_blank" class="entry-heading-link" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)">
@@ -85,25 +83,28 @@
</label> </label>
<span class="share-buttons" ui-if="settingsService.settings.socialButtons"> <span class="share-buttons" ui-if="settingsService.settings.socialButtons">
<a href="mailto:?subject={{entry.title|escape}}&body={{entry.url|escape}}" popup> <a href="mailto:?subject={{entry.title|escape}}&body={{entry.url|escape}}" title="E-mail" popup>
<i class="icon-envelope"></i> <i class="icon-envelope"></i>
</a> </a>
<a href="http://www.facebook.com/sharer.php?u=={{entry.url|escape}}" popup> <a href="https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su={{entry.title|escape}}&body={{entry.url|escape}}" title="Gmail" popup>
<i class="icon-gmail"></i>
</a>
<a href="http://www.facebook.com/sharer.php?u=={{entry.url|escape}}" title="Facebook" popup>
<i class="icon-facebook"></i> <i class="icon-facebook"></i>
</a> </a>
<a href="http://twitter.com/share?text={{entry.title|escape}}&url={{entry.url|escape}}" popup> <a href="http://twitter.com/share?text={{entry.title|escape}}&url={{entry.url|escape}}" title="Twitter" popup>
<i class="icon-twitter"></i> <i class="icon-twitter"></i>
</a> </a>
<a href="https://plus.google.com/share?url={{entry.url|escape}}" popup> <a href="https://plus.google.com/share?url={{entry.url|escape}}" title="Google+" popup>
<i class="icon-google-plus"></i> <i class="icon-google-plus"></i>
</a> </a>
<a href="https://getpocket.com/save?url={{entry.url|escape}}&title={{entry.title|escape}}" popup> <a href="https://getpocket.com/save?url={{entry.url|escape}}&title={{entry.title|escape}}" title="Pocket" popup>
<i class="icon-pocket"></i> <i class="icon-pocket"></i>
</a> </a>
<a href="https://www.instapaper.com/hello2?url={{entry.url|escape}}&title={{entry.title|escape}}" popup> <a href="https://www.instapaper.com/hello2?url={{entry.url|escape}}&title={{entry.title|escape}}" title="Instapaper" popup>
<i class="icon-instapaper"></i> <i class="icon-instapaper"></i>
</a> </a>
<a href="https://bufferapp.com/add?url={{entry.url|escape}}&text={{entry.title|escape}}" popup> <a href="https://bufferapp.com/add?url={{entry.url|escape}}&text={{entry.title|escape}}" title="Buffer" popup>
<i class="icon-buffer"></i> <i class="icon-buffer"></i>
</a> </a>
</span> </span>

View File

@@ -38,4 +38,10 @@
font-style: normal; font-style: normal;
font-family: 'zocial'; font-family: 'zocial';
content: "\00E5"; content: "\00E5";
}
.icon-gmail:before {
font-style: normal;
font-family: 'zocial';
content: "m";
} }