forked from Archives/Athou_commafeed
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8e254dab6 | ||
|
|
4059160d90 | ||
|
|
e0f242fe22 | ||
|
|
05453364ff | ||
|
|
c3aedd935d | ||
|
|
99a7f72448 | ||
|
|
56ae1eadbc | ||
|
|
4828c03bbf | ||
|
|
cfc07764b4 | ||
|
|
91938cc3b9 | ||
|
|
c62a84a9ea | ||
|
|
0b16b6bb86 | ||
|
|
6a8f7f0a40 | ||
|
|
42ca0967b6 | ||
|
|
deb29f0e88 | ||
|
|
714af986b0 | ||
|
|
4ff26366a5 | ||
|
|
9c628a8f53 | ||
|
|
4a40f2b8f7 | ||
|
|
9a2dda626c | ||
|
|
a9ff491da0 | ||
|
|
5c5a7d20de | ||
|
|
05ae4eb529 | ||
|
|
15f93b198c | ||
|
|
0a99dacb6b | ||
|
|
00f6c04611 | ||
|
|
d9b899b53f | ||
|
|
d96f8da8fd | ||
|
|
ababcf7850 | ||
|
|
f23bfaf694 | ||
|
|
cac05dee0b | ||
|
|
155c93d371 | ||
|
|
9a61ee7530 | ||
|
|
4bea1c5e5c | ||
|
|
9ccc26b0b0 | ||
|
|
5cd3787d6f | ||
|
|
807b1f62a1 | ||
|
|
c15db54d5a | ||
|
|
aa7b078121 | ||
|
|
99130d0181 | ||
|
|
90e2036cbe | ||
|
|
c2f3e42867 | ||
|
|
bd33369a41 | ||
|
|
4f625d8ed5 | ||
|
|
866fe56dd2 | ||
|
|
5f37dbca4c | ||
|
|
c49e617dfe | ||
|
|
e763ffd4cf | ||
|
|
20ab7dd3e1 | ||
|
|
55741c6332 | ||
|
|
42d85336a8 | ||
|
|
639b82f494 | ||
|
|
5003c176a2 | ||
|
|
10bfbbec17 | ||
|
|
3da900db7f | ||
|
|
274c5ae165 | ||
|
|
39c4012a1a | ||
|
|
754ac166e0 | ||
|
|
0b18334236 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# allow only what we need
|
||||||
|
!commafeed-server/target/commafeed.jar
|
||||||
|
!commafeed-server/config.yml.example
|
||||||
69
.github/workflows/build.yml
vendored
69
.github/workflows/build.yml
vendored
@@ -1,28 +1,89 @@
|
|||||||
name: Java CI
|
name: Java CI
|
||||||
|
|
||||||
on: [push]
|
on: [ push ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
java: ["8", "11", "17"]
|
java: [ "8", "11", "17" ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java }}
|
java-version: ${{ matrix.java }}
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: "maven"
|
cache: "maven"
|
||||||
|
|
||||||
|
# Build
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn --batch-mode --update-snapshots verify
|
run: mvn --batch-mode --update-snapshots verify
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
|
- name: Upload JAR
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
if: ${{ matrix.java == '8' }}
|
if: ${{ matrix.java == '8' }}
|
||||||
with:
|
with:
|
||||||
name: commafeed.jar
|
name: commafeed.jar
|
||||||
path: commafeed-server/target/commafeed.jar
|
path: commafeed-server/target/commafeed.jar
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
if: ${{ matrix.java == '8' }}
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Docker build and push tag
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm/v7
|
||||||
|
tags: |
|
||||||
|
athou/commafeed:latest
|
||||||
|
athou/commafeed:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Docker build and push master
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
if: ${{ matrix.java == '8' && github.ref_name == 'master' }}
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm/v7
|
||||||
|
tags: athou/commafeed:master
|
||||||
|
|
||||||
|
# Create GitHub release after Docker image has been published
|
||||||
|
- name: Extract Changelog Entry
|
||||||
|
uses: mindsers/changelog-reader-action@v2
|
||||||
|
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
|
||||||
|
id: changelog_reader
|
||||||
|
with:
|
||||||
|
version: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Create GitHub release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: CommaFeed ${{ github.ref_name }}
|
||||||
|
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
commafeed-server/target/commafeed.jar
|
||||||
|
commafeed-server/config.yml.example
|
||||||
80
CHANGELOG
80
CHANGELOG
@@ -1,80 +0,0 @@
|
|||||||
v 3.0.0
|
|
||||||
- complete overhaul of the UI
|
|
||||||
- backend and frontend are now in separate maven modules
|
|
||||||
- no changes to the api or the database
|
|
||||||
|
|
||||||
v 2.6.0
|
|
||||||
- add support for media content as a backup for missing content (useful for youtube feeds)
|
|
||||||
- correctly follow http error code 308 redirects
|
|
||||||
- fixed a bug that prevented users from deleting their account
|
|
||||||
- fixed a bug that made commafeed store entry contents multiple times
|
|
||||||
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed was not "/"
|
|
||||||
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added
|
|
||||||
- removed support for google+ and readability as those services no longer exist
|
|
||||||
- removed support for deploying on openshift
|
|
||||||
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed)
|
|
||||||
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from users that did not log in for a long time
|
|
||||||
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1)
|
|
||||||
- add support for mariadb
|
|
||||||
- add support for java17+ runtime
|
|
||||||
- various security improvements
|
|
||||||
v 2.5.0
|
|
||||||
- unread count is now displayed in a favicon badge when supported
|
|
||||||
- the user agent string for the bot fetching feeds is now configurable
|
|
||||||
- feed parsing performance improvements
|
|
||||||
- support for java9+ runtime
|
|
||||||
- can now properly start from an empty postgresql database
|
|
||||||
v 2.4.0
|
|
||||||
- users were not able to change password or delete account
|
|
||||||
- fix api key generation
|
|
||||||
- feed entries can now be sorted alphabetically
|
|
||||||
- fix facebook sharing
|
|
||||||
- fix layout on iOS
|
|
||||||
- postgresql driver update (fix for postgres 9.6)
|
|
||||||
- various internationalization fixes
|
|
||||||
- security fixes
|
|
||||||
v 2.3.0
|
|
||||||
- dropwizard upgrade 0.9.1
|
|
||||||
- feed enclosures are hidden if they already displayed in the content
|
|
||||||
- fix youtube favicons
|
|
||||||
- various internationalization fixes
|
|
||||||
v 2.2.0
|
|
||||||
- fix youtube and instagram favicon fetching
|
|
||||||
- mark as read filter was lost when a feed was rearranged with drag&drop
|
|
||||||
- feed entry categories are now displayed if available
|
|
||||||
- various performance and dependencies upgrades
|
|
||||||
- java8 is now required
|
|
||||||
v 2.1.0
|
|
||||||
- dropwizard upgrade to 0.8.0
|
|
||||||
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use server.applicationContextPath instead
|
|
||||||
- new setting app.maxFeedCapacity for deleting old entries
|
|
||||||
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title, content, author or url.
|
|
||||||
- ability to use !keyword or -keyword to exclude a keyword from a search query
|
|
||||||
- facebook feeds now show user favicon instead of facebook favicon
|
|
||||||
- new dark theme 'nightsky'
|
|
||||||
v 2.0.3
|
|
||||||
- internet explorer ajax cache workaround
|
|
||||||
- categories are now deletable again
|
|
||||||
- openshift support is back
|
|
||||||
- youtube feeds now show user favicon instead of youtube favicon
|
|
||||||
v 2.0.2
|
|
||||||
- api using the api key is now working again
|
|
||||||
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
|
|
||||||
- fix login on firefox when fields are autofilled by the browser
|
|
||||||
- fix scrolling of subscriptions list on mobile
|
|
||||||
- user is now logged in after registration
|
|
||||||
- fix link to documentation on home page and about page
|
|
||||||
- fields autocomplete is disabled on the profile page
|
|
||||||
- users are able to delete their account again
|
|
||||||
- chinese and malaysian translation files are now correctly loaded
|
|
||||||
- software version in user-agent when fetching feeds is no longer hardcoded
|
|
||||||
- admin settings page is now read only, settings are configured in config.yml
|
|
||||||
- added link to metrics on the admin settings page
|
|
||||||
- Rome (rss library) upgrade to 1.5.0
|
|
||||||
v 2.0.1
|
|
||||||
- the redis pool no longer throws an exception when it is unable to aquire a new connection
|
|
||||||
v2.0.0
|
|
||||||
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory consumption and better overall performances.
|
|
||||||
See the README on how to build CommaFeed from now on.
|
|
||||||
- CommaFeed should no longer fetch the same feed multiple times in a row
|
|
||||||
- Users can use their username or email to log in
|
|
||||||
134
CHANGELOG.md
Normal file
134
CHANGELOG.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [3.2.0]
|
||||||
|
|
||||||
|
- restore the welcome page
|
||||||
|
- only apply hover effect for unread entries (same as commafeed v2)
|
||||||
|
- move notifications at the bottom of the screen
|
||||||
|
- always use https for sharing urls
|
||||||
|
- add support for redis ACLs
|
||||||
|
- transition to google analytics v4
|
||||||
|
|
||||||
|
## [3.1.0]
|
||||||
|
|
||||||
|
- add an even more compact layout
|
||||||
|
- restore hover effect from commafeed 2.x
|
||||||
|
- view mode (compact, expanded, ...) is now stored on the device so you can have a different view mode on desktop and
|
||||||
|
mobile
|
||||||
|
- fix for the "Illegal attempt to associate a collection with two open sessions." error
|
||||||
|
- feed fetching workflow is now orchestrated with rxjava, removing a lot of code
|
||||||
|
|
||||||
|
## [3.0.1]
|
||||||
|
|
||||||
|
- allow env variable substitution in config.yml
|
||||||
|
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
|
||||||
|
its value
|
||||||
|
- allow env variable prefixed with `CF_` to override config.yml properties
|
||||||
|
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
|
||||||
|
|
||||||
|
## [3.0.0]
|
||||||
|
|
||||||
|
- complete overhaul of the UI
|
||||||
|
- backend and frontend are now in separate maven modules
|
||||||
|
- no changes to the api or the database
|
||||||
|
- Docker images are now automatically built and available at https://hub.docker.com/r/athou/commafeed
|
||||||
|
|
||||||
|
## [2.6.0]
|
||||||
|
|
||||||
|
- add support for media content as a backup for missing content (useful for youtube feeds)
|
||||||
|
- correctly follow http error code 308 redirects
|
||||||
|
- fixed a bug that prevented users from deleting their account
|
||||||
|
- fixed a bug that made commafeed store entry contents multiple times
|
||||||
|
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed
|
||||||
|
was not "/"
|
||||||
|
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added
|
||||||
|
- removed support for google+ and readability as those services no longer exist
|
||||||
|
- removed support for deploying on openshift
|
||||||
|
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed)
|
||||||
|
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from
|
||||||
|
users that did not log in for a long time
|
||||||
|
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1)
|
||||||
|
- add support for mariadb
|
||||||
|
- add support for java17+ runtime
|
||||||
|
- various security improvements
|
||||||
|
|
||||||
|
## [2.5.0]
|
||||||
|
|
||||||
|
- unread count is now displayed in a favicon badge when supported
|
||||||
|
- the user agent string for the bot fetching feeds is now configurable
|
||||||
|
- feed parsing performance improvements
|
||||||
|
- support for java9+ runtime
|
||||||
|
- can now properly start from an empty postgresql database
|
||||||
|
|
||||||
|
## [2.4.0]
|
||||||
|
|
||||||
|
- users were not able to change password or delete account
|
||||||
|
- fix api key generation
|
||||||
|
- feed entries can now be sorted alphabetically
|
||||||
|
- fix facebook sharing
|
||||||
|
- fix layout on iOS
|
||||||
|
- postgresql driver update (fix for postgres 9.6)
|
||||||
|
- various internationalization fixes
|
||||||
|
- security fixes
|
||||||
|
|
||||||
|
## [2.3.0]
|
||||||
|
|
||||||
|
- dropwizard upgrade 0.9.1
|
||||||
|
- feed enclosures are hidden if they already displayed in the content
|
||||||
|
- fix youtube favicons
|
||||||
|
- various internationalization fixes
|
||||||
|
|
||||||
|
## [2.2.0]
|
||||||
|
|
||||||
|
- fix youtube and instagram favicon fetching
|
||||||
|
- mark as read filter was lost when a feed was rearranged with drag&drop
|
||||||
|
- feed entry categories are now displayed if available
|
||||||
|
- various performance and dependencies upgrades
|
||||||
|
- java8 is now required
|
||||||
|
|
||||||
|
## [2.1.0]
|
||||||
|
|
||||||
|
- dropwizard upgrade to 0.8.0
|
||||||
|
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use
|
||||||
|
server.applicationContextPath instead
|
||||||
|
- new setting app.maxFeedCapacity for deleting old entries
|
||||||
|
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title,
|
||||||
|
content, author or url.
|
||||||
|
- ability to use !keyword or -keyword to exclude a keyword from a search query
|
||||||
|
- facebook feeds now show user favicon instead of facebook favicon
|
||||||
|
- new dark theme 'nightsky'
|
||||||
|
|
||||||
|
## [2.0.3]
|
||||||
|
|
||||||
|
- internet explorer ajax cache workaround
|
||||||
|
- categories are now deletable again
|
||||||
|
- openshift support is back
|
||||||
|
- youtube feeds now show user favicon instead of youtube favicon
|
||||||
|
|
||||||
|
## [2.0.2]
|
||||||
|
|
||||||
|
- api using the api key is now working again
|
||||||
|
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
|
||||||
|
- fix login on firefox when fields are autofilled by the browser
|
||||||
|
- fix scrolling of subscriptions list on mobile
|
||||||
|
- user is now logged in after registration
|
||||||
|
- fix link to documentation on home page and about page
|
||||||
|
- fields autocomplete is disabled on the profile page
|
||||||
|
- users are able to delete their account again
|
||||||
|
- chinese and malaysian translation files are now correctly loaded
|
||||||
|
- software version in user-agent when fetching feeds is no longer hardcoded
|
||||||
|
- admin settings page is now read only, settings are configured in config.yml
|
||||||
|
- added link to metrics on the admin settings page
|
||||||
|
- Rome (rss library) upgrade to 1.5.0
|
||||||
|
|
||||||
|
## [2.0.1]
|
||||||
|
|
||||||
|
- the redis pool no longer throws an exception when it is unable to aquire a new connection
|
||||||
|
|
||||||
|
## [2.0.0]
|
||||||
|
|
||||||
|
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
|
||||||
|
consumption and better overall performances.
|
||||||
|
See the README on how to build CommaFeed from now on.
|
||||||
|
- CommaFeed should no longer fetch the same feed multiple times in a row
|
||||||
|
- Users can use their username or email to log in
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM eclipse-temurin:17-jre
|
||||||
|
|
||||||
|
EXPOSE 8082
|
||||||
|
|
||||||
|
RUN mkdir -p /commafeed/data
|
||||||
|
VOLUME /commafeed/data
|
||||||
|
ENV CF_SESSION_PATH=/commafeed/data/sessions
|
||||||
|
|
||||||
|
COPY commafeed-server/config.yml.example config.yml
|
||||||
|
COPY commafeed-server/target/commafeed.jar .
|
||||||
|
|
||||||
|
CMD ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "commafeed.jar", "server", "config.yml"]
|
||||||
96
README.md
96
README.md
@@ -1,9 +1,20 @@
|
|||||||
# CommaFeed
|
# CommaFeed
|
||||||
|
|
||||||
Sources for [CommaFeed.com](http://www.commafeed.com/).
|
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
|
||||||
|
|
||||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS.
|

|
||||||
CommaFeed is now considered feature-complete and is in maintenance mode.
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 4 different layouts
|
||||||
|
- Dark theme
|
||||||
|
- Fully responsive
|
||||||
|
- Keyboard shortcuts for almost everything
|
||||||
|
- Support for right-to-left feeds
|
||||||
|
- Translated in 25+ languages
|
||||||
|
- Supports thousands of users and millions of feeds
|
||||||
|
- OPML import/export
|
||||||
|
- REST API
|
||||||
|
|
||||||
## Related open-source projects
|
## Related open-source projects
|
||||||
|
|
||||||
@@ -16,84 +27,53 @@ Browser extensions:
|
|||||||
|
|
||||||
## Deployment on your own server
|
## Deployment on your own server
|
||||||
|
|
||||||
### The very short version (download precompiled package)
|
### Docker
|
||||||
|
|
||||||
|
Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed
|
||||||
|
|
||||||
|
### Download precompiled package
|
||||||
|
|
||||||
mkdir commafeed && cd commafeed
|
mkdir commafeed && cd commafeed
|
||||||
wget https://github.com/Athou/commafeed/releases/download/3.0.0/commafeed.jar
|
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
|
||||||
wget https://raw.githubusercontent.com/Athou/commafeed/3.0.0/commafeed-server/config.yml.example -O config.yml
|
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
|
||||||
vi config.yml
|
|
||||||
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
||||||
|
|
||||||
### The short version (build from sources)
|
The server will listen on http://localhost:8082. The default
|
||||||
|
user is `admin` and the default password is `admin`.
|
||||||
|
|
||||||
|
### Build from sources
|
||||||
|
|
||||||
git clone https://github.com/Athou/commafeed.git
|
git clone https://github.com/Athou/commafeed.git
|
||||||
cd commafeed
|
cd commafeed
|
||||||
./mvnw clean package
|
./mvnw clean package
|
||||||
cp commafeed-server/config.yml.example config.yml
|
cp commafeed-server/config.yml.example config.yml
|
||||||
vi config.yml
|
|
||||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
||||||
|
|
||||||
### The long version (same as the short version, but more detailed)
|
The server will listen on http://localhost:8082. The default
|
||||||
|
user is `admin` and the default password is `admin`.
|
||||||
|
|
||||||
CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x).
|
## Translation
|
||||||
|
|
||||||
For storage, you can either use an embedded file-based H2 database or an external MySQL, PostgreSQL or SQLServer database.
|
Files for internationalization are
|
||||||
You also need the Java 1.8+ JDK in order to build the application.
|
located [here](https://github.com/Athou/commafeed/tree/master/commafeed-client/src/locales).
|
||||||
|
|
||||||
To install the required packages to build CommaFeed on Ubuntu, issue the following commands
|
|
||||||
|
|
||||||
# if this commands works and returns a version >= 1.8.0 you're good to go and you can skip JDK installation
|
|
||||||
javac -version
|
|
||||||
|
|
||||||
# if openjdk-8-jdk is not available on your ubuntu version (14.04 LTS), add the following repo first
|
|
||||||
sudo add-apt-repository ppa:openjdk-r/ppa
|
|
||||||
sudo apt-get update
|
|
||||||
|
|
||||||
sudo apt-get install g++ build-essential openjdk-8-jdk
|
|
||||||
|
|
||||||
# Make sure java8 is the selected java version
|
|
||||||
sudo update-alternatives --config java
|
|
||||||
sudo update-alternatives --config javac
|
|
||||||
|
|
||||||
Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip)
|
|
||||||
|
|
||||||
git clone https://github.com/Athou/commafeed.git
|
|
||||||
cd commafeed
|
|
||||||
|
|
||||||
Now build the application
|
|
||||||
|
|
||||||
./mvnw clean package
|
|
||||||
|
|
||||||
Copy `commafeed-server/config.yml.example` to `./config.yml` then edit the file to your liking.
|
|
||||||
Issue the following command to run the app, the server will listen by default on `http://localhost:8082`. The default user is `admin` and the default password is `admin`.
|
|
||||||
|
|
||||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
|
||||||
|
|
||||||
You can use a proxy http server such as nginx or apache.
|
|
||||||
|
|
||||||
## Translate CommaFeed into your language
|
|
||||||
|
|
||||||
Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/commafeed-client/src/locales).
|
|
||||||
|
|
||||||
To add a new language:
|
To add a new language:
|
||||||
|
|
||||||
- edit `commafeed-client/src/i18n.ts`
|
- edit `commafeed-client/src/i18n.ts`
|
||||||
- add the new locale to the `locales` array.
|
- add the new locale to the `locales` array.
|
||||||
- import the dayjs locale
|
- import the dayjs locale
|
||||||
- edit `commafeed-client/.linguirc` and add the new locale to the `locales` array.
|
- edit `commafeed-client/.linguirc` and add the new locale to the `locales` array.
|
||||||
- run `npm run i18n` and add translations to the newly created `commafeed-client/src/locales/[locale]/messages.po` file
|
- run `npm run i18n` and add translations to the newly created `commafeed-client/src/locales/[locale]/messages.po` file
|
||||||
|
|
||||||
The name of the locale should be the two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
|
The name of the locale should be the
|
||||||
|
two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
- `git clone https://github.com/Athou/CommaFeed`
|
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
- Open `commafeed-server` in your preferred Java IDE.
|
- Open `commafeed-server` in your preferred Java IDE.
|
||||||
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
||||||
- If using Eclipse, Go to Window → Preferences → Maven → Annotation Processing and check "Automatically configure JDT APT"
|
|
||||||
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
@@ -101,11 +81,13 @@ The name of the locale should be the two-letters [ISO-639-1 language code](http:
|
|||||||
- Open `commafeed-client` in your preferred JavaScript IDE.
|
- Open `commafeed-client` in your preferred JavaScript IDE.
|
||||||
- run `npm install`
|
- run `npm install`
|
||||||
- run `npm run dev`
|
- run `npm run dev`
|
||||||
- the frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on port 8083
|
|
||||||
|
The frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on
|
||||||
|
port 8083
|
||||||
|
|
||||||
## Copyright and license
|
## Copyright and license
|
||||||
|
|
||||||
Copyright 2013-2022 CommaFeed.
|
Copyright 2013-2023 CommaFeed.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this work except in compliance with the License.
|
you may not use this work except in compliance with the License.
|
||||||
|
|||||||
30931
commafeed-client/package-lock.json
generated
30931
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,79 +1,83 @@
|
|||||||
{
|
{
|
||||||
"name": "commafeed-client",
|
"name": "commafeed-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "npm run i18n:compile && tsc && vite build",
|
"dev:typescript": "tsc --watch",
|
||||||
"preview": "vite preview",
|
"build": "npm run i18n:compile && tsc && vite build",
|
||||||
"test": "vitest",
|
"preview": "vite preview",
|
||||||
"test:ci": "vitest run",
|
"test": "vitest",
|
||||||
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
|
"test:ci": "vitest run",
|
||||||
"i18n": "npm run i18n:extract && npm run i18n:compile",
|
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
|
||||||
"i18n:extract": "lingui extract --clean",
|
"i18n": "npm run i18n:extract && npm run i18n:compile",
|
||||||
"i18n:compile": "lingui compile --typescript",
|
"i18n:extract": "lingui extract --clean",
|
||||||
"postinstall": "npm run i18n:compile"
|
"i18n:compile": "lingui compile --typescript",
|
||||||
},
|
"postinstall": "npm run i18n:compile"
|
||||||
"dependencies": {
|
},
|
||||||
"@emotion/react": "^11.10.5",
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^4.5.14",
|
"@emotion/react": "^11.10.5",
|
||||||
"@lingui/core": "^3.17.0",
|
"@fontsource/open-sans": "^4.5.14",
|
||||||
"@lingui/macro": "^3.17.0",
|
"@lingui/core": "^3.17.0",
|
||||||
"@lingui/react": "^3.17.0",
|
"@lingui/macro": "^3.17.0",
|
||||||
"@mantine/core": "^5.10.3",
|
"@lingui/react": "^3.17.0",
|
||||||
"@mantine/form": "^5.10.3",
|
"@mantine/core": "^5.10.3",
|
||||||
"@mantine/hooks": "^5.10.3",
|
"@mantine/form": "^5.10.3",
|
||||||
"@mantine/modals": "^5.10.3",
|
"@mantine/hooks": "^5.10.3",
|
||||||
"@mantine/notifications": "^5.10.3",
|
"@mantine/modals": "^5.10.3",
|
||||||
"@mantine/spotlight": "^5.10.3",
|
"@mantine/notifications": "^5.10.3",
|
||||||
"@reduxjs/toolkit": "^1.9.2",
|
"@mantine/spotlight": "^5.10.3",
|
||||||
"axios": "^1.3.2",
|
"@mantine/styles": "^5.10.3",
|
||||||
"dayjs": "^1.11.7",
|
"@reduxjs/toolkit": "^1.9.2",
|
||||||
"interweave": "^13.0.0",
|
"axios": "^1.3.2",
|
||||||
"lodash": "^4.17.21",
|
"dayjs": "^1.11.7",
|
||||||
"make-plural": "^7.2.0",
|
"interweave": "^13.0.0",
|
||||||
"mousetrap": "^1.6.5",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"make-plural": "^7.2.0",
|
||||||
"react-async-hook": "^4.0.0",
|
"mousetrap": "^1.6.5",
|
||||||
"react-contexify": "^6.0.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-async-hook": "^4.0.0",
|
||||||
"react-icons": "^4.7.1",
|
"react-contexify": "^6.0.0",
|
||||||
"react-infinite-scroller": "^1.2.6",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "^8.0.5",
|
"react-ga4": "^2.1.0",
|
||||||
"react-router-dom": "^6.8.0",
|
"react-icons": "^4.7.1",
|
||||||
"react-swipeable": "^7.0.0",
|
"react-infinite-scroller": "^1.2.6",
|
||||||
"swagger-ui-react": "^4.15.5",
|
"react-redux": "^8.0.5",
|
||||||
"tinycon": "^0.6.8",
|
"react-router-dom": "^6.8.0",
|
||||||
"websocket-heartbeat-js": "^1.1.1"
|
"react-swipeable": "^7.0.0",
|
||||||
},
|
"swagger-ui-react": "^4.15.5",
|
||||||
"devDependencies": {
|
"tinycon": "^0.6.8",
|
||||||
"@lingui/cli": "^3.17.0",
|
"use-local-storage": "^3.0.0",
|
||||||
"@types/eslint": "^8.21.0",
|
"websocket-heartbeat-js": "^1.1.1"
|
||||||
"@types/lodash": "^4.14.191",
|
},
|
||||||
"@types/mousetrap": "^1.6.11",
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.27",
|
"@lingui/cli": "^3.17.0",
|
||||||
"@types/react-dom": "^18.0.10",
|
"@types/eslint": "^8.21.0",
|
||||||
"@types/react-infinite-scroller": "^1.2.3",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/swagger-ui-react": "^4.11.0",
|
"@types/mousetrap": "^1.6.11",
|
||||||
"@types/tinycon": "^0.6.3",
|
"@types/react": "^18.0.27",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
"@types/react-dom": "^18.0.10",
|
||||||
"@typescript-eslint/parser": "^5.50.0",
|
"@types/react-infinite-scroller": "^1.2.3",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@types/swagger-ui-react": "^4.11.0",
|
||||||
"eslint": "^8.33.0",
|
"@types/tinycon": "^0.6.3",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"@typescript-eslint/parser": "^5.50.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint": "^8.33.0",
|
||||||
"eslint-plugin-hooks": "^0.4.3",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"prettier": "^2.8.3",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"rollup-plugin-visualizer": "^5.9.0",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"typescript": "^4.9.5",
|
"eslint-plugin-hooks": "^0.4.3",
|
||||||
"vite": "^4.1.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"prettier": "^2.8.3",
|
||||||
"vite-tsconfig-paths": "^4.0.5",
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
"vitest": "^0.28.4",
|
"typescript": "^4.9.5",
|
||||||
"vitest-mock-extended": "^1.0.9"
|
"vite": "^4.1.1",
|
||||||
}
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-tsconfig-paths": "^4.0.5",
|
||||||
|
"vitest": "^0.28.4",
|
||||||
|
"vitest-mock-extended": "^1.0.9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>3.0.0</version>
|
<version>3.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<name>CommaFeed Client</name>
|
<name>CommaFeed Client</name>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { i18n } from "@lingui/core"
|
import { i18n } from "@lingui/core"
|
||||||
import { I18nProvider } from "@lingui/react"
|
import { I18nProvider } from "@lingui/react"
|
||||||
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"
|
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"
|
||||||
import { useColorScheme, useLocalStorage } from "@mantine/hooks"
|
import { useColorScheme } from "@mantine/hooks"
|
||||||
import { ModalsProvider } from "@mantine/modals"
|
import { ModalsProvider } from "@mantine/modals"
|
||||||
import { NotificationsProvider } from "@mantine/notifications"
|
import { NotificationsProvider } from "@mantine/notifications"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
@@ -27,16 +27,15 @@ import { LoginPage } from "pages/auth/LoginPage"
|
|||||||
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
||||||
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||||
import React, { useEffect } from "react"
|
import React, { useEffect } from "react"
|
||||||
import { HashRouter, Navigate, Route, Routes, useNavigate } from "react-router-dom"
|
import ReactGA from "react-ga4"
|
||||||
|
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||||
import Tinycon from "tinycon"
|
import Tinycon from "tinycon"
|
||||||
|
import useLocalStorage from "use-local-storage"
|
||||||
|
import { WelcomePage } from "./pages/WelcomePage"
|
||||||
|
|
||||||
function Providers(props: { children: React.ReactNode }) {
|
function Providers(props: { children: React.ReactNode }) {
|
||||||
const preferredColorScheme = useColorScheme()
|
const preferredColorScheme = useColorScheme()
|
||||||
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
|
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme)
|
||||||
key: "color-scheme",
|
|
||||||
defaultValue: preferredColorScheme,
|
|
||||||
getInitialValueInEffect: true,
|
|
||||||
})
|
|
||||||
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"))
|
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +51,7 @@ function Providers(props: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<NotificationsProvider position="top-center" zIndex={9999}>
|
<NotificationsProvider position="bottom-right" zIndex={9999}>
|
||||||
<ErrorBoundary>{props.children}</ErrorBoundary>
|
<ErrorBoundary>{props.children}</ErrorBoundary>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
@@ -69,6 +68,7 @@ function AppRoutes() {
|
|||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
|
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
|
||||||
|
<Route path="welcome" element={<WelcomePage />} />
|
||||||
<Route path="login" element={<LoginPage />} />
|
<Route path="login" element={<LoginPage />} />
|
||||||
<Route path="register" element={<RegistrationPage />} />
|
<Route path="register" element={<RegistrationPage />} />
|
||||||
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
||||||
@@ -114,6 +114,21 @@ function RedirectHandler() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GoogleAnalyticsHandler() {
|
||||||
|
const location = useLocation()
|
||||||
|
const googleAnalyticsCode = useAppSelector(state => state.server.serverInfos?.googleAnalyticsCode)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (googleAnalyticsCode) ReactGA.initialize(googleAnalyticsCode)
|
||||||
|
}, [googleAnalyticsCode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ReactGA.send({ hitType: "pageview", page: location.pathname })
|
||||||
|
}, [location])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function FaviconHandler() {
|
function FaviconHandler() {
|
||||||
const root = useAppSelector(state => state.tree.rootCategory)
|
const root = useAppSelector(state => state.tree.rootCategory)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -138,6 +153,7 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
<FaviconHandler />
|
<FaviconHandler />
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
<GoogleAnalyticsHandler />
|
||||||
<RedirectHandler />
|
<RedirectHandler />
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
|||||||
axiosInstance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
response => response,
|
response => response,
|
||||||
error => {
|
error => {
|
||||||
if (error.response.status === 401) window.location.hash = "/login"
|
if (error.response.status === 401 && error.response.data === "Credentials are required to access this resource.") {
|
||||||
|
window.location.hash = "/welcome"
|
||||||
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ const sharing: {
|
|||||||
label: "Twitter",
|
label: "Twitter",
|
||||||
icon: SiTwitter,
|
icon: SiTwitter,
|
||||||
color: "#1D9BF0",
|
color: "#1D9BF0",
|
||||||
url: (url, desc) => `http://twitter.com/share?text=${desc}&url=${url}`,
|
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`,
|
||||||
},
|
},
|
||||||
tumblr: {
|
tumblr: {
|
||||||
label: "Tumblr",
|
label: "Tumblr",
|
||||||
icon: SiTumblr,
|
icon: SiTumblr,
|
||||||
color: "#375672",
|
color: "#375672",
|
||||||
url: (url, desc) => `http://www.tumblr.com/share/link?url=${url}&name=${desc}`,
|
url: (url, desc) => `https://www.tumblr.com/share/link?url=${url}&name=${desc}`,
|
||||||
},
|
},
|
||||||
pocket: {
|
pocket: {
|
||||||
label: "Pocket",
|
label: "Pocket",
|
||||||
@@ -97,4 +97,5 @@ export const Constants = {
|
|||||||
mainScrollAreaId: "main-scroll-area-id",
|
mainScrollAreaId: "main-scroll-area-id",
|
||||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||||
},
|
},
|
||||||
|
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { showNotification } from "@mantine/notifications"
|
|||||||
import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { RootState } from "app/store"
|
import { RootState } from "app/store"
|
||||||
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel, ViewMode } from "app/types"
|
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel } from "app/types"
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { reloadEntries } from "./entries"
|
import { reloadEntries } from "./entries"
|
||||||
|
|
||||||
@@ -36,46 +36,67 @@ export const changeReadingOrder = createAsyncThunk<void, ReadingOrder, { state:
|
|||||||
thunkApi.dispatch(reloadEntries())
|
thunkApi.dispatch(reloadEntries())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
export const changeViewMode = createAsyncThunk<void, ViewMode, { state: RootState }>("settings/viewMode", (viewMode, thunkApi) => {
|
export const changeLanguage = createAsyncThunk<
|
||||||
const { settings } = thunkApi.getState().user
|
void,
|
||||||
if (!settings) return
|
string,
|
||||||
client.user.saveSettings({ ...settings, viewMode })
|
{
|
||||||
thunkApi.dispatch(reloadEntries())
|
state: RootState
|
||||||
})
|
}
|
||||||
export const changeLanguage = createAsyncThunk<void, string, { state: RootState }>("settings/language", (language, thunkApi) => {
|
>("settings/language", (language, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, language })
|
client.user.saveSettings({ ...settings, language })
|
||||||
})
|
})
|
||||||
export const changeScrollSpeed = createAsyncThunk<void, boolean, { state: RootState }>("settings/scrollSpeed", (speed, thunkApi) => {
|
export const changeScrollSpeed = createAsyncThunk<
|
||||||
|
void,
|
||||||
|
boolean,
|
||||||
|
{
|
||||||
|
state: RootState
|
||||||
|
}
|
||||||
|
>("settings/scrollSpeed", (speed, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
||||||
})
|
})
|
||||||
export const changeShowRead = createAsyncThunk<void, boolean, { state: RootState }>("settings/showRead", (showRead, thunkApi) => {
|
export const changeShowRead = createAsyncThunk<
|
||||||
|
void,
|
||||||
|
boolean,
|
||||||
|
{
|
||||||
|
state: RootState
|
||||||
|
}
|
||||||
|
>("settings/showRead", (showRead, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, showRead })
|
client.user.saveSettings({ ...settings, showRead })
|
||||||
})
|
})
|
||||||
export const changeScrollMarks = createAsyncThunk<void, boolean, { state: RootState }>("settings/scrollMarks", (scrollMarks, thunkApi) => {
|
export const changeScrollMarks = createAsyncThunk<
|
||||||
|
void,
|
||||||
|
boolean,
|
||||||
|
{
|
||||||
|
state: RootState
|
||||||
|
}
|
||||||
|
>("settings/scrollMarks", (scrollMarks, thunkApi) => {
|
||||||
const { settings } = thunkApi.getState().user
|
const { settings } = thunkApi.getState().user
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
client.user.saveSettings({ ...settings, scrollMarks })
|
client.user.saveSettings({ ...settings, scrollMarks })
|
||||||
})
|
})
|
||||||
export const changeSharingSetting = createAsyncThunk<void, { site: keyof SharingSettings; value: boolean }, { state: RootState }>(
|
export const changeSharingSetting = createAsyncThunk<
|
||||||
"settings/sharingSetting",
|
void,
|
||||||
(sharingSetting, thunkApi) => {
|
{ site: keyof SharingSettings; value: boolean },
|
||||||
const { settings } = thunkApi.getState().user
|
{
|
||||||
if (!settings) return
|
state: RootState
|
||||||
client.user.saveSettings({
|
|
||||||
...settings,
|
|
||||||
sharingSettings: {
|
|
||||||
...settings.sharingSettings,
|
|
||||||
[sharingSetting.site]: sharingSetting.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)
|
>("settings/sharingSetting", (sharingSetting, thunkApi) => {
|
||||||
|
const { settings } = thunkApi.getState().user
|
||||||
|
if (!settings) return
|
||||||
|
client.user.saveSettings({
|
||||||
|
...settings,
|
||||||
|
sharingSettings: {
|
||||||
|
...settings.sharingSettings,
|
||||||
|
[sharingSetting.site]: sharingSetting.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export const userSlice = createSlice({
|
export const userSlice = createSlice({
|
||||||
name: "user",
|
name: "user",
|
||||||
@@ -99,10 +120,6 @@ export const userSlice = createSlice({
|
|||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.readingOrder = action.meta.arg
|
state.settings.readingOrder = action.meta.arg
|
||||||
})
|
})
|
||||||
builder.addCase(changeViewMode.pending, (state, action) => {
|
|
||||||
if (!state.settings) return
|
|
||||||
state.settings.viewMode = action.meta.arg
|
|
||||||
})
|
|
||||||
builder.addCase(changeLanguage.pending, (state, action) => {
|
builder.addCase(changeLanguage.pending, (state, action) => {
|
||||||
if (!state.settings) return
|
if (!state.settings) return
|
||||||
state.settings.language = action.meta.arg
|
state.settings.language = action.meta.arg
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface ApplicationSettings {
|
|||||||
export interface Category {
|
export interface Category {
|
||||||
id: string
|
id: string
|
||||||
parentId?: string
|
parentId?: string
|
||||||
|
parentName?: string
|
||||||
name: string
|
name: string
|
||||||
children: Category[]
|
children: Category[]
|
||||||
feeds: Subscription[]
|
feeds: Subscription[]
|
||||||
@@ -227,7 +228,6 @@ export interface Settings {
|
|||||||
language: string
|
language: string
|
||||||
readingMode: ReadingMode
|
readingMode: ReadingMode
|
||||||
readingOrder: ReadingOrder
|
readingOrder: ReadingOrder
|
||||||
viewMode: ViewMode
|
|
||||||
showRead: boolean
|
showRead: boolean
|
||||||
scrollMarks: boolean
|
scrollMarks: boolean
|
||||||
theme?: string
|
theme?: string
|
||||||
@@ -305,4 +305,4 @@ export type ReadingMode = "all" | "unread"
|
|||||||
|
|
||||||
export type ReadingOrder = "asc" | "desc"
|
export type ReadingOrder = "asc" | "desc"
|
||||||
|
|
||||||
export type ViewMode = "title" | "cozy" | "expanded"
|
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||||
|
|||||||
BIN
commafeed-client/src/assets/welcome_page_dark.png
Normal file
BIN
commafeed-client/src/assets/welcome_page_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
commafeed-client/src/assets/welcome_page_light.png
Normal file
BIN
commafeed-client/src/assets/welcome_page_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -1,12 +1,15 @@
|
|||||||
import { ActionIcon, Button, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Button, ButtonVariant, useMantineTheme } from "@mantine/core"
|
||||||
|
import { ActionIconVariant } from "@mantine/core/lib/ActionIcon/ActionIcon.styles"
|
||||||
import { useMediaQuery } from "@mantine/hooks"
|
import { useMediaQuery } from "@mantine/hooks"
|
||||||
import { forwardRef } from "react"
|
import { forwardRef, MouseEventHandler, ReactNode } from "react"
|
||||||
|
|
||||||
interface ActionButtonProps {
|
interface ActionButtonProps {
|
||||||
className?: string
|
className?: string
|
||||||
icon?: React.ReactNode
|
icon?: ReactNode
|
||||||
label?: string
|
label?: string
|
||||||
onClick?: React.MouseEventHandler
|
onClick?: MouseEventHandler
|
||||||
|
variant?: ActionIconVariant & ButtonVariant
|
||||||
|
showLabelOnMobile?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,13 +17,15 @@ interface ActionButtonProps {
|
|||||||
*/
|
*/
|
||||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||||
const theme = useMantineTheme()
|
const theme = useMantineTheme()
|
||||||
|
const variant = props.variant ?? "subtle"
|
||||||
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg}px)`)
|
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg}px)`)
|
||||||
return mobile ? (
|
const iconOnly = !props.showLabelOnMobile && (mobile || !props.label)
|
||||||
<ActionIcon ref={ref} color={theme.primaryColor} variant="subtle" className={props.className} onClick={props.onClick}>
|
return iconOnly ? (
|
||||||
|
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
) : (
|
) : (
|
||||||
<Button ref={ref} variant="subtle" size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}>
|
<Button ref={ref} variant={variant} size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import throttle from "lodash/throttle"
|
|||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import InfiniteScroll from "react-infinite-scroller"
|
import InfiniteScroll from "react-infinite-scroller"
|
||||||
import { FeedEntry } from "./FeedEntry"
|
import { FeedEntry } from "./FeedEntry"
|
||||||
|
import { useViewMode } from "../../hooks/useViewMode"
|
||||||
|
|
||||||
export function FeedEntries() {
|
export function FeedEntries() {
|
||||||
const source = useAppSelector(state => state.entries.source)
|
const source = useAppSelector(state => state.entries.source)
|
||||||
@@ -28,7 +29,7 @@ export function FeedEntries() {
|
|||||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
|
const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
|
||||||
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
|
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
|
||||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||||
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
|
const { viewMode } = useViewMode()
|
||||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||||
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Anchor, Box, createStyles, Divider, Paper } from "@mantine/core"
|
import { Anchor, Box, createStyles, Divider, Paper } from "@mantine/core"
|
||||||
|
import { MantineNumberSize } from "@mantine/styles"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { markEntry } from "app/slices/entries"
|
import { markEntry } from "app/slices/entries"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch } from "app/store"
|
||||||
import { Entry } from "app/types"
|
import { Entry, ViewMode } from "app/types"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useSwipeable } from "react-swipeable"
|
import { useSwipeable } from "react-swipeable"
|
||||||
|
import { useViewMode } from "../../hooks/useViewMode"
|
||||||
import { FeedEntryBody } from "./FeedEntryBody"
|
import { FeedEntryBody } from "./FeedEntryBody"
|
||||||
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
|
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
|
||||||
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu"
|
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu"
|
||||||
@@ -18,16 +20,23 @@ interface FeedEntryProps {
|
|||||||
onHeaderClick: (e: React.MouseEvent) => void
|
onHeaderClick: (e: React.MouseEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = createStyles((theme, props: FeedEntryProps & { compact: boolean }) => {
|
const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => {
|
||||||
let backgroundColor
|
let backgroundColor
|
||||||
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
|
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
|
||||||
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
|
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
|
||||||
|
|
||||||
let marginY = theme.spacing.xs
|
let marginY = theme.spacing.xs
|
||||||
if (props.compact) marginY = 6
|
if (props.viewMode === "title") marginY = 2
|
||||||
|
else if (props.viewMode === "cozy") marginY = 6
|
||||||
|
|
||||||
let mobileMarginY = 6
|
let mobileMarginY = 6
|
||||||
if (props.compact) mobileMarginY = 4
|
if (props.viewMode === "title") mobileMarginY = 2
|
||||||
|
else if (props.viewMode === "cozy") mobileMarginY = 4
|
||||||
|
|
||||||
|
let backgroundHoverColor = backgroundColor
|
||||||
|
if (!props.expanded && !props.entry.read) {
|
||||||
|
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
|
||||||
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
paper: {
|
paper: {
|
||||||
@@ -38,6 +47,11 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { compact: boolea
|
|||||||
marginTop: mobileMarginY,
|
marginTop: mobileMarginY,
|
||||||
marginBottom: mobileMarginY,
|
marginBottom: mobileMarginY,
|
||||||
},
|
},
|
||||||
|
"@media (hover: hover)": {
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: backgroundHoverColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
maxWidth: Constants.layout.entryMaxWidth,
|
maxWidth: Constants.layout.entryMaxWidth,
|
||||||
@@ -52,11 +66,8 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { compact: boolea
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function FeedEntry(props: FeedEntryProps) {
|
export function FeedEntry(props: FeedEntryProps) {
|
||||||
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
|
const { viewMode } = useViewMode()
|
||||||
const compact = viewMode === "title"
|
const { classes } = useStyles({ ...props, viewMode })
|
||||||
const compactHeader = compact && !props.expanded
|
|
||||||
|
|
||||||
const { classes } = useStyles({ ...props, compact })
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -66,9 +77,18 @@ export function FeedEntry(props: FeedEntryProps) {
|
|||||||
|
|
||||||
const { onContextMenu } = useFeedEntryContextMenu(props.entry)
|
const { onContextMenu } = useFeedEntryContextMenu(props.entry)
|
||||||
|
|
||||||
const spacing = compact ? 8 : "xs"
|
let paddingX: MantineNumberSize = "xs"
|
||||||
const borderRadius = compact ? "xs" : "sm"
|
if (viewMode === "title" || viewMode === "cozy") paddingX = 6
|
||||||
|
|
||||||
|
let paddingY: MantineNumberSize = "xs"
|
||||||
|
if (viewMode === "title") paddingY = 4
|
||||||
|
else if (viewMode === "cozy") paddingY = 8
|
||||||
|
|
||||||
|
let borderRadius: MantineNumberSize = "sm"
|
||||||
|
if (viewMode === "title") borderRadius = 0
|
||||||
|
else if (viewMode === "cozy") borderRadius = "xs"
|
||||||
|
|
||||||
|
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
|
||||||
return (
|
return (
|
||||||
<Paper withBorder radius={borderRadius} className={classes.paper}>
|
<Paper withBorder radius={borderRadius} className={classes.paper}>
|
||||||
<Anchor
|
<Anchor
|
||||||
@@ -80,17 +100,17 @@ export function FeedEntry(props: FeedEntryProps) {
|
|||||||
onAuxClick={props.onHeaderClick}
|
onAuxClick={props.onHeaderClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
<Box p={spacing} {...swipeHandlers}>
|
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
|
||||||
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
||||||
{!compactHeader && <FeedEntryHeader entry={props.entry} expanded={props.expanded} />}
|
{!compactHeader && <FeedEntryHeader entry={props.entry} expanded={props.expanded} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{props.expanded && (
|
{props.expanded && (
|
||||||
<Box px={spacing} pb={spacing}>
|
<Box px={paddingX} pb={paddingY}>
|
||||||
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}>
|
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}>
|
||||||
<FeedEntryBody entry={props.entry} />
|
<FeedEntryBody entry={props.entry} />
|
||||||
</Box>
|
</Box>
|
||||||
<Divider variant="dashed" my={spacing} />
|
<Divider variant="dashed" my={paddingY} />
|
||||||
<FeedEntryFooter entry={props.entry} />
|
<FeedEntryFooter entry={props.entry} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,16 +4,20 @@ import { Constants } from "app/constants"
|
|||||||
import { useAppSelector } from "app/store"
|
import { useAppSelector } from "app/store"
|
||||||
import { flattenCategoryTree } from "app/utils"
|
import { flattenCategoryTree } from "app/utils"
|
||||||
|
|
||||||
type CategorySelectProps = Partial<SelectProps> & { withAll?: boolean }
|
type CategorySelectProps = Partial<SelectProps> & {
|
||||||
|
withAll?: boolean
|
||||||
|
withoutCategoryIds?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export function CategorySelect(props: CategorySelectProps) {
|
export function CategorySelect(props: CategorySelectProps) {
|
||||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||||
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
||||||
const selectData: SelectItem[] | undefined = categories
|
const selectData: SelectItem[] | undefined = categories
|
||||||
?.filter(c => c.id !== Constants.categories.all.id)
|
?.filter(c => c.id !== Constants.categories.all.id)
|
||||||
|
.filter(c => !props.withoutCategoryIds || !props.withoutCategoryIds.includes(c.id))
|
||||||
.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
||||||
.map(c => ({
|
.map(c => ({
|
||||||
label: c.name,
|
label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name,
|
||||||
value: c.id,
|
value: c.id,
|
||||||
}))
|
}))
|
||||||
if (props.withAll) {
|
if (props.withAll) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { t } from "@lingui/macro"
|
import { t } from "@lingui/macro"
|
||||||
import { ActionIcon, Center, Divider, Indicator, Popover, TextInput } from "@mantine/core"
|
import { ActionIcon, Center, Divider, Indicator, Popover, TextInput } from "@mantine/core"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { search } from "app/slices/entries"
|
import { reloadEntries, search } from "app/slices/entries"
|
||||||
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
|
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { ActionButton } from "components/ActionButtton"
|
import { ActionButton } from "components/ActionButtton"
|
||||||
@@ -11,7 +11,6 @@ import { useEffect } from "react"
|
|||||||
import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbSearch, TbUser, TbX } from "react-icons/tb"
|
import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbSearch, TbUser, TbX } from "react-icons/tb"
|
||||||
import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
|
import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
|
||||||
import { ProfileMenu } from "./ProfileMenu"
|
import { ProfileMenu } from "./ProfileMenu"
|
||||||
import { RefreshMenu } from "./RefreshMenu"
|
|
||||||
|
|
||||||
function HeaderDivider() {
|
function HeaderDivider() {
|
||||||
return <Divider orientation="vertical" />
|
return <Divider orientation="vertical" />
|
||||||
@@ -41,7 +40,7 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<Center>
|
<Center>
|
||||||
<ButtonToolbar>
|
<ButtonToolbar>
|
||||||
<RefreshMenu control={<ActionButton icon={<TbRefresh size={iconSize} />} label={t`Refresh`} />} />
|
<ActionButton icon={<TbRefresh size={iconSize} />} label={t`Refresh`} onClick={() => dispatch(reloadEntries())} />
|
||||||
<MarkAllAsReadButton iconSize={iconSize} />
|
<MarkAllAsReadButton iconSize={iconSize} />
|
||||||
|
|
||||||
<HeaderDivider />
|
<HeaderDivider />
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
import { Trans } from "@lingui/macro"
|
import { t, Trans } from "@lingui/macro"
|
||||||
import { Box, Divider, Group, Menu, SegmentedControl, SegmentedControlItem, useMantineColorScheme } from "@mantine/core"
|
import { Box, Divider, Group, Menu, SegmentedControl, SegmentedControlItem, useMantineColorScheme } from "@mantine/core"
|
||||||
|
import { showNotification } from "@mantine/notifications"
|
||||||
|
import { client } from "app/client"
|
||||||
import { redirectToAbout, redirectToAdminUsers, redirectToMetrics, redirectToSettings } from "app/slices/redirect"
|
import { redirectToAbout, redirectToAdminUsers, redirectToMetrics, redirectToSettings } from "app/slices/redirect"
|
||||||
import { changeViewMode } from "app/slices/user"
|
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { ViewMode } from "app/types"
|
import { ViewMode } from "app/types"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { TbChartLine, TbHelp, TbLayoutList, TbList, TbMoon, TbNotes, TbPower, TbSettings, TbSun, TbUsers } from "react-icons/tb"
|
import {
|
||||||
|
TbChartLine,
|
||||||
|
TbHelp,
|
||||||
|
TbLayoutList,
|
||||||
|
TbList,
|
||||||
|
TbListDetails,
|
||||||
|
TbMoon,
|
||||||
|
TbNotes,
|
||||||
|
TbPower,
|
||||||
|
TbSettings,
|
||||||
|
TbSun,
|
||||||
|
TbUsers,
|
||||||
|
TbWorldDownload,
|
||||||
|
} from "react-icons/tb"
|
||||||
|
import { useViewMode } from "../../hooks/useViewMode"
|
||||||
|
|
||||||
interface ProfileMenuProps {
|
interface ProfileMenuProps {
|
||||||
control: React.ReactElement
|
control: React.ReactElement
|
||||||
@@ -40,6 +55,17 @@ const viewModeData: ViewModeControlItem[] = [
|
|||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "detailed",
|
||||||
|
label: (
|
||||||
|
<Group>
|
||||||
|
<TbListDetails size={iconSize} />
|
||||||
|
<Box ml={6}>
|
||||||
|
<Trans>Detailed</Trans>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "expanded",
|
value: "expanded",
|
||||||
label: (
|
label: (
|
||||||
@@ -55,7 +81,8 @@ const viewModeData: ViewModeControlItem[] = [
|
|||||||
|
|
||||||
export function ProfileMenu(props: ProfileMenuProps) {
|
export function ProfileMenu(props: ProfileMenuProps) {
|
||||||
const [opened, setOpened] = useState(false)
|
const [opened, setOpened] = useState(false)
|
||||||
const viewMode = useAppSelector(state => state.user.settings?.viewMode)
|
const { viewMode, setViewMode } = useViewMode()
|
||||||
|
const profile = useAppSelector(state => state.user.profile)
|
||||||
const admin = useAppSelector(state => state.user.profile?.admin)
|
const admin = useAppSelector(state => state.user.profile?.admin)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme()
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme()
|
||||||
@@ -69,6 +96,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
|||||||
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}>
|
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}>
|
||||||
<Menu.Target>{props.control}</Menu.Target>
|
<Menu.Target>{props.control}</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
|
{profile && <Menu.Label>{profile.name}</Menu.Label>}
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
icon={<TbSettings size={iconSize} />}
|
icon={<TbSettings size={iconSize} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -78,6 +106,21 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
|||||||
>
|
>
|
||||||
<Trans>Settings</Trans>
|
<Trans>Settings</Trans>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
icon={<TbWorldDownload size={iconSize} />}
|
||||||
|
onClick={() =>
|
||||||
|
client.feed.refreshAll().then(() => {
|
||||||
|
showNotification({
|
||||||
|
message: t`Your feeds have been queued for refresh.`,
|
||||||
|
color: "green",
|
||||||
|
autoClose: 1000,
|
||||||
|
})
|
||||||
|
setOpened(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trans>Fetch all my feeds now</Trans>
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label>
|
<Menu.Label>
|
||||||
@@ -96,7 +139,7 @@ export function ProfileMenu(props: ProfileMenuProps) {
|
|||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
data={viewModeData}
|
data={viewModeData}
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
onChange={e => dispatch(changeViewMode(e as ViewMode))}
|
onChange={e => setViewMode(e as ViewMode)}
|
||||||
mb="xs"
|
mb="xs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
|
||||||
import { Menu } from "@mantine/core"
|
|
||||||
import { showNotification } from "@mantine/notifications"
|
|
||||||
import { client } from "app/client"
|
|
||||||
import { reloadEntries } from "app/slices/entries"
|
|
||||||
import { useAppDispatch } from "app/store"
|
|
||||||
import { TbRotateClockwise, TbWorldDownload } from "react-icons/tb"
|
|
||||||
|
|
||||||
interface RefreshMenuProps {
|
|
||||||
control: React.ReactElement
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconSize = 16
|
|
||||||
|
|
||||||
export function RefreshMenu(props: RefreshMenuProps) {
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Target>{props.control}</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<Menu.Item icon={<TbRotateClockwise size={iconSize} />} onClick={() => dispatch(reloadEntries())}>
|
|
||||||
<Trans>Reload</Trans>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
icon={<TbWorldDownload size={iconSize} />}
|
|
||||||
onClick={() =>
|
|
||||||
client.feed.refreshAll().then(() =>
|
|
||||||
showNotification({
|
|
||||||
message: t`Your feeds have been queued for refresh.`,
|
|
||||||
color: "green",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Trans>Fetch all my feeds now</Trans>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
7
commafeed-client/src/hooks/useViewMode.ts
Normal file
7
commafeed-client/src/hooks/useViewMode.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import useLocalStorage from "use-local-storage"
|
||||||
|
import { ViewMode } from "../app/types"
|
||||||
|
|
||||||
|
export function useViewMode() {
|
||||||
|
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("view-mode", "detailed")
|
||||||
|
return { viewMode, setViewMode }
|
||||||
|
}
|
||||||
@@ -219,6 +219,10 @@ msgstr "حذف المستخدم"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "تنازلي"
|
msgstr "تنازلي"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "موجز URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "اسم الخلاصة"
|
msgstr "اسم الخلاصة"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "تصفية التعبير"
|
msgstr "تصفية التعبير"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "هل نسيت كلمة المرور؟"
|
msgstr "هل نسيت كلمة المرور؟"
|
||||||
@@ -406,6 +414,7 @@ msgstr "تحميل العلامات ..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "تسجيل الدخول"
|
msgstr "تسجيل الدخول"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "تحديث"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
|
msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "قم بالتسجيل"
|
msgstr "قم بالتسجيل"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "تبديل قراءة حالة الإدخال الحالي"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"
|
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "غير مقروءة"
|
msgstr "غير مقروءة"
|
||||||
@@ -792,10 +802,14 @@ msgstr "موقع الكتروني"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "ليس لديك أي اشتراكات حتى الآن. "
|
msgstr "ليس لديك أي اشتراكات حتى الآن. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "الملف مطلوب"
|
msgstr "الملف مطلوب"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Suprimeix l'usuari"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL del canal"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nom del canal"
|
msgstr "Nom del canal"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Expressió de filtratge"
|
msgstr "Expressió de filtratge"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Heu oblidat la contrasenya?"
|
msgstr "Heu oblidat la contrasenya?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Carregant les etiquetes..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Inicia sessió"
|
msgstr "Inicia sessió"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Actualitzar"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
|
msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Mostra l'ajuda de la drecera del teclat"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Inscriu-te"
|
msgstr "Inscriu-te"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Canvia l'estat de lectura de l'entrada actual"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
|
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Sense llegir"
|
msgstr "Sense llegir"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Lloc web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Encara no teniu cap subscripció. "
|
msgstr "Encara no teniu cap subscripció. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "el fitxer és necessari"
|
msgstr "el fitxer és necessari"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Smazat uživatele"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL zdroje"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Název zdroje"
|
msgstr "Název zdroje"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrování výrazu"
|
msgstr "Filtrování výrazu"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Zapomněli jste heslo?"
|
msgstr "Zapomněli jste heslo?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Načítání značek..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Přihlaste se"
|
msgstr "Přihlaste se"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Obnovit"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
|
msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Zobrazit nápovědu ke klávesovým zkratkám"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Zaregistrujte se"
|
msgstr "Zaregistrujte se"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Přepne stav čtení aktuálního záznamu"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"
|
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Nepřečteno"
|
msgstr "Nepřečteno"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Webové stránky"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Zatím nemáte žádné předplatné. "
|
msgstr "Zatím nemáte žádné předplatné. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Dileu defnyddiwr"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Rhag"
|
msgstr "Rhag"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL porthiant"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Enw porthiant"
|
msgstr "Enw porthiant"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Hidlo mynegiant"
|
msgstr "Hidlo mynegiant"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Wedi anghofio cyfrinair?"
|
msgstr "Wedi anghofio cyfrinair?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Wrthi'n llwytho tagiau..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Mewngofnodi"
|
msgstr "Mewngofnodi"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Adnewyddu"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
|
msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Dangos cymorth llwybr byr bysellfwrdd"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Cofrestrwch"
|
msgstr "Cofrestrwch"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Toglo statws darllen y cofnod cyfredol"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"
|
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Heb ei ddarllen"
|
msgstr "Heb ei ddarllen"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Gwefan"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Nid oes gennych unrhyw danysgrifiadau eto. "
|
msgstr "Nid oes gennych unrhyw danysgrifiadau eto. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "mae angen y ffeil"
|
msgstr "mae angen y ffeil"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Slet bruger"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr ""
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Feednavn"
|
msgstr "Feednavn"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrerende udtryk"
|
msgstr "Filtrerende udtryk"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Glemt adgangskode?"
|
msgstr "Glemt adgangskode?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Indlæser tags..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Log ind"
|
msgstr "Log ind"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Opdater"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registreringer er lukket på denne CommaFeed-instans"
|
msgstr "Registreringer er lukket på denne CommaFeed-instans"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Vis hjælp til tastaturgenveje"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Tilmeld dig"
|
msgstr "Tilmeld dig"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Skift læsestatus for den aktuelle post"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Ulæst"
|
msgstr "Ulæst"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Hjemmeside"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Du har ingen abonnementer endnu. "
|
msgstr "Du har ingen abonnementer endnu. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fil er påkrævet"
|
msgstr "fil er påkrævet"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Benutzer löschen"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Beschr"
|
msgstr "Beschr"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Feed-URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Feedname"
|
msgstr "Feedname"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filterausdruck"
|
msgstr "Filterausdruck"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Passwort vergessen?"
|
msgstr "Passwort vergessen?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Tags werden geladen..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Einloggen"
|
msgstr "Einloggen"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Aktualisieren"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
|
msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Tastenkürzel-Hilfe anzeigen"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Melden Sie sich an"
|
msgstr "Melden Sie sich an"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Lesestatus des aktuellen Eintrags umschalten"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
|
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Ungelesen"
|
msgstr "Ungelesen"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Webseite"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Sie haben noch keine Abonnements. "
|
msgstr "Sie haben noch keine Abonnements. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "Datei ist erforderlich"
|
msgstr "Datei ist erforderlich"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Delete user"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Desc"
|
msgstr "Desc"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr "Detailed"
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Feed URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Feed name"
|
msgstr "Feed name"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr "Fetch all my feeds now"
|
msgstr "Fetch all my feeds now"
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr "Fetch all my feeds now"
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtering expression"
|
msgstr "Filtering expression"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Forgot password?"
|
msgstr "Forgot password?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Loading tags..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Log in"
|
msgstr "Log in"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Refresh"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registrations are closed on this CommaFeed instance"
|
msgstr "Registrations are closed on this CommaFeed instance"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr "Reload"
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr "Right click"
|
msgstr "Right click"
|
||||||
@@ -685,6 +690,7 @@ msgstr "Show keyboard shortcut help"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Sign up"
|
msgstr "Sign up"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Toggle read status of current entry"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Try out CommaFeed with the demo account: demo/demo"
|
msgstr "Try out CommaFeed with the demo account: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr "Try the demo!"
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Unread"
|
msgstr "Unread"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Website"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgstr "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr "Your feeds have been queued for refresh."
|
msgstr "Your feeds have been queued for refresh."
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "file is required"
|
msgstr "file is required"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr "{0} (in {1})"
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Borrar usuario"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL de fuente"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nombre de alimentación"
|
msgstr "Nombre de alimentación"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Expresión de filtrado"
|
msgstr "Expresión de filtrado"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "¿Olvidaste la contraseña?"
|
msgstr "¿Olvidaste la contraseña?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Cargando etiquetas..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Iniciar sesión"
|
msgstr "Iniciar sesión"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Actualizar"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Los registros están cerrados en esta instancia de CommaFeed"
|
msgstr "Los registros están cerrados en esta instancia de CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Mostrar ayuda de atajo de teclado"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Registrarse"
|
msgstr "Registrarse"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Alternar estado de lectura de la entrada actual"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"
|
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "No leído"
|
msgstr "No leído"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Sitio web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Todavía no tienes ninguna suscripción. "
|
msgstr "Todavía no tienes ninguna suscripción. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "archivo requerido"
|
msgstr "archivo requerido"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "حذف کاربر"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "توصیف"
|
msgstr "توصیف"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL فید"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "نام فید"
|
msgstr "نام فید"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "بیان فیلتر"
|
msgstr "بیان فیلتر"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "رمز عبور را فراموش کرده اید؟"
|
msgstr "رمز عبور را فراموش کرده اید؟"
|
||||||
@@ -406,6 +414,7 @@ msgstr "بارگیری برچسب ها..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "وارد شوید"
|
msgstr "وارد شوید"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "تازه کردن"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
|
msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "نمایش راهنمایی میانبر صفحه کلید"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "ثبت نام کنید"
|
msgstr "ثبت نام کنید"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"
|
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "خوانده نشده"
|
msgstr "خوانده نشده"
|
||||||
@@ -792,10 +802,14 @@ msgstr "وب سایت"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "شما هنوز هیچ اشتراکی ندارید. "
|
msgstr "شما هنوز هیچ اشتراکی ندارید. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "فایل مورد نیاز است"
|
msgstr "فایل مورد نیاز است"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Poista käyttäjä"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Syötteen URL-osoite"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Syötteen nimi"
|
msgstr "Syötteen nimi"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Suodattava lauseke"
|
msgstr "Suodattava lauseke"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Unohditko salasanan?"
|
msgstr "Unohditko salasanan?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Ladataan tunnisteita..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Kirjaudu sisään"
|
msgstr "Kirjaudu sisään"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Päivitä"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
|
msgstr "Tämän CommaFeed-esiintymän rekisteröinnit on suljettu"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Näytä pikanäppäimen ohje"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Rekisteröidy"
|
msgstr "Rekisteröidy"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Vaihda nykyisen merkinnän lukutila"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"
|
msgstr "Kokeile CommaFeediä demotilillä: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Lukematon"
|
msgstr "Lukematon"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Verkkosivusto"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Sinulla ei ole vielä tilauksia. "
|
msgstr "Sinulla ei ole vielä tilauksia. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "tiedosto vaaditaan"
|
msgstr "tiedosto vaaditaan"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Effacer l'utilisateur"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Descendant"
|
msgstr "Descendant"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr "Vue détaillée"
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL du flux"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nom du flux"
|
msgstr "Nom du flux"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Expression de filtrage"
|
msgstr "Expression de filtrage"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Mot de passe oublié ?"
|
msgstr "Mot de passe oublié ?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Chargement des tags ..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Connexion"
|
msgstr "Connexion"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Rafraîchir"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
|
msgstr "Les inscriptions sont fermées sur cette instance de CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Montrer les raccourcis clavier"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Créer un compte"
|
msgstr "Créer un compte"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Marquer l'entrée actuelle comme lue/non lue"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
|
msgstr "Essayez CommaFeed avec le compte de démonstration : demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Non lu"
|
msgstr "Non lu"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Site web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Vous n'avez pas encore d'abonnements. Pourquoi ne pas essayer d'en ajouter un en cliquant sur le signe + en haut de la page ?"
|
msgstr "Vous n'avez pas encore d'abonnements. Pourquoi ne pas essayer d'en ajouter un en cliquant sur le signe + en haut de la page ?"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fichier requis"
|
msgstr "fichier requis"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Eliminar usuario"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL da fonte"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nome do feed"
|
msgstr "Nome do feed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Expresión de filtrado"
|
msgstr "Expresión de filtrado"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Esqueceches o contrasinal?"
|
msgstr "Esqueceches o contrasinal?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Cargando etiquetas..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Iniciar sesión"
|
msgstr "Iniciar sesión"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Actualizar"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
|
msgstr "Os rexistros están pechados nesta instancia de CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Mostrar axuda do atallo do teclado"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Rexístrese"
|
msgstr "Rexístrese"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "alternar o estado de lectura da entrada actual"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"
|
msgstr "Proba CommaFeed coa conta de demostración: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Sen ler"
|
msgstr "Sen ler"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Páxina web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Aínda non tes ningunha subscrición. "
|
msgstr "Aínda non tes ningunha subscrición. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "é necesario o ficheiro"
|
msgstr "é necesario o ficheiro"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Felhasználó törlése"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr ""
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Hírcsatorna neve"
|
msgstr "Hírcsatorna neve"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Szűrő kifejezés"
|
msgstr "Szűrő kifejezés"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Elfelejtette a jelszavát?"
|
msgstr "Elfelejtette a jelszavát?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Címkék betöltése..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Jelentkezzen be"
|
msgstr "Jelentkezzen be"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Frissítés"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
|
msgstr "A regisztrációk le vannak zárva ezen a CommaFeed példányon"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "A billentyűparancsok súgójának megjelenítése"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Regisztráljon"
|
msgstr "Regisztráljon"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Az aktuális bejegyzés olvasási állapotának váltása"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"
|
msgstr "Próbálja ki a CommaFeed-et a demo fiókkal: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Olvasatlan"
|
msgstr "Olvasatlan"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Webhely"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Még nincs előfizetése. "
|
msgstr "Még nincs előfizetése. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fájl szükséges"
|
msgstr "fájl szükséges"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Hapus pengguna"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL Umpan"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nama umpan"
|
msgstr "Nama umpan"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Memfilter ekspresi"
|
msgstr "Memfilter ekspresi"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Lupa kata sandi?"
|
msgstr "Lupa kata sandi?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Memuat tag..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Masuk"
|
msgstr "Masuk"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Segarkan"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
|
msgstr "Pendaftaran ditutup pada instans CommaFeed ini"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Tampilkan bantuan pintasan keyboard"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Daftar"
|
msgstr "Daftar"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Beralih status baca entri saat ini"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"
|
msgstr "Cobalah CommaFeed dengan akun demo: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Belum Dibaca"
|
msgstr "Belum Dibaca"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Situs Web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Anda belum memiliki langganan. "
|
msgstr "Anda belum memiliki langganan. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "file diperlukan"
|
msgstr "file diperlukan"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Elimina utente"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL feed"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nome del feed"
|
msgstr "Nome del feed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Espressione filtrante"
|
msgstr "Espressione filtrante"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Password dimenticata?"
|
msgstr "Password dimenticata?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Caricamento tag..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Accedi"
|
msgstr "Accedi"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Aggiorna"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
|
msgstr "Le registrazioni sono chiuse su questa istanza CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Mostra la guida alle scorciatoie da tastiera"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Iscriviti"
|
msgstr "Iscriviti"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Commuta lo stato di lettura della voce corrente"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Prova CommaFeed con il conto demo: demo/demo"
|
msgstr "Prova CommaFeed con il conto demo: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Non letto"
|
msgstr "Non letto"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Sito web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Non hai ancora abbonamenti. "
|
msgstr "Non hai ancora abbonamenti. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "è richiesto il file"
|
msgstr "è richiesto il file"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "ユーザーの削除"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "説明"
|
msgstr "説明"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "フィード URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "フィード名"
|
msgstr "フィード名"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "フィルタリング式"
|
msgstr "フィルタリング式"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "パスワードをお忘れですか?"
|
msgstr "パスワードをお忘れですか?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "タグを読み込んでいます..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "ログイン"
|
msgstr "ログイン"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "リフレッシュ"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "この CommaFeed インスタンスの登録は終了しています"
|
msgstr "この CommaFeed インスタンスの登録は終了しています"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "キーボード ショートカットのヘルプを表示"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "サインアップ"
|
msgstr "サインアップ"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "現在のエントリの読み取りステータスを切り替えます
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "デモアカウントで CommaFeed を試す: demo/demo"
|
msgstr "デモアカウントで CommaFeed を試す: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "未読"
|
msgstr "未読"
|
||||||
@@ -792,10 +802,14 @@ msgstr "ウェブサイト"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "まだサブスクリプションがありません。"
|
msgstr "まだサブスクリプションがありません。"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "ファイルが必要です"
|
msgstr "ファイルが必要です"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "사용자 삭제"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "설명"
|
msgstr "설명"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "피드 URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "피드 이름"
|
msgstr "피드 이름"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "필터링 표현식"
|
msgstr "필터링 표현식"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "비밀번호를 잊으셨나요?"
|
msgstr "비밀번호를 잊으셨나요?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "태그 로드 중..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "로그인"
|
msgstr "로그인"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "새로 고침"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "이 CommaFeed 인스턴스에 대한 등록이 마감되었습니다."
|
msgstr "이 CommaFeed 인스턴스에 대한 등록이 마감되었습니다."
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "키보드 단축키 도움말 표시"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "가입"
|
msgstr "가입"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "현재 항목의 읽기 상태 전환"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"
|
msgstr "데모 계정으로 CommaFeed를 사용해 보세요: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "읽지 않음"
|
msgstr "읽지 않음"
|
||||||
@@ -792,10 +802,14 @@ msgstr "웹사이트"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "아직 구독이 없습니다. "
|
msgstr "아직 구독이 없습니다. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "파일이 필요합니다"
|
msgstr "파일이 필요합니다"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Padam pengguna"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Dec"
|
msgstr "Dec"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL Suapan"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nama suapan"
|
msgstr "Nama suapan"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Ungkapan penapisan"
|
msgstr "Ungkapan penapisan"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Lupa kata laluan?"
|
msgstr "Lupa kata laluan?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Memuatkan tag..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Log masuk"
|
msgstr "Log masuk"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Muat semula"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Pendaftaran ditutup pada contoh CommaFeed ini"
|
msgstr "Pendaftaran ditutup pada contoh CommaFeed ini"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Tunjukkan bantuan pintasan papan kekunci"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Daftar"
|
msgstr "Daftar"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Togol status bacaan entri semasa"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"
|
msgstr "Cuba CommaFeed dengan akaun demo: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Belum dibaca"
|
msgstr "Belum dibaca"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Laman web"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Anda belum mempunyai sebarang langganan lagi. "
|
msgstr "Anda belum mempunyai sebarang langganan lagi. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fail diperlukan"
|
msgstr "fail diperlukan"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Slett bruker"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Feed-URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Feednavn"
|
msgstr "Feednavn"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrerende uttrykk"
|
msgstr "Filtrerende uttrykk"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Glemt passord?"
|
msgstr "Glemt passord?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Laster tagger..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Logg inn"
|
msgstr "Logg inn"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Oppdater"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
|
msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Vis hurtigtasthjelp"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Meld deg på"
|
msgstr "Meld deg på"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Veksle lesestatus for gjeldende oppføring"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Ulest"
|
msgstr "Ulest"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Nettsted"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Du har ingen abonnementer ennå. "
|
msgstr "Du har ingen abonnementer ennå. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fil kreves"
|
msgstr "fil kreves"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Gebruiker verwijderen"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Beschrijving"
|
msgstr "Beschrijving"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Feed-URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Feednaam"
|
msgstr "Feednaam"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Uitdrukking filteren"
|
msgstr "Uitdrukking filteren"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Wachtwoord vergeten?"
|
msgstr "Wachtwoord vergeten?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Tags laden..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Inloggen"
|
msgstr "Inloggen"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Vernieuwen"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registraties zijn gesloten op deze CommaFeed-instantie"
|
msgstr "Registraties zijn gesloten op deze CommaFeed-instantie"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Toon hulp bij sneltoetsen"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Aanmelden"
|
msgstr "Aanmelden"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Toggle leesstatus van huidige invoer"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"
|
msgstr "Probeer CommaFeed uit met het demo-account: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Ongelezen"
|
msgstr "Ongelezen"
|
||||||
@@ -792,10 +802,14 @@ msgstr ""
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Je hebt nog geen abonnementen. "
|
msgstr "Je hebt nog geen abonnementen. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "bestand is vereist"
|
msgstr "bestand is vereist"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Slett bruker"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Feed-URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Feednavn"
|
msgstr "Feednavn"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrerende uttrykk"
|
msgstr "Filtrerende uttrykk"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Glemt passord?"
|
msgstr "Glemt passord?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Laster tagger..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Logg inn"
|
msgstr "Logg inn"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Oppdater"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
|
msgstr "Registreringer er stengt på denne CommaFeed-forekomsten"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Vis hurtigtasthjelp"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Meld deg på"
|
msgstr "Meld deg på"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Veksle lesestatus for gjeldende oppføring"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Ulest"
|
msgstr "Ulest"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Nettsted"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Du har ingen abonnementer ennå. "
|
msgstr "Du har ingen abonnementer ennå. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fil kreves"
|
msgstr "fil kreves"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Usuń użytkownika"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Opis"
|
msgstr "Opis"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL kanału"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "nazwa kanału"
|
msgstr "nazwa kanału"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Wyrażenie filtrujące"
|
msgstr "Wyrażenie filtrujące"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Zapomniałeś hasła?"
|
msgstr "Zapomniałeś hasła?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Ładowanie tagów..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Zaloguj się"
|
msgstr "Zaloguj się"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Odśwież"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Rejestracje są zamknięte w tej instancji CommaFeed"
|
msgstr "Rejestracje są zamknięte w tej instancji CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Pokaż pomoc dotyczącą skrótów klawiaturowych"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Zarejestruj się"
|
msgstr "Zarejestruj się"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Przełącz stan odczytu bieżącego wpisu"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"
|
msgstr "Wypróbuj CommaFeed z kontem demo: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Nieprzeczytane"
|
msgstr "Nieprzeczytane"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Strona internetowa"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Nie masz jeszcze żadnych subskrypcji. "
|
msgstr "Nie masz jeszcze żadnych subskrypcji. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "plik jest wymagany"
|
msgstr "plik jest wymagany"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Excluir usuário"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Descrição"
|
msgstr "Descrição"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL do feed"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Nome do feed"
|
msgstr "Nome do feed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrando expressão"
|
msgstr "Filtrando expressão"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Esqueceu a senha?"
|
msgstr "Esqueceu a senha?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Carregando tags..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Atualizar"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Os registros estão fechados nesta instância do CommaFeed"
|
msgstr "Os registros estão fechados nesta instância do CommaFeed"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Mostrar ajuda de atalho de teclado"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Inscreva-se"
|
msgstr "Inscreva-se"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Alternar o status de leitura da entrada atual"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Experimente o CommaFeed com a conta demo: demo/demo"
|
msgstr "Experimente o CommaFeed com a conta demo: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Não lido"
|
msgstr "Não lido"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Site"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Você ainda não tem nenhuma assinatura. "
|
msgstr "Você ainda não tem nenhuma assinatura. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "o arquivo é obrigatório"
|
msgstr "o arquivo é obrigatório"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Удалить пользователя"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "По убыванию"
|
msgstr "По убыванию"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL-адрес фида"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Имя фида"
|
msgstr "Имя фида"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Выражение фильтрации"
|
msgstr "Выражение фильтрации"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Забыли пароль?"
|
msgstr "Забыли пароль?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Загрузка тегов..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Войти"
|
msgstr "Войти"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Обновить"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Регистрация закрыта для этого экземпляра CommaFeed."
|
msgstr "Регистрация закрыта для этого экземпляра CommaFeed."
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Показать справку по сочетаниям клавиш."
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Зарегистрироваться"
|
msgstr "Зарегистрироваться"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Переключить статус чтения текущей запи
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"
|
msgstr "Попробуйте CommaFeed на демо-счете: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "непрочитано"
|
msgstr "непрочитано"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Веб-сайт"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "У вас пока нет подписок. "
|
msgstr "У вас пока нет подписок. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "требуется файл"
|
msgstr "требуется файл"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Vymažte používateľa"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "URL informačného kanála"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Názov informačného kanála"
|
msgstr "Názov informačného kanála"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrovanie výrazu"
|
msgstr "Filtrovanie výrazu"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Zabudli ste heslo?"
|
msgstr "Zabudli ste heslo?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Načítavam značky..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Prihláste sa"
|
msgstr "Prihláste sa"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Obnoviť"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "V tejto inštancii CommaFeed sú registrácie uzavreté"
|
msgstr "V tejto inštancii CommaFeed sú registrácie uzavreté"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Zobraziť pomoc s klávesovými skratkami"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Zaregistrujte sa"
|
msgstr "Zaregistrujte sa"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Prepne stav čítania aktuálneho záznamu"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"
|
msgstr "Vyskúšajte CommaFeed s demo účtom: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Neprečítané"
|
msgstr "Neprečítané"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Webová stránka"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Zatiaľ nemáte žiadne odbery. "
|
msgstr "Zatiaľ nemáte žiadne odbery. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Ta bort användare"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Flödes-URL"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Flödesnamn"
|
msgstr "Flödesnamn"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtrerande uttryck"
|
msgstr "Filtrerande uttryck"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Glömt lösenord?"
|
msgstr "Glömt lösenord?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Laddar taggar..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Logga in"
|
msgstr "Logga in"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Uppdatera"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Registreringar är stängda på denna CommaFeed-instans"
|
msgstr "Registreringar är stängda på denna CommaFeed-instans"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Visa kortkommandohjälp"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Anmäl dig"
|
msgstr "Anmäl dig"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Växla lässtatus för aktuell post"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "Prova CommaFeed med demokontot: demo/demo"
|
msgstr "Prova CommaFeed med demokontot: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Oläst"
|
msgstr "Oläst"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Webbplats"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Du har inga prenumerationer än. "
|
msgstr "Du har inga prenumerationer än. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "fil krävs"
|
msgstr "fil krävs"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "Kullanıcıyı sil"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "Açılış"
|
msgstr "Açılış"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "Feed URL'si"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "Yayın adı"
|
msgstr "Yayın adı"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "Filtreleme ifadesi"
|
msgstr "Filtreleme ifadesi"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "Parolanızı mı unuttunuz?"
|
msgstr "Parolanızı mı unuttunuz?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "Etiketler yükleniyor..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Giriş"
|
msgstr "Giriş"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "Yenile"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "Bu CommaFeed örneğinde kayıtlar kapalı"
|
msgstr "Bu CommaFeed örneğinde kayıtlar kapalı"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "Klavye kısayolu yardımını göster"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "Kaydolun"
|
msgstr "Kaydolun"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "Geçerli girişin okuma durumunu değiştir"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"
|
msgstr "CommaFeed'i demo hesabıyla deneyin: demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "Okunmadı"
|
msgstr "Okunmadı"
|
||||||
@@ -792,10 +802,14 @@ msgstr "Web sitesi"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "Henüz aboneliğiniz yok. "
|
msgstr "Henüz aboneliğiniz yok. "
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "dosya gerekli"
|
msgstr "dosya gerekli"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ msgstr "删除用户"
|
|||||||
msgid "Desc"
|
msgid "Desc"
|
||||||
msgstr "描述"
|
msgstr "描述"
|
||||||
|
|
||||||
|
#: src/components/header/ProfileMenu.tsx
|
||||||
|
msgid "Detailed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/ProfileMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
#: src/pages/app/SettingsPage.tsx
|
#: src/pages/app/SettingsPage.tsx
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
@@ -288,7 +292,7 @@ msgstr "供稿网址"
|
|||||||
msgid "Feed name"
|
msgid "Feed name"
|
||||||
msgstr "提要名称"
|
msgstr "提要名称"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Fetch all my feeds now"
|
msgid "Fetch all my feeds now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -296,6 +300,10 @@ msgstr ""
|
|||||||
msgid "Filtering expression"
|
msgid "Filtering expression"
|
||||||
msgstr "过滤表达式"
|
msgstr "过滤表达式"
|
||||||
|
|
||||||
|
#: src/pages/app/AboutPage.tsx
|
||||||
|
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr "忘记密码?"
|
msgstr "忘记密码?"
|
||||||
@@ -406,6 +414,7 @@ msgstr "正在加载标签..."
|
|||||||
|
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
#: src/pages/auth/LoginPage.tsx
|
#: src/pages/auth/LoginPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "登录"
|
msgstr "登录"
|
||||||
|
|
||||||
@@ -608,10 +617,6 @@ msgstr "刷新"
|
|||||||
msgid "Registrations are closed on this CommaFeed instance"
|
msgid "Registrations are closed on this CommaFeed instance"
|
||||||
msgstr "此 CommaFeed 实例上的注册已关闭"
|
msgstr "此 CommaFeed 实例上的注册已关闭"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
|
||||||
msgid "Reload"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/KeyboardShortcutsHelp.tsx
|
#: src/components/KeyboardShortcutsHelp.tsx
|
||||||
msgid "Right click"
|
msgid "Right click"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -685,6 +690,7 @@ msgstr "显示键盘快捷键帮助"
|
|||||||
|
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
#: src/pages/auth/RegistrationPage.tsx
|
#: src/pages/auth/RegistrationPage.tsx
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
msgid "Sign up"
|
msgid "Sign up"
|
||||||
msgstr "注册"
|
msgstr "注册"
|
||||||
|
|
||||||
@@ -757,6 +763,10 @@ msgstr "切换当前条目的读取状态"
|
|||||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||||
msgstr "使用演示帐户试用 CommaFeed:demo/demo"
|
msgstr "使用演示帐户试用 CommaFeed:demo/demo"
|
||||||
|
|
||||||
|
#: src/pages/WelcomePage.tsx
|
||||||
|
msgid "Try the demo!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/header/Header.tsx
|
#: src/components/header/Header.tsx
|
||||||
msgid "Unread"
|
msgid "Unread"
|
||||||
msgstr "未读"
|
msgstr "未读"
|
||||||
@@ -792,10 +802,14 @@ msgstr "网站"
|
|||||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||||
msgstr "您还没有任何订阅。"
|
msgstr "您还没有任何订阅。"
|
||||||
|
|
||||||
#: src/components/header/RefreshMenu.tsx
|
#: src/components/header/ProfileMenu.tsx
|
||||||
msgid "Your feeds have been queued for refresh."
|
msgid "Your feeds have been queued for refresh."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/content/add/ImportOpml.tsx
|
#: src/components/content/add/ImportOpml.tsx
|
||||||
msgid "file is required"
|
msgid "file is required"
|
||||||
msgstr "文件是必需的"
|
msgstr "文件是必需的"
|
||||||
|
|
||||||
|
#: src/components/content/add/CategorySelect.tsx
|
||||||
|
msgid "{0} (in {1})"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
126
commafeed-client/src/pages/WelcomePage.tsx
Normal file
126
commafeed-client/src/pages/WelcomePage.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { t } from "@lingui/macro"
|
||||||
|
import { Anchor, Box, Center, Container, Divider, Group, Image, Title, useMantineColorScheme } from "@mantine/core"
|
||||||
|
import { useMediaQuery } from "@mantine/hooks"
|
||||||
|
import welcome_page_dark from "assets/welcome_page_dark.png"
|
||||||
|
import welcome_page_light from "assets/welcome_page_light.png"
|
||||||
|
import { useAsyncCallback } from "react-async-hook"
|
||||||
|
import { SiGithub, TbKey, TbUserPlus } from "react-icons/all"
|
||||||
|
import { SiTwitter } from "react-icons/si"
|
||||||
|
import { TbClock, TbMoon, TbSun } from "react-icons/tb"
|
||||||
|
import { client } from "../app/client"
|
||||||
|
import { Constants } from "../app/constants"
|
||||||
|
import { redirectToLogin, redirectToRegistration, redirectToRootCategory } from "../app/slices/redirect"
|
||||||
|
import { useAppDispatch, useAppSelector } from "../app/store"
|
||||||
|
import { ActionButton } from "../components/ActionButtton"
|
||||||
|
import { ButtonToolbar } from "../components/ButtonToolbar"
|
||||||
|
import { PageTitle } from "./PageTitle"
|
||||||
|
|
||||||
|
export function WelcomePage() {
|
||||||
|
const { colorScheme } = useMantineColorScheme()
|
||||||
|
const image = colorScheme === "light" ? welcome_page_light : welcome_page_dark
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Center my="xl">
|
||||||
|
<Title order={3}>Bloat-free feed reader</Title>
|
||||||
|
</Center>
|
||||||
|
|
||||||
|
<Divider my="xl" />
|
||||||
|
|
||||||
|
<Image src={image} />
|
||||||
|
|
||||||
|
<Divider my="xl" />
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint}px)`)
|
||||||
|
|
||||||
|
if (mobile) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle />
|
||||||
|
<Center>
|
||||||
|
<Buttons />
|
||||||
|
</Center>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group position="apart">
|
||||||
|
<PageTitle />
|
||||||
|
<Buttons />
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Buttons() {
|
||||||
|
const iconSize = 18
|
||||||
|
const serverInfos = useAppSelector(state => state.server.serverInfos)
|
||||||
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme()
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const login = useAsyncCallback(client.user.login, {
|
||||||
|
onSuccess: () => {
|
||||||
|
dispatch(redirectToRootCategory())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonToolbar>
|
||||||
|
{serverInfos?.demoAccountEnabled && (
|
||||||
|
<ActionButton
|
||||||
|
label={t`Try the demo!`}
|
||||||
|
icon={<TbClock size={iconSize} />}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => login.execute({ name: "demo", password: "demo" })}
|
||||||
|
showLabelOnMobile
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ActionButton
|
||||||
|
label={t`Log in`}
|
||||||
|
icon={<TbKey size={iconSize} />}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => dispatch(redirectToLogin())}
|
||||||
|
showLabelOnMobile
|
||||||
|
/>
|
||||||
|
{serverInfos?.allowRegistrations && (
|
||||||
|
<ActionButton
|
||||||
|
label={t`Sign up`}
|
||||||
|
icon={<TbUserPlus size={iconSize} />}
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => dispatch(redirectToRegistration())}
|
||||||
|
showLabelOnMobile
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon={colorScheme === "dark" ? <TbSun size={18} /> : <TbMoon size={iconSize} />}
|
||||||
|
onClick={() => toggleColorScheme()}
|
||||||
|
/>
|
||||||
|
</ButtonToolbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Group>
|
||||||
|
<span>© CommaFeed</span>
|
||||||
|
<span> - </span>
|
||||||
|
<Anchor variant="text" href="https://github.com/Athou/commafeed/" target="_blank" rel="noreferrer">
|
||||||
|
<SiGithub />
|
||||||
|
</Anchor>
|
||||||
|
<Anchor variant="text" href="https://twitter.com/CommaFeed" target="_blank" rel="noreferrer">
|
||||||
|
<SiTwitter />
|
||||||
|
</Anchor>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Accordion, Box, Tabs } from "@mantine/core"
|
import { Accordion, Tabs } from "@mantine/core"
|
||||||
import { client } from "app/client"
|
import { client } from "app/client"
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
import { Gauge } from "components/metrics/Gauge"
|
|
||||||
import { Meter } from "components/metrics/Meter"
|
import { Meter } from "components/metrics/Meter"
|
||||||
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
|
import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
|
||||||
import { Timer } from "components/metrics/Timer"
|
import { Timer } from "components/metrics/Timer"
|
||||||
@@ -9,26 +8,18 @@ import { useAsync } from "react-async-hook"
|
|||||||
import { TbChartAreaLine, TbClock } from "react-icons/tb"
|
import { TbChartAreaLine, TbClock } from "react-icons/tb"
|
||||||
|
|
||||||
const shownMeters: { [key: string]: string } = {
|
const shownMeters: { [key: string]: string } = {
|
||||||
"com.commafeed.backend.feed.FeedQueues.refill": "Refresh queue refill rate",
|
"com.commafeed.backend.feed.FeedRefreshEngine.refill": "Feed queue refill rate",
|
||||||
"com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed": "Feed refreshed",
|
"com.commafeed.backend.feed.FeedRefreshWorker.feedFetched": "Feed fetching rate",
|
||||||
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed updated",
|
"com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed update rate",
|
||||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit",
|
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit rate",
|
||||||
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss",
|
"com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss rate",
|
||||||
}
|
|
||||||
|
|
||||||
const shownGauges: { [key: string]: string } = {
|
|
||||||
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active": "Feed Updater active",
|
|
||||||
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending": "Feed Updater queued",
|
|
||||||
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active": "Feed Worker active",
|
|
||||||
"com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending": "Feed Worker queued",
|
|
||||||
"com.commafeed.backend.feed.FeedQueues.queue": "Feed Refresh queue size",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetricsPage() {
|
export function MetricsPage() {
|
||||||
const query = useAsync(() => client.admin.getMetrics(), [])
|
const query = useAsync(() => client.admin.getMetrics(), [])
|
||||||
|
|
||||||
if (!query.result) return <Loader />
|
if (!query.result) return <Loader />
|
||||||
const { meters, gauges, timers } = query.result.data
|
const { meters, timers } = query.result.data
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="stats">
|
<Tabs defaultValue="stats">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
@@ -48,15 +39,6 @@ export function MetricsPage() {
|
|||||||
</MetricAccordionItem>
|
</MetricAccordionItem>
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Box pt="xs">
|
|
||||||
{Object.keys(shownGauges).map(g => (
|
|
||||||
<Box key={g}>
|
|
||||||
<span>{shownGauges[g]} </span>
|
|
||||||
<Gauge gauge={gauges[g]} />
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="timers" pt="xs">
|
<Tabs.Panel value="timers" pt="xs">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t, Trans } from "@lingui/macro"
|
import { t, Trans } from "@lingui/macro"
|
||||||
import { Anchor, Box, Center, Container, createStyles, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
import { Anchor, Box, Center, Code, Container, createStyles, List, NativeSelect, SimpleGrid, Title } from "@mantine/core"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToApiDocumentation } from "app/slices/redirect"
|
import { redirectToApiDocumentation } from "app/slices/redirect"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
@@ -57,6 +57,7 @@ function NextUnreadBookmarklet() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bitcoinAddress = <Code>{Constants.bitcoinWalletAddress}</Code>
|
||||||
export function AboutPage() {
|
export function AboutPage() {
|
||||||
const version = useAppSelector(state => state.server.serverInfos?.version)
|
const version = useAppSelector(state => state.server.serverInfos?.version)
|
||||||
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
|
const revision = useAppSelector(state => state.server.serverInfos?.gitCommit)
|
||||||
@@ -70,7 +71,7 @@ export function AboutPage() {
|
|||||||
CommaFeed version {version} ({revision})
|
CommaFeed version {version} ({revision})
|
||||||
</Trans>
|
</Trans>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box mt="md">
|
||||||
<Trans>
|
<Trans>
|
||||||
CommaFeed is an open-source project. Sources are hosted on
|
CommaFeed is an open-source project. Sources are hosted on
|
||||||
<Anchor href="https://github.com/Athou/commafeed" target="_blank" rel="noreferrer">
|
<Anchor href="https://github.com/Athou/commafeed" target="_blank" rel="noreferrer">
|
||||||
@@ -114,6 +115,9 @@ export function AboutPage() {
|
|||||||
</Center>
|
</Center>
|
||||||
</form>
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box mt="xs">
|
||||||
|
<Trans>For those of you who prefer bitcoin, here is the address: {bitcoinAddress}</Trans>
|
||||||
|
</Box>
|
||||||
</Section>
|
</Section>
|
||||||
<Section title={t`Goodies`} icon={<TbPuzzle size={24} />}>
|
<Section title={t`Goodies`} icon={<TbPuzzle size={24} />}>
|
||||||
<List>
|
<List>
|
||||||
|
|||||||
@@ -109,7 +109,12 @@ export function CategoryDetailsPage() {
|
|||||||
{editable && (
|
{editable && (
|
||||||
<>
|
<>
|
||||||
<TextInput label={t`Name`} {...form.getInputProps("name")} required />
|
<TextInput label={t`Name`} {...form.getInputProps("name")} required />
|
||||||
<CategorySelect label={t`Parent Category`} {...form.getInputProps("parentId")} clearable />
|
<CategorySelect
|
||||||
|
label={t`Parent Category`}
|
||||||
|
{...form.getInputProps("parentId")}
|
||||||
|
clearable
|
||||||
|
withoutCategoryIds={[id]}
|
||||||
|
/>
|
||||||
<NumberInput label={t`Position`} {...form.getInputProps("position")} required min={0} />
|
<NumberInput label={t`Position`} {...form.getInputProps("position")} required min={0} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
|||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
function FilteringExpressionDescription() {
|
function FilteringExpressionDescription() {
|
||||||
const example = <Code>url.contains('youtube') or (author eq 'athou' and title.contains('github')</Code>
|
const example = <Code>url.contains('youtube') or (author eq 'athou' and title.contains('github'))</Code>
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ app:
|
|||||||
|
|
||||||
# user-agent string that will be used by the http client, leave empty for the default one
|
# user-agent string that will be used by the http client, leave empty for the default one
|
||||||
userAgent:
|
userAgent:
|
||||||
|
|
||||||
# Database connection
|
# Database connection
|
||||||
# -------------------
|
# -------------------
|
||||||
# for MySQL
|
# for MySQL
|
||||||
@@ -92,7 +92,7 @@ database:
|
|||||||
properties:
|
properties:
|
||||||
charSet: UTF-8
|
charSet: UTF-8
|
||||||
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
validationQuery: "/* CommaFeed Health Check */ SELECT 1"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
applicationConnectors:
|
applicationConnectors:
|
||||||
- type: http
|
- type: http
|
||||||
@@ -100,7 +100,7 @@ server:
|
|||||||
adminConnectors:
|
adminConnectors:
|
||||||
- type: http
|
- type: http
|
||||||
port: 8084
|
port: 8084
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level: INFO
|
level: INFO
|
||||||
loggers:
|
loggers:
|
||||||
@@ -108,6 +108,7 @@ logging:
|
|||||||
liquibase: INFO
|
liquibase: INFO
|
||||||
org.hibernate.SQL: INFO # or ALL for sql debugging
|
org.hibernate.SQL: INFO # or ALL for sql debugging
|
||||||
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: WARN
|
||||||
|
org.hibernate.orm.deprecation: "OFF"
|
||||||
appenders:
|
appenders:
|
||||||
- type: console
|
- type: console
|
||||||
- type: file
|
- type: file
|
||||||
@@ -117,14 +118,16 @@ logging:
|
|||||||
archivedLogFilenamePattern: log/commafeed-%d.log
|
archivedLogFilenamePattern: log/commafeed-%d.log
|
||||||
archivedFileCount: 5
|
archivedFileCount: 5
|
||||||
timeZone: UTC
|
timeZone: UTC
|
||||||
|
|
||||||
# Redis pool configuration
|
# Redis pool configuration
|
||||||
# (only used if app.cache is 'redis')
|
# (only used if app.cache is 'redis')
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
password:
|
# username is only required when using ACLs
|
||||||
|
username:
|
||||||
|
password:
|
||||||
timeout: 2000
|
timeout: 2000
|
||||||
database: 0
|
database: 0
|
||||||
maxTotal: 500
|
maxTotal: 500
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ app:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
driverClass: org.h2.Driver
|
driverClass: org.h2.Driver
|
||||||
url: jdbc:h2:/home/commafeed/db
|
url: jdbc:h2:/commafeed/data/db
|
||||||
user: sa
|
user: sa
|
||||||
password: sa
|
password: sa
|
||||||
properties:
|
properties:
|
||||||
@@ -104,9 +104,11 @@ server:
|
|||||||
adminConnectors:
|
adminConnectors:
|
||||||
- type: http
|
- type: http
|
||||||
port: 8084
|
port: 8084
|
||||||
|
requestLog:
|
||||||
|
appenders: [ ]
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level: WARN
|
level: ERROR
|
||||||
loggers:
|
loggers:
|
||||||
com.commafeed: INFO
|
com.commafeed: INFO
|
||||||
liquibase: INFO
|
liquibase: INFO
|
||||||
@@ -128,6 +130,8 @@ logging:
|
|||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
|
# username is only required when using ACLs
|
||||||
|
username:
|
||||||
password:
|
password:
|
||||||
timeout: 2000
|
timeout: 2000
|
||||||
database: 0
|
database: 0
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>3.0.0</version>
|
<version>3.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-server</artifactId>
|
<artifactId>commafeed-server</artifactId>
|
||||||
<name>CommaFeed Server</name>
|
<name>CommaFeed Server</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<guice.version>5.1.0</guice.version>
|
<guice.version>5.1.0</guice.version>
|
||||||
<querydsl.version>4.2.1</querydsl.version>
|
<querydsl.version>4.4.0</querydsl.version>
|
||||||
<rome.version>1.18.0</rome.version>
|
<rome.version>2.1.0</rome.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.dropwizard</groupId>
|
<groupId>io.dropwizard</groupId>
|
||||||
<artifactId>dropwizard-dependencies</artifactId>
|
<artifactId>dropwizard-dependencies</artifactId>
|
||||||
<version>2.1.1</version>
|
<version>2.1.6</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@@ -38,6 +39,13 @@
|
|||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<parameters>true</parameters>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
@@ -105,8 +113,10 @@
|
|||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
|
<transformer
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>com.commafeed.CommaFeedApplication</mainClass>
|
<mainClass>com.commafeed.CommaFeedApplication</mainClass>
|
||||||
</transformer>
|
</transformer>
|
||||||
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
|
<transformer implementation="org.kordamp.shade.resources.PropertiesFileTransformer">
|
||||||
@@ -223,13 +233,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<version>3.0.0</version>
|
<version>3.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.22</version>
|
<version>1.18.26</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -283,7 +293,18 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>be.tomcools</groupId>
|
<groupId>be.tomcools</groupId>
|
||||||
<artifactId>dropwizard-websocket-jee7-bundle</artifactId>
|
<artifactId>dropwizard-websocket-jee7-bundle</artifactId>
|
||||||
<version>2.0.0</version>
|
<version>2.1.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.whitfin</groupId>
|
||||||
|
<artifactId>dropwizard-environment-substitutor</artifactId>
|
||||||
|
<version>1.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.reactivex.rxjava3</groupId>
|
||||||
|
<artifactId>rxjava</artifactId>
|
||||||
|
<version>3.1.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -366,7 +387,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>2.7.2</version>
|
<version>4.3.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.sun.mail</groupId>
|
<groupId>com.sun.mail</groupId>
|
||||||
@@ -398,50 +419,38 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
<version>1.14.3</version>
|
<version>1.15.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ibm.icu</groupId>
|
<groupId>com.ibm.icu</groupId>
|
||||||
<artifactId>icu4j</artifactId>
|
<artifactId>icu4j</artifactId>
|
||||||
<version>70.1</version>
|
<version>73.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.cssparser</groupId>
|
<groupId>net.sourceforge.cssparser</groupId>
|
||||||
<artifactId>cssparser</artifactId>
|
<artifactId>cssparser</artifactId>
|
||||||
<version>0.9.29</version>
|
<version>0.9.30</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>edu.uci.ics</groupId>
|
<groupId>org.netpreserve</groupId>
|
||||||
<artifactId>crawler4j</artifactId>
|
<artifactId>urlcanon</artifactId>
|
||||||
<version>3.5</version>
|
<version>0.4.0</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>log4j</groupId>
|
|
||||||
<artifactId>log4j</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.gwt</groupId>
|
<groupId>org.gwtproject</groupId>
|
||||||
<artifactId>gwt-servlet</artifactId>
|
<artifactId>gwt-servlet</artifactId>
|
||||||
<version>2.9.0</version>
|
<version>2.10.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.hakky54</groupId>
|
<groupId>io.github.hakky54</groupId>
|
||||||
<artifactId>sslcontext-kickstart</artifactId>
|
<artifactId>sslcontext-kickstart</artifactId>
|
||||||
<version>7.2.0</version>
|
<version>7.4.11</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.apis</groupId>
|
<groupId>com.google.apis</groupId>
|
||||||
<artifactId>google-api-services-youtube</artifactId>
|
<artifactId>google-api-services-youtube</artifactId>
|
||||||
<version>v3-rev139-1.20.0</version>
|
<version>v3-rev222-1.25.0</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.google.guava</groupId>
|
|
||||||
<artifactId>guava-jdk5</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -449,14 +458,14 @@
|
|||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
<version>8.0.28</version>
|
<version>8.0.33</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>42.4.1</version>
|
<version>42.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.jtds</groupId>
|
<groupId>net.sourceforge.jtds</groupId>
|
||||||
@@ -482,7 +491,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mock-server</groupId>
|
<groupId>org.mock-server</groupId>
|
||||||
<artifactId>mockserver-junit-jupiter</artifactId>
|
<artifactId>mockserver-junit-jupiter</artifactId>
|
||||||
<version>5.13.2</version>
|
<version>5.15.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -498,7 +507,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.playwright</groupId>
|
<groupId>com.microsoft.playwright</groupId>
|
||||||
<artifactId>playwright</artifactId>
|
<artifactId>playwright</artifactId>
|
||||||
<version>1.24.1</version>
|
<version>1.32.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ import javax.websocket.server.ServerEndpointConfig;
|
|||||||
import org.hibernate.cfg.AvailableSettings;
|
import org.hibernate.cfg.AvailableSettings;
|
||||||
|
|
||||||
import com.codahale.metrics.json.MetricsModule;
|
import com.codahale.metrics.json.MetricsModule;
|
||||||
import com.commafeed.backend.feed.FeedRefreshTaskGiver;
|
import com.commafeed.backend.feed.FeedRefreshEngine;
|
||||||
import com.commafeed.backend.feed.FeedRefreshUpdater;
|
|
||||||
import com.commafeed.backend.feed.FeedRefreshWorker;
|
|
||||||
import com.commafeed.backend.model.AbstractModel;
|
import com.commafeed.backend.model.AbstractModel;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
@@ -32,7 +30,7 @@ import com.commafeed.backend.model.FeedSubscription;
|
|||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserRole;
|
import com.commafeed.backend.model.UserRole;
|
||||||
import com.commafeed.backend.model.UserSettings;
|
import com.commafeed.backend.model.UserSettings;
|
||||||
import com.commafeed.backend.service.StartupService;
|
import com.commafeed.backend.service.DatabaseStartupService;
|
||||||
import com.commafeed.backend.service.UserService;
|
import com.commafeed.backend.service.UserService;
|
||||||
import com.commafeed.backend.task.ScheduledTask;
|
import com.commafeed.backend.task.ScheduledTask;
|
||||||
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
|
import com.commafeed.frontend.auth.SecurityCheckFactoryProvider;
|
||||||
@@ -50,6 +48,8 @@ import com.commafeed.frontend.servlet.NextUnreadServlet;
|
|||||||
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
|
import com.commafeed.frontend.session.SessionHelperFactoryProvider;
|
||||||
import com.commafeed.frontend.ws.WebSocketConfigurator;
|
import com.commafeed.frontend.ws.WebSocketConfigurator;
|
||||||
import com.commafeed.frontend.ws.WebSocketEndpoint;
|
import com.commafeed.frontend.ws.WebSocketEndpoint;
|
||||||
|
import com.fasterxml.jackson.databind.MapperFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.inject.Guice;
|
import com.google.inject.Guice;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.google.inject.Key;
|
import com.google.inject.Key;
|
||||||
@@ -58,6 +58,7 @@ import com.google.inject.TypeLiteral;
|
|||||||
import be.tomcools.dropwizard.websocket.WebsocketBundle;
|
import be.tomcools.dropwizard.websocket.WebsocketBundle;
|
||||||
import io.dropwizard.Application;
|
import io.dropwizard.Application;
|
||||||
import io.dropwizard.assets.AssetsBundle;
|
import io.dropwizard.assets.AssetsBundle;
|
||||||
|
import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
|
||||||
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
|
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
|
||||||
import io.dropwizard.configuration.SubstitutingSourceProvider;
|
import io.dropwizard.configuration.SubstitutingSourceProvider;
|
||||||
import io.dropwizard.db.DataSourceFactory;
|
import io.dropwizard.db.DataSourceFactory;
|
||||||
@@ -69,6 +70,7 @@ import io.dropwizard.setup.Bootstrap;
|
|||||||
import io.dropwizard.setup.Environment;
|
import io.dropwizard.setup.Environment;
|
||||||
import io.dropwizard.web.WebBundle;
|
import io.dropwizard.web.WebBundle;
|
||||||
import io.dropwizard.web.conf.WebConfiguration;
|
import io.dropwizard.web.conf.WebConfiguration;
|
||||||
|
import io.whitfin.dropwizard.configuration.EnvironmentSubstitutor;
|
||||||
|
|
||||||
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||||
|
|
||||||
@@ -87,6 +89,24 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
public void initialize(Bootstrap<CommaFeedConfiguration> bootstrap) {
|
||||||
|
bootstrap.setConfigurationFactoryFactory(new DefaultConfigurationFactoryFactory<CommaFeedConfiguration>() {
|
||||||
|
@Override
|
||||||
|
protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
|
||||||
|
// disable case sensitivity because EnvironmentSubstitutor maps MYPROPERTY to myproperty and not to myProperty
|
||||||
|
return objectMapper
|
||||||
|
.setConfig(objectMapper.getDeserializationConfig().with(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// enable config.yml string substitution
|
||||||
|
// e.g. having a custom config.yml file with app.session.path=${SOME_ENV_VAR} will substitute SOME_ENV_VAR
|
||||||
|
SubstitutingSourceProvider substitutingSourceProvider = new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
|
||||||
|
new EnvironmentVariableSubstitutor(false));
|
||||||
|
// enable config.yml properties override with env variables prefixed with CF_
|
||||||
|
// e.g. setting CF_APP_ALLOWREGISTRATIONS=true will set app.allowRegistrations to true
|
||||||
|
EnvironmentSubstitutor environmentSubstitutor = new EnvironmentSubstitutor("CF", substitutingSourceProvider);
|
||||||
|
bootstrap.setConfigurationSourceProvider(environmentSubstitutor);
|
||||||
|
|
||||||
bootstrap.getObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
|
bootstrap.getObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false));
|
||||||
|
|
||||||
bootstrap.addBundle(websocketBundle = new WebsocketBundle<>());
|
bootstrap.addBundle(websocketBundle = new WebsocketBundle<>());
|
||||||
@@ -124,10 +144,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
|
|
||||||
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
|
bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
|
||||||
bootstrap.addBundle(new MultiPartBundle());
|
bootstrap.addBundle(new MultiPartBundle());
|
||||||
|
|
||||||
// Enable variable substitution with environment variables
|
|
||||||
bootstrap.setConfigurationSourceProvider(
|
|
||||||
new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), new EnvironmentVariableSubstitutor(false)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -177,12 +193,10 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// database init/changelogs
|
// database init/changelogs
|
||||||
environment.lifecycle().manage(injector.getInstance(StartupService.class));
|
environment.lifecycle().manage(injector.getInstance(DatabaseStartupService.class));
|
||||||
|
|
||||||
// background feed fetching
|
// start feed fetching engine
|
||||||
environment.lifecycle().manage(injector.getInstance(FeedRefreshTaskGiver.class));
|
environment.lifecycle().manage(injector.getInstance(FeedRefreshEngine.class));
|
||||||
environment.lifecycle().manage(injector.getInstance(FeedRefreshWorker.class));
|
|
||||||
environment.lifecycle().manage(injector.getInstance(FeedRefreshUpdater.class));
|
|
||||||
|
|
||||||
// prevent caching index.html, so that the webapp is always up to date
|
// prevent caching index.html, so that the webapp is always up to date
|
||||||
environment.servlets()
|
environment.servlets()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import org.apache.http.HttpHeaders;
|
|||||||
import org.apache.http.HttpHost;
|
import org.apache.http.HttpHost;
|
||||||
import org.apache.http.HttpResponseInterceptor;
|
import org.apache.http.HttpResponseInterceptor;
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
import org.apache.http.client.ClientProtocolException;
|
|
||||||
import org.apache.http.client.HttpResponseException;
|
import org.apache.http.client.HttpResponseException;
|
||||||
import org.apache.http.client.config.CookieSpecs;
|
import org.apache.http.client.config.CookieSpecs;
|
||||||
import org.apache.http.client.config.RequestConfig;
|
import org.apache.http.client.config.RequestConfig;
|
||||||
@@ -59,7 +58,7 @@ public class HttpGetter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResult getBinary(String url, int timeout) throws ClientProtocolException, IOException, NotModifiedException {
|
public HttpResult getBinary(String url, int timeout) throws IOException, NotModifiedException {
|
||||||
return getBinary(url, null, null, timeout);
|
return getBinary(url, null, null, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,14 +70,10 @@ public class HttpGetter {
|
|||||||
* header we got last time we queried that url, or null
|
* header we got last time we queried that url, or null
|
||||||
* @param eTag
|
* @param eTag
|
||||||
* header we got last time we queried that url, or null
|
* header we got last time we queried that url, or null
|
||||||
* @return
|
|
||||||
* @throws ClientProtocolException
|
|
||||||
* @throws IOException
|
|
||||||
* @throws NotModifiedException
|
* @throws NotModifiedException
|
||||||
* if the url hasn't changed since we asked for it last time
|
* if the url hasn't changed since we asked for it last time
|
||||||
*/
|
*/
|
||||||
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout)
|
public HttpResult getBinary(String url, String lastModified, String eTag, int timeout) throws IOException, NotModifiedException {
|
||||||
throws ClientProtocolException, IOException, NotModifiedException {
|
|
||||||
HttpResult result = null;
|
HttpResult result = null;
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
@@ -175,13 +170,6 @@ public class HttpGetter {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
CommaFeedConfiguration config = new CommaFeedConfiguration();
|
|
||||||
HttpGetter getter = new HttpGetter(config);
|
|
||||||
HttpResult result = getter.getBinary("https://sourceforge.net/projects/mpv-player-windows/rss", 30000);
|
|
||||||
System.out.println(new String(result.content));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public static class NotModifiedException extends Exception {
|
public static class NotModifiedException extends Exception {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
@@ -189,12 +177,12 @@ public class HttpGetter {
|
|||||||
/**
|
/**
|
||||||
* if the value of this header changed, this is its new value
|
* if the value of this header changed, this is its new value
|
||||||
*/
|
*/
|
||||||
private String newLastModifiedHeader;
|
private final String newLastModifiedHeader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if the value of this header changed, this is its new value
|
* if the value of this header changed, this is its new value
|
||||||
*/
|
*/
|
||||||
private String newEtagHeader;
|
private final String newEtagHeader;
|
||||||
|
|
||||||
public NotModifiedException(String message) {
|
public NotModifiedException(String message) {
|
||||||
this(message, null, null);
|
this(message, null, null);
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
package com.commafeed.backend.cache;
|
package com.commafeed.backend.cache;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import redis.clients.jedis.DefaultJedisClientConfig;
|
||||||
|
import redis.clients.jedis.HostAndPort;
|
||||||
|
import redis.clients.jedis.JedisClientConfig;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
import redis.clients.jedis.JedisPoolConfig;
|
import redis.clients.jedis.JedisPoolConfig;
|
||||||
import redis.clients.jedis.Protocol;
|
import redis.clients.jedis.Protocol;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
public class RedisPoolFactory {
|
public class RedisPoolFactory {
|
||||||
private final String host = "localhost";
|
|
||||||
private final int port = Protocol.DEFAULT_PORT;
|
|
||||||
private String password;
|
|
||||||
private final int timeout = Protocol.DEFAULT_TIMEOUT;
|
|
||||||
private final int database = Protocol.DEFAULT_DATABASE;
|
|
||||||
|
|
||||||
private final int maxTotal = 500;
|
@JsonProperty
|
||||||
|
private String host = "localhost";
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int port = Protocol.DEFAULT_PORT;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int timeout = Protocol.DEFAULT_TIMEOUT;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int database = Protocol.DEFAULT_DATABASE;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int maxTotal = 500;
|
||||||
|
|
||||||
public JedisPool build() {
|
public JedisPool build() {
|
||||||
JedisPoolConfig config = new JedisPoolConfig();
|
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||||
config.setMaxTotal(maxTotal);
|
poolConfig.setMaxTotal(maxTotal);
|
||||||
|
|
||||||
return new JedisPool(config, host, port, timeout, StringUtils.trimToNull(password), database);
|
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
|
||||||
|
.user(username)
|
||||||
|
.password(password)
|
||||||
|
.timeoutMillis(timeout)
|
||||||
|
.database(database)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new JedisPool(poolConfig, new HostAndPort(host, port), clientConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
QFeedSubscription subs = QFeedSubscription.feedSubscription;
|
QFeedSubscription subs = QFeedSubscription.feedSubscription;
|
||||||
QUser user = QUser.user;
|
QUser user = QUser.user;
|
||||||
|
|
||||||
query.join(feed.subscriptions, subs).join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
|
query.join(subs).on(subs.feed.id.eq(feed.id)).join(subs.user, user).where(user.lastLogin.gt(lastLoginThreshold));
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
|
return query.orderBy(feed.disabledUntil.asc()).limit(count).fetch();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.commafeed.backend.feed;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -13,13 +14,19 @@ import org.apache.commons.codec.digest.DigestUtils;
|
|||||||
import com.commafeed.backend.HttpGetter;
|
import com.commafeed.backend.HttpGetter;
|
||||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||||
|
import com.commafeed.backend.feed.FeedParser.FeedParserResult;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
import com.commafeed.backend.urlprovider.FeedURLProvider;
|
||||||
import com.rometools.rome.io.FeedException;
|
import com.rometools.rome.io.FeedException;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Value;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a feed then parses it
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -29,18 +36,18 @@ public class FeedFetcher {
|
|||||||
private final HttpGetter getter;
|
private final HttpGetter getter;
|
||||||
private final Set<FeedURLProvider> urlProviders;
|
private final Set<FeedURLProvider> urlProviders;
|
||||||
|
|
||||||
public FetchedFeed fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
|
public FeedFetcherResult fetch(String feedUrl, boolean extractFeedUrlFromHtml, String lastModified, String eTag, Date lastPublishedDate,
|
||||||
String lastContentHash) throws FeedException, IOException, NotModifiedException {
|
String lastContentHash) throws FeedException, IOException, NotModifiedException {
|
||||||
log.debug("Fetching feed {}", feedUrl);
|
log.debug("Fetching feed {}", feedUrl);
|
||||||
FetchedFeed fetchedFeed = null;
|
|
||||||
|
|
||||||
int timeout = 20000;
|
int timeout = 20000;
|
||||||
|
|
||||||
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout);
|
HttpResult result = getter.getBinary(feedUrl, lastModified, eTag, timeout);
|
||||||
byte[] content = result.getContent();
|
byte[] content = result.getContent();
|
||||||
|
|
||||||
|
FeedParserResult parserResult;
|
||||||
try {
|
try {
|
||||||
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
|
parserResult = parser.parse(result.getUrlAfterRedirect(), content);
|
||||||
} catch (FeedException e) {
|
} catch (FeedException e) {
|
||||||
if (extractFeedUrlFromHtml) {
|
if (extractFeedUrlFromHtml) {
|
||||||
String extractedUrl = extractFeedUrl(urlProviders, feedUrl, StringUtils.newStringUtf8(result.getContent()));
|
String extractedUrl = extractFeedUrl(urlProviders, feedUrl, StringUtils.newStringUtf8(result.getContent()));
|
||||||
@@ -49,7 +56,7 @@ public class FeedFetcher {
|
|||||||
|
|
||||||
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
|
result = getter.getBinary(extractedUrl, lastModified, eTag, timeout);
|
||||||
content = result.getContent();
|
content = result.getContent();
|
||||||
fetchedFeed = parser.parse(result.getUrlAfterRedirect(), content);
|
parserResult = parser.parse(result.getUrlAfterRedirect(), content);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -73,21 +80,20 @@ public class FeedFetcher {
|
|||||||
etagHeaderValueChanged ? result.getETag() : null);
|
etagHeaderValueChanged ? result.getETag() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastPublishedDate != null && fetchedFeed.getFeed().getLastPublishedDate() != null
|
if (lastPublishedDate != null && parserResult.getFeed().getLastPublishedDate() != null
|
||||||
&& lastPublishedDate.getTime() == fetchedFeed.getFeed().getLastPublishedDate().getTime()) {
|
&& lastPublishedDate.getTime() == parserResult.getFeed().getLastPublishedDate().getTime()) {
|
||||||
log.debug("publishedDate not modified: {}", feedUrl);
|
log.debug("publishedDate not modified: {}", feedUrl);
|
||||||
throw new NotModifiedException("publishedDate not modified",
|
throw new NotModifiedException("publishedDate not modified",
|
||||||
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,
|
lastModifiedHeaderValueChanged ? result.getLastModifiedSince() : null,
|
||||||
etagHeaderValueChanged ? result.getETag() : null);
|
etagHeaderValueChanged ? result.getETag() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Feed feed = fetchedFeed.getFeed();
|
Feed feed = parserResult.getFeed();
|
||||||
feed.setLastModifiedHeader(result.getLastModifiedSince());
|
feed.setLastModifiedHeader(result.getLastModifiedSince());
|
||||||
feed.setEtagHeader(FeedUtils.truncate(result.getETag(), 255));
|
feed.setEtagHeader(FeedUtils.truncate(result.getETag(), 255));
|
||||||
feed.setLastContentHash(hash);
|
feed.setLastContentHash(hash);
|
||||||
fetchedFeed.setFetchDuration(result.getDuration());
|
return new FeedFetcherResult(parserResult.getFeed(), parserResult.getEntries(), parserResult.getTitle(),
|
||||||
fetchedFeed.setUrlAfterRedirect(result.getUrlAfterRedirect());
|
result.getUrlAfterRedirect(), result.getDuration());
|
||||||
return fetchedFeed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String extractFeedUrl(Set<FeedURLProvider> urlProviders, String url, String urlContent) {
|
private static String extractFeedUrl(Set<FeedURLProvider> urlProviders, String url, String urlContent) {
|
||||||
@@ -100,4 +106,14 @@ public class FeedFetcher {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class FeedFetcherResult {
|
||||||
|
Feed feed;
|
||||||
|
List<FeedEntry> entries;
|
||||||
|
String title;
|
||||||
|
String urlAfterRedirect;
|
||||||
|
long fetchDuration;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.commafeed.backend.feed;
|
|||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -37,8 +38,12 @@ import com.rometools.rome.io.SyndFeedInput;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Value;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses raw xml as a Feed object
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -50,10 +55,7 @@ public class FeedParser {
|
|||||||
private static final Date START = new Date(86400000);
|
private static final Date START = new Date(86400000);
|
||||||
private static final Date END = new Date(1000L * Integer.MAX_VALUE - 86400000);
|
private static final Date END = new Date(1000L * Integer.MAX_VALUE - 86400000);
|
||||||
|
|
||||||
public FetchedFeed parse(String feedUrl, byte[] xml) throws FeedException {
|
public FeedParserResult parse(String feedUrl, byte[] xml) throws FeedException {
|
||||||
FetchedFeed fetchedFeed = new FetchedFeed();
|
|
||||||
Feed feed = fetchedFeed.getFeed();
|
|
||||||
List<FeedEntry> entries = fetchedFeed.getEntries();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Charset encoding = FeedUtils.guessEncoding(xml);
|
Charset encoding = FeedUtils.guessEncoding(xml);
|
||||||
@@ -63,17 +65,19 @@ public class FeedParser {
|
|||||||
}
|
}
|
||||||
xmlString = FeedUtils.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
xmlString = FeedUtils.replaceHtmlEntitiesWithNumericEntities(xmlString);
|
||||||
InputSource source = new InputSource(new StringReader(xmlString));
|
InputSource source = new InputSource(new StringReader(xmlString));
|
||||||
|
|
||||||
SyndFeed rss = new SyndFeedInput().build(source);
|
SyndFeed rss = new SyndFeedInput().build(source);
|
||||||
handleForeignMarkup(rss);
|
handleForeignMarkup(rss);
|
||||||
|
|
||||||
fetchedFeed.setTitle(rss.getTitle());
|
String title = rss.getTitle();
|
||||||
|
Feed feed = new Feed();
|
||||||
feed.setPushHub(findHub(rss));
|
feed.setPushHub(findHub(rss));
|
||||||
feed.setPushTopic(findSelf(rss));
|
feed.setPushTopic(findSelf(rss));
|
||||||
feed.setUrl(feedUrl);
|
feed.setUrl(feedUrl);
|
||||||
feed.setLink(rss.getLink());
|
feed.setLink(rss.getLink());
|
||||||
List<SyndEntry> items = rss.getEntries();
|
|
||||||
|
|
||||||
for (SyndEntry item : items) {
|
List<FeedEntry> entries = new ArrayList<>();
|
||||||
|
for (SyndEntry item : rss.getEntries()) {
|
||||||
FeedEntry entry = new FeedEntry();
|
FeedEntry entry = new FeedEntry();
|
||||||
|
|
||||||
String guid = item.getUri();
|
String guid = item.getUri();
|
||||||
@@ -121,6 +125,7 @@ public class FeedParser {
|
|||||||
|
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
Date lastEntryDate = null;
|
Date lastEntryDate = null;
|
||||||
Date publishedDate = validateDate(rss.getPublishedDate(), false);
|
Date publishedDate = validateDate(rss.getPublishedDate(), false);
|
||||||
if (!entries.isEmpty()) {
|
if (!entries.isEmpty()) {
|
||||||
@@ -133,10 +138,10 @@ public class FeedParser {
|
|||||||
feed.setAverageEntryInterval(FeedUtils.averageTimeBetweenEntries(entries));
|
feed.setAverageEntryInterval(FeedUtils.averageTimeBetweenEntries(entries));
|
||||||
feed.setLastEntryDate(lastEntryDate);
|
feed.setLastEntryDate(lastEntryDate);
|
||||||
|
|
||||||
|
return new FeedParserResult(feed, entries, title);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new FeedException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
|
throw new FeedException(String.format("Could not parse feed from %s : %s", feedUrl, e.getMessage()), e);
|
||||||
}
|
}
|
||||||
return fetchedFeed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,4 +278,11 @@ public class FeedParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public static class FeedParserResult {
|
||||||
|
Feed feed;
|
||||||
|
List<FeedEntry> entries;
|
||||||
|
String title;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package com.commafeed.backend.feed;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.BlockingDeque;
|
|
||||||
import java.util.concurrent.LinkedBlockingDeque;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
|
||||||
import org.hibernate.SessionFactory;
|
|
||||||
|
|
||||||
import com.codahale.metrics.Gauge;
|
|
||||||
import com.codahale.metrics.Meter;
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
|
||||||
import com.commafeed.backend.dao.UnitOfWork;
|
|
||||||
import com.commafeed.backend.model.Feed;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class FeedQueues {
|
|
||||||
|
|
||||||
private final SessionFactory sessionFactory;
|
|
||||||
private final FeedDAO feedDAO;
|
|
||||||
private final CommaFeedConfiguration config;
|
|
||||||
private final BlockingDeque<FeedRefreshContext> queue = new LinkedBlockingDeque<>();
|
|
||||||
private final Meter refill;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public FeedQueues(SessionFactory sessionFactory, FeedDAO feedDAO, CommaFeedConfiguration config, MetricRegistry metrics) {
|
|
||||||
this.sessionFactory = sessionFactory;
|
|
||||||
this.config = config;
|
|
||||||
this.feedDAO = feedDAO;
|
|
||||||
this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
|
|
||||||
|
|
||||||
metrics.register(MetricRegistry.name(getClass(), "queue"), (Gauge<Integer>) queue::size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* take a feed from the refresh queue
|
|
||||||
*/
|
|
||||||
public synchronized FeedRefreshContext take() {
|
|
||||||
FeedRefreshContext context = queue.poll();
|
|
||||||
if (context != null) {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
refill();
|
|
||||||
try {
|
|
||||||
// try to get something from the queue
|
|
||||||
// if the queue is empty, wait a bit
|
|
||||||
// polling the queue instead of sleeping gives us the opportunity to process a feed immediately if it was added manually with
|
|
||||||
// add()
|
|
||||||
return queue.poll(15, TimeUnit.SECONDS);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException("interrupted while waiting for a feed in the queue", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a feed to the refresh queue
|
|
||||||
*/
|
|
||||||
public void add(Feed feed, boolean urgent) {
|
|
||||||
if (isFeedAlreadyQueued(feed)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeedRefreshContext context = new FeedRefreshContext(feed, urgent);
|
|
||||||
if (urgent) {
|
|
||||||
queue.addFirst(context);
|
|
||||||
} else {
|
|
||||||
queue.addLast(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* refills the refresh queue
|
|
||||||
*/
|
|
||||||
private void refill() {
|
|
||||||
refill.mark();
|
|
||||||
|
|
||||||
// add feeds that are up to refresh from the database
|
|
||||||
int batchSize = Math.min(100, 3 * config.getApplicationSettings().getBackgroundThreads());
|
|
||||||
List<Feed> feeds = UnitOfWork.call(sessionFactory, () -> {
|
|
||||||
List<Feed> list = feedDAO.findNextUpdatable(batchSize, getLastLoginThreshold());
|
|
||||||
|
|
||||||
// set the disabledDate as we use it in feedDAO.findNextUpdatable() to decide what to refresh next
|
|
||||||
Date nextRefreshDate = DateUtils.addMinutes(new Date(), config.getApplicationSettings().getRefreshIntervalMinutes());
|
|
||||||
list.forEach(f -> f.setDisabledUntil(nextRefreshDate));
|
|
||||||
feedDAO.saveOrUpdate(list);
|
|
||||||
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
|
|
||||||
feeds.forEach(f -> add(f, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void giveBack(Feed feed) {
|
|
||||||
String normalized = FeedUtils.normalizeURL(feed.getUrl());
|
|
||||||
feed.setNormalizedUrl(normalized);
|
|
||||||
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
|
|
||||||
feed.setLastUpdated(new Date());
|
|
||||||
UnitOfWork.run(sessionFactory, () -> feedDAO.saveOrUpdate(feed));
|
|
||||||
|
|
||||||
// we just finished updating the feed, remove it from the queue
|
|
||||||
queue.removeIf(c -> isFeedAlreadyQueued(c.getFeed()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Date getLastLoginThreshold() {
|
|
||||||
if (config.getApplicationSettings().getHeavyLoad()) {
|
|
||||||
return DateUtils.addDays(new Date(), -30);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFeedAlreadyQueued(Feed feed) {
|
|
||||||
return queue.stream().anyMatch(c -> c.getFeed().getId().equals(feed.getId()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.commafeed.backend.feed;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class FeedRefreshContext {
|
|
||||||
private Feed feed;
|
|
||||||
private List<FeedEntry> entries;
|
|
||||||
private boolean urgent;
|
|
||||||
|
|
||||||
public FeedRefreshContext(Feed feed, boolean isUrgent) {
|
|
||||||
this.feed = feed;
|
|
||||||
this.urgent = isUrgent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
|
import com.commafeed.backend.dao.UnitOfWork;
|
||||||
|
import com.commafeed.backend.model.Feed;
|
||||||
|
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.processors.PublishProcessor;
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class FeedRefreshEngine implements Managed {
|
||||||
|
|
||||||
|
private final SessionFactory sessionFactory;
|
||||||
|
private final FeedDAO feedDAO;
|
||||||
|
private final FeedRefreshWorker worker;
|
||||||
|
private final FeedRefreshUpdater updater;
|
||||||
|
private final CommaFeedConfiguration config;
|
||||||
|
private final Meter refill;
|
||||||
|
|
||||||
|
private final PublishProcessor<Feed> priorityQueue;
|
||||||
|
private Disposable flow;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public FeedRefreshEngine(SessionFactory sessionFactory, FeedDAO feedDAO, FeedRefreshWorker worker, FeedRefreshUpdater updater,
|
||||||
|
CommaFeedConfiguration config, MetricRegistry metrics) {
|
||||||
|
this.sessionFactory = sessionFactory;
|
||||||
|
this.feedDAO = feedDAO;
|
||||||
|
this.worker = worker;
|
||||||
|
this.updater = updater;
|
||||||
|
this.config = config;
|
||||||
|
this.refill = metrics.meter(MetricRegistry.name(getClass(), "refill"));
|
||||||
|
this.priorityQueue = PublishProcessor.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
Flowable<Feed> database = Flowable.fromCallable(() -> findNextUpdatableFeeds(getBatchSize(), getLastLoginThreshold()))
|
||||||
|
.onErrorResumeNext(e -> {
|
||||||
|
log.error("error while fetching next updatable feeds", e);
|
||||||
|
return Flowable.empty();
|
||||||
|
})
|
||||||
|
// repeat query 15s after the flowable has been emptied
|
||||||
|
// https://github.com/ReactiveX/RxJava/issues/448#issuecomment-233244964
|
||||||
|
.repeatWhen(o -> o.concatMap(v -> Flowable.timer(15, TimeUnit.SECONDS)))
|
||||||
|
.flatMap(Flowable::fromIterable);
|
||||||
|
Flowable<Feed> source = Flowable.merge(priorityQueue, database);
|
||||||
|
|
||||||
|
this.flow = source.subscribeOn(Schedulers.io())
|
||||||
|
// feed fetching
|
||||||
|
.parallel(config.getApplicationSettings().getBackgroundThreads())
|
||||||
|
.runOn(Schedulers.io())
|
||||||
|
.flatMap(f -> Flowable.fromCallable(() -> worker.update(f)).onErrorResumeNext(e -> {
|
||||||
|
log.error("error while fetching feed", e);
|
||||||
|
return Flowable.empty();
|
||||||
|
}))
|
||||||
|
.sequential()
|
||||||
|
// database updating
|
||||||
|
.parallel(config.getApplicationSettings().getDatabaseUpdateThreads())
|
||||||
|
.runOn(Schedulers.io())
|
||||||
|
.flatMap(fae -> Flowable.fromCallable(() -> updater.update(fae.getFeed(), fae.getEntries())).onErrorResumeNext(e -> {
|
||||||
|
log.error("error while updating database", e);
|
||||||
|
return Flowable.empty();
|
||||||
|
}))
|
||||||
|
.sequential()
|
||||||
|
// end flow
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshImmediately(Feed feed) {
|
||||||
|
priorityQueue.onNext(feed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Feed> findNextUpdatableFeeds(int max, Date lastLoginThreshold) {
|
||||||
|
refill.mark();
|
||||||
|
return UnitOfWork.call(sessionFactory, () -> feedDAO.findNextUpdatable(max, lastLoginThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getBatchSize() {
|
||||||
|
return Math.min(Flowable.bufferSize(), 3 * config.getApplicationSettings().getBackgroundThreads());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date getLastLoginThreshold() {
|
||||||
|
return Boolean.TRUE.equals(config.getApplicationSettings().getHeavyLoad()) ? DateUtils.addDays(new Date(), -30) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
flow.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package com.commafeed.backend.feed;
|
|
||||||
|
|
||||||
import java.util.concurrent.LinkedBlockingDeque;
|
|
||||||
import java.util.concurrent.RejectedExecutionHandler;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import com.codahale.metrics.Gauge;
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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}
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class FeedRefreshExecutor {
|
|
||||||
|
|
||||||
private String poolName;
|
|
||||||
private ThreadPoolExecutor pool;
|
|
||||||
private LinkedBlockingDeque<Runnable> queue;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean offer(Runnable r) {
|
|
||||||
Task task = (Task) r;
|
|
||||||
if (task.isUrgent()) {
|
|
||||||
return offerFirst(r);
|
|
||||||
} else {
|
|
||||||
return offerLast(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
@Override
|
|
||||||
protected void afterExecute(Runnable r, Throwable t) {
|
|
||||||
if (t != null) {
|
|
||||||
log.error("thread from pool {} threw a runtime exception", poolName, t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
|
||||||
@Override
|
|
||||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
|
|
||||||
log.debug("{} thread queue full, waiting...", poolName);
|
|
||||||
try {
|
|
||||||
Task task = (Task) r;
|
|
||||||
if (task.isUrgent()) {
|
|
||||||
queue.putFirst(r);
|
|
||||||
} else {
|
|
||||||
queue.put(r);
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e1) {
|
|
||||||
log.error(poolName + " interrupted while waiting for queue.", e1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 void shutdown() {
|
|
||||||
pool.shutdownNow();
|
|
||||||
while (!pool.isTerminated()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.error("{} interrupted while waiting for threads to finish.", poolName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Task extends Runnable {
|
|
||||||
boolean isUrgent();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,9 +22,9 @@ public class FeedRefreshIntervalCalculator {
|
|||||||
this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes();
|
this.refreshIntervalMinutes = config.getApplicationSettings().getRefreshIntervalMinutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date onFetchSuccess(FetchedFeed fetchedFeed) {
|
public Date onFetchSuccess(Feed feed) {
|
||||||
Date defaultRefreshInterval = getDefaultRefreshInterval();
|
Date defaultRefreshInterval = getDefaultRefreshInterval();
|
||||||
return heavyLoad ? computeRefreshIntervalForHeavyLoad(fetchedFeed.getFeed(), defaultRefreshInterval) : defaultRefreshInterval;
|
return heavyLoad ? computeRefreshIntervalForHeavyLoad(feed, defaultRefreshInterval) : defaultRefreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date onFeedNotModified(Feed feed) {
|
public Date onFeedNotModified(Feed feed) {
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
package com.commafeed.backend.feed;
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import com.codahale.metrics.Meter;
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
|
||||||
|
|
||||||
import io.dropwizard.lifecycle.Managed;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infinite loop fetching feeds from @FeedQueues and queuing them to the {@link FeedRefreshWorker} pool.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Singleton
|
|
||||||
public class FeedRefreshTaskGiver implements Managed {
|
|
||||||
|
|
||||||
private final FeedQueues queues;
|
|
||||||
private final FeedRefreshWorker worker;
|
|
||||||
|
|
||||||
private final ExecutorService executor;
|
|
||||||
|
|
||||||
private final Meter feedRefreshed;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public FeedRefreshTaskGiver(FeedQueues queues, FeedDAO feedDAO, FeedRefreshWorker worker, CommaFeedConfiguration config,
|
|
||||||
MetricRegistry metrics) {
|
|
||||||
this.queues = queues;
|
|
||||||
this.worker = worker;
|
|
||||||
|
|
||||||
executor = Executors.newFixedThreadPool(1);
|
|
||||||
feedRefreshed = metrics.meter(MetricRegistry.name(getClass(), "feedRefreshed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
log.info("shutting down feed refresh task giver");
|
|
||||||
executor.shutdownNow();
|
|
||||||
while (!executor.isTerminated()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.error("interrupted while waiting for threads to finish.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
log.info("starting feed refresh task giver");
|
|
||||||
executor.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
while (!executor.isShutdown()) {
|
|
||||||
try {
|
|
||||||
FeedRefreshContext context = queues.take();
|
|
||||||
if (context != null) {
|
|
||||||
feedRefreshed.mark();
|
|
||||||
worker.updateFeed(context);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,17 +20,16 @@ import org.hibernate.SessionFactory;
|
|||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
|
||||||
import com.commafeed.backend.cache.CacheService;
|
import com.commafeed.backend.cache.CacheService;
|
||||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
import com.commafeed.backend.dao.UnitOfWork;
|
import com.commafeed.backend.dao.UnitOfWork;
|
||||||
import com.commafeed.backend.feed.FeedRefreshExecutor.Task;
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
import com.commafeed.backend.model.FeedEntryContent;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.service.FeedUpdateService;
|
import com.commafeed.backend.service.FeedEntryService;
|
||||||
|
import com.commafeed.backend.service.FeedService;
|
||||||
import com.commafeed.backend.service.PubSubService;
|
import com.commafeed.backend.service.PubSubService;
|
||||||
import com.commafeed.frontend.ws.WebSocketMessageBuilder;
|
import com.commafeed.frontend.ws.WebSocketMessageBuilder;
|
||||||
import com.commafeed.frontend.ws.WebSocketSessions;
|
import com.commafeed.frontend.ws.WebSocketSessions;
|
||||||
@@ -40,20 +39,22 @@ import io.dropwizard.lifecycle.Managed;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the feed in the database and inserts new entries
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedRefreshUpdater implements Managed {
|
public class FeedRefreshUpdater implements Managed {
|
||||||
|
|
||||||
private final SessionFactory sessionFactory;
|
private final SessionFactory sessionFactory;
|
||||||
private final FeedUpdateService feedUpdateService;
|
private final FeedService feedService;
|
||||||
|
private final FeedEntryService feedEntryService;
|
||||||
private final PubSubService pubSubService;
|
private final PubSubService pubSubService;
|
||||||
private final FeedQueues queues;
|
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
private final CacheService cache;
|
private final CacheService cache;
|
||||||
private final WebSocketSessions webSocketSessions;
|
private final WebSocketSessions webSocketSessions;
|
||||||
|
|
||||||
private final FeedRefreshExecutor pool;
|
|
||||||
private final Striped<Lock> locks;
|
private final Striped<Lock> locks;
|
||||||
|
|
||||||
private final Meter entryCacheMiss;
|
private final Meter entryCacheMiss;
|
||||||
@@ -62,22 +63,19 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
private final Meter entryInserted;
|
private final Meter entryInserted;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedRefreshUpdater(SessionFactory sessionFactory, FeedUpdateService feedUpdateService, PubSubService pubSubService,
|
public FeedRefreshUpdater(SessionFactory sessionFactory, FeedService feedService, FeedEntryService feedEntryService,
|
||||||
FeedQueues queues, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO,
|
PubSubService pubSubService, CommaFeedConfiguration config, MetricRegistry metrics, FeedSubscriptionDAO feedSubscriptionDAO,
|
||||||
CacheService cache, WebSocketSessions webSocketSessions) {
|
CacheService cache, WebSocketSessions webSocketSessions) {
|
||||||
this.sessionFactory = sessionFactory;
|
this.sessionFactory = sessionFactory;
|
||||||
this.feedUpdateService = feedUpdateService;
|
this.feedService = feedService;
|
||||||
|
this.feedEntryService = feedEntryService;
|
||||||
this.pubSubService = pubSubService;
|
this.pubSubService = pubSubService;
|
||||||
this.queues = queues;
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.feedSubscriptionDAO = feedSubscriptionDAO;
|
this.feedSubscriptionDAO = feedSubscriptionDAO;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.webSocketSessions = webSocketSessions;
|
this.webSocketSessions = webSocketSessions;
|
||||||
|
|
||||||
ApplicationSettings settings = config.getApplicationSettings();
|
locks = Striped.lazyWeakLock(100000);
|
||||||
int threads = Math.max(settings.getDatabaseUpdateThreads(), 1);
|
|
||||||
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"));
|
entryCacheMiss = metrics.meter(MetricRegistry.name(getClass(), "entryCacheMiss"));
|
||||||
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
|
entryCacheHit = metrics.meter(MetricRegistry.name(getClass(), "entryCacheHit"));
|
||||||
@@ -85,20 +83,6 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
entryInserted = metrics.meter(MetricRegistry.name(getClass(), "entryInserted"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws Exception {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() throws Exception {
|
|
||||||
log.info("shutting down feed refresh updater");
|
|
||||||
pool.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeed(FeedRefreshContext context) {
|
|
||||||
pool.execute(new EntryTask(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
private AddEntryResult addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
|
private AddEntryResult addEntry(final Feed feed, final FeedEntry entry, final List<FeedSubscription> subscriptions) {
|
||||||
boolean processed = false;
|
boolean processed = false;
|
||||||
boolean inserted = false;
|
boolean inserted = false;
|
||||||
@@ -123,7 +107,7 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
|
locked2 = lock2.tryLock(1, TimeUnit.MINUTES);
|
||||||
if (locked1 && locked2) {
|
if (locked1 && locked2) {
|
||||||
processed = true;
|
processed = true;
|
||||||
inserted = UnitOfWork.call(sessionFactory, () -> feedUpdateService.addEntry(feed, entry, subscriptions));
|
inserted = UnitOfWork.call(sessionFactory, () -> feedEntryService.addEntry(feed, entry, subscriptions));
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
entryInserted.mark();
|
entryInserted.mark();
|
||||||
}
|
}
|
||||||
@@ -166,76 +150,63 @@ public class FeedRefreshUpdater implements Managed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EntryTask implements Task {
|
public boolean update(Feed feed, List<FeedEntry> entries) {
|
||||||
|
boolean processed = true;
|
||||||
|
boolean insertedAtLeastOneEntry = false;
|
||||||
|
|
||||||
private final FeedRefreshContext context;
|
if (!entries.isEmpty()) {
|
||||||
|
List<String> lastEntries = cache.getLastEntries(feed);
|
||||||
|
List<String> currentEntries = new ArrayList<>();
|
||||||
|
|
||||||
public EntryTask(FeedRefreshContext context) {
|
List<FeedSubscription> subscriptions = null;
|
||||||
this.context = context;
|
for (FeedEntry entry : entries) {
|
||||||
}
|
String cacheKey = cache.buildUniqueEntryKey(feed, entry);
|
||||||
|
if (!lastEntries.contains(cacheKey)) {
|
||||||
@Override
|
log.debug("cache miss for {}", entry.getUrl());
|
||||||
public void run() {
|
if (subscriptions == null) {
|
||||||
boolean processed = true;
|
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
|
||||||
boolean insertedAtLeastOneEntry = false;
|
|
||||||
|
|
||||||
final Feed feed = context.getFeed();
|
|
||||||
List<FeedEntry> entries = context.getEntries();
|
|
||||||
if (entries.isEmpty()) {
|
|
||||||
feed.setMessage("Feed has no entries");
|
|
||||||
} else {
|
|
||||||
List<String> lastEntries = cache.getLastEntries(feed);
|
|
||||||
List<String> currentEntries = new ArrayList<>();
|
|
||||||
|
|
||||||
List<FeedSubscription> subscriptions = null;
|
|
||||||
for (FeedEntry entry : entries) {
|
|
||||||
String cacheKey = cache.buildUniqueEntryKey(feed, entry);
|
|
||||||
if (!lastEntries.contains(cacheKey)) {
|
|
||||||
log.debug("cache miss for {}", entry.getUrl());
|
|
||||||
if (subscriptions == null) {
|
|
||||||
subscriptions = UnitOfWork.call(sessionFactory, () -> feedSubscriptionDAO.findByFeed(feed));
|
|
||||||
}
|
|
||||||
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
|
||||||
processed &= addEntryResult.processed;
|
|
||||||
insertedAtLeastOneEntry |= addEntryResult.inserted;
|
|
||||||
|
|
||||||
entryCacheMiss.mark();
|
|
||||||
} else {
|
|
||||||
log.debug("cache hit for {}", entry.getUrl());
|
|
||||||
entryCacheHit.mark();
|
|
||||||
}
|
}
|
||||||
|
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
||||||
|
processed &= addEntryResult.processed;
|
||||||
|
insertedAtLeastOneEntry |= addEntryResult.inserted;
|
||||||
|
|
||||||
currentEntries.add(cacheKey);
|
entryCacheMiss.mark();
|
||||||
|
} else {
|
||||||
|
log.debug("cache hit for {}", entry.getUrl());
|
||||||
|
entryCacheHit.mark();
|
||||||
}
|
}
|
||||||
cache.setLastEntries(feed, currentEntries);
|
|
||||||
|
|
||||||
if (subscriptions == null) {
|
currentEntries.add(cacheKey);
|
||||||
feed.setMessage("No new entries found");
|
}
|
||||||
} else if (insertedAtLeastOneEntry) {
|
cache.setLastEntries(feed, currentEntries);
|
||||||
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).collect(Collectors.toList());
|
|
||||||
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
|
||||||
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
|
||||||
|
|
||||||
// notify over websocket
|
if (subscriptions == null) {
|
||||||
subscriptions.forEach(sub -> webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub)));
|
feed.setMessage("No new entries found");
|
||||||
}
|
} else if (insertedAtLeastOneEntry) {
|
||||||
}
|
List<User> users = subscriptions.stream().map(FeedSubscription::getUser).collect(Collectors.toList());
|
||||||
|
cache.invalidateUnreadCount(subscriptions.toArray(new FeedSubscription[0]));
|
||||||
|
cache.invalidateUserRootCategory(users.toArray(new User[0]));
|
||||||
|
|
||||||
if (config.getApplicationSettings().getPubsubhubbub()) {
|
// notify over websocket
|
||||||
handlePubSub(feed);
|
subscriptions.forEach(sub -> webSocketSessions.sendMessage(sub.getUser(), WebSocketMessageBuilder.newFeedEntries(sub)));
|
||||||
}
|
|
||||||
if (!processed) {
|
|
||||||
// requeue asap
|
|
||||||
feed.setDisabledUntil(new Date(0));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(config.getApplicationSettings().getPubsubhubbub())) {
|
||||||
|
handlePubSub(feed);
|
||||||
|
}
|
||||||
|
if (!processed) {
|
||||||
|
// requeue asap
|
||||||
|
feed.setDisabledUntil(new Date(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertedAtLeastOneEntry) {
|
||||||
feedUpdated.mark();
|
feedUpdated.mark();
|
||||||
queues.giveBack(feed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
UnitOfWork.run(sessionFactory, () -> feedService.save(feed));
|
||||||
public boolean isUrgent() {
|
|
||||||
return context.isUrgent();
|
return processed;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.feed;
|
package com.commafeed.backend.feed;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -10,91 +11,72 @@ import javax.inject.Singleton;
|
|||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
import com.commafeed.backend.HttpGetter.NotModifiedException;
|
||||||
import com.commafeed.backend.feed.FeedRefreshExecutor.Task;
|
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
|
|
||||||
import io.dropwizard.lifecycle.Managed;
|
import lombok.Value;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls {@link FeedFetcher} and handles its outcome
|
* Calls {@link FeedFetcher} and updates the Feed object, but does not update the database ({@link FeedRefreshUpdater} does that)
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedRefreshWorker implements Managed {
|
public class FeedRefreshWorker {
|
||||||
|
|
||||||
private final FeedRefreshUpdater feedRefreshUpdater;
|
|
||||||
private final FeedRefreshIntervalCalculator refreshIntervalCalculator;
|
private final FeedRefreshIntervalCalculator refreshIntervalCalculator;
|
||||||
private final FeedFetcher fetcher;
|
private final FeedFetcher fetcher;
|
||||||
private final FeedQueues queues;
|
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
private final FeedRefreshExecutor pool;
|
private final Meter feedFetched;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FeedRefreshWorker(FeedRefreshUpdater feedRefreshUpdater, FeedRefreshIntervalCalculator refreshIntervalCalculator,
|
public FeedRefreshWorker(FeedRefreshIntervalCalculator refreshIntervalCalculator, FeedFetcher fetcher, CommaFeedConfiguration config,
|
||||||
FeedFetcher fetcher, FeedQueues queues, CommaFeedConfiguration config, MetricRegistry metrics) {
|
MetricRegistry metrics) {
|
||||||
this.feedRefreshUpdater = feedRefreshUpdater;
|
|
||||||
this.refreshIntervalCalculator = refreshIntervalCalculator;
|
this.refreshIntervalCalculator = refreshIntervalCalculator;
|
||||||
this.fetcher = fetcher;
|
this.fetcher = fetcher;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.queues = queues;
|
this.feedFetched = metrics.meter(MetricRegistry.name(getClass(), "feedFetched"));
|
||||||
int threads = config.getApplicationSettings().getBackgroundThreads();
|
|
||||||
pool = new FeedRefreshExecutor("feed-refresh-worker", threads, Math.min(20 * threads, 1000), metrics);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public FeedRefreshWorkerResult update(Feed feed) {
|
||||||
public void start() throws Exception {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() throws Exception {
|
|
||||||
pool.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateFeed(FeedRefreshContext context) {
|
|
||||||
pool.execute(new FeedTask(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update(FeedRefreshContext context) {
|
|
||||||
Feed feed = context.getFeed();
|
|
||||||
try {
|
try {
|
||||||
String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
|
String url = Optional.ofNullable(feed.getUrlAfterRedirect()).orElse(feed.getUrl());
|
||||||
FetchedFeed fetchedFeed = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
FeedFetcherResult feedFetcherResult = fetcher.fetch(url, false, feed.getLastModifiedHeader(), feed.getEtagHeader(),
|
||||||
feed.getLastPublishedDate(), feed.getLastContentHash());
|
feed.getLastPublishedDate(), feed.getLastContentHash());
|
||||||
// stops here if NotModifiedException or any other exception is thrown
|
// stops here if NotModifiedException or any other exception is thrown
|
||||||
List<FeedEntry> entries = fetchedFeed.getEntries();
|
List<FeedEntry> entries = feedFetcherResult.getEntries();
|
||||||
|
|
||||||
Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
|
Integer maxFeedCapacity = config.getApplicationSettings().getMaxFeedCapacity();
|
||||||
if (maxFeedCapacity > 0) {
|
if (maxFeedCapacity > 0) {
|
||||||
entries = entries.stream().limit(maxFeedCapacity).collect(Collectors.toList());
|
entries = entries.stream().limit(maxFeedCapacity).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
String urlAfterRedirect = fetchedFeed.getUrlAfterRedirect();
|
String urlAfterRedirect = feedFetcherResult.getUrlAfterRedirect();
|
||||||
if (StringUtils.equals(url, urlAfterRedirect)) {
|
if (StringUtils.equals(url, urlAfterRedirect)) {
|
||||||
urlAfterRedirect = null;
|
urlAfterRedirect = null;
|
||||||
}
|
}
|
||||||
feed.setUrlAfterRedirect(urlAfterRedirect);
|
feed.setUrlAfterRedirect(urlAfterRedirect);
|
||||||
feed.setLink(fetchedFeed.getFeed().getLink());
|
feed.setLink(feedFetcherResult.getFeed().getLink());
|
||||||
feed.setLastModifiedHeader(fetchedFeed.getFeed().getLastModifiedHeader());
|
feed.setLastModifiedHeader(feedFetcherResult.getFeed().getLastModifiedHeader());
|
||||||
feed.setEtagHeader(fetchedFeed.getFeed().getEtagHeader());
|
feed.setEtagHeader(feedFetcherResult.getFeed().getEtagHeader());
|
||||||
feed.setLastContentHash(fetchedFeed.getFeed().getLastContentHash());
|
feed.setLastContentHash(feedFetcherResult.getFeed().getLastContentHash());
|
||||||
feed.setLastPublishedDate(fetchedFeed.getFeed().getLastPublishedDate());
|
feed.setLastPublishedDate(feedFetcherResult.getFeed().getLastPublishedDate());
|
||||||
feed.setAverageEntryInterval(fetchedFeed.getFeed().getAverageEntryInterval());
|
feed.setAverageEntryInterval(feedFetcherResult.getFeed().getAverageEntryInterval());
|
||||||
feed.setLastEntryDate(fetchedFeed.getFeed().getLastEntryDate());
|
feed.setLastEntryDate(feedFetcherResult.getFeed().getLastEntryDate());
|
||||||
|
|
||||||
feed.setErrorCount(0);
|
feed.setErrorCount(0);
|
||||||
feed.setMessage(null);
|
feed.setMessage(null);
|
||||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(fetchedFeed));
|
feed.setDisabledUntil(refreshIntervalCalculator.onFetchSuccess(feedFetcherResult.getFeed()));
|
||||||
|
|
||||||
handlePubSub(feed, fetchedFeed.getFeed());
|
handlePubSub(feed, feedFetcherResult.getFeed());
|
||||||
context.setEntries(entries);
|
|
||||||
feedRefreshUpdater.updateFeed(context);
|
|
||||||
|
|
||||||
|
return new FeedRefreshWorkerResult(feed, entries);
|
||||||
} catch (NotModifiedException e) {
|
} catch (NotModifiedException e) {
|
||||||
log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
|
log.debug("Feed not modified : {} - {}", feed.getUrl(), e.getMessage());
|
||||||
|
|
||||||
@@ -110,7 +92,7 @@ public class FeedRefreshWorker implements Managed {
|
|||||||
feed.setEtagHeader(e.getNewEtagHeader());
|
feed.setEtagHeader(e.getNewEtagHeader());
|
||||||
}
|
}
|
||||||
|
|
||||||
queues.giveBack(feed);
|
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String message = "Unable to refresh feed " + feed.getUrl() + " : " + e.getMessage();
|
String message = "Unable to refresh feed " + feed.getUrl() + " : " + e.getMessage();
|
||||||
log.debug(e.getClass().getName() + " " + message, e);
|
log.debug(e.getClass().getName() + " " + message, e);
|
||||||
@@ -119,7 +101,9 @@ public class FeedRefreshWorker implements Managed {
|
|||||||
feed.setMessage(message);
|
feed.setMessage(message);
|
||||||
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed));
|
feed.setDisabledUntil(refreshIntervalCalculator.onFetchError(feed));
|
||||||
|
|
||||||
queues.giveBack(feed);
|
return new FeedRefreshWorkerResult(feed, Collections.emptyList());
|
||||||
|
} finally {
|
||||||
|
feedFetched.mark();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,22 +129,10 @@ public class FeedRefreshWorker implements Managed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FeedTask implements Task {
|
@Value
|
||||||
|
public static class FeedRefreshWorkerResult {
|
||||||
private final FeedRefreshContext context;
|
Feed feed;
|
||||||
|
List<FeedEntry> entries;
|
||||||
public FeedTask(FeedRefreshContext context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
update(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUrgent() {
|
|
||||||
return context.isUrgent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import org.jsoup.nodes.Entities.EscapeMode;
|
|||||||
import org.jsoup.safety.Cleaner;
|
import org.jsoup.safety.Cleaner;
|
||||||
import org.jsoup.safety.Safelist;
|
import org.jsoup.safety.Safelist;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
|
import org.netpreserve.urlcanon.Canonicalizer;
|
||||||
|
import org.netpreserve.urlcanon.ParsedUrl;
|
||||||
import org.w3c.css.sac.InputSource;
|
import org.w3c.css.sac.InputSource;
|
||||||
import org.w3c.dom.css.CSSStyleDeclaration;
|
import org.w3c.dom.css.CSSStyleDeclaration;
|
||||||
|
|
||||||
@@ -41,7 +43,6 @@ import com.ibm.icu.text.CharsetDetector;
|
|||||||
import com.ibm.icu.text.CharsetMatch;
|
import com.ibm.icu.text.CharsetMatch;
|
||||||
import com.steadystate.css.parser.CSSOMParser;
|
import com.steadystate.css.parser.CSSOMParser;
|
||||||
|
|
||||||
import edu.uci.ics.crawler4j.url.URLCanonicalizer;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,7 +180,10 @@ public class FeedUtils {
|
|||||||
if (url == null) {
|
if (url == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String normalized = URLCanonicalizer.getCanonicalURL(url);
|
|
||||||
|
ParsedUrl parsedUrl = ParsedUrl.parseUrl(url);
|
||||||
|
Canonicalizer.AGGRESSIVE.canonicalize(parsedUrl);
|
||||||
|
String normalized = parsedUrl.toString();
|
||||||
if (normalized == null) {
|
if (normalized == null) {
|
||||||
normalized = url;
|
normalized = url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.commafeed.backend.feed;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.commafeed.backend.model.Feed;
|
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public class FetchedFeed {
|
|
||||||
|
|
||||||
private Feed feed = new Feed();
|
|
||||||
private List<FeedEntry> entries = new ArrayList<>();
|
|
||||||
|
|
||||||
private String title;
|
|
||||||
private String urlAfterRedirect;
|
|
||||||
private long fetchDuration;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.commafeed.backend.model;
|
package com.commafeed.backend.model;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
import javax.persistence.OneToMany;
|
|
||||||
import javax.persistence.Table;
|
import javax.persistence.Table;
|
||||||
import javax.persistence.Temporal;
|
import javax.persistence.Temporal;
|
||||||
import javax.persistence.TemporalType;
|
import javax.persistence.TemporalType;
|
||||||
@@ -123,7 +121,4 @@ public class Feed extends AbstractModel {
|
|||||||
@Temporal(TemporalType.TIMESTAMP)
|
@Temporal(TemporalType.TIMESTAMP)
|
||||||
private Date pushLastPing;
|
private Date pushLastPing;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "feed")
|
|
||||||
private Set<FeedSubscription> subscriptions;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class UserSettings extends AbstractModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum ViewMode {
|
public enum ViewMode {
|
||||||
title, cozy, expanded
|
title, cozy, detailed, expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
@OneToOne(fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
public class StartupService implements Managed {
|
public class DatabaseStartupService implements Managed {
|
||||||
|
|
||||||
private final SessionFactory sessionFactory;
|
private final SessionFactory sessionFactory;
|
||||||
private final UserDAO userDAO;
|
private final UserDAO userDAO;
|
||||||
@@ -7,18 +7,24 @@ import java.util.List;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
|
||||||
import com.commafeed.backend.cache.CacheService;
|
import com.commafeed.backend.cache.CacheService;
|
||||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
import com.commafeed.backend.dao.FeedEntryDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||||
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
|
import com.commafeed.backend.model.FeedEntryContent;
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
import com.commafeed.backend.model.FeedEntryStatus;
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
import com.commafeed.backend.model.FeedSubscription;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
||||||
@Singleton
|
@Singleton
|
||||||
public class FeedEntryService {
|
public class FeedEntryService {
|
||||||
@@ -26,8 +32,45 @@ public class FeedEntryService {
|
|||||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
private final FeedEntryDAO feedEntryDAO;
|
private final FeedEntryDAO feedEntryDAO;
|
||||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||||
|
private final FeedEntryContentService feedEntryContentService;
|
||||||
|
private final FeedEntryFilteringService feedEntryFilteringService;
|
||||||
private final CacheService cache;
|
private final CacheService cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this is NOT thread-safe
|
||||||
|
*/
|
||||||
|
public boolean addEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
|
||||||
|
|
||||||
|
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
|
||||||
|
if (existing != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
|
||||||
|
entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
|
||||||
|
entry.setContent(content);
|
||||||
|
entry.setInserted(new Date());
|
||||||
|
entry.setFeed(feed);
|
||||||
|
feedEntryDAO.saveOrUpdate(entry);
|
||||||
|
|
||||||
|
// if filter does not match the entry, mark it as read
|
||||||
|
for (FeedSubscription sub : subscriptions) {
|
||||||
|
boolean matches = true;
|
||||||
|
try {
|
||||||
|
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry);
|
||||||
|
} catch (FeedEntryFilteringService.FeedEntryFilterException e) {
|
||||||
|
log.error("could not evaluate filter {}", sub.getFilter(), e);
|
||||||
|
}
|
||||||
|
if (!matches) {
|
||||||
|
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
|
||||||
|
status.setRead(true);
|
||||||
|
feedEntryStatusDAO.saveOrUpdate(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void markEntry(User user, Long entryId, boolean read) {
|
public void markEntry(User user, Long entryId, boolean read) {
|
||||||
|
|
||||||
FeedEntry entry = feedEntryDAO.findById(entryId);
|
FeedEntry entry = feedEntryDAO.findById(entryId);
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ public class FeedService {
|
|||||||
return feed;
|
return feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void save(Feed feed) {
|
||||||
|
String normalized = FeedUtils.normalizeURL(feed.getUrl());
|
||||||
|
feed.setNormalizedUrl(normalized);
|
||||||
|
feed.setNormalizedUrlHash(DigestUtils.sha1Hex(normalized));
|
||||||
|
feed.setLastUpdated(new Date());
|
||||||
|
feedDAO.saveOrUpdate(feed);
|
||||||
|
}
|
||||||
|
|
||||||
public Favicon fetchFavicon(Feed feed) {
|
public Favicon fetchFavicon(Feed feed) {
|
||||||
|
|
||||||
Favicon icon = null;
|
Favicon icon = null;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import com.commafeed.backend.cache.CacheService;
|
|||||||
import com.commafeed.backend.dao.FeedDAO;
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
||||||
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
||||||
import com.commafeed.backend.feed.FeedQueues;
|
import com.commafeed.backend.feed.FeedRefreshEngine;
|
||||||
import com.commafeed.backend.feed.FeedUtils;
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
@@ -35,7 +35,7 @@ public class FeedSubscriptionService {
|
|||||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||||
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
private final FeedSubscriptionDAO feedSubscriptionDAO;
|
||||||
private final FeedService feedService;
|
private final FeedService feedService;
|
||||||
private final FeedQueues queues;
|
private final FeedRefreshEngine feedRefreshEngine;
|
||||||
private final CacheService cache;
|
private final CacheService cache;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ public class FeedSubscriptionService {
|
|||||||
sub.setTitle(FeedUtils.truncate(title, 128));
|
sub.setTitle(FeedUtils.truncate(title, 128));
|
||||||
feedSubscriptionDAO.saveOrUpdate(sub);
|
feedSubscriptionDAO.saveOrUpdate(sub);
|
||||||
|
|
||||||
queues.add(feed, true);
|
feedRefreshEngine.refreshImmediately(feed);
|
||||||
cache.invalidateUserRootCategory(user);
|
cache.invalidateUserRootCategory(user);
|
||||||
return sub.getId();
|
return sub.getId();
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ public class FeedSubscriptionService {
|
|||||||
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
List<FeedSubscription> subs = feedSubscriptionDAO.findAll(user);
|
||||||
for (FeedSubscription sub : subs) {
|
for (FeedSubscription sub : subs) {
|
||||||
Feed feed = sub.getFeed();
|
Feed feed = sub.getFeed();
|
||||||
queues.add(feed, true);
|
feedRefreshEngine.refreshImmediately(feed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.commafeed.backend.service;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
|
||||||
|
|
||||||
import com.commafeed.backend.dao.FeedEntryDAO;
|
|
||||||
import com.commafeed.backend.dao.FeedEntryStatusDAO;
|
|
||||||
import com.commafeed.backend.model.Feed;
|
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
|
||||||
import com.commafeed.backend.model.FeedEntryStatus;
|
|
||||||
import com.commafeed.backend.model.FeedSubscription;
|
|
||||||
import com.commafeed.backend.service.FeedEntryFilteringService.FeedEntryFilterException;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor(onConstructor = @__({ @Inject }))
|
|
||||||
@Singleton
|
|
||||||
public class FeedUpdateService {
|
|
||||||
|
|
||||||
private final FeedEntryDAO feedEntryDAO;
|
|
||||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
|
||||||
private final FeedEntryContentService feedEntryContentService;
|
|
||||||
private final FeedEntryFilteringService feedEntryFilteringService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this is NOT thread-safe
|
|
||||||
*/
|
|
||||||
public boolean addEntry(Feed feed, FeedEntry entry, List<FeedSubscription> subscriptions) {
|
|
||||||
|
|
||||||
Long existing = feedEntryDAO.findExisting(entry.getGuid(), feed);
|
|
||||||
if (existing != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeedEntryContent content = feedEntryContentService.findOrCreate(entry.getContent(), feed.getLink());
|
|
||||||
entry.setGuidHash(DigestUtils.sha1Hex(entry.getGuid()));
|
|
||||||
entry.setContent(content);
|
|
||||||
entry.setInserted(new Date());
|
|
||||||
entry.setFeed(feed);
|
|
||||||
feedEntryDAO.saveOrUpdate(entry);
|
|
||||||
|
|
||||||
// if filter does not match the entry, mark it as read
|
|
||||||
for (FeedSubscription sub : subscriptions) {
|
|
||||||
boolean matches = true;
|
|
||||||
try {
|
|
||||||
matches = feedEntryFilteringService.filterMatchesEntry(sub.getFilter(), entry);
|
|
||||||
} catch (FeedEntryFilterException e) {
|
|
||||||
log.error("could not evaluate filter {}", sub.getFilter(), e);
|
|
||||||
}
|
|
||||||
if (!matches) {
|
|
||||||
FeedEntryStatus status = new FeedEntryStatus(sub.getUser(), sub, entry);
|
|
||||||
status.setRead(true);
|
|
||||||
feedEntryStatusDAO.saveOrUpdate(status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,10 +17,11 @@ import org.apache.http.client.methods.HttpPost;
|
|||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.HttpGetter;
|
import com.commafeed.backend.HttpGetter;
|
||||||
import com.commafeed.backend.feed.FeedQueues;
|
import com.commafeed.backend.dao.UnitOfWork;
|
||||||
import com.commafeed.backend.feed.FeedUtils;
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
|
import com.commafeed.frontend.resource.PubSubHubbubCallbackREST;
|
||||||
@@ -38,7 +39,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class PubSubService {
|
public class PubSubService {
|
||||||
|
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
private final FeedQueues queues;
|
private final FeedService feedService;
|
||||||
|
private final SessionFactory sessionFactory;
|
||||||
|
|
||||||
public void subscribe(Feed feed) {
|
public void subscribe(Feed feed) {
|
||||||
String hub = feed.getPushHub();
|
String hub = feed.getPushHub();
|
||||||
@@ -73,7 +75,7 @@ public class PubSubService {
|
|||||||
if (code == 400 && StringUtils.contains(message, pushpressError)) {
|
if (code == 400 && StringUtils.contains(message, pushpressError)) {
|
||||||
String[] tokens = message.split(" ");
|
String[] tokens = message.split(" ");
|
||||||
feed.setPushTopic(tokens[tokens.length - 1]);
|
feed.setPushTopic(tokens[tokens.length - 1]);
|
||||||
queues.giveBack(feed);
|
UnitOfWork.run(sessionFactory, () -> feedService.save(feed));
|
||||||
log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
|
log.debug("handled pushpress subfeed {} : {}", topic, feed.getPushTopic());
|
||||||
} else {
|
} else {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public class Category implements Serializable {
|
|||||||
@ApiModelProperty(value = "parent category id")
|
@ApiModelProperty(value = "parent category id")
|
||||||
private String parentId;
|
private String parentId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "parent category name")
|
||||||
|
private String parentName;
|
||||||
|
|
||||||
@ApiModelProperty(value = "category id", required = true)
|
@ApiModelProperty(value = "category id", required = true)
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public class CategoryREST {
|
|||||||
@UnitOfWork
|
@UnitOfWork
|
||||||
@ApiOperation(value = "Get category entries", notes = "Get a list of category entries", response = Entries.class)
|
@ApiOperation(value = "Get category entries", notes = "Get a list of category entries", response = Entries.class)
|
||||||
@Timed
|
@Timed
|
||||||
public Response getCategoryEntries(@ApiParam(hidden = true) @SecurityCheck User user,
|
public Response getCategoryEntries(@ApiParam(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
|
||||||
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id,
|
||||||
@ApiParam(
|
@ApiParam(
|
||||||
value = "all entries or only unread ones",
|
value = "all entries or only unread ones",
|
||||||
@@ -454,17 +454,13 @@ public class CategoryREST {
|
|||||||
child.setPosition(c.getPosition());
|
child.setPosition(c.getPosition());
|
||||||
if (c.getParent() != null && c.getParent().getId() != null) {
|
if (c.getParent() != null && c.getParent().getId() != null) {
|
||||||
child.setParentId(String.valueOf(c.getParent().getId()));
|
child.setParentId(String.valueOf(c.getParent().getId()));
|
||||||
|
child.setParentName(c.getParent().getName());
|
||||||
}
|
}
|
||||||
child.setExpanded(!c.isCollapsed());
|
child.setExpanded(!c.isCollapsed());
|
||||||
category.getChildren().add(child);
|
category.getChildren().add(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(category.getChildren(), new Comparator<Category>() {
|
Collections.sort(category.getChildren(), (o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
|
||||||
@Override
|
|
||||||
public int compare(Category o1, Category o2) {
|
|
||||||
return ObjectUtils.compare(o1.getPosition(), o2.getPosition());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (FeedSubscription subscription : subscriptions) {
|
for (FeedSubscription subscription : subscriptions) {
|
||||||
if (id == null && subscription.getCategory() == null
|
if (id == null && subscription.getCategory() == null
|
||||||
@@ -474,12 +470,8 @@ public class CategoryREST {
|
|||||||
category.getFeeds().add(sub);
|
category.getFeeds().add(sub);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(category.getFeeds(), new Comparator<Subscription>() {
|
Collections.sort(category.getFeeds(), (o1, o2) -> ObjectUtils.compare(o1.getPosition(), o2.getPosition()));
|
||||||
@Override
|
|
||||||
public int compare(Subscription o1, Subscription o2) {
|
|
||||||
return ObjectUtils.compare(o1.getPosition(), o2.getPosition());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return category;
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ import com.commafeed.backend.dao.FeedSubscriptionDAO;
|
|||||||
import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon;
|
import com.commafeed.backend.favicon.AbstractFaviconFetcher.Favicon;
|
||||||
import com.commafeed.backend.feed.FeedEntryKeyword;
|
import com.commafeed.backend.feed.FeedEntryKeyword;
|
||||||
import com.commafeed.backend.feed.FeedFetcher;
|
import com.commafeed.backend.feed.FeedFetcher;
|
||||||
import com.commafeed.backend.feed.FeedQueues;
|
import com.commafeed.backend.feed.FeedFetcher.FeedFetcherResult;
|
||||||
|
import com.commafeed.backend.feed.FeedRefreshEngine;
|
||||||
import com.commafeed.backend.feed.FeedUtils;
|
import com.commafeed.backend.feed.FeedUtils;
|
||||||
import com.commafeed.backend.feed.FetchedFeed;
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
@@ -109,7 +109,7 @@ public class FeedREST {
|
|||||||
private final FeedEntryService feedEntryService;
|
private final FeedEntryService feedEntryService;
|
||||||
private final FeedSubscriptionService feedSubscriptionService;
|
private final FeedSubscriptionService feedSubscriptionService;
|
||||||
private final FeedEntryFilteringService feedEntryFilteringService;
|
private final FeedEntryFilteringService feedEntryFilteringService;
|
||||||
private final FeedQueues queues;
|
private final FeedRefreshEngine feedRefreshEngine;
|
||||||
private final OPMLImporter opmlImporter;
|
private final OPMLImporter opmlImporter;
|
||||||
private final OPMLExporter opmlExporter;
|
private final OPMLExporter opmlExporter;
|
||||||
private final CacheService cache;
|
private final CacheService cache;
|
||||||
@@ -132,7 +132,7 @@ public class FeedREST {
|
|||||||
@UnitOfWork
|
@UnitOfWork
|
||||||
@ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", response = Entries.class)
|
@ApiOperation(value = "Get feed entries", notes = "Get a list of feed entries", response = Entries.class)
|
||||||
@Timed
|
@Timed
|
||||||
public Response getFeedEntries(@ApiParam(hidden = true) @SecurityCheck User user,
|
public Response getFeedEntries(@ApiParam(hidden = true) @SecurityCheck(apiKeyAllowed = true) User user,
|
||||||
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id,
|
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id,
|
||||||
@ApiParam(
|
@ApiParam(
|
||||||
value = "all entries or only unread ones",
|
value = "all entries or only unread ones",
|
||||||
@@ -244,10 +244,10 @@ public class FeedREST {
|
|||||||
url = StringUtils.trimToEmpty(url);
|
url = StringUtils.trimToEmpty(url);
|
||||||
url = prependHttp(url);
|
url = prependHttp(url);
|
||||||
try {
|
try {
|
||||||
FetchedFeed feed = feedFetcher.fetch(url, true, null, null, null, null);
|
FeedFetcherResult feedFetcherResult = feedFetcher.fetch(url, true, null, null, null, null);
|
||||||
info = new FeedInfo();
|
info = new FeedInfo();
|
||||||
info.setUrl(feed.getUrlAfterRedirect());
|
info.setUrl(feedFetcherResult.getUrlAfterRedirect());
|
||||||
info.setTitle(feed.getTitle());
|
info.setTitle(feedFetcherResult.getTitle());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug(e.getMessage(), e);
|
log.debug(e.getMessage(), e);
|
||||||
@@ -303,7 +303,7 @@ public class FeedREST {
|
|||||||
FeedSubscription sub = feedSubscriptionDAO.findById(user, req.getId());
|
FeedSubscription sub = feedSubscriptionDAO.findById(user, req.getId());
|
||||||
if (sub != null) {
|
if (sub != null) {
|
||||||
Feed feed = sub.getFeed();
|
Feed feed = sub.getFeed();
|
||||||
queues.add(feed, true);
|
feedRefreshEngine.refreshImmediately(feed);
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
return Response.ok(Status.NOT_FOUND).build();
|
return Response.ok(Status.NOT_FOUND).build();
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import com.codahale.metrics.annotation.Timed;
|
|||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.dao.FeedDAO;
|
import com.commafeed.backend.dao.FeedDAO;
|
||||||
import com.commafeed.backend.feed.FeedParser;
|
import com.commafeed.backend.feed.FeedParser;
|
||||||
import com.commafeed.backend.feed.FeedQueues;
|
import com.commafeed.backend.feed.FeedParser.FeedParserResult;
|
||||||
import com.commafeed.backend.feed.FetchedFeed;
|
import com.commafeed.backend.feed.FeedRefreshEngine;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ public class PubSubHubbubCallbackREST {
|
|||||||
|
|
||||||
private final FeedDAO feedDAO;
|
private final FeedDAO feedDAO;
|
||||||
private final FeedParser parser;
|
private final FeedParser parser;
|
||||||
private final FeedQueues queues;
|
private final FeedRefreshEngine feedRefreshEngine;
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
private final MetricRegistry metricRegistry;
|
private final MetricRegistry metricRegistry;
|
||||||
|
|
||||||
@@ -100,8 +100,8 @@ public class PubSubHubbubCallbackREST {
|
|||||||
return Response.status(Status.BAD_REQUEST).entity("empty body received").build();
|
return Response.status(Status.BAD_REQUEST).entity("empty body received").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
FetchedFeed fetchedFeed = parser.parse(null, bytes);
|
FeedParserResult feedParserResult = parser.parse(null, bytes);
|
||||||
String topic = fetchedFeed.getFeed().getPushTopic();
|
String topic = feedParserResult.getFeed().getPushTopic();
|
||||||
if (StringUtils.isBlank(topic)) {
|
if (StringUtils.isBlank(topic)) {
|
||||||
return Response.status(Status.BAD_REQUEST).entity("empty topic received").build();
|
return Response.status(Status.BAD_REQUEST).entity("empty topic received").build();
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ public class PubSubHubbubCallbackREST {
|
|||||||
|
|
||||||
for (Feed feed : feeds) {
|
for (Feed feed : feeds) {
|
||||||
log.debug("pushing content to queue for {}", feed.getUrl());
|
log.debug("pushing content to queue for {}", feed.getUrl());
|
||||||
queues.add(feed, false);
|
feedRefreshEngine.refreshImmediately(feed);
|
||||||
}
|
}
|
||||||
metricRegistry.meter(MetricRegistry.name(getClass(), "pushReceived")).mark();
|
metricRegistry.meter(MetricRegistry.name(getClass(), "pushReceived")).mark();
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ public class WebSocketSessions {
|
|||||||
.flatMap(e -> e.getValue().stream())
|
.flatMap(e -> e.getValue().stream())
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
log.debug("sending '{}' to {} users via websocket", text, userSessions.size());
|
if (!userSessions.isEmpty()) {
|
||||||
for (Session userSession : userSessions) {
|
log.debug("sending '{}' to {} users via websocket", text, userSessions.size());
|
||||||
if (userSession.isOpen()) {
|
for (Session userSession : userSessions) {
|
||||||
userSession.getAsyncRemote().sendText(text);
|
if (userSession.isOpen()) {
|
||||||
|
userSession.getAsyncRemote().sendText(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.commafeed.backend.service;
|
package com.commafeed.backend.service;
|
||||||
|
|
||||||
import org.apache.http.HttpHeaders;
|
import org.apache.http.HttpHeaders;
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -15,7 +16,6 @@ import org.mockserver.model.HttpResponse;
|
|||||||
import org.mockserver.model.MediaType;
|
import org.mockserver.model.MediaType;
|
||||||
|
|
||||||
import com.commafeed.CommaFeedConfiguration;
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.feed.FeedQueues;
|
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
|
|
||||||
@ExtendWith(MockServerExtension.class)
|
@ExtendWith(MockServerExtension.class)
|
||||||
@@ -25,7 +25,10 @@ class PubSubServiceTest {
|
|||||||
private CommaFeedConfiguration config;
|
private CommaFeedConfiguration config;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private FeedQueues queues;
|
private FeedService feedService;
|
||||||
|
|
||||||
|
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||||
|
private SessionFactory sessionFactory;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private Feed feed;
|
private Feed feed;
|
||||||
@@ -40,7 +43,7 @@ class PubSubServiceTest {
|
|||||||
this.client = client;
|
this.client = client;
|
||||||
this.client.reset();
|
this.client.reset();
|
||||||
|
|
||||||
this.underTest = new PubSubService(config, queues);
|
this.underTest = new PubSubService(config, feedService, sessionFactory);
|
||||||
|
|
||||||
Integer port = client.getPort();
|
Integer port = client.getPort();
|
||||||
String hubUrl = String.format("http://localhost:%s/hub", port);
|
String hubUrl = String.format("http://localhost:%s/hub", port);
|
||||||
@@ -69,7 +72,7 @@ class PubSubServiceTest {
|
|||||||
.withMethod("POST")
|
.withMethod("POST")
|
||||||
.withPath("/hub"));
|
.withPath("/hub"));
|
||||||
Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString());
|
Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString());
|
||||||
Mockito.verifyNoInteractions(queues);
|
Mockito.verifyNoInteractions(feedService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -83,7 +86,7 @@ class PubSubServiceTest {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Mockito.verify(feed).setPushTopic(Mockito.anyString());
|
Mockito.verify(feed).setPushTopic(Mockito.anyString());
|
||||||
Mockito.verify(queues).giveBack(feed);
|
Mockito.verify(feedService).save(feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -96,7 +99,7 @@ class PubSubServiceTest {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString());
|
Mockito.verify(feed, Mockito.never()).setPushTopic(Mockito.anyString());
|
||||||
Mockito.verifyNoInteractions(queues);
|
Mockito.verifyNoInteractions(feedService);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,12 +15,12 @@ import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
|||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
class AuthentificationIT extends PlaywrightTestBase {
|
class AuthentificationIT extends PlaywrightTestBase {
|
||||||
|
|
||||||
private static final DropwizardAppExtension<CommaFeedConfiguration> EXT = new DropwizardAppExtension<CommaFeedConfiguration>(
|
private static final DropwizardAppExtension<CommaFeedConfiguration> EXT = new DropwizardAppExtension<>(CommaFeedApplication.class,
|
||||||
CommaFeedApplication.class, ResourceHelpers.resourceFilePath("config.test.yml"));
|
ResourceHelpers.resourceFilePath("config.test.yml"));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loginFail() {
|
void loginFail() {
|
||||||
page.navigate("http://localhost:" + EXT.getLocalPort());
|
page.navigate(getLoginPageUrl());
|
||||||
page.locator("[placeholder='User Name or E-mail']").fill("admin");
|
page.locator("[placeholder='User Name or E-mail']").fill("admin");
|
||||||
page.locator("[placeholder='Password']").fill("wrong_password");
|
page.locator("[placeholder='Password']").fill("wrong_password");
|
||||||
page.locator("button:has-text('Log in')").click();
|
page.locator("button:has-text('Log in')").click();
|
||||||
@@ -29,14 +29,14 @@ class AuthentificationIT extends PlaywrightTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loginSuccess() {
|
void loginSuccess() {
|
||||||
page.navigate("http://localhost:" + EXT.getLocalPort());
|
page.navigate(getLoginPageUrl());
|
||||||
PlaywrightTestUtils.login(page);
|
PlaywrightTestUtils.login(page);
|
||||||
PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all");
|
PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void registerFailPasswordTooSimple() {
|
void registerFailPasswordTooSimple() {
|
||||||
page.navigate("http://localhost:" + EXT.getLocalPort());
|
page.navigate(getLoginPageUrl());
|
||||||
page.locator("text=Sign up!").click();
|
page.locator("text=Sign up!").click();
|
||||||
page.locator("[placeholder='User Name']").fill("user");
|
page.locator("[placeholder='User Name']").fill("user");
|
||||||
page.locator("[placeholder='E-mail address']").fill("user@domain.com");
|
page.locator("[placeholder='E-mail address']").fill("user@domain.com");
|
||||||
@@ -52,7 +52,7 @@ class AuthentificationIT extends PlaywrightTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void registerSuccess() {
|
void registerSuccess() {
|
||||||
page.navigate("http://localhost:" + EXT.getLocalPort());
|
page.navigate(getLoginPageUrl());
|
||||||
page.locator("text=Sign up!").click();
|
page.locator("text=Sign up!").click();
|
||||||
page.locator("[placeholder='User Name']").fill("user");
|
page.locator("[placeholder='User Name']").fill("user");
|
||||||
page.locator("[placeholder='E-mail address']").fill("user@domain.com");
|
page.locator("[placeholder='E-mail address']").fill("user@domain.com");
|
||||||
@@ -60,4 +60,8 @@ class AuthentificationIT extends PlaywrightTestBase {
|
|||||||
page.locator("button:has-text('Sign up')").click();
|
page.locator("button:has-text('Sign up')").click();
|
||||||
PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all");
|
PlaywrightAssertions.assertThat(page).hasURL("http://localhost:" + EXT.getLocalPort() + "/#/app/category/all");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getLoginPageUrl() {
|
||||||
|
return "http://localhost:" + EXT.getLocalPort() + "/#/login";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class ReadingIT extends PlaywrightTestBase {
|
|||||||
void scenario() {
|
void scenario() {
|
||||||
// login
|
// login
|
||||||
page.navigate("http://localhost:" + EXT.getLocalPort());
|
page.navigate("http://localhost:" + EXT.getLocalPort());
|
||||||
|
page.locator("button:has-text('Log in')").click();
|
||||||
PlaywrightTestUtils.login(page);
|
PlaywrightTestUtils.login(page);
|
||||||
PlaywrightAssertions.assertThat(page.locator("text=You don't have any subscriptions yet.")).hasCount(1);
|
PlaywrightAssertions.assertThat(page.locator("text=You don't have any subscriptions yet.")).hasCount(1);
|
||||||
|
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@@ -1,10 +1,11 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>3.0.0</version>
|
<version>3.2.0</version>
|
||||||
<name>CommaFeed</name>
|
<name>CommaFeed</name>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
|||||||
32
release.sh
Normal file
32
release.sh
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to create a new release
|
||||||
|
# ------------------------------
|
||||||
|
|
||||||
|
# exit on error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# make sure we're on master
|
||||||
|
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
if [[ "$BRANCH" != "master" ]]; then
|
||||||
|
echo "You're on branch '$BRANCH', you should be on 'master'."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# make sure README.md has been updated
|
||||||
|
read -r -p "Has README.md been updated? (Y/n) " CONFIRM
|
||||||
|
case "$CONFIRM" in
|
||||||
|
n | N) exit ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -r -p "New version (x.y.z): " VERSION
|
||||||
|
|
||||||
|
mvn versions:set -DgenerateBackupPoms=false -DnewVersion="$VERSION"
|
||||||
|
git add .
|
||||||
|
git commit -am "release $VERSION"
|
||||||
|
git tag "$VERSION"
|
||||||
|
|
||||||
|
read -r -p "Push master and tag $VERSION? (y/N) " CONFIRM
|
||||||
|
case "$CONFIRM" in
|
||||||
|
y | Y) git push --atomic origin master "$VERSION" ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user