forked from Archives/Athou_commafeed
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4d62ab47 | ||
|
|
a7b826bd4f | ||
|
|
407481faa6 | ||
|
|
305b68546c | ||
|
|
136c41c6aa | ||
|
|
587b25b18b | ||
|
|
beaa40ad65 | ||
|
|
1389a5a238 | ||
|
|
2f34ff8a9f | ||
|
|
d3626b0e7c | ||
|
|
bb4529b6f1 | ||
|
|
dd94125d52 | ||
|
|
a7149e3740 | ||
|
|
b64d041385 | ||
|
|
cc04bdfbc5 | ||
|
|
d8c772ed5e | ||
|
|
dfcc4eeebd | ||
|
|
e491841d4a | ||
|
|
ccb72837b3 | ||
|
|
6560fc9d05 | ||
|
|
14d5879735 | ||
|
|
7fa8bef3de | ||
|
|
966caae727 | ||
|
|
a14484ee03 | ||
|
|
fb9b42ab12 | ||
|
|
6974abdb95 | ||
|
|
65efdeb1df | ||
|
|
54a39ea0a9 | ||
|
|
641350cbde | ||
|
|
06ece8f5ee | ||
|
|
ca87f1c47a | ||
|
|
c38ddb5d00 | ||
|
|
1acd7c4a01 | ||
|
|
d92c2ebdf7 | ||
|
|
8f19e9408e | ||
|
|
3ecb47da5a | ||
|
|
ae03b42c6d | ||
|
|
ee4eb9bb07 | ||
|
|
a0be2e0879 | ||
|
|
a3414d7156 |
@@ -74,12 +74,15 @@ It will generate a zip file at `target/commafeed.zip` with everything you need t
|
||||
* If you don't use the embedded database, create a database in your external database instance, then uncomment the `Resource` element corresponding to the database engine you use from `conf/tomee.xml` and edit the default credentials.
|
||||
* If you'd like to change the default port (8082), edit `conf/server.xml` and look for `<Connector port="8082" protocol="HTTP/1.1"`. Change the port to the value you'd like to use.
|
||||
* CommaFeed will run on the `/commafeed` context. If you'd like to change the context, go to `webapps` and rename `commafeed.war`. Use the special name `ROOT.war` to deploy to the root context.
|
||||
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
|
||||
* To start and stop the application, use `bin/startup.sh` and `bin/shutdown.sh` on Linux (you need to `chmod +x bin/*.sh`) or `bin\startup.bat` and `bin\shutdown.bat` on Windows.
|
||||
If you use the embedded database, note that the database file will be created in the current directory, so make sure you always start the app in the same directory. You can optionally set an absolute path instead of a relative one in `tomee.xml`.
|
||||
* To update the application with a newer version, pull the latest changes and use the same command you used to build the complete TomEE package, but without the `tomee:build` part (keep `-Pprod -P<database>`).
|
||||
This will generate the file `target/commafeed.war`. Copy this file to your tomee `webapps/` directory.
|
||||
* The application is online at [http://localhost:8082/commafeed](http://localhost:8082/commafeed). Don't forget to set the public URL in the admin settings.
|
||||
* The default user is `admin` and the password is `admin`.
|
||||
|
||||
You can use 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
|
||||
-----------------
|
||||
|
||||
|
||||
21
pom.xml
21
pom.xml
@@ -4,7 +4,7 @@
|
||||
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>1.3.0</version>
|
||||
<packaging>war</packaging>
|
||||
<name>CommaFeed</name>
|
||||
|
||||
@@ -336,23 +336,23 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-core</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.10.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-auth-roles</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.10.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-extensions</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.10.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.wicket</groupId>
|
||||
<artifactId>wicket-cdi</artifactId>
|
||||
<version>6.9.1</version>
|
||||
<version>6.10.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ro.isdc.wro4j</groupId>
|
||||
@@ -372,6 +372,17 @@
|
||||
<scope>provided</scope>
|
||||
</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>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
||||
@@ -19,12 +19,14 @@ import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.HttpResponseException;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.params.CookiePolicy;
|
||||
import org.apache.http.client.params.HttpClientParams;
|
||||
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.HttpParams;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -93,6 +98,8 @@ public class HttpGetter {
|
||||
HttpClient client = newClient(timeout);
|
||||
try {
|
||||
HttpGet httpget = new HttpGet(url);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
|
||||
httpget.addHeader(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANGUAGE);
|
||||
httpget.addHeader(HttpHeaders.PRAGMA, PRAGMA_NO_CACHE);
|
||||
httpget.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
@@ -107,7 +114,7 @@ public class HttpGetter {
|
||||
|
||||
HttpResponse response = null;
|
||||
try {
|
||||
response = client.execute(httpget);
|
||||
response = client.execute(httpget, context);
|
||||
int code = response.getStatusLine().getStatusCode();
|
||||
if (code == HttpStatus.SC_NOT_MODIFIED) {
|
||||
throw new NotModifiedException("'304 - not modified' http code received");
|
||||
@@ -123,15 +130,14 @@ public class HttpGetter {
|
||||
}
|
||||
}
|
||||
Header lastModifiedHeader = response.getFirstHeader(HttpHeaders.LAST_MODIFIED);
|
||||
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
|
||||
|
||||
String lastModifiedResponse = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
|
||||
if (lastModified != null && StringUtils.equals(lastModified, lastModifiedResponse)) {
|
||||
String lastModifiedHeaderValue = lastModifiedHeader == null ? null : StringUtils.trimToNull(lastModifiedHeader.getValue());
|
||||
if (lastModifiedHeaderValue != null && StringUtils.equals(lastModified, lastModifiedHeaderValue)) {
|
||||
throw new NotModifiedException("lastModifiedHeader is the same");
|
||||
}
|
||||
|
||||
String eTagResponse = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
|
||||
if (eTag != null && StringUtils.equals(eTag, eTagResponse)) {
|
||||
Header eTagHeader = response.getFirstHeader(HttpHeaders.ETAG);
|
||||
String eTagHeaderValue = eTagHeader == null ? null : StringUtils.trimToNull(eTagHeader.getValue());
|
||||
if (eTag != null && StringUtils.equals(eTag, eTagHeaderValue)) {
|
||||
throw new NotModifiedException("eTagHeader is the same");
|
||||
}
|
||||
|
||||
@@ -144,10 +150,12 @@ public class HttpGetter {
|
||||
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;
|
||||
result = new HttpResult(content, contentType, lastModifiedHeader == null ? null : lastModifiedHeader.getValue(),
|
||||
eTagHeader == null ? null : eTagHeader.getValue(), duration);
|
||||
result = new HttpResult(content, contentType, lastModifiedHeaderValue, eTagHeaderValue, duration, urlAfterRedirect);
|
||||
} finally {
|
||||
client.getConnectionManager().shutdown();
|
||||
}
|
||||
@@ -161,13 +169,15 @@ public class HttpGetter {
|
||||
private String lastModifiedSince;
|
||||
private String eTag;
|
||||
private long duration;
|
||||
private String urlAfterRedirect;
|
||||
|
||||
public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration) {
|
||||
public HttpResult(byte[] content, String contentType, String lastModifiedSince, String eTag, long duration, String urlAfterRedirect) {
|
||||
this.content = content;
|
||||
this.contentType = contentType;
|
||||
this.lastModifiedSince = lastModifiedSince;
|
||||
this.eTag = eTag;
|
||||
this.duration = duration;
|
||||
this.urlAfterRedirect = urlAfterRedirect;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
@@ -190,6 +200,9 @@ public class HttpGetter {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public String getUrlAfterRedirect() {
|
||||
return urlAfterRedirect;
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpClient newClient(int timeout) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,26 @@ import java.util.Date;
|
||||
|
||||
import javax.ejb.Schedule;
|
||||
import javax.ejb.Stateless;
|
||||
import javax.ejb.TransactionManagement;
|
||||
import javax.ejb.TransactionManagementType;
|
||||
import javax.inject.Inject;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.DatabaseCleaningService;
|
||||
|
||||
/**
|
||||
* Contains all scheduled tasks
|
||||
*
|
||||
*/
|
||||
@Stateless
|
||||
@TransactionManagement(TransactionManagementType.BEAN)
|
||||
public class ScheduledTasks {
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
DatabaseCleaner cleaner;
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
DatabaseCleaningService cleaner;
|
||||
|
||||
/**
|
||||
* clean old read statuses, runs every day at midnight
|
||||
|
||||
@@ -290,10 +290,17 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
||||
setTimeout(query, applicationSettingsService.get().getQueryTimeout());
|
||||
}
|
||||
|
||||
public int deleteOldStatuses(Date olderThan) {
|
||||
Query query = em.createNamedQuery("Statuses.deleteOld");
|
||||
query.setParameter("date", olderThan);
|
||||
return query.executeUpdate();
|
||||
public List<FeedEntryStatus> getOldStatuses(Date olderThan, int limit) {
|
||||
CriteriaQuery<FeedEntryStatus> query = builder.createQuery(getType());
|
||||
Root<FeedEntryStatus> root = query.from(getType());
|
||||
|
||||
Predicate p1 = builder.lessThan(root.get(FeedEntryStatus_.entryInserted), olderThan);
|
||||
Predicate p2 = builder.isFalse(root.get(FeedEntryStatus_.starred));
|
||||
|
||||
query.where(p1, p2);
|
||||
TypedQuery<FeedEntryStatus> q = em.createQuery(query);
|
||||
q.setMaxResults(limit);
|
||||
return q.getResultList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ public class FeedFetcher {
|
||||
feed.setEtagHeader(FeedUtils.truncate(result.geteTag(), 255));
|
||||
feed.setLastContentHash(hash);
|
||||
fetchedFeed.setFetchDuration(result.getDuration());
|
||||
fetchedFeed.setUrlAfterRedirect(result.getUrlAfterRedirect());
|
||||
return fetchedFeed;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import com.codahale.metrics.Gauge;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
|
||||
/**
|
||||
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
|
||||
* {@link Task} instead of {@link Runnable}
|
||||
@@ -19,7 +22,7 @@ public class FeedRefreshExecutor {
|
||||
private ThreadPoolExecutor pool;
|
||||
private LinkedBlockingDeque<Runnable> queue;
|
||||
|
||||
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity) {
|
||||
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity, MetricRegistry metrics) {
|
||||
log.info("Creating pool {} with {} threads", poolName, threads);
|
||||
this.poolName = poolName;
|
||||
pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
|
||||
@@ -51,20 +54,26 @@ public class FeedRefreshExecutor {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), poolName, "active"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
});
|
||||
|
||||
metrics.register(MetricRegistry.name(getClass(), poolName, "pending"), new Gauge<Integer>() {
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return queue.size();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void execute(Task task) {
|
||||
pool.execute(task);
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
public int getActiveCount() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
|
||||
public static interface Task extends Runnable {
|
||||
boolean isUrgent();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
@@ -41,7 +42,7 @@ public class FeedRefreshTaskGiver {
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
FeedRefreshWorker worker;
|
||||
@@ -54,10 +55,17 @@ public class FeedRefreshTaskGiver {
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
private Meter feedRefreshed;
|
||||
private Meter threadWaited;
|
||||
private Meter refill;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
backgroundThreads = applicationSettingsService.get().getBackgroundThreads();
|
||||
executor = Executors.newFixedThreadPool(1);
|
||||
feedRefreshed = metrics.meter(MetricRegistry.name(getClass(), "feedRefreshed"));
|
||||
threadWaited = metrics.meter(MetricRegistry.name(getClass(), "threadWaited"));
|
||||
refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -88,11 +96,11 @@ public class FeedRefreshTaskGiver {
|
||||
try {
|
||||
FeedRefreshContext context = take();
|
||||
if (context != null) {
|
||||
metricsBean.feedRefreshed();
|
||||
feedRefreshed.mark();
|
||||
worker.updateFeed(context);
|
||||
} else {
|
||||
log.debug("nothing to do, sleeping for 15s");
|
||||
metricsBean.threadWaited();
|
||||
threadWaited.mark();
|
||||
try {
|
||||
Thread.sleep(15000);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -138,6 +146,7 @@ public class FeedRefreshTaskGiver {
|
||||
* refills the refresh queue and empties the giveBack queue while at it
|
||||
*/
|
||||
private void refill() {
|
||||
refill.mark();
|
||||
int count = Math.min(100, 3 * backgroundThreads);
|
||||
|
||||
// first, get feeds that are up to refresh from the database
|
||||
|
||||
@@ -19,7 +19,8 @@ import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
@@ -57,7 +58,7 @@ public class FeedRefreshUpdater {
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
FeedSubscriptionDAO feedSubscriptionDAO;
|
||||
@@ -71,12 +72,22 @@ public class FeedRefreshUpdater {
|
||||
private FeedRefreshExecutor pool;
|
||||
private Striped<Lock> locks;
|
||||
|
||||
private Meter entryCacheMiss;
|
||||
private Meter entryCacheHit;
|
||||
private Meter feedUpdated;
|
||||
private Meter entryInserted;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
ApplicationSettings settings = applicationSettingsService.get();
|
||||
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1);
|
||||
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000));
|
||||
pool = new FeedRefreshExecutor("feed-refresh-updater", threads, Math.min(50 * threads, 1000), metrics);
|
||||
locks = Striped.lazyWeakLock(threads * 100000);
|
||||
|
||||
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
|
||||
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
|
||||
feedUpdated = metrics.meter(MetricRegistry.name(getClass(), "feedUpdated"));
|
||||
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -116,10 +127,10 @@ public class FeedRefreshUpdater {
|
||||
subscriptions = feedSubscriptionDAO.findByFeed(feed);
|
||||
}
|
||||
ok &= addEntry(feed, entry, subscriptions);
|
||||
metricsBean.entryCacheMiss();
|
||||
entryCacheMiss.mark();
|
||||
} else {
|
||||
log.debug("cache hit for {}", entry.getUrl());
|
||||
metricsBean.entryCacheHit();
|
||||
entryCacheHit.mark();
|
||||
}
|
||||
|
||||
currentEntries.add(cacheKey);
|
||||
@@ -147,7 +158,7 @@ public class FeedRefreshUpdater {
|
||||
// requeue asap
|
||||
feed.setDisabledUntil(new Date(0));
|
||||
}
|
||||
metricsBean.feedUpdated();
|
||||
feedUpdated.mark();
|
||||
taskGiver.giveBack(feed);
|
||||
}
|
||||
|
||||
@@ -180,7 +191,7 @@ public class FeedRefreshUpdater {
|
||||
if (locked1 && locked2) {
|
||||
boolean inserted = feedUpdateService.addEntry(feed, entry);
|
||||
if (inserted) {
|
||||
metricsBean.entryInserted();
|
||||
entryInserted.mark();
|
||||
}
|
||||
success = true;
|
||||
} else {
|
||||
@@ -213,13 +224,4 @@ public class FeedRefreshUpdater {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return pool.getQueueSize();
|
||||
}
|
||||
|
||||
public int getActiveCount() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||
import com.commafeed.backend.feeds.FeedRefreshExecutor.Task;
|
||||
import com.commafeed.backend.model.ApplicationSettings;
|
||||
@@ -37,6 +40,9 @@ public class FeedRefreshWorker {
|
||||
@Inject
|
||||
FeedRefreshTaskGiver taskGiver;
|
||||
|
||||
@Inject
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@@ -46,7 +52,7 @@ public class FeedRefreshWorker {
|
||||
private void init() {
|
||||
ApplicationSettings settings = applicationSettingsService.get();
|
||||
int threads = settings.getBackgroundThreads();
|
||||
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000));
|
||||
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000), metrics);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -58,14 +64,6 @@ public class FeedRefreshWorker {
|
||||
pool.execute(new FeedTask(context));
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
return pool.getQueueSize();
|
||||
}
|
||||
|
||||
public int getActiveCount() {
|
||||
return pool.getActiveCount();
|
||||
}
|
||||
|
||||
private class FeedTask implements Task {
|
||||
|
||||
private FeedRefreshContext context;
|
||||
@@ -90,17 +88,21 @@ public class FeedRefreshWorker {
|
||||
int refreshInterval = applicationSettingsService.get().getRefreshIntervalMinutes();
|
||||
Date disabledUntil = DateUtils.addMinutes(new Date(), refreshInterval);
|
||||
try {
|
||||
FetchedFeed fetchedFeed = fetcher.fetch(feed.getUrl(), false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||
String url = ObjectUtils.firstNonNull(feed.getUrlAfterRedirect(), feed.getUrl());
|
||||
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||
feed.getLastPublishedDate(), feed.getLastContentHash());
|
||||
// stops here if NotModifiedException or any other exception is
|
||||
// thrown
|
||||
// stops here if NotModifiedException or any other exception is thrown
|
||||
List<FeedEntry> entries = fetchedFeed.getEntries();
|
||||
|
||||
if (applicationSettingsService.get().isHeavyLoad()) {
|
||||
disabledUntil = FeedUtils.buildDisabledUntil(fetchedFeed.getFeed().getLastEntryDate(), fetchedFeed.getFeed()
|
||||
.getAverageEntryInterval(), disabledUntil);
|
||||
}
|
||||
|
||||
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
|
||||
if (StringUtils.equals(url, urlAfterRedirect)) {
|
||||
urlAfterRedirect = null;
|
||||
}
|
||||
feed.setUrlAfterRedirect(urlAfterRedirect);
|
||||
feed.setLink(fetchedFeed.getFeed().getLink());
|
||||
feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
|
||||
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());
|
||||
|
||||
@@ -12,6 +12,7 @@ public class FetchedFeed {
|
||||
private List<FeedEntry> entries = Lists.newArrayList();
|
||||
|
||||
private String title;
|
||||
private String urlAfterRedirect;
|
||||
private long fetchDuration;
|
||||
|
||||
public Feed getFeed() {
|
||||
@@ -45,4 +46,13 @@ public class FetchedFeed {
|
||||
public void setFetchDuration(long fetchDuration) {
|
||||
this.fetchDuration = fetchDuration;
|
||||
}
|
||||
|
||||
public String getUrlAfterRedirect() {
|
||||
return urlAfterRedirect;
|
||||
}
|
||||
|
||||
public void setUrlAfterRedirect(String urlAfterRedirect) {
|
||||
this.urlAfterRedirect = urlAfterRedirect;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@ public class Feed extends AbstractModel {
|
||||
@Column(length = 2048, nullable = false)
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* cache the url after potential http 30x redirects
|
||||
*/
|
||||
@Column(name = "url_after_redirect", length = 2048, nullable = false)
|
||||
private String urlAfterRedirect;
|
||||
|
||||
@Column(length = 2048, nullable = false)
|
||||
private String normalizedUrl;
|
||||
|
||||
@@ -130,11 +136,4 @@ public class Feed extends AbstractModel {
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date pushLastPing;
|
||||
|
||||
public Feed() {
|
||||
|
||||
}
|
||||
|
||||
public Feed(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend.feeds;
|
||||
package com.commafeed.backend.opml;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend.feeds;
|
||||
package com.commafeed.backend.opml;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
@@ -11,10 +11,12 @@ import javax.inject.Inject;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.services.FeedSubscriptionService;
|
||||
@@ -56,8 +58,8 @@ public class OPMLImporter {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleOutline(User user, Outline outline, FeedCategory parent) {
|
||||
|
||||
if (StringUtils.isEmpty(outline.getType())) {
|
||||
List<Outline> children = outline.getChildren();
|
||||
if (CollectionUtils.isNotEmpty(children)) {
|
||||
String name = FeedUtils.truncate(outline.getText(), 128);
|
||||
if (name == null) {
|
||||
name = FeedUtils.truncate(outline.getTitle(), 128);
|
||||
@@ -75,7 +77,6 @@ public class OPMLImporter {
|
||||
feedCategoryDAO.saveOrUpdate(category);
|
||||
}
|
||||
|
||||
List<Outline> children = outline.getChildren();
|
||||
for (Outline child : children) {
|
||||
handleOutline(user, child, category);
|
||||
}
|
||||
@@ -87,7 +88,7 @@ public class OPMLImporter {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "Unnamed subscription";
|
||||
}
|
||||
// make sure we continue with the import process even a feed failed
|
||||
// make sure we continue with the import process even if a feed failed
|
||||
try {
|
||||
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), name, parent);
|
||||
} catch (FeedSubscriptionException e) {
|
||||
@@ -3,7 +3,8 @@ package com.commafeed.backend.services;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import javax.ejb.Singleton;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
@@ -15,7 +16,7 @@ import com.commafeed.backend.dao.ApplicationSettingsDAO;
|
||||
import com.commafeed.backend.model.ApplicationSettings;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
@Singleton
|
||||
@ApplicationScoped
|
||||
public class ApplicationSettingsService {
|
||||
|
||||
@Inject
|
||||
@@ -23,19 +24,21 @@ public class ApplicationSettingsService {
|
||||
|
||||
private ApplicationSettings settings;
|
||||
|
||||
public void save(ApplicationSettings settings) {
|
||||
this.settings = settings;
|
||||
applicationSettingsDAO.saveOrUpdate(settings);
|
||||
applyLogLevel();
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
|
||||
}
|
||||
|
||||
public ApplicationSettings get() {
|
||||
if (settings == null) {
|
||||
settings = Iterables.getFirst(applicationSettingsDAO.findAll(), null);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
public void save(ApplicationSettings settings) {
|
||||
applicationSettingsDAO.saveOrUpdate(settings);
|
||||
this.settings = settings;
|
||||
applyLogLevel();
|
||||
}
|
||||
|
||||
public Date getUnreadThreshold() {
|
||||
int keepStatusDays = get().getKeepStatusDays();
|
||||
return keepStatusDays > 0 ? DateUtils.addDays(new Date(), -1 * keepStatusDays) : null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.commafeed.backend;
|
||||
package com.commafeed.backend.services;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -15,15 +16,15 @@ import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
|
||||
/**
|
||||
* Contains utility methods for cleaning the database
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
public class DatabaseCleaner {
|
||||
public class DatabaseCleaningService {
|
||||
|
||||
@Inject
|
||||
FeedDAO feedDAO;
|
||||
@@ -99,9 +100,19 @@ public class DatabaseCleaner {
|
||||
feedDAO.saveOrUpdate(into);
|
||||
}
|
||||
|
||||
public void cleanStatusesOlderThan(Date olderThan) {
|
||||
public long cleanStatusesOlderThan(Date olderThan) {
|
||||
log.info("cleaning old read statuses");
|
||||
int deleted = feedEntryStatusDAO.deleteOldStatuses(olderThan);
|
||||
log.info("cleaned {} read statuses", deleted);
|
||||
long total = 0;
|
||||
List<FeedEntryStatus> list = Collections.emptyList();
|
||||
do {
|
||||
list = feedEntryStatusDAO.getOldStatuses(olderThan, 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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend;
|
||||
package com.commafeed.backend.startup;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.commafeed.backend;
|
||||
package com.commafeed.backend.startup;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -10,6 +10,8 @@ import com.commafeed.backend.services.UserService;
|
||||
// extend Component in order to benefit from injection
|
||||
public class CommaFeedSessionServices extends Component {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
|
||||
import org.apache.wicket.markup.html.WebMarkupContainer;
|
||||
import org.apache.wicket.markup.html.WebPage;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||
@@ -29,6 +28,7 @@ import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.MailService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.CommaFeedSession;
|
||||
import com.commafeed.frontend.utils.WicketUtils;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
@@ -4,8 +4,8 @@ import javax.inject.Inject;
|
||||
|
||||
import org.apache.wicket.markup.html.WebPage;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.services.UserService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.CommaFeedSession;
|
||||
|
||||
public class DemoLoginPage extends WebPage {
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.apache.wicket.markup.head.CssHeaderItem;
|
||||
import org.apache.wicket.markup.head.IHeaderResponse;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.frontend.CommaFeedSession;
|
||||
@@ -21,7 +22,11 @@ public class HomePage extends BasePage {
|
||||
response.render(CssHeaderItem.forReference(new UserCustomCssReference() {
|
||||
@Override
|
||||
protected String getCss() {
|
||||
UserSettings settings = userSettingsDAO.findByUser(CommaFeedSession.get().getUser());
|
||||
User user = CommaFeedSession.get().getUser();
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
UserSettings settings = userSettingsDAO.findByUser(user);
|
||||
return settings == null ? null : settings.getCustomCss();
|
||||
}
|
||||
}, new PageParameters().add("_t", System.currentTimeMillis()), null));
|
||||
|
||||
@@ -6,9 +6,11 @@ import java.io.OutputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.ext.MessageBodyReader;
|
||||
@@ -19,6 +21,7 @@ import org.apache.http.HttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
|
||||
@Provider
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@@ -30,6 +33,9 @@ public class JsonProvider implements MessageBodyReader<Object>, MessageBodyWrite
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
@Context
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Override
|
||||
public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
|
||||
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
|
||||
@@ -38,10 +44,29 @@ public class JsonProvider implements MessageBodyReader<Object>, MessageBodyWrite
|
||||
httpHeaders.putSingle(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_VALUE);
|
||||
httpHeaders.putSingle(HttpHeaders.PRAGMA, CACHE_CONTROL_VALUE);
|
||||
|
||||
getMapper().writeValue(entityStream, value);
|
||||
ObjectWriter writer = getMapper().writer();
|
||||
if (hasPrettyPrint(annotations)) {
|
||||
writer = writer.withDefaultPrettyPrinter();
|
||||
}
|
||||
writer.writeValue(entityStream, value);
|
||||
|
||||
}
|
||||
|
||||
private boolean hasPrettyPrint(Annotation[] annotations) {
|
||||
boolean prettyPrint = false;
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (PrettyPrint.class.equals(annotation.annotationType())) {
|
||||
prettyPrint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prettyPrint && request != null) {
|
||||
prettyPrint = Boolean.parseBoolean(request.getParameter("pretty"));
|
||||
}
|
||||
return prettyPrint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
|
||||
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
|
||||
|
||||
12
src/main/java/com/commafeed/frontend/rest/PrettyPrint.java
Normal file
12
src/main/java/com/commafeed/frontend/rest/PrettyPrint.java
Normal 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 {
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.apache.wicket.protocol.http.servlet.ServletWebResponse;
|
||||
import org.apache.wicket.request.cycle.RequestCycle;
|
||||
import org.apache.wicket.util.crypt.Base64;
|
||||
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
@@ -42,6 +43,9 @@ public abstract class AbstractREST {
|
||||
@Context
|
||||
private HttpServletResponse response;
|
||||
|
||||
@Inject
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
private UserDAO userDAO;
|
||||
|
||||
@@ -93,10 +97,13 @@ public abstract class AbstractREST {
|
||||
}
|
||||
|
||||
@AroundInvoke
|
||||
public Object checkSecurity(InvocationContext context) throws Exception {
|
||||
public Object intercept(InvocationContext context) throws Exception {
|
||||
Method method = context.getMethod();
|
||||
|
||||
// check security
|
||||
boolean allowed = true;
|
||||
User user = null;
|
||||
Method method = context.getMethod();
|
||||
|
||||
SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method.getAnnotation(SecurityCheck.class) : method
|
||||
.getDeclaringClass().getAnnotation(SecurityCheck.class);
|
||||
|
||||
@@ -118,7 +125,16 @@ public abstract class AbstractREST {
|
||||
|
||||
}
|
||||
|
||||
return context.proceed();
|
||||
Object result = null;
|
||||
com.codahale.metrics.Timer.Context timer = metrics.timer(
|
||||
MetricRegistry.name(method.getDeclaringClass(), method.getName(), "responseTime")).time();
|
||||
try {
|
||||
result = context.proceed();
|
||||
} finally {
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkRole(Role requiredRole) {
|
||||
|
||||
@@ -17,9 +17,7 @@ import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.DatabaseCleaner;
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.dao.FeedDAO.DuplicateMode;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
@@ -33,14 +31,17 @@ import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.DatabaseCleaningService;
|
||||
import com.commafeed.backend.services.FeedService;
|
||||
import com.commafeed.backend.services.PasswordEncryptionService;
|
||||
import com.commafeed.backend.services.UserService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.SecurityCheck;
|
||||
import com.commafeed.frontend.model.FeedCount;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.frontend.model.request.FeedMergeRequest;
|
||||
import com.commafeed.frontend.model.request.IDRequest;
|
||||
import com.commafeed.frontend.rest.PrettyPrint;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
@@ -70,10 +71,10 @@ public class AdminREST extends AbstractREST {
|
||||
FeedDAO feedDAO;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
MetricRegistry metrics;
|
||||
|
||||
@Inject
|
||||
DatabaseCleaner cleaner;
|
||||
DatabaseCleaningService cleaner;
|
||||
|
||||
@Inject
|
||||
FeedRefreshWorker feedRefreshWorker;
|
||||
@@ -226,21 +227,10 @@ public class AdminREST extends AbstractREST {
|
||||
|
||||
@Path("/metrics")
|
||||
@GET
|
||||
@PrettyPrint
|
||||
@ApiOperation(value = "Retrieve server metrics")
|
||||
public Response getMetrics(@QueryParam("backlog") @DefaultValue("false") boolean backlog) {
|
||||
Map<String, Object> map = Maps.newLinkedHashMap();
|
||||
map.put("lastMinute", metricsBean.getLastMinute());
|
||||
map.put("lastHour", metricsBean.getLastHour());
|
||||
if (backlog) {
|
||||
map.put("backlog", taskGiver.getUpdatableCount());
|
||||
}
|
||||
map.put("http_active", feedRefreshWorker.getActiveCount());
|
||||
map.put("http_queue", feedRefreshWorker.getQueueSize());
|
||||
map.put("database_active", feedRefreshUpdater.getActiveCount());
|
||||
map.put("database_queue", feedRefreshUpdater.getQueueSize());
|
||||
map.put("cache", metricsBean.getCacheStats());
|
||||
|
||||
return Response.ok(map).build();
|
||||
public Response getMetrics() {
|
||||
return Response.ok(metrics).build();
|
||||
}
|
||||
|
||||
@Path("/cleanup/feeds")
|
||||
|
||||
@@ -170,8 +170,9 @@ public class CategoryREST extends AbstractREST {
|
||||
.isImageProxyEnabled()));
|
||||
}
|
||||
entries.setName(parent.getName());
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>category not found</message>").build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean hasMore = entries.getEntries().size() > limit;
|
||||
@@ -200,7 +201,11 @@ public class CategoryREST extends AbstractREST {
|
||||
int offset = 0;
|
||||
int limit = 20;
|
||||
|
||||
Entries entries = (Entries) getCategoryEntries(id, readType, null, offset, limit, order, null, false, null).getEntity();
|
||||
Response response = getCategoryEntries(id, readType, null, offset, limit, order, null, false, null);
|
||||
if (response.getStatus() != Status.OK.getStatusCode()) {
|
||||
return response;
|
||||
}
|
||||
Entries entries = (Entries) response.getEntity();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang.ObjectUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.cache.CacheService;
|
||||
import com.commafeed.backend.dao.FeedCategoryDAO;
|
||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||
@@ -47,8 +46,6 @@ import com.commafeed.backend.feeds.FeedFetcher;
|
||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||
import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.feeds.FetchedFeed;
|
||||
import com.commafeed.backend.feeds.OPMLExporter;
|
||||
import com.commafeed.backend.feeds.OPMLImporter;
|
||||
import com.commafeed.backend.model.Feed;
|
||||
import com.commafeed.backend.model.FeedCategory;
|
||||
import com.commafeed.backend.model.FeedEntryStatus;
|
||||
@@ -56,9 +53,12 @@ import com.commafeed.backend.model.FeedSubscription;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.opml.OPMLExporter;
|
||||
import com.commafeed.backend.opml.OPMLImporter;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.FeedEntryService;
|
||||
import com.commafeed.backend.services.FeedSubscriptionService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
@@ -181,6 +181,8 @@ public class FeedREST extends AbstractREST {
|
||||
entries.setHasMore(true);
|
||||
entries.getEntries().remove(entries.getEntries().size() - 1);
|
||||
}
|
||||
} else {
|
||||
return Response.status(Status.NOT_FOUND).entity("<message>feed not found</message>").build();
|
||||
}
|
||||
|
||||
entries.setTimestamp(System.currentTimeMillis());
|
||||
@@ -202,7 +204,11 @@ public class FeedREST extends AbstractREST {
|
||||
int offset = 0;
|
||||
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();
|
||||
feed.setFeedType("rss_2.0");
|
||||
@@ -235,7 +241,7 @@ public class FeedREST extends AbstractREST {
|
||||
try {
|
||||
FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null);
|
||||
info = new FeedInfo();
|
||||
info.setUrl(feed.getFeed().getUrl());
|
||||
info.setUrl(feed.getUrlAfterRedirect());
|
||||
info.setTitle(feed.getTitle());
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.commafeed.frontend.rest.resources;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.Consumes;
|
||||
@@ -22,7 +23,8 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.commafeed.backend.MetricsBean;
|
||||
import com.codahale.metrics.Meter;
|
||||
import com.codahale.metrics.MetricRegistry;
|
||||
import com.commafeed.backend.dao.FeedDAO;
|
||||
import com.commafeed.backend.feeds.FeedParser;
|
||||
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
|
||||
@@ -52,8 +54,13 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
|
||||
@Inject
|
||||
ApplicationSettingsService applicationSettingsService;
|
||||
|
||||
@Inject
|
||||
MetricsBean metricsBean;
|
||||
private Meter pushReceived;
|
||||
|
||||
@PostConstruct
|
||||
public void initMetrics() {
|
||||
pushReceived = metrics.meter(MetricRegistry.name(getClass(), "pushReceived"));
|
||||
|
||||
}
|
||||
|
||||
@Path("/callback")
|
||||
@GET
|
||||
@@ -119,7 +126,7 @@ public class PubSubHubbubCallbackREST extends AbstractREST {
|
||||
log.debug("pushing content to queue for {}", feed.getUrl());
|
||||
taskGiver.add(feed, false);
|
||||
}
|
||||
metricsBean.pushReceived(feeds.size());
|
||||
pushReceived.mark();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Could not parse pubsub callback: " + e.getMessage());
|
||||
|
||||
@@ -10,10 +10,10 @@ import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.feeds.FeedUtils;
|
||||
import com.commafeed.backend.services.ApplicationPropertiesService;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.model.ServerInfo;
|
||||
import com.wordnik.swagger.annotations.Api;
|
||||
import com.wordnik.swagger.annotations.ApiOperation;
|
||||
|
||||
@@ -11,7 +11,6 @@ import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.backend.StartupBean;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserRoleDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
@@ -25,6 +24,7 @@ import com.commafeed.backend.model.UserSettings.ViewMode;
|
||||
import com.commafeed.backend.services.ApplicationSettingsService;
|
||||
import com.commafeed.backend.services.PasswordEncryptionService;
|
||||
import com.commafeed.backend.services.UserService;
|
||||
import com.commafeed.backend.startup.StartupBean;
|
||||
import com.commafeed.frontend.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Settings;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
|
||||
@@ -28,6 +28,8 @@ import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
/**
|
||||
* See http://en.wikipedia.org/wiki/URL_normalization for a reference Note: some
|
||||
* parts of the code are adapted from: http://stackoverflow.com/a/4057470/405418
|
||||
@@ -46,7 +48,7 @@ public class URLCanonicalizer {
|
||||
URL canonicalURL = new URL(UrlResolver.resolveUrl(context == null ? "" : context, href));
|
||||
|
||||
String host = canonicalURL.getHost().toLowerCase();
|
||||
if (host == "") {
|
||||
if (StringUtils.isBlank(host)) {
|
||||
// This is an invalid Url.
|
||||
return null;
|
||||
}
|
||||
|
||||
11
src/main/resources/changelogs/db.changelog-1.3.xml
Normal file
11
src/main/resources/changelogs/db.changelog-1.3.xml
Normal 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>
|
||||
@@ -6,5 +6,6 @@
|
||||
<include file="changelogs/db.changelog-1.0.xml" />
|
||||
<include file="changelogs/db.changelog-1.1.xml" />
|
||||
<include file="changelogs/db.changelog-1.2.xml" />
|
||||
<include file="changelogs/db.changelog-1.3.xml" />
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -17,10 +17,10 @@ subscribe.feed_url=URL Ffrwd
|
||||
subscribe.feed_name=Enw Ffrwd
|
||||
subscribe.category=Categori
|
||||
|
||||
import.google_reader_prefix=Gadawa i mi fewnforio dy ffrydiau o dy
|
||||
import.google_reader_prefix=Gad i mi fewnforio dy ffrydiau o dy
|
||||
import.google_reader_suffix= gyfrif.
|
||||
import.google_download=Fel arall, lanlwytho dy ffeil tanysgrifiadau.xml
|
||||
import.google_download_link=Lawrlwytho fe yma.
|
||||
import.google_download=Fel arall, lanlwytha dy ffeil tanysgrifiadau.xml
|
||||
import.google_download_link=Lawrlwytha fe yma.
|
||||
import.xml_file=Ffeil OPML
|
||||
|
||||
new_category.name=Enw
|
||||
@@ -31,14 +31,14 @@ toolbar.all=Popeth
|
||||
toolbar.previous_entry=Eitem blaenorol
|
||||
toolbar.next_entry=Eitem nesaf
|
||||
toolbar.refresh=Adnewyddu
|
||||
toolbar.refresh_all=Force refresh all my feeds ####### Needs translation
|
||||
toolbar.refresh_all=Gorfodi ail-lwytho pob ffrwd
|
||||
toolbar.sort_by_asc_desc=Trefnu yn ôl dyddiad
|
||||
toolbar.titles_only=Teitlau yn unig
|
||||
toolbar.expanded_view=Golygfa estynedig
|
||||
toolbar.mark_all_as_read=Marcio popeth fel darllenwyd
|
||||
toolbar.mark_all_older_day=Eitemau sy'n hyn na diwrnod
|
||||
toolbar.mark_all_older_week=Eitemau sy'n hyn nag wythnos
|
||||
toolbar.mark_all_older_two_weeks=Eitemau sy'n hyn na phythefnos
|
||||
toolbar.expanded_view=Golwg estynedig
|
||||
toolbar.mark_all_as_read=Nodi'r cyfan fel wedi ei ddarllen
|
||||
toolbar.mark_all_older_day=Eitemau hyn na diwrnod
|
||||
toolbar.mark_all_older_week=Eitemau hyn nag wythnos
|
||||
toolbar.mark_all_older_two_weeks=Eitemau hyn na phythefnos
|
||||
toolbar.settings=Gosodiadau
|
||||
toolbar.profile=Proffil
|
||||
toolbar.admin=Gweinyddwr
|
||||
@@ -46,42 +46,42 @@ toolbar.about=Ynghylch
|
||||
toolbar.logout=Allgofnodi
|
||||
toolbar.donate=Rhoddi
|
||||
|
||||
view.entry_source=from ####### Needs translation
|
||||
view.entry_author=by ####### Needs translation
|
||||
view.error_while_loading_feed=Gwall tra'n llwytho'r ffrwd
|
||||
view.keep_unread=Cadw fel heb ei darllen
|
||||
view.no_unread_items=dim eitemau heb eu darllen
|
||||
view.mark_up_to_here=Mark as read up to here ####### Needs translation
|
||||
view.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
view.entry_source=o
|
||||
view.entry_author=gan
|
||||
view.error_while_loading_feed=Gwall wrth lwytho'r ffrwd
|
||||
view.keep_unread=Parhau i'w nodi fel heb ei ddarllen
|
||||
view.no_unread_items=: Dim eitemau heb eu darllen ###### Cynnwys y colon oherwydd gystrawen y cyd-destyn
|
||||
view.mark_up_to_here=Nodi'r rhai hyd yma fel wedi eu darllen
|
||||
view.search_for=yn chwilio am:
|
||||
view.no_search_results=Ni chanfuwyd unrhyw beth gyda'r geiriau hynny
|
||||
|
||||
feedsearch.hint=Teipio tanysgrifiad...
|
||||
feedsearch.hint=Rho'r tanysgrifiad...
|
||||
feedsearch.help=Defnyddia'r dychwelwr i ddethol a saethau i lywio
|
||||
feedsearch.result_prefix=Dy danysgrifiadau:
|
||||
|
||||
settings.general=Cyffredinol
|
||||
settings.general.language=Iaith
|
||||
settings.general.language.contribute=Cyfrannu gyda chyfieithiadau
|
||||
settings.general.language.contribute=Cyfrannu drwy gyfieithu
|
||||
settings.general.show_unread=Dangos ffrydiau a chategoriau gyda dim eitemau heb eu darllen
|
||||
settings.general.social_buttons=Dangos botymau rhannu
|
||||
settings.general.scroll_marks=Mewn golygfa estynedig, sgrolio trwy eitemau yn marcio fel darllenwyd
|
||||
settings.appearance=Golygfa
|
||||
settings.general.scroll_marks=Marcio eitemau fel wedi eu darllen wrth sgrolio drwyddynt yn y golwg estynedig ###### Defnyddio gystrawen debyg i'r ddau uwch.
|
||||
settings.appearance=Golwg
|
||||
settings.theme=Thema
|
||||
settings.submit_your_theme=Cyflwyno dy thema
|
||||
settings.submit_your_theme=Cyflwyna dy thema
|
||||
settings.custom_css=CSS wedi'i addasu
|
||||
|
||||
details.feed_details=Manylion ffrwd
|
||||
details.url=URL
|
||||
details.website=Website ####### Needs translation
|
||||
details.website=Gwefan
|
||||
details.name=Enw
|
||||
details.category=Categori
|
||||
details.position=Safle
|
||||
details.last_refresh=Adnewyddiad diwethaf
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.message=Neges adnewyddiad diwethaf
|
||||
details.next_refresh=Adnewyddiad nesaf
|
||||
details.queued_for_refresh=Ciwiwyd am adnewyddu
|
||||
details.queued_for_refresh=Ciwiwyd i'w adnewyddu
|
||||
details.feed_url=URL Ffrwd
|
||||
details.generate_api_key_first=Cynhyrchu allwedd API yn dy broffil yn gyntaf.
|
||||
details.generate_api_key_first=Rhaid creu allwedd API yn dy broffil yn gyntaf.
|
||||
details.unsubscribe=Dad-danysgrifio
|
||||
details.category_details=Manylion categori
|
||||
details.parent_category=Categori rhiant
|
||||
@@ -91,59 +91,59 @@ profile.email=E-bost
|
||||
profile.change_password=Newid cyfrinair
|
||||
profile.confirm_password=Cadarnhau cyfrinair
|
||||
profile.minimum_6_chars=Isafswm 6 nod
|
||||
profile.passwords_do_not_match=Cyfrineiriau yn wahanol
|
||||
profile.api_key=allwedd API
|
||||
profile.api_key_not_generated=Heb gynhyrchu eto
|
||||
profile.generate_new_api_key=Cynhyrchu allwedd API newydd
|
||||
profile.generate_new_api_key_info=Newid cyfrinair yn cynhyrchu allwedd API newydd
|
||||
profile.passwords_do_not_match=Mae'r cyfrineiriau yn wahanol
|
||||
profile.api_key=Allwedd API
|
||||
profile.api_key_not_generated=Heb ei gynhyrchu eto
|
||||
profile.generate_new_api_key=Creu allwedd API newydd
|
||||
profile.generate_new_api_key_info=Mae newid cyfrinair yn creu allwedd API newydd
|
||||
profile.opml_export=Allforio OPML
|
||||
profile.delete_account=Dileu cyfrif
|
||||
|
||||
about.rest_api=REST API
|
||||
about.keyboard_shortcuts=Llwybr byr bysellfwrdd
|
||||
about.version=CommaFeed version ####### Needs translation
|
||||
about.line1_prefix=CommaFeed yn prosiect cod agored. Mae'r cod ar
|
||||
about.version=Fersiwn CommaFeed: ###### Cynnwys y colon oherwydd gystrawen y cyd-destun
|
||||
about.line1_prefix=Mae CommaFeed yn prosiect cod agored. Mae'r cod ar
|
||||
about.line1_suffix=.
|
||||
about.line2_prefix=Os wyt ti'n ffeindio problem, plis adrodda fe ar dudalen problemau o'r
|
||||
about.line2_prefix=Os wyt ti'n ffeindio problem, plîs gad wybod amdano ar dudalen problemau o'r
|
||||
about.line2_suffix=\ prosiect.
|
||||
about.line3=Os wyt ti'n hoffi'r prosiect, plis ystyried cyfraniad er mwyn cefnogi'r datblygwr a helpu gyda chynnal a chadw o'r wefan hon.
|
||||
about.line4=I'r rhai sy'n hoff o bitcoin, dyma'r gyfeiriad
|
||||
about.line3=Os wyt ti'n hoffi'r prosiect, plîs ystyria cyfrannu i gefnogi'r datblygwr a helpu gyda chynnal a chadw'r wefan hon.
|
||||
about.line4=I'r rhai sy'n hoff o Bitcoin, dyma'r cyfeiriad
|
||||
about.goodies=Goodies
|
||||
about.goodies.android_app=Android app ####### Needs translation
|
||||
about.goodies.android_app=Ap Android
|
||||
about.goodies.subscribe_url=URL Tanysgrifio
|
||||
about.goodies.chrome_extension=estyniad Chrome
|
||||
about.goodies.firefox_extension=estyniad Firefox
|
||||
about.goodies.opera_extension=estyniad Opera
|
||||
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio (clicio)
|
||||
about.goodies.subscribe_bookmarklet_asc=Oldest first ####### Needs translation
|
||||
about.goodies.subscribe_bookmarklet_desc=Newest first ####### Needs translation
|
||||
about.goodies.subscribe_bookmarklet=Ychwanegu botwm tanysgrifio ###### Dim angen 'Click' - digon amlwg o'r cyd-destyn
|
||||
about.goodies.subscribe_bookmarklet_asc=Hynaf yn gyntaf
|
||||
about.goodies.subscribe_bookmarklet_desc=Diweddaraf yn gyntaf
|
||||
about.goodies.next_unread_bookmarklet=Botwm eitem nesaf heb ei ddarllen (llusgo i far nodau)
|
||||
about.translation=Translation
|
||||
about.translation.message=Rydym ni angen dy help i gyfieithu CommaFeed.
|
||||
about.translation=Cyfieithiad
|
||||
about.translation.message=Rydym angen dy help i gyfieithu CommaFeed.
|
||||
about.translation.link=Gweler sut i gyfrannu i gyfieithiadau.
|
||||
about.announcements=Datganiadau
|
||||
about.rest_api.line1=Mae CommaFeed wedi cael ei adeiladu ar JAX-RS ac AngularJS. Mae REST API ar gael.
|
||||
about.rest_api.line1=Adeiladir CommaFeed ar JAX-RS ac AngularJS. Mae REST API ar gael.
|
||||
about.rest_api.link_to_documentation=Dolen i'r ddogfennaeth.
|
||||
|
||||
about.shortcuts.mouse_middleclick=llygoden clic-canol
|
||||
about.shortcuts.open_next_entry=agor eitem nesaf
|
||||
about.shortcuts.open_previous_entry=agor eitem flaenorol
|
||||
about.shortcuts.spacebar=space/shift+space ####### Needs translation
|
||||
about.shortcuts.move_page_down_up=moves the page down/up ####### Needs translation
|
||||
about.shortcuts.focus_next_entry=gosod ffocws ar eitem nesaf heb ei hagor
|
||||
about.shortcuts.focus_previous_entry=gosod ffocws ar eitem flaenorol heb ei hagor
|
||||
about.shortcuts.open_next_feed=agor ffrwd neu gategori nesaf
|
||||
about.shortcuts.open_previous_feed=agor ffrwd neu gategori blaenorol
|
||||
about.shortcuts.open_close_current_entry=agor/cau eitem gyfredol
|
||||
about.shortcuts.open_current_entry_in_new_window=agor eitem gyfredol mewn ffenestr newydd
|
||||
about.shortcuts.open_current_entry_in_new_window_background=agor eitem gyfredol mewn ffenestr newydd yn y cefndir
|
||||
about.shortcuts.star_unstar=serennu/dadserennu eitem gyfredol
|
||||
about.shortcuts.mark_current_entry=marcio eitem gyfredol fel darllenwyd/heb ddarllen
|
||||
about.shortcuts.mark_all_as_read=marcio popeth fel darllenwyd
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=agor eitem mewn tab newydd a marcio fel darllenwyd
|
||||
about.shortcuts.fullscreen=toggle full screen mode ####### Needs translation
|
||||
about.shortcuts.font_size=increase/decrease font size of the current entry ####### Needs translation
|
||||
about.shortcuts.go_to_all=go to the All view ####### Needs translation
|
||||
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
|
||||
about.shortcuts.feed_search=llywio i danysgrifiad trwy rhoi ei enw mewn
|
||||
about.shortcuts.mouse_middleclick=clic botwm canol llygoden
|
||||
about.shortcuts.open_next_entry=agor yr eitem nesaf
|
||||
about.shortcuts.open_previous_entry=agor yr eitem flaenorol
|
||||
about.shortcuts.spacebar=space/shift+space
|
||||
about.shortcuts.move_page_down_up=symud y tudalen i lawr/fyny
|
||||
about.shortcuts.focus_next_entry=newid ffocws i'r eitem nesaf heb ei hagor
|
||||
about.shortcuts.focus_previous_entry=newid ffocws i'r eitem flaenorol heb ei hagor
|
||||
about.shortcuts.open_next_feed=agor y ffrwd neu gategori nesaf
|
||||
about.shortcuts.open_previous_feed=agor y ffrwd neu gategori blaenorol
|
||||
about.shortcuts.open_close_current_entry=agor/cau yr eitem gyfredol
|
||||
about.shortcuts.open_current_entry_in_new_window=agor yr eitem gyfredol mewn ffenestr newydd
|
||||
about.shortcuts.open_current_entry_in_new_window_background=agor yr eitem gyfredol mewn ffenestr newydd yn y cefndir
|
||||
about.shortcuts.star_unstar=serennu/dadserennu'r eitem gyfredol
|
||||
about.shortcuts.mark_current_entry=marcio'r eitem gyfredol fel wedi/heb ei ddarllen
|
||||
about.shortcuts.mark_all_as_read=marcio popeth fel wedi ei ddarllen
|
||||
about.shortcuts.open_in_new_tab_mark_as_read=agor yr eitem mewn tab newydd a'i farcio fel wedi ei ddarllen
|
||||
about.shortcuts.fullscreen=toglo'r golwg sgrin lawn
|
||||
about.shortcuts.font_size=cynyddu/lleihau maint ffont yr eitem gyfredol
|
||||
about.shortcuts.go_to_all=newid i olwg 'Popeth'
|
||||
about.shortcuts.go_to_starred=newid i olwg 'Serennwyd'
|
||||
about.shortcuts.feed_search=llywio i danysgrifiad gan roi ei enw mewn
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ subscribe.category=Kategória
|
||||
import.google_reader_prefix=Importujte si RSS zdroje s vášho
|
||||
import.google_reader_suffix= účtu.
|
||||
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
|
||||
|
||||
new_category.name=Názov
|
||||
@@ -31,7 +31,7 @@ toolbar.all=Všetky
|
||||
toolbar.previous_entry=Predchádzajúca položka
|
||||
toolbar.next_entry=Nasledujúca položka
|
||||
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.titles_only=Náhľad titulkov
|
||||
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.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.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
view.search_for=Hľadaný výraz:
|
||||
view.no_search_results=Nenašla sa žiadna zhoda pre hľadaný výraz.
|
||||
|
||||
feedsearch.hint=Zadajte názov odoberania...
|
||||
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.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.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.appearance=Vzhľad
|
||||
settings.theme=Motív
|
||||
settings.submit_your_theme=Nahrať vlastný motív
|
||||
settings.custom_css=Vlastný motív (CSS)
|
||||
settings.submit_your_theme=Nahrať vlastný motív vzhľadu
|
||||
settings.custom_css=Vlastný motív vzhľadu (CSS)
|
||||
|
||||
details.feed_details=Detaily odoberania
|
||||
details.url=URL odkaz
|
||||
@@ -76,10 +76,10 @@ details.website=Web stránka
|
||||
details.name=Názov
|
||||
details.category=Kategória
|
||||
details.position=Pozícia
|
||||
details.last_refresh=Posledné obnovenie
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.last_refresh=Predchádzajúce obnovenie
|
||||
details.message=Predchádzajúca správa obnovenia
|
||||
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.generate_api_key_first=Vygenerujte si API kľúč vo vašom profile.
|
||||
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_not_generated=Nie je vygenerovaný
|
||||
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.delete_account=Odstrániť účet
|
||||
|
||||
about.rest_api=REST API
|
||||
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_suffix=.
|
||||
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.firefox_extension=Rozšírenie pre prehliadač Firefox
|
||||
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_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.translation=Preklad
|
||||
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.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.
|
||||
@@ -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.open_next_entry=zobraziť nasledujú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.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.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_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_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_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.fullscreen=zapnúť/vypnúť zobrazenie na celú obrazovku
|
||||
about.shortcuts.font_size=zväčšiť/zmenšiť veľkost písma pre vybranú položku
|
||||
about.shortcuts.go_to_all=go to the All view ####### Needs translation
|
||||
about.shortcuts.go_to_starred=go to the Starred view ####### Needs translation
|
||||
about.shortcuts.feed_search=prejsť k odoberanému RSS zdroju vložením jeho názvu
|
||||
|
||||
about.shortcuts.fullscreen=prepnutie zobrazenia na celú obrazovku
|
||||
about.shortcuts.font_size=zmeniť veľkosť písma pre vybranú položku
|
||||
about.shortcuts.go_to_all=zobraziť všetky položky
|
||||
about.shortcuts.go_to_starred=zobraziť obľúbené položiek
|
||||
about.shortcuts.feed_search=presun na odoberaný RSS zdroj vložením jeho názvu
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
global.save=Spara
|
||||
global.cancel=Avbryt
|
||||
global.delete=Radera
|
||||
global.required=Obligatorisk
|
||||
global.download=Ladda ned
|
||||
global.link=Länka
|
||||
global.bookmark=Bokmärk
|
||||
global.close=Stäng
|
||||
|
||||
tree.subscribe=Prenumerera
|
||||
tree.import=Importera
|
||||
tree.new_category=Ny kategori
|
||||
tree.all=Alla
|
||||
tree.starred=Stjärnmärkt
|
||||
|
||||
subscribe.feed_url=Prenumerationens URL
|
||||
subscribe.feed_name=Prenumerationens namn
|
||||
subscribe.category=Kategori
|
||||
|
||||
import.google_reader_prefix=Låt mig importera dina prenumerationer från ditt
|
||||
import.google_reader_suffix=-konto.
|
||||
import.google_download=Alternativt, ladda upp din subscriptions.xml-fil.
|
||||
import.google_download_link=Ladda ned den här.
|
||||
import.xml_file=OPML-fil
|
||||
|
||||
new_category.name=Namn
|
||||
new_category.parent=Överordnad
|
||||
|
||||
toolbar.unread=Oläst
|
||||
toolbar.all=Alla
|
||||
toolbar.previous_entry=Föregående post
|
||||
toolbar.next_entry=Nästa post
|
||||
toolbar.refresh=Uppdatera
|
||||
toolbar.refresh_all=Tvinga uppdatering av alla prenumerationer
|
||||
toolbar.sort_by_asc_desc=Sortera efter datum stigande/fallande
|
||||
toolbar.titles_only=Endast titlar
|
||||
toolbar.expanded_view=Expanderad vy
|
||||
toolbar.mark_all_as_read=Markera alla som lästa
|
||||
toolbar.mark_all_older_day=Poster äldre än en dag
|
||||
toolbar.mark_all_older_week=Poster äldre än en vecka
|
||||
toolbar.mark_all_older_two_weeks=Poster äldre än två veckor
|
||||
toolbar.settings=Inställningar
|
||||
toolbar.profile=Profil
|
||||
toolbar.admin=Administratör
|
||||
toolbar.about=Om
|
||||
toolbar.logout=Logga ut
|
||||
toolbar.donate=Donera
|
||||
|
||||
view.entry_source=från
|
||||
view.entry_author=av
|
||||
view.error_while_loading_feed=Fel under laddning av denna prenumeration
|
||||
view.keep_unread=Håll oläst
|
||||
view.no_unread_items=har inga olästa poster.
|
||||
view.mark_up_to_here=Markera som läst upp till denna post
|
||||
view.search_for=searching for: ####### Needs translation
|
||||
view.no_search_results=No match found for the requested keywords ####### Needs translation
|
||||
|
||||
feedsearch.hint=Skriv in en prenumeration...
|
||||
feedsearch.help=Använd retur-tangenten för att välja och piltangenterna för att navigera.
|
||||
feedsearch.result_prefix=Dina prenumerationer:
|
||||
|
||||
settings.general=Allmänt
|
||||
settings.general.language=Språk
|
||||
settings.general.language.contribute=Bidra med översättningar
|
||||
settings.general.show_unread=Visa prenumerationer och kategorier utan olästa poster
|
||||
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.appearance=Utseende
|
||||
settings.theme=Tema
|
||||
settings.submit_your_theme=Skicka in ditt tema
|
||||
settings.custom_css=Anpassad CSS
|
||||
|
||||
details.feed_details=Prenumerationsdetaljer
|
||||
details.url=URL
|
||||
details.website=Webbsida
|
||||
details.name=Namn
|
||||
details.category=Kategori
|
||||
details.position=Position
|
||||
details.last_refresh=Senaste uppdatering
|
||||
details.message=Last refresh message ####### Needs translation
|
||||
details.next_refresh=Nästa uppdatering
|
||||
details.queued_for_refresh=I kö för uppdatering
|
||||
details.feed_url=Prenumerationens URL
|
||||
details.generate_api_key_first=Skapa en API-nyckel på din profil först.
|
||||
details.unsubscribe=Avprenumerera
|
||||
details.category_details=Kategoridetaljer
|
||||
details.parent_category=Överordnad kategori
|
||||
|
||||
profile.user_name=Användarnamn
|
||||
profile.email=E-mail
|
||||
profile.change_password=Ändra lösenord
|
||||
profile.confirm_password=Bekräfta lösenord
|
||||
profile.minimum_6_chars=Minst 6 bokstäver
|
||||
profile.passwords_do_not_match=Lösenorden matchar inte
|
||||
profile.api_key=API-nyckel
|
||||
profile.api_key_not_generated=Inte skapad än
|
||||
profile.generate_new_api_key=Skapa ny API-nyckel
|
||||
profile.generate_new_api_key_info=Lösenordsbyte skapar ny API-nyckel
|
||||
profile.opml_export=OPML-export
|
||||
profile.delete_account=Radera konto
|
||||
|
||||
about.rest_api=REST-API
|
||||
about.keyboard_shortcuts=Tangentbordsgenvägar
|
||||
about.version=CommaFeed-version
|
||||
about.line1_prefix=CommaFeed är ett open-source-projekt. Källan är tillgänglig på
|
||||
about.line1_suffix=.
|
||||
about.line2_prefix=Om du träffar på ett problem, meddela det på "Issues"-sidan för
|
||||
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.line4=För er som föredrar Bitcoin, här är adressen
|
||||
about.goodies=Godsaker
|
||||
about.goodies.android_app=Android-app
|
||||
about.goodies.subscribe_url=Prenumerations-URL
|
||||
about.goodies.chrome_extension=Chrome-tillägg
|
||||
about.goodies.firefox_extension=Firefox-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_asc=äldst 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.translation=Översättning
|
||||
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.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.link_to_documentation=Länk till dokumentation.
|
||||
|
||||
about.shortcuts.mouse_middleclick=mitten-musknapp
|
||||
about.shortcuts.open_next_entry=öppna nästa post
|
||||
about.shortcuts.open_previous_entry=öppna föregående post
|
||||
about.shortcuts.spacebar=mellanslag/shift+mellanslag
|
||||
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_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_previous_feed=öppna föregående prenumeration eller kategori
|
||||
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_background=öppna nuvarande post i nytt bakgrundsfönster
|
||||
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_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.fullscreen=växla till/från fullskärmsläge
|
||||
about.shortcuts.font_size=öka/minska teckenstorlek av nuvarande post
|
||||
about.shortcuts.go_to_all=se alla poster
|
||||
about.shortcuts.go_to_starred=se stjärnmärkta poster
|
||||
about.shortcuts.feed_search=navigera till en prenumeration via prenumerationsnamn
|
||||
|
||||
global.save=Spara
|
||||
global.cancel=Avbryt
|
||||
global.delete=Radera
|
||||
global.required=Obligatorisk
|
||||
global.download=Ladda ned
|
||||
global.link=Länka
|
||||
global.bookmark=Bokmärk
|
||||
global.close=Stäng
|
||||
|
||||
tree.subscribe=Prenumerera
|
||||
tree.import=Importera
|
||||
tree.new_category=Ny kategori
|
||||
tree.all=Alla
|
||||
tree.starred=Stjärnmärkt
|
||||
|
||||
subscribe.feed_url=Prenumerationens URL
|
||||
subscribe.feed_name=Prenumerationens namn
|
||||
subscribe.category=Kategori
|
||||
|
||||
import.google_reader_prefix=Låt mig importera dina prenumerationer från ditt
|
||||
import.google_reader_suffix=-konto.
|
||||
import.google_download=Alternativt, ladda upp din subscriptions.xml-fil.
|
||||
import.google_download_link=Ladda ned den här.
|
||||
import.xml_file=OPML-fil
|
||||
|
||||
new_category.name=Namn
|
||||
new_category.parent=Överordnad
|
||||
|
||||
toolbar.unread=Oläst
|
||||
toolbar.all=Alla
|
||||
toolbar.previous_entry=Föregående post
|
||||
toolbar.next_entry=Nästa post
|
||||
toolbar.refresh=Uppdatera
|
||||
toolbar.refresh_all=Tvinga uppdatering av alla prenumerationer
|
||||
toolbar.sort_by_asc_desc=Sortera efter datum stigande/fallande
|
||||
toolbar.titles_only=Endast titlar
|
||||
toolbar.expanded_view=Expanderad vy
|
||||
toolbar.mark_all_as_read=Markera alla som lästa
|
||||
toolbar.mark_all_older_day=Poster äldre än en dag
|
||||
toolbar.mark_all_older_week=Poster äldre än en vecka
|
||||
toolbar.mark_all_older_two_weeks=Poster äldre än två veckor
|
||||
toolbar.settings=Inställningar
|
||||
toolbar.profile=Profil
|
||||
toolbar.admin=Administratör
|
||||
toolbar.about=Om
|
||||
toolbar.logout=Logga ut
|
||||
toolbar.donate=Donera
|
||||
|
||||
view.entry_source=från
|
||||
view.entry_author=av
|
||||
view.error_while_loading_feed=Fel under laddning av denna prenumeration
|
||||
view.keep_unread=Håll oläst
|
||||
view.no_unread_items=har inga olästa poster.
|
||||
view.mark_up_to_here=Markera som läst upp till denna post
|
||||
view.search_for=söker efter:
|
||||
view.no_search_results=Inga resultat för valda nyckelord
|
||||
|
||||
feedsearch.hint=Skriv in en prenumeration...
|
||||
feedsearch.help=Använd retur-tangenten för att välja och piltangenterna för att navigera.
|
||||
feedsearch.result_prefix=Dina prenumerationer:
|
||||
|
||||
settings.general=Allmänt
|
||||
settings.general.language=Språk
|
||||
settings.general.language.contribute=Bidra med översättningar
|
||||
settings.general.show_unread=Visa prenumerationer och kategorier utan olästa poster
|
||||
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.appearance=Utseende
|
||||
settings.theme=Tema
|
||||
settings.submit_your_theme=Skicka in ditt tema
|
||||
settings.custom_css=Anpassad CSS
|
||||
|
||||
details.feed_details=Prenumerationsdetaljer
|
||||
details.url=URL
|
||||
details.website=Webbsida
|
||||
details.name=Namn
|
||||
details.category=Kategori
|
||||
details.position=Position
|
||||
details.last_refresh=Senaste uppdatering
|
||||
details.message=Senaste uppdateringsmeddelande
|
||||
details.next_refresh=Nästa uppdatering
|
||||
details.queued_for_refresh=I kö för uppdatering
|
||||
details.feed_url=Prenumerationens URL
|
||||
details.generate_api_key_first=Skapa en API-nyckel på din profil först.
|
||||
details.unsubscribe=Avprenumerera
|
||||
details.category_details=Kategoridetaljer
|
||||
details.parent_category=Överordnad kategori
|
||||
|
||||
profile.user_name=Användarnamn
|
||||
profile.email=E-mail
|
||||
profile.change_password=Ändra lösenord
|
||||
profile.confirm_password=Bekräfta lösenord
|
||||
profile.minimum_6_chars=Minst 6 bokstäver
|
||||
profile.passwords_do_not_match=Lösenorden matchar inte
|
||||
profile.api_key=API-nyckel
|
||||
profile.api_key_not_generated=Inte skapad än
|
||||
profile.generate_new_api_key=Skapa ny API-nyckel
|
||||
profile.generate_new_api_key_info=Lösenordsbyte skapar ny API-nyckel
|
||||
profile.opml_export=OPML-export
|
||||
profile.delete_account=Radera konto
|
||||
|
||||
about.rest_api=REST-API
|
||||
about.keyboard_shortcuts=Tangentbordsgenvägar
|
||||
about.version=CommaFeed-version
|
||||
about.line1_prefix=CommaFeed är ett open-source-projekt. Källan är tillgänglig på
|
||||
about.line1_suffix=.
|
||||
about.line2_prefix=Om du träffar på ett problem, meddela det på "Issues"-sidan för
|
||||
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.line4=För er som föredrar Bitcoin, här är adressen
|
||||
about.goodies=Godsaker
|
||||
about.goodies.android_app=Android-app
|
||||
about.goodies.subscribe_url=Prenumerations-URL
|
||||
about.goodies.chrome_extension=Chrome-tillägg
|
||||
about.goodies.firefox_extension=Firefox-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_asc=äldst 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.translation=Översättning
|
||||
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.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.link_to_documentation=Länk till dokumentation.
|
||||
|
||||
about.shortcuts.mouse_middleclick=mitten-musknapp
|
||||
about.shortcuts.open_next_entry=öppna nästa post
|
||||
about.shortcuts.open_previous_entry=öppna föregående post
|
||||
about.shortcuts.spacebar=mellanslag/shift+mellanslag
|
||||
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_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_previous_feed=öppna föregående prenumeration eller kategori
|
||||
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_background=öppna nuvarande post i nytt bakgrundsfönster
|
||||
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_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.fullscreen=växla till/från fullskärmsläge
|
||||
about.shortcuts.font_size=öka/minska teckenstorlek av nuvarande post
|
||||
about.shortcuts.go_to_all=se alla poster
|
||||
about.shortcuts.go_to_starred=se stjärnmärkta poster
|
||||
about.shortcuts.feed_search=navigera till en prenumeration via prenumerationsnamn
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ log4j.logger.com.commafeed=INFO, CONSOLE
|
||||
log4j.logger.org=WARN, CONSOLE
|
||||
log4j.logger.ro=WARN, CONSOLE
|
||||
log4j.logger.com.wordnik=FATAL, CONSOLE
|
||||
log4j.logger.com.codahale=WARN, CONSOLE
|
||||
|
||||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||
|
||||
@@ -665,15 +665,17 @@ module.controller('FeedListCtrl', [
|
||||
'$route',
|
||||
'$state',
|
||||
'$window',
|
||||
'$timeout',
|
||||
'$location',
|
||||
'EntryService',
|
||||
'SettingsService',
|
||||
'FeedService',
|
||||
'CategoryService',
|
||||
'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) {
|
||||
|
||||
$window = angular.element($window);
|
||||
AnalyticsService.track();
|
||||
|
||||
$scope.keywords = $location.search().q;
|
||||
@@ -727,9 +729,6 @@ module.controller('FeedListCtrl', [
|
||||
}
|
||||
|
||||
var callback = function(data) {
|
||||
if (data.offset === 0) {
|
||||
$scope.entries = [];
|
||||
}
|
||||
for ( var i = 0; i < data.entries.length; i++) {
|
||||
var entry = data.entries[i];
|
||||
if (!_.some($scope.entries, {
|
||||
@@ -758,6 +757,79 @@ module.controller('FeedListCtrl', [
|
||||
}, 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) {
|
||||
$state.transitionTo('feeds.view', {
|
||||
_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) {
|
||||
$scope.$apply(function() {
|
||||
$scope.navigationMode = 'keyboard';
|
||||
@@ -1416,10 +1478,13 @@ module.controller('HelpController', ['$scope', 'CategoryService', 'AnalyticsServ
|
||||
}]);
|
||||
|
||||
module.controller('FooterController', ['$scope', function($scope) {
|
||||
|
||||
var baseUrl = window.location.href.substring(0, window.location.href.lastIndexOf('#'));
|
||||
var hostname = window.location.hostname;
|
||||
$scope.subToMeUrl = baseUrl + 'rest/feed/subscribe?url={feed}';
|
||||
$scope.subToMeName = hostname.indexOf('www.commafeed.com') !== -1 ? 'CommaFeed' : 'CommaFeed (' + hostname + ')';
|
||||
|
||||
}]);
|
||||
|
||||
module.controller('MetricsCtrl', ['$scope', 'AdminMetricsService', function($scope, AdminMetricsService) {
|
||||
$scope.metrics = AdminMetricsService.get();
|
||||
}]);
|
||||
|
||||
@@ -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
|
||||
* reaches top or bottom
|
||||
@@ -409,27 +319,24 @@ module.directive('droppable', ['CategoryService', 'FeedService', function(Catego
|
||||
};
|
||||
}]);
|
||||
|
||||
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();
|
||||
};
|
||||
module.directive('metricMeter', function() {
|
||||
return {
|
||||
scope : {
|
||||
metric : '=',
|
||||
label : '='
|
||||
},
|
||||
restrict : 'E',
|
||||
templateUrl : 'templates/_metrics.meter.html'
|
||||
};
|
||||
});
|
||||
|
||||
var tokens = keywords.split(' ');
|
||||
for ( var i = 0; i < tokens.length; i++) {
|
||||
html = handleKeyword(tokens[i], html);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
module.directive('metricGauge', function() {
|
||||
return {
|
||||
scope : {
|
||||
metric : '=',
|
||||
label : '='
|
||||
},
|
||||
restrict : 'E',
|
||||
templateUrl : 'templates/_metrics.gauge.html'
|
||||
};
|
||||
});
|
||||
@@ -20,4 +20,29 @@ module.filter('entryDate', function() {
|
||||
|
||||
module.filter('escape', function() {
|
||||
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;
|
||||
};
|
||||
});
|
||||
@@ -100,7 +100,12 @@ app.config(['$routeProvider', '$stateProvider', '$urlRouterProvider', '$httpProv
|
||||
templateUrl : 'templates/admin.settings.html',
|
||||
controller : 'ManageSettingsCtrl'
|
||||
});
|
||||
|
||||
$stateProvider.state('admin.metrics', {
|
||||
url : '/metrics',
|
||||
templateUrl : 'templates/admin.metrics.html',
|
||||
controller : 'MetricsCtrl'
|
||||
});
|
||||
|
||||
$urlRouterProvider.when('/', '/feeds/view/category/all');
|
||||
$urlRouterProvider.when('/admin', '/admin/settings');
|
||||
$urlRouterProvider.otherwise('/');
|
||||
|
||||
@@ -291,6 +291,12 @@ module.factory('AdminSettingsService', ['$resource', function($resource) {
|
||||
return res;
|
||||
}]);
|
||||
|
||||
module.factory('AdminMetricsService', ['$resource', function($resource) {
|
||||
var res = $resource('rest/admin/metrics/');
|
||||
return res;
|
||||
}]);
|
||||
|
||||
|
||||
module.factory('AdminCleanupService', ['$resource', function($resource) {
|
||||
var actions = {
|
||||
findDuplicateFeeds : {
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-horizontal .control-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bidi-embed {
|
||||
unicode-bidi: embed;
|
||||
}
|
||||
@@ -65,6 +69,10 @@
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn,.btn-large,.btn-small,.btn-mini {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
}
|
||||
body.left-menu-active .sidebar-nav-fixed {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
body.left-menu-active .main-content {
|
||||
display: none !important;
|
||||
|
||||
@@ -1,66 +1,110 @@
|
||||
#theme-svetla {
|
||||
/*background color*/
|
||||
body, div.form-actions, .toolbar {
|
||||
background: #E3D4D1;
|
||||
}
|
||||
/*bg color*/
|
||||
body, div.form-actions, .toolbar, .entrylist-header ng-scope {
|
||||
background: #AFB8BE;
|
||||
}
|
||||
|
||||
div.form-actions, div.page-header {
|
||||
border: none;
|
||||
}
|
||||
.form-actions, div.page-header {
|
||||
border: none;
|
||||
}
|
||||
|
||||
pre, #feed-accordion .unread .entry-heading {
|
||||
background: transparent;
|
||||
}
|
||||
pre, #feed-accordion .unread .entry-heading {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/*feeds tree*/
|
||||
.css-treeview li .tree-item:hover {
|
||||
background: grey;
|
||||
}
|
||||
/*feeds tree*/
|
||||
.css-treeview li .tree-item:hover {
|
||||
}
|
||||
|
||||
/*feeds list*/
|
||||
/*feeds list*/
|
||||
|
||||
#feed-accordion .entry-buttons { /*share panel*/
|
||||
background: transparent;
|
||||
/* uncomment if ---> border: 1px solid black; */
|
||||
}
|
||||
#feed-accordion .entry-buttons { /*share panel*/
|
||||
background: transparent;
|
||||
border:none;
|
||||
/* ---> border: 1px solid black; */
|
||||
}
|
||||
|
||||
#feed-accordion .entry {
|
||||
border: none;
|
||||
}
|
||||
#feed-accordion .entry {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#feed-accordion .unread .entry-heading:hover {
|
||||
background: grey;
|
||||
}
|
||||
/* read feed bg */
|
||||
#feed-accordion .entry-heading {
|
||||
}
|
||||
|
||||
/* readed feed background */
|
||||
#feed-accordion .entry-heading {
|
||||
background: #987B77;
|
||||
}
|
||||
.dropdown-menu.pull-right li.divider {
|
||||
height: 0px;
|
||||
background: transparent;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
ul.dropdown-menu.pull-right li.divider {
|
||||
height: 0px;
|
||||
background: transparent;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
background: #E6E6E6;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
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 {
|
||||
background: #B8ACA4;
|
||||
border: 1px solid grey;/* ! */
|
||||
}
|
||||
.btn.dropdown-toggle {
|
||||
/*background: #eae7e3;*/
|
||||
}
|
||||
|
||||
button.btn.dropdown-toggle {
|
||||
background: #7B7672;
|
||||
}
|
||||
.btn.active {
|
||||
box-shadow: none;/*!*/
|
||||
}
|
||||
|
||||
button.btn.dropdown-toggle:hover {
|
||||
background: #e6e6e6;
|
||||
}
|
||||
span.hidden-phone.hidden-tablet.ng-binding {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
button.btn.dropdown-toggle:hover {
|
||||
background: #e6e6e6;
|
||||
}
|
||||
span.hidden-phone.hidden-tablet.ng-binding {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
4
src/main/webapp/templates/_metrics.gauge.html
Normal file
4
src/main/webapp/templates/_metrics.gauge.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<span>{{label}}</span>
|
||||
<span>{{metric.value}}</span>
|
||||
</div>
|
||||
20
src/main/webapp/templates/_metrics.meter.html
Normal file
20
src/main/webapp/templates/_metrics.meter.html
Normal 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>
|
||||
13
src/main/webapp/templates/admin.metrics.html
Normal file
13
src/main/webapp/templates/admin.metrics.html
Normal 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>
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 ui-view></div>
|
||||
</div>
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
ng-class="{'expanded' : settingsService.settings.viewMode == 'expanded' }">
|
||||
<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}}"
|
||||
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 }">
|
||||
<div class="entry-heading">
|
||||
<a href="{{entry.url}}" target="_blank" class="entry-heading-link" ng-click="noop($event)" ng-mouseup="entryClicked(entry, $event)">
|
||||
@@ -85,25 +83,28 @@
|
||||
</label>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
6
src/main/webapp/vendor/zocial/zocial.css
vendored
6
src/main/webapp/vendor/zocial/zocial.css
vendored
@@ -38,4 +38,10 @@
|
||||
font-style: normal;
|
||||
font-family: 'zocial';
|
||||
content: "\00E5";
|
||||
}
|
||||
|
||||
.icon-gmail:before {
|
||||
font-style: normal;
|
||||
font-family: 'zocial';
|
||||
content: "m";
|
||||
}
|
||||
Reference in New Issue
Block a user