Compare commits

...

40 Commits
1.2.0 ... 1.3.0

Author SHA1 Message Date
Athou
1f4d62ab47 1.3.0 release 2013-09-25 13:47:13 +02:00
Athou
a7b826bd4f prevent unintentional entry list reset 2013-09-20 08:11:28 +02:00
Athou
407481faa6 delete operation does not support limit. limit on select and delete afterwards 2013-09-18 09:24:31 +02:00
Athou
305b68546c create a new transaction for each delete chunk 2013-09-18 09:24:05 +02:00
Athou
136c41c6aa delete old read statuses by chunks in order to avoid large transactions 2013-09-17 13:01:27 +02:00
Athou
587b25b18b Merge pull request #510 from Cymrodor/patch-1
Update cy.properties
2013-09-16 19:40:29 -07:00
Cymrodor
beaa40ad65 Update cy.properties
Ychwanegu, cwtogi, cywiro a thacluso.
2013-09-16 23:21:39 +01:00
Athou
1389a5a238 readme update 2013-09-16 20:32:37 +02:00
Athou
2f34ff8a9f prevent NPE if session does not exist 2013-09-16 07:01:47 +02:00
Athou
d3626b0e7c reduce blockquotes font size 2013-09-10 19:07:17 +02:00
Athou
bb4529b6f1 improve scrolling performance by registering events only once instead of once per entry 2013-09-10 16:12:39 +02:00
Athou
dd94125d52 remove unneeded synchronization locks on settings 2013-09-08 19:08:26 +02:00
Athou
a7149e3740 don't start a new reporter every time the registry is injected 2013-09-05 16:30:14 +02:00
Athou
b64d041385 Merge pull request #505 from ekovi/patch-3
Update _svetla.scss
2013-09-01 01:36:17 -07:00
Athou
cc04bdfbc5 Merge pull request #504 from LpSamuelm/patch-17
Translated new labels to Swedish
2013-09-01 01:34:53 -07:00
Athou
d8c772ed5e compact forms 2013-09-01 10:33:36 +02:00
Athou
dfcc4eeebd return an error message when feed/category is not found instead of returning an empty feed/category 2013-09-01 10:33:35 +02:00
ekovi
e491841d4a Update _svetla.scss
some changes and fixes
2013-08-30 20:37:36 +02:00
LpSamuelm
ccb72837b3 Translated new labels to Swedish 2013-08-30 09:24:47 +02:00
Athou
6560fc9d05 display gauges as well 2013-08-23 14:12:13 +02:00
Athou
14d5879735 fix issue where only the first directive was shown 2013-08-23 13:40:23 +02:00
Athou
7fa8bef3de initial metrics page setup 2013-08-23 12:58:24 +02:00
Athou
966caae727 store and use urlAfterRedirect if different than the actual url 2013-08-22 15:55:05 +02:00
Athou
a14484ee03 retrieve the final url after potential http 30x redirect 2013-08-22 15:36:04 +02:00
Athou
fb9b42ab12 added log4j entry for metrics 2013-08-22 15:27:24 +02:00
Athou
6974abdb95 don't compare strings with == 2013-08-22 12:04:00 +02:00
Athou
65efdeb1df wicket update 2013-08-22 09:13:56 +02:00
Athou
54a39ea0a9 fix scrolling issues on some mobile devices (#482) 2013-08-22 09:13:56 +02:00
Athou
641350cbde detect categories in opml files by checking if they have children 2013-08-22 06:20:44 +02:00
Athou
06ece8f5ee Merge pull request #497 from ekovi/patch-1
translation of additional entries
2013-08-21 20:44:32 -07:00
ekovi
ca87f1c47a translation of additional entries 2013-08-21 21:17:06 +02:00
Athou
c38ddb5d00 add a note about hsqldb data location (fix #496) 2013-08-21 13:04:12 +02:00
Athou
1acd7c4a01 set serialid 2013-08-20 09:47:08 +02:00
Athou
d92c2ebdf7 measure refill rate 2013-08-18 17:19:01 +02:00
Athou
8f19e9408e report through jmx 2013-08-18 17:13:45 +02:00
Athou
3ecb47da5a use timers instead of meters 2013-08-18 16:42:01 +02:00
Athou
ae03b42c6d pretty print response if method is annotated with @PrettyPrint or 'pretty' req param is set to true 2013-08-18 16:29:41 +02:00
Athou
ee4eb9bb07 use codahale metrics library instead of our own 2013-08-18 16:29:07 +02:00
Athou
a0be2e0879 added gmail social sharing button 2013-08-17 21:55:29 +02:00
Athou
a3414d7156 let's use snapshots 2013-08-17 13:47:14 +02:00
54 changed files with 860 additions and 742 deletions

View File

@@ -74,12 +74,15 @@ It will generate a zip file at `target/commafeed.zip` with everything you need t
* If you don't use the embedded database, create a database in your external database instance, then uncomment the `Resource` element corresponding to the database engine you use from `conf/tomee.xml` and edit the default credentials.
* If you'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
View File

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

View File

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

View File

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

View File

@@ -4,27 +4,26 @@ import java.util.Date;
import javax.ejb.Schedule;
import javax.ejb.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

View File

@@ -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();
}
}

View File

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

View File

@@ -7,6 +7,9 @@ import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
/**
* Wraps a {@link ThreadPoolExecutor} instance. Blocks when queue is full instead of rejecting the task. Allow priority queueing by using
* {@link Task} instead of {@link Runnable}
@@ -19,7 +22,7 @@ public class FeedRefreshExecutor {
private ThreadPoolExecutor pool;
private LinkedBlockingDeque<Runnable> queue;
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity) {
public FeedRefreshExecutor(final String poolName, int threads, int queueCapacity, MetricRegistry metrics) {
log.info("Creating pool {} with {} threads", poolName, threads);
this.poolName = poolName;
pool = new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, queue = new LinkedBlockingDeque<Runnable>(queueCapacity) {
@@ -51,20 +54,26 @@ public class FeedRefreshExecutor {
}
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "active"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return pool.getActiveCount();
}
});
metrics.register(MetricRegistry.name(getClass(), poolName, "pending"), new Gauge<Integer>() {
@Override
public Integer getValue() {
return queue.size();
}
});
}
public void execute(Task task) {
pool.execute(task);
}
public int getQueueSize() {
return queue.size();
}
public int getActiveCount() {
return pool.getActiveCount();
}
public static interface Task extends Runnable {
boolean isUrgent();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.commafeed.backend.feeds;
package com.commafeed.backend.opml;
import java.util.Date;
import java.util.List;

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.commafeed.backend;
package com.commafeed.backend.services;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -15,15 +16,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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,7 +37,6 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
@@ -47,8 +46,6 @@ import com.commafeed.backend.feeds.FeedFetcher;
import com.commafeed.backend.feeds.FeedRefreshTaskGiver;
import com.commafeed.backend.feeds.FeedUtils;
import com.commafeed.backend.feeds.FetchedFeed;
import com.commafeed.backend.feeds.OPMLExporter;
import com.commafeed.backend.feeds.OPMLImporter;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
@@ -56,9 +53,12 @@ import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingMode;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.opml.OPMLExporter;
import com.commafeed.backend.opml.OPMLImporter;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.FeedEntryService;
import com.commafeed.backend.services.FeedSubscriptionService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
@@ -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) {

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
@@ -25,6 +24,7 @@ import com.commafeed.backend.model.UserSettings.ViewMode;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService;
import com.commafeed.backend.startup.StartupBean;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Settings;
import com.commafeed.frontend.model.UserModel;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}]);

View File

@@ -59,96 +59,6 @@ module.directive('ngBlur', function() {
};
});
/**
* Fired when the top of the element is not visible anymore
*/
module.directive('onScrollMiddle', function() {
return {
restrict : 'A',
link : function(scope, element, attrs) {
var w = $(window);
var e = $(element);
var d = $(document);
var offset = parseInt(attrs.onScrollMiddleOffset, 10);
var down = function() {
var docTop = w.scrollTop();
var elemTop = e.offset().top;
var threshold = docTop === 0 ? elemTop - 1 : docTop + offset;
return (elemTop > threshold) ? 'below' : 'above';
};
var up = function() {
var docTop = w.scrollTop();
var elemTop = e.offset().top;
var elemBottom = elemTop + e.height();
var threshold = docTop === 0 ? elemBottom - 1 : docTop + offset;
return (elemBottom > threshold) ? 'below' : 'above';
};
if (!w.data.scrollInit) {
w.data.scrollPosition = d.scrollTop();
w.data.scrollDirection = 'down';
var onScroll = function(e) {
var scroll = d.scrollTop();
w.data.scrollDirection = (scroll - w.data.scrollPosition > 0) ? 'down' : 'up';
w.data.scrollPosition = scroll;
scope.$apply();
};
w.bind('scroll', _.throttle(onScroll, 500));
w.data.scrollInit = true;
}
scope.$watch(down, function downCallback(value, oldValue) {
if (value != oldValue && value == 'above')
scope.$eval(attrs.onScrollMiddle);
});
scope.$watch(up, function upCallback(value, oldValue) {
if (value != oldValue && value == 'below')
scope.$eval(attrs.onScrollMiddle);
});
}
};
});
/**
* Scrolls to the element if the value is true and the attribute is not fully
* visible, unless the attribute scroll-to-force is true
*/
module.directive('scrollTo', ['$timeout', function($timeout) {
return {
restrict : 'A',
link : function(scope, element, attrs) {
scope.$watch(attrs.scrollTo, function(value) {
if (!value)
return;
var force = scope.$eval(attrs.scrollToForce);
// timeout here to execute after dom update
$timeout(function() {
var docTop = $(window).scrollTop();
var docBottom = docTop + $(window).height();
var elemTop = $(element).offset().top;
var elemBottom = elemTop + $(element).height();
if (!force && (elemTop > docTop) && (elemBottom < docBottom)) {
// element is entirely visible
return;
} else {
var offset = parseInt(attrs.scrollToOffset, 10);
var scrollTop = $(element).offset().top + offset;
$('html, body').animate({
scrollTop : scrollTop
}, 0);
}
});
});
}
};
}]);
/**
* Prevent mousewheel scrolling from propagating to the parent when scrollbar
* 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'
};
});

View File

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

View File

@@ -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('/');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
</div>
</div>
<div 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>

View File

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

View File

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